pi-cache-optimizer 2.8.16 → 2.8.17

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/usage.ts ADDED
@@ -0,0 +1,172 @@
1
+ import { type UnknownRecord, asRecord, getNonNegativeNumber, getNumber } from "./common.ts";
2
+ import { getAssistantRecord } from "./model-identity.ts";
3
+ import { type UsageSnapshot } from "./stats-store.ts";
4
+
5
+ export function usageRecordFromAssistant(message: unknown): UnknownRecord | undefined {
6
+ return asRecord(getAssistantRecord(message)?.usage);
7
+ }
8
+
9
+ export function getNestedRecord(record: UnknownRecord | undefined, key: string): UnknownRecord | undefined {
10
+ return asRecord(record?.[key]);
11
+ }
12
+
13
+ export function getFirstNonNegativeNumber(...values: unknown[]): number | undefined {
14
+ for (const value of values) {
15
+ const number = getNumber(value);
16
+ if (number !== undefined && number >= 0) return number;
17
+ }
18
+ return undefined;
19
+ }
20
+
21
+ export function readCachedTokensFromDetails(details: UnknownRecord | undefined): number | undefined {
22
+ return getFirstNonNegativeNumber(details?.cached_tokens, details?.cachedTokens);
23
+ }
24
+
25
+ export function readCacheWriteFromDetails(details: UnknownRecord | undefined): number | undefined {
26
+ return getFirstNonNegativeNumber(details?.cache_write_tokens, details?.cacheWriteTokens);
27
+ }
28
+
29
+ // Pi normalizes provider-specific raw usage (prompt_cache_hit_tokens, cached_tokens,
30
+ // cache_read_input_tokens, etc.) into a common shape:
31
+ // input = uncached prompt portion (total prompt minus cacheRead minus cacheWrite)
32
+ // cacheRead = tokens read from a previously-cached prefix
33
+ // cacheWrite= tokens newly written into cache in this request
34
+ //
35
+ // We reconstruct the total prompt-token count as input + cacheRead + cacheWrite.
36
+ // Pi guarantees that input, cacheRead, and cacheWrite are always present on
37
+ // assistant messages processed through its provider pipeline (at least as zero).
38
+ //
39
+ // Only DeepSeek sets allowInputOnly=true so that a cache miss (cacheRead=0) still
40
+ // contributes total input tokens to the denominator.
41
+ export function getPiNormalizedUsage(message: unknown, allowInputOnly = false): UsageSnapshot | undefined {
42
+ const usage = usageRecordFromAssistant(message);
43
+ if (!usage) return undefined;
44
+
45
+ const input = getNonNegativeNumber(usage, "input");
46
+ const cacheRead = getNonNegativeNumber(usage, "cacheRead");
47
+ const cacheWrite = getNonNegativeNumber(usage, "cacheWrite");
48
+ const hasCacheSignal = cacheRead !== undefined || cacheWrite !== undefined;
49
+
50
+ if (!hasCacheSignal && (input === undefined || !allowInputOnly)) return undefined;
51
+
52
+ // Under healthy Pi normalization input is the uncached portion, so
53
+ // totalInput = input + cacheRead + cacheWrite gives the full prompt token count.
54
+ // Guard against degenerate reads where a broken proxy omits prompt_tokens and
55
+ // Pi's input falls to zero: totalInput must never be less than cacheRead + cacheWrite.
56
+ const computed = (input ?? 0) + (cacheRead ?? 0) + (cacheWrite ?? 0);
57
+ const floor = (cacheRead ?? 0) + (cacheWrite ?? 0);
58
+ return {
59
+ cacheRead: cacheRead ?? 0,
60
+ cacheWrite: cacheWrite ?? 0,
61
+ totalInput: computed >= floor ? computed : floor,
62
+ };
63
+ }
64
+
65
+ // Raw fallback for DeepSeek responses that still carry their native usage fields.
66
+ // In practice Pi normalizes usage before message_end fires, so this path is only
67
+ // reached when Pi-normalized fields are absent (e.g. custom/foreign providers).
68
+ export function getDeepSeekRawUsage(message: unknown): UsageSnapshot | undefined {
69
+ const usage = usageRecordFromAssistant(message);
70
+ if (!usage) return undefined;
71
+
72
+ const cacheRead = getFirstNonNegativeNumber(usage.prompt_cache_hit_tokens);
73
+ if (cacheRead === undefined) return undefined;
74
+
75
+ const cacheMiss = getFirstNonNegativeNumber(usage.prompt_cache_miss_tokens);
76
+ const promptTokens = getFirstNonNegativeNumber(usage.prompt_tokens);
77
+ // DeepSeek guarantees prompt_tokens = prompt_cache_hit_tokens + prompt_cache_miss_tokens.
78
+ const totalInput = promptTokens ?? cacheRead + (cacheMiss ?? 0);
79
+
80
+ return { cacheRead, cacheWrite: 0, totalInput };
81
+ }
82
+
83
+ // Raw fallback for OpenAI-family responses that still carry their native usage fields.
84
+ // In practice Pi normalizes usage before message_end fires, so this path is only
85
+ // reached when Pi-normalized fields are absent (e.g. custom/foreign providers).
86
+ export function getOpenAIRawUsage(message: unknown): UsageSnapshot | undefined {
87
+ const usage = usageRecordFromAssistant(message);
88
+ if (!usage) return undefined;
89
+
90
+ const promptDetails = getNestedRecord(usage, "prompt_tokens_details") ?? getNestedRecord(usage, "promptTokensDetails");
91
+ const inputDetails = getNestedRecord(usage, "input_tokens_details") ?? getNestedRecord(usage, "inputTokensDetails");
92
+ const cacheRead = readCachedTokensFromDetails(promptDetails) ?? readCachedTokensFromDetails(inputDetails);
93
+ if (cacheRead === undefined) return undefined;
94
+
95
+ const cacheWrite = readCacheWriteFromDetails(promptDetails) ?? readCacheWriteFromDetails(inputDetails) ?? 0;
96
+ const totalInput = getFirstNonNegativeNumber(
97
+ usage.prompt_tokens,
98
+ usage.promptTokens,
99
+ usage.input_tokens,
100
+ usage.inputTokens,
101
+ ) ?? cacheRead + cacheWrite;
102
+
103
+ return { cacheRead, cacheWrite, totalInput };
104
+ }
105
+
106
+ // Raw fallback for Anthropic/Claude responses that still carry their native usage fields.
107
+ // In practice Pi normalizes usage before message_end fires, so this path is only
108
+ // reached when Pi-normalized fields are absent (e.g. custom/foreign providers).
109
+ export function getAnthropicRawUsage(message: unknown): UsageSnapshot | undefined {
110
+ const usage = usageRecordFromAssistant(message);
111
+ if (!usage) return undefined;
112
+
113
+ const cacheRead = getFirstNonNegativeNumber(usage.cache_read_input_tokens, usage.cacheReadInputTokens);
114
+ const cacheWrite = getFirstNonNegativeNumber(usage.cache_creation_input_tokens, usage.cacheCreationInputTokens);
115
+ if (cacheRead === undefined && cacheWrite === undefined) return undefined;
116
+
117
+ // Anthropic input_tokens = tokens after the last cache breakpoint (neither read nor written).
118
+ const input = getFirstNonNegativeNumber(usage.input_tokens, usage.inputTokens) ?? 0;
119
+ return {
120
+ cacheRead: cacheRead ?? 0,
121
+ cacheWrite: cacheWrite ?? 0,
122
+ totalInput: input + (cacheRead ?? 0) + (cacheWrite ?? 0),
123
+ };
124
+ }
125
+
126
+ // Raw fallback for Gemini/Vertex responses that still carry their native usage fields.
127
+ // In practice Pi normalizes usage before message_end fires, so this path is only
128
+ // reached when Pi-normalized fields are absent (e.g. custom/foreign providers).
129
+ export function getGeminiRawUsage(message: unknown): UsageSnapshot | undefined {
130
+ const record = getAssistantRecord(message);
131
+ if (!record) return undefined;
132
+
133
+ const usage = asRecord(record.usage);
134
+ const metadata =
135
+ getNestedRecord(record, "usageMetadata") ??
136
+ getNestedRecord(record, "usage_metadata") ??
137
+ getNestedRecord(usage, "usageMetadata") ??
138
+ getNestedRecord(usage, "usage_metadata") ??
139
+ usage;
140
+ if (!metadata) return undefined;
141
+
142
+ const cacheRead = getFirstNonNegativeNumber(
143
+ metadata.cachedContentTokenCount,
144
+ metadata.cached_content_token_count,
145
+ );
146
+ if (cacheRead === undefined) return undefined;
147
+
148
+ const totalInput = getFirstNonNegativeNumber(
149
+ metadata.promptTokenCount,
150
+ metadata.prompt_token_count,
151
+ metadata.inputTokenCount,
152
+ metadata.input_token_count,
153
+ usage?.input_tokens,
154
+ usage?.inputTokens,
155
+ usage?.prompt_tokens,
156
+ usage?.promptTokens,
157
+ ) ?? cacheRead;
158
+
159
+ return { cacheRead, cacheWrite: 0, totalInput };
160
+ }
161
+
162
+ // Try Pi-normalized usage first (always present for messages that went through Pi's
163
+ // provider pipeline). Fall back to provider-specific raw-field readers when Pi-normalized
164
+ // fields are absent (e.g. messages from custom/foreign providers whose raw usage shape
165
+ // matches the official API).
166
+ export function normalizeWithFallback(
167
+ message: unknown,
168
+ rawNormalizer: (message: unknown) => UsageSnapshot | undefined,
169
+ options: { allowInputOnlyPiUsage?: boolean } = {},
170
+ ): UsageSnapshot | undefined {
171
+ return getPiNormalizedUsage(message, options.allowInputOnlyPiUsage) ?? rawNormalizer(message);
172
+ }