codex-copilot-dx 0.4.35 → 0.4.37
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 +12 -0
- package/bin/cli.mjs +1 -0
- package/package.json +1 -1
- package/src/adapter.mjs +116 -866
- package/src/anthropic-handler.mjs +128 -0
- package/src/anthropic.mjs +1 -1
- package/src/auth.mjs +12 -35
- package/src/cli-options.mjs +21 -8
- 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 +6 -1
- package/src/stream-errors.mjs +17 -0
- package/src/stream.mjs +2 -7
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { adapterHealthPayload } from "./running-adapter.mjs";
|
|
3
|
+
import { copilotRuntimeStatus } from "./copilot.mjs";
|
|
4
|
+
import { imageOptimizationStats } from "./image-optimization.mjs";
|
|
5
|
+
import { responseHistoryStats } from "./response-history.mjs";
|
|
6
|
+
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
7
|
+
|
|
8
|
+
export const ADAPTER_STATUS_PATH = "/_ccdx/status";
|
|
9
|
+
const OBSERVABILITY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
10
|
+
|
|
11
|
+
const ROUTE_NAMES = Object.freeze([
|
|
12
|
+
"responses",
|
|
13
|
+
"responses_compact",
|
|
14
|
+
"models",
|
|
15
|
+
"messages",
|
|
16
|
+
"messages_count_tokens",
|
|
17
|
+
"not_found",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function emptyCounter() {
|
|
21
|
+
return {
|
|
22
|
+
total: 0,
|
|
23
|
+
active: 0,
|
|
24
|
+
completed: 0,
|
|
25
|
+
errors: 0,
|
|
26
|
+
aborted: 0,
|
|
27
|
+
duration_ms_total: 0,
|
|
28
|
+
duration_ms_max: 0,
|
|
29
|
+
status_2xx: 0,
|
|
30
|
+
status_3xx: 0,
|
|
31
|
+
status_4xx: 0,
|
|
32
|
+
status_5xx: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function statusBucket(statusCode) {
|
|
37
|
+
if (statusCode >= 200 && statusCode < 300) return "status_2xx";
|
|
38
|
+
if (statusCode >= 300 && statusCode < 400) return "status_3xx";
|
|
39
|
+
if (statusCode >= 400 && statusCode < 500) return "status_4xx";
|
|
40
|
+
if (statusCode >= 500) return "status_5xx";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function counterSnapshot(counter) {
|
|
45
|
+
return {
|
|
46
|
+
...counter,
|
|
47
|
+
duration_ms_avg: counter.completed > 0
|
|
48
|
+
? Number((counter.duration_ms_total / counter.completed).toFixed(1))
|
|
49
|
+
: 0,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function createRequestMetrics({ now = Date.now } = {}) {
|
|
54
|
+
const total = emptyCounter();
|
|
55
|
+
const routes = Object.fromEntries(ROUTE_NAMES.map((name) => [name, emptyCounter()]));
|
|
56
|
+
|
|
57
|
+
const begin = (routeName) => {
|
|
58
|
+
const route = routes[routeName] || routes.not_found;
|
|
59
|
+
const startedAt = now();
|
|
60
|
+
total.total += 1;
|
|
61
|
+
total.active += 1;
|
|
62
|
+
route.total += 1;
|
|
63
|
+
route.active += 1;
|
|
64
|
+
let completed = false;
|
|
65
|
+
|
|
66
|
+
return ({ statusCode = 0, aborted = false } = {}) => {
|
|
67
|
+
if (completed) return;
|
|
68
|
+
completed = true;
|
|
69
|
+
const durationMs = Math.max(0, now() - startedAt);
|
|
70
|
+
for (const counter of [total, route]) {
|
|
71
|
+
counter.active = Math.max(0, counter.active - 1);
|
|
72
|
+
counter.completed += 1;
|
|
73
|
+
counter.duration_ms_total += durationMs;
|
|
74
|
+
counter.duration_ms_max = Math.max(counter.duration_ms_max, durationMs);
|
|
75
|
+
if (aborted) counter.aborted += 1;
|
|
76
|
+
if (statusCode >= 400) counter.errors += 1;
|
|
77
|
+
const bucket = statusBucket(statusCode);
|
|
78
|
+
if (bucket) counter[bucket] += 1;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
begin,
|
|
85
|
+
snapshot() {
|
|
86
|
+
return {
|
|
87
|
+
...counterSnapshot(total),
|
|
88
|
+
by_route: Object.fromEntries(ROUTE_NAMES.map((name) => [name, counterSnapshot(routes[name])])),
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function classifyAdapterRoute(method, pathname) {
|
|
95
|
+
if (method === "POST" && pathname === "/v1/responses") return "responses";
|
|
96
|
+
if (method === "POST" && pathname === "/v1/responses/compact") return "responses_compact";
|
|
97
|
+
if (method === "GET" && pathname === "/v1/models") return "models";
|
|
98
|
+
if (method === "POST" && pathname === "/v1/messages") return "messages";
|
|
99
|
+
if (method === "POST" && pathname === "/v1/messages/count_tokens") return "messages_count_tokens";
|
|
100
|
+
return "not_found";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isLoopbackAddress(address) {
|
|
104
|
+
const normalized = String(address || "").trim().toLowerCase();
|
|
105
|
+
if (normalized === "::1" || normalized === "[::1]") return true;
|
|
106
|
+
const ipv4 = normalized.startsWith("::ffff:") ? normalized.slice(7) : normalized;
|
|
107
|
+
return isIP(ipv4) === 4 && ipv4.startsWith("127.");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function modelRegistryStatus(modelRegistry) {
|
|
111
|
+
const modelData = Array.isArray(modelRegistry?.models)
|
|
112
|
+
? modelRegistry.models
|
|
113
|
+
: modelRegistry?.models?.data;
|
|
114
|
+
return {
|
|
115
|
+
source: String(modelRegistry?.source || "built-in"),
|
|
116
|
+
models: Array.isArray(modelData) ? modelData.length : 0,
|
|
117
|
+
claude_models: Array.isArray(modelRegistry?.modelDefs) ? modelRegistry.modelDefs.length : 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function runtimeStatusPayload({ metrics, admission, modelRegistry } = {}) {
|
|
122
|
+
const memory = process.memoryUsage();
|
|
123
|
+
return {
|
|
124
|
+
...adapterHealthPayload(),
|
|
125
|
+
uptime_ms: Math.round(process.uptime() * 1000),
|
|
126
|
+
process: {
|
|
127
|
+
rss_bytes: memory.rss,
|
|
128
|
+
heap_used_bytes: memory.heapUsed,
|
|
129
|
+
heap_total_bytes: memory.heapTotal,
|
|
130
|
+
external_bytes: memory.external,
|
|
131
|
+
array_buffers_bytes: memory.arrayBuffers,
|
|
132
|
+
},
|
|
133
|
+
requests: metrics?.snapshot?.() || createRequestMetrics().snapshot(),
|
|
134
|
+
admission: admission?.diagnostics?.() || admission?.stats?.() || null,
|
|
135
|
+
response_history: responseHistoryStats(),
|
|
136
|
+
image_optimization: imageOptimizationStats(),
|
|
137
|
+
copilot: copilotRuntimeStatus(),
|
|
138
|
+
models: modelRegistryStatus(modelRegistry),
|
|
139
|
+
limits: {
|
|
140
|
+
max_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxBodyBytes,
|
|
141
|
+
max_decoded_body_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxDecodedBodyBytes,
|
|
142
|
+
max_sse_buffer_bytes: OBSERVABILITY_RUNTIME_CONFIG.maxSseBufferBytes,
|
|
143
|
+
response_history_max_bytes: OBSERVABILITY_RUNTIME_CONFIG.responseHistoryMaxBytes,
|
|
144
|
+
response_history_max_entries: OBSERVABILITY_RUNTIME_CONFIG.responseHistoryMaxEntries,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
const requestContextStorage = new AsyncLocalStorage();
|
|
5
|
+
|
|
6
|
+
export function createRequestId() {
|
|
7
|
+
return randomUUID();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function runWithRequestContext(context, callback) {
|
|
11
|
+
return requestContextStorage.run(context, callback);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function currentRequestContext() {
|
|
15
|
+
return requestContextStorage.getStore() || null;
|
|
16
|
+
}
|
package/src/response-history.mjs
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { httpError } from "./http-transport.mjs";
|
|
2
|
+
import { loadRuntimeConfig, parsePositiveInteger, RUNTIME_DEFAULTS } from "./runtime-config.mjs";
|
|
2
3
|
|
|
3
|
-
const DEFAULT_MAX_BYTES =
|
|
4
|
-
const DEFAULT_MAX_ENTRIES =
|
|
5
|
-
|
|
6
|
-
function positiveInt(value, fallback) {
|
|
7
|
-
const n = Number.parseInt(value, 10);
|
|
8
|
-
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
9
|
-
}
|
|
4
|
+
const DEFAULT_MAX_BYTES = RUNTIME_DEFAULTS.responseHistoryMaxBytes;
|
|
5
|
+
const DEFAULT_MAX_ENTRIES = RUNTIME_DEFAULTS.responseHistoryMaxEntries;
|
|
10
6
|
|
|
11
7
|
function cloneJson(value) {
|
|
12
8
|
return value === undefined ? undefined : structuredClone(value);
|
|
@@ -76,8 +72,9 @@ const childrenById = new Map();
|
|
|
76
72
|
const treeLru = new Map();
|
|
77
73
|
const evictedIds = new Set();
|
|
78
74
|
let totalBytes = 0;
|
|
79
|
-
|
|
80
|
-
let
|
|
75
|
+
const HISTORY_RUNTIME_CONFIG = loadRuntimeConfig();
|
|
76
|
+
let maxBytes = HISTORY_RUNTIME_CONFIG.responseHistoryMaxBytes;
|
|
77
|
+
let maxEntries = HISTORY_RUNTIME_CONFIG.responseHistoryMaxEntries;
|
|
81
78
|
|
|
82
79
|
function rememberEvictedId(id) {
|
|
83
80
|
evictedIds.add(id);
|
|
@@ -169,8 +166,8 @@ export function clearResponseHistoryForTests() {
|
|
|
169
166
|
}
|
|
170
167
|
|
|
171
168
|
export function configureResponseHistoryForTests({ maxBytes: nextMaxBytes, maxEntries: nextMaxEntries } = {}) {
|
|
172
|
-
if (nextMaxBytes !== undefined) maxBytes =
|
|
173
|
-
if (nextMaxEntries !== undefined) maxEntries =
|
|
169
|
+
if (nextMaxBytes !== undefined) maxBytes = parsePositiveInteger(nextMaxBytes, DEFAULT_MAX_BYTES);
|
|
170
|
+
if (nextMaxEntries !== undefined) maxEntries = parsePositiveInteger(nextMaxEntries, DEFAULT_MAX_ENTRIES);
|
|
174
171
|
}
|
|
175
172
|
|
|
176
173
|
export function responseHistoryStats() {
|
|
@@ -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
|
+
}
|