fix(opencode): support Luna Responses Lite over OAuth (#36685)
Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com>
This commit is contained in:
parent
7ec3d67829
commit
67aa9cee23
2 changed files with 76 additions and 1 deletions
|
|
@ -6,12 +6,15 @@ import { setTimeout as sleep } from "node:timers/promises"
|
||||||
import { createServer } from "http"
|
import { createServer } from "http"
|
||||||
import { OpenAIWebSocketPool } from "./ws-pool"
|
import { OpenAIWebSocketPool } from "./ws-pool"
|
||||||
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
|
||||||
|
import { isRecord } from "@/util/record"
|
||||||
|
|
||||||
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
|
||||||
const ISSUER = "https://auth.openai.com"
|
const ISSUER = "https://auth.openai.com"
|
||||||
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
|
||||||
const OAUTH_PORT = 1455
|
const OAUTH_PORT = 1455
|
||||||
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
const OAUTH_POLLING_SAFETY_MARGIN_MS = 3000
|
||||||
|
const CODEX_COMPATIBILITY_VERSION = "0.144.0"
|
||||||
|
const RESPONSES_LITE_MODEL = "gpt-5.6-luna"
|
||||||
const ALLOWED_MODELS = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
const ALLOWED_MODELS = new Set(["gpt-5.5", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini"])
|
||||||
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
|
const DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
|
||||||
|
|
||||||
|
|
@ -417,6 +420,9 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||||
|
|
||||||
const requestInit = {
|
const requestInit = {
|
||||||
...init,
|
...init,
|
||||||
|
body: parsed.pathname.endsWith("/responses")
|
||||||
|
? prepareResponsesLiteRequest(init?.body, headers)
|
||||||
|
: init?.body,
|
||||||
headers,
|
headers,
|
||||||
}
|
}
|
||||||
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
if (websocketFetch && parsed.pathname.endsWith("/responses")) return websocketFetch(url, requestInit)
|
||||||
|
|
@ -561,3 +567,61 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function prepareResponsesLiteRequest(body: BodyInit | null | undefined, headers: Headers) {
|
||||||
|
if (typeof body !== "string") return body
|
||||||
|
const request: unknown = JSON.parse(body)
|
||||||
|
if (!isRecord(request)) return body
|
||||||
|
if (request.model !== RESPONSES_LITE_MODEL) return body
|
||||||
|
if (!Array.isArray(request.input)) throw new Error("Responses Lite requires an input array")
|
||||||
|
if (request.tools !== undefined && !Array.isArray(request.tools)) {
|
||||||
|
throw new Error("Responses Lite requires a tools array")
|
||||||
|
}
|
||||||
|
if (request.instructions !== undefined && typeof request.instructions !== "string") {
|
||||||
|
throw new Error("Responses Lite requires string instructions")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sessionID = headers.get("session-id")
|
||||||
|
if (!sessionID) throw new Error("Responses Lite requires a session-id header")
|
||||||
|
|
||||||
|
function stripImageDetail(value: unknown) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(stripImageDetail)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!isRecord(value)) return
|
||||||
|
if (value.type === "input_image") delete value.detail
|
||||||
|
Object.values(value).forEach(stripImageDetail)
|
||||||
|
}
|
||||||
|
|
||||||
|
stripImageDetail(request.input)
|
||||||
|
request.input = [
|
||||||
|
{ type: "additional_tools", role: "developer", tools: request.tools ?? [] },
|
||||||
|
...(request.instructions
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
type: "message",
|
||||||
|
role: "developer",
|
||||||
|
content: [{ type: "input_text", text: request.instructions }],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
...request.input,
|
||||||
|
]
|
||||||
|
delete request.tools
|
||||||
|
delete request.instructions
|
||||||
|
request.tool_choice = "auto"
|
||||||
|
request.parallel_tool_calls = false
|
||||||
|
request.prompt_cache_key = sessionID
|
||||||
|
request.reasoning = {
|
||||||
|
...(isRecord(request.reasoning) ? request.reasoning : {}),
|
||||||
|
context: "all_turns",
|
||||||
|
}
|
||||||
|
|
||||||
|
headers.set("session-id", sessionID)
|
||||||
|
headers.set("x-session-affinity", sessionID)
|
||||||
|
headers.set("version", CODEX_COMPATIBILITY_VERSION)
|
||||||
|
headers.set(OpenAIWebSocketPool.RESPONSES_LITE_HEADER, "true")
|
||||||
|
headers.delete("content-length")
|
||||||
|
return JSON.stringify(request)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import { isRecord } from "@/util/record"
|
||||||
import { OpenAIWebSocket } from "./ws"
|
import { OpenAIWebSocket } from "./ws"
|
||||||
|
|
||||||
export const TITLE_HEADER = "x-opencode-title"
|
export const TITLE_HEADER = "x-opencode-title"
|
||||||
|
export const RESPONSES_LITE_HEADER = "x-openai-internal-codex-responses-lite"
|
||||||
|
const RESPONSES_LITE_CLIENT_METADATA = "ws_request_header_x_openai_internal_codex_responses_lite"
|
||||||
|
|
||||||
export interface CreateWebSocketFetchOptions {
|
export interface CreateWebSocketFetchOptions {
|
||||||
httpFetch?: typeof globalThis.fetch
|
httpFetch?: typeof globalThis.fetch
|
||||||
|
|
@ -98,7 +100,16 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
|
||||||
})
|
})
|
||||||
const response = OpenAIWebSocket.streamResponsesWebSocket({
|
const response = OpenAIWebSocket.streamResponsesWebSocket({
|
||||||
socket: entry.socket,
|
socket: entry.socket,
|
||||||
body,
|
body:
|
||||||
|
internalHeaders[RESPONSES_LITE_HEADER] === "true"
|
||||||
|
? {
|
||||||
|
...body,
|
||||||
|
client_metadata: {
|
||||||
|
...(isRecord(body.client_metadata) ? body.client_metadata : {}),
|
||||||
|
[RESPONSES_LITE_CLIENT_METADATA]: "true",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: body,
|
||||||
idleTimeout,
|
idleTimeout,
|
||||||
signal: init?.signal ?? undefined,
|
signal: init?.signal ?? undefined,
|
||||||
onFirstEvent: (error) => resolveFirstEvent(error ?? true),
|
onFirstEvent: (error) => resolveFirstEvent(error ?? true),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue