@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -9,343 +9,345 @@ import { stripMcpPrefixFromParsedEvent } from "./mcp.js";
|
|
|
9
9
|
const MAX_UNTERMINATED_SSE_BUFFER = 256 * 1024;
|
|
10
10
|
|
|
11
11
|
interface OpenContentBlockState {
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
type: string;
|
|
13
|
+
partialJson: string;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
interface StreamTruncatedContext {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
inFlightEvent?: string;
|
|
18
|
+
lastEventType?: string;
|
|
19
|
+
openContentBlockIndex?: number;
|
|
20
|
+
hasPartialJson?: boolean;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export class StreamTruncatedError extends Error {
|
|
24
|
-
|
|
24
|
+
readonly context: StreamTruncatedContext;
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
26
|
+
constructor(message: string, context: StreamTruncatedContext = {}) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "StreamTruncatedError";
|
|
29
|
+
this.context = context;
|
|
30
|
+
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Update running usage stats from a parsed SSE event.
|
|
35
35
|
*/
|
|
36
36
|
export function extractUsageFromSSEEvent(parsed: unknown, stats: UsageStats): void {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
37
|
+
const p = parsed as Record<string, unknown> | null;
|
|
38
|
+
if (!p) return;
|
|
39
|
+
|
|
40
|
+
// message_delta: cumulative usage (preferred, overwrites)
|
|
41
|
+
if (p.type === "message_delta" && p.usage) {
|
|
42
|
+
const u = p.usage as Record<string, unknown>;
|
|
43
|
+
if (typeof u.input_tokens === "number") stats.inputTokens = u.input_tokens;
|
|
44
|
+
if (typeof u.output_tokens === "number") stats.outputTokens = u.output_tokens;
|
|
45
|
+
if (typeof u.cache_read_input_tokens === "number") stats.cacheReadTokens = u.cache_read_input_tokens;
|
|
46
|
+
if (typeof u.cache_creation_input_tokens === "number") stats.cacheWriteTokens = u.cache_creation_input_tokens;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// message_start: initial usage (only set if we haven't seen message_delta yet)
|
|
51
|
+
if (p.type === "message_start") {
|
|
52
|
+
const msg = p.message as Record<string, unknown> | undefined;
|
|
53
|
+
if (msg?.usage) {
|
|
54
|
+
const u = msg.usage as Record<string, unknown>;
|
|
55
|
+
if (stats.inputTokens === 0 && typeof u.input_tokens === "number") {
|
|
56
|
+
stats.inputTokens = u.input_tokens;
|
|
57
|
+
}
|
|
58
|
+
if (stats.cacheReadTokens === 0 && typeof u.cache_read_input_tokens === "number") {
|
|
59
|
+
stats.cacheReadTokens = u.cache_read_input_tokens;
|
|
60
|
+
}
|
|
61
|
+
if (stats.cacheWriteTokens === 0 && typeof u.cache_creation_input_tokens === "number") {
|
|
62
|
+
stats.cacheWriteTokens = u.cache_creation_input_tokens;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
64
65
|
}
|
|
65
|
-
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Extract the combined SSE data payload from one event block.
|
|
70
70
|
*/
|
|
71
71
|
export function getSSEDataPayload(eventBlock: string): string | null {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
72
|
+
if (!eventBlock) return null;
|
|
73
|
+
|
|
74
|
+
const dataLines: string[] = [];
|
|
75
|
+
for (const line of eventBlock.split("\n")) {
|
|
76
|
+
if (!line.startsWith("data:")) continue;
|
|
77
|
+
dataLines.push(line.slice(5).trimStart());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (dataLines.length === 0) return null;
|
|
81
|
+
const payload = dataLines.join("\n");
|
|
82
|
+
if (!payload || payload === "[DONE]") return null;
|
|
83
|
+
return payload;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
function getSSEEventType(eventBlock: string): string | null {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
for (const line of eventBlock.split("\n")) {
|
|
88
|
+
if (!line.startsWith("event:")) continue;
|
|
89
|
+
const eventType = line.slice(6).trimStart();
|
|
90
|
+
if (eventType) return eventType;
|
|
91
|
+
}
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
return null;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
function formatSSEEventBlock(eventType: string, parsed: unknown, prettyPrint: boolean): string {
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const json = prettyPrint ? JSON.stringify(parsed, null, 2) : JSON.stringify(parsed);
|
|
98
|
+
const lines = [`event: ${eventType}`];
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
100
|
+
for (const line of json.split("\n")) {
|
|
101
|
+
lines.push(`data: ${line}`);
|
|
102
|
+
}
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
lines.push("", "");
|
|
105
|
+
return lines.join("\n");
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
function hasRecordedUsage(stats: UsageStats): boolean {
|
|
109
|
-
|
|
109
|
+
return stats.inputTokens > 0 || stats.outputTokens > 0 || stats.cacheReadTokens > 0 || stats.cacheWriteTokens > 0;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function getErrorMessage(parsed: unknown): string {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
if (!parsed || typeof parsed !== "object") {
|
|
114
|
+
return "stream terminated with error event";
|
|
115
|
+
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
const error = (parsed as Record<string, unknown>).error;
|
|
118
|
+
if (!error || typeof error !== "object") {
|
|
119
|
+
return "stream terminated with error event";
|
|
120
|
+
}
|
|
121
121
|
|
|
122
|
-
|
|
123
|
-
|
|
122
|
+
const message = (error as Record<string, unknown>).message;
|
|
123
|
+
return typeof message === "string" && message ? message : "stream terminated with error event";
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
function getEventIndex(parsed: Record<string, unknown>, eventType: string): number {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
127
|
+
const index = parsed.index;
|
|
128
|
+
if (typeof index !== "number") {
|
|
129
|
+
throw new Error(`invalid SSE ${eventType} event: missing numeric index`);
|
|
130
|
+
}
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
return index;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
function getEventLabel(parsed: Record<string, unknown>, eventType: string): string {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
136
|
+
switch (eventType) {
|
|
137
|
+
case "content_block_start": {
|
|
138
|
+
const contentBlock = parsed.content_block;
|
|
139
|
+
const blockType =
|
|
140
|
+
contentBlock && typeof contentBlock === "object"
|
|
141
|
+
? (contentBlock as Record<string, unknown>).type
|
|
142
|
+
: undefined;
|
|
143
|
+
return typeof blockType === "string" && blockType ? `content_block_start(${blockType})` : eventType;
|
|
144
|
+
}
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
case "content_block_delta": {
|
|
147
|
+
const delta = parsed.delta;
|
|
148
|
+
const deltaType = delta && typeof delta === "object" ? (delta as Record<string, unknown>).type : undefined;
|
|
149
|
+
return typeof deltaType === "string" && deltaType ? `content_block_delta(${deltaType})` : eventType;
|
|
150
|
+
}
|
|
149
151
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
152
|
+
default:
|
|
153
|
+
return eventType;
|
|
154
|
+
}
|
|
153
155
|
}
|
|
154
156
|
|
|
155
157
|
function getOpenBlockContext(openContentBlocks: Map<number, OpenContentBlockState>): StreamTruncatedContext | null {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
158
|
+
for (const [index, blockState] of openContentBlocks) {
|
|
159
|
+
if (blockState.type === "tool_use") {
|
|
160
|
+
return {
|
|
161
|
+
inFlightEvent: blockState.partialJson
|
|
162
|
+
? "content_block_delta(input_json_delta)"
|
|
163
|
+
: "content_block_start(tool_use)",
|
|
164
|
+
openContentBlockIndex: index,
|
|
165
|
+
hasPartialJson: blockState.partialJson.length > 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
165
168
|
}
|
|
166
|
-
}
|
|
167
169
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
const firstOpenBlock = openContentBlocks.entries().next().value as [number, OpenContentBlockState] | undefined;
|
|
171
|
+
if (!firstOpenBlock) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const [index, blockState] = firstOpenBlock;
|
|
176
|
+
return {
|
|
177
|
+
inFlightEvent: `content_block_start(${blockState.type})`,
|
|
178
|
+
openContentBlockIndex: index,
|
|
179
|
+
hasPartialJson: blockState.partialJson.length > 0,
|
|
180
|
+
};
|
|
179
181
|
}
|
|
180
182
|
|
|
181
183
|
function createStreamTruncatedError(context: StreamTruncatedContext = {}): StreamTruncatedError {
|
|
182
|
-
|
|
184
|
+
return new StreamTruncatedError("Stream truncated without message_stop", context);
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
function getBufferedEventContext(eventBlock: string, lastEventType: string | null): StreamTruncatedContext {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
const context: StreamTruncatedContext = {
|
|
189
|
+
lastEventType: lastEventType ?? undefined,
|
|
190
|
+
};
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
192
|
+
const payload = getSSEDataPayload(eventBlock);
|
|
193
|
+
if (!payload) {
|
|
194
|
+
return context;
|
|
195
|
+
}
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
try {
|
|
198
|
+
const parsed = JSON.parse(payload) as Record<string, unknown>;
|
|
199
|
+
const eventType = getSSEEventType(eventBlock) ?? (typeof parsed.type === "string" ? parsed.type : null);
|
|
200
|
+
if (eventType) {
|
|
201
|
+
context.inFlightEvent = getEventLabel(parsed, eventType);
|
|
202
|
+
}
|
|
203
|
+
} catch {
|
|
204
|
+
// JSON parse failed; context will be returned without inFlightEvent
|
|
200
205
|
}
|
|
201
|
-
} catch {
|
|
202
|
-
// JSON parse failed; context will be returned without inFlightEvent
|
|
203
|
-
}
|
|
204
206
|
|
|
205
|
-
|
|
207
|
+
return context;
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
function validateEventState(
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
parsed: Record<string, unknown>,
|
|
212
|
+
eventType: string,
|
|
213
|
+
openContentBlocks: Map<number, OpenContentBlockState>,
|
|
212
214
|
): void {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (openContentBlocks.has(index)) {
|
|
222
|
-
throw new Error(`duplicate content_block_start for index ${index}`);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const blockType = (contentBlock as Record<string, unknown>).type;
|
|
226
|
-
if (typeof blockType !== "string" || !blockType) {
|
|
227
|
-
throw new Error("invalid SSE content_block_start event: missing content_block.type");
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
openContentBlocks.set(index, {
|
|
231
|
-
type: blockType,
|
|
232
|
-
partialJson: "",
|
|
233
|
-
});
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
215
|
+
switch (eventType) {
|
|
216
|
+
case "content_block_start": {
|
|
217
|
+
const index = getEventIndex(parsed, eventType);
|
|
218
|
+
const contentBlock = parsed.content_block;
|
|
219
|
+
if (!contentBlock || typeof contentBlock !== "object") {
|
|
220
|
+
throw new Error("invalid SSE content_block_start event: missing content_block");
|
|
221
|
+
}
|
|
236
222
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if (!delta || typeof delta !== "object") {
|
|
246
|
-
throw new Error("invalid SSE content_block_delta event: missing delta");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const deltaType = (delta as Record<string, unknown>).type;
|
|
250
|
-
if (deltaType === "input_json_delta") {
|
|
251
|
-
if (blockState.type !== "tool_use") {
|
|
252
|
-
throw new Error(`orphan input_json_delta for non-tool_use block ${index}`);
|
|
253
|
-
}
|
|
223
|
+
if (openContentBlocks.has(index)) {
|
|
224
|
+
throw new Error(`duplicate content_block_start for index ${index}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const blockType = (contentBlock as Record<string, unknown>).type;
|
|
228
|
+
if (typeof blockType !== "string" || !blockType) {
|
|
229
|
+
throw new Error("invalid SSE content_block_start event: missing content_block.type");
|
|
230
|
+
}
|
|
254
231
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
232
|
+
openContentBlocks.set(index, {
|
|
233
|
+
type: blockType,
|
|
234
|
+
partialJson: "",
|
|
235
|
+
});
|
|
236
|
+
return;
|
|
258
237
|
}
|
|
259
238
|
|
|
260
|
-
|
|
261
|
-
|
|
239
|
+
case "content_block_delta": {
|
|
240
|
+
const index = getEventIndex(parsed, eventType);
|
|
241
|
+
const blockState = openContentBlocks.get(index);
|
|
242
|
+
if (!blockState) {
|
|
243
|
+
throw new Error(`orphan content_block_delta for index ${index}`);
|
|
244
|
+
}
|
|
262
245
|
|
|
263
|
-
|
|
264
|
-
|
|
246
|
+
const delta = parsed.delta;
|
|
247
|
+
if (!delta || typeof delta !== "object") {
|
|
248
|
+
throw new Error("invalid SSE content_block_delta event: missing delta");
|
|
249
|
+
}
|
|
265
250
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
251
|
+
const deltaType = (delta as Record<string, unknown>).type;
|
|
252
|
+
if (deltaType === "input_json_delta") {
|
|
253
|
+
if (blockState.type !== "tool_use") {
|
|
254
|
+
throw new Error(`orphan input_json_delta for non-tool_use block ${index}`);
|
|
255
|
+
}
|
|
272
256
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
257
|
+
const partialJson = (delta as Record<string, unknown>).partial_json;
|
|
258
|
+
if (typeof partialJson !== "string") {
|
|
259
|
+
throw new Error("invalid SSE content_block_delta event: missing delta.partial_json");
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
blockState.partialJson += partialJson;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return;
|
|
278
266
|
}
|
|
279
|
-
}
|
|
280
267
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
268
|
+
case "content_block_stop": {
|
|
269
|
+
const index = getEventIndex(parsed, eventType);
|
|
270
|
+
const blockState = openContentBlocks.get(index);
|
|
271
|
+
if (!blockState) {
|
|
272
|
+
throw new Error(`orphan content_block_stop for index ${index}`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (blockState.type === "tool_use" && blockState.partialJson) {
|
|
276
|
+
try {
|
|
277
|
+
JSON.parse(blockState.partialJson);
|
|
278
|
+
} catch {
|
|
279
|
+
throw new Error(`incomplete tool_use partial_json for index ${index}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
284
282
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
283
|
+
openContentBlocks.delete(index);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
288
290
|
}
|
|
289
291
|
|
|
290
292
|
function getOpenBlockError(openContentBlocks: Map<number, OpenContentBlockState>): Error | null {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
+
const openBlockContext = getOpenBlockContext(openContentBlocks);
|
|
294
|
+
return openBlockContext ? createStreamTruncatedError(openBlockContext) : null;
|
|
293
295
|
}
|
|
294
296
|
|
|
295
297
|
function getMessageStopBlockError(openContentBlocks: Map<number, OpenContentBlockState>): Error | null {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
298
|
+
for (const [index, blockState] of openContentBlocks) {
|
|
299
|
+
if (blockState.partialJson) {
|
|
300
|
+
return new Error(`incomplete tool_use partial_json for index ${index}`);
|
|
301
|
+
}
|
|
299
302
|
}
|
|
300
|
-
}
|
|
301
303
|
|
|
302
|
-
|
|
304
|
+
return null;
|
|
303
305
|
}
|
|
304
306
|
|
|
305
307
|
function normalizeChunk(text: string): string {
|
|
306
|
-
|
|
308
|
+
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
307
309
|
}
|
|
308
310
|
|
|
309
311
|
function toStreamError(error: unknown): Error {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
312
|
+
if (error instanceof Error) {
|
|
313
|
+
return error;
|
|
314
|
+
}
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
return new Error(String(error));
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
/**
|
|
318
320
|
* Parse one SSE event payload and return account-error details if present.
|
|
319
321
|
*/
|
|
320
322
|
export function getMidStreamAccountError(parsed: unknown): {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
+
reason: string;
|
|
324
|
+
invalidateToken: boolean;
|
|
323
325
|
} | null {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
326
|
+
const p = parsed as Record<string, unknown> | null;
|
|
327
|
+
if (!p || p.type !== "error" || !p.error) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const err = p.error as Record<string, unknown>;
|
|
332
|
+
const errorBody = {
|
|
333
|
+
error: {
|
|
334
|
+
type: String(err.type || ""),
|
|
335
|
+
message: String(err.message || ""),
|
|
336
|
+
},
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// Mid-stream errors do not include a reliable HTTP status. Use 400-style
|
|
340
|
+
// body parsing to identify account-specific errors.
|
|
341
|
+
if (!isAccountSpecificError(400, errorBody)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
const reason = parseRateLimitReason(400, errorBody);
|
|
344
346
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
347
|
+
return {
|
|
348
|
+
reason,
|
|
349
|
+
invalidateToken: reason === "AUTH_FAILED",
|
|
350
|
+
};
|
|
349
351
|
}
|
|
350
352
|
|
|
351
353
|
/**
|
|
@@ -354,210 +356,210 @@ export function getMidStreamAccountError(parsed: unknown): {
|
|
|
354
356
|
* account-specific errors.
|
|
355
357
|
*/
|
|
356
358
|
export function transformResponse(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
359
|
+
response: Response,
|
|
360
|
+
onUsage?: ((stats: UsageStats) => void) | null,
|
|
361
|
+
onAccountError?: ((details: { reason: string; invalidateToken: boolean }) => void) | null,
|
|
362
|
+
onStreamError?: ((error: Error) => void) | null,
|
|
361
363
|
): Response {
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
let parsed: unknown;
|
|
389
|
-
try {
|
|
390
|
-
parsed = JSON.parse(payload);
|
|
391
|
-
} catch {
|
|
392
|
-
throw new Error("invalid SSE event: malformed JSON payload");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const eventType =
|
|
396
|
-
getSSEEventType(eventBlock) ?? ((parsed as Record<string, unknown> | null)?.type as string | undefined);
|
|
397
|
-
if (typeof eventType !== "string" || !eventType) {
|
|
398
|
-
throw new Error("invalid SSE event: missing event type");
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
const parsedRecord = parsed as Record<string, unknown>;
|
|
402
|
-
lastEventType = getEventLabel(parsedRecord, eventType);
|
|
403
|
-
if (strictEventValidation) {
|
|
404
|
-
validateEventState(parsedRecord, eventType, openContentBlocks);
|
|
405
|
-
}
|
|
406
|
-
stripMcpPrefixFromParsedEvent(parsedRecord);
|
|
407
|
-
|
|
408
|
-
if (onUsage) {
|
|
409
|
-
extractUsageFromSSEEvent(parsedRecord, stats);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (onAccountError && !accountErrorHandled) {
|
|
413
|
-
const details = getMidStreamAccountError(parsedRecord);
|
|
414
|
-
if (details) {
|
|
415
|
-
accountErrorHandled = true;
|
|
416
|
-
onAccountError(details);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
if (eventType === "message_stop") {
|
|
421
|
-
if (strictEventValidation) {
|
|
422
|
-
const openBlockError = getMessageStopBlockError(openContentBlocks);
|
|
423
|
-
if (openBlockError) {
|
|
424
|
-
throw openBlockError;
|
|
364
|
+
if (!response.body || !isEventStreamResponse(response)) return response;
|
|
365
|
+
|
|
366
|
+
const reader = response.body.getReader();
|
|
367
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
368
|
+
const encoder = new TextEncoder();
|
|
369
|
+
|
|
370
|
+
const stats: UsageStats = {
|
|
371
|
+
inputTokens: 0,
|
|
372
|
+
outputTokens: 0,
|
|
373
|
+
cacheReadTokens: 0,
|
|
374
|
+
cacheWriteTokens: 0,
|
|
375
|
+
};
|
|
376
|
+
let sseBuffer = "";
|
|
377
|
+
let accountErrorHandled = false;
|
|
378
|
+
let hasSeenMessageStop = false;
|
|
379
|
+
let hasSeenError = false;
|
|
380
|
+
let lastEventType: string | null = null;
|
|
381
|
+
const strictEventValidation = !onUsage && !onAccountError;
|
|
382
|
+
const openContentBlocks = new Map<number, OpenContentBlockState>();
|
|
383
|
+
|
|
384
|
+
function enqueueNormalizedEvent(controller: ReadableStreamDefaultController<Uint8Array>, eventBlock: string): void {
|
|
385
|
+
const payload = getSSEDataPayload(eventBlock);
|
|
386
|
+
if (!payload) {
|
|
387
|
+
return;
|
|
425
388
|
}
|
|
426
389
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
if (eventType === "error") {
|
|
434
|
-
hasSeenError = true;
|
|
435
|
-
}
|
|
390
|
+
let parsed: unknown;
|
|
391
|
+
try {
|
|
392
|
+
parsed = JSON.parse(payload);
|
|
393
|
+
} catch {
|
|
394
|
+
throw new Error("invalid SSE event: malformed JSON payload");
|
|
395
|
+
}
|
|
436
396
|
|
|
437
|
-
|
|
397
|
+
const eventType =
|
|
398
|
+
getSSEEventType(eventBlock) ?? ((parsed as Record<string, unknown> | null)?.type as string | undefined);
|
|
399
|
+
if (typeof eventType !== "string" || !eventType) {
|
|
400
|
+
throw new Error("invalid SSE event: missing event type");
|
|
401
|
+
}
|
|
438
402
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
403
|
+
const parsedRecord = parsed as Record<string, unknown>;
|
|
404
|
+
lastEventType = getEventLabel(parsedRecord, eventType);
|
|
405
|
+
if (strictEventValidation) {
|
|
406
|
+
validateEventState(parsedRecord, eventType, openContentBlocks);
|
|
407
|
+
}
|
|
408
|
+
stripMcpPrefixFromParsedEvent(parsedRecord);
|
|
443
409
|
|
|
444
|
-
|
|
445
|
-
|
|
410
|
+
if (onUsage) {
|
|
411
|
+
extractUsageFromSSEEvent(parsedRecord, stats);
|
|
412
|
+
}
|
|
446
413
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
414
|
+
if (onAccountError && !accountErrorHandled) {
|
|
415
|
+
const details = getMidStreamAccountError(parsedRecord);
|
|
416
|
+
if (details) {
|
|
417
|
+
accountErrorHandled = true;
|
|
418
|
+
onAccountError(details);
|
|
419
|
+
}
|
|
452
420
|
}
|
|
453
|
-
return emitted;
|
|
454
|
-
}
|
|
455
421
|
|
|
456
|
-
|
|
457
|
-
|
|
422
|
+
if (eventType === "message_stop") {
|
|
423
|
+
if (strictEventValidation) {
|
|
424
|
+
const openBlockError = getMessageStopBlockError(openContentBlocks);
|
|
425
|
+
if (openBlockError) {
|
|
426
|
+
throw openBlockError;
|
|
427
|
+
}
|
|
458
428
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
429
|
+
openContentBlocks.clear();
|
|
430
|
+
}
|
|
462
431
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
}
|
|
466
|
-
}
|
|
432
|
+
hasSeenMessageStop = true;
|
|
433
|
+
}
|
|
467
434
|
|
|
468
|
-
|
|
469
|
-
|
|
435
|
+
if (eventType === "error") {
|
|
436
|
+
hasSeenError = true;
|
|
437
|
+
}
|
|
470
438
|
|
|
471
|
-
|
|
472
|
-
try {
|
|
473
|
-
onStreamError(streamError);
|
|
474
|
-
} catch {
|
|
475
|
-
// Error handler failed; continue with cleanup
|
|
476
|
-
}
|
|
477
|
-
}
|
|
439
|
+
controller.enqueue(encoder.encode(formatSSEEventBlock(eventType, parsedRecord, strictEventValidation)));
|
|
478
440
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
// Reader cancel failed; stream may already be closed
|
|
441
|
+
if (eventType === "error" && strictEventValidation) {
|
|
442
|
+
throw new Error(getErrorMessage(parsedRecord));
|
|
443
|
+
}
|
|
483
444
|
}
|
|
484
445
|
|
|
485
|
-
controller
|
|
486
|
-
|
|
446
|
+
function processBufferedEvents(controller: ReadableStreamDefaultController<Uint8Array>): boolean {
|
|
447
|
+
let emitted = false;
|
|
487
448
|
|
|
488
|
-
const stream = new ReadableStream({
|
|
489
|
-
async pull(controller) {
|
|
490
|
-
try {
|
|
491
449
|
while (true) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
450
|
+
const boundary = sseBuffer.indexOf("\n\n");
|
|
451
|
+
if (boundary === -1) {
|
|
452
|
+
if (sseBuffer.length > MAX_UNTERMINATED_SSE_BUFFER) {
|
|
453
|
+
throw new Error("unterminated SSE event buffer exceeded limit");
|
|
454
|
+
}
|
|
455
|
+
return emitted;
|
|
498
456
|
}
|
|
499
457
|
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
throw createStreamTruncatedError(getBufferedEventContext(sseBuffer, lastEventType));
|
|
503
|
-
}
|
|
458
|
+
const eventBlock = sseBuffer.slice(0, boundary);
|
|
459
|
+
sseBuffer = sseBuffer.slice(boundary + 2);
|
|
504
460
|
|
|
505
|
-
|
|
506
|
-
|
|
461
|
+
if (!eventBlock.trim()) {
|
|
462
|
+
continue;
|
|
507
463
|
}
|
|
508
464
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
throw createStreamTruncatedError({
|
|
517
|
-
inFlightEvent: lastEventType ?? undefined,
|
|
518
|
-
lastEventType: lastEventType ?? undefined,
|
|
519
|
-
});
|
|
520
|
-
}
|
|
521
|
-
}
|
|
465
|
+
enqueueNormalizedEvent(controller, eventBlock);
|
|
466
|
+
emitted = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function failStream(controller: ReadableStreamDefaultController<Uint8Array>, error: unknown): Promise<void> {
|
|
471
|
+
const streamError = toStreamError(error);
|
|
522
472
|
|
|
523
|
-
|
|
524
|
-
|
|
473
|
+
if (onStreamError) {
|
|
474
|
+
try {
|
|
475
|
+
onStreamError(streamError);
|
|
476
|
+
} catch {
|
|
477
|
+
// Error handler failed; continue with cleanup
|
|
525
478
|
}
|
|
479
|
+
}
|
|
526
480
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
481
|
+
try {
|
|
482
|
+
await reader.cancel(streamError);
|
|
483
|
+
} catch {
|
|
484
|
+
// Reader cancel failed; stream may already be closed
|
|
485
|
+
}
|
|
530
486
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
487
|
+
controller.error(streamError);
|
|
488
|
+
}
|
|
535
489
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
490
|
+
const stream = new ReadableStream({
|
|
491
|
+
async pull(controller) {
|
|
492
|
+
try {
|
|
493
|
+
while (true) {
|
|
494
|
+
const { done, value } = await reader.read();
|
|
495
|
+
if (done) {
|
|
496
|
+
const flushedText = decoder.decode();
|
|
497
|
+
if (flushedText) {
|
|
498
|
+
sseBuffer += normalizeChunk(flushedText);
|
|
499
|
+
processBufferedEvents(controller);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (sseBuffer.trim()) {
|
|
503
|
+
if (strictEventValidation) {
|
|
504
|
+
throw createStreamTruncatedError(getBufferedEventContext(sseBuffer, lastEventType));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
enqueueNormalizedEvent(controller, sseBuffer);
|
|
508
|
+
sseBuffer = "";
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (strictEventValidation) {
|
|
512
|
+
const openBlockError = getOpenBlockError(openContentBlocks);
|
|
513
|
+
if (openBlockError) {
|
|
514
|
+
throw openBlockError;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (!hasSeenMessageStop && !hasSeenError) {
|
|
518
|
+
throw createStreamTruncatedError({
|
|
519
|
+
inFlightEvent: lastEventType ?? undefined,
|
|
520
|
+
lastEventType: lastEventType ?? undefined,
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (onUsage && hasRecordedUsage(stats)) {
|
|
526
|
+
onUsage(stats);
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
controller.close();
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const text = decoder.decode(value, { stream: true });
|
|
534
|
+
if (!text) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
sseBuffer += normalizeChunk(text);
|
|
539
|
+
if (processBufferedEvents(controller)) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} catch (error) {
|
|
544
|
+
await failStream(controller, error);
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
cancel(reason) {
|
|
548
|
+
return reader.cancel(reason);
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return new Response(stream, {
|
|
553
|
+
status: response.status,
|
|
554
|
+
statusText: response.statusText,
|
|
555
|
+
headers: response.headers,
|
|
556
|
+
});
|
|
555
557
|
}
|
|
556
558
|
|
|
557
559
|
/**
|
|
558
560
|
* Check whether a response is an SSE event stream.
|
|
559
561
|
*/
|
|
560
562
|
export function isEventStreamResponse(response: Response): boolean {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
+
const contentType = response.headers.get("content-type") || "";
|
|
564
|
+
return contentType.toLowerCase().includes("text/event-stream");
|
|
563
565
|
}
|