codex-copilot-dx 0.4.43 → 0.4.44
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 +20 -12
- package/bin/cli.mjs +4 -4
- package/package.json +2 -1
- package/src/adapter.mjs +16 -8
- package/src/anthropic-handler.mjs +13 -5
- package/src/anthropic.mjs +40 -5
- package/src/auth.mjs +2 -2
- package/src/bounded-result-cache.mjs +180 -0
- package/src/claude-config.mjs +1 -1
- package/src/cli-options.mjs +7 -5
- package/src/copilot.mjs +11 -4
- package/src/doctor.mjs +11 -2
- package/src/http-transport.mjs +4 -1
- package/src/image-optimization.mjs +74 -65
- package/src/observability.mjs +2 -1
- package/src/responses-bridge.mjs +46 -10
- package/src/responses-chat-payload.mjs +2 -1
- package/src/responses-compaction.mjs +113 -0
- package/src/responses-handler.mjs +28 -14
- package/src/responses-proxy.mjs +123 -23
- package/src/stream-contract.mjs +102 -0
- package/src/stream-errors.mjs +3 -1
- package/src/stream-performance.mjs +177 -0
- package/src/upstream-headers.mjs +67 -0
package/src/responses-proxy.mjs
CHANGED
|
@@ -6,33 +6,82 @@ import {
|
|
|
6
6
|
writeOrDrain,
|
|
7
7
|
} from "./http-transport.mjs";
|
|
8
8
|
import { openCopilotResponse, rememberResponseHistory } from "./responses-request.mjs";
|
|
9
|
+
import {
|
|
10
|
+
compactionInputWithoutTrigger,
|
|
11
|
+
parseResponsesCompactionResult,
|
|
12
|
+
} from "./responses-compaction.mjs";
|
|
9
13
|
import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
10
14
|
import { endStreamWithError } from "./stream-errors.mjs";
|
|
15
|
+
import {
|
|
16
|
+
incompleteUpstreamStream,
|
|
17
|
+
invalidUpstreamStream,
|
|
18
|
+
requireUpstreamEventStream,
|
|
19
|
+
ToolArgumentDeltaGuard,
|
|
20
|
+
} from "./stream-contract.mjs";
|
|
21
|
+
import {
|
|
22
|
+
isResponsesOutputEvent,
|
|
23
|
+
markFirstOutput,
|
|
24
|
+
markOutputTokens,
|
|
25
|
+
markStreamFailure,
|
|
26
|
+
} from "./stream-performance.mjs";
|
|
27
|
+
import { safeUpstreamResponseHeaders } from "./upstream-headers.mjs";
|
|
11
28
|
import { recordResponsesUsage } from "./usage.mjs";
|
|
12
29
|
|
|
13
30
|
const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
|
|
31
|
+
const RESPONSES_TERMINAL_EVENT_TYPES = new Set([
|
|
32
|
+
"response.completed",
|
|
33
|
+
"response.incomplete",
|
|
34
|
+
"response.failed",
|
|
35
|
+
"error",
|
|
36
|
+
]);
|
|
14
37
|
|
|
15
|
-
function
|
|
16
|
-
if (!data ||
|
|
38
|
+
function inspectResponseSseEvent(state, eventName, data) {
|
|
39
|
+
if (!data || state.sawTerminal) return;
|
|
40
|
+
if (data === "[DONE]") {
|
|
41
|
+
throw invalidUpstreamStream(
|
|
42
|
+
"Upstream sent [DONE] before a terminal Responses event",
|
|
43
|
+
"upstream_stream_terminal_missing",
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
let event;
|
|
17
47
|
try {
|
|
18
|
-
|
|
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
|
-
}
|
|
48
|
+
event = JSON.parse(data);
|
|
30
49
|
} catch {
|
|
31
|
-
|
|
50
|
+
throw invalidUpstreamStream(
|
|
51
|
+
"Upstream Responses SSE event contained invalid JSON",
|
|
52
|
+
"upstream_stream_json_invalid",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
const eventType = event.type || eventName;
|
|
56
|
+
const outputTokens = event.response?.usage?.output_tokens ?? event.usage?.output_tokens;
|
|
57
|
+
if (outputTokens !== undefined) markOutputTokens(outputTokens);
|
|
58
|
+
if (!state.sawOutput && isResponsesOutputEvent(event, eventType)) {
|
|
59
|
+
state.sawOutput = true;
|
|
60
|
+
markFirstOutput();
|
|
61
|
+
}
|
|
62
|
+
if (["response.function_call_arguments.delta", "response.custom_tool_call_input.delta"].includes(eventType)) {
|
|
63
|
+
state.toolArgumentGuard.observe(event.output_index ?? event.item_id ?? event.call_id, event.delta);
|
|
32
64
|
}
|
|
65
|
+
if (!RESPONSES_TERMINAL_EVENT_TYPES.has(eventType)) return;
|
|
66
|
+
const expectedStatus = {
|
|
67
|
+
"response.completed": "completed",
|
|
68
|
+
"response.incomplete": "incomplete",
|
|
69
|
+
"response.failed": "failed",
|
|
70
|
+
}[eventType];
|
|
71
|
+
if (expectedStatus && (!event.response?.id || event.response.status !== expectedStatus)) {
|
|
72
|
+
throw invalidUpstreamStream(
|
|
73
|
+
`Upstream ${eventType} event contained an invalid response resource`,
|
|
74
|
+
"upstream_stream_terminal_invalid",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
state.sawTerminal = true;
|
|
78
|
+
if (eventType === "response.failed" || eventType === "error") markStreamFailure();
|
|
79
|
+
if (eventType !== "response.completed") return;
|
|
80
|
+
const response = event.response;
|
|
81
|
+
state.completed = { response, event };
|
|
33
82
|
}
|
|
34
83
|
|
|
35
|
-
function readSseEvents(buffer,
|
|
84
|
+
function readSseEvents(buffer, onEvent) {
|
|
36
85
|
while (true) {
|
|
37
86
|
const match = buffer.match(/\r?\n\r?\n/);
|
|
38
87
|
if (!match) return buffer;
|
|
@@ -41,15 +90,31 @@ function readSseEvents(buffer, onData) {
|
|
|
41
90
|
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
42
91
|
}
|
|
43
92
|
buffer = buffer.slice(match.index + match[0].length);
|
|
44
|
-
const
|
|
45
|
-
|
|
93
|
+
const lines = chunk.split(/\r?\n/);
|
|
94
|
+
const eventName = lines
|
|
95
|
+
.find((line) => line.startsWith("event:"))
|
|
96
|
+
?.slice(6)
|
|
97
|
+
.trim();
|
|
98
|
+
const data = lines
|
|
46
99
|
.filter((line) => line.startsWith("data:"))
|
|
47
100
|
.map((line) => line.slice(5).trimStart())
|
|
48
101
|
.join("\n");
|
|
49
|
-
if (data)
|
|
102
|
+
if (data) onEvent(eventName, data);
|
|
50
103
|
}
|
|
51
104
|
}
|
|
52
105
|
|
|
106
|
+
function storeCompletedResponse(reqContext, completed) {
|
|
107
|
+
if (!completed) return;
|
|
108
|
+
rememberResponseHistory(reqContext, completed.response);
|
|
109
|
+
recordResponsesUsage({
|
|
110
|
+
surface: reqContext.surface,
|
|
111
|
+
mode: "stream",
|
|
112
|
+
model: reqContext.body?.model,
|
|
113
|
+
response: completed.response,
|
|
114
|
+
event: completed.event,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
53
118
|
export async function proxyCopilotResponses(reqContext, req, res, upstream = copilotResponses, options = {}) {
|
|
54
119
|
let opened;
|
|
55
120
|
try {
|
|
@@ -65,15 +130,22 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
65
130
|
}
|
|
66
131
|
|
|
67
132
|
if (reqContext.body.stream) {
|
|
133
|
+
await requireUpstreamEventStream(resp);
|
|
68
134
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
69
135
|
res.writeHead(resp.status, {
|
|
70
|
-
|
|
136
|
+
...safeUpstreamResponseHeaders(resp.headers, { defaultContentType: "text/event-stream" }),
|
|
71
137
|
"Cache-Control": "no-cache",
|
|
72
138
|
Connection: "keep-alive",
|
|
73
139
|
});
|
|
74
140
|
const reader = resp.body.getReader();
|
|
75
141
|
const decoder = new TextDecoder();
|
|
76
142
|
let buffer = "";
|
|
143
|
+
const streamState = {
|
|
144
|
+
completed: null,
|
|
145
|
+
sawTerminal: false,
|
|
146
|
+
sawOutput: false,
|
|
147
|
+
toolArgumentGuard: new ToolArgumentDeltaGuard(),
|
|
148
|
+
};
|
|
77
149
|
try {
|
|
78
150
|
while (true) {
|
|
79
151
|
const { done, value } = await reader.read();
|
|
@@ -82,17 +154,24 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
82
154
|
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
83
155
|
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
84
156
|
}
|
|
85
|
-
readSseEvents(buffer, (data) =>
|
|
157
|
+
readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
|
|
158
|
+
if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
|
|
159
|
+
storeCompletedResponse(reqContext, streamState.completed);
|
|
86
160
|
res.end();
|
|
87
161
|
return;
|
|
88
162
|
}
|
|
89
163
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
90
164
|
buffer += decoder.decode(value, { stream: true });
|
|
91
|
-
buffer = readSseEvents(buffer, (data) =>
|
|
165
|
+
buffer = readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
|
|
92
166
|
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
93
167
|
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
94
168
|
}
|
|
95
169
|
if (!await writeOrDrain(res, value)) return;
|
|
170
|
+
if (streamState.sawTerminal) {
|
|
171
|
+
storeCompletedResponse(reqContext, streamState.completed);
|
|
172
|
+
res.end();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
96
175
|
}
|
|
97
176
|
} catch (e) {
|
|
98
177
|
logRequestFailure("Responses", e, options.abort);
|
|
@@ -103,7 +182,28 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
103
182
|
}
|
|
104
183
|
} else {
|
|
105
184
|
const data = await resp.text();
|
|
106
|
-
|
|
185
|
+
if (resp.ok && reqContext.surface === "responses_compact") {
|
|
186
|
+
const response = parseResponsesCompactionResult(
|
|
187
|
+
compactionInputWithoutTrigger(reqContext.body.input),
|
|
188
|
+
data,
|
|
189
|
+
);
|
|
190
|
+
rememberResponseHistory(reqContext, response);
|
|
191
|
+
recordResponsesUsage({
|
|
192
|
+
surface: reqContext.surface,
|
|
193
|
+
mode: "json",
|
|
194
|
+
model: reqContext.body?.model,
|
|
195
|
+
response,
|
|
196
|
+
event: response,
|
|
197
|
+
});
|
|
198
|
+
res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
|
|
199
|
+
contentType: "application/json",
|
|
200
|
+
}));
|
|
201
|
+
res.end(JSON.stringify(response));
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
|
|
205
|
+
contentType: "application/json",
|
|
206
|
+
}));
|
|
107
207
|
res.end(data);
|
|
108
208
|
if (resp.ok) {
|
|
109
209
|
try {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const DEFAULT_BLANK_TOOL_ARGUMENT_LIMIT = 20;
|
|
2
|
+
|
|
3
|
+
function streamProtocolError(message, code) {
|
|
4
|
+
const error = new Error(message);
|
|
5
|
+
error.statusCode = 502;
|
|
6
|
+
error.code = code;
|
|
7
|
+
error.jsonBody = {
|
|
8
|
+
error: {
|
|
9
|
+
message,
|
|
10
|
+
type: "upstream_protocol_error",
|
|
11
|
+
code,
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
return error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function invalidUpstreamStream(message, code = "upstream_stream_invalid") {
|
|
18
|
+
return streamProtocolError(message, code);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function withChatStreamUsage(request) {
|
|
22
|
+
const streamOptions = request?.stream_options && typeof request.stream_options === "object"
|
|
23
|
+
? request.stream_options
|
|
24
|
+
: {};
|
|
25
|
+
if (request?.stream === true && streamOptions.include_usage === true) return request;
|
|
26
|
+
return {
|
|
27
|
+
...request,
|
|
28
|
+
stream: true,
|
|
29
|
+
stream_options: { ...streamOptions, include_usage: true },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function requireUpstreamEventStream(response) {
|
|
34
|
+
let error = null;
|
|
35
|
+
if (!response?.body) {
|
|
36
|
+
error = streamProtocolError(
|
|
37
|
+
"Upstream returned a successful streaming response without a body",
|
|
38
|
+
"upstream_stream_body_missing",
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
const contentType = response.headers?.get?.("content-type") || "";
|
|
42
|
+
const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();
|
|
43
|
+
if (mediaType !== "text/event-stream") {
|
|
44
|
+
error = streamProtocolError(
|
|
45
|
+
"Upstream returned a successful streaming response with a non-SSE Content-Type",
|
|
46
|
+
"upstream_stream_content_type_invalid",
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!error) return;
|
|
52
|
+
await response?.body?.cancel?.().catch(() => {});
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function incompleteUpstreamStream(expectedTerminator) {
|
|
57
|
+
return streamProtocolError(
|
|
58
|
+
`Upstream stream ended before ${expectedTerminator}`,
|
|
59
|
+
"upstream_stream_incomplete",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function upstreamChatStreamError(event) {
|
|
64
|
+
const payload = event?.error;
|
|
65
|
+
if (!payload || typeof payload !== "object") return null;
|
|
66
|
+
const type = typeof payload.type === "string" && payload.type ? `${payload.type}: ` : "";
|
|
67
|
+
const message = typeof payload.message === "string" && payload.message
|
|
68
|
+
? payload.message.slice(0, 500)
|
|
69
|
+
: "Upstream stream error";
|
|
70
|
+
return streamProtocolError(
|
|
71
|
+
`Upstream Chat Completions SSE error: ${type}${message}`,
|
|
72
|
+
"upstream_chat_stream_error",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class ToolArgumentDeltaGuard {
|
|
77
|
+
constructor(limit = DEFAULT_BLANK_TOOL_ARGUMENT_LIMIT) {
|
|
78
|
+
this.limit = limit;
|
|
79
|
+
this.whitespaceRuns = new Map();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
observe(callIndex, delta) {
|
|
83
|
+
if (typeof delta !== "string") return;
|
|
84
|
+
const key = callIndex ?? "unknown";
|
|
85
|
+
let count = this.whitespaceRuns.get(key) || 0;
|
|
86
|
+
for (const character of delta) {
|
|
87
|
+
if (character === "\r" || character === "\n" || character === "\t") {
|
|
88
|
+
count += 1;
|
|
89
|
+
if (count > this.limit) {
|
|
90
|
+
this.whitespaceRuns.set(key, count);
|
|
91
|
+
throw streamProtocolError(
|
|
92
|
+
`Upstream sent more than ${this.limit} consecutive line-break or tab characters in tool arguments`,
|
|
93
|
+
"upstream_tool_arguments_stalled",
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
} else if (character !== " ") {
|
|
97
|
+
count = 0;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.whitespaceRuns.set(key, count);
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/stream-errors.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { writeOrDrain } from "./http-transport.mjs";
|
|
2
|
+
import { markStreamFailure } from "./stream-performance.mjs";
|
|
2
3
|
|
|
3
4
|
function streamErrorData(protocol, error, abort) {
|
|
4
|
-
const code = abort?.reason || "upstream_stream_error";
|
|
5
|
+
const code = abort?.reason || error?.code || "upstream_stream_error";
|
|
5
6
|
const message = `${code}: ${error?.message || "Upstream stream failed"}`;
|
|
6
7
|
if (protocol === "anthropic") {
|
|
7
8
|
return { type: "error", error: { type: "api_error", message } };
|
|
@@ -10,6 +11,7 @@ function streamErrorData(protocol, error, abort) {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export async function endStreamWithError(res, protocol, error, abort) {
|
|
14
|
+
markStreamFailure();
|
|
13
15
|
if (res.destroyed || res.writableEnded) return;
|
|
14
16
|
const data = streamErrorData(protocol, error, abort);
|
|
15
17
|
await writeOrDrain(res, `event: error\ndata: ${JSON.stringify(data)}\n\n`).catch(() => false);
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { performance } from "node:perf_hooks";
|
|
2
|
+
import { currentRequestContext } from "./request-context.mjs";
|
|
3
|
+
|
|
4
|
+
const PERFORMANCE_ROUTES = Object.freeze(["responses", "responses_compact", "messages"]);
|
|
5
|
+
const TTFT_EDGES_MS = Object.freeze([
|
|
6
|
+
100, 200, 300, 500, 700, 1_000, 1_400, 2_000, 2_800, 4_000,
|
|
7
|
+
5_500, 8_000, 12_000, 16_000, 24_000, 36_000, 60_000, 120_000, 300_000,
|
|
8
|
+
]);
|
|
9
|
+
const TPOT_EDGES_US = Object.freeze([
|
|
10
|
+
500, 1_000, 2_000, 3_333, 5_000, 6_667, 8_333, 10_000, 12_500, 14_286,
|
|
11
|
+
16_667, 20_000, 25_000, 33_333, 40_000, 50_000, 66_667, 100_000,
|
|
12
|
+
150_000, 250_000, 500_000, 1_000_000, 2_500_000, 10_000_000,
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
const RESPONSES_OUTPUT_EVENT_TYPES = new Set([
|
|
16
|
+
"response.output_text.delta",
|
|
17
|
+
"response.function_call_arguments.delta",
|
|
18
|
+
"response.custom_tool_call_input.delta",
|
|
19
|
+
"response.refusal.delta",
|
|
20
|
+
"response.reasoning_text.delta",
|
|
21
|
+
"response.reasoning_summary_text.delta",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function nonEmptyString(value) {
|
|
25
|
+
return typeof value === "string" && value.length > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function isResponsesOutputEvent(event, eventType = event?.type) {
|
|
29
|
+
return RESPONSES_OUTPUT_EVENT_TYPES.has(eventType) && nonEmptyString(event?.delta);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isChatOutputDelta(delta) {
|
|
33
|
+
if (!delta || typeof delta !== "object") return false;
|
|
34
|
+
return nonEmptyString(delta.content)
|
|
35
|
+
|| (Array.isArray(delta.tool_calls) && delta.tool_calls.length > 0)
|
|
36
|
+
|| nonEmptyString(delta.refusal)
|
|
37
|
+
|| nonEmptyString(delta.reasoning)
|
|
38
|
+
|| nonEmptyString(delta.reasoning_content)
|
|
39
|
+
|| nonEmptyString(delta.reasoning_text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createHistogram(edges) {
|
|
43
|
+
return {
|
|
44
|
+
edges,
|
|
45
|
+
counts: new Uint32Array(edges.length + 1),
|
|
46
|
+
samples: 0,
|
|
47
|
+
total: 0,
|
|
48
|
+
max: 0,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function observe(histogram, value) {
|
|
53
|
+
if (!Number.isFinite(value) || value < 0) return;
|
|
54
|
+
let index = 0;
|
|
55
|
+
while (index < histogram.edges.length && value > histogram.edges[index]) index += 1;
|
|
56
|
+
histogram.counts[index] += 1;
|
|
57
|
+
histogram.samples += 1;
|
|
58
|
+
histogram.total += value;
|
|
59
|
+
histogram.max = Math.max(histogram.max, value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function histogramSnapshot(histogram, unit) {
|
|
63
|
+
let lower = 0;
|
|
64
|
+
const buckets = histogram.edges.map((upper, index) => {
|
|
65
|
+
const bucket = { lower, upper, count: histogram.counts[index] };
|
|
66
|
+
lower = upper;
|
|
67
|
+
return bucket;
|
|
68
|
+
});
|
|
69
|
+
buckets.push({ lower, upper: null, count: histogram.counts.at(-1) });
|
|
70
|
+
return {
|
|
71
|
+
unit,
|
|
72
|
+
samples: histogram.samples,
|
|
73
|
+
avg: histogram.samples > 0 ? Number((histogram.total / histogram.samples).toFixed(1)) : 0,
|
|
74
|
+
max: histogram.max,
|
|
75
|
+
buckets,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createRoutePerformance() {
|
|
80
|
+
return {
|
|
81
|
+
ttft: createHistogram(TTFT_EDGES_MS),
|
|
82
|
+
tpot: createHistogram(TPOT_EDGES_US),
|
|
83
|
+
success_with_output: 0,
|
|
84
|
+
errors_with_output: 0,
|
|
85
|
+
zero_output_errors: 0,
|
|
86
|
+
neutral: 0,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function routeSnapshot(route) {
|
|
91
|
+
return {
|
|
92
|
+
success_with_output: route.success_with_output,
|
|
93
|
+
errors_with_output: route.errors_with_output,
|
|
94
|
+
zero_output_errors: route.zero_output_errors,
|
|
95
|
+
neutral: route.neutral,
|
|
96
|
+
ttft_ms: histogramSnapshot(route.ttft, "ms"),
|
|
97
|
+
tpot_us: histogramSnapshot(route.tpot, "us"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function createStreamPerformanceMetrics({ now = () => performance.now() } = {}) {
|
|
102
|
+
const routes = Object.fromEntries(PERFORMANCE_ROUTES.map((name) => [name, createRoutePerformance()]));
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
begin(routeName) {
|
|
106
|
+
const route = routes[routeName];
|
|
107
|
+
if (!route) return null;
|
|
108
|
+
let upstreamStartedAt = null;
|
|
109
|
+
let firstOutputAt = null;
|
|
110
|
+
let outputTokens = null;
|
|
111
|
+
let failed = false;
|
|
112
|
+
let finished = false;
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
upstreamStarted() {
|
|
116
|
+
if (upstreamStartedAt === null) upstreamStartedAt = now();
|
|
117
|
+
},
|
|
118
|
+
firstOutput() {
|
|
119
|
+
if (firstOutputAt === null) firstOutputAt = now();
|
|
120
|
+
},
|
|
121
|
+
setOutputTokens(value) {
|
|
122
|
+
const tokens = Number(value);
|
|
123
|
+
if (Number.isFinite(tokens) && tokens >= 0) outputTokens = tokens;
|
|
124
|
+
},
|
|
125
|
+
fail() {
|
|
126
|
+
failed = true;
|
|
127
|
+
},
|
|
128
|
+
finish({ failed: finishFailed = false } = {}) {
|
|
129
|
+
if (finished) return;
|
|
130
|
+
finished = true;
|
|
131
|
+
failed ||= finishFailed;
|
|
132
|
+
const finishedAt = now();
|
|
133
|
+
if (upstreamStartedAt === null) {
|
|
134
|
+
route.neutral += 1;
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (firstOutputAt === null) {
|
|
138
|
+
if (failed) route.zero_output_errors += 1;
|
|
139
|
+
else route.neutral += 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
observe(route.ttft, Math.max(0, firstOutputAt - upstreamStartedAt));
|
|
143
|
+
if (outputTokens !== null && outputTokens >= 2) {
|
|
144
|
+
observe(route.tpot, Math.max(0, ((finishedAt - firstOutputAt) * 1_000) / (outputTokens - 1)));
|
|
145
|
+
}
|
|
146
|
+
if (failed) route.errors_with_output += 1;
|
|
147
|
+
else route.success_with_output += 1;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
snapshot() {
|
|
152
|
+
return {
|
|
153
|
+
by_route: Object.fromEntries(PERFORMANCE_ROUTES.map((name) => [name, routeSnapshot(routes[name])])),
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function tracker() {
|
|
160
|
+
return currentRequestContext()?.streamPerformance || null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function markUpstreamStarted() {
|
|
164
|
+
tracker()?.upstreamStarted();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function markFirstOutput() {
|
|
168
|
+
tracker()?.firstOutput();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function markOutputTokens(value) {
|
|
172
|
+
tracker()?.setOutputTokens(value);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function markStreamFailure() {
|
|
176
|
+
tracker()?.fail();
|
|
177
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const REQUEST_ID_HEADERS = [
|
|
2
|
+
"x-request-id",
|
|
3
|
+
"x-github-request-id",
|
|
4
|
+
"x-ms-request-id",
|
|
5
|
+
"request-id",
|
|
6
|
+
"x-correlation-id",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
const SAFE_EXACT_HEADERS = new Set([
|
|
10
|
+
"retry-after",
|
|
11
|
+
"ratelimit-limit",
|
|
12
|
+
"ratelimit-remaining",
|
|
13
|
+
"ratelimit-reset",
|
|
14
|
+
"ratelimit-policy",
|
|
15
|
+
"openai-model",
|
|
16
|
+
"openai-processing-ms",
|
|
17
|
+
"traceparent",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const SAFE_HEADER_PREFIXES = [
|
|
21
|
+
"x-ratelimit-",
|
|
22
|
+
"x-ms-ratelimit-",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function headerValue(headers, name) {
|
|
26
|
+
const value = headers?.get?.(name);
|
|
27
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function canonicalHeaderName(name) {
|
|
31
|
+
return String(name).split("-").map((part) => (
|
|
32
|
+
part ? `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}` : part
|
|
33
|
+
)).join("-");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isSafeMetadataHeader(name) {
|
|
37
|
+
return SAFE_EXACT_HEADERS.has(name)
|
|
38
|
+
|| SAFE_HEADER_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function safeUpstreamResponseHeaders(headers, {
|
|
42
|
+
contentType,
|
|
43
|
+
defaultContentType,
|
|
44
|
+
} = {}) {
|
|
45
|
+
const out = {};
|
|
46
|
+
const resolvedContentType = contentType
|
|
47
|
+
?? headerValue(headers, "content-type")
|
|
48
|
+
?? defaultContentType;
|
|
49
|
+
if (resolvedContentType) out["Content-Type"] = resolvedContentType;
|
|
50
|
+
|
|
51
|
+
if (headers && typeof headers[Symbol.iterator] === "function") {
|
|
52
|
+
for (const [rawName, value] of headers) {
|
|
53
|
+
const name = String(rawName).toLowerCase();
|
|
54
|
+
if (!isSafeMetadataHeader(name)) continue;
|
|
55
|
+
out[canonicalHeaderName(name)] = value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const name of REQUEST_ID_HEADERS) {
|
|
60
|
+
const value = headerValue(headers, name);
|
|
61
|
+
if (value) {
|
|
62
|
+
out["X-Upstream-Request-Id"] = value;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|