codexProxyService.ts ×11

Frontier kind: Code frontier

unlabeled · c_a19c754d474d

23 tests · 20404 LOC · 85 files · introduces 0 tests · 181 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
11 ranges181 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
1586 ranges20404 lines · 85 files · Browse complete extent
All tests (intent)
23 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

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 native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 181 introduced LOC across 11 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/platform/agentHost/node/codex/codexProxyService.ts 181 introduced LOC · 11 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- codexProxyService.ts
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 {
130 const dir = process.env[DEBUG_DUMP_DIR_ENV];
139 }
140 }
142 function writeJsonError(res: http.ServerResponse, status: number, type: string, message: string): void {
143 if (res.headersSent || res.writableEnded) {
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,
166 @ICopilotApiService private readonly _copilotApiService: ICopilotApiService,
168 super(PROXY_USER_FACING_NAME, logService);
169 }
171 > protected createState(githubToken: string): ICodexProxyState {
172 return { githubToken, lastPrimaryModel: undefined };
173 }
175 > async start(githubToken: string): Promise<ICodexProxyHandle> {
176 const { runtime, release } = await this.acquire(githubToken);
177 // Most recent token wins for the runtime — single-tenant assumption.
201 };
202 }
204 > protected override async handleRequest(
205 req: http.IncomingMessage,
206 res: http.ServerResponse,
246 writeJsonError(res, 404, 'not_found_error', `No route for ${method} ${pathname}`);
247 }
249 > private async _handleResponses(
250 req: http.IncomingMessage,
251 res: http.ServerResponse,
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,
430 state: { lastPrimaryModel: string | undefined },
453 return { body: JSON.stringify(parsed), remappedFrom: model, remappedTo: target };
454 }
456 >
457 function buildOutboundHeaders(inbound: http.IncomingHttpHeaders): Record<string, string> {
458 const out: Record<string, string> = {};
463 return out;
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 {
479 const slashIndex = userAgent.indexOf('/');