@acosmi/sdk-ts 2.19.3 → 2.19.5

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.
@@ -14,7 +14,6 @@ var BusinessError = class extends Error {
14
14
  this.code = code;
15
15
  }
16
16
  };
17
- new OpenAIAdapter();
18
17
 
19
18
  // src/models/adapters/openai.ts
20
19
  var OpenAIAdapter = class {
@@ -182,16 +181,19 @@ function convertOpenAIToChatResponse(oai) {
182
181
  role: "assistant",
183
182
  content: [],
184
183
  stop_reason: "",
184
+ // [W-SDK-OPENAI-PARITY] 缺 usage 的响应按 0 计数, 而不是让 `oai.usage.prompt_tokens`
185
+ // 抛 TypeError 把整条响应打死 —— 上游不计量是常见形态, 正文照样是有效的。
186
+ // 与 Go 侧同名函数逐字同语义 (那里 Usage 是值类型, 缺席即零值)。
185
187
  usage: {
186
- input_tokens: oai.usage.prompt_tokens,
187
- output_tokens: oai.usage.completion_tokens
188
+ input_tokens: oai.usage?.prompt_tokens ?? 0,
189
+ output_tokens: oai.usage?.completion_tokens ?? 0
188
190
  },
189
191
  tokenRemaining: -1,
190
192
  callRemaining: -1,
191
193
  modelTokenRemaining: -1,
192
194
  modelTokenRemainingETU: -1
193
195
  };
194
- if (oai.choices.length > 0) {
196
+ if (Array.isArray(oai.choices) && oai.choices.length > 0) {
195
197
  const choice = oai.choices[0];
196
198
  switch (choice.finish_reason) {
197
199
  case "stop":
@@ -265,12 +267,14 @@ function parseOpenAIResponseToAnthropic(raw) {
265
267
  content: [],
266
268
  model: oaiResp.model,
267
269
  stop_reason: "",
270
+ // [W-SDK-OPENAI-PARITY] 同 convertOpenAIToChatResponse: 缺 usage 按 0 计数, 不抛。
271
+ // 两处都要改 —— 只改一处等于「非流式路径修好了一半」, 而两条路径是同一族上游响应。
268
272
  usage: {
269
- input_tokens: oaiResp.usage.prompt_tokens,
270
- output_tokens: oaiResp.usage.completion_tokens
273
+ input_tokens: oaiResp.usage?.prompt_tokens ?? 0,
274
+ output_tokens: oaiResp.usage?.completion_tokens ?? 0
271
275
  }
272
276
  };
273
- if (oaiResp.choices.length > 0) {
277
+ if (Array.isArray(oaiResp.choices) && oaiResp.choices.length > 0) {
274
278
  const choice = oaiResp.choices[0];
275
279
  switch (choice.finish_reason) {
276
280
  case "stop":
@@ -308,6 +312,21 @@ function parseOpenAIResponseToAnthropic(raw) {
308
312
  }
309
313
  return resp;
310
314
  }
315
+ function readStreamUsage(chunk) {
316
+ const raw = chunk.usage;
317
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
318
+ return null;
319
+ }
320
+ const { prompt_tokens: promptTokens, completion_tokens: completionTokens } = raw;
321
+ const usage = {};
322
+ if (typeof promptTokens === "number" && Number.isFinite(promptTokens)) {
323
+ usage.input_tokens = promptTokens;
324
+ }
325
+ if (typeof completionTokens === "number" && Number.isFinite(completionTokens)) {
326
+ usage.output_tokens = completionTokens;
327
+ }
328
+ return usage;
329
+ }
311
330
  var OpenAIStreamConverter = class {
312
331
  messageStarted = false;
313
332
  thinkingStarted = false;
@@ -326,8 +345,18 @@ var OpenAIStreamConverter = class {
326
345
  toolArgsAccum = /* @__PURE__ */ new Map();
327
346
  /** 上一次解析出的 tool 键,供缺 index 且缺 id 的后续增量沿用。 */
328
347
  lastToolKey = null;
329
- /** 已发出 message_delta/message_stop,避免 finish_reason 与 `[DONE]` 各收一次。 */
348
+ /** 已发出 message_delta/message_stop,避免 finish_reason、usage 尾帧、`[DONE]` 与
349
+ * {@link flush} 重复收口 —— 整条流恰好一个 message_stop。 */
330
350
  messageClosed = false;
351
+ /** 已为仍打开的块发出 content_block_stop。与 {@link messageClosed} 分开记:块在
352
+ * finish_reason 帧上就关,message_delta/message_stop 却可能推迟到之后的帧。 */
353
+ blocksClosed = false;
354
+ /** [W-QUAD-CHAIN-20260914] finish_reason 已到、但 message_delta/message_stop 因等待
355
+ * usage 尾帧而推迟时记下的 stop_reason;没有推迟中的收尾时为 null。 */
356
+ pendingStopReason = null;
357
+ /** [W-QUAD-CHAIN-20260914] 最近一次带 usage 对象的帧搬出的 usage(后到覆盖先到);
358
+ * 整条流从未出现 usage 对象时为 null,收尾 message_delta 据此不写 usage 键。 */
359
+ usage = null;
331
360
  /**
332
361
  * 解析一条 tool_call delta 归属的块键。
333
362
  *
@@ -352,15 +381,18 @@ var OpenAIStreamConverter = class {
352
381
  return 0;
353
382
  }
354
383
  /**
355
- * 关闭仍打开的 text / thinking / tool 块,并收口 message。
384
+ * 关闭仍打开的 text / thinking / tool 块(整条流只关一次)。
356
385
  *
357
386
  * 由 `finish_reason` 分支与 `[DONE]` 分支共用:上游断流或只发 `[DONE]` 而不发
358
387
  * `finish_reason` 时,此前一个 `content_block_stop` 都不会发,下游拿到的是一个
359
388
  * 永不闭合的 tool_use 块。
389
+ *
390
+ * [W-QUAD-CHAIN-20260914] 此前关块与 message_delta/message_stop 是同一个方法里同一时刻
391
+ * 的事;usage 尾帧要求收尾推迟,于是拆成本方法与 {@link emitMessageEnd} 两段,各自防重。
360
392
  */
361
- closeOpenBlocks(events, stopReason) {
362
- if (this.messageClosed) return;
363
- this.messageClosed = true;
393
+ closeContentBlocks(events) {
394
+ if (this.blocksClosed) return;
395
+ this.blocksClosed = true;
364
396
  if (this.textStarted) {
365
397
  events.push({
366
398
  event: "content_block_stop",
@@ -380,10 +412,23 @@ var OpenAIStreamConverter = class {
380
412
  data: JSON.stringify({ type: "content_block_stop", index: idx })
381
413
  });
382
414
  }
383
- events.push({
384
- event: "message_delta",
385
- data: JSON.stringify({ type: "message_delta", delta: { stop_reason: stopReason } })
386
- });
415
+ }
416
+ /**
417
+ * 发出 message_delta + message_stop,整条流只发一次;同时清掉推迟中的收尾。
418
+ *
419
+ * 见过 usage 对象就把它放进 message_delta:usage 必须出现在唯一的 message_stop 之前,
420
+ * message_stop 之后下游已无处安放用量。整条流从未出现 usage 对象时不写 usage 键 ——
421
+ * 缺席表示「上游没给」,不是 0。
422
+ */
423
+ emitMessageEnd(events, stopReason) {
424
+ if (this.messageClosed) return;
425
+ this.messageClosed = true;
426
+ this.pendingStopReason = null;
427
+ const messageDelta = { type: "message_delta", delta: { stop_reason: stopReason } };
428
+ if (this.usage !== null) {
429
+ messageDelta.usage = this.usage;
430
+ }
431
+ events.push({ event: "message_delta", data: JSON.stringify(messageDelta) });
387
432
  events.push({
388
433
  event: "message_stop",
389
434
  data: JSON.stringify({ type: "message_stop" })
@@ -396,8 +441,11 @@ var OpenAIStreamConverter = class {
396
441
  convert(data) {
397
442
  if (data === "[DONE]") {
398
443
  const events2 = [];
399
- if (this.messageStarted) {
400
- this.closeOpenBlocks(events2, "end_turn");
444
+ if (this.pendingStopReason !== null) {
445
+ this.emitMessageEnd(events2, this.pendingStopReason);
446
+ } else if (this.messageStarted) {
447
+ this.closeContentBlocks(events2);
448
+ this.emitMessageEnd(events2, "end_turn");
401
449
  }
402
450
  return { events: events2, done: true };
403
451
  }
@@ -408,10 +456,18 @@ var OpenAIStreamConverter = class {
408
456
  throw new Error(`parse openai stream chunk: ${e instanceof Error ? e.message : String(e)}`);
409
457
  }
410
458
  const events = [];
459
+ const frameUsage = readStreamUsage(chunk);
460
+ if (frameUsage !== null) {
461
+ this.usage = frameUsage;
462
+ }
411
463
  if (!Array.isArray(chunk.choices) || chunk.choices.length === 0) {
464
+ if (frameUsage !== null && this.pendingStopReason !== null) {
465
+ this.emitMessageEnd(events, this.pendingStopReason);
466
+ }
412
467
  return { events, done: false };
413
468
  }
414
469
  const choice = chunk.choices[0];
470
+ const delta = choice.delta ?? {};
415
471
  if (!this.messageStarted) {
416
472
  this.messageStarted = true;
417
473
  const msgJSON = JSON.stringify({
@@ -426,8 +482,17 @@ var OpenAIStreamConverter = class {
426
482
  });
427
483
  events.push({ event: "message_start", data: msgJSON });
428
484
  }
429
- if (choice.delta.reasoning_content && choice.delta.reasoning_content !== "") {
485
+ if (delta.reasoning_content && delta.reasoning_content !== "") {
430
486
  if (!this.thinkingStarted) {
487
+ if (this.textStarted) {
488
+ const stopJSON = JSON.stringify({
489
+ type: "content_block_stop",
490
+ index: this.blockIndex
491
+ });
492
+ events.push({ event: "content_block_stop", data: stopJSON });
493
+ this.blockIndex++;
494
+ this.textStarted = false;
495
+ }
431
496
  this.thinkingStarted = true;
432
497
  this.thinkingBlockIndex = this.blockIndex;
433
498
  const blockJSON = JSON.stringify({
@@ -440,11 +505,11 @@ var OpenAIStreamConverter = class {
440
505
  const deltaJSON = JSON.stringify({
441
506
  type: "content_block_delta",
442
507
  index: this.thinkingBlockIndex,
443
- delta: { type: "thinking_delta", thinking: choice.delta.reasoning_content }
508
+ delta: { type: "thinking_delta", thinking: delta.reasoning_content }
444
509
  });
445
510
  events.push({ event: "content_block_delta", data: deltaJSON });
446
511
  }
447
- if (choice.delta.content && choice.delta.content !== "") {
512
+ if (delta.content && delta.content !== "") {
448
513
  if (this.thinkingStarted && !this.thinkingStopped) {
449
514
  this.thinkingStopped = true;
450
515
  const stopJSON = JSON.stringify({
@@ -466,11 +531,11 @@ var OpenAIStreamConverter = class {
466
531
  const deltaJSON = JSON.stringify({
467
532
  type: "content_block_delta",
468
533
  index: this.blockIndex,
469
- delta: { type: "text_delta", text: choice.delta.content }
534
+ delta: { type: "text_delta", text: delta.content }
470
535
  });
471
536
  events.push({ event: "content_block_delta", data: deltaJSON });
472
537
  }
473
- for (const tc of choice.delta.tool_calls ?? []) {
538
+ for (const tc of delta.tool_calls ?? []) {
474
539
  const fn = tc.function;
475
540
  const toolKey = this.resolveToolKey(tc);
476
541
  if (!this.toolBlockIndex.has(toolKey)) {
@@ -545,10 +610,35 @@ var OpenAIStreamConverter = class {
545
610
  default:
546
611
  stopReason = choice.finish_reason;
547
612
  }
548
- this.closeOpenBlocks(events, stopReason);
613
+ if (!this.messageClosed && this.pendingStopReason === null) {
614
+ this.closeContentBlocks(events);
615
+ if (this.usage !== null) {
616
+ this.emitMessageEnd(events, stopReason);
617
+ } else {
618
+ this.pendingStopReason = stopReason;
619
+ }
620
+ }
621
+ }
622
+ if (frameUsage !== null && this.pendingStopReason !== null) {
623
+ this.emitMessageEnd(events, this.pendingStopReason);
549
624
  }
550
625
  return { events, done: false };
551
626
  }
627
+ /**
628
+ * 流在**没有** `[DONE]` 的情况下结束 (EOF) 时, 由驱动方在读循环结束后调用一次。
629
+ *
630
+ * 只补发「finish_reason 已到、仅因等待 usage 尾帧而推迟」的 message_delta + message_stop。
631
+ * 从未收到 finish_reason 的流是被截断的流, 这里刻意**不**替它伪造正常结束 —— 一个
632
+ * `end_turn` 的 message_stop 会把被截断的回答当成完整回答交给下游。
633
+ * 已经收口 (usage 帧 / `[DONE]` / 上一次 flush) 后再调用返回空数组, 不会发出第二个 message_stop。
634
+ */
635
+ flush() {
636
+ const events = [];
637
+ if (this.pendingStopReason !== null) {
638
+ this.emitMessageEnd(events, this.pendingStopReason);
639
+ }
640
+ return events;
641
+ }
552
642
  };
553
643
  function newOpenAIStreamConverter() {
554
644
  return new OpenAIStreamConverter();