From 69131295db4d73573ed4248c5a57fcf8186971aa Mon Sep 17 00:00:00 2001 From: Bexultan Date: Sun, 15 Feb 2026 05:40:55 +0500 Subject: [PATCH] fix(github): Implement dynamic fallback for Codex models requiring /responses endpoint (#127) * fix(github): add dynamic fallback to /responses for Codex models * Refactor GithubExecutor: use config for URL detection --- open-sse/config/constants.js | 1 + open-sse/executors/github.js | 109 ++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/open-sse/config/constants.js b/open-sse/config/constants.js index f1dde12d..ee456100 100644 --- a/open-sse/config/constants.js +++ b/open-sse/config/constants.js @@ -134,6 +134,7 @@ export const PROVIDERS = { }, github: { baseUrl: "https://api.githubcopilot.com/chat/completions", // GitHub Copilot API endpoint for chat + responsesUrl: "https://api.githubcopilot.com/responses", format: "openai", // GitHub Copilot uses OpenAI-compatible format headers: { "copilot-integration-id": "vscode-chat", diff --git a/open-sse/executors/github.js b/open-sse/executors/github.js index f2e95041..08172c1d 100644 --- a/open-sse/executors/github.js +++ b/open-sse/executors/github.js @@ -1,9 +1,15 @@ import { BaseExecutor } from "./base.js"; -import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.js"; +import { PROVIDERS, OAUTH_ENDPOINTS, HTTP_STATUS } from "../config/constants.js"; +import { openaiToOpenAIResponsesRequest } from "../translator/request/openai-responses.js"; +import { openaiResponsesToOpenAIResponse } from "../translator/response/openai-responses.js"; +import { initState } from "../translator/index.js"; +import { parseSSELine, formatSSE } from "../utils/streamHelpers.js"; +import crypto from "crypto"; export class GithubExecutor extends BaseExecutor { constructor() { super("github", PROVIDERS.github); + this.knownCodexModels = new Set(); } buildUrl(model, stream, urlIndex = 0) { @@ -28,6 +34,107 @@ export class GithubExecutor extends BaseExecutor { }; } + async execute(options) { + const { model, log } = options; + + if (this.knownCodexModels.has(model)) { + log?.debug("GITHUB", `Using cached /responses route for ${model}`); + return this.executeWithResponsesEndpoint(options); + } + + const result = await super.execute(options); + + if (result.response.status === HTTP_STATUS.BAD_REQUEST) { + const errorBody = await result.response.clone().text(); + + if (errorBody.includes("not accessible via the /chat/completions endpoint")) { + log?.warn("GITHUB", `Model ${model} requires /responses. Switching...`); + this.knownCodexModels.add(model); + return this.executeWithResponsesEndpoint(options); + } + } + + return result; + } + + async executeWithResponsesEndpoint({ model, body, stream, credentials, signal, log }) { + const url = this.config.responsesUrl; + const headers = this.buildHeaders(credentials, stream); + + const transformedBody = openaiToOpenAIResponsesRequest(model, body, stream, credentials); + + log?.debug("GITHUB", "Sending translated request to /responses"); + + const response = await fetch(url, { + method: "POST", + headers, + body: JSON.stringify(transformedBody), + signal + }); + + if (!response.ok) { + return { response, url, headers, transformedBody }; + } + + const state = initState("openai-responses"); + state.model = model; + + const decoder = new TextDecoder(); + let buffer = ""; + + const transformStream = new TransformStream({ + async transform(chunk, controller) { + buffer += decoder.decode(chunk, { stream: true }); + const lines = buffer.split("\n"); + + buffer = lines.pop() || ""; + + for (const line of lines) { + const trimmed = line.trim(); + if (!trimmed) continue; + + const parsed = parseSSELine(trimmed); + if (!parsed) continue; + + if (parsed.done) { + controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); + continue; + } + + const converted = openaiResponsesToOpenAIResponse(parsed, state); + if (converted) { + const sseString = formatSSE(converted, "openai"); + controller.enqueue(new TextEncoder().encode(sseString)); + } + } + }, + flush(controller) { + if (buffer.trim()) { + const parsed = parseSSELine(buffer.trim()); + if (parsed && !parsed.done) { + const converted = openaiResponsesToOpenAIResponse(parsed, state); + if (converted) { + controller.enqueue(new TextEncoder().encode(formatSSE(converted, "openai"))); + } + } + } + } + }); + + const convertedStream = response.body.pipeThrough(transformStream); + + return { + response: new Response(convertedStream, { + status: response.status, + statusText: response.statusText, + headers: response.headers + }), + url, + headers, + transformedBody + }; + } + async refreshCopilotToken(githubAccessToken, log) { try { const response = await fetch("https://api.github.com/copilot_internal/v2/token", {