From 4745fca27d4f9faa05b699a936a72e0616e52206 Mon Sep 17 00:00:00 2001 From: Koosha Paridehpour <42529354+KooshaPari@users.noreply.github.com> Date: Mon, 14 Sep 2026 20:11:18 -0700 Subject: [PATCH] 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! --- open-sse/executors/zed-hosted.ts | 53 ++++++++++++++++- .../unit/zed-hosted-proxy-enums-13362.test.ts | 57 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 tests/unit/zed-hosted-proxy-enums-13362.test.ts diff --git a/open-sse/executors/zed-hosted.ts b/open-sse/executors/zed-hosted.ts index ba66706f10..03209e00ca 100644 --- a/open-sse/executors/zed-hosted.ts +++ b/open-sse/executors/zed-hosted.ts @@ -81,6 +81,49 @@ function normalizeZedProvider(value: unknown, model: unknown): ZedProviderName { return ZED_PROVIDER.openai; } +function asMutableRecord(value: unknown): Record | null { + return value && typeof value === "object" ? (value as Record) : 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 = { + 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, true, credentials); + return adaptGeminiRequestForZed( + openaiToGeminiRequest(model, body as Record, true, credentials) + ); } if (provider === ZED_PROVIDER.openai) { - return openaiToOpenAIResponsesRequest(model, body, true, credentials); + return adaptResponsesRequestForZed( + openaiToOpenAIResponsesRequest(model, body, true, credentials) + ); } return { ...(body as Record), @@ -551,6 +598,8 @@ export class ZedHostedExecutor extends BaseExecutor { export default ZedHostedExecutor; export const __test__ = { + adaptGeminiRequestForZed, + adaptResponsesRequestForZed, normalizeZedProvider, unwrapZedLine, wrapZedCompletionStream, diff --git a/tests/unit/zed-hosted-proxy-enums-13362.test.ts b/tests/unit/zed-hosted-proxy-enums-13362.test.ts new file mode 100644 index 0000000000..675afd3c6a --- /dev/null +++ b/tests/unit/zed-hosted-proxy-enums-13362.test.ts @@ -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"] + ); +});