diff --git a/open-sse/executors/github.ts b/open-sse/executors/github.ts index f4ab762f80..036998c417 100644 --- a/open-sse/executors/github.ts +++ b/open-sse/executors/github.ts @@ -27,7 +27,10 @@ export class GithubExecutor extends BaseExecutor { buildUrl(model: string, _stream: boolean, _urlIndex = 0) { const targetFormat = getModelTargetFormat("gh", model); - if (targetFormat === "openai-responses") { + // 9router#102: Copilot Codex models advertise supported_endpoints: ["/responses"] + // and 400 on /chat/completions. Route any *-codex id to /responses even when it + // isn't in the curated registry, so newly-shipped Codex models work out of the box. + if (targetFormat === "openai-responses" || /codex/i.test(model)) { return ( this.config.responsesBaseUrl || this.config.baseUrl?.replace(/\/chat\/completions\/?$/, "/responses") || diff --git a/tests/unit/executor-github.test.ts b/tests/unit/executor-github.test.ts index 6d8194e043..ddcbf272cc 100644 --- a/tests/unit/executor-github.test.ts +++ b/tests/unit/executor-github.test.ts @@ -71,6 +71,33 @@ test("GithubExecutor.buildUrl keeps GitHub Claude Opus 4.6 on /chat/completions" assert.equal(url, "https://api.githubcopilot.com/chat/completions"); }); +test("GithubExecutor.buildUrl routes unlisted Codex models to /responses (9router#102)", () => { + // Copilot Codex models advertise supported_endpoints: ["/responses"]. When such + // a model isn't in the curated gh registry, getModelTargetFormat returns null and + // the request fell through to /chat/completions -> upstream 400 "model is not + // accessible via the /chat/completions endpoint". Any *-codex id must route to + // /responses regardless of whether it's explicitly registered. + const executor = new GithubExecutor(); + for (const model of [ + "gpt-5-codex", + "gpt-5.1-codex", + "gpt-5.1-codex-mini", + "gpt-5.1-codex-max", + "gpt-5.2-codex", + ]) { + assert.equal( + executor.buildUrl(model, true), + "https://api.githubcopilot.com/responses", + `${model} must route to /responses` + ); + } + // Non-codex unlisted models keep the chat/completions default. + assert.equal( + executor.buildUrl("some-random-chat-model", true), + "https://api.githubcopilot.com/chat/completions" + ); +}); + test("GithubExecutor.transformRequest injects JSON response instructions for Claude and strips reasoning fields", () => { const executor = new GithubExecutor(); const body = {