@sunerpy/opencode-kiro-auth 0.5.0 → 0.5.2

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.
@@ -2,3 +2,18 @@ import type { ToolCall } from '../../plugin/types.js';
2
2
  export declare function parseBracketToolCalls(text: string): ToolCall[];
3
3
  export declare function deduplicateToolCalls(toolCalls: ToolCall[]): ToolCall[];
4
4
  export declare function cleanToolCallsFromText(text: string, toolCalls: ToolCall[]): string;
5
+ export declare const DSML_MARKER = "<\uFF5CDSML\uFF5Cfunction_calls";
6
+ /**
7
+ * Recognize Anthropic XML, deepseek DSML, and legacy bracket tool-call dialects
8
+ * in `text`. Returns the parsed tool calls plus `cleanedText` with EXACTLY the
9
+ * matched dialect spans removed (all other text preserved verbatim).
10
+ *
11
+ * Phantom-execution guards:
12
+ * - only COMPLETE closed tags match (an unclosed `<invoke name="x">` is text);
13
+ * - candidates inside fenced/inline code are skipped;
14
+ * - a dialect that yields no parseable call is stripped, never fabricated.
15
+ */
16
+ export declare function parseTextToolCalls(text: string): {
17
+ toolCalls: ToolCall[];
18
+ cleanedText: string;
19
+ };
@@ -43,3 +43,245 @@ export function cleanToolCallsFromText(text, toolCalls) {
43
43
  cleaned = cleaned.replace(/\s+/g, ' ').trim();
44
44
  return cleaned;
45
45
  }
46
+ // ---------------------------------------------------------------------------
47
+ // Text-dialect tool-call recovery (bleed-stop)
48
+ //
49
+ // Some models emit tool calls as literal TEXT dialects instead of the
50
+ // structured toolUseEvent path:
51
+ // (a) Anthropic XML: <function_calls><invoke name="X"><parameter name="k">v</parameter></invoke></function_calls>
52
+ // (and a standalone <invoke name="X">...</invoke>)
53
+ // (b) deepseek DSML: <|DSML|function_calls... (U+FF5C '|', NOT ASCII '|')
54
+ // (c) bracket: [Called X with args:{...}] (legacy, kept)
55
+ //
56
+ // These leak verbatim and stall the turn. `parseTextToolCalls` rescues them
57
+ // into structured ToolCalls and returns the text with EXACTLY the matched
58
+ // spans removed. It is deliberately conservative: only COMPLETE closed tags
59
+ // match, candidates inside fenced/inline code are skipped, and a dialect that
60
+ // cannot be parsed is stripped (never fabricated into a phantom call).
61
+ // ---------------------------------------------------------------------------
62
+ // deepseek DSML opening marker — the exact U+FF5C ('|') form observed leaking.
63
+ export const DSML_MARKER = '<\uFF5CDSML\uFF5Cfunction_calls';
64
+ function genToolUseId() {
65
+ return `tool_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
66
+ }
67
+ /**
68
+ * Compute char ranges that are inside fenced code blocks (``` ... ```) or
69
+ * inline code spans (` ... `). Candidates overlapping any of these are skipped
70
+ * so a model *explaining* or *showing* tool-call syntax is never executed.
71
+ */
72
+ function computeCodeRanges(text) {
73
+ const ranges = [];
74
+ const fence = /```[\s\S]*?```/g;
75
+ let m;
76
+ while ((m = fence.exec(text)) !== null) {
77
+ ranges.push([m.index, m.index + m[0].length]);
78
+ }
79
+ const inFence = (i) => ranges.some(([s, e]) => i >= s && i < e);
80
+ const inline = /`[^`\n]+`/g;
81
+ while ((m = inline.exec(text)) !== null) {
82
+ if (!inFence(m.index))
83
+ ranges.push([m.index, m.index + m[0].length]);
84
+ }
85
+ return ranges;
86
+ }
87
+ function overlapsCode(start, end, codeRanges) {
88
+ return codeRanges.some(([s, e]) => start < e && end > s);
89
+ }
90
+ function overlapsClaimed(start, end, claimed) {
91
+ return claimed.some(([s, e]) => start < e && end > s);
92
+ }
93
+ /** Parse `<parameter name="K">V</parameter>` pairs from an invoke body into an input object. */
94
+ function parseInvokeParameters(body) {
95
+ const input = {};
96
+ const paramRe = /<parameter\s+name="([^"]+)"\s*>([\s\S]*?)<\/parameter>/g;
97
+ let pm;
98
+ while ((pm = paramRe.exec(body)) !== null) {
99
+ const key = pm[1];
100
+ const rawVal = pm[2];
101
+ if (key === undefined)
102
+ continue;
103
+ const val = rawVal ?? '';
104
+ try {
105
+ input[key] = JSON.parse(val);
106
+ }
107
+ catch {
108
+ input[key] = val;
109
+ }
110
+ }
111
+ return input;
112
+ }
113
+ /** Build a ToolCall from a single complete `<invoke name="...">...</invoke>` block. */
114
+ function toolCallFromInvoke(name, body) {
115
+ return {
116
+ toolUseId: genToolUseId(),
117
+ name,
118
+ input: parseInvokeParameters(body)
119
+ };
120
+ }
121
+ /**
122
+ * Match complete Anthropic XML dialects:
123
+ * - `<function_calls>...(invoke)+...</function_calls>` blocks
124
+ * - standalone `<invoke name="X">...</invoke>` NOT inside a function_calls block
125
+ * Skips candidates inside code and records claimed ranges to avoid double-match.
126
+ */
127
+ function matchAnthropicXml(text, codeRanges, claimed) {
128
+ const matches = [];
129
+ // (1) complete <function_calls>...</function_calls> blocks
130
+ const blockRe = /<function_calls>[\s\S]*?<\/function_calls>/g;
131
+ let bm;
132
+ while ((bm = blockRe.exec(text)) !== null) {
133
+ const start = bm.index;
134
+ const end = start + bm[0].length;
135
+ if (overlapsCode(start, end, codeRanges))
136
+ continue;
137
+ const invokeRe = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/g;
138
+ const toolCalls = [];
139
+ let im;
140
+ while ((im = invokeRe.exec(bm[0])) !== null) {
141
+ const name = im[1];
142
+ if (!name)
143
+ continue;
144
+ toolCalls.push(toolCallFromInvoke(name, im[2] ?? ''));
145
+ }
146
+ // Only treat as a tool-call span if at least one invoke parsed; otherwise
147
+ // it is not a real dialect payload — leave the text untouched.
148
+ if (toolCalls.length === 0)
149
+ continue;
150
+ matches.push({ start, end, toolCalls });
151
+ claimed.push([start, end]);
152
+ }
153
+ // (2) standalone complete <invoke ...>...</invoke> not inside a claimed block
154
+ const invokeRe = /<invoke\s+name="([^"]+)"\s*>([\s\S]*?)<\/invoke>/g;
155
+ let sm;
156
+ while ((sm = invokeRe.exec(text)) !== null) {
157
+ const start = sm.index;
158
+ const end = start + sm[0].length;
159
+ const name = sm[1];
160
+ if (!name)
161
+ continue;
162
+ if (overlapsCode(start, end, codeRanges))
163
+ continue;
164
+ if (overlapsClaimed(start, end, claimed))
165
+ continue;
166
+ matches.push({ start, end, toolCalls: [toolCallFromInvoke(name, sm[2] ?? '')] });
167
+ claimed.push([start, end]);
168
+ }
169
+ return matches;
170
+ }
171
+ /**
172
+ * Match the deepseek DSML dialect. The strip span runs from the exact U+FF5C
173
+ * marker to its closing counterpart if one exists, else to end-of-text (the
174
+ * observed trailing leak). Best-effort recovery of name/args; if not cleanly
175
+ * recoverable the span is stripped WITHOUT fabricating a call.
176
+ */
177
+ function matchDsml(text, codeRanges, claimed) {
178
+ const matches = [];
179
+ let from = 0;
180
+ for (;;) {
181
+ const start = text.indexOf(DSML_MARKER, from);
182
+ if (start === -1)
183
+ break;
184
+ // Closing counterpart: a DSML end token (U+FF5C ... end ... U+FF5C) after
185
+ // the marker, else consume to end-of-text.
186
+ const rest = text.slice(start + DSML_MARKER.length);
187
+ const closeRe = /<\uFF5C[^>]*?end[^>]*?>|<\/\uFF5CDSML[^>]*?>/;
188
+ const cm = closeRe.exec(rest);
189
+ const end = cm !== null ? start + DSML_MARKER.length + cm.index + cm[0].length : text.length;
190
+ from = end;
191
+ if (overlapsCode(start, end, codeRanges))
192
+ continue;
193
+ if (overlapsClaimed(start, end, claimed))
194
+ continue;
195
+ // Best-effort recovery — only fires on a clean name + JSON args pair.
196
+ const span = text.slice(start, end);
197
+ const toolCalls = [];
198
+ const nameM = /name["\uFF5C=:\s]+["']?([A-Za-z0-9_]+)/.exec(span);
199
+ const jsonM = /(\{[\s\S]*\})/.exec(span);
200
+ if (nameM?.[1] && jsonM?.[1]) {
201
+ try {
202
+ toolCalls.push({
203
+ toolUseId: genToolUseId(),
204
+ name: nameM[1],
205
+ input: JSON.parse(jsonM[1])
206
+ });
207
+ }
208
+ catch {
209
+ // fall through to strip-only
210
+ }
211
+ }
212
+ matches.push({ start, end, toolCalls });
213
+ claimed.push([start, end]);
214
+ }
215
+ return matches;
216
+ }
217
+ /**
218
+ * Match legacy `[Called X with args:{...}]` spans (for cleanedText) and their
219
+ * tool calls, skipping candidates inside code.
220
+ */
221
+ function matchBracket(text, codeRanges, claimed) {
222
+ const matches = [];
223
+ const pattern = /\[Called\s+(\w+)\s+with\s+args:\s*(\{[^}]*(?:\{[^}]*\}[^}]*)*\})\]/gs;
224
+ let m;
225
+ while ((m = pattern.exec(text)) !== null) {
226
+ const start = m.index;
227
+ const end = start + m[0].length;
228
+ const name = m[1];
229
+ const argsStr = m[2];
230
+ if (!name || !argsStr)
231
+ continue;
232
+ if (overlapsCode(start, end, codeRanges))
233
+ continue;
234
+ if (overlapsClaimed(start, end, claimed))
235
+ continue;
236
+ let input;
237
+ try {
238
+ input = JSON.parse(argsStr);
239
+ }
240
+ catch {
241
+ continue;
242
+ }
243
+ matches.push({
244
+ start,
245
+ end,
246
+ toolCalls: [{ toolUseId: genToolUseId(), name, input }]
247
+ });
248
+ claimed.push([start, end]);
249
+ }
250
+ return matches;
251
+ }
252
+ /**
253
+ * Recognize Anthropic XML, deepseek DSML, and legacy bracket tool-call dialects
254
+ * in `text`. Returns the parsed tool calls plus `cleanedText` with EXACTLY the
255
+ * matched dialect spans removed (all other text preserved verbatim).
256
+ *
257
+ * Phantom-execution guards:
258
+ * - only COMPLETE closed tags match (an unclosed `<invoke name="x">` is text);
259
+ * - candidates inside fenced/inline code are skipped;
260
+ * - a dialect that yields no parseable call is stripped, never fabricated.
261
+ */
262
+ export function parseTextToolCalls(text) {
263
+ if (!text)
264
+ return { toolCalls: [], cleanedText: text };
265
+ const codeRanges = computeCodeRanges(text);
266
+ const claimed = [];
267
+ const matches = [
268
+ ...matchAnthropicXml(text, codeRanges, claimed),
269
+ ...matchDsml(text, codeRanges, claimed),
270
+ ...matchBracket(text, codeRanges, claimed)
271
+ ];
272
+ if (matches.length === 0)
273
+ return { toolCalls: [], cleanedText: text };
274
+ matches.sort((a, b) => a.start - b.start);
275
+ const toolCalls = [];
276
+ let cleanedText = '';
277
+ let cursor = 0;
278
+ for (const mt of matches) {
279
+ if (mt.start < cursor)
280
+ continue; // defensive against any residual overlap
281
+ cleanedText += text.slice(cursor, mt.start);
282
+ cursor = mt.end;
283
+ toolCalls.push(...mt.toolCalls);
284
+ }
285
+ cleanedText += text.slice(cursor);
286
+ return { toolCalls, cleanedText };
287
+ }
@@ -7,6 +7,28 @@ import { convertToolsToCodeWhisperer, deduplicateToolResults } from '../infrastr
7
7
  import { getEffectiveEffort, resolveEffort } from './effort.js';
8
8
  import { convertImagesToKiroFormat, extractAllImages, extractTextFromParts } from './image-handler.js';
9
9
  import { resolveModelVariant } from './models.js';
10
+ function jsonSchemaTypeOf(value) {
11
+ if (Array.isArray(value))
12
+ return 'array';
13
+ if (value === null)
14
+ return 'string';
15
+ const t = typeof value;
16
+ if (t === 'number' || t === 'boolean' || t === 'string' || t === 'object')
17
+ return t;
18
+ return 'string';
19
+ }
20
+ // No `required` fields are inferred: fabricated required keys risk a 400.
21
+ function inferToolSpecFromHistory(name, toolUsesInHistory) {
22
+ const sample = toolUsesInHistory.find((tu) => tu.name === name && tu.input && typeof tu.input === 'object' && !Array.isArray(tu.input));
23
+ const properties = {};
24
+ if (sample && sample.input) {
25
+ for (const [key, val] of Object.entries(sample.input)) {
26
+ properties[key] = { type: jsonSchemaTypeOf(val) };
27
+ }
28
+ }
29
+ const json = Object.keys(properties).length > 0 ? { type: 'object', properties } : { type: 'object' };
30
+ return { name, description: `Tool ${name}`, inputSchema: { json } };
31
+ }
10
32
  function buildCodeWhispererRequest(body, model, auth, think = false, budget = 20000, showToast) {
11
33
  const req = typeof body === 'string' ? JSON.parse(body) : body;
12
34
  const { messages, tools, system } = req;
@@ -228,13 +250,10 @@ function buildCodeWhispererRequest(body, model, auth, think = false, budget = 20
228
250
  const existingToolNames = new Set(existingTools.map((t) => t.toolSpecification?.name).filter(Boolean));
229
251
  const missingToolNames = Array.from(toolNamesInHistory).filter((name) => !existingToolNames.has(name));
230
252
  if (missingToolNames.length > 0) {
231
- const placeholderTools = missingToolNames.map((name) => ({
232
- toolSpecification: {
233
- name,
234
- description: 'Tool',
235
- inputSchema: { json: { type: 'object', properties: {} } }
236
- }
237
- }));
253
+ const placeholderTools = missingToolNames.map((name) => {
254
+ const inferred = inferToolSpecFromHistory(name, toolUsesInHistory);
255
+ return { toolSpecification: inferred };
256
+ });
238
257
  if (!uim.userInputMessageContext)
239
258
  uim.userInputMessageContext = {};
240
259
  uim.userInputMessageContext.tools = [...existingTools, ...placeholderTools];
@@ -0,0 +1,29 @@
1
+ import type { ToolCall } from '../types.js';
2
+ /**
3
+ * Streaming suppression gate for text-dialect tool calls.
4
+ *
5
+ * Visible assistant reply text is pushed through the gate as it streams. While
6
+ * no dialect opening marker has appeared, the gate returns the safe prefix to
7
+ * stream (holding back only a possible partial-marker tail). Once a marker
8
+ * appears, everything from the marker onward is withheld. At finalization,
9
+ * `finalize()` parses the full accumulated text into structured tool calls and
10
+ * returns the remaining non-dialect text (dialect spans removed) that still
11
+ * needs to be emitted.
12
+ */
13
+ export declare class DialectGate {
14
+ private accumulated;
15
+ private emitted;
16
+ private markerSeen;
17
+ /** Append a visible-text chunk; returns the substring safe to emit now. */
18
+ push(text: string): string;
19
+ /** True once a dialect opening marker has been observed (streaming suppressed). */
20
+ get suppressing(): boolean;
21
+ /**
22
+ * Finalize: parse the full accumulated text into structured tool calls and
23
+ * return the non-dialect text that was buffered but not yet emitted.
24
+ */
25
+ finalize(): {
26
+ toolCalls: ToolCall[];
27
+ remainderText: string;
28
+ };
29
+ }
@@ -0,0 +1,92 @@
1
+ import { DSML_MARKER, parseTextToolCalls } from '../../infrastructure/transformers/tool-call-parser.js';
2
+ // Opening markers that signal a text-dialect tool call may be starting. Once
3
+ // any of these appears in the accumulated visible text, we stop streaming
4
+ // further visible text (buffer it) so a dialect span is never emitted as
5
+ // visible `delta.content`. Authoritative parsing happens only at finalization
6
+ // on the FULL accumulated text (never per-fragment).
7
+ const OPENING_MARKERS = ['<function_calls', '<invoke name=', DSML_MARKER];
8
+ const MAX_MARKER_LEN = Math.max(...OPENING_MARKERS.map((m) => m.length));
9
+ /** Earliest index of any opening marker in `text`, or -1. */
10
+ function firstMarkerIndex(text) {
11
+ let earliest = -1;
12
+ for (const marker of OPENING_MARKERS) {
13
+ const idx = text.indexOf(marker);
14
+ if (idx !== -1 && (earliest === -1 || idx < earliest))
15
+ earliest = idx;
16
+ }
17
+ return earliest;
18
+ }
19
+ /**
20
+ * Length of the longest suffix of `text` that is a proper prefix of some
21
+ * opening marker — i.e. the tail might be the start of a marker split across
22
+ * chunks. That tail is reserved (not emitted yet) to avoid streaming half a
23
+ * marker as visible text.
24
+ */
25
+ function partialMarkerTail(text) {
26
+ const maxLook = Math.min(text.length, MAX_MARKER_LEN - 1);
27
+ for (let len = maxLook; len > 0; len--) {
28
+ const tail = text.slice(text.length - len);
29
+ for (const marker of OPENING_MARKERS) {
30
+ if (marker.length > len && marker.startsWith(tail))
31
+ return len;
32
+ }
33
+ }
34
+ return 0;
35
+ }
36
+ /**
37
+ * Streaming suppression gate for text-dialect tool calls.
38
+ *
39
+ * Visible assistant reply text is pushed through the gate as it streams. While
40
+ * no dialect opening marker has appeared, the gate returns the safe prefix to
41
+ * stream (holding back only a possible partial-marker tail). Once a marker
42
+ * appears, everything from the marker onward is withheld. At finalization,
43
+ * `finalize()` parses the full accumulated text into structured tool calls and
44
+ * returns the remaining non-dialect text (dialect spans removed) that still
45
+ * needs to be emitted.
46
+ */
47
+ export class DialectGate {
48
+ accumulated = '';
49
+ emitted = 0;
50
+ markerSeen = false;
51
+ /** Append a visible-text chunk; returns the substring safe to emit now. */
52
+ push(text) {
53
+ if (!text)
54
+ return '';
55
+ this.accumulated += text;
56
+ if (!this.markerSeen) {
57
+ const markerIdx = firstMarkerIndex(this.accumulated);
58
+ if (markerIdx !== -1)
59
+ this.markerSeen = true;
60
+ }
61
+ let safeEnd;
62
+ if (this.markerSeen) {
63
+ safeEnd = firstMarkerIndex(this.accumulated);
64
+ if (safeEnd === -1)
65
+ safeEnd = this.accumulated.length;
66
+ }
67
+ else {
68
+ // No marker yet — but the tail could be the start of one; reserve it.
69
+ safeEnd = this.accumulated.length - partialMarkerTail(this.accumulated);
70
+ }
71
+ if (safeEnd <= this.emitted)
72
+ return '';
73
+ const out = this.accumulated.slice(this.emitted, safeEnd);
74
+ this.emitted = safeEnd;
75
+ return out;
76
+ }
77
+ /** True once a dialect opening marker has been observed (streaming suppressed). */
78
+ get suppressing() {
79
+ return this.markerSeen;
80
+ }
81
+ /**
82
+ * Finalize: parse the full accumulated text into structured tool calls and
83
+ * return the non-dialect text that was buffered but not yet emitted.
84
+ */
85
+ finalize() {
86
+ const { toolCalls, cleanedText } = parseTextToolCalls(this.accumulated);
87
+ // Text before the first marker was already emitted verbatim and is never
88
+ // part of a dialect span, so cleanedText[0..emitted) == what we emitted.
89
+ const remainderText = cleanedText.length > this.emitted ? cleanedText.slice(this.emitted) : '';
90
+ return { toolCalls, remainderText };
91
+ }
92
+ }
@@ -1,6 +1,6 @@
1
- import { parseBracketToolCalls } from '../../infrastructure/transformers/tool-call-parser.js';
2
1
  import { getContextWindowSize } from '../models.js';
3
2
  import { estimateTokens } from '../response.js';
3
+ import { DialectGate } from './dialect-gate.js';
4
4
  import { convertToOpenAI } from './openai-converter.js';
5
5
  import { findRealTag } from './stream-parser.js';
6
6
  import { createTextDeltaEvents, createThinkingDeltaEvents, stopBlock } from './stream-state.js';
@@ -24,6 +24,19 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
24
24
  let contextUsagePercentage = null;
25
25
  const toolCalls = [];
26
26
  let currentToolCall = null;
27
+ // Text deltas route through the gate so a dialect span is withheld from the
28
+ // visible stream once its opening marker appears (recovered at finalization).
29
+ const dialectGate = new DialectGate();
30
+ const toChunk = (ev) => {
31
+ if (ev.type === 'content_block_delta' && ev.delta?.type === 'text_delta') {
32
+ const safe = dialectGate.push(ev.delta.text ?? '');
33
+ if (!safe)
34
+ return null;
35
+ const gated = { ...ev, delta: { ...ev.delta, text: safe } };
36
+ return convertToOpenAI(gated, conversationId, model);
37
+ }
38
+ return convertToOpenAI(ev, conversationId, model);
39
+ };
27
40
  // Probe (opus-4.8): reasoning streams via `reasoningContentEvent{text,signature}` as a
28
41
  // contiguous run BEFORE `assistantResponseEvent.content`; no `<thinking>` tags emitted.
29
42
  // signature is metadata and ignored for rendering.
@@ -65,7 +78,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
65
78
  }
66
79
  if (reasoningStarted) {
67
80
  for (const ev of createTextDeltaEvents(text, streamState)) {
68
- const _c = convertToOpenAI(ev, conversationId, model);
81
+ const _c = toChunk(ev);
69
82
  if (_c !== null)
70
83
  yield _c;
71
84
  }
@@ -74,7 +87,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
74
87
  if (!thinkingRequested) {
75
88
  for (const ev of createTextDeltaEvents(text, streamState)) {
76
89
  {
77
- const _c = convertToOpenAI(ev, conversationId, model);
90
+ const _c = toChunk(ev);
78
91
  if (_c !== null)
79
92
  yield _c;
80
93
  }
@@ -142,7 +155,7 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
142
155
  }
143
156
  }
144
157
  for (const ev of deltaEvents) {
145
- const chunk = convertToOpenAI(ev, conversationId, model);
158
+ const chunk = toChunk(ev);
146
159
  if (chunk !== null)
147
160
  yield chunk;
148
161
  }
@@ -218,21 +231,28 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
218
231
  }
219
232
  else {
220
233
  for (const ev of createTextDeltaEvents(streamState.buffer, streamState)) {
221
- const _c = convertToOpenAI(ev, conversationId, model);
234
+ const _c = toChunk(ev);
222
235
  if (_c !== null)
223
236
  yield _c;
224
237
  }
225
238
  streamState.buffer = '';
226
239
  }
227
240
  }
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);
245
+ if (_c !== null)
246
+ yield _c;
247
+ }
248
+ }
228
249
  for (const ev of stopBlock(streamState.textBlockIndex, streamState)) {
229
250
  const _c = convertToOpenAI(ev, conversationId, model);
230
251
  if (_c !== null)
231
252
  yield _c;
232
253
  }
233
- const bracketToolCalls = parseBracketToolCalls(totalContent);
234
- if (bracketToolCalls.length > 0) {
235
- for (const btc of bracketToolCalls) {
254
+ if (dialectToolCalls.length > 0) {
255
+ for (const btc of dialectToolCalls) {
236
256
  toolCalls.push({
237
257
  toolUseId: btc.toolUseId,
238
258
  name: btc.name,
@@ -241,12 +261,14 @@ export async function* transformSdkStream(sdkResponse, model, conversationId) {
241
261
  }
242
262
  }
243
263
  if (toolCalls.length > 0) {
244
- const baseIndex = streamState.nextBlockIndex;
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.
245
267
  for (let i = 0; i < toolCalls.length; i++) {
246
268
  const tc = toolCalls[i];
247
269
  if (!tc)
248
270
  continue;
249
- const blockIndex = baseIndex + i;
271
+ const blockIndex = i;
250
272
  {
251
273
  const _c = convertToOpenAI({
252
274
  type: 'content_block_start',
@@ -216,12 +216,14 @@ export async function* transformKiroStream(response, model, conversationId) {
216
216
  }
217
217
  }
218
218
  if (toolCalls.length > 0) {
219
- const baseIndex = streamState.nextBlockIndex;
219
+ // OpenAI tool_calls[].index must be the tool call's own 0-based ordinal,
220
+ // NOT the Anthropic content_block global index (offset by reasoning/text
221
+ // blocks) — a global index misaligns the AI-SDK accumulator and drops the call.
220
222
  for (let i = 0; i < toolCalls.length; i++) {
221
223
  const tc = toolCalls[i];
222
224
  if (!tc)
223
225
  continue;
224
- const blockIndex = baseIndex + i;
226
+ const blockIndex = i;
225
227
  {
226
228
  const _c = convertToOpenAI({
227
229
  type: 'content_block_start',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunerpy/opencode-kiro-auth",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",