@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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. 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
- type: string;
13
- partialJson: string;
12
+ type: string;
13
+ partialJson: string;
14
14
  }
15
15
 
16
16
  interface StreamTruncatedContext {
17
- inFlightEvent?: string;
18
- lastEventType?: string;
19
- openContentBlockIndex?: number;
20
- hasPartialJson?: boolean;
17
+ inFlightEvent?: string;
18
+ lastEventType?: string;
19
+ openContentBlockIndex?: number;
20
+ hasPartialJson?: boolean;
21
21
  }
22
22
 
23
23
  export class StreamTruncatedError extends Error {
24
- readonly context: StreamTruncatedContext;
24
+ readonly context: StreamTruncatedContext;
25
25
 
26
- constructor(message: string, context: StreamTruncatedContext = {}) {
27
- super(message);
28
- this.name = "StreamTruncatedError";
29
- this.context = context;
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
- 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
- }
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
- 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;
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
- 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
- }
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
- return null;
93
+ return null;
94
94
  }
95
95
 
96
96
  function formatSSEEventBlock(eventType: string, parsed: unknown, prettyPrint: boolean): string {
97
- const json = prettyPrint ? JSON.stringify(parsed, null, 2) : JSON.stringify(parsed);
98
- const lines = [`event: ${eventType}`];
97
+ const json = prettyPrint ? JSON.stringify(parsed, null, 2) : JSON.stringify(parsed);
98
+ const lines = [`event: ${eventType}`];
99
99
 
100
- for (const line of json.split("\n")) {
101
- lines.push(`data: ${line}`);
102
- }
100
+ for (const line of json.split("\n")) {
101
+ lines.push(`data: ${line}`);
102
+ }
103
103
 
104
- lines.push("", "");
105
- return lines.join("\n");
104
+ lines.push("", "");
105
+ return lines.join("\n");
106
106
  }
107
107
 
108
108
  function hasRecordedUsage(stats: UsageStats): boolean {
109
- return stats.inputTokens > 0 || stats.outputTokens > 0 || stats.cacheReadTokens > 0 || stats.cacheWriteTokens > 0;
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
- if (!parsed || typeof parsed !== "object") {
114
- return "stream terminated with error event";
115
- }
113
+ if (!parsed || typeof parsed !== "object") {
114
+ return "stream terminated with error event";
115
+ }
116
116
 
117
- const error = (parsed as Record<string, unknown>).error;
118
- if (!error || typeof error !== "object") {
119
- return "stream terminated with error event";
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
- const message = (error as Record<string, unknown>).message;
123
- return typeof message === "string" && message ? message : "stream terminated with error event";
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
- const index = parsed.index;
128
- if (typeof index !== "number") {
129
- throw new Error(`invalid SSE ${eventType} event: missing numeric index`);
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
- return index;
132
+ return index;
133
133
  }
134
134
 
135
135
  function getEventLabel(parsed: Record<string, unknown>, eventType: string): string {
136
- switch (eventType) {
137
- case "content_block_start": {
138
- const contentBlock = parsed.content_block;
139
- const blockType =
140
- contentBlock && typeof contentBlock === "object" ? (contentBlock as Record<string, unknown>).type : undefined;
141
- return typeof blockType === "string" && blockType ? `content_block_start(${blockType})` : eventType;
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
- case "content_block_delta": {
145
- const delta = parsed.delta;
146
- const deltaType = delta && typeof delta === "object" ? (delta as Record<string, unknown>).type : undefined;
147
- return typeof deltaType === "string" && deltaType ? `content_block_delta(${deltaType})` : eventType;
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
- default:
151
- return eventType;
152
- }
152
+ default:
153
+ return eventType;
154
+ }
153
155
  }
154
156
 
155
157
  function getOpenBlockContext(openContentBlocks: Map<number, OpenContentBlockState>): StreamTruncatedContext | null {
156
- for (const [index, blockState] of openContentBlocks) {
157
- if (blockState.type === "tool_use") {
158
- return {
159
- inFlightEvent: blockState.partialJson
160
- ? "content_block_delta(input_json_delta)"
161
- : "content_block_start(tool_use)",
162
- openContentBlockIndex: index,
163
- hasPartialJson: blockState.partialJson.length > 0,
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
- const firstOpenBlock = openContentBlocks.entries().next().value as [number, OpenContentBlockState] | undefined;
169
- if (!firstOpenBlock) {
170
- return null;
171
- }
172
-
173
- const [index, blockState] = firstOpenBlock;
174
- return {
175
- inFlightEvent: `content_block_start(${blockState.type})`,
176
- openContentBlockIndex: index,
177
- hasPartialJson: blockState.partialJson.length > 0,
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
- return new StreamTruncatedError("Stream truncated without message_stop", context);
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
- const context: StreamTruncatedContext = {
187
- lastEventType: lastEventType ?? undefined,
188
- };
188
+ const context: StreamTruncatedContext = {
189
+ lastEventType: lastEventType ?? undefined,
190
+ };
189
191
 
190
- const payload = getSSEDataPayload(eventBlock);
191
- if (!payload) {
192
- return context;
193
- }
192
+ const payload = getSSEDataPayload(eventBlock);
193
+ if (!payload) {
194
+ return context;
195
+ }
194
196
 
195
- try {
196
- const parsed = JSON.parse(payload) as Record<string, unknown>;
197
- const eventType = getSSEEventType(eventBlock) ?? (typeof parsed.type === "string" ? parsed.type : null);
198
- if (eventType) {
199
- context.inFlightEvent = getEventLabel(parsed, eventType);
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
- return context;
207
+ return context;
206
208
  }
207
209
 
208
210
  function validateEventState(
209
- parsed: Record<string, unknown>,
210
- eventType: string,
211
- openContentBlocks: Map<number, OpenContentBlockState>,
211
+ parsed: Record<string, unknown>,
212
+ eventType: string,
213
+ openContentBlocks: Map<number, OpenContentBlockState>,
212
214
  ): void {
213
- switch (eventType) {
214
- case "content_block_start": {
215
- const index = getEventIndex(parsed, eventType);
216
- const contentBlock = parsed.content_block;
217
- if (!contentBlock || typeof contentBlock !== "object") {
218
- throw new Error("invalid SSE content_block_start event: missing content_block");
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
- case "content_block_delta": {
238
- const index = getEventIndex(parsed, eventType);
239
- const blockState = openContentBlocks.get(index);
240
- if (!blockState) {
241
- throw new Error(`orphan content_block_delta for index ${index}`);
242
- }
243
-
244
- const delta = parsed.delta;
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
- const partialJson = (delta as Record<string, unknown>).partial_json;
256
- if (typeof partialJson !== "string") {
257
- throw new Error("invalid SSE content_block_delta event: missing delta.partial_json");
232
+ openContentBlocks.set(index, {
233
+ type: blockType,
234
+ partialJson: "",
235
+ });
236
+ return;
258
237
  }
259
238
 
260
- blockState.partialJson += partialJson;
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
- return;
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
- case "content_block_stop": {
267
- const index = getEventIndex(parsed, eventType);
268
- const blockState = openContentBlocks.get(index);
269
- if (!blockState) {
270
- throw new Error(`orphan content_block_stop for index ${index}`);
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
- if (blockState.type === "tool_use" && blockState.partialJson) {
274
- try {
275
- JSON.parse(blockState.partialJson);
276
- } catch {
277
- throw new Error(`incomplete tool_use partial_json for index ${index}`);
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
- openContentBlocks.delete(index);
282
- return;
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
- default:
286
- return;
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
- const openBlockContext = getOpenBlockContext(openContentBlocks);
292
- return openBlockContext ? createStreamTruncatedError(openBlockContext) : null;
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
- for (const [index, blockState] of openContentBlocks) {
297
- if (blockState.partialJson) {
298
- return new Error(`incomplete tool_use partial_json for index ${index}`);
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
- return null;
304
+ return null;
303
305
  }
304
306
 
305
307
  function normalizeChunk(text: string): string {
306
- return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
308
+ return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
307
309
  }
308
310
 
309
311
  function toStreamError(error: unknown): Error {
310
- if (error instanceof Error) {
311
- return error;
312
- }
312
+ if (error instanceof Error) {
313
+ return error;
314
+ }
313
315
 
314
- return new Error(String(error));
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
- reason: string;
322
- invalidateToken: boolean;
323
+ reason: string;
324
+ invalidateToken: boolean;
323
325
  } | null {
324
- const p = parsed as Record<string, unknown> | null;
325
- if (!p || p.type !== "error" || !p.error) {
326
- return null;
327
- }
328
-
329
- const err = p.error as Record<string, unknown>;
330
- const errorBody = {
331
- error: {
332
- type: String(err.type || ""),
333
- message: String(err.message || ""),
334
- },
335
- };
336
-
337
- // Mid-stream errors do not include a reliable HTTP status. Use 400-style
338
- // body parsing to identify account-specific errors.
339
- if (!isAccountSpecificError(400, errorBody)) {
340
- return null;
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
- const reason = parseRateLimitReason(400, errorBody);
345
+ const reason = parseRateLimitReason(400, errorBody);
344
346
 
345
- return {
346
- reason,
347
- invalidateToken: reason === "AUTH_FAILED",
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
- response: Response,
358
- onUsage?: ((stats: UsageStats) => void) | null,
359
- onAccountError?: ((details: { reason: string; invalidateToken: boolean }) => void) | null,
360
- onStreamError?: ((error: Error) => void) | null,
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
- if (!response.body || !isEventStreamResponse(response)) return response;
363
-
364
- const reader = response.body.getReader();
365
- const decoder = new TextDecoder("utf-8", { fatal: true });
366
- const encoder = new TextEncoder();
367
-
368
- const stats: UsageStats = {
369
- inputTokens: 0,
370
- outputTokens: 0,
371
- cacheReadTokens: 0,
372
- cacheWriteTokens: 0,
373
- };
374
- let sseBuffer = "";
375
- let accountErrorHandled = false;
376
- let hasSeenMessageStop = false;
377
- let hasSeenError = false;
378
- let lastEventType: string | null = null;
379
- const strictEventValidation = !onUsage && !onAccountError;
380
- const openContentBlocks = new Map<number, OpenContentBlockState>();
381
-
382
- function enqueueNormalizedEvent(controller: ReadableStreamDefaultController<Uint8Array>, eventBlock: string): void {
383
- const payload = getSSEDataPayload(eventBlock);
384
- if (!payload) {
385
- return;
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
- openContentBlocks.clear();
428
- }
429
-
430
- hasSeenMessageStop = true;
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
- controller.enqueue(encoder.encode(formatSSEEventBlock(eventType, parsedRecord, strictEventValidation)));
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
- if (eventType === "error" && strictEventValidation) {
440
- throw new Error(getErrorMessage(parsedRecord));
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
- function processBufferedEvents(controller: ReadableStreamDefaultController<Uint8Array>): boolean {
445
- let emitted = false;
410
+ if (onUsage) {
411
+ extractUsageFromSSEEvent(parsedRecord, stats);
412
+ }
446
413
 
447
- while (true) {
448
- const boundary = sseBuffer.indexOf("\n\n");
449
- if (boundary === -1) {
450
- if (sseBuffer.length > MAX_UNTERMINATED_SSE_BUFFER) {
451
- throw new Error("unterminated SSE event buffer exceeded limit");
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
- const eventBlock = sseBuffer.slice(0, boundary);
457
- sseBuffer = sseBuffer.slice(boundary + 2);
422
+ if (eventType === "message_stop") {
423
+ if (strictEventValidation) {
424
+ const openBlockError = getMessageStopBlockError(openContentBlocks);
425
+ if (openBlockError) {
426
+ throw openBlockError;
427
+ }
458
428
 
459
- if (!eventBlock.trim()) {
460
- continue;
461
- }
429
+ openContentBlocks.clear();
430
+ }
462
431
 
463
- enqueueNormalizedEvent(controller, eventBlock);
464
- emitted = true;
465
- }
466
- }
432
+ hasSeenMessageStop = true;
433
+ }
467
434
 
468
- async function failStream(controller: ReadableStreamDefaultController<Uint8Array>, error: unknown): Promise<void> {
469
- const streamError = toStreamError(error);
435
+ if (eventType === "error") {
436
+ hasSeenError = true;
437
+ }
470
438
 
471
- if (onStreamError) {
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
- try {
480
- await reader.cancel(streamError);
481
- } catch {
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.error(streamError);
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
- const { done, value } = await reader.read();
493
- if (done) {
494
- const flushedText = decoder.decode();
495
- if (flushedText) {
496
- sseBuffer += normalizeChunk(flushedText);
497
- processBufferedEvents(controller);
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
- if (sseBuffer.trim()) {
501
- if (strictEventValidation) {
502
- throw createStreamTruncatedError(getBufferedEventContext(sseBuffer, lastEventType));
503
- }
458
+ const eventBlock = sseBuffer.slice(0, boundary);
459
+ sseBuffer = sseBuffer.slice(boundary + 2);
504
460
 
505
- enqueueNormalizedEvent(controller, sseBuffer);
506
- sseBuffer = "";
461
+ if (!eventBlock.trim()) {
462
+ continue;
507
463
  }
508
464
 
509
- if (strictEventValidation) {
510
- const openBlockError = getOpenBlockError(openContentBlocks);
511
- if (openBlockError) {
512
- throw openBlockError;
513
- }
514
-
515
- if (!hasSeenMessageStop && !hasSeenError) {
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
- if (onUsage && hasRecordedUsage(stats)) {
524
- onUsage(stats);
473
+ if (onStreamError) {
474
+ try {
475
+ onStreamError(streamError);
476
+ } catch {
477
+ // Error handler failed; continue with cleanup
525
478
  }
479
+ }
526
480
 
527
- controller.close();
528
- return;
529
- }
481
+ try {
482
+ await reader.cancel(streamError);
483
+ } catch {
484
+ // Reader cancel failed; stream may already be closed
485
+ }
530
486
 
531
- const text = decoder.decode(value, { stream: true });
532
- if (!text) {
533
- continue;
534
- }
487
+ controller.error(streamError);
488
+ }
535
489
 
536
- sseBuffer += normalizeChunk(text);
537
- if (processBufferedEvents(controller)) {
538
- return;
539
- }
540
- }
541
- } catch (error) {
542
- await failStream(controller, error);
543
- }
544
- },
545
- cancel(reason) {
546
- return reader.cancel(reason);
547
- },
548
- });
549
-
550
- return new Response(stream, {
551
- status: response.status,
552
- statusText: response.statusText,
553
- headers: response.headers,
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
- const contentType = response.headers.get("content-type") || "";
562
- return contentType.toLowerCase().includes("text/event-stream");
563
+ const contentType = response.headers.get("content-type") || "";
564
+ return contentType.toLowerCase().includes("text/event-stream");
563
565
  }