fix(zed-hosted): send the provider wire values cloud.zed.dev accepts (#10051)

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 <root@srv1710948.hstgr.cloud>
This commit is contained in:
Arnav Rastogi
2026-08-13 16:22:27 +05:30
committed by GitHub
parent 397ec88751
commit 48124fca5a
3 changed files with 67 additions and 20 deletions

View File

@@ -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];

View File

@@ -53,7 +53,9 @@ async function readAll(stream: ReadableStream<Uint8Array>): Promise<string> {
function wrapAnthropic(options?: Record<string, unknown>): Promise<string> {
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<Uint8Array>);
}

View File

@@ -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);
});
});