fix(zed-hosted): map developer role and Gemini enums for Zed proxy (#13543)

Zed-hosted requests are re-mapped to the narrower enums the Zed proxy validates. Responses input items with `role: "developer"` go back to `system` (#13362). For Gemini, `safetySettings[].threshold: "OFF"` becomes `BLOCK_NONE` and `functionCallingConfig.mode` `VALIDATED`/`AUTO`/`ANY`/`NONE` become Zed's lowercase `auto`/`any`/`none` (#13363). Chosen over #13541, which carried the identical commit.

Maintainer changes: the inline mapping used five `as any` casts, which `no-explicit-any` rejects in `open-sse/`. It is now two typed helpers (`adaptGeminiRequestForZed`, `adaptResponsesRequestForZed`, exposed through `__test__`) plus a small mode table; behavior is unchanged. Added `tests/unit/zed-hosted-proxy-enums-13362.test.ts` (4 cases). Zed suites 47/47; `check:open-sse-typecheck` clean.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
This commit is contained in:
Koosha Paridehpour
2026-09-14 20:11:18 -07:00
committed by GitHub
parent 2d1a281d12
commit 4745fca27d
2 changed files with 108 additions and 2 deletions

View File

@@ -81,6 +81,49 @@ function normalizeZedProvider(value: unknown, model: unknown): ZedProviderName {
return ZED_PROVIDER.openai;
}
function asMutableRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
// Zed's Google proxy enums are narrower than Google's own (#13363): the safety
// threshold only accepts BLOCK_NONE (not "OFF"), and FunctionCallingMode is
// lowercase auto/any/none (no VALIDATED).
const ZED_FUNCTION_CALLING_MODES: Record<string, string> = {
VALIDATED: "auto",
AUTO: "auto",
ANY: "any",
NONE: "none",
};
function adaptGeminiRequestForZed(request: unknown): unknown {
const record = asMutableRecord(request);
if (!record) return request;
if (Array.isArray(record.safetySettings)) {
for (const entry of record.safetySettings) {
const setting = asMutableRecord(entry);
if (setting?.threshold === "OFF") setting.threshold = "BLOCK_NONE";
}
}
const callingConfig = asMutableRecord(asMutableRecord(record.toolConfig)?.functionCallingConfig);
const mappedMode = callingConfig
? ZED_FUNCTION_CALLING_MODES[String(callingConfig.mode || "").toUpperCase()]
: undefined;
if (callingConfig && mappedMode) callingConfig.mode = mappedMode;
return request;
}
// Zed's OpenAI proxy Role enum only has user/assistant/system/tool — no
// "developer" (#13362) — so developer-role input items go back to system.
function adaptResponsesRequestForZed(request: unknown): unknown {
const input = asMutableRecord(request)?.input;
if (!Array.isArray(input)) return request;
for (const entry of input) {
const item = asMutableRecord(entry);
if (item?.role === "developer") item.role = "system";
}
return request;
}
function buildProviderRequest(
provider: ZedProviderName,
model: string,
@@ -92,10 +135,14 @@ function buildProviderRequest(
return openaiToClaudeRequest(model, body, true);
}
if (provider === ZED_PROVIDER.google) {
return openaiToGeminiRequest(model, body as Record<string, unknown>, true, credentials);
return adaptGeminiRequestForZed(
openaiToGeminiRequest(model, body as Record<string, unknown>, true, credentials)
);
}
if (provider === ZED_PROVIDER.openai) {
return openaiToOpenAIResponsesRequest(model, body, true, credentials);
return adaptResponsesRequestForZed(
openaiToOpenAIResponsesRequest(model, body, true, credentials)
);
}
return {
...(body as Record<string, unknown>),
@@ -551,6 +598,8 @@ export class ZedHostedExecutor extends BaseExecutor {
export default ZedHostedExecutor;
export const __test__ = {
adaptGeminiRequestForZed,
adaptResponsesRequestForZed,
normalizeZedProvider,
unwrapZedLine,
wrapZedCompletionStream,

View File

@@ -0,0 +1,57 @@
import test from "node:test";
import assert from "node:assert/strict";
const { __test__ } = await import("../../open-sse/executors/zed-hosted.ts");
const { adaptGeminiRequestForZed, adaptResponsesRequestForZed } = __test__;
// #13362 / #13363: Zed's hosted proxy validates narrower enums than the upstream
// APIs, so requests translated for Google/OpenAI must be re-mapped before dispatch.
test("#13363 Gemini safety threshold OFF becomes BLOCK_NONE for Zed", () => {
const request = {
safetySettings: [
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "OFF" },
{ category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_ONLY_HIGH" },
],
};
adaptGeminiRequestForZed(request);
assert.deepEqual(
request.safetySettings.map((s) => s.threshold),
["BLOCK_NONE", "BLOCK_ONLY_HIGH"]
);
});
test("#13363 Gemini function-calling modes map to Zed's lowercase enum", () => {
for (const [mode, expected] of [
["VALIDATED", "auto"],
["AUTO", "auto"],
["ANY", "any"],
["NONE", "none"],
]) {
const request = { toolConfig: { functionCallingConfig: { mode } } };
adaptGeminiRequestForZed(request);
assert.equal(request.toolConfig.functionCallingConfig.mode, expected, mode);
}
});
test("#13363 Gemini request without safety or tool config passes through untouched", () => {
const request = { contents: [{ role: "user", parts: [{ text: "hi" }] }] };
const before = JSON.stringify(request);
assert.equal(adaptGeminiRequestForZed(request), request);
assert.equal(JSON.stringify(request), before);
assert.equal(adaptGeminiRequestForZed(null), null);
});
test("#13362 Responses developer-role input items become system for Zed", () => {
const request = {
input: [
{ role: "developer", content: "be terse" },
{ role: "user", content: "hello" },
],
};
adaptResponsesRequestForZed(request);
assert.deepEqual(
request.input.map((item) => item.role),
["system", "user"]
);
});