/** * OpenAI Chat Completions inbound (/v1/chat/completions) for GitHub Copilot App * and other OpenAI-compatible clients. * * Ordinary openai-chat routes send directly on the Chat Completions wire. Routes * that need Responses-only behavior keep the Chat -> Responses -> Chat bridge. */ import { FORWARD_HEADERS } from "../adapters/openai-responses"; import { assertChatCompletionsRoutingBody, ChatCompletionsRequestError, chatCompletionsToResponsesBody, } from "../chat/inbound"; import { chatCompletionsErrorResponse, collectChatCompletion, isChatCompletionsStreamError, responsesJsonToChatCompletion, responsesSseToChatCompletionsSse, } from "../chat/outbound"; import { classifyError, cyberPolicyErrorType, CYBER_POLICY_ERROR_CODE, isCyberPolicyCode } from "../lib/errors"; import { redactSecretString } from "../lib/redact"; import { resolveClientRetryAfter } from "../lib/retry-after"; import { estimateTokens } from "../lib/token-estimate"; import { NoEligiblePolicyCandidateError, UnknownRoutingPolicyError, routeModel } from "../router"; import { evidenceFromBody } from "../routing/request-evidence"; import { resolveWireProtocolOverride } from "./adapter-resolve"; import { resolveOpenCodeGoTransport } from "../providers/opencode-go-transport"; import { normalizeLogConversationId, sessionLaneIdFromRequest } from "./request-log-conversation"; import type { OcxConfig } from "../types"; import { readJsonRequestBody } from "./request-decompress"; import { addFinalRequestLog, httpStatusForRequestLogTerminal, recordFirstOutput, type RequestLogContext, type RequestLogEntry, } from "./request-log"; import { responseWithDeferredRequestLog } from "./relay"; import { handleResponses } from "./responses"; import type { AdmissionLease } from "../lib/admission"; import type { DataPlaneAdmission } from "./auth-cors"; import { tryClaimNativeMainProfileForTurn } from "../codex/native-main-admission"; import { createTranslatorBudget, finalizeTranslatorBudgetResponse, isTranslatorBudgetExceededError, type TranslatorBudget, } from "../lib/translator-budget"; import { handleNativeChatCompletions, isNativeChatRouteEligible } from "./chat-native"; import { jsonCompletionSse } from "./chat-native-sse"; import { parseRequestEffortRowId } from "./effort-row"; import { parseSyntheticRowId } from "./fast-row"; import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers"; import { CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, isCodexReserveHelperUnsupported } from "../codex/loopback-target"; type Rec = Record; function isRec(v: unknown): v is Rec { return !!v && typeof v === "object" && !Array.isArray(v); } async function readChatBody(req: Request, budget: TranslatorBudget): Promise { try { return await readJsonRequestBody(req, budget); } catch (err) { if (isTranslatorBudgetExceededError(err)) throw err; throw new ChatCompletionsRequestError(err instanceof Error && err.message ? err.message : "Invalid JSON body"); } } export async function handleChatCompletions( req: Request, config: OcxConfig, logCtx: RequestLogContext, logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission }, ): Promise { const translatorBudget = createTranslatorBudget(); try { return finalizeTranslatorBudgetResponse( await handleChatCompletionsWithBudget(req, config, logCtx, translatorBudget, logIds), translatorBudget, ); } catch (error) { translatorBudget.dispose(); if (isTranslatorBudgetExceededError(error)) { if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 502, { closeReason: "non_stream" }); return chatCompletionsErrorResponse(502, "upstream translation buffer exceeded the safe limit", "upstream_error", "translation_buffer_limit"); } if (isChatCompletionsStreamError(error)) { if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, error.status, { closeReason: "non_stream" }); return chatCompletionsErrorResponse(error.status, error.message, error.type, error.code); } throw error; } } async function handleChatCompletionsWithBudget( req: Request, config: OcxConfig, logCtx: RequestLogContext, translatorBudget: TranslatorBudget, logIds?: { requestId: string; start: number; turnAdmissionLease?: AdmissionLease; admission?: DataPlaneAdmission }, ): Promise { let chatBody: Rec; try { const rawBody = await readChatBody(req, translatorBudget); assertChatCompletionsRoutingBody(rawBody); chatBody = rawBody; } catch (err) { const overflow = isTranslatorBudgetExceededError(err); const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500; if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" }); return chatCompletionsErrorResponse( status, overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err), overflow ? "request_too_large" : undefined, overflow ? "translation_buffer_limit" : undefined, ); } const requestedModel = chatBody.model as string; const { fastRow, effortRow } = parseSyntheticRowId(requestedModel, config); if (effortRow) chatBody.model = effortRow.baseId; if (fastRow) { chatBody.model = fastRow.baseId; // A caller intent; decideTier rules on it downstream. Unlike an effort row this does NOT // block the native-chat shortcut below: native chat carries service_tier itself and runs // the same policy, so blocking it would degrade the request for no reason. chatBody.service_tier = "priority"; } const stream = chatBody.stream === true; // Best-effort Grok attribution: the managed fence stamps this header on every model // it registers (extra_headers, sent verbatim by upstream Grok). Dashboard usage // bucketing only — never an auth or billing signal. if (req.headers.get("x-opencodex-grok") === "1") logCtx.surface = "grok"; let directRoute = false; let settledRoute: ReturnType | null = null; let chatNativeRoute: ReturnType | null = null; try { const route = routeModel(config, chatBody.model as string, evidenceFromBody(chatBody)); route.provider = resolveOpenCodeGoTransport(route.provider, sessionLaneIdFromRequest(req.headers) ?? normalizeLogConversationId(req.headers.get("x-opencode-session"))); // Settle the wire once so every branch below reads the adapter this model will // actually use, not the provider-wide default (#404). route.provider = resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, "chat"); logCtx.model = route.modelId; logCtx.providerAdapter = route.provider.adapter; logCtx.requestedModel = requestedModel; if (route.routeReason === "model-alias" || route.modelId !== requestedModel && requestedModel.includes("/")) logCtx.requestedAlias = requestedModel; logCtx.provider = route.providerName; logCtx.routeDecision = route.routeDecision; settledRoute = route; if (route.provider.adapter === "openai-responses") { directRoute = route.codexAccountMode === "direct"; } if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") { const parts: string[] = []; if (chatBody.messages !== undefined) parts.push(JSON.stringify(chatBody.messages)); if (chatBody.tools !== undefined) parts.push(JSON.stringify(chatBody.tools)); logCtx.usageLogInputTokens = Math.max(1, estimateTokens(parts.join("\n"), requestedModel)); } if (!effortRow && isNativeChatRouteEligible(route, chatBody)) chatNativeRoute = route; } catch (err) { if (err instanceof UnknownRoutingPolicyError) { logCtx.requestedModel = requestedModel; if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" }); return chatCompletionsErrorResponse(404, err.message, "invalid_request_error"); } if (err instanceof NoEligiblePolicyCandidateError) { logCtx.routeDecision = err.trace; if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, 404, { closeReason: "non_stream" }); return chatCompletionsErrorResponse(404, err.message, "invalid_request_error"); } /* unknown model: let handleResponses shape the 404 */ } if (chatNativeRoute) { return handleNativeChatCompletions({ req, config, logCtx, ...(logIds ? { logIds } : {}), route: chatNativeRoute, chatBody, requestedModel, requestedStream: stream, translatorBudget, }); } let internalBody: Rec; try { // Validate the full Chat boundary after routing. Native Chat keeps `chatBody` as // its wire source; this Responses projection is used only by the fallback path. internalBody = chatCompletionsToResponsesBody(chatBody); if (effortRow) { internalBody.reasoning = { ...(isRec(internalBody.reasoning) ? internalBody.reasoning : {}), effort: effortRow.effort, }; } } catch (err) { const overflow = isTranslatorBudgetExceededError(err); const status = overflow ? 413 : err instanceof ChatCompletionsRequestError ? 400 : 500; if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" }); return chatCompletionsErrorResponse( status, overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err), overflow ? "request_too_large" : undefined, overflow ? "translation_buffer_limit" : undefined, ); } // Routed adapters only support streamed turns; always stream internally and fold // for non-streaming clients. Native Chat uses the caller's original stream bit. internalBody.stream = true; if (settledRoute?.provider.adapter === "openai-responses") { // ChatGPT backend rejects store:true and unsupported sampling knobs. internalBody.store = false; delete internalBody.max_output_tokens; delete internalBody.temperature; delete internalBody.top_p; delete internalBody.stop; delete internalBody.user; } else if (internalBody.store === undefined) { internalBody.store = false; } if (settledRoute && internalBody.reasoning !== undefined) { const { stripEmptyLadderEffort, supportedLadderFor } = await import("./effort-policy"); const ladder = supportedLadderFor({ provider: settledRoute.provider, modelId: settledRoute.modelId }); const next = stripEmptyLadderEffort(internalBody.reasoning, ladder); if (next === undefined) delete internalBody.reasoning; else internalBody.reasoning = next; } const visionDescribeTerminal = req.headers.get("x-opencodex-vision-describe") === "1"; // Concrete helper targets must fail before optional stored-main credential enrichment. // Unresolved combos are checked after their concrete child route is selected in Responses. if (settledRoute && !settledRoute.combo && isCanonicalOpenAiForwardProvider(settledRoute.provider) && isCodexReserveHelperUnsupported(config, settledRoute.modelId, logIds?.admission, visionDescribeTerminal)) { return chatCompletionsErrorResponse(400, CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE, "invalid_request_error"); } const headers = new Headers({ "content-type": "application/json" }); // Internal bridge metadata; the Go resolver scopes and hashes it before upstream use. const openCodeSession = req.headers.get("x-opencode-session"); if (openCodeSession) headers.set("x-opencode-session", openCodeSession); for (const name of FORWARD_HEADERS) { if (name === "authorization" && !directRoute) continue; const value = req.headers.get(name); if (value) headers.set(name, value); } // Prefer main ChatGPT auth so OpenAI-backed sidecars remain reachable on routed turns. if (!directRoute) { // This enrichment is optional for routed/non-main providers. If native main // is fenced, omit it and let auth-context reject only a final physical-main // selection while healthy pool/provider routes continue. if (tryClaimNativeMainProfileForTurn(logIds?.turnAdmissionLease)) { try { const { getMainAccountToken } = await import("../codex/main-account"); const token = getMainAccountToken(); if (token) { headers.set("authorization", `Bearer ${token.accessToken}`); headers.set("chatgpt-account-id", token.chatgptAccountId); } } catch { /* optional */ } } } let internalBodyJson: string; try { internalBodyJson = JSON.stringify(internalBody); translatorBudget.chargeRetained( new TextEncoder().encode(internalBodyJson).byteLength, { kind: "request_copies" }, ); } catch (err) { const overflow = isTranslatorBudgetExceededError(err); const status = overflow ? 413 : 500; if (logIds) addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, { closeReason: "non_stream" }); return chatCompletionsErrorResponse( status, overflow ? "request translation buffer exceeded the safe limit" : err instanceof Error ? err.message : String(err), overflow ? "request_too_large" : undefined, overflow ? "translation_buffer_limit" : undefined, ); } const internalReq = new Request("http://localhost/v1/responses", { method: "POST", headers, body: internalBodyJson, }); let nativeLogged = false; const finalizeNativeLog = (status: number, meta: { terminalStatus?: RequestLogEntry["terminalStatus"]; closeReason: "terminal" | "client_cancel" | "non_stream" }) => { if (!logIds || nativeLogged) return; nativeLogged = true; addFinalRequestLog(logIds.requestId, logIds.start, logCtx, status, meta); }; const upstream = await handleResponses(internalReq, config, logCtx, { ...(logIds?.turnAdmissionLease ? { turnAdmissionLease: logIds.turnAdmissionLease } : {}), // #1686: the Chat surface translates its body and replays here, so the admission fact has // to ride along or a bearer-admitted Chat caller would still be refused by Direct. ...(logIds?.admission ? { admission: logIds.admission } : {}), abortSignal: req.signal, // Body is Responses-shaped by now, but the client spoke Chat Completions. inboundWire: "chat", // Terminal vision-describe marker (roadmap 180): the bridge rebuilds // headers from the FORWARD_HEADERS allowlist, which would drop the raw // header — so the fact is detected here and carried as an option flag. ...(visionDescribeTerminal ? { visionDescribeTerminal: true } : {}), translatorBudget, ...(logIds ? { onFirstOutput: () => recordFirstOutput(logCtx, logIds.start) } : {}), onNativePassthroughTerminal: status => finalizeNativeLog(httpStatusForRequestLogTerminal(status, logCtx), { terminalStatus: status, closeReason: "terminal" }), onNativePassthroughCancel: () => finalizeNativeLog(499, { closeReason: "client_cancel" }), }); // Rewrite non-2xx before deferred logging so /api/logs records the client-facing status // (e.g. cyber_policy remapped from a passthrough 5xx to HTTP 400). if (!upstream.ok) { let message = `upstream error (${upstream.status})`; let upstreamCode: string | null | undefined; let upstreamType: string | undefined; try { const text = await upstream.text(); try { const parsed = JSON.parse(text) as { error?: { message?: string; type?: string; code?: string | null } | string; message?: string; type?: string; code?: string | null; }; const nested = typeof parsed?.error === "object" && parsed.error ? parsed.error : undefined; const flat = typeof parsed?.error === "string" ? parsed.error : parsed?.message; const rawFallback = text ? `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}` : message; const upstreamMessage = nested?.message || flat; message = upstreamMessage ? redactSecretString(upstreamMessage).slice(0, 500) : rawFallback; const structuredType = nested?.type ?? parsed.type; const structuredCode = nested?.code ?? parsed.code; if (typeof structuredType === "string") upstreamType = structuredType; if (structuredCode === null || typeof structuredCode === "string") upstreamCode = structuredCode; } catch { if (text) message = `upstream error (${upstream.status}): ${redactSecretString(text).slice(0, 400)}`; } } catch { /* keep fallback */ } const classified = classifyError( upstream.status, upstreamType ?? (upstream.status === 401 ? "authentication_error" : upstream.status === 429 ? "rate_limit_error" : upstream.status >= 500 ? "server_error" : "invalid_request_error"), message, ); if (isCyberPolicyCode(upstreamCode) || classified.code === CYBER_POLICY_ERROR_CODE) { classified.code = CYBER_POLICY_ERROR_CODE; classified.type = cyberPolicyErrorType(upstreamType); } else if (upstreamCode === "model_not_found") { // Structured model_not_found must win over classifyError's generic remaps. classified.code = "model_not_found"; classified.type = "invalid_request_error"; } else if (upstreamCode !== undefined && upstreamCode !== null && classified.code == null) { classified.code = upstreamCode; } const status = isCyberPolicyCode(classified.code) ? 400 : upstream.status; const retryAfter = isCyberPolicyCode(classified.code) ? undefined : resolveClientRetryAfter({ status: upstream.status, message, upstreamRetryAfter: upstream.headers.get("retry-after"), }); const rewritten = new Response(JSON.stringify({ error: { message: classified.message, type: classified.type, param: null, code: classified.code, }, }), { status, headers: { "Content-Type": "application/json", ...(retryAfter ? { "Retry-After": retryAfter } : {}), }, }); return logIds ? responseWithDeferredRequestLog(rewritten, logIds.requestId, logIds.start, logCtx) : rewritten; } const contentType = upstream.headers.get("content-type") ?? ""; // JSON is not complete for the client until its Chat projection succeeds. // Logging the upstream JSON body here would persist 200 before a later // conversion/serialization error, double-counting both the request and usage. const response = logIds && contentType.includes("text/event-stream") ? responseWithDeferredRequestLog(upstream, logIds.requestId, logIds.start, logCtx) : upstream; if (contentType.includes("text/event-stream") && response.body) { const chatSse = responsesSseToChatCompletionsSse(response.body, requestedModel, { translatorBudget }); if (stream) { // Stream failures surface as an error SSE frame then abort the body — never a // success completion that embeds `[error] ...` + clean [DONE]. return new Response(chatSse, { status: 200, headers: { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive", }, }); } try { const completion = await collectChatCompletion(chatSse, requestedModel, translatorBudget); return new Response(JSON.stringify(completion), { status: 200, headers: { "Content-Type": "application/json" }, }); } catch (err) { if (isChatCompletionsStreamError(err)) { return chatCompletionsErrorResponse(err.status, err.message, err.type, err.code); } return chatCompletionsErrorResponse( 502, err instanceof Error ? err.message : String(err), "server_error", ); } } // Defensive: JSON despite stream:true. const finishJson = (result: Response): Response => { finalizeNativeLog(result.status, { closeReason: "non_stream" }); return result; }; let json: unknown; try { json = await response.json(); } catch { return finishJson(chatCompletionsErrorResponse(502, "internal replay returned a non-JSON response", "server_error")); } const status = (json as Rec)?.status; if (status === "failed") { const error = (json as { error?: { message?: string; type?: string; code?: string | null } }).error; const message = redactSecretString(error?.message ?? "upstream request failed"); const classified = classifyError(502, error?.type ?? "server_error", message); if (error?.code === "translation_buffer_limit") { classified.code = "translation_buffer_limit"; classified.type = "upstream_error"; } else if (isCyberPolicyCode(error?.code) || classified.code === CYBER_POLICY_ERROR_CODE) { classified.code = CYBER_POLICY_ERROR_CODE; classified.type = cyberPolicyErrorType(error?.type); } else if (error?.code === "model_not_found") { // Same deliberate preserve as the non-OK path: structured code beats generic classify. classified.code = "model_not_found"; classified.type = "invalid_request_error"; } return finishJson(chatCompletionsErrorResponse( classified.code === "translation_buffer_limit" ? 502 : isCyberPolicyCode(classified.code) ? 400 : 502, message, classified.type, classified.code, )); } const completion = responsesJsonToChatCompletion(json, requestedModel, translatorBudget); const body = stream ? jsonCompletionSse(completion, requestedModel, translatorBudget) : JSON.stringify(completion); if (!stream) translatorBudget.chargeRetained(Buffer.byteLength(body) * 2, { kind: "live_transient" }); return finishJson(new Response(body, { status: 200, headers: stream ? { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", Connection: "keep-alive" } : { "Content-Type": "application/json" }, })); }