@foldrun/core 2.0.0 → 2.1.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.
package/src/translator.ts CHANGED
@@ -22,7 +22,9 @@
22
22
  // and their results, streaming — and drops what a Chat-Completions endpoint
23
23
  // has no word for (cache_control, top_k, server-side tools, thinking on
24
24
  // endpoints that lack reasoning_effort). Anything dropped is said once in
25
- // the run trace. The translation is pure functions over JSON so the tests
25
+ // the run trace. Those endpoints cache prompt prefixes on their own side;
26
+ // the cached share they report comes back as cache_read_input_tokens, so a
27
+ // step's record says how much of its context was re-read. The translation is pure functions over JSON so the tests
26
28
  // can drive every mapping without a network; the server around them is a
27
29
  // few dozen lines.
28
30
 
@@ -37,6 +39,34 @@ import { captureRefusalHeaders, isRefusalStatus, refusalLine } from "./refusal.t
37
39
 
38
40
  type Json = Record<string, unknown>;
39
41
 
42
+ /**
43
+ * An OpenAI-shaped usage object as Anthropic's: fresh input, cached input,
44
+ * output. Both OpenAI shapes count the cached part INSIDE the prompt total
45
+ * (Chat's prompt_tokens_details.cached_tokens, Responses'
46
+ * input_tokens_details.cached_tokens; DeepSeek says prompt_cache_hit_tokens),
47
+ * so it is taken out of input here or the step would count it twice.
48
+ */
49
+ export function anthropicUsage(u: Json | undefined, wire: TranslatorWire): { input: number; cached: number; output: number } {
50
+ if (!u) return { input: 0, cached: 0, output: 0 };
51
+ const total = Number((wire === "chat" ? u.prompt_tokens : u.input_tokens) ?? 0);
52
+ const details = (wire === "chat" ? u.prompt_tokens_details : u.input_tokens_details) as Json | undefined;
53
+ const raw = Number(details?.cached_tokens ?? u.prompt_cache_hit_tokens ?? 0);
54
+ const cached = Number.isFinite(raw) ? Math.max(0, Math.min(raw, total)) : 0;
55
+ return {
56
+ input: total - cached,
57
+ cached,
58
+ output: Number((wire === "chat" ? u.completion_tokens : u.output_tokens) ?? 0),
59
+ };
60
+ }
61
+
62
+ function usageBlock(u: { input: number; cached: number; output: number } | null): Json {
63
+ return {
64
+ input_tokens: u?.input ?? 0,
65
+ output_tokens: u?.output ?? 0,
66
+ ...(u && u.cached > 0 ? { cache_read_input_tokens: u.cached } : {}),
67
+ };
68
+ }
69
+
40
70
  export type TranslatorWire = "chat" | "responses";
41
71
 
42
72
  export interface TranslatorSpec {
@@ -493,10 +523,7 @@ export function fromChatCompletion(res: Json, requestedModel: string, stopSequen
493
523
  content,
494
524
  stop_reason: stop.reason,
495
525
  stop_sequence: stop.sequence,
496
- usage: {
497
- input_tokens: Number(usage.prompt_tokens ?? 0),
498
- output_tokens: Number(usage.completion_tokens ?? 0),
499
- },
526
+ usage: usageBlock(anthropicUsage(usage, "chat")),
500
527
  };
501
528
  }
502
529
 
@@ -515,7 +542,7 @@ export class StreamTranslator {
515
542
  private tools = new Map<number, { block: number; argsSeen: boolean }>();
516
543
  /** The last choice's stop fields: finish_reason and any matched stop. */
517
544
  private finish: Json = {};
518
- private usage: { input: number; output: number } | null = null;
545
+ private usage: { input: number; cached: number; output: number } | null = null;
519
546
  private id = `msg_${crypto.randomBytes(8).toString("hex")}`;
520
547
  private readonly model: string;
521
548
  private readonly stops: string[];
@@ -561,7 +588,7 @@ export class StreamTranslator {
561
588
  const out = this.start(chunk);
562
589
  const u = chunk.usage as Json | undefined;
563
590
  if (u && (u.prompt_tokens !== undefined || u.completion_tokens !== undefined)) {
564
- this.usage = { input: Number(u.prompt_tokens ?? 0), output: Number(u.completion_tokens ?? 0) };
591
+ this.usage = anthropicUsage(u, "chat");
565
592
  }
566
593
  const choice = ((chunk.choices as Json[] | undefined) ?? [])[0];
567
594
  if (!choice) return out;
@@ -650,7 +677,7 @@ export class StreamTranslator {
650
677
  this.event("message_delta", {
651
678
  type: "message_delta",
652
679
  delta: { stop_reason: stop.reason, stop_sequence: stop.sequence },
653
- usage: { input_tokens: this.usage?.input ?? 0, output_tokens: this.usage?.output ?? 0 },
680
+ usage: usageBlock(this.usage),
654
681
  }),
655
682
  );
656
683
  out.push(this.event("message_stop", { type: "message_stop" }));
@@ -691,10 +718,7 @@ export function fromResponses(res: Json, requestedModel: string): Json {
691
718
  content,
692
719
  stop_reason: stop,
693
720
  stop_sequence: null,
694
- usage: {
695
- input_tokens: Number(usage.input_tokens ?? 0),
696
- output_tokens: Number(usage.output_tokens ?? 0),
697
- },
721
+ usage: usageBlock(anthropicUsage(usage, "responses")),
698
722
  };
699
723
  }
700
724
 
@@ -710,7 +734,7 @@ export class ResponsesStreamTranslator {
710
734
  private nextIndex = 0;
711
735
  /** Responses output_index → Anthropic block index and kind. */
712
736
  private blocks = new Map<number, { block: number; kind: "text" | "tool"; argsSeen: boolean }>();
713
- private usage: { input: number; output: number } | null = null;
737
+ private usage: { input: number; cached: number; output: number } | null = null;
714
738
  private incomplete: string | null = null;
715
739
  private sawTool = false;
716
740
  private sawRefusal = false;
@@ -812,7 +836,7 @@ export class ResponsesStreamTranslator {
812
836
  if (type === "response.completed" || type === "response.incomplete" || type === "response.failed") {
813
837
  const r = (ev.response as Json | undefined) ?? {};
814
838
  const u = r.usage as Json | undefined;
815
- if (u) this.usage = { input: Number(u.input_tokens ?? 0), output: Number(u.output_tokens ?? 0) };
839
+ if (u) this.usage = anthropicUsage(u, "responses");
816
840
  const reason = (r.incomplete_details as Json | undefined)?.reason;
817
841
  if (typeof reason === "string") this.incomplete = reason;
818
842
  return out;
@@ -834,7 +858,7 @@ export class ResponsesStreamTranslator {
834
858
  this.event("message_delta", {
835
859
  type: "message_delta",
836
860
  delta: { stop_reason: reason, stop_sequence: null },
837
- usage: { input_tokens: this.usage?.input ?? 0, output_tokens: this.usage?.output ?? 0 },
861
+ usage: usageBlock(this.usage),
838
862
  }),
839
863
  );
840
864
  out.push(this.event("message_stop", { type: "message_stop" }));