src/vs/platform/agentHost/node/codex/codexProxyService.ts

484 LOC · 386 covered · 98 uncovered · 54 ranges · 40 concepts · 13 introducers · 23 tests

File neighbourhood

The centred file is linked to every concept that introduces one of its ranges, every test that runs code from the file, and the gray connector concepts standing between those tests and the file's own introducer concepts. Undirected links join concepts to every file where they introduce source and concepts to the tests they introduce; arrows show specialization between the displayed concepts and bridge only concepts omitted from this view. Concept colors match the source ranges below; connector concepts have no source color and are shown in gray.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the file

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the related-file, concept, and source links on this page.

Focused file, its introducer and connector concepts, their introduced files, and tests that run code from the filecodexAgent.ts ×13 · 83 introduced LOCcodexAgent.ts ×13codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig preserves legacy read-only permissions on restore|occurrence=1 · 0 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig exposes a single permissions-preset chip|occurrence=1 · 0 introduced LOCcodexSessionConfigKeys.t…codexAgent.ts ×1 · 27 introduced LOCcodexAgent.ts ×1codexAgent.ts ×24 · 89 introduced LOCcodexAgent.ts ×24codexSessionConfigKeys.ts ×1 · 7 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×1 · 2 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×7 · 49 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×1 · 11 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×1 · 2 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×2 · 4 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×3 · 18 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×3 · 19 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×2 · 3 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×2 · 2 introduced LOCcodexSessionConfigKeys.t…codexSessionConfigKeys.ts ×6 · 23 introduced LOCcodexSessionConfigKeys.t…codexAgent.ts ×1 · 2 introduced LOCcodexAgent.ts ×1codexAgent.ts ×1 · 2 introduced LOCcodexAgent.ts ×1codexAgent.ts ×3 · 7 introduced LOCcodexAgent.ts ×3codexAgent.ts ×1 · 2 introduced LOCcodexAgent.ts ×1codexAgent.ts ×1 · 2 introduced LOCcodexAgent.ts ×1codexAgent.ts ×1 · 2 introduced LOCcodexAgent.ts ×1codexAgent.ts ×1 · 10 introduced LOCcodexAgent.ts ×1codexAgent.ts ×8 · 19 introduced LOCcodexAgent.ts ×8codexAgent.ts ×158 · 1640 introduced LOCcodexAgent.ts ×158codexProxyService.ts ×1 · 2 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×1 · 2 introduced LOCcodexProxyService.ts ×1codexProxyService.test|title=CodexProxyService forwards transformed user-agent to CAPI responses|occurrence=1, codexProxyService.test|title=CodexProxyService keeps the suffix when transforming a multi-segment user-agent|occurrence=1 · 0 introduced LOCcodexProxyService.test|t…codexProxyService.ts ×3 · 7 introduced LOCcodexProxyService.ts ×3codexProxyService.test|title=CodexProxyService omits User-Agent when the inbound request has none|occurrence=1 · 0 introduced LOCcodexProxyService.test|t…codexProxyService.ts ×19 · 118 introduced LOCcodexProxyService.ts ×19codexProxyService.ts ×1 · 9 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×10 · 43 introduced LOCcodexProxyService.ts ×10codexProxyService.ts ×1 · 3 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×1 · 5 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×1 · 2 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×2 · 4 introduced LOCcodexProxyService.ts ×2codexProxyService.ts ×1 · 1 introduced LOCcodexProxyService.ts ×1codexProxyService.ts ×2 · 9 introduced LOCcodexProxyService.ts ×2codexProxyService.ts ×11 · 181 introduced LOCcodexProxyService.ts ×11codexModelRefresh.test|title=CodexAgent model refresh keeps the last known-good models when a periodic refresh fails|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexModelRefresh.test|title=CodexAgent model refresh keeps the last known-good models when a periodic refresh fails|occurrence=1codexModelRefresh.test|t…codexPackagePaths.test|title=codex package paths codexBinaryTriple every suffix produced by codexPackageSuffix maps to a rust target triple|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths codexBinaryTriple every suffix produced by codexPackageSuffix maps to a rust target triple|occurrence=1codexPackagePaths.test|t…codexPackagePaths.test|title=codex package paths codexBinaryTriple returns undefined for unknown suffixes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths codexBinaryTriple returns undefined for unknown suffixes|occurrence=1codexPackagePaths.test|t…codexPackagePaths.test|title=codex package paths codexPackageSuffix every supported (platform, arch) returns the npm optionalDependencies suffix|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths codexPackageSuffix every supported (platform, arch) returns the npm optionalDependencies suffix|occurrence=1codexPackagePaths.test|t…codexPackagePaths.test|title=codex package paths codexPackageSuffix never returns a -musl suffix on Linux (Codex is statically musl-linked)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths codexPackageSuffix never returns a -musl suffix on Linux (Codex is statically musl-linked)|occurrence=1codexPackagePaths.test|t…codexPackagePaths.test|title=codex package paths codexPackageSuffix returns undefined for unsupported platforms and architectures|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths codexPackageSuffix returns undefined for unsupported platforms and architectures|occurrence=1codexPackagePaths.test|t…codex resolves|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths resolveCodexDevSdkRoot returns the directory containing node_modules when @openai/codex resolves|occurrence=1codex resolves|occurrenc…codexPackagePaths.test|title=codex package paths resolveCodexDevSdkRoot returns undefined when resolution throws (e.g. built product without the devDependency)|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexPackagePaths.test|title=codex package paths resolveCodexDevSdkRoot returns undefined when resolution throws (e.g. built product without the devDependency)|occurrence=1codexPackagePaths.test|t…codexProxyService.test|title=CodexProxyService forwards the auto-review reviewer model unchanged when no primary model has been seen|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService forwards the auto-review reviewer model unchanged when no primary model has been seen|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService forwards transformed user-agent to CAPI responses|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService forwards transformed user-agent to CAPI responses|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService keeps the suffix when transforming a multi-segment user-agent|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService keeps the suffix when transforming a multi-segment user-agent|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService omits User-Agent when the inbound request has none|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService omits User-Agent when the inbound request has none|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService remapCodexReviewerModel records the primary model and leaves the body untouched|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService remapCodexReviewerModel records the primary model and leaves the body untouched|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService remapCodexReviewerModel remaps the reviewer model and reports the substitution|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService remapCodexReviewerModel remaps the reviewer model and reports the substitution|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService remapCodexReviewerModel returns the original body for unparseable or model-less payloads|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService remapCodexReviewerModel returns the original body for unparseable or model-less payloads|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService remaps the reviewer model onto the most recent primary model|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService remaps the reviewer model onto the most recent primary model|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService remaps the unsupported auto-review reviewer model onto the last primary model|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService remaps the unsupported auto-review reviewer model onto the last primary model|occurrence=1codexProxyService.test|t…codexProxyService.test|title=CodexProxyService serves an empty Codex model catalog|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexProxyService.test|title=CodexProxyService serves an empty Codex model catalog|occurrence=1codexProxyService.test|t…codexSessionConfigKeys.test|title=codexSessionConfigKeys expands permissions presets and falls back to legacy axes|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexSessionConfigKeys.test|title=codexSessionConfigKeys expands permissions presets and falls back to legacy axes|occurrence=1codexSessionConfigKeys.t…codexSessionConfigKeys.test|title=codexSessionConfigKeys inverts presets and migrates legacy axes without escalating|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexSessionConfigKeys.test|title=codexSessionConfigKeys inverts presets and migrates legacy axes without escalating|occurrence=1codexSessionConfigKeys.t…codexSessionConfigKeys.test|title=codexSessionConfigKeys narrows valid values and rejects invalid values|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexSessionConfigKeys.test|title=codexSessionConfigKeys narrows valid values and rejects invalid values|occurrence=1codexSessionConfigKeys.t…codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig exposes a single permissions-preset chip|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig exposes a single permissions-preset chip|occurrence=1codexSessionConfigKeys.t…codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig preserves legacy read-only permissions on restore|occurrence=1 · introduced test · mocha:v1|namespace=vscode@05c208e9e28d8c1c723fa08f85e2b7a96092e8e5|file=vs/platform/agentHost/test/node/codex/codexSessionConfigKeys.test|title=codexSessionConfigKeys resolveSessionConfig preserves legacy read-only permissions on restore|occurrence=1codexSessionConfigKeys.t…Focused file · src/vs/platform/agentHost/node/codex/codexProxyService.ts · 484 LOCcodex/codexProxyService.…

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the related-file, concept, and source links on this page while the interactive map is unavailable.

1 > /*--------------------------------------------------------------------------------------------- codexProxyService.ts ×11
2 > * Copyright (c) Microsoft Corporation. All rights reserved.
3 > * Licensed under the MIT License. See License.txt in the project root for license information.
4 > *--------------------------------------------------------------------------------------------*/
5 >
6 > import type * as http from 'http';
7 > import * as fs from 'fs';
8 > import { join } from '../../../../base/common/path.js';
9 > import { createDecorator } from '../../../instantiation/common/instantiation.js';
10 > import { ILogService } from '../../../log/common/log.js';
11 > import { CopilotApiError, ICopilotApiService } from '../shared/copilotApiService.js';
12 > import { buildForwardedChatError, encodeForwardedChatError } from '../shared/forwardedChatError.js';
13 > import {
14 > ILoopbackProxyHandle,
15 > ILoopbackProxyRuntime,
16 > IProxyInFlight,
17 > LoopbackProxyServer,
18 > readProxyRequestBody,
19 > } from '../shared/loopbackProxyServer.js';
20 >
21 > /**
22 > * Refcounted handle to the local OpenAI-Responses → CAPI proxy.
23 > *
24 > * The handle owns a nonce that the codex CLI passes as `Bearer <nonce>` on
25 > * every request. The proxy validates that nonce, then re-issues the request
26 > * to CAPI using the **current** GitHub Copilot token — which can rotate
27 > * underneath the codex process without affecting it. Call
28 > * {@link setToken} when the upstream token changes; in-flight requests keep
29 > * using the value they captured at dispatch time, new requests pick up the
30 > * fresh value.
31 > *
32 > * Subprocess-ownership invariant: any subprocess given `baseUrl` / `nonce`
33 > * MUST be killed before this handle is disposed; otherwise the proxy may
34 > * rebind on a different port on next `start()` and the subprocess silently
35 > * loses its endpoint.
36 > */
37 > export interface ICodexProxyHandle extends ILoopbackProxyHandle {
38 > /** e.g. `http://127.0.0.1:54321` — no trailing slash. */
39 > readonly baseUrl: string;
40 > /** Random per-process nonce used as `Bearer <nonce>` by the codex CLI. */
41 > readonly nonce: string;
42 > /**
43 > * Replace the GitHub Copilot token used for outbound CAPI calls. The
44 > * codex process and its nonce are unchanged.
45 > */
46 > setToken(githubToken: string): void;
47 > }
48 >
49 > export interface ICodexProxyService {
50 > readonly _serviceBrand: undefined;
51 >
52 > /**
53 > * Start the proxy (if not already running) and return a refcounted
54 > * handle. The provided token is the initial value; rotate via
55 > * {@link ICodexProxyHandle.setToken}.
56 > */
57 > start(githubToken: string): Promise<ICodexProxyHandle>;
58 >
59 > /** Force-close the proxy regardless of refcount. Idempotent. */
60 > dispose(): void;
61 > }
62 >
63 > export const ICodexProxyService = createDecorator<ICodexProxyService>('codexProxyService');
64 >
65 > /** Subclass-owned per-bind mutable state: the active outbound CAPI token. */
66 > interface ICodexProxyState {
67 > /** Token cell — read fresh on each outbound request. */
68 > githubToken: string;
69 > /**
70 > * Most recent *primary* (non-reviewer) model id forwarded on this bind,
71 > * observed from normal turn requests. Used to remap the unsupported
72 > * auto-review reviewer model (see {@link CODEX_AUTO_REVIEW_MODEL}) onto a
73 > * model that is known to be supported by the Copilot CAPI. `undefined`
74 > * until the first primary request is seen.
75 > *
76 > * Bind-global, not per-session: the proxy is a single refcounted bind
77 > * shared by every concurrent Codex session and reviewer requests carry no
78 > * session identity, so this tracks the last primary model seen across all
79 > * sessions. Under the documented single-tenant assumption (one active model
80 > * at a time) that is correct; with two concurrent sessions on *different*
81 > * models where one uses Auto-review, the reviewer may run on the other
82 > * session's model. That only affects reviewer model choice, never
83 > * correctness of the primary turns (which are forwarded verbatim).
84 > */
85 > lastPrimaryModel: string | undefined;
86 > }
87 >
88 > /**
89 > * Model id the Codex app-server uses for its built-in auto-review reviewer
90 > * (the "Auto-review" permissions preset routes eligible approvals through it).
91 > *
92 > * This is a specialized OpenAI model that is **not** part of the GitHub
93 > * Copilot CAPI catalog, so forwarding it verbatim yields a 400
94 > * `model_not_supported`. The app-server treats that as the review having
95 > * *failed* and rejects the action inline ("Automatic approval review failed")
96 > * without ever emitting an `item/autoApprovalReview/completed` notification —
97 > * which breaks the entire Auto-review preset. We transparently remap it onto
98 > * the session's primary model (see {@link ICodexProxyState.lastPrimaryModel})
99 > * so the reviewer runs on a supported model; only the underlying model
100 > * differs, the app-server's review instructions are unchanged.
101 > */
102 > const CODEX_AUTO_REVIEW_MODEL = 'codex-auto-review';
103 >
104 > type ICodexProxyRuntime = ILoopbackProxyRuntime<ICodexProxyState>;
105 >
106 > const PROXY_USER_FACING_NAME = 'CodexProxyService';
107 >
108 > /**
109 > * User-agent prefix applied to outbound CAPI requests so the codex proxy's
110 > * traffic is identifiable server-side. Mirrors `oaiLanguageModelServer.ts`
111 > * in the Copilot Chat extension, which tags Codex requests with the same
112 > * prefix.
113 > */
114 > const USER_AGENT_PREFIX = 'vscode_codex';
115 >
116 > /**
117 > * When set to an absolute directory path, every `/v1/responses` request body
118 > * and its full upstream response stream are written to that directory as
119 > * `req-NNN-<ts>.json` and `res-NNN-<ts>.txt` so we can diff bodies / decode
120 > * SSE without flooding the log channel. Off by default.
121 > */
122 > const DEBUG_DUMP_DIR_ENV = 'VSCODE_CODEX_PROXY_DUMP_DIR';
123 >
124 > let _dumpSeq = 0;
125 function nextDumpSeq(): string {
126 return String(++_dumpSeq).padStart(4, '0');
127 }
129 > function getDumpDir(): string | undefined { codexProxyService.ts ×19
130 > const dir = process.env[DEBUG_DUMP_DIR_ENV];
131 > if (!dir) {
132 > return undefined;
133 > }
134 try {
135 fs.mkdirSync(dir, { recursive: true });
136 return dir;
137 } catch {
138 return undefined;
139 }
142 function writeJsonError(res: http.ServerResponse, status: number, type: string, message: string): void {
143 if (res.headersSent || res.writableEnded) {
144 return;
145 }
146 res.writeHead(status, { 'Content-Type': 'application/json' });
147 res.end(JSON.stringify({ error: { type, message } }));
148 }
150 > /**
151 > * Local HTTP server that speaks the OpenAI Responses API on its inbound
152 > * side and forwards to {@link ICopilotApiService.responses} on the
153 > * outbound side. The codex app-server connects via env / `--config
154 > * openai_base_url=<baseUrl>/v1` + Bearer `<nonce>` and sees this as a
155 > * real OpenAI endpoint.
156 > *
157 > * Lifecycle: refcounted handles, single shared bind, in-flight requests
158 > * aborted on teardown.
159 > */
160 > export class CodexProxyService extends LoopbackProxyServer<ICodexProxyState, string> implements ICodexProxyService {
161 >
162 > declare readonly _serviceBrand: undefined;
163 >
164 > constructor(
165 > @ILogService logService: ILogService, codexProxyService.ts ×10
166 > @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
167 > ) {
168 > super(PROXY_USER_FACING_NAME, logService);
169 > }
171 > protected createState(githubToken: string): ICodexProxyState {
172 > return { githubToken, lastPrimaryModel: undefined }; codexProxyService.ts ×10
173 > }
175 > async start(githubToken: string): Promise<ICodexProxyHandle> {
176 > const { runtime, release } = await this.acquire(githubToken); codexProxyService.ts ×10
177 > // Most recent token wins for the runtime — single-tenant assumption.
178 > // Covers concurrent callers that awaited the same bind.
179 > runtime.state.githubToken = githubToken;
180 >
181 > let disposed = false;
182 > return {
183 > baseUrl: runtime.baseUrl,
184 > nonce: runtime.nonce,
185 > setToken: (newToken: string) => {
186 if (disposed) {
187 return;
188 }
189 // Update the shared runtime's token cell. In-flight requests
190 // keep the value they captured at dispatch; new requests
191 // pick up the fresh value on `_handleResponses`.
192 runtime.state.githubToken = newToken;
193 },
194 > dispose: () => { codexProxyService.ts ×10
195 > if (disposed) {
196 return;
197 }
198 > disposed = true; codexProxyService.ts ×10
199 > release();
200 > },
201 > };
202 > }
204 > protected override async handleRequest(
205 > req: http.IncomingMessage, codexProxyService.ts ×10
206 > res: http.ServerResponse,
207 > runtime: ICodexProxyRuntime,
208 > ): Promise<void> {
209 > const method = req.method ?? 'GET';
210 > const pathname = new URL(req.url ?? '/', 'http://127.0.0.1').pathname;
211 > const incomingHeaders = Object.keys(req.headers).join(', ');
212 > this._logService.info(`[${PROXY_USER_FACING_NAME}] >>> ${method} ${pathname} (headers: ${incomingHeaders})`);
213 >
214 > if (method === 'GET' && pathname === '/') {
215 res.writeHead(200, { 'Content-Type': 'text/plain' });
216 res.end('ok');
217 return;
218 }
220 > // Codex CLI sends `Bearer <nonce>` — plain nonce, no sessionId suffix.
221 > const authHeader = req.headers['authorization'];
222 > const expected = `Bearer ${runtime.nonce}`;
223 > if (typeof authHeader !== 'string' || authHeader !== expected) {
224 writeJsonError(res, 401, 'authentication_error', 'Invalid authentication');
225 return;
226 }
228 > if (method === 'GET' && pathname === '/v1/models') {
229 > // The Codex endpoint expects its own rich `ModelsResponse` schema, not codexProxyService.ts ×1
230 > // CAPI's model shape. VS Code already owns CAPI model discovery and
231 > // supplies the selected model when starting a turn, so an empty remote
232 > // catalog keeps Codex's bundled model metadata while avoiding a noisy
233 > // refresh failure on every proxy-backed runtime start.
234 > res.writeHead(200, { 'Content-Type': 'application/json' });
235 > res.end(JSON.stringify({ models: [] }));
236 > return;
237 > }
239 > // Codex sends `/v1/responses`, `//responses` (when base_url ends in `/`),
240 > // or plain `/responses`. Accept all three.
241 > if (method === 'POST' && (pathname === '/v1/responses' || pathname === '/responses' || pathname === '//responses')) { codexProxyService.ts ×10
242 > await this._handleResponses(req, res, runtime); codexProxyService.ts ×19
243 > return;
244 > }
245
246 writeJsonError(res, 404, 'not_found_error', `No route for ${method} ${pathname}`);
249 > private async _handleResponses(
250 > req: http.IncomingMessage, codexProxyService.ts ×19
251 > res: http.ServerResponse,
252 > runtime: ICodexProxyRuntime,
253 > ): Promise<void> {
254 > let body: string;
255 > try {
256 > body = await readProxyRequestBody(req);
257 > } catch (err) {
258 writeJsonError(res, 400, 'invalid_request_error', `Failed to read request body: ${err instanceof Error ? err.message : String(err)}`);
259 return;
260 }
262 > // Remap the unsupported auto-review reviewer model onto the session's
263 > // primary model before forwarding, so the "Auto-review" preset works
264 > // against the Copilot CAPI (which does not expose `codex-auto-review`).
265 > // All downstream handling (dump, logging, forward) uses the outbound
266 > // body so logs reflect exactly what is sent upstream.
267 > const remap = remapCodexReviewerModel(body, runtime.state);
268 > if (remap.remappedFrom) {
269 > this._logService.info(`[${PROXY_USER_FACING_NAME}] remapped unsupported reviewer model '${remap.remappedFrom}' -> '${remap.remappedTo}'`); codexProxyService.ts ×1
270 > }
271 > body = remap.body; codexProxyService.ts ×19
272 >
273 > const dumpDir = getDumpDir();
274 > const dumpSeq = dumpDir ? nextDumpSeq() : undefined;
275 > if (dumpDir && dumpSeq) {
276 const reqFile = join(dumpDir, `req-${dumpSeq}-${Date.now()}.json`);
277 try {
278 fs.writeFileSync(reqFile, body);
279 this._logService.info(`[${PROXY_USER_FACING_NAME}] dumped request body to ${reqFile}`);
280 } catch (err) {
281 this._logService.warn(`[${PROXY_USER_FACING_NAME}] failed to dump request body: ${err instanceof Error ? err.message : String(err)}`);
282 }
283 }
285 > const parsed = JSON.parse(body);
286 > this._logService.info(`[${PROXY_USER_FACING_NAME}] >>> /responses body: model=${parsed.model ?? '<none>'}, previous_response_id=${parsed.previous_response_id ?? '<none>'}, stream=${parsed.stream ?? '<none>'}, input_items=${Array.isArray(parsed.input) ? parsed.input.length : '<not-array>'}`);
287 > if (Array.isArray(parsed.input)) {
288 > for (let i = 0; i < parsed.input.length; i++) {
289 const item = parsed.input[i];
290 const type = item?.type ?? '<none>';
291 const keys = item && typeof item === 'object' ? Object.keys(item).join(',') : typeof item;
292 let detail = '';
293 if (type === 'message') {
294 const text: string = item?.content?.[0]?.text ?? '';
295 detail = `role=${item?.role ?? '?'} chars=${text.length}`;
296 } else if (type === 'function_call') {
297 detail = `name=${item?.name ?? '?'} call_id=${item?.call_id ?? '?'}`;
298 } else if (type === 'function_call_output') {
299 const output = item?.output ?? '';
300 detail = `call_id=${item?.call_id ?? '?'} output_chars=${typeof output === 'string' ? output.length : 0}`;
301 } else if (type === 'reasoning') {
302 const summary = item?.summary ?? item?.content ?? '';
303 detail = `summary_chars=${typeof summary === 'string' ? summary.length : JSON.stringify(summary).length} encrypted=${typeof item?.encrypted_content === 'string'}`;
304 } else {
305 detail = JSON.stringify(item).slice(0, 120);
306 }
307 this._logService.info(`[${PROXY_USER_FACING_NAME}] input[${i}] type=${type} keys=[${keys}] ${detail}`);
308 }
310 > const topLevelKeys = Object.keys(parsed).filter(k => k !== 'input').sort();
311 > this._logService.info(`[${PROXY_USER_FACING_NAME}] top-level keys (excl. input)=[${topLevelKeys.join(', ')}]`);
312 > for (const k of topLevelKeys) {
313 > if (k === 'instructions' || k === 'tools') {
314 const v = parsed[k];
315 const size = typeof v === 'string' ? v.length : JSON.stringify(v).length;
316 this._logService.info(`[${PROXY_USER_FACING_NAME}] ${k}=<${size} chars elided>`);
317 continue;
318 }
319 > const v = parsed[k]; codexProxyService.ts ×19
320 > const preview = typeof v === 'object' ? JSON.stringify(v).slice(0, 300) : String(v);
321 > this._logService.info(`[${PROXY_USER_FACING_NAME}] ${k}=${preview}`);
322 > }
323 > } catch {
324 this._logService.info(`[${PROXY_USER_FACING_NAME}] >>> /responses body (unparseable): ${body.slice(0, 200)}`);
325 }
327 > const entry: IProxyInFlight = { ac: new AbortController(), res, clientGone: false };
328 > runtime.inFlight.add(entry);
329 > const onClose = () => {
330 entry.clientGone = true;
331 entry.ac.abort();
332 };
333 > res.on('close', onClose); codexProxyService.ts ×19
334 >
335 > // Snapshot the token at dispatch time so an in-flight request keeps
336 > // using the value it started with; subsequent requests will pick up
337 > // whatever `runtime.state.githubToken` has been rotated to.
338 > const dispatchedToken = runtime.state.githubToken;
339 >
340 > const headers = buildOutboundHeaders(req.headers);
341 >
342 > try {
343 > this._logService.info(`[${PROXY_USER_FACING_NAME}] forwarding to CAPI responses...`);
344 > const upstream = await this._copilotApiService.responses(dispatchedToken, body, { headers, signal: entry.ac.signal, suppressIntegrationId: true });
345 > const contentType = upstream.headers.get('content-type') ?? 'application/json';
346 > const upstreamHeaders = [...upstream.headers.entries()].map(([k, v]) => `${k}: ${v}`).join(', ');
347 > this._logService.info(`[${PROXY_USER_FACING_NAME}] <<< CAPI response: status=${upstream.status}, contentType=${contentType}, headers=[${upstreamHeaders}]`);
348 > res.writeHead(upstream.status, { 'Content-Type': contentType });
349 > if (!upstream.body) {
350 res.end();
351 return;
352 }
353 > const reader = upstream.body.getReader(); codexProxyService.ts ×19
354 > const resDumpStream = dumpDir && dumpSeq
355 ? fs.createWriteStream(join(dumpDir, `res-${dumpSeq}-${Date.now()}.txt`))
356 > : undefined; codexProxyService.ts ×19
357 > let sseBuf = '';
358 > const eventCounts: Record<string, number> = {};
359 > try {
360 > while (true) {
361 > const { done, value } = await reader.read();
362 > if (done) {
363 > break;
364 > }
365 > if (entry.clientGone) {
366 break;
367 }
368 > if (value && value.byteLength > 0) { codexProxyService.ts ×19
369 > const buf = Buffer.from(value);
370 > res.write(buf);
371 > if (resDumpStream) {
372 resDumpStream.write(buf);
373 }
374 > sseBuf += buf.toString('utf8'); codexProxyService.ts ×19
375 > let nl: number;
376 > while ((nl = sseBuf.indexOf('\n')) >= 0) {
377 > const line = sseBuf.slice(0, nl).trimEnd();
378 > sseBuf = sseBuf.slice(nl + 1);
379 > if (line.startsWith('event:')) {
380 > const ev = line.slice('event:'.length).trim();
381 > eventCounts[ev] = (eventCounts[ev] ?? 0) + 1;
382 > }
383 > }
384 > }
385 > }
386 > } finally {
387 > try { reader.releaseLock(); } catch { /* ignore */ }
388 > resDumpStream?.end();
389 > }
390 > if (Object.keys(eventCounts).length) {
391 > const summary = Object.entries(eventCounts).map(([k, v]) => `${k}=${v}`).join(', ');
392 > this._logService.info(`[${PROXY_USER_FACING_NAME}] <<< SSE event counts: ${summary}`);
393 > }
394 > res.end();
395 > } catch (err) {
396 if (entry.clientGone) {
397 this._logService.info(`[${PROXY_USER_FACING_NAME}] client disconnected during upstream call`);
398 return;
399 }
400 if (err instanceof CopilotApiError) {
401 this._logService.error(`[${PROXY_USER_FACING_NAME}] CAPI error: status=${err.status}, message=${err.message}`);
402 const marker = encodeForwardedChatError(buildForwardedChatError(err));
403 writeJsonError(res, err.status, 'api_error', `${err.message} ${marker}`);
404 return;
405 }
406 this._logService.error(`[${PROXY_USER_FACING_NAME}] upstream error: ${err instanceof Error ? err.message : String(err)}`);
407 writeJsonError(res, 502, 'api_error', err instanceof Error ? err.message : String(err));
408 > } finally { codexProxyService.ts ×19
409 > res.removeListener('close', onClose);
410 > runtime.inFlight.delete(entry);
411 > }
412 > }
414 >
415 > /**
416 > * Compute the outbound `/v1/responses` body, transparently remapping the
417 > * unsupported Codex auto-review reviewer model (see
418 > * {@link CODEX_AUTO_REVIEW_MODEL}) onto the last-seen primary model. Records
419 > * the primary model on `state` as a side effect so a later reviewer request
420 > * can be remapped.
421 > *
422 > * Returns the original body untouched — and forwards verbatim, exactly as
423 > * before — when it is unparseable, carries no `model`, already uses a primary
424 > * model, or when no primary model has been observed yet (graceful
425 > * degradation: the reviewer request still 400s, i.e. no worse than not
426 > * remapping at all).
427 > */
428 > export function remapCodexReviewerModel(
429 > body: string, codexProxyService.ts ×2
430 > state: { lastPrimaryModel: string | undefined },
431 > ): { readonly body: string; readonly remappedFrom?: string; readonly remappedTo?: string } {
432 > let parsed: { model?: unknown };
433 > try {
434 > parsed = JSON.parse(body);
435 > } catch {
436 > return { body }; codexProxyService.ts ×2
437 > }
438 > const model = typeof parsed.model === 'string' ? parsed.model : undefined; codexProxyService.ts ×2
439 > if (!model) {
440 > return { body }; codexProxyService.ts ×2
441 > }
442 > if (model !== CODEX_AUTO_REVIEW_MODEL) { codexProxyService.ts ×1
443 > // A normal turn request — remember its model so we can substitute it codexProxyService.ts ×1
444 > // for a subsequent reviewer request.
445 > state.lastPrimaryModel = model;
446 > return { body };
447 > }
448 > const target = state.lastPrimaryModel; codexProxyService.ts ×1
449 > if (!target) {
450 > return { body }; codexProxyService.ts ×1
451 > }
452 > (parsed as { model: string }).model = target; codexProxyService.ts ×1
453 > return { body: JSON.stringify(parsed), remappedFrom: model, remappedTo: target };
454 > }
456 >
457 > function buildOutboundHeaders(inbound: http.IncomingHttpHeaders): Record<string, string> { codexProxyService.ts ×19
458 > const out: Record<string, string> = {};
459 > const userAgent = inbound['user-agent'];
460 > if (typeof userAgent === 'string' && userAgent.length > 0) {
461 > out['User-Agent'] = transformUserAgent(userAgent); codexProxyService.ts ×3
462 > }
463 > return out; codexProxyService.ts ×19
464 > }
466 > /**
467 > * Transform an incoming user-agent string by replacing the client name portion
468 > * (before the first `/`) with {@link USER_AGENT_PREFIX}. This mirrors the
469 > * transform in `oaiLanguageModelServer.ts` in the Copilot Chat extension,
470 > * ensuring all Codex requests are tagged with a consistent prefix for
471 > * server-side identification.
472 > *
473 > * Examples:
474 > * - `codex/1.2.3` → `vscode_codex/1.2.3`
475 > * - `OpenAI/Python/1.0` → `vscode_codex/Python/1.0`
476 > * - `unknown` → `vscode_codex/unknown`
477 > */
478 > function transformUserAgent(userAgent: string): string { codexProxyService.ts ×3
479 > const slashIndex = userAgent.indexOf('/');
480 > if (slashIndex === -1) {
481 return `${USER_AGENT_PREFIX}/${userAgent}`;
482 }
483 > return `${USER_AGENT_PREFIX}${userAgent.substring(slashIndex)}`; codexProxyService.ts ×3
484 > }