import type { ProviderInvokeInput, ProviderOutput, ProviderResult, ProviderSupport, } from "cursor-byok:provider"; import type { PluginContext } from "cursor-byok:plugin"; import { HttpError, streamOpenAiResponses } from "cursor-byok:protocol/openai-responses"; import { codexModels, reasoningEfforts } from "./models.ts"; import { type AccountData, accountData, chatGptAccountId, quotaExhaustedPatch, RESOURCE_TYPE, } from "./resources.ts"; const RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses"; /** 流内错误只有文本可用,按额度关键词分类。 */ export function isQuotaError(error: string): boolean { const message = error.toLowerCase(); return message.includes("insufficient_quota") || message.includes("usage_limit_reached") || message.includes("exceeded your current quota") || message.includes("quota_exceeded") || message.includes("5-hour") || message.includes("5 hour") || (message.includes("429") && (message.includes("quota") || message.includes("usage_limit") || message.includes("insufficient"))); } /** HTTP 失败携带结构化状态码,429 时放宽响应体的匹配条件。 */ function isQuotaHttpError(error: HttpError): boolean { const body = error.body.toLowerCase(); return body.includes("insufficient_quota") || body.includes("usage_limit_reached") || body.includes("exceeded your current quota") || body.includes("quota_exceeded") || body.includes("5-hour") || body.includes("5 hour") || (error.status === 429 && (body.includes("quota") || body.includes("usage_limit") || body.includes("insufficient"))); } function invalidResult(message: string, stateMessage: string): ProviderResult { return { status: "resource-error", message, patch: { state: { status: "invalid", message: stateMessage } }, }; } function headers(data: AccountData, cacheKey: string | null): Record { const result: Record = { authorization: `Bearer ${data.accessToken}`, originator: "codex_cli_rs", }; const accountId = chatGptAccountId(data.accessToken); if (accountId) result["ChatGPT-Account-Id"] = accountId; // Codex 后端的缓存亲和契约:session-id / thread-id / prompt_cache_key // 三者同源(见 codex-rs client.rs);缺头会导致请求落在随机分片上。 if (cacheKey !== null) { result["session-id"] = cacheKey; result["thread-id"] = cacheKey; result["x-client-request-id"] = cacheKey; } return result; } async function invoke( input: ProviderInvokeInput, output: ProviderOutput, context: PluginContext, ): Promise { if (!input.resource) { return { status: "request-error", message: "add a ChatGPT account before calling Codex" }; } let data: AccountData; try { data = accountData(input.resource); } catch (error) { const message = error instanceof Error ? error.message : String(error); return invalidResult(message, message); } const efforts = reasoningEfforts(input.model); const reasoning = input.request.reasoning; const effort = reasoning.effort !== null && efforts.includes(reasoning.effort) ? reasoning.effort : null; try { await streamOpenAiResponses( { url: RESPONSES_URL, model: input.model.id, // Codex 订阅端点不接受 max_output_tokens;fast 档位经协议库映射为 // service_tier: "priority" 后透传。 request: { ...input.request, reasoning: { enabled: reasoning.enabled, effort }, maxOutputTokens: null, }, headers: headers(data, input.request.cacheKey), extraBody: { store: false }, }, output, context, ); return { status: "completed" }; } catch (error) { if (error instanceof HttpError) { if (error.status === 401) { return invalidResult(error.message, "ChatGPT authorization expired; sign in again"); } if (isQuotaHttpError(error)) { return { status: "resource-error", message: error.message, patch: quotaExhaustedPatch(data, error.body), }; } return { status: "request-error", message: error.message }; } const message = error instanceof Error ? error.message : String(error); if (isQuotaError(message)) { return { status: "resource-error", message, patch: quotaExhaustedPatch(data, message) }; } return { status: "request-error", message }; } } export const codexProvider: ProviderSupport = { id: "codex", displayName: "OpenAI Codex", description: { "en-US": "ChatGPT subscription access through the official Codex Responses API.", "zh-CN": "通过官方 Codex Responses API 使用 ChatGPT 订阅。", }, providerType: "openai", resourceType: RESOURCE_TYPE, models: codexModels, invoke, };