1 line
16 KiB
Plaintext
1 line
16 KiB
Plaintext
{"version":3,"file":"openai-codex-responses.d.ts","sourceRoot":"","sources":["../../src/providers/openai-codex-responses.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAKX,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,MAAM,aAAa,CAAC;AA4BrB,MAAM,WAAW,2BAA4B,SAAQ,aAAa;IACjE,eAAe,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;IAC3E,gBAAgB,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,KAAK,GAAG,IAAI,GAAG,IAAI,CAAC;IACzE,aAAa,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;CAC1C;AAkDD,eAAO,MAAM,0BAA0B,EAAE,cAAc,CAAC,wBAAwB,EAAE,2BAA2B,CAoH5G,CAAC;AAEF,eAAO,MAAM,gCAAgC,EAAE,cAAc,CAAC,wBAAwB,EAAE,mBAAmB,CAiB1G,CAAC","sourcesContent":["// NEVER convert to top-level import - breaks browser/Vite builds (web-ui)\nlet _os: typeof import(\"node:os\") | null = null;\nif (typeof process !== \"undefined\" && (process.versions?.node || process.versions?.bun)) {\n\timport(\"node:os\").then((m) => {\n\t\t_os = m;\n\t});\n}\n\nimport type { Tool as OpenAITool, ResponseInput, ResponseStreamEvent } from \"openai/resources/responses/responses.js\";\nimport { getEnvApiKey } from \"../env-api-keys.js\";\nimport { supportsXhigh } from \"../models.js\";\nimport type {\n\tApi,\n\tAssistantMessage,\n\tContext,\n\tModel,\n\tSimpleStreamOptions,\n\tStreamFunction,\n\tStreamOptions,\n} from \"../types.js\";\nimport { AssistantMessageEventStream } from \"../utils/event-stream.js\";\nimport { convertResponsesMessages, convertResponsesTools, processResponsesStream } from \"./openai-responses-shared.js\";\nimport { buildBaseOptions, clampReasoning } from \"./simple-options.js\";\n\n// ============================================================================\n// Configuration\n// ============================================================================\n\nconst CODEX_URL = \"https://chatgpt.com/backend-api/codex/responses\";\nconst JWT_CLAIM_PATH = \"https://api.openai.com/auth\" as const;\nconst MAX_RETRIES = 3;\nconst BASE_DELAY_MS = 1000;\nconst CODEX_TOOL_CALL_PROVIDERS = new Set([\"openai\", \"openai-codex\", \"opencode\"]);\n\nconst CODEX_RESPONSE_STATUSES = new Set<CodexResponseStatus>([\n\t\"completed\",\n\t\"incomplete\",\n\t\"failed\",\n\t\"cancelled\",\n\t\"queued\",\n\t\"in_progress\",\n]);\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport interface OpenAICodexResponsesOptions extends StreamOptions {\n\treasoningEffort?: \"none\" | \"minimal\" | \"low\" | \"medium\" | \"high\" | \"xhigh\";\n\treasoningSummary?: \"auto\" | \"concise\" | \"detailed\" | \"off\" | \"on\" | null;\n\ttextVerbosity?: \"low\" | \"medium\" | \"high\";\n}\n\ntype CodexResponseStatus = \"completed\" | \"incomplete\" | \"failed\" | \"cancelled\" | \"queued\" | \"in_progress\";\n\ninterface RequestBody {\n\tmodel: string;\n\tstore?: boolean;\n\tstream?: boolean;\n\tinstructions?: string;\n\tinput?: ResponseInput;\n\ttools?: OpenAITool[];\n\ttool_choice?: \"auto\";\n\tparallel_tool_calls?: boolean;\n\ttemperature?: number;\n\treasoning?: { effort?: string; summary?: string };\n\ttext?: { verbosity?: string };\n\tinclude?: string[];\n\tprompt_cache_key?: string;\n\t[key: string]: unknown;\n}\n\n// ============================================================================\n// Retry Helpers\n// ============================================================================\n\nfunction isRetryableError(status: number, errorText: string): boolean {\n\tif (status === 429 || status === 500 || status === 502 || status === 503 || status === 504) {\n\t\treturn true;\n\t}\n\treturn /rate.?limit|overloaded|service.?unavailable|upstream.?connect|connection.?refused/i.test(errorText);\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal?.aborted) {\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t\treturn;\n\t\t}\n\t\tconst timeout = setTimeout(resolve, ms);\n\t\tsignal?.addEventListener(\"abort\", () => {\n\t\t\tclearTimeout(timeout);\n\t\t\treject(new Error(\"Request was aborted\"));\n\t\t});\n\t});\n}\n\n// ============================================================================\n// Main Stream Function\n// ============================================================================\n\nexport const streamOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", OpenAICodexResponsesOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): AssistantMessageEventStream => {\n\tconst stream = new AssistantMessageEventStream();\n\n\t(async () => {\n\t\tconst output: AssistantMessage = {\n\t\t\trole: \"assistant\",\n\t\t\tcontent: [],\n\t\t\tapi: \"openai-codex-responses\" as Api,\n\t\t\tprovider: model.provider,\n\t\t\tmodel: model.id,\n\t\t\tusage: {\n\t\t\t\tinput: 0,\n\t\t\t\toutput: 0,\n\t\t\t\tcacheRead: 0,\n\t\t\t\tcacheWrite: 0,\n\t\t\t\ttotalTokens: 0,\n\t\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t\t},\n\t\t\tstopReason: \"stop\",\n\t\t\ttimestamp: Date.now(),\n\t\t};\n\n\t\ttry {\n\t\t\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider) || \"\";\n\t\t\tif (!apiKey) {\n\t\t\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t\t\t}\n\n\t\t\tconst accountId = extractAccountId(apiKey);\n\t\t\tconst body = buildRequestBody(model, context, options);\n\t\t\toptions?.onPayload?.(body);\n\t\t\tconst headers = buildHeaders(model.headers, options?.headers, accountId, apiKey, options?.sessionId);\n\t\t\tconst bodyJson = JSON.stringify(body);\n\n\t\t\t// Fetch with retry logic for rate limits and transient errors\n\t\t\tlet response: Response | undefined;\n\t\t\tlet lastError: Error | undefined;\n\n\t\t\tfor (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n\t\t\t\tif (options?.signal?.aborted) {\n\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t}\n\n\t\t\t\ttry {\n\t\t\t\t\tresponse = await fetch(CODEX_URL, {\n\t\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\t\theaders,\n\t\t\t\t\t\tbody: bodyJson,\n\t\t\t\t\t\tsignal: options?.signal,\n\t\t\t\t\t});\n\n\t\t\t\t\tif (response.ok) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst errorText = await response.text();\n\t\t\t\t\tif (attempt < MAX_RETRIES && isRetryableError(response.status, errorText)) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Parse error for friendly message on final attempt or non-retryable error\n\t\t\t\t\tconst fakeResponse = new Response(errorText, {\n\t\t\t\t\t\tstatus: response.status,\n\t\t\t\t\t\tstatusText: response.statusText,\n\t\t\t\t\t});\n\t\t\t\t\tconst info = await parseErrorResponse(fakeResponse);\n\t\t\t\t\tthrow new Error(info.friendlyMessage || info.message);\n\t\t\t\t} catch (error) {\n\t\t\t\t\tif (error instanceof Error) {\n\t\t\t\t\t\tif (error.name === \"AbortError\" || error.message === \"Request was aborted\") {\n\t\t\t\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tlastError = error instanceof Error ? error : new Error(String(error));\n\t\t\t\t\t// Network errors are retryable\n\t\t\t\t\tif (attempt < MAX_RETRIES && !lastError.message.includes(\"usage limit\")) {\n\t\t\t\t\t\tconst delayMs = BASE_DELAY_MS * 2 ** attempt;\n\t\t\t\t\t\tawait sleep(delayMs, options?.signal);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tthrow lastError;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif (!response?.ok) {\n\t\t\t\tthrow lastError ?? new Error(\"Failed after retries\");\n\t\t\t}\n\n\t\t\tif (!response.body) {\n\t\t\t\tthrow new Error(\"No response body\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"start\", partial: output });\n\t\t\tawait processStream(response, output, stream, model);\n\n\t\t\tif (options?.signal?.aborted) {\n\t\t\t\tthrow new Error(\"Request was aborted\");\n\t\t\t}\n\n\t\t\tstream.push({ type: \"done\", reason: output.stopReason as \"stop\" | \"length\" | \"toolUse\", message: output });\n\t\t\tstream.end();\n\t\t} catch (error) {\n\t\t\toutput.stopReason = options?.signal?.aborted ? \"aborted\" : \"error\";\n\t\t\toutput.errorMessage = error instanceof Error ? error.message : String(error);\n\t\t\tstream.push({ type: \"error\", reason: output.stopReason, error: output });\n\t\t\tstream.end();\n\t\t}\n\t})();\n\n\treturn stream;\n};\n\nexport const streamSimpleOpenAICodexResponses: StreamFunction<\"openai-codex-responses\", SimpleStreamOptions> = (\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: SimpleStreamOptions,\n): AssistantMessageEventStream => {\n\tconst apiKey = options?.apiKey || getEnvApiKey(model.provider);\n\tif (!apiKey) {\n\t\tthrow new Error(`No API key for provider: ${model.provider}`);\n\t}\n\n\tconst base = buildBaseOptions(model, options, apiKey);\n\tconst reasoningEffort = supportsXhigh(model) ? options?.reasoning : clampReasoning(options?.reasoning);\n\n\treturn streamOpenAICodexResponses(model, context, {\n\t\t...base,\n\t\treasoningEffort,\n\t} satisfies OpenAICodexResponsesOptions);\n};\n\n// ============================================================================\n// Request Building\n// ============================================================================\n\nfunction buildRequestBody(\n\tmodel: Model<\"openai-codex-responses\">,\n\tcontext: Context,\n\toptions?: OpenAICodexResponsesOptions,\n): RequestBody {\n\tconst messages = convertResponsesMessages(model, context, CODEX_TOOL_CALL_PROVIDERS, {\n\t\tincludeSystemPrompt: false,\n\t});\n\n\tconst body: RequestBody = {\n\t\tmodel: model.id,\n\t\tstore: false,\n\t\tstream: true,\n\t\tinstructions: context.systemPrompt,\n\t\tinput: messages,\n\t\ttext: { verbosity: options?.textVerbosity || \"medium\" },\n\t\tinclude: [\"reasoning.encrypted_content\"],\n\t\tprompt_cache_key: options?.sessionId,\n\t\ttool_choice: \"auto\",\n\t\tparallel_tool_calls: true,\n\t};\n\n\tif (options?.temperature !== undefined) {\n\t\tbody.temperature = options.temperature;\n\t}\n\n\tif (context.tools) {\n\t\tbody.tools = convertResponsesTools(context.tools, { strict: null });\n\t}\n\n\tif (options?.reasoningEffort !== undefined) {\n\t\tbody.reasoning = {\n\t\t\teffort: clampReasoningEffort(model.id, options.reasoningEffort),\n\t\t\tsummary: options.reasoningSummary ?? \"auto\",\n\t\t};\n\t}\n\n\treturn body;\n}\n\nfunction clampReasoningEffort(modelId: string, effort: string): string {\n\tconst id = modelId.includes(\"/\") ? modelId.split(\"/\").pop()! : modelId;\n\tif (id.startsWith(\"gpt-5.2\") && effort === \"minimal\") return \"low\";\n\tif (id === \"gpt-5.1\" && effort === \"xhigh\") return \"high\";\n\tif (id === \"gpt-5.1-codex-mini\") return effort === \"high\" || effort === \"xhigh\" ? \"high\" : \"medium\";\n\treturn effort;\n}\n\n// ============================================================================\n// Response Processing\n// ============================================================================\n\nasync function processStream(\n\tresponse: Response,\n\toutput: AssistantMessage,\n\tstream: AssistantMessageEventStream,\n\tmodel: Model<\"openai-codex-responses\">,\n): Promise<void> {\n\tawait processResponsesStream(mapCodexEvents(parseSSE(response)), output, stream, model);\n}\n\nasync function* mapCodexEvents(events: AsyncIterable<Record<string, unknown>>): AsyncGenerator<ResponseStreamEvent> {\n\tfor await (const event of events) {\n\t\tconst type = typeof event.type === \"string\" ? event.type : undefined;\n\t\tif (!type) continue;\n\n\t\tif (type === \"error\") {\n\t\t\tconst code = (event as { code?: string }).code || \"\";\n\t\t\tconst message = (event as { message?: string }).message || \"\";\n\t\t\tthrow new Error(`Codex error: ${message || code || JSON.stringify(event)}`);\n\t\t}\n\n\t\tif (type === \"response.failed\") {\n\t\t\tconst msg = (event as { response?: { error?: { message?: string } } }).response?.error?.message;\n\t\t\tthrow new Error(msg || \"Codex response failed\");\n\t\t}\n\n\t\tif (type === \"response.done\" || type === \"response.completed\") {\n\t\t\tconst response = (event as { response?: { status?: unknown } }).response;\n\t\t\tconst normalizedResponse = response\n\t\t\t\t? { ...response, status: normalizeCodexStatus(response.status) }\n\t\t\t\t: response;\n\t\t\tyield { ...event, type: \"response.completed\", response: normalizedResponse } as ResponseStreamEvent;\n\t\t\tcontinue;\n\t\t}\n\n\t\tyield event as unknown as ResponseStreamEvent;\n\t}\n}\n\nfunction normalizeCodexStatus(status: unknown): CodexResponseStatus | undefined {\n\tif (typeof status !== \"string\") return undefined;\n\treturn CODEX_RESPONSE_STATUSES.has(status as CodexResponseStatus) ? (status as CodexResponseStatus) : undefined;\n}\n\n// ============================================================================\n// SSE Parsing\n// ============================================================================\n\nasync function* parseSSE(response: Response): AsyncGenerator<Record<string, unknown>> {\n\tif (!response.body) return;\n\n\tconst reader = response.body.getReader();\n\tconst decoder = new TextDecoder();\n\tlet buffer = \"\";\n\n\twhile (true) {\n\t\tconst { done, value } = await reader.read();\n\t\tif (done) break;\n\t\tbuffer += decoder.decode(value, { stream: true });\n\n\t\tlet idx = buffer.indexOf(\"\\n\\n\");\n\t\twhile (idx !== -1) {\n\t\t\tconst chunk = buffer.slice(0, idx);\n\t\t\tbuffer = buffer.slice(idx + 2);\n\n\t\t\tconst dataLines = chunk\n\t\t\t\t.split(\"\\n\")\n\t\t\t\t.filter((l) => l.startsWith(\"data:\"))\n\t\t\t\t.map((l) => l.slice(5).trim());\n\t\t\tif (dataLines.length > 0) {\n\t\t\t\tconst data = dataLines.join(\"\\n\").trim();\n\t\t\t\tif (data && data !== \"[DONE]\") {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tyield JSON.parse(data);\n\t\t\t\t\t} catch {}\n\t\t\t\t}\n\t\t\t}\n\t\t\tidx = buffer.indexOf(\"\\n\\n\");\n\t\t}\n\t}\n}\n\n// ============================================================================\n// Error Handling\n// ============================================================================\n\nasync function parseErrorResponse(response: Response): Promise<{ message: string; friendlyMessage?: string }> {\n\tconst raw = await response.text();\n\tlet message = raw || response.statusText || \"Request failed\";\n\tlet friendlyMessage: string | undefined;\n\n\ttry {\n\t\tconst parsed = JSON.parse(raw) as {\n\t\t\terror?: { code?: string; type?: string; message?: string; plan_type?: string; resets_at?: number };\n\t\t};\n\t\tconst err = parsed?.error;\n\t\tif (err) {\n\t\t\tconst code = err.code || err.type || \"\";\n\t\t\tif (/usage_limit_reached|usage_not_included|rate_limit_exceeded/i.test(code) || response.status === 429) {\n\t\t\t\tconst plan = err.plan_type ? ` (${err.plan_type.toLowerCase()} plan)` : \"\";\n\t\t\t\tconst mins = err.resets_at\n\t\t\t\t\t? Math.max(0, Math.round((err.resets_at * 1000 - Date.now()) / 60000))\n\t\t\t\t\t: undefined;\n\t\t\t\tconst when = mins !== undefined ? ` Try again in ~${mins} min.` : \"\";\n\t\t\t\tfriendlyMessage = `You have hit your ChatGPT usage limit${plan}.${when}`.trim();\n\t\t\t}\n\t\t\tmessage = err.message || friendlyMessage || message;\n\t\t}\n\t} catch {}\n\n\treturn { message, friendlyMessage };\n}\n\n// ============================================================================\n// Auth & Headers\n// ============================================================================\n\nfunction extractAccountId(token: string): string {\n\ttry {\n\t\tconst parts = token.split(\".\");\n\t\tif (parts.length !== 3) throw new Error(\"Invalid token\");\n\t\tconst payload = JSON.parse(atob(parts[1]));\n\t\tconst accountId = payload?.[JWT_CLAIM_PATH]?.chatgpt_account_id;\n\t\tif (!accountId) throw new Error(\"No account ID in token\");\n\t\treturn accountId;\n\t} catch {\n\t\tthrow new Error(\"Failed to extract accountId from token\");\n\t}\n}\n\nfunction buildHeaders(\n\tinitHeaders: Record<string, string> | undefined,\n\tadditionalHeaders: Record<string, string> | undefined,\n\taccountId: string,\n\ttoken: string,\n\tsessionId?: string,\n): Headers {\n\tconst headers = new Headers(initHeaders);\n\theaders.set(\"Authorization\", `Bearer ${token}`);\n\theaders.set(\"chatgpt-account-id\", accountId);\n\theaders.set(\"OpenAI-Beta\", \"responses=experimental\");\n\theaders.set(\"originator\", \"pi\");\n\tconst userAgent = _os ? `pi (${_os.platform()} ${_os.release()}; ${_os.arch()})` : \"pi (browser)\";\n\theaders.set(\"User-Agent\", userAgent);\n\theaders.set(\"accept\", \"text/event-stream\");\n\theaders.set(\"content-type\", \"application/json\");\n\tfor (const [key, value] of Object.entries(additionalHeaders || {})) {\n\t\theaders.set(key, value);\n\t}\n\n\tif (sessionId) {\n\t\theaders.set(\"session_id\", sessionId);\n\t}\n\n\treturn headers;\n}\n"]} |