/** * Issue #2911 — Built-in GitHub Copilot Claude Opus / Gemini models fail. * * The `github` provider has `format: "openai"` (baseUrl .../chat/completions) * and a separate `responsesBaseUrl` (.../responses). A model only routes to the * Responses API when it sets `targetFormat: "openai-responses"`. * * GitHub Copilot's Responses API does NOT serve the Claude/Gemini models, so * `claude-opus-4.7`, `gemini-3.1-pro-preview` and other non-OpenAI models * failed with a 400. The working `claude-sonnet-4.6` * carries no `targetFormat` and goes through chat/completions. * * Fix: drop `targetFormat: "openai-responses"` from the Claude/Gemini entries so * they use the provider default (chat/completions). The native OpenAI `gpt-*` * models legitimately keep the Responses API and must NOT be touched. */ import test from "node:test"; import assert from "node:assert/strict"; const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts"); const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); type ModelEntry = { id: string; targetFormat?: string; [k: string]: unknown }; function githubModel(id: string): ModelEntry | undefined { const provider = (REGISTRY as Record)["github"]; return provider?.models?.find((m) => m.id === id); } // Claude/Gemini models that must NOT route through the Responses API. const MUST_NOT_BE_RESPONSES = [ "claude-fable-5", "claude-opus-4.7", "claude-opus-4.8", "claude-opus-4.8-fast", "claude-opus-4.5", "claude-sonnet-4.6", "claude-sonnet-5", "claude-sonnet-4.5", "claude-haiku-4.5", "gemini-3.1-pro-preview", "gemini-3.5-flash", ]; for (const id of MUST_NOT_BE_RESPONSES) { test(`#2911 github/${id} must not use openai-responses targetFormat`, () => { const model = githubModel(id); assert.ok(model, `${id} must be registered under github`); assert.notEqual( model.targetFormat, "openai-responses", `github/${id} must route via chat/completions (Copilot Responses API rejects it)` ); }); } test("#2911 github/claude-sonnet-4.6 baseline stays on chat/completions (no targetFormat)", () => { const model = githubModel("claude-sonnet-4.6"); assert.ok(model, "claude-sonnet-4.6 must be registered"); assert.notEqual(model.targetFormat, "openai-responses"); }); // Regression guard: models with `/responses` in the curated Copilot catalog keep the Responses API. for (const id of [ "gpt-5.3-codex", "gpt-5.4-mini", "gpt-5.4", "gpt-5.5", "mai-code-1-flash", "gpt-5-mini", "oswe-vscode-prime", ]) { test(`#2911 github/${id} (OpenAI-native) still uses openai-responses`, () => { const model = githubModel(id); assert.ok(model, `${id} must be registered`); assert.equal( model.targetFormat, "openai-responses", `github/${id} is OpenAI-native and must keep the Responses API` ); }); } // Sanity: lookup-by-id helper resolves the same entries. test("#2911 getModelsByProviderId(github) reflects the targetFormat changes", () => { const models = getModelsByProviderId("github") as ModelEntry[]; const opus47 = models.find((m) => m.id === "claude-opus-4.7"); assert.ok(opus47, "claude-opus-4.7 resolvable via getModelsByProviderId"); assert.notEqual(opus47.targetFormat, "openai-responses"); });