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