codex-copilot-dx 0.4.34 → 0.4.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/bin/cli.mjs +11 -7
- package/package.json +3 -2
- package/scripts/benchmark.mjs +87 -11
- package/src/adapter.mjs +115 -837
- package/src/anthropic-handler.mjs +128 -0
- package/src/anthropic.mjs +5 -5
- package/src/auth.mjs +12 -35
- package/src/claude-config.mjs +7 -6
- package/src/config.mjs +11 -10
- package/src/copilot.mjs +38 -29
- package/src/doctor.mjs +18 -8
- package/src/github-identity.mjs +30 -0
- package/src/http-transport.mjs +154 -9
- package/src/image-optimization.mjs +24 -6
- package/src/models.mjs +13 -0
- package/src/observability.mjs +147 -0
- package/src/request-context.mjs +16 -0
- package/src/response-history.mjs +8 -11
- package/src/responses-bridge.mjs +282 -0
- package/src/responses-handler.mjs +176 -0
- package/src/responses-proxy.mjs +122 -0
- package/src/responses-request.mjs +202 -0
- package/src/runtime-config.mjs +38 -0
- package/src/status.mjs +5 -1
- package/src/stream-errors.mjs +17 -0
- package/src/stream.mjs +2 -7
package/src/adapter.mjs
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import {
|
|
3
|
-
import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact
|
|
4
|
-
import { webStreamLines } from "./stream.mjs";
|
|
5
|
-
import { anthropicToChat, chatToAnthropic, streamAnthropicFromLines, countTokens } from "./anthropic.mjs";
|
|
6
|
-
import { CODEX_AUTO_REVIEW_MODEL, claudeDesktopModelsResponse, resolveAnthropicModel, resolveOpenAIModel, modelIsResponsesOnly, modelSupportsChatCompletions } from "./models.mjs";
|
|
2
|
+
import { createAnthropicCountTokensHandler, createAnthropicMessagesHandler } from "./anthropic-handler.mjs";
|
|
3
|
+
import { chatCompletions, listModels, responses as copilotResponses, responsesCompact as copilotResponsesCompact } from "./copilot.mjs";
|
|
7
4
|
import { isValidModelList } from "./model-cache.mjs";
|
|
5
|
+
import { claudeDesktopModelsResponse } from "./models.mjs";
|
|
6
|
+
import {
|
|
7
|
+
ADAPTER_STATUS_PATH,
|
|
8
|
+
classifyAdapterRoute,
|
|
9
|
+
createRequestMetrics,
|
|
10
|
+
isLoopbackAddress,
|
|
11
|
+
runtimeStatusPayload,
|
|
12
|
+
} from "./observability.mjs";
|
|
13
|
+
import { createRequestId, runWithRequestContext } from "./request-context.mjs";
|
|
8
14
|
import { status } from "./status.mjs";
|
|
9
|
-
import { recordAnthropicUsage, recordResponsesUsage } from "./usage.mjs";
|
|
10
15
|
import { ADAPTER_HEALTH_PATH, adapterHealthPayload } from "./running-adapter.mjs";
|
|
16
|
+
import { createResponsesCompactHandler, createResponsesHandler } from "./responses-handler.mjs";
|
|
17
|
+
import { loadRuntimeConfig, parsePositiveInteger } from "./runtime-config.mjs";
|
|
11
18
|
import {
|
|
12
|
-
|
|
19
|
+
createRequestAdmission,
|
|
13
20
|
createRequestAbort,
|
|
14
21
|
httpError,
|
|
15
|
-
isAbortLikeError,
|
|
16
22
|
logRequestFailure,
|
|
17
|
-
readJsonBody,
|
|
18
23
|
sendJsonError,
|
|
19
|
-
sendUpstreamError,
|
|
20
|
-
writeOrDrain,
|
|
21
24
|
} from "./http-transport.mjs";
|
|
22
|
-
import {
|
|
23
|
-
clearResponseHistoryForTests,
|
|
24
|
-
configureResponseHistoryForTests,
|
|
25
|
-
materializeResponseHistory,
|
|
26
|
-
rememberResponseHistoryNode,
|
|
27
|
-
responseHistoryStats,
|
|
28
|
-
} from "./response-history.mjs";
|
|
29
25
|
|
|
30
26
|
export {
|
|
31
27
|
abortErrorStatusCode,
|
|
28
|
+
createRequestAdmission,
|
|
32
29
|
createRequestAbort,
|
|
33
30
|
isAbortLikeError,
|
|
34
31
|
readJsonBody,
|
|
@@ -39,57 +36,24 @@ export {
|
|
|
39
36
|
configureResponseHistoryForTests,
|
|
40
37
|
responseHistoryStats,
|
|
41
38
|
} from "./response-history.mjs";
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Decide whether a model must be proxied straight to /responses (vs converted to
|
|
57
|
-
// /chat/completions). Prefer real endpoint metadata; fall back to the static list.
|
|
58
|
-
function isResponsesOnlyModel(model) {
|
|
59
|
-
const endpoints = getCachedModelEndpoints(model);
|
|
60
|
-
if (endpoints) {
|
|
61
|
-
const fakeModel = { supported_endpoints: endpoints };
|
|
62
|
-
if (modelSupportsChatCompletions(fakeModel)) return false;
|
|
63
|
-
if (modelIsResponsesOnly(fakeModel)) return true;
|
|
64
|
-
}
|
|
65
|
-
return RESPONSES_ONLY_FALLBACK.has(model);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Direct Copilot Responses API proxy.
|
|
69
|
-
|
|
70
|
-
const DEFAULT_UPSTREAM_TIMEOUT_MS = 120 * 1000;
|
|
71
|
-
const DEFAULT_STREAM_HANDSHAKE_TIMEOUT_MS = 120 * 1000;
|
|
72
|
-
const DEFAULT_STREAM_IDLE_TIMEOUT_MS = 120 * 1000;
|
|
73
|
-
const DEFAULT_MAX_SSE_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
74
|
-
|
|
75
|
-
function positiveInt(value, fallback) {
|
|
76
|
-
const n = Number.parseInt(value, 10);
|
|
77
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const UPSTREAM_TIMEOUT_MS = positiveInt(process.env.CCDX_UPSTREAM_TIMEOUT_MS, DEFAULT_UPSTREAM_TIMEOUT_MS);
|
|
81
|
-
const STREAM_HANDSHAKE_TIMEOUT_MS = positiveInt(process.env.CCDX_STREAM_HANDSHAKE_TIMEOUT_MS, DEFAULT_STREAM_HANDSHAKE_TIMEOUT_MS);
|
|
82
|
-
const STREAM_IDLE_TIMEOUT_MS = positiveInt(process.env.CCDX_STREAM_IDLE_TIMEOUT_MS, DEFAULT_STREAM_IDLE_TIMEOUT_MS);
|
|
83
|
-
const MAX_SSE_BUFFER_BYTES = positiveInt(process.env.CCDX_MAX_SSE_BUFFER_BYTES, DEFAULT_MAX_SSE_BUFFER_BYTES);
|
|
39
|
+
export { forwardToChat, responsesToChat } from "./responses-bridge.mjs";
|
|
40
|
+
export {
|
|
41
|
+
isEncryptedContentVerificationError,
|
|
42
|
+
isImageNamespaceCollisionError,
|
|
43
|
+
openCopilotResponse,
|
|
44
|
+
prepareResponsesRequest,
|
|
45
|
+
rememberResponseHistory,
|
|
46
|
+
sanitizeEncryptedReasoningRequest,
|
|
47
|
+
sanitizeImageNamespaceCollisionRequest,
|
|
48
|
+
stripInternalResponsesInputFields,
|
|
49
|
+
} from "./responses-request.mjs";
|
|
50
|
+
|
|
51
|
+
const ADAPTER_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
84
52
|
|
|
85
53
|
export function requestPath(reqUrl) {
|
|
86
54
|
return new URL(reqUrl || "/", "http://localhost").pathname;
|
|
87
55
|
}
|
|
88
56
|
|
|
89
|
-
function cloneJson(value) {
|
|
90
|
-
return value === undefined ? undefined : structuredClone(value);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
57
|
function bearerToken(headers = {}) {
|
|
94
58
|
const value = headers.authorization || headers.Authorization || "";
|
|
95
59
|
const match = /^Bearer\s+(.+)$/i.exec(String(value));
|
|
@@ -117,597 +81,6 @@ function sendLastKnownGoodModels(res, modelRegistry) {
|
|
|
117
81
|
return true;
|
|
118
82
|
}
|
|
119
83
|
|
|
120
|
-
function responsesInputItems(input, { clone = true } = {}) {
|
|
121
|
-
if (input === undefined || input === null) return [];
|
|
122
|
-
if (typeof input === "string") {
|
|
123
|
-
return [{ type: "message", role: "user", content: [{ type: "input_text", text: input }] }];
|
|
124
|
-
}
|
|
125
|
-
if (Array.isArray(input)) return clone ? cloneJson(input) : input;
|
|
126
|
-
return [clone ? cloneJson(input) : input];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function stripInternalResponsesInputFields(inputItems) {
|
|
130
|
-
if (!Array.isArray(inputItems)) return inputItems;
|
|
131
|
-
for (const item of inputItems) {
|
|
132
|
-
if (!item || typeof item !== "object") continue;
|
|
133
|
-
for (const key of Object.keys(item)) {
|
|
134
|
-
if (key.startsWith("internal_")) delete item[key];
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
return inputItems;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function responsesOutputItems(output) {
|
|
141
|
-
if (!Array.isArray(output)) return [];
|
|
142
|
-
return output.filter((item) => item?.type === "message" || item?.type === "function_call");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function stripEncryptedReasoningValue(value, state) {
|
|
146
|
-
if (Array.isArray(value)) {
|
|
147
|
-
return value.map((item) => stripEncryptedReasoningValue(item, state));
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (value && typeof value === "object") {
|
|
151
|
-
const out = {};
|
|
152
|
-
for (const [key, child] of Object.entries(value)) {
|
|
153
|
-
if (key === "encrypted_content") {
|
|
154
|
-
state.changed = true;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
out[key] = stripEncryptedReasoningValue(child, state);
|
|
158
|
-
}
|
|
159
|
-
return out;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
return value;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function isEncryptedReasoningInputItem(item) {
|
|
166
|
-
return item && typeof item === "object"
|
|
167
|
-
&& (item.type === "reasoning" || Object.prototype.hasOwnProperty.call(item, "encrypted_content"));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
171
|
-
const state = { changed: false };
|
|
172
|
-
let body = cloneJson(reqContext.body);
|
|
173
|
-
if (Array.isArray(body.input)) {
|
|
174
|
-
const input = [];
|
|
175
|
-
for (const item of body.input) {
|
|
176
|
-
if (isEncryptedReasoningInputItem(item)) {
|
|
177
|
-
state.changed = true;
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
input.push(stripEncryptedReasoningValue(item, state));
|
|
181
|
-
}
|
|
182
|
-
body.input = input;
|
|
183
|
-
} else {
|
|
184
|
-
body = stripEncryptedReasoningValue(body, state);
|
|
185
|
-
}
|
|
186
|
-
if (!state.changed) return null;
|
|
187
|
-
const historyInputItems = Array.isArray(reqContext.historyInputItems)
|
|
188
|
-
? reqContext.historyInputItems
|
|
189
|
-
.filter((item) => !isEncryptedReasoningInputItem(item))
|
|
190
|
-
.map((item) => stripEncryptedReasoningValue(item, { changed: false }))
|
|
191
|
-
: reqContext.historyInputItems;
|
|
192
|
-
return {
|
|
193
|
-
...reqContext,
|
|
194
|
-
body,
|
|
195
|
-
inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
|
|
196
|
-
historyInputItems,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export function isEncryptedContentVerificationError(statusCode, text) {
|
|
201
|
-
if (statusCode < 400 || !text) return false;
|
|
202
|
-
const lower = String(text).toLowerCase();
|
|
203
|
-
return lower.includes("encrypted content")
|
|
204
|
-
&& lower.includes("could not be verified")
|
|
205
|
-
&& (lower.includes("could not be decrypted") || lower.includes("could not be parsed"));
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function isImageNamespaceCollisionError(statusCode, text) {
|
|
209
|
-
if (statusCode < 400 || !text) return false;
|
|
210
|
-
const lower = String(text).toLowerCase();
|
|
211
|
-
return lower.includes("namespace")
|
|
212
|
-
&& lower.includes("image_gen")
|
|
213
|
-
&& lower.includes("collid");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isImageNamespaceTool(tool, { collisionFallback = false } = {}) {
|
|
217
|
-
if (!tool || typeof tool !== "object") return false;
|
|
218
|
-
const type = String(tool.type || "").toLowerCase();
|
|
219
|
-
const name = String(tool.name || tool.function?.name || "").toLowerCase();
|
|
220
|
-
const namespace = String(tool.namespace || "").toLowerCase();
|
|
221
|
-
if (["image_gen", "image_generation"].includes(type)) return true;
|
|
222
|
-
if (["image_gen", "image_generation"].includes(name)) return true;
|
|
223
|
-
if (namespace === "image_gen" || namespace === "image_generation") return true;
|
|
224
|
-
return collisionFallback && [type, name, namespace].some((value) => value.startsWith("image_gen"));
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export function sanitizeImageNamespaceCollisionRequest(reqContext) {
|
|
228
|
-
if (!Array.isArray(reqContext?.body?.tools)) return null;
|
|
229
|
-
const body = cloneJson(reqContext.body);
|
|
230
|
-
const filtered = body.tools.filter((tool) => !isImageNamespaceTool(tool, { collisionFallback: true }));
|
|
231
|
-
if (filtered.length === body.tools.length) return null;
|
|
232
|
-
if (filtered.length) body.tools = filtered;
|
|
233
|
-
else delete body.tools;
|
|
234
|
-
return { ...reqContext, body };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
|
|
238
|
-
let encryptedRetried = false;
|
|
239
|
-
let imageNamespaceRetried = false;
|
|
240
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
241
|
-
const resp = await upstream(reqContext.body, { signal: options.signal });
|
|
242
|
-
if (resp.ok) return { resp, reqContext };
|
|
243
|
-
|
|
244
|
-
const errorText = await resp.text();
|
|
245
|
-
if (!imageNamespaceRetried && isImageNamespaceCollisionError(resp.status, errorText)) {
|
|
246
|
-
const retryContext = sanitizeImageNamespaceCollisionRequest(reqContext);
|
|
247
|
-
if (retryContext) {
|
|
248
|
-
imageNamespaceRetried = true;
|
|
249
|
-
reqContext = retryContext;
|
|
250
|
-
console.warn(status("warn", "image_gen namespace rejected by upstream; retrying without the conflicting image tool"));
|
|
251
|
-
continue;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
if (!encryptedRetried && isEncryptedContentVerificationError(resp.status, errorText)) {
|
|
255
|
-
const retryContext = sanitizeEncryptedReasoningRequest(reqContext);
|
|
256
|
-
if (retryContext) {
|
|
257
|
-
encryptedRetried = true;
|
|
258
|
-
reqContext = retryContext;
|
|
259
|
-
console.warn(status("warn", "encrypted reasoning rejected by upstream; retrying without encrypted reasoning"));
|
|
260
|
-
continue;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
return { resp, reqContext, errorText };
|
|
264
|
-
}
|
|
265
|
-
throw httpError("Responses compatibility retry limit exceeded", 502);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// The GPT (gpt-5.6) Responses backend registers a built-in `image_gen` tool
|
|
269
|
-
// namespace. When the client also sends an image generation tool, the upstream
|
|
270
|
-
// rejects the request with "User-defined namespace 'image_gen' collides ...".
|
|
271
|
-
// Strip exact built-in image tools before proxying. Broader namespace variants
|
|
272
|
-
// are removed only after the upstream explicitly reports a collision.
|
|
273
|
-
function isBuiltinImageTool(tool) {
|
|
274
|
-
return isImageNamespaceTool(tool);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
278
|
-
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
279
|
-
const currentInputItems = responsesInputItems(body.input, { clone: !mutate });
|
|
280
|
-
const previousId = body.previous_response_id;
|
|
281
|
-
|
|
282
|
-
if (previousId !== undefined && previousId !== null) {
|
|
283
|
-
body.input = [...materializeResponseHistory(previousId), ...currentInputItems];
|
|
284
|
-
} else {
|
|
285
|
-
body.input = currentInputItems;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
delete body.previous_response_id;
|
|
289
|
-
delete body.store;
|
|
290
|
-
if (Array.isArray(body.tools)) {
|
|
291
|
-
body.tools = body.tools.filter((tool) => !isBuiltinImageTool(tool));
|
|
292
|
-
if (!body.tools.length) delete body.tools;
|
|
293
|
-
}
|
|
294
|
-
stripInternalResponsesInputFields(body.input);
|
|
295
|
-
stripInternalResponsesInputFields(currentInputItems);
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
body,
|
|
299
|
-
inputItems: body.input,
|
|
300
|
-
historyParentId: previousId ?? null,
|
|
301
|
-
historyInputItems: currentInputItems,
|
|
302
|
-
takeHistoryOwnership: mutate,
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
export function rememberResponseHistory(reqContext, responseJson) {
|
|
307
|
-
if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
|
|
308
|
-
const sourceInputItems = reqContext.historyInputItems || reqContext.inputItems;
|
|
309
|
-
const sourceOutputItems = responsesOutputItems(responseJson.output);
|
|
310
|
-
rememberResponseHistoryNode({
|
|
311
|
-
id: responseJson.id,
|
|
312
|
-
parentId: reqContext.historyParentId,
|
|
313
|
-
inputItems: sourceInputItems,
|
|
314
|
-
outputItems: sourceOutputItems,
|
|
315
|
-
takeOwnership: reqContext.takeHistoryOwnership,
|
|
316
|
-
});
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function storeCompletedResponseFromSse(reqContext, data) {
|
|
320
|
-
if (!data || data === "[DONE]") return;
|
|
321
|
-
try {
|
|
322
|
-
const event = JSON.parse(data);
|
|
323
|
-
const response = event.response;
|
|
324
|
-
if (response?.id && response.status === "completed") {
|
|
325
|
-
rememberResponseHistory(reqContext, response);
|
|
326
|
-
recordResponsesUsage({
|
|
327
|
-
surface: reqContext.surface,
|
|
328
|
-
mode: "stream",
|
|
329
|
-
model: reqContext.body?.model,
|
|
330
|
-
response,
|
|
331
|
-
event,
|
|
332
|
-
});
|
|
333
|
-
}
|
|
334
|
-
} catch {
|
|
335
|
-
// Ignore non-JSON stream fragments.
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
function readSseEvents(buffer, onData) {
|
|
340
|
-
while (true) {
|
|
341
|
-
const match = buffer.match(/\r?\n\r?\n/);
|
|
342
|
-
if (!match) return buffer;
|
|
343
|
-
const chunk = buffer.slice(0, match.index);
|
|
344
|
-
if (Buffer.byteLength(chunk) > MAX_SSE_BUFFER_BYTES) {
|
|
345
|
-
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
346
|
-
}
|
|
347
|
-
buffer = buffer.slice(match.index + match[0].length);
|
|
348
|
-
const data = chunk
|
|
349
|
-
.split(/\r?\n/)
|
|
350
|
-
.filter((line) => line.startsWith("data:"))
|
|
351
|
-
.map((line) => line.slice(5).trimStart())
|
|
352
|
-
.join("\n");
|
|
353
|
-
if (data) onData(data);
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
function streamErrorData(protocol, error, abort) {
|
|
358
|
-
const code = abort?.reason || "upstream_stream_error";
|
|
359
|
-
const message = `${code}: ${error?.message || "Upstream stream failed"}`;
|
|
360
|
-
if (protocol === "anthropic") {
|
|
361
|
-
return { type: "error", error: { type: "api_error", message } };
|
|
362
|
-
}
|
|
363
|
-
return { type: "error", code, message, param: null };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
async function endStreamWithError(res, protocol, error, abort) {
|
|
367
|
-
if (res.destroyed || res.writableEnded) return;
|
|
368
|
-
const data = streamErrorData(protocol, error, abort);
|
|
369
|
-
await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`).catch(() => false);
|
|
370
|
-
if (!res.destroyed && !res.writableEnded) res.end();
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
|
|
374
|
-
const opened = await openCopilotResponse(reqContext, upstream, options);
|
|
375
|
-
const { resp, errorText } = opened;
|
|
376
|
-
reqContext = opened.reqContext;
|
|
377
|
-
if (errorText !== undefined) {
|
|
378
|
-
sendUpstreamError(res, resp, errorText);
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
if (reqContext.body.stream) {
|
|
383
|
-
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
384
|
-
res.writeHead(resp.status, {
|
|
385
|
-
"Content-Type": resp.headers.get("content-type") || "text/event-stream",
|
|
386
|
-
"Cache-Control": "no-cache",
|
|
387
|
-
Connection: "keep-alive",
|
|
388
|
-
});
|
|
389
|
-
const reader = resp.body.getReader();
|
|
390
|
-
const decoder = new TextDecoder();
|
|
391
|
-
let buffer = "";
|
|
392
|
-
try {
|
|
393
|
-
while (true) {
|
|
394
|
-
const { done, value } = await reader.read();
|
|
395
|
-
if (done) {
|
|
396
|
-
buffer += decoder.decode();
|
|
397
|
-
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
398
|
-
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
399
|
-
}
|
|
400
|
-
readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
401
|
-
res.end();
|
|
402
|
-
return;
|
|
403
|
-
}
|
|
404
|
-
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
405
|
-
buffer += decoder.decode(value, { stream: true });
|
|
406
|
-
buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
407
|
-
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
408
|
-
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
409
|
-
}
|
|
410
|
-
if (!await writeOrDrain(res, value)) return;
|
|
411
|
-
}
|
|
412
|
-
} catch (e) {
|
|
413
|
-
logRequestFailure("Responses", e, options.abort);
|
|
414
|
-
await endStreamWithError(res, "responses", e, options.abort);
|
|
415
|
-
} finally {
|
|
416
|
-
await reader.cancel().catch(() => {});
|
|
417
|
-
reader.releaseLock();
|
|
418
|
-
}
|
|
419
|
-
} else {
|
|
420
|
-
const data = await resp.text();
|
|
421
|
-
res.writeHead(resp.status, { "Content-Type": "application/json" });
|
|
422
|
-
res.end(data);
|
|
423
|
-
if (resp.ok) {
|
|
424
|
-
try {
|
|
425
|
-
const response = JSON.parse(data);
|
|
426
|
-
rememberResponseHistory(reqContext, response);
|
|
427
|
-
recordResponsesUsage({
|
|
428
|
-
surface: reqContext.surface,
|
|
429
|
-
mode: "json",
|
|
430
|
-
model: reqContext.body?.model,
|
|
431
|
-
response,
|
|
432
|
-
event: response,
|
|
433
|
-
});
|
|
434
|
-
} catch {}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Chat Completions conversion for older models.
|
|
440
|
-
|
|
441
|
-
export function responsesToChat(body) {
|
|
442
|
-
const messages = [];
|
|
443
|
-
if (body.instructions) messages.push({ role: "system", content: body.instructions });
|
|
444
|
-
|
|
445
|
-
const messageContent = (content) => {
|
|
446
|
-
if (typeof content === "string") return content;
|
|
447
|
-
if (!Array.isArray(content)) return JSON.stringify(content);
|
|
448
|
-
const parts = [];
|
|
449
|
-
for (const part of content) {
|
|
450
|
-
if (!part || typeof part !== "object") continue;
|
|
451
|
-
if (["input_text", "output_text", "text"].includes(part.type)) {
|
|
452
|
-
parts.push({ type: "text", text: String(part.text || "") });
|
|
453
|
-
} else if (part.type === "input_image" || part.type === "image_url") {
|
|
454
|
-
const raw = part.image_url ?? part.url;
|
|
455
|
-
const imageUrl = typeof raw === "string"
|
|
456
|
-
? { url: raw }
|
|
457
|
-
: raw && typeof raw === "object" ? cloneJson(raw) : null;
|
|
458
|
-
if (imageUrl && part.detail !== undefined && imageUrl.detail === undefined) imageUrl.detail = part.detail;
|
|
459
|
-
if (imageUrl?.url) parts.push({ type: "image_url", image_url: imageUrl });
|
|
460
|
-
} else {
|
|
461
|
-
parts.push({ type: "text", text: JSON.stringify(part) });
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
465
|
-
return parts;
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
if (typeof body.input === "string") {
|
|
469
|
-
messages.push({ role: "user", content: body.input });
|
|
470
|
-
} else if (Array.isArray(body.input)) {
|
|
471
|
-
for (const item of body.input) {
|
|
472
|
-
if (item.type === "message") {
|
|
473
|
-
messages.push({ role: item.role, content: messageContent(item.content) });
|
|
474
|
-
} else if (item.type === "function_call") {
|
|
475
|
-
messages.push({
|
|
476
|
-
role: "assistant",
|
|
477
|
-
content: null,
|
|
478
|
-
tool_calls: [{ id: item.call_id || randomUUID(), type: "function", function: { name: item.name, arguments: item.arguments } }],
|
|
479
|
-
});
|
|
480
|
-
} else if (item.type === "function_call_output") {
|
|
481
|
-
messages.push({ role: "tool", tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) });
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const chatReq = { model: body.model, messages, stream: true };
|
|
487
|
-
for (const k of ["temperature", "top_p", "stop", "presence_penalty", "frequency_penalty"]) {
|
|
488
|
-
if (body[k] !== undefined) chatReq[k] = body[k];
|
|
489
|
-
}
|
|
490
|
-
const maxTok = body.max_output_tokens ?? body.max_tokens ?? body.max_completion_tokens;
|
|
491
|
-
if (maxTok !== undefined) chatReq.max_completion_tokens = maxTok;
|
|
492
|
-
|
|
493
|
-
if (body.tools?.length) {
|
|
494
|
-
chatReq.tools = body.tools
|
|
495
|
-
.map((tool) => {
|
|
496
|
-
if (tool?.type !== "function") return null;
|
|
497
|
-
if (tool.function?.name) return cloneJson(tool);
|
|
498
|
-
const fn = {
|
|
499
|
-
name: tool.name,
|
|
500
|
-
description: tool.description,
|
|
501
|
-
parameters: tool.parameters,
|
|
502
|
-
strict: tool.strict,
|
|
503
|
-
};
|
|
504
|
-
for (const key of Object.keys(fn)) if (fn[key] === undefined) delete fn[key];
|
|
505
|
-
return { type: "function", function: fn };
|
|
506
|
-
})
|
|
507
|
-
.filter(Boolean)
|
|
508
|
-
.filter((t) => t.function?.name);
|
|
509
|
-
if (!chatReq.tools.length) delete chatReq.tools;
|
|
510
|
-
}
|
|
511
|
-
if (body.tool_choice !== undefined) {
|
|
512
|
-
if (typeof body.tool_choice === "string") {
|
|
513
|
-
chatReq.tool_choice = body.tool_choice;
|
|
514
|
-
} else if (body.tool_choice?.type === "function" && body.tool_choice.name) {
|
|
515
|
-
chatReq.tool_choice = { type: "function", function: { name: body.tool_choice.name } };
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
if (body.parallel_tool_calls !== undefined) chatReq.parallel_tool_calls = body.parallel_tool_calls;
|
|
519
|
-
const textFormat = body.text?.format;
|
|
520
|
-
if (textFormat?.type === "json_schema") {
|
|
521
|
-
chatReq.response_format = {
|
|
522
|
-
type: "json_schema",
|
|
523
|
-
json_schema: Object.fromEntries(Object.entries({
|
|
524
|
-
name: textFormat.name,
|
|
525
|
-
description: textFormat.description,
|
|
526
|
-
schema: textFormat.schema,
|
|
527
|
-
strict: textFormat.strict,
|
|
528
|
-
}).filter(([, value]) => value !== undefined)),
|
|
529
|
-
};
|
|
530
|
-
} else if (textFormat?.type === "json_object") {
|
|
531
|
-
chatReq.response_format = { type: "json_object" };
|
|
532
|
-
}
|
|
533
|
-
return chatReq;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
537
|
-
|
|
538
|
-
function chatToResponses(chatResp, model) {
|
|
539
|
-
const id = `resp_${uid()}`, choice = chatResp.choices?.[0], msg = choice?.message, output = [];
|
|
540
|
-
if (msg?.content) output.push({ type: "message", id: `msg_${uid()}`, role: "assistant", status: "completed", content: [{ type: "output_text", text: msg.content }] });
|
|
541
|
-
if (msg?.tool_calls) for (const tc of msg.tool_calls) output.push({ type: "function_call", id: tc.id, call_id: tc.id, name: tc.function.name, arguments: tc.function.arguments, status: "completed" });
|
|
542
|
-
return { id, object: "response", status: "completed", model: chatResp.model || model, output, usage: chatResp.usage ? { input_tokens: chatResp.usage.prompt_tokens || 0, output_tokens: chatResp.usage.completion_tokens || 0, total_tokens: chatResp.usage.total_tokens || 0 } : undefined };
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
546
|
-
delete chatReq.max_tokens;
|
|
547
|
-
let resp;
|
|
548
|
-
try {
|
|
549
|
-
const chatCompletionsFn = options.chatCompletionsFn || chatCompletions;
|
|
550
|
-
resp = await chatCompletionsFn({
|
|
551
|
-
...chatReq,
|
|
552
|
-
stream: true,
|
|
553
|
-
}, { signal: options.signal });
|
|
554
|
-
} catch (e) {
|
|
555
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
556
|
-
await onError(statusCode, e.message);
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
if (!resp.ok) {
|
|
560
|
-
await onError(resp.status, await resp.text());
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
564
|
-
const respId = `resp_${uid()}`;
|
|
565
|
-
let actualModel = chatReq.model || "unknown";
|
|
566
|
-
let fullText = "";
|
|
567
|
-
let messageItem = null;
|
|
568
|
-
let nextOutputIndex = 0;
|
|
569
|
-
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
570
|
-
const toolCalls = new Map();
|
|
571
|
-
|
|
572
|
-
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
573
|
-
|
|
574
|
-
const ensureMessageItem = async () => {
|
|
575
|
-
if (messageItem) return messageItem;
|
|
576
|
-
messageItem = { id: `msg_${uid()}`, outputIndex: nextOutputIndex++ };
|
|
577
|
-
await emitEvent("response.output_item.added", {
|
|
578
|
-
output_index: messageItem.outputIndex,
|
|
579
|
-
item: { type: "message", id: messageItem.id, role: "assistant", status: "in_progress", content: [] },
|
|
580
|
-
});
|
|
581
|
-
await emitEvent("response.content_part.added", {
|
|
582
|
-
output_index: messageItem.outputIndex,
|
|
583
|
-
content_index: 0,
|
|
584
|
-
part: { type: "output_text", text: "" },
|
|
585
|
-
});
|
|
586
|
-
return messageItem;
|
|
587
|
-
};
|
|
588
|
-
|
|
589
|
-
const ensureToolCall = async (chunk) => {
|
|
590
|
-
const key = Number.isInteger(chunk.index) ? `index:${chunk.index}` : `id:${chunk.id || toolCalls.size}`;
|
|
591
|
-
if (toolCalls.has(key)) return { tool: toolCalls.get(key), created: false };
|
|
592
|
-
const id = chunk.id || `call_${uid()}`;
|
|
593
|
-
const tool = {
|
|
594
|
-
id,
|
|
595
|
-
callId: id,
|
|
596
|
-
name: chunk.function?.name || "",
|
|
597
|
-
arguments: "",
|
|
598
|
-
outputIndex: nextOutputIndex++,
|
|
599
|
-
};
|
|
600
|
-
toolCalls.set(key, tool);
|
|
601
|
-
await emitEvent("response.output_item.added", {
|
|
602
|
-
output_index: tool.outputIndex,
|
|
603
|
-
item: {
|
|
604
|
-
type: "function_call",
|
|
605
|
-
id: tool.id,
|
|
606
|
-
call_id: tool.callId,
|
|
607
|
-
name: tool.name,
|
|
608
|
-
arguments: "",
|
|
609
|
-
status: "in_progress",
|
|
610
|
-
},
|
|
611
|
-
});
|
|
612
|
-
return { tool, created: true };
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
const emitCompleted = async () => {
|
|
616
|
-
if (!messageItem && toolCalls.size === 0) await ensureMessageItem();
|
|
617
|
-
const output = [];
|
|
618
|
-
if (messageItem) {
|
|
619
|
-
const item = {
|
|
620
|
-
type: "message",
|
|
621
|
-
id: messageItem.id,
|
|
622
|
-
role: "assistant",
|
|
623
|
-
status: "completed",
|
|
624
|
-
content: [{ type: "output_text", text: fullText }],
|
|
625
|
-
};
|
|
626
|
-
await emitEvent("response.output_text.done", { output_index: messageItem.outputIndex, content_index: 0, text: fullText });
|
|
627
|
-
await emitEvent("response.content_part.done", { output_index: messageItem.outputIndex, content_index: 0, part: item.content[0] });
|
|
628
|
-
await emitEvent("response.output_item.done", { output_index: messageItem.outputIndex, item });
|
|
629
|
-
output[messageItem.outputIndex] = item;
|
|
630
|
-
}
|
|
631
|
-
for (const tool of toolCalls.values()) {
|
|
632
|
-
const item = {
|
|
633
|
-
type: "function_call",
|
|
634
|
-
id: tool.id,
|
|
635
|
-
call_id: tool.callId,
|
|
636
|
-
name: tool.name,
|
|
637
|
-
arguments: tool.arguments,
|
|
638
|
-
status: "completed",
|
|
639
|
-
};
|
|
640
|
-
await emitEvent("response.function_call_arguments.done", {
|
|
641
|
-
output_index: tool.outputIndex,
|
|
642
|
-
item_id: tool.id,
|
|
643
|
-
arguments: tool.arguments,
|
|
644
|
-
});
|
|
645
|
-
await emitEvent("response.output_item.done", { output_index: tool.outputIndex, item });
|
|
646
|
-
output[tool.outputIndex] = item;
|
|
647
|
-
}
|
|
648
|
-
await emitEvent("response.completed", {
|
|
649
|
-
response: {
|
|
650
|
-
id: respId,
|
|
651
|
-
object: "response",
|
|
652
|
-
status: "completed",
|
|
653
|
-
model: actualModel,
|
|
654
|
-
output: output.filter(Boolean),
|
|
655
|
-
usage,
|
|
656
|
-
},
|
|
657
|
-
});
|
|
658
|
-
};
|
|
659
|
-
|
|
660
|
-
try {
|
|
661
|
-
for await (const line of webStreamLines(resp, {
|
|
662
|
-
onChunk: () => options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
663
|
-
})) {
|
|
664
|
-
if (!line.startsWith("data: ")) continue;
|
|
665
|
-
const data = line.slice(6).trim();
|
|
666
|
-
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
667
|
-
let parsed;
|
|
668
|
-
try { parsed = JSON.parse(data); } catch { continue; }
|
|
669
|
-
if (parsed.model) actualModel = parsed.model;
|
|
670
|
-
if (parsed.usage) {
|
|
671
|
-
usage = {
|
|
672
|
-
input_tokens: parsed.usage.prompt_tokens || 0,
|
|
673
|
-
output_tokens: parsed.usage.completion_tokens || 0,
|
|
674
|
-
total_tokens: parsed.usage.total_tokens || 0,
|
|
675
|
-
};
|
|
676
|
-
const cached = parsed.usage.prompt_tokens_details?.cached_tokens;
|
|
677
|
-
if (cached) usage.input_tokens_details = { cached_tokens: cached };
|
|
678
|
-
}
|
|
679
|
-
const delta = parsed.choices?.[0]?.delta;
|
|
680
|
-
if (!delta) continue;
|
|
681
|
-
if (delta.content) {
|
|
682
|
-
const message = await ensureMessageItem();
|
|
683
|
-
fullText += delta.content;
|
|
684
|
-
await emitEvent("response.output_text.delta", { output_index: message.outputIndex, content_index: 0, delta: delta.content });
|
|
685
|
-
}
|
|
686
|
-
if (delta.tool_calls) {
|
|
687
|
-
for (const tc of delta.tool_calls) {
|
|
688
|
-
const { tool, created } = await ensureToolCall(tc);
|
|
689
|
-
if (tc.id) tool.callId = tc.id;
|
|
690
|
-
if (!created && tc.function?.name) tool.name += tc.function.name;
|
|
691
|
-
if (tc.function?.arguments) {
|
|
692
|
-
tool.arguments += tc.function.arguments;
|
|
693
|
-
await emitEvent("response.function_call_arguments.delta", {
|
|
694
|
-
output_index: tool.outputIndex,
|
|
695
|
-
item_id: tool.id,
|
|
696
|
-
delta: tc.function.arguments,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
} catch (e) {
|
|
703
|
-
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
704
|
-
await onError(statusCode, e?.message || "upstream stream error");
|
|
705
|
-
return;
|
|
706
|
-
}
|
|
707
|
-
await emitCompleted();
|
|
708
|
-
onDone();
|
|
709
|
-
}
|
|
710
|
-
|
|
711
84
|
// Shared request handler. Keeping this separate from the listener makes the
|
|
712
85
|
// complete HTTP routing layer testable without opening a local port.
|
|
713
86
|
export function createAdapterHandler(options = {}) {
|
|
@@ -720,16 +93,58 @@ export function createAdapterHandler(options = {}) {
|
|
|
720
93
|
const responsesCompactFn = options.responsesCompactFn || copilotResponsesCompact;
|
|
721
94
|
const listModelsFn = options.listModelsFn || listModels;
|
|
722
95
|
const openAIModelEnv = options.openAIModelEnv || process.env;
|
|
723
|
-
const upstreamTimeoutMs =
|
|
724
|
-
const streamHandshakeTimeoutMs =
|
|
725
|
-
const streamIdleTimeoutMs =
|
|
96
|
+
const upstreamTimeoutMs = parsePositiveInteger(options.upstreamTimeoutMs, ADAPTER_RUNTIME_CONFIG.upstreamTimeoutMs);
|
|
97
|
+
const streamHandshakeTimeoutMs = parsePositiveInteger(options.streamHandshakeTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamHandshakeTimeoutMs);
|
|
98
|
+
const streamIdleTimeoutMs = parsePositiveInteger(options.streamIdleTimeoutMs, ADAPTER_RUNTIME_CONFIG.streamIdleTimeoutMs);
|
|
99
|
+
const acquireRequest = options.acquireRequest || createRequestAdmission();
|
|
100
|
+
const requestMetrics = options.requestMetrics || createRequestMetrics();
|
|
726
101
|
const claudeDesktopModelOptions = () => {
|
|
727
102
|
const modelDefs = options.modelRegistry?.modelDefs || options.claudeDesktopModelDefs;
|
|
728
103
|
return Array.isArray(modelDefs) ? { modelDefs } : {};
|
|
729
104
|
};
|
|
105
|
+
const responsesHandler = createResponsesHandler({
|
|
106
|
+
acquireRequest,
|
|
107
|
+
chatCompletionsFn,
|
|
108
|
+
openAIModelEnv,
|
|
109
|
+
responsesFn,
|
|
110
|
+
streamHandshakeTimeoutMs,
|
|
111
|
+
streamIdleTimeoutMs,
|
|
112
|
+
upstreamTimeoutMs,
|
|
113
|
+
});
|
|
114
|
+
const responsesCompactHandler = createResponsesCompactHandler({
|
|
115
|
+
acquireRequest,
|
|
116
|
+
openAIModelEnv,
|
|
117
|
+
responsesCompactFn,
|
|
118
|
+
streamHandshakeTimeoutMs,
|
|
119
|
+
streamIdleTimeoutMs,
|
|
120
|
+
upstreamTimeoutMs,
|
|
121
|
+
});
|
|
122
|
+
const anthropicCountTokensHandler = createAnthropicCountTokensHandler({ acquireRequest });
|
|
123
|
+
const anthropicMessagesHandler = createAnthropicMessagesHandler({
|
|
124
|
+
acquireRequest,
|
|
125
|
+
chatCompletionsFn,
|
|
126
|
+
environment: process.env,
|
|
127
|
+
modelOptions: claudeDesktopModelOptions,
|
|
128
|
+
streamHandshakeTimeoutMs,
|
|
129
|
+
streamIdleTimeoutMs,
|
|
130
|
+
upstreamTimeoutMs,
|
|
131
|
+
});
|
|
730
132
|
|
|
731
|
-
|
|
732
|
-
|
|
133
|
+
const dispatch = (req, res, pathname) => {
|
|
134
|
+
if (req.method === "GET" && pathname === ADAPTER_STATUS_PATH) {
|
|
135
|
+
if (!isLoopbackAddress(req.socket?.remoteAddress)) {
|
|
136
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
137
|
+
res.end(JSON.stringify({ error: "Runtime status is available only from loopback" }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
141
|
+
res.end(JSON.stringify(runtimeStatusPayload({
|
|
142
|
+
metrics: requestMetrics,
|
|
143
|
+
admission: acquireRequest,
|
|
144
|
+
modelRegistry: options.modelRegistry,
|
|
145
|
+
})));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
733
148
|
|
|
734
149
|
if (req.method === "GET" && pathname === ADAPTER_HEALTH_PATH) {
|
|
735
150
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
@@ -738,109 +153,11 @@ export function createAdapterHandler(options = {}) {
|
|
|
738
153
|
}
|
|
739
154
|
|
|
740
155
|
if (req.method === "POST" && pathname === "/v1/responses") {
|
|
741
|
-
return (
|
|
742
|
-
const abort = createRequestAbort(req, res);
|
|
743
|
-
try {
|
|
744
|
-
const parsed = await readJsonBody(req);
|
|
745
|
-
abort.setTimeout(
|
|
746
|
-
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
747
|
-
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
748
|
-
);
|
|
749
|
-
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
750
|
-
prepared.surface = "responses";
|
|
751
|
-
const model = parsed.model || "unknown";
|
|
752
|
-
const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
|
|
753
|
-
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
754
|
-
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
755
|
-
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
756
|
-
if (requestedModel === CODEX_AUTO_REVIEW_MODEL || isResponsesOnlyModel(upstreamModel)) {
|
|
757
|
-
await proxyCopilotResponses(prepared, req, res, responsesFn, {
|
|
758
|
-
signal: abort.signal,
|
|
759
|
-
abort,
|
|
760
|
-
streamIdleTimeoutMs,
|
|
761
|
-
});
|
|
762
|
-
} else {
|
|
763
|
-
const chatReq = responsesToChat(prepared.body);
|
|
764
|
-
if (parsed.stream) {
|
|
765
|
-
await forwardToChat(chatReq, async (event, data) => {
|
|
766
|
-
if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
767
|
-
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
768
|
-
if (event === "response.completed") {
|
|
769
|
-
rememberResponseHistory(prepared, data.response);
|
|
770
|
-
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
771
|
-
}
|
|
772
|
-
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg) => {
|
|
773
|
-
if (!res.headersSent) {
|
|
774
|
-
res.writeHead(statusCode || 500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
775
|
-
res.end(errMsg);
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
await endStreamWithError(res, "responses", new Error(errMsg), abort);
|
|
779
|
-
}, {
|
|
780
|
-
signal: abort.signal,
|
|
781
|
-
abort,
|
|
782
|
-
streamIdleTimeoutMs,
|
|
783
|
-
chatCompletionsFn,
|
|
784
|
-
});
|
|
785
|
-
} else {
|
|
786
|
-
chatReq.stream = false;
|
|
787
|
-
delete chatReq.max_tokens;
|
|
788
|
-
try {
|
|
789
|
-
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
790
|
-
const data = await upstream.text();
|
|
791
|
-
if (!upstream.ok) {
|
|
792
|
-
sendUpstreamError(res, upstream, data);
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const resp = chatToResponses(JSON.parse(data), model);
|
|
796
|
-
rememberResponseHistory(prepared, resp);
|
|
797
|
-
recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response: resp, event: resp });
|
|
798
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
799
|
-
res.end(JSON.stringify(resp));
|
|
800
|
-
} catch (e) {
|
|
801
|
-
logRequestFailure("Responses", e, abort);
|
|
802
|
-
sendJsonError(res, e, 502);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
} catch (e) {
|
|
807
|
-
logRequestFailure("Responses", e, abort);
|
|
808
|
-
sendJsonError(res, e, 502);
|
|
809
|
-
} finally {
|
|
810
|
-
abort.cleanup();
|
|
811
|
-
}
|
|
812
|
-
})();
|
|
156
|
+
return responsesHandler(req, res);
|
|
813
157
|
}
|
|
814
158
|
|
|
815
159
|
if (req.method === "POST" && pathname === "/v1/responses/compact") {
|
|
816
|
-
(
|
|
817
|
-
const abort = createRequestAbort(req, res);
|
|
818
|
-
try {
|
|
819
|
-
const parsed = await readJsonBody(req);
|
|
820
|
-
abort.setTimeout(
|
|
821
|
-
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
822
|
-
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
823
|
-
);
|
|
824
|
-
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
825
|
-
prepared.surface = "responses_compact";
|
|
826
|
-
const model = parsed.model || "unknown";
|
|
827
|
-
const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
|
|
828
|
-
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
829
|
-
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
830
|
-
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
831
|
-
await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
832
|
-
signal: abort.signal,
|
|
833
|
-
abort,
|
|
834
|
-
streamIdleTimeoutMs,
|
|
835
|
-
});
|
|
836
|
-
} catch (e) {
|
|
837
|
-
logRequestFailure("Responses compact", e, abort);
|
|
838
|
-
sendJsonError(res, e, 502);
|
|
839
|
-
} finally {
|
|
840
|
-
abort.cleanup();
|
|
841
|
-
}
|
|
842
|
-
})();
|
|
843
|
-
return;
|
|
160
|
+
return responsesCompactHandler(req, res);
|
|
844
161
|
}
|
|
845
162
|
|
|
846
163
|
if (req.method === "GET" && pathname === "/v1/models") {
|
|
@@ -885,91 +202,52 @@ export function createAdapterHandler(options = {}) {
|
|
|
885
202
|
}
|
|
886
203
|
|
|
887
204
|
if (req.method === "POST" && pathname === "/v1/messages/count_tokens") {
|
|
888
|
-
(
|
|
889
|
-
const abort = createRequestAbort(req, res);
|
|
890
|
-
try {
|
|
891
|
-
const parsed = await readJsonBody(req);
|
|
892
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
893
|
-
res.end(JSON.stringify(await countTokens(parsed)));
|
|
894
|
-
} catch (e) {
|
|
895
|
-
sendJsonError(res, e);
|
|
896
|
-
} finally {
|
|
897
|
-
abort.cleanup();
|
|
898
|
-
}
|
|
899
|
-
})();
|
|
900
|
-
return;
|
|
205
|
+
return anthropicCountTokensHandler(req, res);
|
|
901
206
|
}
|
|
902
207
|
|
|
903
208
|
if (req.method === "POST" && pathname === "/v1/messages") {
|
|
904
|
-
(
|
|
905
|
-
const abort = createRequestAbort(req, res);
|
|
906
|
-
try {
|
|
907
|
-
const parsed = await readJsonBody(req);
|
|
908
|
-
abort.setTimeout(
|
|
909
|
-
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
910
|
-
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
911
|
-
);
|
|
912
|
-
const { requestedModel, upstreamModel } = resolveAnthropicModel(parsed.model || "unknown", process.env, claudeDesktopModelOptions());
|
|
913
|
-
const modelNote = upstreamModel === requestedModel ? requestedModel : `${requestedModel} -> ${upstreamModel}`;
|
|
914
|
-
console.log(status("info", `messages model=${modelNote} stream=${!!parsed.stream}`));
|
|
915
|
-
const chatReq = anthropicToChat(parsed, { upstreamModel });
|
|
916
|
-
const forceRequestedModel = upstreamModel !== requestedModel;
|
|
917
|
-
if (parsed.stream) {
|
|
918
|
-
const upstream = await chatCompletionsFn({ ...chatReq, stream: true }, { signal: abort.signal });
|
|
919
|
-
if (!upstream.ok) {
|
|
920
|
-
if (!res.headersSent) res.writeHead(upstream.status);
|
|
921
|
-
res.end(await upstream.text());
|
|
922
|
-
return;
|
|
923
|
-
}
|
|
924
|
-
abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout");
|
|
925
|
-
res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
926
|
-
let messageId;
|
|
927
|
-
let usage;
|
|
928
|
-
try {
|
|
929
|
-
await streamAnthropicFromLines(
|
|
930
|
-
webStreamLines(upstream, {
|
|
931
|
-
onChunk: () => abort.setTimeout(streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
932
|
-
}),
|
|
933
|
-
async (event, data) => {
|
|
934
|
-
if (event === "message_start") messageId = data.message?.id;
|
|
935
|
-
if (event === "message_delta") usage = data.usage;
|
|
936
|
-
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
937
|
-
},
|
|
938
|
-
requestedModel,
|
|
939
|
-
{ forceModel: forceRequestedModel },
|
|
940
|
-
);
|
|
941
|
-
} catch (e) {
|
|
942
|
-
logRequestFailure("Messages", e, abort);
|
|
943
|
-
await endStreamWithError(res, "anthropic", e, abort);
|
|
944
|
-
return;
|
|
945
|
-
}
|
|
946
|
-
recordAnthropicUsage({ surface: "messages", mode: "stream", model: requestedModel, responseId: messageId, usage });
|
|
947
|
-
if (!res.writableEnded) res.end();
|
|
948
|
-
} else {
|
|
949
|
-
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
950
|
-
const data = await upstream.text();
|
|
951
|
-
if (!upstream.ok) {
|
|
952
|
-
sendUpstreamError(res, upstream, data);
|
|
953
|
-
return;
|
|
954
|
-
}
|
|
955
|
-
const anthropicMsg = chatToAnthropic(JSON.parse(data), requestedModel, { forceModel: forceRequestedModel });
|
|
956
|
-
recordAnthropicUsage({ surface: "messages", mode: "json", model: anthropicMsg.model, responseId: anthropicMsg.id, usage: anthropicMsg.usage });
|
|
957
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
958
|
-
res.end(JSON.stringify(anthropicMsg));
|
|
959
|
-
}
|
|
960
|
-
} catch (e) {
|
|
961
|
-
logRequestFailure("Messages", e, abort);
|
|
962
|
-
sendJsonError(res, e, 502);
|
|
963
|
-
} finally {
|
|
964
|
-
abort.cleanup();
|
|
965
|
-
}
|
|
966
|
-
})();
|
|
967
|
-
return;
|
|
209
|
+
return anthropicMessagesHandler(req, res);
|
|
968
210
|
}
|
|
969
211
|
|
|
970
212
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
971
213
|
res.end(JSON.stringify({ error: "Not found" }));
|
|
972
214
|
};
|
|
215
|
+
|
|
216
|
+
return (req, res) => {
|
|
217
|
+
const pathname = requestPath(req.url);
|
|
218
|
+
const requestId = createRequestId();
|
|
219
|
+
if (typeof res.setHeader === "function") {
|
|
220
|
+
res.setHeader("X-Request-Id", requestId);
|
|
221
|
+
} else if (res.headers && typeof res.headers === "object") {
|
|
222
|
+
res.headers["X-Request-Id"] = requestId;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const trackRequest = pathname !== ADAPTER_HEALTH_PATH && pathname !== ADAPTER_STATUS_PATH;
|
|
226
|
+
const complete = trackRequest
|
|
227
|
+
? requestMetrics.begin(classifyAdapterRoute(req.method, pathname))
|
|
228
|
+
: () => {};
|
|
229
|
+
if (trackRequest && typeof res.once === "function") {
|
|
230
|
+
res.once("finish", () => complete({ statusCode: res.statusCode }));
|
|
231
|
+
res.once("close", () => complete({
|
|
232
|
+
statusCode: res.statusCode,
|
|
233
|
+
aborted: !res.writableFinished && !res.writableEnded,
|
|
234
|
+
}));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
const result = runWithRequestContext({ requestId, pathname }, () => dispatch(req, res, pathname));
|
|
239
|
+
if (result && typeof result.then === "function") {
|
|
240
|
+
return result.catch((error) => {
|
|
241
|
+
complete({ statusCode: res.statusCode >= 400 ? res.statusCode : 500 });
|
|
242
|
+
throw error;
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
complete({ statusCode: res.statusCode >= 400 ? res.statusCode : 500 });
|
|
248
|
+
throw error;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
973
251
|
}
|
|
974
252
|
|
|
975
253
|
// Public server entry point.
|