codex-copilot-dx 0.4.42 → 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 +28 -16
- package/bin/cli.mjs +4 -4
- package/package.json +2 -1
- package/src/adapter.mjs +17 -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 +42 -8
- package/src/doctor.mjs +11 -2
- package/src/http-transport.mjs +5 -2
- package/src/image-optimization.mjs +312 -116
- package/src/observability.mjs +2 -1
- package/src/responses-bridge.mjs +76 -20
- package/src/responses-byte-budget.mjs +314 -0
- package/src/responses-chat-payload.mjs +129 -0
- package/src/responses-compaction.mjs +113 -0
- package/src/responses-content.mjs +86 -0
- package/src/responses-handler.mjs +67 -20
- package/src/responses-image-limit.mjs +15 -26
- package/src/responses-proxy.mjs +123 -23
- package/src/responses-request.mjs +43 -6
- 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
|
@@ -31,7 +31,24 @@ export function stripInternalResponsesInputFields(inputItems) {
|
|
|
31
31
|
|
|
32
32
|
function responsesOutputItems(output) {
|
|
33
33
|
if (!Array.isArray(output)) return [];
|
|
34
|
-
return output.filter((item) =>
|
|
34
|
+
return output.filter((item) => ["message", "function_call", "custom_tool_call"].includes(item?.type));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isCompactedResponsesOutput(output) {
|
|
38
|
+
return Array.isArray(output) && output.some((item) => item?.type === "compaction");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSuccessfulCompactionResponse(response) {
|
|
42
|
+
if (!isCompactedResponsesOutput(response?.output)) return false;
|
|
43
|
+
if (Object.prototype.hasOwnProperty.call(response, "status") && response.status !== "completed") {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
return response.object === "response.compaction" || response.status === "completed";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function replayableResponsesOutputItems(output) {
|
|
50
|
+
if (!Array.isArray(output)) return [];
|
|
51
|
+
return output.filter((item) => item && typeof item === "object");
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
function stripEncryptedReasoningValue(value, state) {
|
|
@@ -62,16 +79,21 @@ function isEncryptedReasoningInputItem(item) {
|
|
|
62
79
|
export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
63
80
|
const state = { changed: false };
|
|
64
81
|
let body = cloneJson(reqContext.body);
|
|
82
|
+
let currentInputStart = reqContext.currentInputStart;
|
|
65
83
|
if (Array.isArray(body.input)) {
|
|
66
84
|
const input = [];
|
|
67
|
-
|
|
85
|
+
let retainedBeforeCurrent = 0;
|
|
86
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
87
|
+
const item = body.input[index];
|
|
68
88
|
if (isEncryptedReasoningInputItem(item)) {
|
|
69
89
|
state.changed = true;
|
|
70
90
|
continue;
|
|
71
91
|
}
|
|
72
92
|
input.push(stripEncryptedReasoningValue(item, state));
|
|
93
|
+
if (Number.isFinite(currentInputStart) && index < currentInputStart) retainedBeforeCurrent += 1;
|
|
73
94
|
}
|
|
74
95
|
body.input = input;
|
|
96
|
+
if (Number.isFinite(currentInputStart)) currentInputStart = retainedBeforeCurrent;
|
|
75
97
|
} else {
|
|
76
98
|
body = stripEncryptedReasoningValue(body, state);
|
|
77
99
|
}
|
|
@@ -84,6 +106,7 @@ export function sanitizeEncryptedReasoningRequest(reqContext) {
|
|
|
84
106
|
return {
|
|
85
107
|
...reqContext,
|
|
86
108
|
body,
|
|
109
|
+
currentInputStart,
|
|
87
110
|
inputItems: Array.isArray(body.input) ? body.input : reqContext.inputItems,
|
|
88
111
|
historyInputItems,
|
|
89
112
|
};
|
|
@@ -129,8 +152,14 @@ export function sanitizeImageNamespaceCollisionRequest(reqContext) {
|
|
|
129
152
|
export async function openCopilotResponse(reqContext, upstream = copilotResponses, options = {}) {
|
|
130
153
|
let encryptedRetried = false;
|
|
131
154
|
let imageNamespaceRetried = false;
|
|
155
|
+
let payloadPrepared = false;
|
|
132
156
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
133
|
-
const resp = await upstream(reqContext.body, {
|
|
157
|
+
const resp = await upstream(reqContext.body, {
|
|
158
|
+
signal: options.signal,
|
|
159
|
+
currentInputStart: reqContext.currentInputStart,
|
|
160
|
+
payloadPrepared,
|
|
161
|
+
});
|
|
162
|
+
payloadPrepared = true;
|
|
134
163
|
if (resp.ok) return { resp, reqContext };
|
|
135
164
|
|
|
136
165
|
const errorText = await resp.text();
|
|
@@ -164,6 +193,9 @@ function isBuiltinImageTool(tool) {
|
|
|
164
193
|
export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
165
194
|
const body = mutate ? reqBody : cloneJson(reqBody);
|
|
166
195
|
const currentInputItems = responsesInputItems(body.input, { clone: !mutate });
|
|
196
|
+
const currentInputRefs = new Set(
|
|
197
|
+
currentInputItems.filter((item) => item && typeof item === "object"),
|
|
198
|
+
);
|
|
167
199
|
const previousId = body.previous_response_id;
|
|
168
200
|
let historyItems = [];
|
|
169
201
|
|
|
@@ -193,10 +225,12 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
193
225
|
}
|
|
194
226
|
stripInternalResponsesInputFields(body.input);
|
|
195
227
|
stripInternalResponsesInputFields(historyInputItems);
|
|
228
|
+
const retainedCurrentInputStart = body.input.findIndex((item) => currentInputRefs.has(item));
|
|
196
229
|
|
|
197
230
|
return {
|
|
198
231
|
body,
|
|
199
232
|
inputItems: body.input,
|
|
233
|
+
currentInputStart: retainedCurrentInputStart < 0 ? body.input.length : retainedCurrentInputStart,
|
|
200
234
|
historyParentId: previousId ?? null,
|
|
201
235
|
historyInputItems,
|
|
202
236
|
takeHistoryOwnership: mutate,
|
|
@@ -205,11 +239,14 @@ export function prepareResponsesRequest(reqBody, { mutate = false } = {}) {
|
|
|
205
239
|
|
|
206
240
|
export function rememberResponseHistory(reqContext, responseJson) {
|
|
207
241
|
if (!responseJson?.id || !Array.isArray(reqContext?.historyInputItems || reqContext?.inputItems)) return;
|
|
208
|
-
const
|
|
209
|
-
const
|
|
242
|
+
const compacted = isSuccessfulCompactionResponse(responseJson);
|
|
243
|
+
const sourceInputItems = compacted ? [] : reqContext.historyInputItems || reqContext.inputItems;
|
|
244
|
+
const sourceOutputItems = compacted
|
|
245
|
+
? replayableResponsesOutputItems(responseJson.output)
|
|
246
|
+
: responsesOutputItems(responseJson.output);
|
|
210
247
|
rememberResponseHistoryNode({
|
|
211
248
|
id: responseJson.id,
|
|
212
|
-
parentId: reqContext.historyParentId,
|
|
249
|
+
parentId: compacted ? null : reqContext.historyParentId,
|
|
213
250
|
inputItems: sourceInputItems,
|
|
214
251
|
outputItems: sourceOutputItems,
|
|
215
252
|
takeOwnership: reqContext.takeHistoryOwnership,
|
|
@@ -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
|
+
}
|