Просмотр исходного кода

fix(opencode): support Luna Responses Lite over OAuth (#36685)

Co-authored-by: AidenGeunGeun <eastlandwyvern@gmail.com>
Aiden Cline 1 месяц назад
Родитель
Сommit
67aa9cee23

+ 64 - 0
packages/opencode/src/plugin/openai/codex.ts

@@ -6,12 +6,15 @@ import { setTimeout as sleep } from "node:timers/promises"
 import { createServer } from "http"
 import { OpenAIWebSocketPool } from "./ws-pool"
 import { OauthCallbackPage } from "@opencode-ai/core/oauth/page"
+import { isRecord } from "@/util/record"
 
 const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann"
 const ISSUER = "https://auth.openai.com"
 const CODEX_API_ENDPOINT = "https://chatgpt.com/backend-api/codex/responses"
 const OAUTH_PORT = 1455
 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 DISALLOWED_MODELS = new Set(["gpt-5.5-pro"])
 
@@ -417,6 +420,9 @@ export async function CodexAuthPlugin(input: PluginInput, options: CodexAuthPlug
 
             const requestInit = {
               ...init,
+              body: parsed.pathname.endsWith("/responses")
+                ? prepareResponsesLiteRequest(init?.body, headers)
+                : init?.body,
               headers,
             }
             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)
+}

+ 12 - 1
packages/opencode/src/plugin/openai/ws-pool.ts

@@ -4,6 +4,8 @@ import { isRecord } from "@/util/record"
 import { OpenAIWebSocket } from "./ws"
 
 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 {
   httpFetch?: typeof globalThis.fetch
@@ -98,7 +100,16 @@ export function createWebSocketFetch(options?: CreateWebSocketFetchOptions) {
       })
       const response = OpenAIWebSocket.streamResponsesWebSocket({
         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,
         signal: init?.signal ?? undefined,
         onFirstEvent: (error) => resolveFirstEvent(error ?? true),