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
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chatCompletions } from "./copilot.mjs";
|
|
3
|
+
import { webStreamLines } from "./stream.mjs";
|
|
4
|
+
import { abortErrorStatusCode, isAbortLikeError } from "./http-transport.mjs";
|
|
5
|
+
|
|
6
|
+
function cloneJson(value) {
|
|
7
|
+
return value === undefined ? undefined : structuredClone(value);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function responsesToChat(body) {
|
|
11
|
+
const messages = [];
|
|
12
|
+
if (body.instructions) messages.push({ role: "system", content: body.instructions });
|
|
13
|
+
|
|
14
|
+
const messageContent = (content) => {
|
|
15
|
+
if (typeof content === "string") return content;
|
|
16
|
+
if (!Array.isArray(content)) return JSON.stringify(content);
|
|
17
|
+
const parts = [];
|
|
18
|
+
for (const part of content) {
|
|
19
|
+
if (!part || typeof part !== "object") continue;
|
|
20
|
+
if (["input_text", "output_text", "text"].includes(part.type)) {
|
|
21
|
+
parts.push({ type: "text", text: String(part.text || "") });
|
|
22
|
+
} else if (part.type === "input_image" || part.type === "image_url") {
|
|
23
|
+
const raw = part.image_url ?? part.url;
|
|
24
|
+
const imageUrl = typeof raw === "string"
|
|
25
|
+
? { url: raw }
|
|
26
|
+
: raw && typeof raw === "object" ? cloneJson(raw) : null;
|
|
27
|
+
if (imageUrl && part.detail !== undefined && imageUrl.detail === undefined) imageUrl.detail = part.detail;
|
|
28
|
+
if (imageUrl?.url) parts.push({ type: "image_url", image_url: imageUrl });
|
|
29
|
+
} else {
|
|
30
|
+
parts.push({ type: "text", text: JSON.stringify(part) });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (parts.length === 1 && parts[0].type === "text") return parts[0].text;
|
|
34
|
+
return parts;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
if (typeof body.input === "string") {
|
|
38
|
+
messages.push({ role: "user", content: body.input });
|
|
39
|
+
} else if (Array.isArray(body.input)) {
|
|
40
|
+
for (const item of body.input) {
|
|
41
|
+
if (item.type === "message") {
|
|
42
|
+
messages.push({ role: item.role, content: messageContent(item.content) });
|
|
43
|
+
} else if (item.type === "function_call") {
|
|
44
|
+
messages.push({
|
|
45
|
+
role: "assistant",
|
|
46
|
+
content: null,
|
|
47
|
+
tool_calls: [{ id: item.call_id || randomUUID(), type: "function", function: { name: item.name, arguments: item.arguments } }],
|
|
48
|
+
});
|
|
49
|
+
} else if (item.type === "function_call_output") {
|
|
50
|
+
messages.push({ role: "tool", tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const chatReq = { model: body.model, messages, stream: true };
|
|
56
|
+
for (const k of ["temperature", "top_p", "stop", "presence_penalty", "frequency_penalty"]) {
|
|
57
|
+
if (body[k] !== undefined) chatReq[k] = body[k];
|
|
58
|
+
}
|
|
59
|
+
const maxTok = body.max_output_tokens ?? body.max_tokens ?? body.max_completion_tokens;
|
|
60
|
+
if (maxTok !== undefined) chatReq.max_completion_tokens = maxTok;
|
|
61
|
+
|
|
62
|
+
if (body.tools?.length) {
|
|
63
|
+
chatReq.tools = body.tools
|
|
64
|
+
.map((tool) => {
|
|
65
|
+
if (tool?.type !== "function") return null;
|
|
66
|
+
if (tool.function?.name) return cloneJson(tool);
|
|
67
|
+
const fn = {
|
|
68
|
+
name: tool.name,
|
|
69
|
+
description: tool.description,
|
|
70
|
+
parameters: tool.parameters,
|
|
71
|
+
strict: tool.strict,
|
|
72
|
+
};
|
|
73
|
+
for (const key of Object.keys(fn)) if (fn[key] === undefined) delete fn[key];
|
|
74
|
+
return { type: "function", function: fn };
|
|
75
|
+
})
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.filter((t) => t.function?.name);
|
|
78
|
+
if (!chatReq.tools.length) delete chatReq.tools;
|
|
79
|
+
}
|
|
80
|
+
if (body.tool_choice !== undefined) {
|
|
81
|
+
if (typeof body.tool_choice === "string") {
|
|
82
|
+
chatReq.tool_choice = body.tool_choice;
|
|
83
|
+
} else if (body.tool_choice?.type === "function" && body.tool_choice.name) {
|
|
84
|
+
chatReq.tool_choice = { type: "function", function: { name: body.tool_choice.name } };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (body.parallel_tool_calls !== undefined) chatReq.parallel_tool_calls = body.parallel_tool_calls;
|
|
88
|
+
const textFormat = body.text?.format;
|
|
89
|
+
if (textFormat?.type === "json_schema") {
|
|
90
|
+
chatReq.response_format = {
|
|
91
|
+
type: "json_schema",
|
|
92
|
+
json_schema: Object.fromEntries(Object.entries({
|
|
93
|
+
name: textFormat.name,
|
|
94
|
+
description: textFormat.description,
|
|
95
|
+
schema: textFormat.schema,
|
|
96
|
+
strict: textFormat.strict,
|
|
97
|
+
}).filter(([, value]) => value !== undefined)),
|
|
98
|
+
};
|
|
99
|
+
} else if (textFormat?.type === "json_object") {
|
|
100
|
+
chatReq.response_format = { type: "json_object" };
|
|
101
|
+
}
|
|
102
|
+
return chatReq;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function uid() { return randomUUID().replace(/-/g, ""); }
|
|
106
|
+
|
|
107
|
+
export function chatToResponses(chatResp, model) {
|
|
108
|
+
const id = `resp_${uid()}`, choice = chatResp.choices?.[0], msg = choice?.message, output = [];
|
|
109
|
+
if (msg?.content) output.push({ type: "message", id: `msg_${uid()}`, role: "assistant", status: "completed", content: [{ type: "output_text", text: msg.content }] });
|
|
110
|
+
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" });
|
|
111
|
+
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 };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function forwardToChat(chatReq, emitEvent, onDone, onError, options = {}) {
|
|
115
|
+
delete chatReq.max_tokens;
|
|
116
|
+
let resp;
|
|
117
|
+
try {
|
|
118
|
+
const chatCompletionsFn = options.chatCompletionsFn || chatCompletions;
|
|
119
|
+
try {
|
|
120
|
+
resp = await chatCompletionsFn({
|
|
121
|
+
...chatReq,
|
|
122
|
+
stream: true,
|
|
123
|
+
}, { signal: options.signal });
|
|
124
|
+
} finally {
|
|
125
|
+
options.releaseRequest?.();
|
|
126
|
+
}
|
|
127
|
+
} catch (e) {
|
|
128
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
129
|
+
await onError(statusCode, e.message);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!resp.ok) {
|
|
133
|
+
await onError(resp.status, await resp.text());
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
137
|
+
const respId = `resp_${uid()}`;
|
|
138
|
+
let actualModel = chatReq.model || "unknown";
|
|
139
|
+
let fullText = "";
|
|
140
|
+
let messageItem = null;
|
|
141
|
+
let nextOutputIndex = 0;
|
|
142
|
+
let usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
143
|
+
const toolCalls = new Map();
|
|
144
|
+
|
|
145
|
+
await emitEvent("response.created", { response: { id: respId, object: "response", status: "in_progress", model: actualModel, output: [] } });
|
|
146
|
+
|
|
147
|
+
const ensureMessageItem = async () => {
|
|
148
|
+
if (messageItem) return messageItem;
|
|
149
|
+
messageItem = { id: `msg_${uid()}`, outputIndex: nextOutputIndex++ };
|
|
150
|
+
await emitEvent("response.output_item.added", {
|
|
151
|
+
output_index: messageItem.outputIndex,
|
|
152
|
+
item: { type: "message", id: messageItem.id, role: "assistant", status: "in_progress", content: [] },
|
|
153
|
+
});
|
|
154
|
+
await emitEvent("response.content_part.added", {
|
|
155
|
+
output_index: messageItem.outputIndex,
|
|
156
|
+
content_index: 0,
|
|
157
|
+
part: { type: "output_text", text: "" },
|
|
158
|
+
});
|
|
159
|
+
return messageItem;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const ensureToolCall = async (chunk) => {
|
|
163
|
+
const key = Number.isInteger(chunk.index) ? `index:${chunk.index}` : `id:${chunk.id || toolCalls.size}`;
|
|
164
|
+
if (toolCalls.has(key)) return { tool: toolCalls.get(key), created: false };
|
|
165
|
+
const id = chunk.id || `call_${uid()}`;
|
|
166
|
+
const tool = {
|
|
167
|
+
id,
|
|
168
|
+
callId: id,
|
|
169
|
+
name: chunk.function?.name || "",
|
|
170
|
+
arguments: "",
|
|
171
|
+
outputIndex: nextOutputIndex++,
|
|
172
|
+
};
|
|
173
|
+
toolCalls.set(key, tool);
|
|
174
|
+
await emitEvent("response.output_item.added", {
|
|
175
|
+
output_index: tool.outputIndex,
|
|
176
|
+
item: {
|
|
177
|
+
type: "function_call",
|
|
178
|
+
id: tool.id,
|
|
179
|
+
call_id: tool.callId,
|
|
180
|
+
name: tool.name,
|
|
181
|
+
arguments: "",
|
|
182
|
+
status: "in_progress",
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return { tool, created: true };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const emitCompleted = async () => {
|
|
189
|
+
if (!messageItem && toolCalls.size === 0) await ensureMessageItem();
|
|
190
|
+
const output = [];
|
|
191
|
+
if (messageItem) {
|
|
192
|
+
const item = {
|
|
193
|
+
type: "message",
|
|
194
|
+
id: messageItem.id,
|
|
195
|
+
role: "assistant",
|
|
196
|
+
status: "completed",
|
|
197
|
+
content: [{ type: "output_text", text: fullText }],
|
|
198
|
+
};
|
|
199
|
+
await emitEvent("response.output_text.done", { output_index: messageItem.outputIndex, content_index: 0, text: fullText });
|
|
200
|
+
await emitEvent("response.content_part.done", { output_index: messageItem.outputIndex, content_index: 0, part: item.content[0] });
|
|
201
|
+
await emitEvent("response.output_item.done", { output_index: messageItem.outputIndex, item });
|
|
202
|
+
output[messageItem.outputIndex] = item;
|
|
203
|
+
}
|
|
204
|
+
for (const tool of toolCalls.values()) {
|
|
205
|
+
const item = {
|
|
206
|
+
type: "function_call",
|
|
207
|
+
id: tool.id,
|
|
208
|
+
call_id: tool.callId,
|
|
209
|
+
name: tool.name,
|
|
210
|
+
arguments: tool.arguments,
|
|
211
|
+
status: "completed",
|
|
212
|
+
};
|
|
213
|
+
await emitEvent("response.function_call_arguments.done", {
|
|
214
|
+
output_index: tool.outputIndex,
|
|
215
|
+
item_id: tool.id,
|
|
216
|
+
arguments: tool.arguments,
|
|
217
|
+
});
|
|
218
|
+
await emitEvent("response.output_item.done", { output_index: tool.outputIndex, item });
|
|
219
|
+
output[tool.outputIndex] = item;
|
|
220
|
+
}
|
|
221
|
+
await emitEvent("response.completed", {
|
|
222
|
+
response: {
|
|
223
|
+
id: respId,
|
|
224
|
+
object: "response",
|
|
225
|
+
status: "completed",
|
|
226
|
+
model: actualModel,
|
|
227
|
+
output: output.filter(Boolean),
|
|
228
|
+
usage,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
for await (const line of webStreamLines(resp, {
|
|
235
|
+
onChunk: () => options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout"),
|
|
236
|
+
})) {
|
|
237
|
+
if (!line.startsWith("data: ")) continue;
|
|
238
|
+
const data = line.slice(6).trim();
|
|
239
|
+
if (data === "[DONE]") { await emitCompleted(); onDone(); return; }
|
|
240
|
+
let parsed;
|
|
241
|
+
try { parsed = JSON.parse(data); } catch { continue; }
|
|
242
|
+
if (parsed.model) actualModel = parsed.model;
|
|
243
|
+
if (parsed.usage) {
|
|
244
|
+
usage = {
|
|
245
|
+
input_tokens: parsed.usage.prompt_tokens || 0,
|
|
246
|
+
output_tokens: parsed.usage.completion_tokens || 0,
|
|
247
|
+
total_tokens: parsed.usage.total_tokens || 0,
|
|
248
|
+
};
|
|
249
|
+
const cached = parsed.usage.prompt_tokens_details?.cached_tokens;
|
|
250
|
+
if (cached) usage.input_tokens_details = { cached_tokens: cached };
|
|
251
|
+
}
|
|
252
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
253
|
+
if (!delta) continue;
|
|
254
|
+
if (delta.content) {
|
|
255
|
+
const message = await ensureMessageItem();
|
|
256
|
+
fullText += delta.content;
|
|
257
|
+
await emitEvent("response.output_text.delta", { output_index: message.outputIndex, content_index: 0, delta: delta.content });
|
|
258
|
+
}
|
|
259
|
+
if (delta.tool_calls) {
|
|
260
|
+
for (const tc of delta.tool_calls) {
|
|
261
|
+
const { tool, created } = await ensureToolCall(tc);
|
|
262
|
+
if (tc.id) tool.callId = tc.id;
|
|
263
|
+
if (!created && tc.function?.name) tool.name += tc.function.name;
|
|
264
|
+
if (tc.function?.arguments) {
|
|
265
|
+
tool.arguments += tc.function.arguments;
|
|
266
|
+
await emitEvent("response.function_call_arguments.delta", {
|
|
267
|
+
output_index: tool.outputIndex,
|
|
268
|
+
item_id: tool.id,
|
|
269
|
+
delta: tc.function.arguments,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
const statusCode = isAbortLikeError(e) ? abortErrorStatusCode(options.abort?.reason) : 502;
|
|
277
|
+
await onError(statusCode, e?.message || "upstream stream error");
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
await emitCompleted();
|
|
281
|
+
onDone();
|
|
282
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { getCachedModelEndpoints } from "./copilot.mjs";
|
|
2
|
+
import {
|
|
3
|
+
createRequestAbort,
|
|
4
|
+
logRequestFailure,
|
|
5
|
+
readJsonBody,
|
|
6
|
+
sendJsonError,
|
|
7
|
+
sendUpstreamError,
|
|
8
|
+
writeOrDrain,
|
|
9
|
+
} from "./http-transport.mjs";
|
|
10
|
+
import {
|
|
11
|
+
CODEX_AUTO_REVIEW_MODEL,
|
|
12
|
+
modelIsResponsesOnly,
|
|
13
|
+
modelSupportsChatCompletions,
|
|
14
|
+
resolveOpenAIModel,
|
|
15
|
+
} from "./models.mjs";
|
|
16
|
+
import { chatToResponses, forwardToChat, responsesToChat } from "./responses-bridge.mjs";
|
|
17
|
+
import { proxyCopilotResponses } from "./responses-proxy.mjs";
|
|
18
|
+
import { prepareResponsesRequest, rememberResponseHistory } from "./responses-request.mjs";
|
|
19
|
+
import { status } from "./status.mjs";
|
|
20
|
+
import { endStreamWithError } from "./stream-errors.mjs";
|
|
21
|
+
import { recordResponsesUsage } from "./usage.mjs";
|
|
22
|
+
|
|
23
|
+
const RESPONSES_ONLY_FALLBACK = new Set([
|
|
24
|
+
"gpt-5.6-luna",
|
|
25
|
+
"gpt-5.6-sol",
|
|
26
|
+
"gpt-5.6-terra",
|
|
27
|
+
"gpt-5.5",
|
|
28
|
+
"gpt-5.4",
|
|
29
|
+
"gpt-5.4-mini",
|
|
30
|
+
"gpt-5.3-codex",
|
|
31
|
+
"gpt-5.2-codex",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function isResponsesOnlyModel(model) {
|
|
35
|
+
const endpoints = getCachedModelEndpoints(model);
|
|
36
|
+
if (endpoints) {
|
|
37
|
+
const fakeModel = { supported_endpoints: endpoints };
|
|
38
|
+
if (modelSupportsChatCompletions(fakeModel)) return false;
|
|
39
|
+
if (modelIsResponsesOnly(fakeModel)) return true;
|
|
40
|
+
}
|
|
41
|
+
return RESPONSES_ONLY_FALLBACK.has(model);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createResponsesHandler(options) {
|
|
45
|
+
const {
|
|
46
|
+
acquireRequest,
|
|
47
|
+
chatCompletionsFn,
|
|
48
|
+
openAIModelEnv,
|
|
49
|
+
responsesFn,
|
|
50
|
+
streamHandshakeTimeoutMs,
|
|
51
|
+
streamIdleTimeoutMs,
|
|
52
|
+
upstreamTimeoutMs,
|
|
53
|
+
} = options;
|
|
54
|
+
|
|
55
|
+
return async function handleResponses(req, res) {
|
|
56
|
+
const abort = createRequestAbort(req, res);
|
|
57
|
+
let releaseRequest = () => {};
|
|
58
|
+
try {
|
|
59
|
+
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
60
|
+
const parsed = await readJsonBody(req);
|
|
61
|
+
abort.setTimeout(
|
|
62
|
+
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
63
|
+
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
64
|
+
);
|
|
65
|
+
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
66
|
+
prepared.surface = "responses";
|
|
67
|
+
const model = parsed.model || "unknown";
|
|
68
|
+
const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
|
|
69
|
+
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
70
|
+
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
71
|
+
console.log(status("info", `responses model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
72
|
+
if (requestedModel === CODEX_AUTO_REVIEW_MODEL || isResponsesOnlyModel(upstreamModel)) {
|
|
73
|
+
await proxyCopilotResponses(prepared, req, res, responsesFn, {
|
|
74
|
+
signal: abort.signal,
|
|
75
|
+
abort,
|
|
76
|
+
releaseRequest,
|
|
77
|
+
streamIdleTimeoutMs,
|
|
78
|
+
});
|
|
79
|
+
} else {
|
|
80
|
+
const chatReq = responsesToChat(prepared.body);
|
|
81
|
+
if (parsed.stream) {
|
|
82
|
+
await forwardToChat(chatReq, async (event, data) => {
|
|
83
|
+
if (!res.headersSent) res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", Connection: "keep-alive" });
|
|
84
|
+
await writeOrDrain(res, `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
85
|
+
if (event === "response.completed") {
|
|
86
|
+
rememberResponseHistory(prepared, data.response);
|
|
87
|
+
recordResponsesUsage({ surface: prepared.surface, mode: "stream", model, response: data.response, event: data });
|
|
88
|
+
}
|
|
89
|
+
}, () => { if (!res.writableEnded) res.end(); }, async (statusCode, errMsg) => {
|
|
90
|
+
if (!res.headersSent) {
|
|
91
|
+
res.writeHead(statusCode || 500, { "Content-Type": "text/plain; charset=utf-8" });
|
|
92
|
+
res.end(errMsg);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
await endStreamWithError(res, "responses", new Error(errMsg), abort);
|
|
96
|
+
}, {
|
|
97
|
+
signal: abort.signal,
|
|
98
|
+
abort,
|
|
99
|
+
streamIdleTimeoutMs,
|
|
100
|
+
chatCompletionsFn,
|
|
101
|
+
releaseRequest,
|
|
102
|
+
});
|
|
103
|
+
} else {
|
|
104
|
+
chatReq.stream = false;
|
|
105
|
+
delete chatReq.max_tokens;
|
|
106
|
+
try {
|
|
107
|
+
const upstream = await chatCompletionsFn({ ...chatReq, stream: false }, { signal: abort.signal });
|
|
108
|
+
releaseRequest();
|
|
109
|
+
const data = await upstream.text();
|
|
110
|
+
if (!upstream.ok) {
|
|
111
|
+
sendUpstreamError(res, upstream, data);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const response = chatToResponses(JSON.parse(data), model);
|
|
115
|
+
rememberResponseHistory(prepared, response);
|
|
116
|
+
recordResponsesUsage({ surface: prepared.surface, mode: "json", model, response, event: response });
|
|
117
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
118
|
+
res.end(JSON.stringify(response));
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logRequestFailure("Responses", error, abort);
|
|
121
|
+
sendJsonError(res, error, 502);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logRequestFailure("Responses", error, abort);
|
|
127
|
+
sendJsonError(res, error, 502);
|
|
128
|
+
} finally {
|
|
129
|
+
releaseRequest();
|
|
130
|
+
abort.cleanup();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function createResponsesCompactHandler(options) {
|
|
136
|
+
const {
|
|
137
|
+
acquireRequest,
|
|
138
|
+
openAIModelEnv,
|
|
139
|
+
responsesCompactFn,
|
|
140
|
+
streamHandshakeTimeoutMs,
|
|
141
|
+
streamIdleTimeoutMs,
|
|
142
|
+
upstreamTimeoutMs,
|
|
143
|
+
} = options;
|
|
144
|
+
|
|
145
|
+
return async function handleResponsesCompact(req, res) {
|
|
146
|
+
const abort = createRequestAbort(req, res);
|
|
147
|
+
let releaseRequest = () => {};
|
|
148
|
+
try {
|
|
149
|
+
releaseRequest = await acquireRequest(req, { signal: abort.signal });
|
|
150
|
+
const parsed = await readJsonBody(req);
|
|
151
|
+
abort.setTimeout(
|
|
152
|
+
parsed.stream ? streamHandshakeTimeoutMs : upstreamTimeoutMs,
|
|
153
|
+
parsed.stream ? "stream_handshake_timeout" : "upstream_timeout",
|
|
154
|
+
);
|
|
155
|
+
const prepared = prepareResponsesRequest(parsed, { mutate: true });
|
|
156
|
+
prepared.surface = "responses_compact";
|
|
157
|
+
const model = parsed.model || "unknown";
|
|
158
|
+
const { requestedModel, upstreamModel } = resolveOpenAIModel(model, openAIModelEnv);
|
|
159
|
+
if (upstreamModel !== requestedModel) prepared.body.model = upstreamModel;
|
|
160
|
+
const upstreamLog = upstreamModel === requestedModel ? "" : ` upstream_model=${upstreamModel}`;
|
|
161
|
+
console.log(status("info", `responses compact model=${requestedModel}${upstreamLog} stream=${!!parsed.stream}`));
|
|
162
|
+
await proxyCopilotResponses(prepared, req, res, responsesCompactFn, {
|
|
163
|
+
signal: abort.signal,
|
|
164
|
+
abort,
|
|
165
|
+
releaseRequest,
|
|
166
|
+
streamIdleTimeoutMs,
|
|
167
|
+
});
|
|
168
|
+
} catch (error) {
|
|
169
|
+
logRequestFailure("Responses compact", error, abort);
|
|
170
|
+
sendJsonError(res, error, 502);
|
|
171
|
+
} finally {
|
|
172
|
+
releaseRequest();
|
|
173
|
+
abort.cleanup();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { responses as copilotResponses } from "./copilot.mjs";
|
|
2
|
+
import {
|
|
3
|
+
httpError,
|
|
4
|
+
logRequestFailure,
|
|
5
|
+
sendUpstreamError,
|
|
6
|
+
writeOrDrain,
|
|
7
|
+
} from "./http-transport.mjs";
|
|
8
|
+
import { openCopilotResponse, rememberResponseHistory } from "./responses-request.mjs";
|
|
9
|
+
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
10
|
+
import { endStreamWithError } from "./stream-errors.mjs";
|
|
11
|
+
import { recordResponsesUsage } from "./usage.mjs";
|
|
12
|
+
|
|
13
|
+
const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
|
|
14
|
+
|
|
15
|
+
function storeCompletedResponseFromSse(reqContext, data) {
|
|
16
|
+
if (!data || data === "[DONE]") return;
|
|
17
|
+
try {
|
|
18
|
+
const event = JSON.parse(data);
|
|
19
|
+
const response = event.response;
|
|
20
|
+
if (response?.id && response.status === "completed") {
|
|
21
|
+
rememberResponseHistory(reqContext, response);
|
|
22
|
+
recordResponsesUsage({
|
|
23
|
+
surface: reqContext.surface,
|
|
24
|
+
mode: "stream",
|
|
25
|
+
model: reqContext.body?.model,
|
|
26
|
+
response,
|
|
27
|
+
event,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// Ignore non-JSON stream fragments.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readSseEvents(buffer, onData) {
|
|
36
|
+
while (true) {
|
|
37
|
+
const match = buffer.match(/\r?\n\r?\n/);
|
|
38
|
+
if (!match) return buffer;
|
|
39
|
+
const chunk = buffer.slice(0, match.index);
|
|
40
|
+
if (Buffer.byteLength(chunk) > MAX_SSE_BUFFER_BYTES) {
|
|
41
|
+
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
42
|
+
}
|
|
43
|
+
buffer = buffer.slice(match.index + match[0].length);
|
|
44
|
+
const data = chunk
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.filter((line) => line.startsWith("data:"))
|
|
47
|
+
.map((line) => line.slice(5).trimStart())
|
|
48
|
+
.join("\n");
|
|
49
|
+
if (data) onData(data);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
|
|
54
|
+
let opened;
|
|
55
|
+
try {
|
|
56
|
+
opened = await openCopilotResponse(reqContext, upstream, options);
|
|
57
|
+
} finally {
|
|
58
|
+
options.releaseRequest?.();
|
|
59
|
+
}
|
|
60
|
+
const { resp, errorText } = opened;
|
|
61
|
+
reqContext = opened.reqContext;
|
|
62
|
+
if (errorText !== undefined) {
|
|
63
|
+
sendUpstreamError(res, resp, errorText);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (reqContext.body.stream) {
|
|
68
|
+
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
69
|
+
res.writeHead(resp.status, {
|
|
70
|
+
"Content-Type": resp.headers.get("content-type") || "text/event-stream",
|
|
71
|
+
"Cache-Control": "no-cache",
|
|
72
|
+
Connection: "keep-alive",
|
|
73
|
+
});
|
|
74
|
+
const reader = resp.body.getReader();
|
|
75
|
+
const decoder = new TextDecoder();
|
|
76
|
+
let buffer = "";
|
|
77
|
+
try {
|
|
78
|
+
while (true) {
|
|
79
|
+
const { done, value } = await reader.read();
|
|
80
|
+
if (done) {
|
|
81
|
+
buffer += decoder.decode();
|
|
82
|
+
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
83
|
+
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
84
|
+
}
|
|
85
|
+
readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
86
|
+
res.end();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
90
|
+
buffer += decoder.decode(value, { stream: true });
|
|
91
|
+
buffer = readSseEvents(buffer, (data) => storeCompletedResponseFromSse(reqContext, data));
|
|
92
|
+
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
93
|
+
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
94
|
+
}
|
|
95
|
+
if (!await writeOrDrain(res, value)) return;
|
|
96
|
+
}
|
|
97
|
+
} catch (e) {
|
|
98
|
+
logRequestFailure("Responses", e, options.abort);
|
|
99
|
+
await endStreamWithError(res, "responses", e, options.abort);
|
|
100
|
+
} finally {
|
|
101
|
+
await reader.cancel().catch(() => {});
|
|
102
|
+
reader.releaseLock();
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
const data = await resp.text();
|
|
106
|
+
res.writeHead(resp.status, { "Content-Type": "application/json" });
|
|
107
|
+
res.end(data);
|
|
108
|
+
if (resp.ok) {
|
|
109
|
+
try {
|
|
110
|
+
const response = JSON.parse(data);
|
|
111
|
+
rememberResponseHistory(reqContext, response);
|
|
112
|
+
recordResponsesUsage({
|
|
113
|
+
surface: reqContext.surface,
|
|
114
|
+
mode: "json",
|
|
115
|
+
model: reqContext.body?.model,
|
|
116
|
+
response,
|
|
117
|
+
event: response,
|
|
118
|
+
});
|
|
119
|
+
} catch {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|