diff --git a/open-sse/executors/zed-hosted.ts b/open-sse/executors/zed-hosted.ts index 7e75c6cecc..ef1ae4ade6 100644 --- a/open-sse/executors/zed-hosted.ts +++ b/open-sse/executors/zed-hosted.ts @@ -46,11 +46,21 @@ import { } from "../shared/zedAuth.ts"; import { resolveSuppressThinkClose, THINKING_MARKER_HEADER } from "../utils/thinkCloseMarker.ts"; +// Wire values for the `provider` field of POST /completions. These are NOT +// display names: cloud.zed.dev matches them exactly, and an unrecognized value +// fails the whole request with `500 {"message":"An internal server error +// occurred."}` before the model is ever looked at — which is why every model id, +// including invalid ones, produced an identical 500. +// +// The spellings come from Zed's own GET /models catalog, which reports +// `anthropic`, `open_ai` and `google` (note the underscore); `x_ai` follows the +// same convention. Feeding a catalog value back through normalizeZedProvider is +// therefore identity, as it must be. const ZED_PROVIDER = { - anthropic: "Anthropic", - openai: "OpenAi", - google: "Google", - xai: "XAi", + anthropic: "anthropic", + openai: "open_ai", + google: "google", + xai: "x_ai", } as const; type ZedProviderName = (typeof ZED_PROVIDER)[keyof typeof ZED_PROVIDER]; diff --git a/tests/unit/zed-hosted-think-close-marker.test.ts b/tests/unit/zed-hosted-think-close-marker.test.ts index 09bb442136..a4bbebddda 100644 --- a/tests/unit/zed-hosted-think-close-marker.test.ts +++ b/tests/unit/zed-hosted-think-close-marker.test.ts @@ -53,7 +53,9 @@ async function readAll(stream: ReadableStream): Promise { function wrapAnthropic(options?: Record): Promise { const response = new Response(buildZedAnthropicNdjson(), { status: 200 }); - const wrapped = wrapZedCompletionStream(response, "Anthropic", "claude-test", options); + // "anthropic" is the wire value normalizeZedProvider now returns (and the one + // cloud.zed.dev accepts); the capitalized spelling 500s upstream. + const wrapped = wrapZedCompletionStream(response, "anthropic", "claude-test", options); return readAll(wrapped.body as ReadableStream); } diff --git a/tests/unit/zed-provider.test.ts b/tests/unit/zed-provider.test.ts index d8105bcb43..e9f0cfdfab 100644 --- a/tests/unit/zed-provider.test.ts +++ b/tests/unit/zed-provider.test.ts @@ -260,22 +260,57 @@ describe("mapZedModel", () => { // ─── Executor: provider-family inference ──────────────────────────────────── describe("normalizeZedProvider", () => { - test("maps explicit provider strings", () => { - assert.equal(normalizeZedProvider("anthropic", "any"), "Anthropic"); - assert.equal(normalizeZedProvider("openai", "any"), "OpenAi"); - assert.equal(normalizeZedProvider("open_ai", "any"), "OpenAi"); - assert.equal(normalizeZedProvider("google", "any"), "Google"); - assert.equal(normalizeZedProvider("gemini", "any"), "Google"); - assert.equal(normalizeZedProvider("xai", "any"), "XAi"); - assert.equal(normalizeZedProvider("x-ai", "any"), "XAi"); + // The return value is not internal — it is serialized straight into the + // `provider` field of the POST /completions envelope, so it must be a wire + // value cloud.zed.dev accepts. Verified live against the API: + // + // {"provider":"anthropic",...} -> 200 + // {"provider":"Anthropic",...} -> 500 {"message":"An internal server error occurred."} + // {"provider":"open_ai",...} -> reaches the OpenAI request parser + // {"provider":"openai",...} -> 500 (same internal error) + // + // Zed's own GET /models catalog reports exactly `anthropic`, `open_ai` and + // `google`, which is the authority these values follow. + test("returns the wire values cloud.zed.dev accepts", () => { + assert.equal(normalizeZedProvider("anthropic", "any"), "anthropic"); + assert.equal(normalizeZedProvider("openai", "any"), "open_ai"); + assert.equal(normalizeZedProvider("open_ai", "any"), "open_ai"); + assert.equal(normalizeZedProvider("google", "any"), "google"); + assert.equal(normalizeZedProvider("gemini", "any"), "google"); + assert.equal(normalizeZedProvider("xai", "any"), "x_ai"); + assert.equal(normalizeZedProvider("x-ai", "any"), "x_ai"); + }); + + test("round-trips the provider values Zed's own catalog reports", () => { + // rawById entries carry `provider` straight from GET /models. Feeding those + // back must be identity — anything else corrupts a value Zed already gave us. + for (const wire of ["anthropic", "open_ai", "google"]) { + assert.equal(normalizeZedProvider(wire, "any"), wire); + } }); test("infers from the model id when provider is absent", () => { - assert.equal(normalizeZedProvider(null, "claude-sonnet-5"), "Anthropic"); - assert.equal(normalizeZedProvider(null, "gemini-3.1-pro"), "Google"); - assert.equal(normalizeZedProvider(null, "grok-4"), "XAi"); - assert.equal(normalizeZedProvider(null, "gpt-5.5"), "OpenAi"); - assert.equal(normalizeZedProvider(null, "some-unknown-model"), "OpenAi"); + assert.equal(normalizeZedProvider(null, "claude-sonnet-5"), "anthropic"); + assert.equal(normalizeZedProvider(null, "gemini-3.1-pro"), "google"); + assert.equal(normalizeZedProvider(null, "grok-4"), "x_ai"); + assert.equal(normalizeZedProvider(null, "gpt-5.5"), "open_ai"); + assert.equal(normalizeZedProvider(null, "some-unknown-model"), "open_ai"); + }); + + test("never emits a capitalized provider — the shape that 500s upstream", () => { + const cases: [unknown, string][] = [ + ["anthropic", "any"], + ["openai", "any"], + ["google", "any"], + ["xai", "any"], + [null, "claude-sonnet-5"], + [null, "gpt-5.5"], + [null, "some-unknown-model"], + ]; + for (const [raw, model] of cases) { + const out = normalizeZedProvider(raw, model); + assert.equal(out, out.toLowerCase(), `${String(raw)}/${model} produced "${out}"`); + } }); }); @@ -354,7 +389,7 @@ describe("ZedHostedExecutor.resolveModel + zedLlmFetch (mocked upstream)", () => undefined, undefined ); - assert.equal(result.provider, "Anthropic"); + assert.equal(result.provider, "anthropic"); assert.ok(calls.some((u) => u.includes("/client/llm_tokens"))); assert.ok(calls.some((u) => u.includes("/models"))); }); @@ -378,7 +413,7 @@ describe("ZedHostedExecutor.resolveModel + zedLlmFetch (mocked upstream)", () => undefined, { warn: (_tag: string, msg: string) => warnCalls.push(msg) } as ExecutorLog ); - assert.equal(result.provider, "Google"); + assert.equal(result.provider, "google"); assert.ok(warnCalls.length > 0); }); });