@bitkyc08/opencodex 2.12.0 → 2.13.0
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 +1 -1
- package/gui/dist/assets/index-BHldBl6_.js +76 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-output-schema.ts +137 -0
- package/src/adapters/anthropic.ts +15 -0
- package/src/adapters/google-antigravity-replay.ts +269 -5
- package/src/adapters/openai-chat.ts +25 -18
- package/src/adapters/openai-responses.ts +63 -17
- package/src/bridge.ts +11 -5
- package/src/claude/inbound.ts +24 -3
- package/src/cli/account-api.ts +2 -0
- package/src/cli/account-extended.ts +262 -1
- package/src/cli/account.ts +3 -1
- package/src/cli/dispatch.ts +532 -0
- package/src/cli/doctor.ts +26 -21
- package/src/cli/help.ts +9 -278
- package/src/cli/index.ts +45 -506
- package/src/cli/lab.ts +171 -65
- package/src/cli/registry.ts +384 -0
- package/src/cli/root.ts +86 -0
- package/src/cli/status.ts +9 -2
- package/src/cli/v2.ts +37 -2
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-store.ts +12 -1
- package/src/codex/admission.ts +2 -1
- package/src/codex/auth-api.ts +3 -2
- package/src/codex/catalog/metadata.ts +6 -4
- package/src/codex/catalog/parsing.ts +31 -7
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/catalog/sync.ts +65 -11
- package/src/codex/features.ts +437 -31
- package/src/codex/history-job.ts +6 -11
- package/src/codex/history-provider.ts +10 -9
- package/src/codex/inject.ts +14 -1
- package/src/codex/native-profile-startup.ts +49 -3
- package/src/codex/native-residue.ts +14 -2
- package/src/codex/paths.ts +75 -1
- package/src/codex/reset-credit-recovery.ts +1044 -0
- package/src/codex/shim.ts +876 -16
- package/src/codex/sync.ts +27 -2
- package/src/config.ts +148 -10
- package/src/generated/compatibility-version.json +2572 -0
- package/src/images/loop.ts +1 -1
- package/src/lab/artifacts/sanitize.ts +3 -3
- package/src/lab/automation/budgets.ts +78 -0
- package/src/lab/automation/config-persistence.ts +256 -0
- package/src/lab/automation/constants.ts +39 -0
- package/src/lab/automation/cooldown.ts +103 -0
- package/src/lab/automation/dispatch.ts +211 -0
- package/src/lab/automation/index.ts +13 -0
- package/src/lab/automation/orchestrator.ts +480 -0
- package/src/lab/automation/persistence.ts +512 -0
- package/src/lab/automation/planner.ts +371 -0
- package/src/lab/automation/policy.ts +136 -0
- package/src/lab/automation/queue.ts +191 -0
- package/src/lab/automation/recovery.ts +24 -0
- package/src/lab/automation/route-context.ts +21 -0
- package/src/lab/automation/run-key.ts +44 -0
- package/src/lab/automation/runs-query.ts +34 -0
- package/src/lab/automation/types.ts +160 -0
- package/src/lab/conformance/types.ts +23 -0
- package/src/lab/events/types.ts +2 -1
- package/src/lab/events/validate.ts +2 -1
- package/src/lab/fabric/constants.ts +40 -0
- package/src/lab/fabric/executor.ts +492 -0
- package/src/lab/fabric/index.ts +80 -0
- package/src/lab/fabric/manifest.ts +222 -0
- package/src/lab/fabric/observe.ts +454 -0
- package/src/lab/fabric/patch.ts +79 -0
- package/src/lab/fabric/producer-child.ts +139 -0
- package/src/lab/fabric/producer-isolate.ts +276 -0
- package/src/lab/fabric/producer-protocol.ts +61 -0
- package/src/lab/fabric/scratch.ts +439 -0
- package/src/lab/fabric/subject.ts +106 -0
- package/src/lab/fabric/types.ts +134 -0
- package/src/lab/fabric/verifier.ts +98 -0
- package/src/lab/index.ts +2 -0
- package/src/lab/ledger/store.ts +173 -0
- package/src/lab/live/executor.ts +29 -4
- package/src/lab/live/transport.ts +3 -1
- package/src/lab/observe/from-conformance.ts +12 -57
- package/src/lab/observe/from-live.ts +9 -1
- package/src/lab/paths.ts +74 -9
- package/src/lab/projection/schema.ts +14 -1
- package/src/lab/projection/verdicts.ts +89 -53
- package/src/lab/projection/verification.ts +83 -3
- package/src/lab/query/catalog.ts +34 -5
- package/src/lab/query/freshness.ts +53 -0
- package/src/lab/query/index.ts +11 -0
- package/src/lab/query/latest-observation.ts +59 -0
- package/src/lab/query/passive-production.ts +159 -0
- package/src/lab/subject/installation-salt.ts +24 -4
- package/src/lab/subject/protocol-subject.ts +80 -0
- package/src/lab/subject/route-subject.ts +2 -1
- package/src/lib/fabric-task-execution-authority.ts +7 -0
- package/src/lib/fabric-task-host.ts +29 -0
- package/src/lib/lab-live-route-production.ts +130 -0
- package/src/lib/local-management-capability.ts +100 -0
- package/src/lib/server-resource-ownership.ts +71 -0
- package/src/lib/windows-elevation.ts +8 -1
- package/src/lib/windows-user-principal.ts +62 -2
- package/src/lib/winsw.ts +8 -2
- package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
- package/src/oauth/account-import/index.ts +15 -0
- package/src/oauth/account-import/parser.ts +83 -0
- package/src/oauth/account-import/registry.ts +18 -0
- package/src/oauth/account-import/service.ts +75 -0
- package/src/oauth/account-import/types.ts +91 -0
- package/src/oauth/anthropic.ts +12 -1
- package/src/oauth/callback-server.ts +8 -2
- package/src/oauth/chatgpt.ts +12 -1
- package/src/oauth/google-antigravity.ts +27 -0
- package/src/oauth/health.ts +12 -48
- package/src/oauth/index.ts +122 -0
- package/src/oauth/kimi.ts +16 -2
- package/src/oauth/nous.ts +749 -0
- package/src/oauth/store.ts +56 -3
- package/src/providers/codex-capacity.ts +1 -0
- package/src/providers/registry.ts +161 -16
- package/src/responses/custom-tool-compat.ts +182 -0
- package/src/responses/parser.ts +22 -0
- package/src/responses/reasoning-replay-cache.ts +168 -11
- package/src/responses/schema.ts +1 -1
- package/src/router.ts +5 -20
- package/src/routing/capability.ts +9 -3
- package/src/routing/compatibility/assemble.ts +159 -0
- package/src/routing/compatibility/behavior.ts +224 -0
- package/src/routing/compatibility/catalog.ts +99 -0
- package/src/routing/compatibility/endpoint.ts +52 -0
- package/src/routing/compatibility/policy.ts +181 -0
- package/src/routing/compatibility/reader.ts +110 -0
- package/src/routing/compatibility/subject.ts +191 -0
- package/src/routing/compatibility/types.ts +64 -0
- package/src/routing/compatibility/version.ts +104 -0
- package/src/routing/evaluator.ts +27 -32
- package/src/routing/profile.ts +118 -4
- package/src/routing/trace.ts +82 -28
- package/src/server/auth-cors.ts +7 -0
- package/src/server/background-lifecycle.ts +20 -7
- package/src/server/chat-completions.ts +2 -2
- package/src/server/claude-messages.ts +6 -3
- package/src/server/direct-local-http.ts +343 -0
- package/src/server/index.ts +80 -18
- package/src/server/lifecycle.ts +12 -4
- package/src/server/local-management-read-client.ts +90 -0
- package/src/server/management/agent-settings-routes.ts +27 -3
- package/src/server/management/lab-automation-routes.ts +206 -0
- package/src/server/management/lab-routes.ts +13 -0
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +57 -1
- package/src/server/management/provider-routes.ts +16 -0
- package/src/server/management/routing-profile-routes.ts +12 -24
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +68 -3
- package/src/server/proxy-liveness.ts +3 -2
- package/src/server/request-decompress.ts +9 -3
- package/src/server/request-log.ts +53 -4
- package/src/server/responses/collaboration.ts +93 -17
- package/src/server/responses/compact.ts +3 -0
- package/src/server/responses/core.ts +202 -15
- package/src/server/responses/policy-fallback.ts +1 -0
- package/src/server/responses-custom-tool-repair.ts +280 -0
- package/src/server/responses-terminal-repair.ts +342 -0
- package/src/server/startup-action-control.ts +8 -1
- package/src/service.ts +278 -12
- package/src/types.ts +49 -3
- package/src/usage/expected-prices.ts +30 -0
- package/src/usage/log.ts +30 -4
- package/src/usage/summary.ts +153 -2
- package/src/web-search/loop.ts +1 -1
- package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
- package/src/cli/internal-dispatch.ts +0 -20
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
|
+
import {
|
|
3
|
+
customToolItemId,
|
|
4
|
+
restoreRoutedCustomCalls,
|
|
5
|
+
unwrapRoutedCustomToolArguments,
|
|
6
|
+
} from "../responses/custom-tool-compat";
|
|
7
|
+
import {
|
|
8
|
+
replaceSseDataPayload,
|
|
9
|
+
sseDataPayload,
|
|
10
|
+
type SseBlockRewrite,
|
|
11
|
+
} from "./sse-payload-rewrite";
|
|
12
|
+
|
|
13
|
+
/** Exact compact prefix used by our upstream rewriter; progressive matching also
|
|
14
|
+
* tolerates insignificant JSON whitespace via FREEFORM_WRAP_PREFIX_RE. */
|
|
15
|
+
const FREEFORM_WRAP_PREFIX = '{"input":"';
|
|
16
|
+
const FREEFORM_WRAP_PREFIX_RE = /^\s*\{\s*"input"\s*:\s*"/;
|
|
17
|
+
|
|
18
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
19
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Progressive decode of the freeform `{ "input": "…" }` wrapper.
|
|
24
|
+
* Returns null when the accumulated text does not (yet) match that wrapper so
|
|
25
|
+
* callers suppress deltas and rely on `response.custom_tool_call_input.done`.
|
|
26
|
+
*/
|
|
27
|
+
function partialCustomToolInput(argumentsText: string): string | null {
|
|
28
|
+
const match = FREEFORM_WRAP_PREFIX_RE.exec(argumentsText);
|
|
29
|
+
if (!match) return null;
|
|
30
|
+
const body = argumentsText.slice(match[0]!.length);
|
|
31
|
+
let output = "";
|
|
32
|
+
for (let index = 0; index < body.length; index++) {
|
|
33
|
+
const char = body[index];
|
|
34
|
+
if (char === '"') break;
|
|
35
|
+
if (char !== "\\") {
|
|
36
|
+
output += char;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const escaped = body[index + 1];
|
|
40
|
+
if (escaped === undefined) break;
|
|
41
|
+
index += 1;
|
|
42
|
+
if (escaped === "n") output += "\n";
|
|
43
|
+
else if (escaped === "t") output += "\t";
|
|
44
|
+
else if (escaped === "r") output += "\r";
|
|
45
|
+
else if (escaped === "b") output += "\b";
|
|
46
|
+
else if (escaped === "f") output += "\f";
|
|
47
|
+
else if (escaped === "u") {
|
|
48
|
+
const hex = body.slice(index + 1, index + 5);
|
|
49
|
+
if (hex.length !== 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) break;
|
|
50
|
+
output += String.fromCharCode(Number.parseInt(hex, 16));
|
|
51
|
+
index += 4;
|
|
52
|
+
} else output += escaped;
|
|
53
|
+
}
|
|
54
|
+
return output;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function replaceSseEventName(block: string, type: string): string {
|
|
58
|
+
const newline = block.includes("\r\n") ? "\r\n" : "\n";
|
|
59
|
+
const lines = block.split(/\r?\n/);
|
|
60
|
+
let replaced = false;
|
|
61
|
+
const next = lines.map(line => {
|
|
62
|
+
if (!replaced && line.startsWith("event:")) {
|
|
63
|
+
replaced = true;
|
|
64
|
+
return `event: ${type}`;
|
|
65
|
+
}
|
|
66
|
+
return line;
|
|
67
|
+
});
|
|
68
|
+
return next.join(newline);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type OpenCustomCall = {
|
|
72
|
+
argumentsText: string;
|
|
73
|
+
emittedInput: string;
|
|
74
|
+
retainedBytes: number;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type PendingArgumentBlock = {
|
|
78
|
+
block: string;
|
|
79
|
+
itemId?: string;
|
|
80
|
+
outputIndex?: number;
|
|
81
|
+
retainedBytes: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export function createRoutedCustomToolRestoreBlockRewrite(
|
|
85
|
+
names: ReadonlySet<string>,
|
|
86
|
+
budget?: TranslatorBudget,
|
|
87
|
+
): SseBlockRewrite {
|
|
88
|
+
const itemNames = new Map<string, string>();
|
|
89
|
+
const ordinaryItemIds = new Set<string>();
|
|
90
|
+
const openCalls = new Map<string, OpenCustomCall>();
|
|
91
|
+
let pendingArguments: PendingArgumentBlock[] = [];
|
|
92
|
+
let disposed = false;
|
|
93
|
+
|
|
94
|
+
const releaseCall = (itemId: string): void => {
|
|
95
|
+
const open = openCalls.get(itemId);
|
|
96
|
+
if (!open) return;
|
|
97
|
+
if (open.retainedBytes > 0) {
|
|
98
|
+
budget?.releaseRetained(open.retainedBytes, { kind: "retained_collectors" });
|
|
99
|
+
}
|
|
100
|
+
openCalls.delete(itemId);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const releaseAll = (): void => {
|
|
104
|
+
if (disposed) return;
|
|
105
|
+
disposed = true;
|
|
106
|
+
for (const itemId of openCalls.keys()) releaseCall(itemId);
|
|
107
|
+
const pendingBytes = pendingArguments.reduce((total, pending) => total + pending.retainedBytes, 0);
|
|
108
|
+
if (pendingBytes > 0) {
|
|
109
|
+
budget?.releaseRetained(pendingBytes, { kind: "retained_collectors" });
|
|
110
|
+
}
|
|
111
|
+
pendingArguments = [];
|
|
112
|
+
itemNames.clear();
|
|
113
|
+
ordinaryItemIds.clear();
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const retainPendingArgument = (
|
|
117
|
+
block: string,
|
|
118
|
+
itemId: string | undefined,
|
|
119
|
+
outputIndex: number | undefined,
|
|
120
|
+
): void => {
|
|
121
|
+
const retainedBytes = Buffer.byteLength(block, "utf8");
|
|
122
|
+
if (retainedBytes > 0) budget?.chargeRetained(retainedBytes, { kind: "retained_collectors" });
|
|
123
|
+
pendingArguments.push({ block, itemId, outputIndex, retainedBytes });
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const takePendingArguments = (
|
|
127
|
+
itemId: string | undefined,
|
|
128
|
+
outputIndex: number | undefined,
|
|
129
|
+
): string[] => {
|
|
130
|
+
const matched: PendingArgumentBlock[] = [];
|
|
131
|
+
const remaining: PendingArgumentBlock[] = [];
|
|
132
|
+
for (const pending of pendingArguments) {
|
|
133
|
+
const matches = pending.itemId !== undefined
|
|
134
|
+
? itemId !== undefined && pending.itemId === itemId
|
|
135
|
+
: outputIndex !== undefined && pending.outputIndex === outputIndex;
|
|
136
|
+
(matches ? matched : remaining).push(pending);
|
|
137
|
+
}
|
|
138
|
+
pendingArguments = remaining;
|
|
139
|
+
const retainedBytes = matched.reduce((total, pending) => total + pending.retainedBytes, 0);
|
|
140
|
+
if (retainedBytes > 0) {
|
|
141
|
+
budget?.releaseRetained(retainedBytes, { kind: "retained_collectors" });
|
|
142
|
+
}
|
|
143
|
+
// Index-matched entries carry no item id. Stamp the resolved id so replay
|
|
144
|
+
// classifies the event instead of buffering it a second time.
|
|
145
|
+
return matched.map(pending => {
|
|
146
|
+
if (pending.itemId !== undefined || itemId === undefined) return pending.block;
|
|
147
|
+
const payload = sseDataPayload(pending.block);
|
|
148
|
+
if (payload === null) return pending.block;
|
|
149
|
+
try {
|
|
150
|
+
const parsed: unknown = JSON.parse(payload);
|
|
151
|
+
if (!isPlainObject(parsed)) return pending.block;
|
|
152
|
+
return replaceSseDataPayload(pending.block, JSON.stringify({ ...parsed, item_id: itemId }));
|
|
153
|
+
} catch {
|
|
154
|
+
return pending.block;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const rewrite: SseBlockRewrite = (block: string): readonly string[] => {
|
|
160
|
+
if (disposed) return [block];
|
|
161
|
+
const payload = sseDataPayload(block);
|
|
162
|
+
if (payload === null || payload === "[DONE]") return [block];
|
|
163
|
+
let parsed: unknown;
|
|
164
|
+
try {
|
|
165
|
+
parsed = JSON.parse(payload);
|
|
166
|
+
} catch {
|
|
167
|
+
return [block];
|
|
168
|
+
}
|
|
169
|
+
if (!isPlainObject(parsed)) return [block];
|
|
170
|
+
|
|
171
|
+
const type = typeof parsed.type === "string" ? parsed.type : "";
|
|
172
|
+
const outputIndex = typeof parsed.output_index === "number"
|
|
173
|
+
&& Number.isInteger(parsed.output_index)
|
|
174
|
+
&& parsed.output_index >= 0
|
|
175
|
+
? parsed.output_index
|
|
176
|
+
: undefined;
|
|
177
|
+
if (
|
|
178
|
+
(type === "response.output_item.added" || type === "response.output_item.done")
|
|
179
|
+
&& isPlainObject(parsed.item)
|
|
180
|
+
&& parsed.item.type === "function_call"
|
|
181
|
+
&& typeof parsed.item.name === "string"
|
|
182
|
+
) {
|
|
183
|
+
const upstreamItemId = typeof parsed.item.id === "string" ? parsed.item.id : undefined;
|
|
184
|
+
const routed = names.has(parsed.item.name);
|
|
185
|
+
if (upstreamItemId) {
|
|
186
|
+
if (routed) {
|
|
187
|
+
itemNames.set(upstreamItemId, parsed.item.name);
|
|
188
|
+
ordinaryItemIds.delete(upstreamItemId);
|
|
189
|
+
} else {
|
|
190
|
+
ordinaryItemIds.add(upstreamItemId);
|
|
191
|
+
}
|
|
192
|
+
if (routed && type === "response.output_item.added") {
|
|
193
|
+
openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const pending = takePendingArguments(upstreamItemId, outputIndex);
|
|
197
|
+
if (!routed) {
|
|
198
|
+
if (type === "response.output_item.done" && upstreamItemId) ordinaryItemIds.delete(upstreamItemId);
|
|
199
|
+
return [...pending, block];
|
|
200
|
+
}
|
|
201
|
+
if (upstreamItemId && pending.length > 0 && !openCalls.has(upstreamItemId)) {
|
|
202
|
+
openCalls.set(upstreamItemId, { argumentsText: "", emittedInput: "", retainedBytes: 0 });
|
|
203
|
+
}
|
|
204
|
+
const restored = restoreRoutedCustomCalls(parsed, names);
|
|
205
|
+
const restoredBlock = restored.changed
|
|
206
|
+
? replaceSseDataPayload(block, JSON.stringify(restored.value))
|
|
207
|
+
: block;
|
|
208
|
+
const replayed = pending.flatMap(pendingBlock => rewrite(pendingBlock));
|
|
209
|
+
if (type === "response.output_item.done" && upstreamItemId) releaseCall(upstreamItemId);
|
|
210
|
+
return type === "response.output_item.added"
|
|
211
|
+
? [restoredBlock, ...replayed]
|
|
212
|
+
: [...replayed, restoredBlock];
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const upstreamItemId = typeof parsed.item_id === "string" ? parsed.item_id : undefined;
|
|
216
|
+
const argumentEvent = type === "response.function_call_arguments.delta"
|
|
217
|
+
|| type === "response.function_call_arguments.done";
|
|
218
|
+
if (argumentEvent && (!upstreamItemId || (!itemNames.has(upstreamItemId) && !ordinaryItemIds.has(upstreamItemId)))) {
|
|
219
|
+
retainPendingArgument(block, upstreamItemId, outputIndex);
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
type === "response.function_call_arguments.delta"
|
|
224
|
+
&& upstreamItemId
|
|
225
|
+
&& itemNames.has(upstreamItemId)
|
|
226
|
+
) {
|
|
227
|
+
const open = openCalls.get(upstreamItemId) ?? { argumentsText: "", emittedInput: "", retainedBytes: 0 };
|
|
228
|
+
const delta = typeof parsed.delta === "string" ? parsed.delta : "";
|
|
229
|
+
const deltaBytes = Buffer.byteLength(delta, "utf8");
|
|
230
|
+
if (deltaBytes > 0) budget?.chargeRetained(deltaBytes, { kind: "retained_collectors" });
|
|
231
|
+
open.argumentsText += delta;
|
|
232
|
+
open.retainedBytes += deltaBytes;
|
|
233
|
+
openCalls.set(upstreamItemId, open);
|
|
234
|
+
// Still accumulating toward the compact wrapper, or an unrecognized shape:
|
|
235
|
+
// suppress progressive emission and let the done event carry input.
|
|
236
|
+
if (FREEFORM_WRAP_PREFIX.startsWith(open.argumentsText)) return [];
|
|
237
|
+
const fullInput = partialCustomToolInput(open.argumentsText);
|
|
238
|
+
if (fullInput === null) return [];
|
|
239
|
+
if (!fullInput.startsWith(open.emittedInput) || fullInput.length === open.emittedInput.length) return [];
|
|
240
|
+
const inputDelta = fullInput.slice(open.emittedInput.length);
|
|
241
|
+
open.emittedInput = fullInput;
|
|
242
|
+
const nextType = "response.custom_tool_call_input.delta";
|
|
243
|
+
const next = {
|
|
244
|
+
...parsed,
|
|
245
|
+
type: nextType,
|
|
246
|
+
item_id: customToolItemId(upstreamItemId),
|
|
247
|
+
delta: inputDelta,
|
|
248
|
+
};
|
|
249
|
+
return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (
|
|
253
|
+
type === "response.function_call_arguments.done"
|
|
254
|
+
&& upstreamItemId
|
|
255
|
+
&& itemNames.has(upstreamItemId)
|
|
256
|
+
) {
|
|
257
|
+
const nextType = "response.custom_tool_call_input.done";
|
|
258
|
+
const source = typeof parsed.arguments === "string"
|
|
259
|
+
? parsed.arguments
|
|
260
|
+
: openCalls.get(upstreamItemId)?.argumentsText ?? "";
|
|
261
|
+
const { arguments: _arguments, ...rest } = parsed;
|
|
262
|
+
const next = {
|
|
263
|
+
...rest,
|
|
264
|
+
type: nextType,
|
|
265
|
+
item_id: customToolItemId(upstreamItemId),
|
|
266
|
+
input: unwrapRoutedCustomToolArguments(source),
|
|
267
|
+
};
|
|
268
|
+
return [replaceSseDataPayload(replaceSseEventName(block, nextType), JSON.stringify(next))];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const restored = restoreRoutedCustomCalls(parsed, names);
|
|
272
|
+
const terminal = type === "response.completed" || type === "response.failed" || type === "response.incomplete";
|
|
273
|
+
if (terminal) releaseAll();
|
|
274
|
+
return restored.changed
|
|
275
|
+
? [replaceSseDataPayload(block, JSON.stringify(restored.value))]
|
|
276
|
+
: [block];
|
|
277
|
+
};
|
|
278
|
+
rewrite.dispose = releaseAll;
|
|
279
|
+
return rewrite;
|
|
280
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { TranslatorBudget } from "../lib/translator-budget";
|
|
2
|
+
import type { ResponsesTerminalRepairPolicy } from "../providers/registry";
|
|
3
|
+
import { nextSseBlock, sseDataPayload } from "./sse-payload-rewrite";
|
|
4
|
+
|
|
5
|
+
export interface ResponsesTerminalRepairScheduler {
|
|
6
|
+
nowMs(): number;
|
|
7
|
+
schedule(callback: () => void, delayMs: number): unknown;
|
|
8
|
+
cancel(handle: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const systemScheduler: ResponsesTerminalRepairScheduler = {
|
|
12
|
+
nowMs: () => Date.now(),
|
|
13
|
+
schedule(callback, delayMs) {
|
|
14
|
+
const handle = setTimeout(callback, delayMs);
|
|
15
|
+
(handle as { unref?: () => void }).unref?.();
|
|
16
|
+
return handle;
|
|
17
|
+
},
|
|
18
|
+
cancel(handle) { clearTimeout(handle as ReturnType<typeof setTimeout>); },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function outputIndex(value: unknown): number | null {
|
|
26
|
+
return Number.isInteger(value) && (value as number) >= 0 ? value as number : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function isCompleteItem(item: Record<string, unknown>): boolean {
|
|
30
|
+
if (item.status !== "completed") return false;
|
|
31
|
+
if (item.type === "reasoning") {
|
|
32
|
+
return typeof item.id === "string" && item.id.length > 0
|
|
33
|
+
&& Array.isArray(item.content)
|
|
34
|
+
&& item.content.every(part => isPlainRecord(part)
|
|
35
|
+
&& part.type === "reasoning_text" && typeof part.text === "string");
|
|
36
|
+
}
|
|
37
|
+
if (item.type === "message") {
|
|
38
|
+
return typeof item.id === "string" && item.id.length > 0
|
|
39
|
+
&& item.role === "assistant" && Array.isArray(item.content)
|
|
40
|
+
&& item.content.every(part => isPlainRecord(part)
|
|
41
|
+
&& part.type === "output_text" && typeof part.text === "string");
|
|
42
|
+
}
|
|
43
|
+
if (item.type === "function_call") {
|
|
44
|
+
if (typeof item.id !== "string" || item.id.length === 0) return false;
|
|
45
|
+
if (typeof item.call_id !== "string" || item.call_id.length === 0) return false;
|
|
46
|
+
if (typeof item.name !== "string" || item.name.length === 0) return false;
|
|
47
|
+
if (typeof item.arguments !== "string") return false;
|
|
48
|
+
try {
|
|
49
|
+
return isPlainRecord(JSON.parse(item.arguments));
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Relay a native Responses SSE body while repairing the narrow DeepSeek shape where every
|
|
59
|
+
* output item is complete but the protocol terminal is missing or indefinitely delayed.
|
|
60
|
+
*/
|
|
61
|
+
export function relayResponsesSseWithTerminalRepair(
|
|
62
|
+
body: ReadableStream<Uint8Array>,
|
|
63
|
+
upstream: AbortController,
|
|
64
|
+
policy: ResponsesTerminalRepairPolicy,
|
|
65
|
+
budget: TranslatorBudget,
|
|
66
|
+
scheduler: ResponsesTerminalRepairScheduler = systemScheduler,
|
|
67
|
+
): ReadableStream<Uint8Array> {
|
|
68
|
+
const reader = body.getReader();
|
|
69
|
+
const decoder = new TextDecoder();
|
|
70
|
+
const encoder = new TextEncoder();
|
|
71
|
+
const added = new Map<number, { type?: unknown; id?: unknown }>();
|
|
72
|
+
const completed = new Map<number, { item: Record<string, unknown>; bytes: number }>();
|
|
73
|
+
let created: Record<string, unknown> | null = null;
|
|
74
|
+
let createdBytes = 0;
|
|
75
|
+
let maxSequence = -1;
|
|
76
|
+
let buffer = "";
|
|
77
|
+
let bufferBytes = 0;
|
|
78
|
+
let timer: unknown;
|
|
79
|
+
let timerGeneration = 0;
|
|
80
|
+
let realTerminalSeen = false;
|
|
81
|
+
let tainted = false;
|
|
82
|
+
let disposed = false;
|
|
83
|
+
let controllerRef: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
84
|
+
let activeRead: Promise<void> | null = null;
|
|
85
|
+
|
|
86
|
+
const onUpstreamAbort = (): void => {
|
|
87
|
+
if (disposed) return;
|
|
88
|
+
dispose();
|
|
89
|
+
reader.cancel(upstream.signal.reason).catch(() => {});
|
|
90
|
+
try { controllerRef?.close(); } catch { /* already closed */ }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const releaseRetainedState = (): void => {
|
|
94
|
+
if (createdBytes > 0) budget.releaseRetained(createdBytes, { kind: "retained_collectors" });
|
|
95
|
+
createdBytes = 0;
|
|
96
|
+
for (const retained of completed.values()) {
|
|
97
|
+
budget.releaseRetained(retained.bytes, { kind: "retained_collectors" });
|
|
98
|
+
}
|
|
99
|
+
completed.clear();
|
|
100
|
+
created = null;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const releaseBuffer = (): void => {
|
|
104
|
+
if (bufferBytes > 0) budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
105
|
+
buffer = "";
|
|
106
|
+
bufferBytes = 0;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const cancelTimer = (): void => {
|
|
110
|
+
timerGeneration += 1;
|
|
111
|
+
if (timer !== undefined) scheduler.cancel(timer);
|
|
112
|
+
timer = undefined;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const dispose = (): void => {
|
|
116
|
+
if (disposed) return;
|
|
117
|
+
disposed = true;
|
|
118
|
+
upstream.signal.removeEventListener("abort", onUpstreamAbort);
|
|
119
|
+
cancelTimer();
|
|
120
|
+
releaseRetainedState();
|
|
121
|
+
releaseBuffer();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const replaceBuffer = (next: string): void => {
|
|
125
|
+
const nextBytes = encoder.encode(next).byteLength;
|
|
126
|
+
const reservation = budget.reserveTransient(nextBytes, { kind: "live_transient" });
|
|
127
|
+
reservation.commitRetained();
|
|
128
|
+
if (bufferBytes > 0) budget.releaseRetained(bufferBytes, { kind: "live_transient" });
|
|
129
|
+
buffer = next;
|
|
130
|
+
bufferBytes = nextBytes;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const appendBuffer = (fragment: string): void => {
|
|
134
|
+
if (!fragment) return;
|
|
135
|
+
replaceBuffer(buffer + fragment);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const retainCreated = (response: Record<string, unknown>): void => {
|
|
139
|
+
const bytes = encoder.encode(JSON.stringify(response)).byteLength;
|
|
140
|
+
budget.chargeRetained(bytes, { kind: "retained_collectors" });
|
|
141
|
+
if (createdBytes > 0) budget.releaseRetained(createdBytes, { kind: "retained_collectors" });
|
|
142
|
+
created = response;
|
|
143
|
+
createdBytes = bytes;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const retainCompleted = (index: number, item: Record<string, unknown>): void => {
|
|
147
|
+
const bytes = encoder.encode(JSON.stringify(item)).byteLength;
|
|
148
|
+
budget.chargeRetained(bytes, { kind: "retained_collectors" });
|
|
149
|
+
const previous = completed.get(index);
|
|
150
|
+
if (previous) budget.releaseRetained(previous.bytes, { kind: "retained_collectors" });
|
|
151
|
+
completed.set(index, { item, bytes });
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const completeCandidate = (): boolean => {
|
|
155
|
+
if (realTerminalSeen || tainted || !created || completed.size === 0 || added.size !== completed.size) return false;
|
|
156
|
+
for (const index of added.keys()) {
|
|
157
|
+
const retained = completed.get(index);
|
|
158
|
+
if (!retained || !isCompleteItem(retained.item)) return false;
|
|
159
|
+
}
|
|
160
|
+
for (const index of completed.keys()) if (!added.has(index)) return false;
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const syntheticTerminal = (kind: "completed" | "incomplete"): Uint8Array => {
|
|
165
|
+
const output = [...completed.entries()]
|
|
166
|
+
.sort(([left], [right]) => left - right)
|
|
167
|
+
.map(([, retained]) => retained.item);
|
|
168
|
+
const response = {
|
|
169
|
+
...(created ?? {}),
|
|
170
|
+
status: kind,
|
|
171
|
+
completed_at: Math.floor(scheduler.nowMs() / 1_000),
|
|
172
|
+
output,
|
|
173
|
+
...(kind === "incomplete"
|
|
174
|
+
? { incomplete_details: { reason: "missing_terminal_event" } }
|
|
175
|
+
: {}),
|
|
176
|
+
};
|
|
177
|
+
const type = `response.${kind}`;
|
|
178
|
+
return encoder.encode(`event: ${type}\ndata: ${JSON.stringify({
|
|
179
|
+
type,
|
|
180
|
+
response,
|
|
181
|
+
sequence_number: maxSequence + 1,
|
|
182
|
+
})}\n\n`);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const emitSynthetic = (
|
|
186
|
+
kind: "completed" | "incomplete",
|
|
187
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
188
|
+
): boolean => {
|
|
189
|
+
if (disposed || realTerminalSeen) return false;
|
|
190
|
+
realTerminalSeen = true;
|
|
191
|
+
cancelTimer();
|
|
192
|
+
controller.enqueue(syntheticTerminal(kind));
|
|
193
|
+
releaseRetainedState();
|
|
194
|
+
return true;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const commitSynthetic = (generation: number): void => {
|
|
198
|
+
if (disposed || realTerminalSeen || generation !== timerGeneration || !completeCandidate()) return;
|
|
199
|
+
timer = undefined;
|
|
200
|
+
try {
|
|
201
|
+
if (controllerRef && emitSynthetic("completed", controllerRef)) controllerRef.close();
|
|
202
|
+
} catch {
|
|
203
|
+
/* downstream already closed */
|
|
204
|
+
}
|
|
205
|
+
reader.cancel("Responses terminal repaired after complete output").catch(() => {});
|
|
206
|
+
dispose();
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const maybeArmTimer = (): void => {
|
|
210
|
+
if (!completeCandidate()) return;
|
|
211
|
+
const generation = timerGeneration;
|
|
212
|
+
timer = scheduler.schedule(() => commitSynthetic(generation), policy.graceMs);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const inspectPayload = (payload: string | null): "done" | "ordinary" => {
|
|
216
|
+
if (payload === "[DONE]") return "done";
|
|
217
|
+
if (!payload || realTerminalSeen) return "ordinary";
|
|
218
|
+
let parsed: unknown;
|
|
219
|
+
try {
|
|
220
|
+
parsed = JSON.parse(payload);
|
|
221
|
+
} catch {
|
|
222
|
+
tainted = true;
|
|
223
|
+
return "ordinary";
|
|
224
|
+
}
|
|
225
|
+
if (!isPlainRecord(parsed)) {
|
|
226
|
+
tainted = true;
|
|
227
|
+
return "ordinary";
|
|
228
|
+
}
|
|
229
|
+
if (Number.isInteger(parsed.sequence_number)) {
|
|
230
|
+
maxSequence = Math.max(maxSequence, parsed.sequence_number as number);
|
|
231
|
+
}
|
|
232
|
+
const type = parsed.type;
|
|
233
|
+
if (type === "response.completed" || type === "response.failed" || type === "response.incomplete") {
|
|
234
|
+
realTerminalSeen = true;
|
|
235
|
+
cancelTimer();
|
|
236
|
+
releaseRetainedState();
|
|
237
|
+
return "ordinary";
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
cancelTimer();
|
|
241
|
+
if (type === "response.created" && isPlainRecord(parsed.response)) {
|
|
242
|
+
retainCreated(parsed.response);
|
|
243
|
+
} else if (type === "response.output_item.added") {
|
|
244
|
+
const index = outputIndex(parsed.output_index);
|
|
245
|
+
if (index === null || !isPlainRecord(parsed.item) || added.has(index) || completed.has(index)) {
|
|
246
|
+
tainted = true;
|
|
247
|
+
} else {
|
|
248
|
+
added.set(index, { type: parsed.item.type, id: parsed.item.id });
|
|
249
|
+
}
|
|
250
|
+
} else if (type === "response.output_item.done") {
|
|
251
|
+
const index = outputIndex(parsed.output_index);
|
|
252
|
+
if (index === null || !isPlainRecord(parsed.item) || !added.has(index) || completed.has(index)) {
|
|
253
|
+
tainted = true;
|
|
254
|
+
} else {
|
|
255
|
+
const opened = added.get(index)!;
|
|
256
|
+
if (opened.type !== parsed.item.type || opened.id !== parsed.item.id) tainted = true;
|
|
257
|
+
retainCompleted(index, parsed.item);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
maybeArmTimer();
|
|
261
|
+
return "ordinary";
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const emitBlocks = (
|
|
265
|
+
controller: ReadableStreamDefaultController<Uint8Array>,
|
|
266
|
+
): { closed: boolean; emitted: boolean } => {
|
|
267
|
+
let next: ReturnType<typeof nextSseBlock>;
|
|
268
|
+
let emitted = false;
|
|
269
|
+
while ((next = nextSseBlock(buffer))) {
|
|
270
|
+
replaceBuffer(next.rest);
|
|
271
|
+
const kind = inspectPayload(sseDataPayload(next.block));
|
|
272
|
+
if (kind === "done" && !realTerminalSeen) {
|
|
273
|
+
emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
|
|
274
|
+
}
|
|
275
|
+
controller.enqueue(encoder.encode(next.block + next.delimiter));
|
|
276
|
+
emitted = true;
|
|
277
|
+
if (kind === "done") {
|
|
278
|
+
reader.cancel("Responses stream ended with DONE").catch(() => {});
|
|
279
|
+
dispose();
|
|
280
|
+
controller.close();
|
|
281
|
+
return { closed: true, emitted };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return { closed: false, emitted };
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const readOnce = async (controller: ReadableStreamDefaultController<Uint8Array>): Promise<void> => {
|
|
288
|
+
try {
|
|
289
|
+
for (;;) {
|
|
290
|
+
const { done, value } = await reader.read();
|
|
291
|
+
if (disposed) return;
|
|
292
|
+
if (done) {
|
|
293
|
+
appendBuffer(decoder.decode());
|
|
294
|
+
if (buffer.length > 0) {
|
|
295
|
+
// A delimiter-less suffix is not a complete SSE event. Preserve the
|
|
296
|
+
// upstream bytes for passthrough compatibility, but never let a
|
|
297
|
+
// truncated lifecycle frame establish synthetic success.
|
|
298
|
+
tainted = true;
|
|
299
|
+
controller.enqueue(encoder.encode(buffer));
|
|
300
|
+
}
|
|
301
|
+
if (!realTerminalSeen) {
|
|
302
|
+
emitSynthetic(completeCandidate() ? "completed" : "incomplete", controller);
|
|
303
|
+
}
|
|
304
|
+
releaseBuffer();
|
|
305
|
+
dispose();
|
|
306
|
+
controller.close();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
appendBuffer(decoder.decode(value, { stream: true }));
|
|
310
|
+
const result = emitBlocks(controller);
|
|
311
|
+
if (result.closed || result.emitted) return;
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (disposed) return;
|
|
315
|
+
dispose();
|
|
316
|
+
controller.error(error);
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return new ReadableStream<Uint8Array>({
|
|
321
|
+
start(controller) {
|
|
322
|
+
controllerRef = controller;
|
|
323
|
+
if (upstream.signal.aborted) {
|
|
324
|
+
onUpstreamAbort();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
upstream.signal.addEventListener("abort", onUpstreamAbort, { once: true });
|
|
328
|
+
},
|
|
329
|
+
pull(controller) {
|
|
330
|
+
if (disposed) return;
|
|
331
|
+
if (!activeRead) {
|
|
332
|
+
activeRead = readOnce(controller).finally(() => { activeRead = null; });
|
|
333
|
+
}
|
|
334
|
+
return activeRead;
|
|
335
|
+
},
|
|
336
|
+
cancel(reason) {
|
|
337
|
+
dispose();
|
|
338
|
+
upstream.abort(reason);
|
|
339
|
+
return reader.cancel(reason);
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
@@ -164,10 +164,17 @@ function runCliInstall(
|
|
|
164
164
|
const cli = join(import.meta.dir, "..", "cli", "index.ts");
|
|
165
165
|
const argv = [cli, ...startupInstallArgv(action, options)];
|
|
166
166
|
return new Promise((resolve, reject) => {
|
|
167
|
+
const timeout = process.platform === "win32" && action === "install-service"
|
|
168
|
+
? 0
|
|
169
|
+
: 60_000;
|
|
167
170
|
execFile(bun, argv, {
|
|
168
171
|
encoding: "utf8",
|
|
169
172
|
env: process.env,
|
|
170
|
-
|
|
173
|
+
// A fresh Windows scheduler install now owns its UAC prompt inside this CLI
|
|
174
|
+
// transaction. Killing only the CLI at 60s can orphan its elevated schtasks child,
|
|
175
|
+
// which may register the task after the Dashboard has reported failure. Keep the
|
|
176
|
+
// async request/attempt lock alive until Windows returns approval or cancellation.
|
|
177
|
+
timeout,
|
|
171
178
|
windowsHide: true,
|
|
172
179
|
maxBuffer: 256 * 1024,
|
|
173
180
|
}, (error, stdout, stderr) => {
|