@shpitdev/convex-cliproxy-observability 0.2.2 → 0.3.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.
@@ -1,36 +1,95 @@
1
+ import { TOKEN_FIELDS, type TokenField } from "../model-call/index.js";
1
2
  import type { CliproxyProjection, RecordValue } from "./types.js";
2
- import { count, object } from "./values.js";
3
+ import { count } from "./values.js";
4
+ /** Names the rule below in each usage measurement. */
5
+ const USAGE_SEMANTICS = "client_protocol_usage_v2";
6
+ /**
7
+ * The one token normalization rule, read from terminal native usage (`details.field` for
8
+ * nested counts). `first` takes the first listed field reported. `base` needs its field and
9
+ * adds each `plus` field, counting an absent one as 0. Input includes cache reads and writes,
10
+ * as OpenAI-style usage reports it. Anthropic reports them apart from `input_tokens` and omits
11
+ * them when no cache is used, as do CLIProxy's translators, so its input is the sum.
12
+ * `totalTokens` is only ever the reported `total_tokens`; Anthropic reports none and it is
13
+ * never summed. Nothing is manufactured from byte or text lengths.
14
+ */
15
+ const TOKEN_NORMALIZATION: Record<
16
+ Exclude<CliproxyProjection["protocol"], "unknown">,
17
+ Record<TokenField, { first: readonly string[] } | { base: string; plus: readonly string[] }>
18
+ > = {
19
+ anthropic_messages: {
20
+ inputTokens: {
21
+ base: "input_tokens",
22
+ plus: ["cache_read_input_tokens", "cache_creation_input_tokens"],
23
+ },
24
+ outputTokens: { first: ["output_tokens"] },
25
+ totalTokens: { first: ["total_tokens"] },
26
+ reasoningTokens: { first: ["output_tokens_details.thinking_tokens"] },
27
+ cachedInputTokens: { first: ["cache_read_input_tokens"] },
28
+ cacheCreationInputTokens: { first: ["cache_creation_input_tokens"] },
29
+ },
30
+ responses: {
31
+ inputTokens: { first: ["input_tokens"] },
32
+ outputTokens: { first: ["output_tokens"] },
33
+ totalTokens: { first: ["total_tokens"] },
34
+ reasoningTokens: { first: ["output_tokens_details.reasoning_tokens"] },
35
+ cachedInputTokens: { first: ["input_tokens_details.cached_tokens"] },
36
+ cacheCreationInputTokens: {
37
+ first: [
38
+ "input_tokens_details.cache_write_tokens",
39
+ "input_tokens_details.cache_creation_tokens",
40
+ ],
41
+ },
42
+ },
43
+ chat_completions: {
44
+ inputTokens: { first: ["prompt_tokens"] },
45
+ outputTokens: { first: ["completion_tokens"] },
46
+ totalTokens: { first: ["total_tokens"] },
47
+ reasoningTokens: { first: ["completion_tokens_details.reasoning_tokens"] },
48
+ cachedInputTokens: { first: ["prompt_tokens_details.cached_tokens"] },
49
+ cacheCreationInputTokens: {
50
+ first: [
51
+ "prompt_tokens_details.cache_write_tokens",
52
+ "prompt_tokens_details.cached_creation_tokens",
53
+ "prompt_tokens_details.cache_creation_tokens",
54
+ ],
55
+ },
56
+ },
57
+ };
58
+ export function normalizeUsage(
59
+ protocol: keyof typeof TOKEN_NORMALIZATION,
60
+ reported: ReadonlyMap<string, number>,
61
+ ): Partial<Record<TokenField, number>> {
62
+ const out: Partial<Record<TokenField, number>> = {};
63
+ for (const field of TOKEN_FIELDS) {
64
+ const rule = TOKEN_NORMALIZATION[protocol][field];
65
+ if ("first" in rule) {
66
+ out[field] = rule.first
67
+ .map((name) => reported.get(name))
68
+ .find((value) => value !== undefined);
69
+ continue;
70
+ }
71
+ const base = reported.get(rule.base);
72
+ out[field] =
73
+ base === undefined
74
+ ? undefined
75
+ : count(rule.plus.reduce((total, name) => total + (reported.get(name) ?? 0), base));
76
+ }
77
+ return out;
78
+ }
79
+ /** Native counts keyed as usage measurements name them. */
80
+ function reportedCounts(raw: RecordValue): Map<string, number> {
81
+ const reported = new Map<string, number>();
82
+ for (const [key, value] of Object.entries(raw)) {
83
+ if (count(value) !== undefined) reported.set(key, value as number);
84
+ else if (value && typeof value === "object" && !Array.isArray(value))
85
+ for (const [field, nested] of Object.entries(value))
86
+ if (count(nested) !== undefined) reported.set(`${key}.${field}`, nested as number);
87
+ }
88
+ return reported;
89
+ }
3
90
  export function extractUsage(projection: CliproxyProjection, raw: RecordValue) {
4
- // Anthropic reports disjoint uncached/cache creation/cache read input counts.
5
- // Never manufacture totals, costs, or token counts from byte/text lengths.
6
91
  projection.metadata.usage = raw;
7
- const anthropic = projection.protocol === "anthropic_messages";
8
-
9
- const responses = projection.protocol === "responses";
10
-
11
- const nativeInput = count(raw.input_tokens);
12
- const cacheRead = count(raw.cache_read_input_tokens);
13
- const cacheCreated = count(raw.cache_creation_input_tokens);
14
- const anthropicInput =
15
- nativeInput !== undefined && cacheRead !== undefined && cacheCreated !== undefined
16
- ? count(nativeInput + cacheRead + cacheCreated)
17
- : undefined;
18
- Object.assign(projection.scalars, {
19
- inputTokens: anthropic ? anthropicInput : responses ? nativeInput : count(raw.prompt_tokens),
20
- outputTokens: count(raw[anthropic || responses ? "output_tokens" : "completion_tokens"]),
21
- totalTokens: count(raw.total_tokens),
22
- cachedInputTokens: count(
23
- anthropic
24
- ? raw.cache_read_input_tokens
25
- : object(raw[responses ? "input_tokens_details" : "prompt_tokens_details"]).cached_tokens,
26
- ),
27
- reasoningTokens: count(
28
- object(raw[anthropic || responses ? "output_tokens_details" : "completion_tokens_details"])[
29
- anthropic ? "thinking_tokens" : "reasoning_tokens"
30
- ],
31
- ),
32
- });
33
- projection.metadata.tokenSemantics = anthropic
34
- ? "input_is_sum_of_reported_input_cache_creation_cache_read; cached_input_is_cache_read; missing_components_mean_unknown; no_synthesized_total"
35
- : "input_includes_reported_cached_input; terminal_usage_snapshot";
92
+ projection.metadata.tokenSemantics = USAGE_SEMANTICS;
93
+ if (projection.protocol !== "unknown")
94
+ Object.assign(projection.scalars, normalizeUsage(projection.protocol, reportedCounts(raw)));
36
95
  }