@qilin-agent/token-meter 3.1.3 → 3.1.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.
package/README.i18n.yaml CHANGED
@@ -5,60 +5,60 @@
5
5
  /:
6
6
  en: 6eda56ef696001df
7
7
  zh: 13f961071e8609a7
8
- /qilin-token-meter:
9
- en: 1db93ead2015348e
10
- zh: d458c4979077cc4a
11
- /qilin-token-meter/summary:
8
+ /qilin-agent-token-meter:
9
+ en: 8ae25b1d039ca515
10
+ zh: b278cba8dfc29d0e
11
+ /qilin-agent-token-meter/summary:
12
12
  en: 106e3eda9a85bdd1
13
13
  zh: 051bc764bf7a1912
14
- /qilin-token-meter/table-of-contents:
14
+ /qilin-agent-token-meter/table-of-contents:
15
15
  en: d152484eb41ac6b4
16
16
  zh: 09388d293f9be9cb
17
- /qilin-token-meter/use-this-package:
17
+ /qilin-agent-token-meter/use-this-package:
18
18
  en: 5907ef1472773a47
19
19
  zh: 06f1e988290a32c6
20
- /qilin-token-meter/use-this-package/when-to-choose-it:
20
+ /qilin-agent-token-meter/use-this-package/when-to-choose-it:
21
21
  en: e8c4cd1e945f86ba
22
22
  zh: 133c1f1a85179d30
23
- /qilin-token-meter/use-this-package/measuring-pressure:
23
+ /qilin-agent-token-meter/use-this-package/measuring-pressure:
24
24
  en: 58de3b364ab5af6b
25
25
  zh: c5440af43d321e56
26
- /qilin-token-meter/use-this-package/session-projections:
26
+ /qilin-agent-token-meter/use-this-package/session-projections:
27
27
  en: 0a4c3ae2763be227
28
28
  zh: 0e2c5a77f9b4b94f
29
- /qilin-token-meter/use-this-package/composition:
29
+ /qilin-agent-token-meter/use-this-package/composition:
30
30
  en: 27b823c34f4fd96c
31
31
  zh: 904373f93056da3e
32
- /qilin-token-meter/use-this-package/reading-the-numbers:
32
+ /qilin-agent-token-meter/use-this-package/reading-the-numbers:
33
33
  en: 7950203e0c146123
34
34
  zh: 4fe421b2268f12a9
35
- /qilin-token-meter/understand-the-implementation:
35
+ /qilin-agent-token-meter/understand-the-implementation:
36
36
  en: 9630d7e26193a512
37
37
  zh: 94222c2a487c4b8c
38
- /qilin-token-meter/understand-the-implementation/design-philosophy:
38
+ /qilin-agent-token-meter/understand-the-implementation/design-philosophy:
39
39
  en: 921a2c40433abc06
40
40
  zh: 5172de58e8724be8
41
- /qilin-token-meter/understand-the-implementation/source-map:
41
+ /qilin-agent-token-meter/understand-the-implementation/source-map:
42
42
  en: 0bea6321416c49d4
43
43
  zh: 5446b39910df9829
44
- /qilin-token-meter/understand-the-implementation/fold-flow:
44
+ /qilin-agent-token-meter/understand-the-implementation/fold-flow:
45
45
  en: c97959d84cf8a1a4
46
46
  zh: f17570caa42a1a8e
47
- /qilin-token-meter/understand-the-implementation/projection-semantics:
47
+ /qilin-agent-token-meter/understand-the-implementation/projection-semantics:
48
48
  en: 0269213216881c60
49
49
  zh: 5c1c4c46c0e6d109
50
- /qilin-token-meter/further-exploration:
50
+ /qilin-agent-token-meter/further-exploration:
51
51
  en: 0e57783e04ab4bae
52
52
  zh: df3e56f0117e125e
53
- /qilin-token-meter/model-experience:
53
+ /qilin-agent-token-meter/model-experience:
54
54
  en: 17231a6190c084ce
55
55
  zh: 7485c8e03f5a6726
56
- /qilin-token-meter/model-experience/kv-cache-effect:
56
+ /qilin-agent-token-meter/model-experience/kv-cache-effect:
57
57
  en: 40294c1c31b74824
58
58
  zh: c2284876b56d2dbc
59
- /qilin-token-meter/known-limitations-and-deferred-work:
59
+ /qilin-agent-token-meter/known-limitations-and-deferred-work:
60
60
  en: a8c57e6f8c5009a2
61
61
  zh: 5975018d14bfbdbb
62
- /qilin-token-meter/known-limitations-and-deferred-work/dev-note:
62
+ /qilin-agent-token-meter/known-limitations-and-deferred-work/dev-note:
63
63
  en: 86d686346e2ee1d8
64
64
  zh: d660b9884e4764a4
package/lib/index.js CHANGED
@@ -4,6 +4,7 @@ import { assembleAssistantStream, lastAssistantStreamChunk } from "@qilin-agent/
4
4
  import { deepFreeze } from "@qilin-agent/util-values";
5
5
  import { SessionLogOffset, SessionSeq, canonicalHeader, deriveEventMessage, headerEquals, isSurfaceEvent } from "@qilin-agent/session";
6
6
  import { z as z$1 } from "zod";
7
+ import { lastAssistantStreamChunk as lastAssistantStreamChunk$1 } from "@qilin-agent/llm/assistant-stream";
7
8
  //#region lib/types/estimate.js
8
9
  /**
9
10
  * Fixed-density heuristic token pricing shared by the meter service and the
@@ -583,6 +584,220 @@ function priceSurface(nodes, pricing, fileText) {
583
584
  };
584
585
  }
585
586
  //#endregion
587
+ //#region lib/types/turn-usage.js
588
+ function isCount(value) {
589
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
590
+ }
591
+ function safeSum(values) {
592
+ let total = 0;
593
+ for (const value of values) {
594
+ total += value;
595
+ if (!Number.isSafeInteger(total)) return void 0;
596
+ }
597
+ return total;
598
+ }
599
+ function messageRoute(message) {
600
+ const { provider, model } = message.source;
601
+ return provider.length > 0 && model.length > 0 ? {
602
+ provider,
603
+ model
604
+ } : void 0;
605
+ }
606
+ function streamUsage(stream) {
607
+ return lastAssistantStreamChunk$1(stream, "usage")?.usage;
608
+ }
609
+ function normalizeUsage(usage, route) {
610
+ const { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, totalTokens } = usage;
611
+ if (!isCount(inputTokens) || !isCount(outputTokens)) return void 0;
612
+ if (cacheReadTokens !== void 0 && !isCount(cacheReadTokens)) return void 0;
613
+ if (cacheWriteTokens !== void 0 && !isCount(cacheWriteTokens)) return void 0;
614
+ if (reasoningTokens !== void 0 && (!isCount(reasoningTokens) || reasoningTokens > outputTokens)) return;
615
+ const knownPrompt = safeSum([
616
+ inputTokens,
617
+ ...cacheReadTokens === void 0 ? [] : [cacheReadTokens],
618
+ ...cacheWriteTokens === void 0 ? [] : [cacheWriteTokens]
619
+ ]);
620
+ if (knownPrompt === void 0) return void 0;
621
+ let exactTotal;
622
+ if (totalTokens !== void 0) {
623
+ if (!isCount(totalTokens)) return void 0;
624
+ const exactPrompt = totalTokens - outputTokens;
625
+ if (!isCount(exactPrompt) || exactPrompt < knownPrompt) return void 0;
626
+ if (cacheReadTokens !== void 0 && cacheWriteTokens !== void 0 && exactPrompt !== knownPrompt) return;
627
+ exactTotal = totalTokens;
628
+ } else {
629
+ if (cacheReadTokens === void 0 || cacheWriteTokens === void 0) return void 0;
630
+ const derivedTotal = safeSum([knownPrompt, outputTokens]);
631
+ if (derivedTotal === void 0) return void 0;
632
+ exactTotal = derivedTotal;
633
+ }
634
+ return {
635
+ inputTokens,
636
+ outputTokens,
637
+ totalTokens: exactTotal,
638
+ ...cacheReadTokens === void 0 ? {} : { cacheReadTokens },
639
+ ...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
640
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens },
641
+ ...route === void 0 ? {} : { route }
642
+ };
643
+ }
644
+ function aggregateAttempts(attempts) {
645
+ if (attempts.length === 0) return void 0;
646
+ const inputTokens = safeSum(attempts.map((attempt) => attempt.inputTokens));
647
+ const outputTokens = safeSum(attempts.map((attempt) => attempt.outputTokens));
648
+ const totalTokens = safeSum(attempts.map((attempt) => attempt.totalTokens));
649
+ if (inputTokens === void 0 || outputTokens === void 0 || totalTokens === void 0) return void 0;
650
+ const cacheRead = attempts.map((attempt) => attempt.cacheReadTokens);
651
+ const cacheWrite = attempts.map((attempt) => attempt.cacheWriteTokens);
652
+ const reasoning = attempts.map((attempt) => attempt.reasoningTokens);
653
+ const cacheReadTokens = cacheRead.every(isCount) ? safeSum(cacheRead) : void 0;
654
+ const cacheWriteTokens = cacheWrite.every(isCount) ? safeSum(cacheWrite) : void 0;
655
+ const reasoningTokens = reasoning.every(isCount) ? safeSum(reasoning) : void 0;
656
+ let routes;
657
+ const attributed = attempts.map((attempt) => attempt.route);
658
+ if (attributed.every((route) => route !== void 0)) {
659
+ const unique = /* @__PURE__ */ new Map();
660
+ for (const route of attributed) unique.set(`${route.provider}\0${route.model}`, route);
661
+ routes = [...unique.values()];
662
+ }
663
+ return {
664
+ uncachedInputTokens: inputTokens,
665
+ outputTokens,
666
+ totalTokens,
667
+ ...cacheReadTokens === void 0 ? {} : { cacheReadTokens },
668
+ ...cacheWriteTokens === void 0 ? {} : { cacheWriteTokens },
669
+ ...reasoningTokens === void 0 ? {} : { reasoningTokens },
670
+ ...routes === void 0 ? {} : { routes }
671
+ };
672
+ }
673
+ function sameAttempt(state, turn, step) {
674
+ return state.turn === turn && state.step === step;
675
+ }
676
+ /**
677
+ * Fold one complete Turn's durable attempt lifecycle into exact token accounting.
678
+ *
679
+ * No attempt is inferred from a usage sample. Any missing lifecycle boundary,
680
+ * incomplete attempt usage, unsafe count, or contradictory exact total makes
681
+ * the whole disclosure unavailable.
682
+ * @param events - Turn-local durable events from `turn/start` through `turn/end`.
683
+ * @returns exact aggregate usage, or undefined when it cannot be proven.
684
+ */
685
+ function deriveTurnTokenUsage(events) {
686
+ let state = { kind: "idle" };
687
+ const attempts = [];
688
+ let turn;
689
+ let sawEnd = false;
690
+ let invalid = false;
691
+ const closeOpen = (route) => {
692
+ if (state.kind !== "open" || state.sample === void 0) return false;
693
+ const normalized = normalizeUsage(state.sample, route);
694
+ if (normalized === void 0) return false;
695
+ attempts.push(normalized);
696
+ return true;
697
+ };
698
+ for (const event of events) {
699
+ if (invalid) break;
700
+ if (event.type === "turn/start") {
701
+ if (turn !== void 0 || state.kind !== "idle") invalid = true;
702
+ else turn = event.data.turn;
703
+ continue;
704
+ }
705
+ if (turn === void 0) {
706
+ invalid = true;
707
+ break;
708
+ }
709
+ if (event.type === "turn/end") {
710
+ if (event.data.turn !== turn || state.kind !== "idle" || sawEnd) invalid = true;
711
+ else sawEnd = true;
712
+ continue;
713
+ }
714
+ if (sawEnd) {
715
+ invalid = true;
716
+ break;
717
+ }
718
+ if (event.type === "step/start") {
719
+ if (event.data.turn !== turn || state.kind !== "idle") invalid = true;
720
+ else state = {
721
+ kind: "open",
722
+ turn,
723
+ step: event.data.step
724
+ };
725
+ continue;
726
+ }
727
+ if (event.type === "llm/retry-started") {
728
+ if (event.data.turn !== turn || state.kind !== "settled" || state.by !== "retry" || !sameAttempt(state, event.data.turn, event.data.step)) invalid = true;
729
+ else state = {
730
+ kind: "open",
731
+ turn,
732
+ step: event.data.step
733
+ };
734
+ continue;
735
+ }
736
+ if (event.type === "assistant/attempt") {
737
+ if (event.data.turn !== turn || state.kind !== "open" || !sameAttempt(state, event.data.turn, event.data.step)) {
738
+ invalid = true;
739
+ continue;
740
+ }
741
+ const sample = streamUsage(event.data.stream) ?? state.sample;
742
+ state = {
743
+ kind: "open",
744
+ turn,
745
+ step: event.data.step,
746
+ ...sample === void 0 ? {} : { sample }
747
+ };
748
+ if (!closeOpen()) invalid = true;
749
+ else state = {
750
+ kind: "finishClosed",
751
+ turn,
752
+ step: event.data.step
753
+ };
754
+ continue;
755
+ }
756
+ if (event.type === "assistant/message") {
757
+ if (event.data.turn !== turn || state.kind !== "open" || !sameAttempt(state, event.data.turn, event.data.step)) {
758
+ invalid = true;
759
+ continue;
760
+ }
761
+ const sample = event.data.usage ?? streamUsage(event.data.stream);
762
+ if (sample !== void 0) state = {
763
+ ...state,
764
+ sample
765
+ };
766
+ if (!closeOpen(messageRoute(event.data.message))) invalid = true;
767
+ else state = {
768
+ kind: "settled",
769
+ turn,
770
+ step: event.data.step,
771
+ by: "message"
772
+ };
773
+ continue;
774
+ }
775
+ if (event.type === "llm/retry") {
776
+ if (event.data.turn !== turn || state.kind === "idle" || !sameAttempt(state, event.data.turn, event.data.step)) {
777
+ invalid = true;
778
+ continue;
779
+ }
780
+ if (state.kind === "settled" || state.kind === "open" && !closeOpen()) invalid = true;
781
+ if (!invalid) state = {
782
+ kind: "settled",
783
+ turn,
784
+ step: event.data.step,
785
+ by: "retry"
786
+ };
787
+ continue;
788
+ }
789
+ if (event.type === "step/end") {
790
+ if (event.data.turn !== turn || state.kind === "idle" || !sameAttempt(state, event.data.turn, event.data.step)) {
791
+ invalid = true;
792
+ continue;
793
+ }
794
+ if (state.kind === "open" && !closeOpen()) invalid = true;
795
+ if (!invalid) state = { kind: "idle" };
796
+ }
797
+ }
798
+ return invalid || !sawEnd || state.kind !== "idle" ? void 0 : aggregateAttempts(attempts);
799
+ }
800
+ //#endregion
586
801
  //#region lib/types/index.js
587
802
  /**
588
803
  * Single replay-aware token-meter service for request and surface pressure.
@@ -792,4 +1007,4 @@ var TokenMeter = class extends Service {
792
1007
  }
793
1008
  };
794
1009
  //#endregion
795
- export { TokenMeter, TokenMeter as default };
1010
+ export { TokenMeter, TokenMeter as default, deriveTurnTokenUsage };
@@ -9,6 +9,7 @@ import type { Message } from '@qilin-agent/llm';
9
9
  import type { EpochHeader, Session } from '@qilin-agent/session';
10
10
  import type { TokenMeasurement, TokenMeterConfig } from './types.ts';
11
11
  export type * from './types.ts';
12
+ export { deriveTurnTokenUsage } from './turn-usage.ts';
12
13
  export type * from './usage-projection.ts';
13
14
  export type * from './breakdown-projection.ts';
14
15
  declare module '@qilin-agent/kylin' {
@@ -13,6 +13,9 @@ import { contextPressureProjectionDefinition, tokenUsageProjectionDefinition } f
13
13
  import { estimateContent, estimateMessage, estimateToolsTokens, ROLE_OVERHEAD } from "./estimate.js";
14
14
  import { commitSurfaceTokens, planSurfaceTokens } from "./surface-fold.js";
15
15
  import { priceSurface } from "./route-pricing.js";
16
+ // The exact per-Turn fold, for consumers that account for a Turn outside the
17
+ // browser transcript; a deployment's usage reporter is the current one.
18
+ export { deriveTurnTokenUsage } from "./turn-usage.js";
16
19
  /** Sum disjoint provider usage buckets without double-counting reasoning output. */
17
20
  function usageTokens(usage) {
18
21
  return usage.inputTokens
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qilin-agent/token-meter",
3
3
  "description": "Replay-aware token measurement service (ctx.tokenMeter) for the QiLin",
4
- "version": "3.1.3",
4
+ "version": "3.1.5",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -40,25 +40,25 @@
40
40
  "license": "MIT",
41
41
  "peerDependencies": {
42
42
  "@qilin-agent/kylin": "~4.0.3",
43
- "@qilin-agent/llm-retry": "3.1.3",
44
- "@qilin-agent/compaction": "3.1.3",
45
- "@qilin-agent/llm": "3.1.3",
46
- "@qilin-agent/session": "3.1.3",
47
- "@qilin-agent/compaction-image-offload": "3.1.3",
48
- "@qilin-agent/session-projection": "3.1.3"
43
+ "@qilin-agent/llm": "3.1.5",
44
+ "@qilin-agent/compaction": "3.1.5",
45
+ "@qilin-agent/llm-retry": "3.1.5",
46
+ "@qilin-agent/compaction-image-offload": "3.1.5",
47
+ "@qilin-agent/session": "3.1.5",
48
+ "@qilin-agent/session-projection": "3.1.5"
49
49
  },
50
50
  "dependencies": {
51
51
  "zod": "^4.4.3",
52
- "@qilin-agent/schemastery": "~3.18.3",
53
- "@qilin-agent/util-values": "3.1.3"
52
+ "@qilin-agent/util-values": "3.1.5",
53
+ "@qilin-agent/schemastery": "~3.18.3"
54
54
  },
55
55
  "devDependencies": {
56
- "@qilin-agent/compaction": "3.1.3",
57
56
  "@qilin-agent/kylin": "~4.0.3",
58
- "@qilin-agent/llm-retry": "3.1.3",
59
- "@qilin-agent/llm": "3.1.3",
60
- "@qilin-agent/session": "3.1.3",
61
- "@qilin-agent/compaction-image-offload": "3.1.3",
62
- "@qilin-agent/session-projection": "3.1.3"
57
+ "@qilin-agent/llm": "3.1.5",
58
+ "@qilin-agent/compaction": "3.1.5",
59
+ "@qilin-agent/llm-retry": "3.1.5",
60
+ "@qilin-agent/session-projection": "3.1.5",
61
+ "@qilin-agent/compaction-image-offload": "3.1.5",
62
+ "@qilin-agent/session": "3.1.5"
63
63
  }
64
64
  }