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