From 48124fca5a811b9fa54fdd54858e2dca694dd476 Mon Sep 17 00:00:00 2001 From: Arnav Rastogi Date: Thu, 13 Aug 2026 16:22:27 +0530 Subject: [PATCH] fix(zed-hosted): send the provider wire values cloud.zed.dev accepts (#10051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every zed-hosted completion failed with 500 {"error":{"message":"[500]: An internal server error occurred."}} for every model id, including deliberately invalid ones. Root cause: ZED_PROVIDER held display-cased names ("Anthropic", "OpenAi", "Google", "XAi"), and normalizeZedProvider's return value is serialized straight into the `provider` field of the POST /completions envelope. Zed matches that field exactly and fails the request before looking at the model, which is why the model id never mattered. Verified live against cloud.zed.dev with an otherwise identical request: {"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) The spellings now follow Zed's own GET /models catalog, which reports `anthropic`, `open_ai` and `google`. That also makes normalizeZedProvider identity on catalog values instead of corrupting a value Zed just supplied — previously it accepted the correct lowercase input and re-cased it into the form that 500s. `x_ai` follows the same underscore convention; this account's catalog exposes no xAI models, so that one spelling is by convention rather than observation. The constant is module-local and every branch compares against it, so internal dispatch (initProviderState / convertProviderEvent / buildProviderRequest) is unaffected. Two existing tests asserted the display-cased values and one passed "Anthropic" to wrapZedCompletionStream directly; all are updated to the wire values the executor now produces. Co-authored-by: root --- open-sse/executors/zed-hosted.ts | 18 +++-- .../zed-hosted-think-close-marker.test.ts | 4 +- tests/unit/zed-provider.test.ts | 65 ++++++++++++++----- 3 files changed, 67 insertions(+), 20 deletions(-) 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); }); });