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];