codex-copilot-dx 0.6.12 → 0.6.14
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 +31 -41
- package/package.json +1 -1
- package/scripts/benchmark.mjs +74 -0
- package/src/anthropic.mjs +22 -2
- package/src/config.mjs +38 -3
- package/src/doctor.mjs +3 -4
- package/src/pm-studio-asar.mjs +136 -22
- package/src/pm-studio-setup.mjs +103 -301
- package/src/pm-studio-status.mjs +15 -22
- package/src/responses-bridge.mjs +44 -7
- package/src/responses-compaction.mjs +1 -50
- package/src/responses-proxy.mjs +116 -35
- package/src/responses-request.mjs +2 -3
- package/src/stream.mjs +27 -13
- package/src/pm-studio-provenance.mjs +0 -701
package/src/pm-studio-status.mjs
CHANGED
|
@@ -40,12 +40,6 @@ const PM_RELAY_ROUTE_NAMES = Object.freeze([
|
|
|
40
40
|
"pm_chat_completions",
|
|
41
41
|
]);
|
|
42
42
|
|
|
43
|
-
function selectRecipe(metadata, recipes) {
|
|
44
|
-
return recipes.find((recipe) => recipe.version === String(metadata.version)
|
|
45
|
-
&& recipe.build === String(metadata.build)
|
|
46
|
-
&& recipe.bundleIdentifier === String(metadata.bundleIdentifier));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
43
|
function safeMessage(error) {
|
|
50
44
|
return String(error?.message || "inspection failed").replace(/\s+/g, " ").trim();
|
|
51
45
|
}
|
|
@@ -169,21 +163,20 @@ export async function inspectPmStudioStatus({
|
|
|
169
163
|
build: String(metadata.build),
|
|
170
164
|
bundleIdentifier: String(metadata.bundleIdentifier),
|
|
171
165
|
};
|
|
172
|
-
let recipe =
|
|
166
|
+
let recipe = null;
|
|
173
167
|
let compatibilityError = null;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
168
|
+
try {
|
|
169
|
+
recipe = resolveCompatibleRecipe({
|
|
170
|
+
appPath,
|
|
171
|
+
home,
|
|
172
|
+
backupRoot,
|
|
173
|
+
metadata: normalized,
|
|
174
|
+
recipes,
|
|
175
|
+
operations,
|
|
176
|
+
inspectApp,
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
compatibilityError = error;
|
|
187
180
|
}
|
|
188
181
|
if (recipe) {
|
|
189
182
|
app = { ...inspectApp({ appPath, recipe, operations }), recipe: recipe.id };
|
|
@@ -272,8 +265,8 @@ function appStatusItem(result, commandName) {
|
|
|
272
265
|
if (app.state === "legacy") {
|
|
273
266
|
return { kind: "err", component: "App patch", detail: `${version}; legacy global-origin patch`, message: `PM Studio ${version} has a legacy global-origin patch; run ${commandName} pms setup to migrate` };
|
|
274
267
|
}
|
|
275
|
-
if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version};
|
|
276
|
-
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version};
|
|
268
|
+
if (app.state === "clean") return { kind: "warn", component: "App patch", detail: `${version}; compatible structure, not patched`, message: `PM Studio ${version} has a compatible local patch structure but is not patched; run ${commandName} pms setup` };
|
|
269
|
+
if (app.state === "unsupported") return { kind: "err", component: "App patch", detail: `${version}; structure incompatible`, message: `PM Studio ${version} does not expose one uniquely compatible patch structure; no files will be changed` };
|
|
277
270
|
if (app.state === "drift" && app.patchRecipeMatched) {
|
|
278
271
|
return { kind: "err", component: "App patch", detail: `${version}; patch record drift`, message: `PM Studio ${version} matches the patch recipe, but its installed patch record is not verified: ${app.issues.join("; ")}` };
|
|
279
272
|
}
|
package/src/responses-bridge.mjs
CHANGED
|
@@ -38,6 +38,23 @@ export function responsesBodyUsesCustomTools(body) {
|
|
|
38
38
|
&& body.input.some((item) => item?.type === "custom_tool_call" || item?.type === "custom_tool_call_output");
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function chatCompatibilityError(kind, itemType) {
|
|
42
|
+
const type = typeof itemType === "string" && itemType ? itemType : "unknown";
|
|
43
|
+
const message = `Responses ${kind} type "${type}" cannot be represented by Chat Completions`;
|
|
44
|
+
const error = new Error(message);
|
|
45
|
+
error.statusCode = 400;
|
|
46
|
+
error.code = "ccdx_responses_chat_incompatible";
|
|
47
|
+
error.jsonBody = {
|
|
48
|
+
error: {
|
|
49
|
+
message,
|
|
50
|
+
type: "invalid_request_error",
|
|
51
|
+
code: error.code,
|
|
52
|
+
item_type: type,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
return error;
|
|
56
|
+
}
|
|
57
|
+
|
|
41
58
|
export function responsesToChat(body) {
|
|
42
59
|
const messages = [];
|
|
43
60
|
if (body.instructions) messages.push({ role: "system", content: body.instructions });
|
|
@@ -65,19 +82,36 @@ export function responsesToChat(body) {
|
|
|
65
82
|
if (typeof body.input === "string") {
|
|
66
83
|
messages.push({ role: "user", content: body.input });
|
|
67
84
|
} else if (Array.isArray(body.input)) {
|
|
68
|
-
for (
|
|
69
|
-
|
|
85
|
+
for (let index = 0; index < body.input.length; index += 1) {
|
|
86
|
+
const item = body.input[index];
|
|
87
|
+
const isEasyMessage = item?.type === undefined
|
|
88
|
+
&& ["user", "assistant", "system", "developer"].includes(item?.role)
|
|
89
|
+
&& Object.prototype.hasOwnProperty.call(item, "content");
|
|
90
|
+
if (item?.type === "message" || isEasyMessage) {
|
|
70
91
|
messages.push({ role: item.role, content: messageContent(item.content) });
|
|
71
|
-
} else if (["input_image", "image_url", "image"].includes(item
|
|
92
|
+
} else if (["input_image", "image_url", "image"].includes(item?.type)) {
|
|
72
93
|
messages.push({ role: "user", content: messageContent([item]) });
|
|
73
|
-
} else if (item
|
|
94
|
+
} else if (item?.type === "function_call") {
|
|
95
|
+
const toolCalls = [];
|
|
96
|
+
while (index < body.input.length && body.input[index]?.type === "function_call") {
|
|
97
|
+
const call = body.input[index];
|
|
98
|
+
toolCalls.push({
|
|
99
|
+
id: call.call_id || randomUUID(),
|
|
100
|
+
type: "function",
|
|
101
|
+
function: { name: call.name, arguments: call.arguments },
|
|
102
|
+
});
|
|
103
|
+
index += 1;
|
|
104
|
+
}
|
|
105
|
+
index -= 1;
|
|
74
106
|
messages.push({
|
|
75
107
|
role: "assistant",
|
|
76
108
|
content: null,
|
|
77
|
-
tool_calls:
|
|
109
|
+
tool_calls: toolCalls,
|
|
78
110
|
});
|
|
79
|
-
} else if (item
|
|
111
|
+
} else if (item?.type === "function_call_output") {
|
|
80
112
|
messages.push({ role: "tool", tool_call_id: item.call_id, content: typeof item.output === "string" ? item.output : JSON.stringify(item.output) });
|
|
113
|
+
} else {
|
|
114
|
+
throw chatCompatibilityError("input item", item?.type);
|
|
81
115
|
}
|
|
82
116
|
}
|
|
83
117
|
}
|
|
@@ -90,9 +124,10 @@ export function responsesToChat(body) {
|
|
|
90
124
|
if (maxTok !== undefined) chatReq.max_completion_tokens = maxTok;
|
|
91
125
|
|
|
92
126
|
if (body.tools?.length) {
|
|
127
|
+
const unsupportedTool = body.tools.find((tool) => tool?.type !== "function");
|
|
128
|
+
if (unsupportedTool) throw chatCompatibilityError("tool", unsupportedTool?.type);
|
|
93
129
|
chatReq.tools = body.tools
|
|
94
130
|
.map((tool) => {
|
|
95
|
-
if (tool?.type !== "function") return null;
|
|
96
131
|
if (tool.function?.name) return cloneJson(tool);
|
|
97
132
|
const fn = {
|
|
98
133
|
name: tool.name,
|
|
@@ -112,6 +147,8 @@ export function responsesToChat(body) {
|
|
|
112
147
|
chatReq.tool_choice = body.tool_choice;
|
|
113
148
|
} else if (body.tool_choice?.type === "function" && body.tool_choice.name) {
|
|
114
149
|
chatReq.tool_choice = { type: "function", function: { name: body.tool_choice.name } };
|
|
150
|
+
} else {
|
|
151
|
+
throw chatCompatibilityError("tool choice", body.tool_choice?.type);
|
|
115
152
|
}
|
|
116
153
|
}
|
|
117
154
|
if (body.parallel_tool_calls !== undefined) chatReq.parallel_tool_calls = body.parallel_tool_calls;
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
1
|
import { httpError } from "./http-transport.mjs";
|
|
3
2
|
|
|
4
3
|
const COMPACTION_TRIGGER_TYPE = "compaction_trigger";
|
|
5
|
-
const RETAINED_MESSAGE_ROLES = new Set(["user", "assistant", "developer", "system"]);
|
|
6
|
-
const RETAINED_MESSAGE_TOKEN_BUDGET = 64_000;
|
|
7
|
-
const APPROX_BYTES_PER_TOKEN = 4;
|
|
8
4
|
|
|
9
5
|
function compactionError(message) {
|
|
10
6
|
const error = httpError(message, 502);
|
|
@@ -19,47 +15,6 @@ function compactionError(message) {
|
|
|
19
15
|
return error;
|
|
20
16
|
}
|
|
21
17
|
|
|
22
|
-
function normalizeMessageContent(content) {
|
|
23
|
-
if (typeof content === "string") return [{ type: "input_text", text: content }];
|
|
24
|
-
if (!Array.isArray(content)) return [];
|
|
25
|
-
return content.map((part) => {
|
|
26
|
-
if (!part || typeof part !== "object") return part;
|
|
27
|
-
return part.type === "output_text"
|
|
28
|
-
? { ...part, type: "input_text" }
|
|
29
|
-
: structuredClone(part);
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function approximateTextTokens(content) {
|
|
34
|
-
return content.reduce((total, part) => {
|
|
35
|
-
if (part?.type !== "input_text" || typeof part.text !== "string") return total;
|
|
36
|
-
return total + Math.ceil(Buffer.byteLength(part.text) / APPROX_BYTES_PER_TOKEN);
|
|
37
|
-
}, 0);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function retainedMessages(inputItems) {
|
|
41
|
-
const kept = [];
|
|
42
|
-
let usedTokens = 0;
|
|
43
|
-
for (let index = inputItems.length - 1; index >= 0; index -= 1) {
|
|
44
|
-
const item = inputItems[index];
|
|
45
|
-
if (item?.type !== "message" || !RETAINED_MESSAGE_ROLES.has(item.role)) continue;
|
|
46
|
-
|
|
47
|
-
const content = normalizeMessageContent(item.content);
|
|
48
|
-
usedTokens += Math.max(approximateTextTokens(content), 1);
|
|
49
|
-
if (usedTokens > RETAINED_MESSAGE_TOKEN_BUDGET && kept.length > 0) break;
|
|
50
|
-
|
|
51
|
-
kept.push({
|
|
52
|
-
type: "message",
|
|
53
|
-
id: `msg_${randomUUID().replaceAll("-", "")}`,
|
|
54
|
-
status: item.status ?? "completed",
|
|
55
|
-
role: item.role,
|
|
56
|
-
content,
|
|
57
|
-
...(item.phase !== undefined ? { phase: structuredClone(item.phase) } : {}),
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return kept.reverse();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
18
|
export function prepareResponsesCompactionRequest(reqContext) {
|
|
64
19
|
const input = Array.isArray(reqContext?.body?.input) ? reqContext.body.input : [];
|
|
65
20
|
const compactionInputItems = compactionInputWithoutTrigger(input);
|
|
@@ -74,7 +29,7 @@ export function compactionInputWithoutTrigger(input) {
|
|
|
74
29
|
: [];
|
|
75
30
|
}
|
|
76
31
|
|
|
77
|
-
export function createResponsesCompactionResult(
|
|
32
|
+
export function createResponsesCompactionResult(_inputItems, generated) {
|
|
78
33
|
if (!generated || typeof generated !== "object" || !generated.id || !Array.isArray(generated.output)) {
|
|
79
34
|
throw compactionError("Copilot compaction returned an invalid response envelope");
|
|
80
35
|
}
|
|
@@ -95,10 +50,6 @@ export function createResponsesCompactionResult(inputItems, generated) {
|
|
|
95
50
|
return {
|
|
96
51
|
...generated,
|
|
97
52
|
object: "response.compaction",
|
|
98
|
-
output: [
|
|
99
|
-
...retainedMessages(Array.isArray(inputItems) ? inputItems : []),
|
|
100
|
-
...compactionItems,
|
|
101
|
-
],
|
|
102
53
|
};
|
|
103
54
|
}
|
|
104
55
|
|
package/src/responses-proxy.mjs
CHANGED
|
@@ -81,15 +81,38 @@ function inspectResponseSseEvent(state, eventName, data) {
|
|
|
81
81
|
state.completed = { response, event };
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
84
|
+
function createResponseSseInspector(state) {
|
|
85
|
+
let eventBuffer = Buffer.allocUnsafe(Math.min(4096, MAX_SSE_BUFFER_BYTES + 4));
|
|
86
|
+
let eventLength = 0;
|
|
87
|
+
let previous1 = -1;
|
|
88
|
+
let previous2 = -1;
|
|
89
|
+
let previous3 = -1;
|
|
90
|
+
|
|
91
|
+
const ensureCapacity = (required) => {
|
|
92
|
+
if (required > MAX_SSE_BUFFER_BYTES + 4) {
|
|
93
|
+
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
94
|
+
}
|
|
95
|
+
if (required <= eventBuffer.length) return;
|
|
96
|
+
let capacity = eventBuffer.length;
|
|
97
|
+
while (capacity < required) capacity = Math.min(MAX_SSE_BUFFER_BYTES + 4, Math.max(capacity * 2, required));
|
|
98
|
+
const next = Buffer.allocUnsafe(capacity);
|
|
99
|
+
eventBuffer.copy(next, 0, 0, eventLength);
|
|
100
|
+
eventBuffer = next;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const append = (bytes) => {
|
|
104
|
+
if (!bytes.byteLength) return;
|
|
105
|
+
ensureCapacity(eventLength + bytes.byteLength);
|
|
106
|
+
eventBuffer.set(bytes, eventLength);
|
|
107
|
+
eventLength += bytes.byteLength;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const inspectEvent = (delimiterLength) => {
|
|
111
|
+
const contentLength = eventLength - delimiterLength;
|
|
112
|
+
if (contentLength > MAX_SSE_BUFFER_BYTES) {
|
|
90
113
|
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
91
114
|
}
|
|
92
|
-
|
|
115
|
+
const chunk = eventBuffer.subarray(0, contentLength).toString("utf8");
|
|
93
116
|
const lines = chunk.split(/\r?\n/);
|
|
94
117
|
const eventName = lines
|
|
95
118
|
.find((line) => line.startsWith("event:"))
|
|
@@ -99,8 +122,73 @@ function readSseEvents(buffer, onEvent) {
|
|
|
99
122
|
.filter((line) => line.startsWith("data:"))
|
|
100
123
|
.map((line) => line.slice(5).trimStart())
|
|
101
124
|
.join("\n");
|
|
102
|
-
if (data)
|
|
125
|
+
if (data) inspectResponseSseEvent(state, eventName, data);
|
|
126
|
+
eventLength = 0;
|
|
127
|
+
previous1 = -1;
|
|
128
|
+
previous2 = -1;
|
|
129
|
+
previous3 = -1;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
push(value) {
|
|
134
|
+
let segmentStart = 0;
|
|
135
|
+
for (let index = 0; index < value.byteLength; index += 1) {
|
|
136
|
+
const byte = value[index];
|
|
137
|
+
let delimiterLength = 0;
|
|
138
|
+
if (byte === 0x0a && previous1 === 0x0a) {
|
|
139
|
+
delimiterLength = previous2 === 0x0d ? 3 : 2;
|
|
140
|
+
} else if (byte === 0x0a && previous1 === 0x0d && previous2 === 0x0a) {
|
|
141
|
+
delimiterLength = previous3 === 0x0d ? 4 : 3;
|
|
142
|
+
}
|
|
143
|
+
if (delimiterLength) {
|
|
144
|
+
append(value.subarray(segmentStart, index + 1));
|
|
145
|
+
inspectEvent(delimiterLength);
|
|
146
|
+
segmentStart = index + 1;
|
|
147
|
+
if (state.sawTerminal) return index + 1;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
previous3 = previous2;
|
|
151
|
+
previous2 = previous1;
|
|
152
|
+
previous1 = byte;
|
|
153
|
+
}
|
|
154
|
+
append(value.subarray(segmentStart));
|
|
155
|
+
if (eventLength > MAX_SSE_BUFFER_BYTES + 3) {
|
|
156
|
+
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function invalidResponsesResponse(message) {
|
|
164
|
+
const error = httpError(message, 502);
|
|
165
|
+
error.code = "ccdx_invalid_responses_response";
|
|
166
|
+
error.jsonBody = {
|
|
167
|
+
error: {
|
|
168
|
+
message,
|
|
169
|
+
type: "upstream_error",
|
|
170
|
+
code: error.code,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
return error;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function parseSuccessfulResponsesResult(text) {
|
|
177
|
+
let response;
|
|
178
|
+
try {
|
|
179
|
+
response = JSON.parse(text);
|
|
180
|
+
} catch {
|
|
181
|
+
throw invalidResponsesResponse("Copilot Responses returned invalid JSON");
|
|
182
|
+
}
|
|
183
|
+
if (!response
|
|
184
|
+
|| typeof response !== "object"
|
|
185
|
+
|| Array.isArray(response)
|
|
186
|
+
|| typeof response.id !== "string"
|
|
187
|
+
|| response.id.length === 0
|
|
188
|
+
|| !Array.isArray(response.output)) {
|
|
189
|
+
throw invalidResponsesResponse("Copilot Responses returned an invalid response envelope");
|
|
103
190
|
}
|
|
191
|
+
return response;
|
|
104
192
|
}
|
|
105
193
|
|
|
106
194
|
function storeCompletedResponse(reqContext, completed) {
|
|
@@ -138,35 +226,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
138
226
|
Connection: "keep-alive",
|
|
139
227
|
});
|
|
140
228
|
const reader = resp.body.getReader();
|
|
141
|
-
const decoder = new TextDecoder();
|
|
142
|
-
let buffer = "";
|
|
143
229
|
const streamState = {
|
|
144
230
|
completed: null,
|
|
145
231
|
sawTerminal: false,
|
|
146
232
|
sawOutput: false,
|
|
147
233
|
toolArgumentGuard: new ToolArgumentDeltaGuard(),
|
|
148
234
|
};
|
|
235
|
+
const inspector = createResponseSseInspector(streamState);
|
|
149
236
|
try {
|
|
150
237
|
while (true) {
|
|
151
238
|
const { done, value } = await reader.read();
|
|
152
239
|
if (done) {
|
|
153
|
-
buffer += decoder.decode();
|
|
154
|
-
if (Buffer.byteLength(buffer) > MAX_SSE_BUFFER_BYTES) {
|
|
155
|
-
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
156
|
-
}
|
|
157
|
-
readSseEvents(buffer, (eventName, data) => inspectResponseSseEvent(streamState, eventName, data));
|
|
158
240
|
if (!streamState.sawTerminal) throw incompleteUpstreamStream("a terminal Responses event");
|
|
159
241
|
storeCompletedResponse(reqContext, streamState.completed);
|
|
160
242
|
res.end();
|
|
161
243
|
return { successful: Boolean(streamState.completed), compacted: false };
|
|
162
244
|
}
|
|
163
245
|
options.abort?.setTimeout(options.streamIdleTimeoutMs, "stream_idle_timeout");
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
if (
|
|
167
|
-
throw httpError(`Upstream SSE buffer exceeds ${MAX_SSE_BUFFER_BYTES} bytes`, 502);
|
|
168
|
-
}
|
|
169
|
-
if (!await writeOrDrain(res, value)) return;
|
|
246
|
+
const terminalOffset = inspector.push(value);
|
|
247
|
+
const forwarded = terminalOffset === null ? value : value.subarray(0, terminalOffset);
|
|
248
|
+
if (forwarded.byteLength > 0 && !await writeOrDrain(res, forwarded)) return;
|
|
170
249
|
if (streamState.sawTerminal) {
|
|
171
250
|
storeCompletedResponse(reqContext, streamState.completed);
|
|
172
251
|
res.end();
|
|
@@ -202,24 +281,26 @@ export async function proxyCopilotResponses(reqContext, req, res, upstream = cop
|
|
|
202
281
|
res.end(JSON.stringify(response));
|
|
203
282
|
return { successful: true, compacted: true };
|
|
204
283
|
}
|
|
284
|
+
if (resp.ok) {
|
|
285
|
+
const response = parseSuccessfulResponsesResult(data);
|
|
286
|
+
rememberResponseHistory(reqContext, response);
|
|
287
|
+
recordResponsesUsage({
|
|
288
|
+
surface: reqContext.surface,
|
|
289
|
+
mode: "json",
|
|
290
|
+
model: reqContext.body?.model,
|
|
291
|
+
response,
|
|
292
|
+
event: response,
|
|
293
|
+
});
|
|
294
|
+
res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
|
|
295
|
+
contentType: "application/json",
|
|
296
|
+
}));
|
|
297
|
+
res.end(data);
|
|
298
|
+
return { successful: true, compacted: false };
|
|
299
|
+
}
|
|
205
300
|
res.writeHead(resp.status, safeUpstreamResponseHeaders(resp.headers, {
|
|
206
301
|
contentType: "application/json",
|
|
207
302
|
}));
|
|
208
303
|
res.end(data);
|
|
209
|
-
if (resp.ok) {
|
|
210
|
-
try {
|
|
211
|
-
const response = JSON.parse(data);
|
|
212
|
-
rememberResponseHistory(reqContext, response);
|
|
213
|
-
recordResponsesUsage({
|
|
214
|
-
surface: reqContext.surface,
|
|
215
|
-
mode: "json",
|
|
216
|
-
model: reqContext.body?.model,
|
|
217
|
-
response,
|
|
218
|
-
event: response,
|
|
219
|
-
});
|
|
220
|
-
return { successful: true, compacted: false };
|
|
221
|
-
} catch {}
|
|
222
|
-
}
|
|
223
304
|
return { successful: false, compacted: false };
|
|
224
305
|
}
|
|
225
306
|
}
|
|
@@ -46,7 +46,7 @@ export function stripInternalResponsesInputFields(inputItems, { assertActive } =
|
|
|
46
46
|
|
|
47
47
|
function responsesOutputItems(output) {
|
|
48
48
|
if (!Array.isArray(output)) return [];
|
|
49
|
-
return output.filter((item) =>
|
|
49
|
+
return output.filter((item) => item && typeof item === "object");
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function isCompactedResponsesOutput(output) {
|
|
@@ -62,8 +62,7 @@ function isSuccessfulCompactionResponse(response) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function replayableResponsesOutputItems(output) {
|
|
65
|
-
|
|
66
|
-
return output.filter((item) => item && typeof item === "object");
|
|
65
|
+
return responsesOutputItems(output);
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
function isEncryptedContentPart(value) {
|
package/src/stream.mjs
CHANGED
|
@@ -4,33 +4,47 @@ import { loadRuntimeConfig } from "./runtime-config.mjs";
|
|
|
4
4
|
|
|
5
5
|
const MAX_SSE_BUFFER_BYTES = loadRuntimeConfig().maxSseBufferBytes;
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
if (
|
|
7
|
+
function appendLineFragment(parts, fragment, currentBytes, maxBufferBytes) {
|
|
8
|
+
if (!fragment) return currentBytes;
|
|
9
|
+
const nextBytes = currentBytes + Buffer.byteLength(fragment);
|
|
10
|
+
if (nextBytes > maxBufferBytes) {
|
|
9
11
|
throw new Error(`Upstream SSE buffer exceeds ${maxBufferBytes} bytes`);
|
|
10
12
|
}
|
|
13
|
+
parts.push(fragment);
|
|
14
|
+
return nextBytes;
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export async function* webStreamLines(response, { onChunk, maxBufferBytes = MAX_SSE_BUFFER_BYTES } = {}) {
|
|
14
18
|
const reader = response.body.getReader();
|
|
15
19
|
const decoder = new TextDecoder();
|
|
16
|
-
let
|
|
20
|
+
let lineParts = [];
|
|
21
|
+
let lineBytes = 0;
|
|
17
22
|
try {
|
|
18
23
|
while (true) {
|
|
19
24
|
const { done, value } = await reader.read();
|
|
20
25
|
if (done) break;
|
|
21
26
|
onChunk?.(value);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const decoded = decoder.decode(value, { stream: true });
|
|
28
|
+
let start = 0;
|
|
29
|
+
while (start < decoded.length) {
|
|
30
|
+
const newline = decoded.indexOf("\n", start);
|
|
31
|
+
if (newline === -1) {
|
|
32
|
+
lineBytes = appendLineFragment(lineParts, decoded.slice(start), lineBytes, maxBufferBytes);
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
lineBytes = appendLineFragment(lineParts, decoded.slice(start, newline), lineBytes, maxBufferBytes);
|
|
36
|
+
const line = lineParts.length > 1 ? lineParts.join("") : (lineParts[0] || "");
|
|
37
|
+
yield line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
38
|
+
lineParts = [];
|
|
39
|
+
lineBytes = 0;
|
|
40
|
+
start = newline + 1;
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
lineBytes = appendLineFragment(lineParts, decoder.decode(), lineBytes, maxBufferBytes);
|
|
44
|
+
if (lineParts.length > 0) {
|
|
45
|
+
const line = lineParts.length > 1 ? lineParts.join("") : lineParts[0];
|
|
46
|
+
yield line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
47
|
+
}
|
|
34
48
|
} finally {
|
|
35
49
|
await reader.cancel().catch(() => {});
|
|
36
50
|
reader.releaseLock();
|