Repository navigation
fix(github): Implement dynamic fallback for Codex models requiring /responses endpoint #127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+72
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+99
to
+108
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"))); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+112
to
+120
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 trimmed = buffer.trim(); | |
| if (!trimmed) { | |
| return; | |
| } | |
| const parsed = parseSSELine(trimmed); | |
| if (!parsed) { | |
| return; | |
| } | |
| if (parsed.done) { | |
| controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n")); | |
| return; | |
| } | |
| const converted = openaiResponsesToOpenAIResponse(parsed, state); | |
| if (converted) { | |
| controller.enqueue(new TextEncoder().encode(formatSSE(converted, "openai"))); | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The fallback trigger checks
errorBody.includes("not accessible via the /chat/completions endpoint")on the raw response text. Since GitHub returns structured JSON errors, it would be more robust toJSON.parseand inspecterror.message(and guard parse failures). This avoids false positives and makes the fallback resilient to formatting changes (whitespace, wrapping, localization).