@sunerpy/opencode-kiro-auth 0.6.2 → 0.8.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.
Files changed (35) hide show
  1. package/README.md +12 -4
  2. package/dist/core/auth/auth-handler.js +4 -2
  3. package/dist/core/auth/idc-auth-method.js +15 -3
  4. package/dist/core/auth/token-keepalive.d.ts +27 -0
  5. package/dist/core/auth/token-keepalive.js +137 -0
  6. package/dist/core/auth/token-refresher.d.ts +20 -1
  7. package/dist/core/auth/token-refresher.js +124 -22
  8. package/dist/core/request/error-handler.d.ts +6 -3
  9. package/dist/core/request/error-handler.js +50 -24
  10. package/dist/core/request/request-handler.d.ts +2 -0
  11. package/dist/core/request/request-handler.js +5 -2
  12. package/dist/infrastructure/transformers/event-stream-parser.js +1 -1
  13. package/dist/infrastructure/transformers/tool-call-parser.js +1 -1
  14. package/dist/kiro/oauth-idc.js +54 -59
  15. package/dist/plugin/accounts.js +13 -8
  16. package/dist/plugin/config/loader.js +49 -1
  17. package/dist/plugin/config/schema.d.ts +14 -0
  18. package/dist/plugin/config/schema.js +14 -2
  19. package/dist/plugin/health.d.ts +22 -0
  20. package/dist/plugin/health.js +56 -1
  21. package/dist/plugin/image-handler.js +1 -1
  22. package/dist/plugin/logger.js +2 -2
  23. package/dist/plugin/request.js +1 -1
  24. package/dist/plugin/response.js +1 -1
  25. package/dist/plugin/storage/locked-operations.d.ts +7 -0
  26. package/dist/plugin/storage/locked-operations.js +93 -1
  27. package/dist/plugin/storage/migrations.js +1 -1
  28. package/dist/plugin/storage/sqlite.d.ts +4 -0
  29. package/dist/plugin/storage/sqlite.js +65 -3
  30. package/dist/plugin/streaming/sdk-stream-transformer.js +233 -244
  31. package/dist/plugin/streaming/stream-transformer.js +1 -1
  32. package/dist/plugin/sync/kiro-cli.js +0 -2
  33. package/dist/plugin.d.ts +2 -0
  34. package/dist/plugin.js +28 -1
  35. package/package.json +1 -1
@@ -39,7 +39,15 @@ export class KiroDatabase {
39
39
  runMigrations(this.db);
40
40
  }
41
41
  getAccounts() {
42
- return this.db.prepare('SELECT * FROM accounts').all();
42
+ return this.db
43
+ .prepare(`
44
+ SELECT accounts.*
45
+ FROM accounts
46
+ WHERE NOT EXISTS (
47
+ SELECT 1 FROM removed_accounts WHERE removed_accounts.id = accounts.id
48
+ )
49
+ `)
50
+ .all();
43
51
  }
44
52
  upsertAccountInternal(acc) {
45
53
  this.db
@@ -62,14 +70,22 @@ export class KiroDatabase {
62
70
  `)
63
71
  .run(acc.id, acc.email, acc.authMethod, acc.region, acc.oidcRegion || null, acc.clientId || null, acc.clientSecret || null, acc.profileArn || null, acc.startUrl || null, acc.refreshToken, acc.accessToken, acc.expiresAt, acc.rateLimitResetTime || 0, acc.isHealthy ? 1 : 0, acc.unhealthyReason || null, acc.recoveryTime || null, acc.failCount || 0, acc.lastUsed || 0, acc.usedCount || 0, acc.limitCount || 0, acc.lastSync || 0);
64
72
  }
73
+ isRemovedSync(id) {
74
+ return !!this.db.prepare('SELECT id FROM removed_accounts WHERE id = ?').get(id);
75
+ }
76
+ purgeRemovedAccountsSync() {
77
+ this.db.prepare('DELETE FROM accounts WHERE id IN (SELECT id FROM removed_accounts)').run();
78
+ }
65
79
  async upsertAccount(acc) {
66
80
  await withDatabaseLock(this.path, async () => {
67
81
  const existing = this.getAccounts().map(this.rowToAccount);
68
82
  const merged = mergeAccounts(existing, [acc]);
69
83
  const deduplicated = deduplicateAccounts(merged);
84
+ const writable = deduplicated.filter((a) => !this.isRemovedSync(a.id));
70
85
  this.db.exec('BEGIN TRANSACTION');
71
86
  try {
72
- for (const account of deduplicated) {
87
+ this.purgeRemovedAccountsSync();
88
+ for (const account of writable) {
73
89
  this.upsertAccountInternal(account);
74
90
  }
75
91
  this.db.exec('COMMIT');
@@ -85,9 +101,11 @@ export class KiroDatabase {
85
101
  const existing = this.getAccounts().map(this.rowToAccount);
86
102
  const merged = mergeAccounts(existing, accounts);
87
103
  const deduplicated = deduplicateAccounts(merged);
104
+ const writable = deduplicated.filter((a) => !this.isRemovedSync(a.id));
88
105
  this.db.exec('BEGIN TRANSACTION');
89
106
  try {
90
- for (const account of deduplicated) {
107
+ this.purgeRemovedAccountsSync();
108
+ for (const account of writable) {
91
109
  this.upsertAccountInternal(account);
92
110
  }
93
111
  this.db.exec('COMMIT');
@@ -103,6 +121,50 @@ export class KiroDatabase {
103
121
  this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
104
122
  });
105
123
  }
124
+ async removeAccountWithTombstone(id) {
125
+ await withDatabaseLock(this.path, async () => {
126
+ this.db.exec('BEGIN TRANSACTION');
127
+ try {
128
+ this.db.prepare('DELETE FROM accounts WHERE id = ?').run(id);
129
+ this.db
130
+ .prepare('INSERT OR REPLACE INTO removed_accounts (id, removed_at) VALUES (?, ?)')
131
+ .run(id, Date.now());
132
+ this.db.exec('COMMIT');
133
+ }
134
+ catch (e) {
135
+ this.db.exec('ROLLBACK');
136
+ throw e;
137
+ }
138
+ });
139
+ }
140
+ async cleanupSupersededIdentities(keepId, email, authMethod, profileArn) {
141
+ const supersededIds = [];
142
+ await withDatabaseLock(this.path, async () => {
143
+ this.db.exec('BEGIN TRANSACTION');
144
+ try {
145
+ const rows = this.db
146
+ .prepare('SELECT id FROM accounts WHERE email = ? AND auth_method = ? AND profile_arn IS ? AND id != ?')
147
+ .all(email, authMethod, profileArn ?? null, keepId);
148
+ for (const row of rows) {
149
+ if (typeof row === 'object' && row !== null && 'id' in row && typeof row.id === 'string')
150
+ supersededIds.push(row.id);
151
+ }
152
+ const deleteStmt = this.db.prepare('DELETE FROM accounts WHERE id = ?');
153
+ const tombstoneStmt = this.db.prepare('INSERT OR REPLACE INTO removed_accounts (id, removed_at) VALUES (?, ?)');
154
+ const removedAt = Date.now();
155
+ for (const id of supersededIds) {
156
+ deleteStmt.run(id);
157
+ tombstoneStmt.run(id, removedAt);
158
+ }
159
+ this.db.exec('COMMIT');
160
+ }
161
+ catch (e) {
162
+ this.db.exec('ROLLBACK');
163
+ throw e;
164
+ }
165
+ });
166
+ return supersededIds;
167
+ }
106
168
  async addRemovedAccount(id) {
107
169
  await withDatabaseLock(this.path, async () => {
108
170
  this.db
@@ -17,7 +17,6 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
17
17
  nextBlockIndex: 0,
18
18
  stoppedBlocks: new Set()
19
19
  };
20
- let totalContent = '';
21
20
  let textOnlyContent = '';
22
21
  let outputTokens = 0;
23
22
  let inputTokens = 0;
@@ -46,297 +45,287 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
46
45
  if (!eventStream) {
47
46
  throw new Error('SDK response has no event stream');
48
47
  }
49
- try {
50
- for await (const event of eventStream) {
51
- if (event.reasoningContentEvent?.text) {
52
- const reasoningText = event.reasoningContentEvent.text;
53
- if (reasoningClosed) {
54
- // Defensive, normally unreached (probe: reasoning is contiguous-before-text).
55
- // The stopped thinking index cannot be reused, so open a fresh one.
56
- streamState.thinkingBlockIndex = null;
57
- reasoningClosed = false;
58
- }
59
- reasoningStarted = true;
60
- for (const ev of createThinkingDeltaEvents(reasoningText, streamState)) {
48
+ for await (const event of eventStream) {
49
+ if (event.reasoningContentEvent?.text) {
50
+ const reasoningText = event.reasoningContentEvent.text;
51
+ if (reasoningClosed) {
52
+ // Defensive, normally unreached (probe: reasoning is contiguous-before-text).
53
+ // The stopped thinking index cannot be reused, so open a fresh one.
54
+ streamState.thinkingBlockIndex = null;
55
+ reasoningClosed = false;
56
+ }
57
+ reasoningStarted = true;
58
+ for (const ev of createThinkingDeltaEvents(reasoningText, streamState)) {
59
+ const _c = convertToOpenAI(ev, conversationId, model);
60
+ if (_c !== null)
61
+ yield _c;
62
+ }
63
+ continue;
64
+ }
65
+ if (event.assistantResponseEvent?.content) {
66
+ const text = event.assistantResponseEvent.content;
67
+ textOnlyContent += text;
68
+ if (reasoningStarted && !reasoningClosed) {
69
+ for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
61
70
  const _c = convertToOpenAI(ev, conversationId, model);
62
71
  if (_c !== null)
63
72
  yield _c;
64
73
  }
65
- continue;
74
+ reasoningClosed = true;
66
75
  }
67
- if (event.assistantResponseEvent?.content) {
68
- const text = event.assistantResponseEvent.content;
69
- totalContent += text;
70
- textOnlyContent += text;
71
- if (reasoningStarted && !reasoningClosed) {
72
- for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
73
- const _c = convertToOpenAI(ev, conversationId, model);
74
- if (_c !== null)
75
- yield _c;
76
- }
77
- reasoningClosed = true;
76
+ if (reasoningStarted) {
77
+ for (const ev of createTextDeltaEvents(text, streamState)) {
78
+ const _c = toChunk(ev);
79
+ if (_c !== null)
80
+ yield _c;
78
81
  }
79
- if (reasoningStarted) {
80
- for (const ev of createTextDeltaEvents(text, streamState)) {
82
+ continue;
83
+ }
84
+ if (!thinkingRequested) {
85
+ for (const ev of createTextDeltaEvents(text, streamState)) {
86
+ {
81
87
  const _c = toChunk(ev);
82
88
  if (_c !== null)
83
89
  yield _c;
84
90
  }
85
- continue;
86
91
  }
87
- if (!thinkingRequested) {
88
- for (const ev of createTextDeltaEvents(text, streamState)) {
89
- {
90
- const _c = toChunk(ev);
91
- if (_c !== null)
92
- yield _c;
92
+ continue;
93
+ }
94
+ streamState.buffer += text;
95
+ const deltaEvents = [];
96
+ while (streamState.buffer.length > 0) {
97
+ if (!streamState.inThinking && !streamState.thinkingExtracted) {
98
+ const startPos = findRealTag(streamState.buffer, THINKING_START_TAG);
99
+ if (startPos !== -1) {
100
+ const before = streamState.buffer.slice(0, startPos);
101
+ if (before) {
102
+ deltaEvents.push(...createTextDeltaEvents(before, streamState));
93
103
  }
104
+ streamState.buffer = streamState.buffer.slice(startPos + THINKING_START_TAG.length);
105
+ streamState.inThinking = true;
106
+ continue;
94
107
  }
95
- continue;
96
- }
97
- streamState.buffer += text;
98
- const deltaEvents = [];
99
- while (streamState.buffer.length > 0) {
100
- if (!streamState.inThinking && !streamState.thinkingExtracted) {
101
- const startPos = findRealTag(streamState.buffer, THINKING_START_TAG);
102
- if (startPos !== -1) {
103
- const before = streamState.buffer.slice(0, startPos);
104
- if (before) {
105
- deltaEvents.push(...createTextDeltaEvents(before, streamState));
106
- }
107
- streamState.buffer = streamState.buffer.slice(startPos + THINKING_START_TAG.length);
108
- streamState.inThinking = true;
109
- continue;
110
- }
111
- const safeLen = Math.max(0, streamState.buffer.length - THINKING_START_TAG.length);
112
- if (safeLen > 0) {
113
- const safeText = streamState.buffer.slice(0, safeLen);
114
- if (safeText) {
115
- deltaEvents.push(...createTextDeltaEvents(safeText, streamState));
116
- }
117
- streamState.buffer = streamState.buffer.slice(safeLen);
108
+ const safeLen = Math.max(0, streamState.buffer.length - THINKING_START_TAG.length);
109
+ if (safeLen > 0) {
110
+ const safeText = streamState.buffer.slice(0, safeLen);
111
+ if (safeText) {
112
+ deltaEvents.push(...createTextDeltaEvents(safeText, streamState));
118
113
  }
119
- break;
114
+ streamState.buffer = streamState.buffer.slice(safeLen);
120
115
  }
121
- if (streamState.inThinking) {
122
- const endPos = findRealTag(streamState.buffer, THINKING_END_TAG);
123
- if (endPos !== -1) {
124
- const thinkingPart = streamState.buffer.slice(0, endPos);
125
- if (thinkingPart) {
126
- deltaEvents.push(...createThinkingDeltaEvents(thinkingPart, streamState));
127
- }
128
- streamState.buffer = streamState.buffer.slice(endPos + THINKING_END_TAG.length);
129
- streamState.inThinking = false;
130
- streamState.thinkingExtracted = true;
131
- deltaEvents.push(...createThinkingDeltaEvents('', streamState));
132
- deltaEvents.push(...stopBlock(streamState.thinkingBlockIndex, streamState));
133
- if (streamState.buffer.startsWith('\n\n')) {
134
- streamState.buffer = streamState.buffer.slice(2);
135
- }
136
- continue;
116
+ break;
117
+ }
118
+ if (streamState.inThinking) {
119
+ const endPos = findRealTag(streamState.buffer, THINKING_END_TAG);
120
+ if (endPos !== -1) {
121
+ const thinkingPart = streamState.buffer.slice(0, endPos);
122
+ if (thinkingPart) {
123
+ deltaEvents.push(...createThinkingDeltaEvents(thinkingPart, streamState));
137
124
  }
138
- const safeLen = Math.max(0, streamState.buffer.length - THINKING_END_TAG.length);
139
- if (safeLen > 0) {
140
- const safeThinking = streamState.buffer.slice(0, safeLen);
141
- if (safeThinking) {
142
- deltaEvents.push(...createThinkingDeltaEvents(safeThinking, streamState));
143
- }
144
- streamState.buffer = streamState.buffer.slice(safeLen);
125
+ streamState.buffer = streamState.buffer.slice(endPos + THINKING_END_TAG.length);
126
+ streamState.inThinking = false;
127
+ streamState.thinkingExtracted = true;
128
+ deltaEvents.push(...createThinkingDeltaEvents('', streamState));
129
+ deltaEvents.push(...stopBlock(streamState.thinkingBlockIndex, streamState));
130
+ if (streamState.buffer.startsWith('\n\n')) {
131
+ streamState.buffer = streamState.buffer.slice(2);
145
132
  }
146
- break;
133
+ continue;
147
134
  }
148
- if (streamState.thinkingExtracted) {
149
- const rest = streamState.buffer;
150
- streamState.buffer = '';
151
- if (rest) {
152
- deltaEvents.push(...createTextDeltaEvents(rest, streamState));
135
+ const safeLen = Math.max(0, streamState.buffer.length - THINKING_END_TAG.length);
136
+ if (safeLen > 0) {
137
+ const safeThinking = streamState.buffer.slice(0, safeLen);
138
+ if (safeThinking) {
139
+ deltaEvents.push(...createThinkingDeltaEvents(safeThinking, streamState));
153
140
  }
154
- break;
141
+ streamState.buffer = streamState.buffer.slice(safeLen);
155
142
  }
143
+ break;
156
144
  }
157
- for (const ev of deltaEvents) {
158
- const chunk = toChunk(ev);
159
- if (chunk !== null)
160
- yield chunk;
145
+ if (streamState.thinkingExtracted) {
146
+ const rest = streamState.buffer;
147
+ streamState.buffer = '';
148
+ if (rest) {
149
+ deltaEvents.push(...createTextDeltaEvents(rest, streamState));
150
+ }
151
+ break;
161
152
  }
162
153
  }
163
- else if (event.toolUseEvent) {
164
- const tc = event.toolUseEvent;
165
- if (tc.name)
166
- totalContent += tc.name;
167
- if (tc.input)
168
- totalContent += tc.input;
169
- if (tc.name && tc.toolUseId) {
170
- if (currentToolCall && currentToolCall.toolUseId === tc.toolUseId) {
171
- currentToolCall.input += tc.input || '';
172
- }
173
- else {
174
- if (currentToolCall)
175
- toolCalls.push(currentToolCall);
176
- currentToolCall = {
177
- toolUseId: tc.toolUseId,
178
- name: tc.name,
179
- input: tc.input || ''
180
- };
181
- }
182
- if (tc.stop && currentToolCall) {
154
+ for (const ev of deltaEvents) {
155
+ const chunk = toChunk(ev);
156
+ if (chunk !== null)
157
+ yield chunk;
158
+ }
159
+ }
160
+ else if (event.toolUseEvent) {
161
+ const tc = event.toolUseEvent;
162
+ if (tc.name && tc.toolUseId) {
163
+ if (currentToolCall && currentToolCall.toolUseId === tc.toolUseId) {
164
+ currentToolCall.input += tc.input || '';
165
+ }
166
+ else {
167
+ if (currentToolCall)
183
168
  toolCalls.push(currentToolCall);
184
- currentToolCall = null;
185
- }
169
+ currentToolCall = {
170
+ toolUseId: tc.toolUseId,
171
+ name: tc.name,
172
+ input: tc.input || ''
173
+ };
186
174
  }
187
- }
188
- else if (event.metadataEvent) {
189
- if (event.metadataEvent.contextUsagePercentage) {
190
- contextUsagePercentage = event.metadataEvent.contextUsagePercentage;
175
+ if (tc.stop && currentToolCall) {
176
+ toolCalls.push(currentToolCall);
177
+ currentToolCall = null;
191
178
  }
192
179
  }
193
- else if (event.contextUsageEvent) {
194
- const cue = event.contextUsageEvent;
195
- if (cue.contextUsagePercentage) {
196
- contextUsagePercentage = cue.contextUsagePercentage;
197
- }
180
+ }
181
+ else if (event.metadataEvent) {
182
+ if (event.metadataEvent.contextUsagePercentage) {
183
+ contextUsagePercentage = event.metadataEvent.contextUsagePercentage;
198
184
  }
199
185
  }
200
- if (currentToolCall) {
201
- toolCalls.push(currentToolCall);
202
- currentToolCall = null;
186
+ else if (event.contextUsageEvent) {
187
+ const cue = event.contextUsageEvent;
188
+ if (cue.contextUsagePercentage) {
189
+ contextUsagePercentage = cue.contextUsagePercentage;
190
+ }
203
191
  }
204
- // Reasoning-only responses (reasoning but no reply text): close the thinking block.
205
- if (reasoningStarted && !reasoningClosed) {
206
- for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
192
+ }
193
+ if (currentToolCall) {
194
+ toolCalls.push(currentToolCall);
195
+ currentToolCall = null;
196
+ }
197
+ // Reasoning-only responses (reasoning but no reply text): close the thinking block.
198
+ if (reasoningStarted && !reasoningClosed) {
199
+ for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
200
+ const _c = convertToOpenAI(ev, conversationId, model);
201
+ if (_c !== null)
202
+ yield _c;
203
+ }
204
+ reasoningClosed = true;
205
+ }
206
+ if (thinkingRequested && streamState.buffer) {
207
+ if (streamState.inThinking) {
208
+ for (const ev of createThinkingDeltaEvents(streamState.buffer, streamState)) {
207
209
  const _c = convertToOpenAI(ev, conversationId, model);
208
210
  if (_c !== null)
209
211
  yield _c;
210
212
  }
211
- reasoningClosed = true;
212
- }
213
- if (thinkingRequested && streamState.buffer) {
214
- if (streamState.inThinking) {
215
- for (const ev of createThinkingDeltaEvents(streamState.buffer, streamState)) {
216
- const _c = convertToOpenAI(ev, conversationId, model);
217
- if (_c !== null)
218
- yield _c;
219
- }
220
- streamState.buffer = '';
221
- for (const ev of createThinkingDeltaEvents('', streamState)) {
222
- const _c = convertToOpenAI(ev, conversationId, model);
223
- if (_c !== null)
224
- yield _c;
225
- }
226
- for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
227
- const _c = convertToOpenAI(ev, conversationId, model);
228
- if (_c !== null)
229
- yield _c;
230
- }
213
+ streamState.buffer = '';
214
+ for (const ev of createThinkingDeltaEvents('', streamState)) {
215
+ const _c = convertToOpenAI(ev, conversationId, model);
216
+ if (_c !== null)
217
+ yield _c;
231
218
  }
232
- else {
233
- for (const ev of createTextDeltaEvents(streamState.buffer, streamState)) {
234
- const _c = toChunk(ev);
235
- if (_c !== null)
236
- yield _c;
237
- }
238
- streamState.buffer = '';
219
+ for (const ev of stopBlock(streamState.thinkingBlockIndex, streamState)) {
220
+ const _c = convertToOpenAI(ev, conversationId, model);
221
+ if (_c !== null)
222
+ yield _c;
239
223
  }
240
224
  }
241
- const { toolCalls: dialectToolCalls, remainderText } = dialectGate.finalize();
242
- if (remainderText) {
243
- for (const ev of createTextDeltaEvents(remainderText, streamState)) {
244
- const _c = convertToOpenAI(ev, conversationId, model);
225
+ else {
226
+ for (const ev of createTextDeltaEvents(streamState.buffer, streamState)) {
227
+ const _c = toChunk(ev);
245
228
  if (_c !== null)
246
229
  yield _c;
247
230
  }
231
+ streamState.buffer = '';
248
232
  }
249
- for (const ev of stopBlock(streamState.textBlockIndex, streamState)) {
233
+ }
234
+ const { toolCalls: dialectToolCalls, remainderText } = dialectGate.finalize();
235
+ if (remainderText) {
236
+ for (const ev of createTextDeltaEvents(remainderText, streamState)) {
250
237
  const _c = convertToOpenAI(ev, conversationId, model);
251
238
  if (_c !== null)
252
239
  yield _c;
253
240
  }
254
- if (dialectToolCalls.length > 0) {
255
- for (const btc of dialectToolCalls) {
256
- toolCalls.push({
257
- toolUseId: btc.toolUseId,
258
- name: btc.name,
259
- input: typeof btc.input === 'string' ? btc.input : JSON.stringify(btc.input)
260
- });
261
- }
241
+ }
242
+ for (const ev of stopBlock(streamState.textBlockIndex, streamState)) {
243
+ const _c = convertToOpenAI(ev, conversationId, model);
244
+ if (_c !== null)
245
+ yield _c;
246
+ }
247
+ if (dialectToolCalls.length > 0) {
248
+ for (const btc of dialectToolCalls) {
249
+ toolCalls.push({
250
+ toolUseId: btc.toolUseId,
251
+ name: btc.name,
252
+ input: typeof btc.input === 'string' ? btc.input : JSON.stringify(btc.input)
253
+ });
262
254
  }
263
- if (toolCalls.length > 0) {
264
- // OpenAI tool_calls[].index must be the tool call's own 0-based ordinal,
265
- // NOT the Anthropic content_block global index (offset by reasoning/text
266
- // blocks) a global index misaligns the AI-SDK accumulator and drops the call.
267
- for (let i = 0; i < toolCalls.length; i++) {
268
- const tc = toolCalls[i];
269
- if (!tc)
270
- continue;
271
- const blockIndex = i;
272
- {
273
- const _c = convertToOpenAI({
274
- type: 'content_block_start',
275
- index: blockIndex,
276
- content_block: {
277
- type: 'tool_use',
278
- id: tc.toolUseId,
279
- name: tc.name,
280
- input: {}
281
- }
282
- }, conversationId, model);
283
- if (_c !== null)
284
- yield _c;
285
- }
286
- let inputJson;
287
- try {
288
- const parsed = JSON.parse(tc.input);
289
- inputJson = JSON.stringify(parsed);
290
- }
291
- catch (e) {
292
- inputJson = tc.input;
293
- }
294
- {
295
- const _c = convertToOpenAI({
296
- type: 'content_block_delta',
297
- index: blockIndex,
298
- delta: {
299
- type: 'input_json_delta',
300
- partial_json: inputJson
301
- }
302
- }, conversationId, model);
303
- if (_c !== null)
304
- yield _c;
305
- }
306
- {
307
- const _c = convertToOpenAI({ type: 'content_block_stop', index: blockIndex }, conversationId, model);
308
- if (_c !== null)
309
- yield _c;
310
- }
255
+ }
256
+ if (toolCalls.length > 0) {
257
+ // OpenAI tool_calls[].index must be the tool call's own 0-based ordinal,
258
+ // NOT the Anthropic content_block global index (offset by reasoning/text
259
+ // blocks) a global index misaligns the AI-SDK accumulator and drops the call.
260
+ for (let i = 0; i < toolCalls.length; i++) {
261
+ const tc = toolCalls[i];
262
+ if (!tc)
263
+ continue;
264
+ const blockIndex = i;
265
+ {
266
+ const _c = convertToOpenAI({
267
+ type: 'content_block_start',
268
+ index: blockIndex,
269
+ content_block: {
270
+ type: 'tool_use',
271
+ id: tc.toolUseId,
272
+ name: tc.name,
273
+ input: {}
274
+ }
275
+ }, conversationId, model);
276
+ if (_c !== null)
277
+ yield _c;
278
+ }
279
+ let inputJson;
280
+ try {
281
+ const parsed = JSON.parse(tc.input);
282
+ inputJson = JSON.stringify(parsed);
283
+ }
284
+ catch {
285
+ inputJson = tc.input;
286
+ }
287
+ {
288
+ const _c = convertToOpenAI({
289
+ type: 'content_block_delta',
290
+ index: blockIndex,
291
+ delta: {
292
+ type: 'input_json_delta',
293
+ partial_json: inputJson
294
+ }
295
+ }, conversationId, model);
296
+ if (_c !== null)
297
+ yield _c;
298
+ }
299
+ {
300
+ const _c = convertToOpenAI({ type: 'content_block_stop', index: blockIndex }, conversationId, model);
301
+ if (_c !== null)
302
+ yield _c;
311
303
  }
312
304
  }
313
- outputTokens = estimateTokens(textOnlyContent);
314
- if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
315
- const contextWindow = getContextWindowSize(model);
316
- const totalTokens = Math.round((contextWindow * contextUsagePercentage) / 100);
317
- inputTokens = Math.max(0, totalTokens - outputTokens);
318
- }
319
- {
320
- const _c = convertToOpenAI({
321
- type: 'message_delta',
322
- delta: { stop_reason: toolCalls.length > 0 ? 'tool_use' : 'end_turn' },
323
- usage: {
324
- input_tokens: inputTokens,
325
- output_tokens: outputTokens,
326
- cache_creation_input_tokens: 0,
327
- cache_read_input_tokens: 0
328
- }
329
- }, conversationId, model);
330
- if (_c !== null)
331
- yield _c;
332
- }
333
- {
334
- const _c = convertToOpenAI({ type: 'message_stop' }, conversationId, model);
335
- if (_c !== null)
336
- yield _c;
337
- }
338
305
  }
339
- catch (e) {
340
- throw e;
306
+ outputTokens = estimateTokens(textOnlyContent);
307
+ if (contextUsagePercentage !== null && contextUsagePercentage > 0) {
308
+ const contextWindow = getContextWindowSize(model);
309
+ const totalTokens = Math.round((contextWindow * contextUsagePercentage) / 100);
310
+ inputTokens = Math.max(0, totalTokens - outputTokens);
311
+ }
312
+ {
313
+ const _c = convertToOpenAI({
314
+ type: 'message_delta',
315
+ delta: { stop_reason: toolCalls.length > 0 ? 'tool_use' : 'end_turn' },
316
+ usage: {
317
+ input_tokens: inputTokens,
318
+ output_tokens: outputTokens,
319
+ cache_creation_input_tokens: 0,
320
+ cache_read_input_tokens: 0
321
+ }
322
+ }, conversationId, model);
323
+ if (_c !== null)
324
+ yield _c;
325
+ }
326
+ {
327
+ const _c = convertToOpenAI({ type: 'message_stop' }, conversationId, model);
328
+ if (_c !== null)
329
+ yield _c;
341
330
  }
342
331
  }
@@ -243,7 +243,7 @@ export async function* transformKiroStream(response, model, conversationId) {
243
243
  const parsed = JSON.parse(tc.input);
244
244
  inputJson = JSON.stringify(parsed);
245
245
  }
246
- catch (e) {
246
+ catch {
247
247
  inputJson = tc.input;
248
248
  }
249
249
  {