@acosmi/sdk-ts 2.19.0 → 2.19.1

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.
@@ -306,16 +306,90 @@ var OpenAIStreamConverter = class {
306
306
  * 可能已被 text/tool 推进的 this.blockIndex (否则 content_block_stop 索引错配)。 */
307
307
  thinkingBlockIndex = 0;
308
308
  textStarted = false;
309
- /** OpenAI tool_call index → Anthropic block index */
309
+ /** OpenAI tool_call → Anthropic block index。键正常是 `tc.index`;上游省略
310
+ * index 时退化为 `id:<tool_call_id>`,两者都没有时沿用上一个键(见
311
+ * {@link resolveToolKey})。 */
310
312
  toolBlockIndex = /* @__PURE__ */ new Map();
311
313
  blockIndex = 0;
314
+ /** 每个 tool block 已发出的 `partial_json` 累积,用于识别「每片重发全量参数」
315
+ * 的上游(见 tool_calls 分支的累计判别)。 */
316
+ toolArgsAccum = /* @__PURE__ */ new Map();
317
+ /** 上一次解析出的 tool 键,供缺 index 且缺 id 的后续增量沿用。 */
318
+ lastToolKey = null;
319
+ /** 已发出 message_delta/message_stop,避免 finish_reason 与 `[DONE]` 各收一次。 */
320
+ messageClosed = false;
321
+ /**
322
+ * 解析一条 tool_call delta 归属的块键。
323
+ *
324
+ * OpenAI 流式规范里 `index` 是必填,但兼容实现常有省略。此前这里直接用
325
+ * `tc.index` 做 Map 键:两个都省略 index 的 tool_call 会共用键 `undefined`,
326
+ * 于是只开一个块、两段参数拼进同一条 `partial_json` 流,产出 `{…}{…}` 这种
327
+ * 必然非法的 JSON。这里按「index → id → 沿用上一个」三级降级,让至少一种
328
+ * 稳定标识生效。
329
+ */
330
+ resolveToolKey(tc) {
331
+ if (typeof tc.index === "number" && Number.isFinite(tc.index)) {
332
+ this.lastToolKey = tc.index;
333
+ return tc.index;
334
+ }
335
+ if (typeof tc.id === "string" && tc.id !== "") {
336
+ const key = `id:${tc.id}`;
337
+ this.lastToolKey = key;
338
+ return key;
339
+ }
340
+ if (this.lastToolKey !== null) return this.lastToolKey;
341
+ this.lastToolKey = 0;
342
+ return 0;
343
+ }
344
+ /**
345
+ * 关闭仍打开的 text / thinking / tool 块,并收口 message。
346
+ *
347
+ * 由 `finish_reason` 分支与 `[DONE]` 分支共用:上游断流或只发 `[DONE]` 而不发
348
+ * `finish_reason` 时,此前一个 `content_block_stop` 都不会发,下游拿到的是一个
349
+ * 永不闭合的 tool_use 块。
350
+ */
351
+ closeOpenBlocks(events, stopReason) {
352
+ if (this.messageClosed) return;
353
+ this.messageClosed = true;
354
+ if (this.textStarted) {
355
+ events.push({
356
+ event: "content_block_stop",
357
+ data: JSON.stringify({ type: "content_block_stop", index: this.blockIndex })
358
+ });
359
+ this.textStarted = false;
360
+ } else if (this.thinkingStarted && !this.thinkingStopped) {
361
+ this.thinkingStopped = true;
362
+ events.push({
363
+ event: "content_block_stop",
364
+ data: JSON.stringify({ type: "content_block_stop", index: this.thinkingBlockIndex })
365
+ });
366
+ }
367
+ for (const idx of this.toolBlockIndex.values()) {
368
+ events.push({
369
+ event: "content_block_stop",
370
+ data: JSON.stringify({ type: "content_block_stop", index: idx })
371
+ });
372
+ }
373
+ events.push({
374
+ event: "message_delta",
375
+ data: JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason } })
376
+ });
377
+ events.push({
378
+ event: "message_stop",
379
+ data: JSON.stringify({ type: "message_stop" })
380
+ });
381
+ }
312
382
  /**
313
383
  * 将一行 OpenAI SSE data 转换为零或多个 Anthropic 格式 StreamEvent
314
384
  * 返回 { events, done }
315
385
  */
316
386
  convert(data) {
317
387
  if (data === "[DONE]") {
318
- return { events: [], done: true };
388
+ const events2 = [];
389
+ if (this.messageStarted) {
390
+ this.closeOpenBlocks(events2, "end_turn");
391
+ }
392
+ return { events: events2, done: true };
319
393
  }
320
394
  let chunk;
321
395
  try {
@@ -387,7 +461,9 @@ var OpenAIStreamConverter = class {
387
461
  events.push({ event: "content_block_delta", data: deltaJSON });
388
462
  }
389
463
  for (const tc of choice.delta.tool_calls ?? []) {
390
- if (!this.toolBlockIndex.has(tc.index)) {
464
+ const fn = tc.function;
465
+ const toolKey = this.resolveToolKey(tc);
466
+ if (!this.toolBlockIndex.has(toolKey)) {
391
467
  if (this.thinkingStarted && !this.thinkingStopped) {
392
468
  this.thinkingStopped = true;
393
469
  const stopJSON = JSON.stringify({
@@ -406,56 +482,46 @@ var OpenAIStreamConverter = class {
406
482
  this.blockIndex++;
407
483
  this.textStarted = false;
408
484
  }
409
- this.toolBlockIndex.set(tc.index, this.blockIndex);
485
+ this.toolBlockIndex.set(toolKey, this.blockIndex);
410
486
  const blockJSON = JSON.stringify({
411
487
  type: "content_block_start",
412
488
  index: this.blockIndex,
413
489
  content_block: {
414
490
  type: "tool_use",
415
491
  id: tc.id,
416
- name: tc.function.name,
492
+ name: fn?.name,
417
493
  input: {}
418
494
  }
419
495
  });
420
496
  events.push({ event: "content_block_start", data: blockJSON });
421
497
  this.blockIndex++;
422
498
  }
423
- if (tc.function.arguments && tc.function.arguments !== "") {
424
- const idx = this.toolBlockIndex.get(tc.index);
425
- const deltaJSON = JSON.stringify({
426
- type: "content_block_delta",
427
- index: idx,
428
- delta: {
429
- type: "input_json_delta",
430
- partial_json: tc.function.arguments
431
- }
432
- });
433
- events.push({ event: "content_block_delta", data: deltaJSON });
499
+ if (fn?.arguments !== void 0 && fn.arguments !== null && fn.arguments !== "") {
500
+ const rawArgs = typeof fn.arguments === "string" ? fn.arguments : JSON.stringify(fn.arguments);
501
+ const accum = this.toolArgsAccum.get(toolKey) ?? "";
502
+ let emit = rawArgs;
503
+ if (accum !== "" && rawArgs.length > accum.length && rawArgs.startsWith(accum)) {
504
+ emit = rawArgs.slice(accum.length);
505
+ this.toolArgsAccum.set(toolKey, rawArgs);
506
+ } else {
507
+ this.toolArgsAccum.set(toolKey, accum + rawArgs);
508
+ }
509
+ if (emit !== "") {
510
+ const idx = this.toolBlockIndex.get(toolKey);
511
+ const deltaJSON = JSON.stringify({
512
+ type: "content_block_delta",
513
+ index: idx,
514
+ delta: {
515
+ type: "input_json_delta",
516
+ partial_json: emit
517
+ }
518
+ });
519
+ events.push({ event: "content_block_delta", data: deltaJSON });
520
+ }
434
521
  }
435
522
  }
436
523
  if (choice.finish_reason != null && choice.finish_reason !== "") {
437
- if (this.textStarted) {
438
- const stopJSON2 = JSON.stringify({
439
- type: "content_block_stop",
440
- index: this.blockIndex
441
- });
442
- events.push({ event: "content_block_stop", data: stopJSON2 });
443
- } else if (this.thinkingStarted && !this.thinkingStopped) {
444
- this.thinkingStopped = true;
445
- const stopJSON2 = JSON.stringify({
446
- type: "content_block_stop",
447
- index: this.thinkingBlockIndex
448
- });
449
- events.push({ event: "content_block_stop", data: stopJSON2 });
450
- }
451
- for (const idx of this.toolBlockIndex.values()) {
452
- const stopJSON2 = JSON.stringify({
453
- type: "content_block_stop",
454
- index: idx
455
- });
456
- events.push({ event: "content_block_stop", data: stopJSON2 });
457
- }
458
- let stopReason = "end_turn";
524
+ let stopReason;
459
525
  switch (choice.finish_reason) {
460
526
  case "tool_calls":
461
527
  stopReason = "tool_use";
@@ -463,14 +529,13 @@ var OpenAIStreamConverter = class {
463
529
  case "length":
464
530
  stopReason = "max_tokens";
465
531
  break;
532
+ case "stop":
533
+ stopReason = "end_turn";
534
+ break;
535
+ default:
536
+ stopReason = choice.finish_reason;
466
537
  }
467
- const deltaJSON = JSON.stringify({
468
- type: "message_delta",
469
- delta: { stop_reason: stopReason }
470
- });
471
- events.push({ event: "message_delta", data: deltaJSON });
472
- const stopJSON = JSON.stringify({ type: "message_stop" });
473
- events.push({ event: "message_stop", data: stopJSON });
538
+ this.closeOpenBlocks(events, stopReason);
474
539
  }
475
540
  return { events, done: false };
476
541
  }