agl 41.1.0 → 41.2.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/changelog.md +13 -0
- package/dist_serve/bundle.js +1734 -1734
- package/dist_serve/bundle.js.map +2 -2
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +2 -2
- package/dist_ts/classes.inferencegateway.js +4 -3
- package/dist_ts/classes.inferenceusage.d.ts +19 -7
- package/dist_ts/classes.inferenceusage.js +49 -13
- package/dist_ts/classes.inferenceusagetap.d.ts +15 -3
- package/dist_ts/classes.inferenceusagetap.js +16 -9
- package/dist_ts/classes.usagefeed.js +3 -2
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/metrics.d.ts +29 -7
- package/dist_ts_interfaces/metrics.js +14 -1
- package/package.json +5 -5
- package/readme.md +15 -4
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +2 -2
- package/ts/classes.inferencegateway.ts +3 -2
- package/ts/classes.inferenceusage.ts +58 -19
- package/ts/classes.inferenceusagetap.ts +27 -8
- package/ts/classes.usagefeed.ts +2 -1
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/metrics.ts +34 -7
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/classes.controllersocketclient.ts +5 -2
- package/ts_web/elements.metricsboard.ts +68 -18
|
@@ -8,8 +8,26 @@ export type TControllerUsageSpan = 'minute' | 'hour';
|
|
|
8
8
|
export type TControllerUsageWindow = '1h' | '24h' | '7d';
|
|
9
9
|
export declare const controllerUsageWindows: readonly TControllerUsageWindow[];
|
|
10
10
|
export type TControllerUsageProvider = 'anthropic' | 'openai';
|
|
11
|
+
/**
|
|
12
|
+
* The tokens of the answers one bucket counts. Input counts only the tokens read neither from nor written to the
|
|
13
|
+
* provider's prompt cache. `cacheReadTokens` and `cacheWriteTokens` add up the cache reads and writes the answers
|
|
14
|
+
* stated; `cacheReadUnknownCalls` and `cacheWriteUnknownCalls` (each at most `calls`) count the answers that did not
|
|
15
|
+
* state theirs, which read or wrote an unknown number, not none. With either above 0 the stated sum is not the whole
|
|
16
|
+
* count, and `controllerUsageTotalIsLowerBound` of the bucket's provider says whether its total is then one. A bucket
|
|
17
|
+
* stored before these were counted reads with all its answers unknown.
|
|
18
|
+
*/
|
|
19
|
+
export interface IControllerUsageCounts {
|
|
20
|
+
/** The answers counted. */
|
|
21
|
+
calls: number;
|
|
22
|
+
inputTokens: number;
|
|
23
|
+
outputTokens: number;
|
|
24
|
+
cacheReadTokens: number;
|
|
25
|
+
cacheWriteTokens: number;
|
|
26
|
+
cacheReadUnknownCalls: number;
|
|
27
|
+
cacheWriteUnknownCalls: number;
|
|
28
|
+
}
|
|
11
29
|
/** One span of one model's tokens on one harness, account and session; never a prompt, a credential or an answer. */
|
|
12
|
-
export interface IControllerUsageBucket {
|
|
30
|
+
export interface IControllerUsageBucket extends IControllerUsageCounts {
|
|
13
31
|
/** The bucket's digest: SHA-256 of its span, start and dimensions. */
|
|
14
32
|
id: string;
|
|
15
33
|
span: TControllerUsageSpan;
|
|
@@ -23,13 +41,17 @@ export interface IControllerUsageBucket {
|
|
|
23
41
|
accountId: string;
|
|
24
42
|
/** The session id the client sent; `null` when it sent none. */
|
|
25
43
|
session: string | null;
|
|
26
|
-
/** The answers counted. */
|
|
27
|
-
calls: number;
|
|
28
|
-
inputTokens: number;
|
|
29
|
-
outputTokens: number;
|
|
30
|
-
cacheReadTokens: number;
|
|
31
|
-
cacheWriteTokens: number;
|
|
32
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Whether a provider's answers state their input without the tokens read from or written to the cache, so that a bucket
|
|
47
|
+
* of that provider with unknown cache counts (`cacheReadUnknownCalls` or `cacheWriteUnknownCalls` above 0) has total
|
|
48
|
+
* tokens (input, output, cache reads and cache writes) that are a lower bound of what its answers used. An Anthropic
|
|
49
|
+
* account's answers leave the cache out of their input, so a cache count such an answer did not state is counted
|
|
50
|
+
* nowhere. An OpenAI account's answers state every input token in their input count, the Messages answers the proxy
|
|
51
|
+
* translates from them included, so their unknown cache counts lie inside the input and their total is exact. Every
|
|
52
|
+
* provider must be named here: a new one fails the type check instead of reading as exact.
|
|
53
|
+
*/
|
|
54
|
+
export declare const controllerUsageTotalIsLowerBound: Readonly<Record<TControllerUsageProvider, boolean>>;
|
|
33
55
|
/** The span each window reads and how many spans it shows, the current one included. */
|
|
34
56
|
export declare const controllerUsageWindowSpans: Record<TControllerUsageWindow, {
|
|
35
57
|
span: TControllerUsageSpan;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
export const controllerUsageWindows = ['1h', '24h', '7d'];
|
|
2
|
+
/**
|
|
3
|
+
* Whether a provider's answers state their input without the tokens read from or written to the cache, so that a bucket
|
|
4
|
+
* of that provider with unknown cache counts (`cacheReadUnknownCalls` or `cacheWriteUnknownCalls` above 0) has total
|
|
5
|
+
* tokens (input, output, cache reads and cache writes) that are a lower bound of what its answers used. An Anthropic
|
|
6
|
+
* account's answers leave the cache out of their input, so a cache count such an answer did not state is counted
|
|
7
|
+
* nowhere. An OpenAI account's answers state every input token in their input count, the Messages answers the proxy
|
|
8
|
+
* translates from them included, so their unknown cache counts lie inside the input and their total is exact. Every
|
|
9
|
+
* provider must be named here: a new one fails the type check instead of reading as exact.
|
|
10
|
+
*/
|
|
11
|
+
export const controllerUsageTotalIsLowerBound = {
|
|
12
|
+
anthropic: true,
|
|
13
|
+
openai: false,
|
|
14
|
+
};
|
|
2
15
|
const minuteMs = 60_000;
|
|
3
16
|
const hourMs = 60 * minuteMs;
|
|
4
17
|
/** The span each window reads and how many spans it shows, the current one included. */
|
|
@@ -32,4 +45,4 @@ export const applyControllerUsageDelta = (viewArg, deltaArg) => {
|
|
|
32
45
|
viewArg.revision = deltaArg.revision;
|
|
33
46
|
return 'applied';
|
|
34
47
|
};
|
|
35
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
48
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX2ludGVyZmFjZXMvbWV0cmljcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBc0MsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBdUM3Rjs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUF3RDtJQUNuRyxTQUFTLEVBQUUsSUFBSTtJQUNmLE1BQU0sRUFBRSxLQUFLO0NBQ2QsQ0FBQztBQUVGLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQztBQUN4QixNQUFNLE1BQU0sR0FBRyxFQUFFLEdBQUcsUUFBUSxDQUFDO0FBRTdCLHdGQUF3RjtBQUN4RixNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBa0Y7SUFDdkgsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFO0lBQ25DLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRTtJQUNsQyxJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFO0NBQ3RDLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBeUMsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsQ0FBQztBQUU5Ryx3R0FBd0c7QUFDeEcsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQUcsQ0FBQyxTQUFpQyxFQUFFLE1BQWMsRUFBVSxFQUFFO0lBQ3RHLE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsMEJBQTBCLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDOUQsTUFBTSxNQUFNLEdBQUcscUJBQXFCLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDM0MsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsR0FBRyxNQUFNLEdBQUcsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDO0FBQ3JFLENBQUMsQ0FBQztBQUVGOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxZQUFZLEVBQUUsR0FBRyxFQUFXLENBQUM7QUFxQnJGOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLENBQUMsT0FBNkIsRUFBRSxRQUErQixFQUE0QixFQUFFO0lBQ3BJLElBQUksT0FBTyxJQUFJLFFBQVEsSUFBSSxRQUFRLENBQUMsS0FBSyxLQUFLLE9BQU8sQ0FBQyxLQUFLLElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxPQUFPLENBQUMsTUFBTTtXQUM1RixRQUFRLENBQUMsUUFBUSxLQUFLLE9BQU8sQ0FBQyxRQUFRLEdBQUcsQ0FBQztRQUFFLE9BQU8sWUFBWSxDQUFDO0lBQ3JFLEtBQUssTUFBTSxNQUFNLElBQUksUUFBUSxDQUFDLE9BQU87UUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQzlFLE9BQU8sQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQztJQUNyQyxPQUFPLFNBQVMsQ0FBQztBQUNuQixDQUFDLENBQUMifQ==
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agl",
|
|
3
|
-
"version": "41.
|
|
3
|
+
"version": "41.2.0",
|
|
4
4
|
"controllerUpgradeManagementVersion": 2,
|
|
5
5
|
"controllerManagementProtocolVersion": 1,
|
|
6
6
|
"private": false,
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
"@lossless.org/objectstorage": "10.7.1",
|
|
35
35
|
"@modelcontextprotocol/sdk": "1.30.0",
|
|
36
36
|
"@modelprofile.com/catalog": "^1.1.0",
|
|
37
|
-
"@modelprofile.com/flexharness": "
|
|
38
|
-
"@modelprofile.com/flexharness-agent": "
|
|
39
|
-
"@modelprofile.com/flexharness-models": "
|
|
40
|
-
"@modelprofile.com/flexharness-providers": "
|
|
37
|
+
"@modelprofile.com/flexharness": "17.0.0",
|
|
38
|
+
"@modelprofile.com/flexharness-agent": "17.0.0",
|
|
39
|
+
"@modelprofile.com/flexharness-models": "17.0.0",
|
|
40
|
+
"@modelprofile.com/flexharness-providers": "17.0.0",
|
|
41
41
|
"@modelprofile.com/mcp-crossharness": "5.9.0",
|
|
42
42
|
"@modelprofile.com/mcp-system": "3.1.0",
|
|
43
43
|
"@opencode-ai/sdk": "1.18.15",
|
package/readme.md
CHANGED
|
@@ -628,7 +628,15 @@ proxy then keeps the account out of rotation itself until it restarts, and says
|
|
|
628
628
|
The proxy also records the tokens every answer states -- input, output, cache reads and cache writes -- per minute and per
|
|
629
629
|
hour, model, client name, account and the session id the client sent, minutes for a day and hours for eight days, also
|
|
630
630
|
for an Anthropic API key; it keeps no prompt, answer or credential, and a write that fails is reported on its standard
|
|
631
|
-
error and retried with the next.
|
|
631
|
+
error and retried with the next. An answer that does not state its cache reads or writes (a ChatGPT answer without
|
|
632
|
+
`input_tokens_details.cache_write_tokens`, a Claude answer whose `cache_read_input_tokens` or
|
|
633
|
+
`cache_creation_input_tokens` is missing or null) has unknown cache reads or writes, not none: each bucket counts the
|
|
634
|
+
reads and writes its answers stated (`cacheReadTokens`, `cacheWriteTokens`) and, beside them, the answers that did not
|
|
635
|
+
state theirs (`cacheReadUnknownCalls`, `cacheWriteUnknownCalls`). An OpenAI account's unknown cache counts stay in its
|
|
636
|
+
input count, since OpenAI counts every input token there; a Claude account's input leaves the cache out, so a bucket of
|
|
637
|
+
one with unknown cache counts adds up to less than its answers used (`controllerUsageTotalIsLowerBound`). A bucket
|
|
638
|
+
stored before AGL counted them reads with all its answers unknown. The controller reads these only while a browser
|
|
639
|
+
watches a usage window:
|
|
632
640
|
`controller.metrics.usage.watch { window }` (`1h` by minute, `24h` or `7d` by hour) answers the window's buckets with
|
|
633
641
|
a `watchId`, the feed's `epoch` and a `revision`, and the changes follow on that connection alone as `usage.changed`
|
|
634
642
|
events, each continuing the revision before it, until `controller.metrics.usage.unwatch { watchId }` or the
|
|
@@ -642,7 +650,10 @@ is told to watch again and refused. An unreachable proxy refuses a watch with `p
|
|
|
642
650
|
|
|
643
651
|
The web app's header has a **Metrics** button. It shows the token usage in the main pane, as the Board button
|
|
644
652
|
shows the board; leaving it ends its watch. A toggle chooses the last hour, day or week. Totals for input, output,
|
|
645
|
-
cache reads, cache writes and calls head the view;
|
|
653
|
+
cache reads, cache writes and calls head the view; cache reads and writes read **Unknown** when no call stated any, and
|
|
654
|
+
**≥** the stated count when some calls did not, with the number of calls whose count is unknown under the tile (the
|
|
655
|
+
model and session tables say the same, and their **Total** reads **≥** when it leaves out a Claude answer's unknown
|
|
656
|
+
cache count); below them a line chart shows the tokens per minute by model, each
|
|
646
657
|
model's line drawn from zero (each completed minute for the hour, the hourly average for the day and week, the current
|
|
647
658
|
hour's over its minutes so far), and pointing at a moment lists every model's value there with the time. A table lists
|
|
648
659
|
every model the window counted, sorted by total tokens. A model's row opens a second table of its sessions by client and session id. The
|
|
@@ -737,8 +748,8 @@ accepted without effect. What cannot be translated faithfully is refused with a
|
|
|
737
748
|
Code then continues without them), server-side context management, code execution containers, remote MCP servers and
|
|
738
749
|
deferred tool loading, whose betas `CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1` keeps Claude Code from sending. Of an
|
|
739
750
|
answer Claude Code gets the model's text, its tool calls, its reasoning summary as thinking, the stop reason and the
|
|
740
|
-
token counts (input, output, cache reads
|
|
741
|
-
not reach it.
|
|
751
|
+
token counts (input, output, cache reads and writes, a cache count `null` when the backend did not state it); the
|
|
752
|
+
backend's response id, its own headers and the rest of its response do not reach it.
|
|
742
753
|
|
|
743
754
|
Reasoning effort: Claude Code sends its effort as `output_config.effort` (`low` to `max`, chosen with `/effort`,
|
|
744
755
|
`--effort` or `CLAUDE_CODE_EFFORT_LEVEL`), together with adaptive thinking; Claude Code 2.1.288, the release of AGL's
|
package/ts/00_commitinfo_data.ts
CHANGED
|
@@ -3765,7 +3765,7 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
|
|
|
3765
3765
|
},
|
|
3766
3766
|
"protocolVersion": {
|
|
3767
3767
|
"type": "number",
|
|
3768
|
-
"const":
|
|
3768
|
+
"const": 47
|
|
3769
3769
|
},
|
|
3770
3770
|
"upgradeManagementVersion": {
|
|
3771
3771
|
"type": "number"
|
|
@@ -3891,7 +3891,7 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
|
|
|
3891
3891
|
],
|
|
3892
3892
|
"additionalProperties": false
|
|
3893
3893
|
},
|
|
3894
|
-
"declaration": "interface IControllerStatus {\n packageName: string;\n packageVersion: string;\n protocolVersion:
|
|
3894
|
+
"declaration": "interface IControllerStatus {\n packageName: string;\n packageVersion: string;\n protocolVersion: 47;\n upgradeManagementVersion: number;\n /**\n * The protocol of the controller's loopback management endpoint (`controllerManagementProtocolVersion`); absent\n * from a controller older than the endpoint, which is reached over its socket.\n */\n managementProtocolVersion?: number;\n controllerPid: number;\n processGroupId: number;\n processFingerprint: string;\n processMode: TControllerProcessMode;\n lifecycleState: TControllerLifecycleState;\n startedAt: number;\n setupRequired: boolean;\n /** Present when the controller runs as the systemd user unit (`agl service install`); absent for a detached or foreground one. */\n serviceUnit?: \"agl-controller.service\";\n /** The TLS mode of the runtime configuration the controller runs with; absent until it is resolved. */\n tlsMode?: TControllerTlsMode;\n upgrade?: IControllerUpgradeStatus;\n /** AGL's inference proxy, by state only; details need a signed-in owner (`controller.proxy.status.get`). */\n proxy?: { state: TControllerProxyState };\n harnesses: Array<{\n harnessId: TControllerSessionHarnessId;\n pid?: number;\n state: TControllerHarnessLifecycleState;\n healthy: boolean;\n version?: string;\n supportsLocalAttachments?: boolean;\n connectionMode?: 'local' | 'shared' | 'remote';\n diagnostic?: IControllerCodexDiagnostic;\n /** Codex only: automatic reconnection of this entry's profile to the local shared app-server. */\n rejoin?: IControllerCodexRejoinStatus;\n /**\n * Codex only, on the controller status: every connected or required profile. The entry's\n * `healthy` is then true only when each of them is, apart from a profile on an account Codex\n * cannot run on now, listed `stopped` with an `account_unavailable` diagnostic that says why;\n * its other members describe the default profile.\n */\n profiles?: IControllerCodexProfileStatus[];\n }>;\n}"
|
|
3895
3895
|
},
|
|
3896
3896
|
{
|
|
3897
3897
|
"name": "IControllerTerminalResource",
|
|
@@ -1009,10 +1009,11 @@ export class InferenceGateway {
|
|
|
1009
1009
|
if (spent) this.options.router.recordSpend(accountId, route.loginId, spent);
|
|
1010
1010
|
else this.options.router.uncountedSpend(accountId, answeredArg.model ?? route.model.modelId);
|
|
1011
1011
|
} : undefined)
|
|
1012
|
-
: new InferenceClaudeUsageTap(answerFormat, (usageArg) => {
|
|
1012
|
+
: new InferenceClaudeUsageTap(answerFormat, (usageArg, cacheStatedArg) => {
|
|
1013
1013
|
if (route.provider === 'anthropic') this.options.router.countTokens(accountId, usageArg);
|
|
1014
1014
|
recordUsage({ inputTokens: usageArg.inputTokens, outputTokens: usageArg.outputTokens,
|
|
1015
|
-
cacheReadTokens:
|
|
1015
|
+
cacheReadTokens: cacheStatedArg.reads ? usageArg.cacheReadTokens : null,
|
|
1016
|
+
cacheWriteTokens: cacheStatedArg.writes ? usageArg.cacheWrite5mTokens + usageArg.cacheWrite1hTokens : null });
|
|
1016
1017
|
// An API key's answer is priced as it ends, and its spend counts against the key's limit from then on.
|
|
1017
1018
|
}, route.provider === 'anthropic' && route.credential === 'api_key' ? (answeredArg) => {
|
|
1018
1019
|
const spent = inferenceAnthropicSpend(answeredArg, route.model.modelId, Date.now(), inferenceAnthropicServerToolTypes(body));
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import * as plugins from './plugins.js';
|
|
2
2
|
import { inferenceProviders, type TInferenceProvider } from './functions.proxycontract.js';
|
|
3
|
-
import type { IControllerUsageBucket, TControllerUsageSpan } from '../ts_interfaces/index.js';
|
|
3
|
+
import type { IControllerUsageBucket, IControllerUsageCounts, TControllerUsageSpan } from '../ts_interfaces/index.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The tokens one answer used, by kind. Input counts only the tokens read neither from nor written to the provider's
|
|
7
7
|
* prompt cache; the cache reads and writes are counted on their own, so the four add up to everything the answer used.
|
|
8
|
+
* `cacheReadTokens` and `cacheWriteTokens` are `null` when the answer did not state them, which is not the same as
|
|
9
|
+
* stating none. Whether the input count then holds them depends on the provider (`controllerUsageTotalIsLowerBound`):
|
|
10
|
+
* an OpenAI account's input counts every input token, an Anthropic account's leaves the cache out.
|
|
8
11
|
*/
|
|
9
12
|
export interface IInferenceTokenCounts {
|
|
10
13
|
inputTokens: number;
|
|
11
14
|
outputTokens: number;
|
|
12
|
-
cacheReadTokens: number;
|
|
13
|
-
cacheWriteTokens: number;
|
|
15
|
+
cacheReadTokens: number | null;
|
|
16
|
+
cacheWriteTokens: number | null;
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
@@ -33,8 +36,12 @@ export interface IInferenceUsageLedger {
|
|
|
33
36
|
record(recordArg: IInferenceUsageRecord): void;
|
|
34
37
|
}
|
|
35
38
|
|
|
39
|
+
/** The counts a bucket stored before unknown cache counts were kept lacks. */
|
|
40
|
+
type TUnknownCountKey = 'cacheReadUnknownCalls' | 'cacheWriteUnknownCalls';
|
|
41
|
+
const unknownCountKeys: readonly TUnknownCountKey[] = ['cacheReadUnknownCalls', 'cacheWriteUnknownCalls'];
|
|
42
|
+
|
|
36
43
|
/** One minute or hour of one model's tokens on one harness, account and session, as the proxy keeps it. */
|
|
37
|
-
export interface IInferenceUsageBucket extends
|
|
44
|
+
export interface IInferenceUsageBucket extends IControllerUsageCounts {
|
|
38
45
|
/** The bucket's digest: SHA-256 of its span, start and dimensions. */
|
|
39
46
|
id: string;
|
|
40
47
|
span: TControllerUsageSpan;
|
|
@@ -45,16 +52,29 @@ export interface IInferenceUsageBucket extends IInferenceTokenCounts {
|
|
|
45
52
|
harness: string | null;
|
|
46
53
|
accountId: string;
|
|
47
54
|
session: string | null;
|
|
48
|
-
/** The answers counted. */
|
|
49
|
-
calls: number;
|
|
50
55
|
/** When a flush last added to the bucket. */
|
|
51
56
|
updatedAt: Date;
|
|
52
57
|
}
|
|
53
58
|
|
|
54
|
-
|
|
59
|
+
/**
|
|
60
|
+
* A stored bucket. One written before unknown cache counts were kept has neither `cacheReadUnknownCalls` nor
|
|
61
|
+
* `cacheWriteUnknownCalls`: it did not record whether its answers stated their cache reads and writes, so each of its
|
|
62
|
+
* answers reads as one that stated neither.
|
|
63
|
+
*/
|
|
64
|
+
export interface IInferenceUsageDocument extends Omit<IInferenceUsageBucket, TUnknownCountKey>,
|
|
65
|
+
Partial<Pick<IInferenceUsageBucket, TUnknownCountKey>> {
|
|
55
66
|
expiresAt: Date;
|
|
56
67
|
}
|
|
57
68
|
|
|
69
|
+
/** A stored bucket's counts, those of a bucket written before unknown cache counts were kept with every answer unknown. */
|
|
70
|
+
const storedCounts = (documentArg: Omit<IInferenceUsageDocument, 'id' | 'span' | 'start' | 'provider' | 'model' | 'harness'
|
|
71
|
+
| 'accountId' | 'session' | 'updatedAt' | 'expiresAt'>): IControllerUsageCounts => ({
|
|
72
|
+
calls: documentArg.calls, inputTokens: documentArg.inputTokens, outputTokens: documentArg.outputTokens,
|
|
73
|
+
cacheReadTokens: documentArg.cacheReadTokens, cacheWriteTokens: documentArg.cacheWriteTokens,
|
|
74
|
+
cacheReadUnknownCalls: documentArg.cacheReadUnknownCalls ?? documentArg.calls,
|
|
75
|
+
cacheWriteUnknownCalls: documentArg.cacheWriteUnknownCalls ?? documentArg.calls,
|
|
76
|
+
});
|
|
77
|
+
|
|
58
78
|
/** A usage flush that failed, as the proxy reports it: no session id, prompt or account id. */
|
|
59
79
|
export interface IInferenceUsageFlushFailure {
|
|
60
80
|
/** The buckets kept for the next flush. */
|
|
@@ -96,10 +116,14 @@ const pageDocuments = 128;
|
|
|
96
116
|
|
|
97
117
|
const validDate = (valueArg: unknown): valueArg is Date => valueArg instanceof Date && Number.isFinite(valueArg.getTime());
|
|
98
118
|
const validCount = (valueArg: unknown): valueArg is number => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
|
|
119
|
+
/** A count of a bucket's answers: whole, and at most the answers it counts. */
|
|
120
|
+
const countWithin = (countArg: unknown, callsArg: unknown): boolean => validCount(countArg) && validCount(callsArg) && countArg <= callsArg;
|
|
99
121
|
|
|
100
122
|
const assertDocument: (valueArg: unknown) => asserts valueArg is IInferenceUsageDocument = (valueArg) => {
|
|
101
123
|
const value = valueArg as Partial<IInferenceUsageDocument> | null;
|
|
102
|
-
|
|
124
|
+
// A bucket written before unknown cache counts were kept has neither of them; every bucket written since has both.
|
|
125
|
+
const kept = !!value && typeof value === 'object' && Object.hasOwn(value, 'cacheWriteUnknownCalls');
|
|
126
|
+
if (!value || typeof value !== 'object' || Object.keys(value).length !== 15 + (kept ? unknownCountKeys.length : 0)
|
|
103
127
|
|| typeof value.id !== 'string' || !hex64.test(value.id)
|
|
104
128
|
|| !usageSpans.includes(value.span as TControllerUsageSpan)
|
|
105
129
|
|| !validDate(value.start) || value.start.getTime() % spanMs[value.span as TControllerUsageSpan] !== 0
|
|
@@ -111,6 +135,7 @@ const assertDocument: (valueArg: unknown) => asserts valueArg is IInferenceUsage
|
|
|
111
135
|
|| !validCount(value.calls) || value.calls < 1
|
|
112
136
|
|| !validCount(value.inputTokens) || !validCount(value.outputTokens)
|
|
113
137
|
|| !validCount(value.cacheReadTokens) || !validCount(value.cacheWriteTokens)
|
|
138
|
+
|| (kept && !unknownCountKeys.every((keyArg) => countWithin(value[keyArg], value.calls)))
|
|
114
139
|
|| !validDate(value.updatedAt) || !validDate(value.expiresAt)) {
|
|
115
140
|
throw new Error('Invalid inference usage bucket.');
|
|
116
141
|
}
|
|
@@ -134,6 +159,9 @@ export class InferenceUsageModel extends plugins.nosqldb.SmartDataDbDoc<Inferenc
|
|
|
134
159
|
@plugins.nosqldb.svDb() public outputTokens!: number;
|
|
135
160
|
@plugins.nosqldb.svDb() public cacheReadTokens!: number;
|
|
136
161
|
@plugins.nosqldb.svDb() public cacheWriteTokens!: number;
|
|
162
|
+
/** These two are absent only on a bucket written before unknown cache counts were kept; every write sets them. */
|
|
163
|
+
@plugins.nosqldb.svDb() public cacheReadUnknownCalls?: number;
|
|
164
|
+
@plugins.nosqldb.svDb() public cacheWriteUnknownCalls?: number;
|
|
137
165
|
@plugins.nosqldb.svDb()
|
|
138
166
|
@plugins.nosqldb.index() public updatedAt!: Date;
|
|
139
167
|
@plugins.nosqldb.svDb()
|
|
@@ -259,9 +287,11 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
259
287
|
public record(recordArg: IInferenceUsageRecord): void {
|
|
260
288
|
try {
|
|
261
289
|
if (this.stopped) return;
|
|
262
|
-
const counts = [recordArg.inputTokens, recordArg.outputTokens, recordArg.cacheReadTokens, recordArg.cacheWriteTokens];
|
|
290
|
+
const counts = [recordArg.inputTokens, recordArg.outputTokens, recordArg.cacheReadTokens ?? 0, recordArg.cacheWriteTokens ?? 0];
|
|
263
291
|
if (!inferenceProviders.includes(recordArg.provider) || !usageModelPattern.test(recordArg.model)
|
|
264
292
|
|| (recordArg.harness !== null && !usageHarnessPattern.test(recordArg.harness)) || !hex64.test(recordArg.accountId)
|
|
293
|
+
|| (recordArg.cacheReadTokens !== null && !validCount(recordArg.cacheReadTokens))
|
|
294
|
+
|| (recordArg.cacheWriteTokens !== null && !validCount(recordArg.cacheWriteTokens))
|
|
265
295
|
|| !counts.every(validCount) || counts.every((countArg) => countArg === 0)) {
|
|
266
296
|
this.droppedRecords += 1;
|
|
267
297
|
return;
|
|
@@ -280,12 +310,12 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
280
310
|
this.droppedRecords += 1;
|
|
281
311
|
return;
|
|
282
312
|
}
|
|
313
|
+
const answer = answerCounts(recordArg);
|
|
283
314
|
for (const { span, start, id } of buckets) {
|
|
284
315
|
const held = this.pending.get(id);
|
|
285
|
-
if (held) { addCounts(held,
|
|
316
|
+
if (held) { addCounts(held, answer); continue; }
|
|
286
317
|
this.pending.set(id, { id, span, start: new Date(start), provider: recordArg.provider, model: recordArg.model,
|
|
287
|
-
harness: recordArg.harness, accountId: recordArg.accountId, session,
|
|
288
|
-
outputTokens: recordArg.outputTokens, cacheReadTokens: recordArg.cacheReadTokens, cacheWriteTokens: recordArg.cacheWriteTokens });
|
|
318
|
+
harness: recordArg.harness, accountId: recordArg.accountId, session, ...answer });
|
|
289
319
|
}
|
|
290
320
|
} catch {
|
|
291
321
|
this.droppedRecords += 1;
|
|
@@ -364,7 +394,7 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
364
394
|
// The buckets that were not written join those recorded since, as far as the pending limit allows.
|
|
365
395
|
for (const bucket of failed) {
|
|
366
396
|
const held = this.pending.get(bucket.id);
|
|
367
|
-
if (held) addCounts(held, bucket
|
|
397
|
+
if (held) addCounts(held, bucket);
|
|
368
398
|
else if (this.pending.size < maximumPendingBuckets) this.pending.set(bucket.id, bucket);
|
|
369
399
|
else this.droppedRecords += bucket.calls;
|
|
370
400
|
}
|
|
@@ -388,7 +418,10 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
388
418
|
return;
|
|
389
419
|
}
|
|
390
420
|
const result = await InferenceUsageModel.exact.transition({ current, change: (modelArg) => {
|
|
391
|
-
|
|
421
|
+
// A bucket written before unknown cache counts were kept gets them, its own answers as unknown.
|
|
422
|
+
const counts = storedCounts(modelArg);
|
|
423
|
+
addCounts(counts, bucketArg);
|
|
424
|
+
Object.assign(modelArg, counts);
|
|
392
425
|
modelArg.updatedAt = updatedAt;
|
|
393
426
|
} }, { signal: signalArg });
|
|
394
427
|
if (result.status === 'transitioned') {
|
|
@@ -418,15 +451,21 @@ export class InferenceUsageStore implements IInferenceUsageLedger {
|
|
|
418
451
|
}
|
|
419
452
|
|
|
420
453
|
/** A stored bucket as a reader receives it: its start in Unix milliseconds, without its bookkeeping. */
|
|
421
|
-
const wireBucket = (documentArg:
|
|
454
|
+
const wireBucket = (documentArg: IInferenceUsageDocument): IControllerUsageBucket => ({ id: documentArg.id, span: documentArg.span,
|
|
422
455
|
start: documentArg.start.getTime(), provider: documentArg.provider, model: documentArg.model, harness: documentArg.harness,
|
|
423
|
-
accountId: documentArg.accountId, session: documentArg.session,
|
|
424
|
-
|
|
456
|
+
accountId: documentArg.accountId, session: documentArg.session, ...storedCounts(documentArg) });
|
|
457
|
+
|
|
458
|
+
/** One answer as a bucket counts it: the cache counts it stated, and for each it did not, one answer unknown. */
|
|
459
|
+
const answerCounts = (countsArg: IInferenceTokenCounts): IControllerUsageCounts => ({ calls: 1, inputTokens: countsArg.inputTokens,
|
|
460
|
+
outputTokens: countsArg.outputTokens, cacheReadTokens: countsArg.cacheReadTokens ?? 0, cacheWriteTokens: countsArg.cacheWriteTokens ?? 0,
|
|
461
|
+
cacheReadUnknownCalls: countsArg.cacheReadTokens === null ? 1 : 0, cacheWriteUnknownCalls: countsArg.cacheWriteTokens === null ? 1 : 0 });
|
|
425
462
|
|
|
426
|
-
const addCounts = (targetArg:
|
|
427
|
-
targetArg.calls +=
|
|
463
|
+
const addCounts = (targetArg: IControllerUsageCounts, addArg: IControllerUsageCounts): void => {
|
|
464
|
+
targetArg.calls += addArg.calls;
|
|
428
465
|
targetArg.inputTokens += addArg.inputTokens;
|
|
429
466
|
targetArg.outputTokens += addArg.outputTokens;
|
|
430
467
|
targetArg.cacheReadTokens += addArg.cacheReadTokens;
|
|
431
468
|
targetArg.cacheWriteTokens += addArg.cacheWriteTokens;
|
|
469
|
+
targetArg.cacheReadUnknownCalls += addArg.cacheReadUnknownCalls;
|
|
470
|
+
targetArg.cacheWriteUnknownCalls += addArg.cacheWriteUnknownCalls;
|
|
432
471
|
};
|
|
@@ -29,6 +29,16 @@ interface IStatedUsage {
|
|
|
29
29
|
ephemeral_1h_input_tokens?: number;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Which cache counts an Anthropic Messages answer stated: `reads` its `cache_read_input_tokens`, `writes` its
|
|
34
|
+
* `cache_creation_input_tokens` or its `cache_creation` split. Anthropic documents both as `number or null`, and a
|
|
35
|
+
* translated answer gives null for a count the model did not state.
|
|
36
|
+
*/
|
|
37
|
+
export interface IInferenceClaudeCacheStated {
|
|
38
|
+
reads: boolean;
|
|
39
|
+
writes: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
/** What the relay hands each chunk of an answer it passed on, and the end of the answer. */
|
|
33
43
|
export interface IInferenceUsageTap {
|
|
34
44
|
chunk(bytesArg: Uint8Array): void;
|
|
@@ -43,7 +53,9 @@ export interface IInferenceUsageTap {
|
|
|
43
53
|
* A stream (`sse`) states usage in `message_start` (`message.usage`, `message.model`) and in `message_delta` (`usage`,
|
|
44
54
|
* cumulative), so only those events' data lines are parsed and every other line is skipped unread; a cut stream reports
|
|
45
55
|
* what arrived. A JSON answer states `usage` and `model` at its top level and is read at its end. Anything unreadable
|
|
46
|
-
* reports nothing: the estimate is the authority's, and the answer is never at stake.
|
|
56
|
+
* reports nothing: the estimate is the authority's, and the answer is never at stake. The report says which cache
|
|
57
|
+
* counts the answer stated (`IInferenceClaudeCacheStated`): one it did not state is unknown, not none. The usage
|
|
58
|
+
* reported for the estimate counts an unknown one as none, as the account service's token usage has no unknown.
|
|
47
59
|
*
|
|
48
60
|
* `answered`, given for an answer an API key served, takes at the end what the answer stated for its price: the model
|
|
49
61
|
* that answered and its usage object as sent, a stream's merged field by field from `message_start` and the
|
|
@@ -66,7 +78,8 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
|
|
|
66
78
|
private stopped = false;
|
|
67
79
|
private ended = false;
|
|
68
80
|
|
|
69
|
-
constructor(private readonly format: 'sse' | 'json',
|
|
81
|
+
constructor(private readonly format: 'sse' | 'json',
|
|
82
|
+
private readonly report: (usageArg: TTokenUsage, cacheStatedArg: IInferenceClaudeCacheStated) => void,
|
|
70
83
|
private readonly answered?: (answeredArg: IInferenceAnsweredUsage) => void) {}
|
|
71
84
|
|
|
72
85
|
/** Reads a chunk the relay passed on; never throws. */
|
|
@@ -102,7 +115,11 @@ export class InferenceClaudeUsageTap implements IInferenceUsageTap {
|
|
|
102
115
|
}
|
|
103
116
|
this.stopped = true;
|
|
104
117
|
const usage = this.usage();
|
|
105
|
-
if (usage)
|
|
118
|
+
if (usage) {
|
|
119
|
+
this.report(usage, { reads: this.stated.cache_read_input_tokens !== undefined,
|
|
120
|
+
writes: this.stated.cache_creation_input_tokens !== undefined || this.stated.ephemeral_5m_input_tokens !== undefined
|
|
121
|
+
|| this.stated.ephemeral_1h_input_tokens !== undefined });
|
|
122
|
+
}
|
|
106
123
|
} catch { this.stopped = true; } finally {
|
|
107
124
|
try {
|
|
108
125
|
this.answered?.({ ...(this.model === undefined ? {} : { model: this.model }), ...(this.raw ? { usage: this.raw } : {}),
|
|
@@ -207,7 +224,8 @@ const searchCallTypes: ReadonlyMap<string, 'web' | 'file'> = new Map([['web_sear
|
|
|
207
224
|
* `response.output_item.done`; a data-only frame is kept up to the frame limit and parsed only when it names a terminal
|
|
208
225
|
* type, or a finished output item that is a search call. A JSON answer states `usage` at its top level. `input_tokens`
|
|
209
226
|
* counts every input token, so the cached reads (`cached_tokens`) and cache writes (`cache_write_tokens`) of
|
|
210
|
-
* `input_tokens_details` are taken out of it.
|
|
227
|
+
* `input_tokens_details` are taken out of it. An answer that does not state `cached_tokens` or `cache_write_tokens`
|
|
228
|
+
* reports those as unknown (`null`), never as none, and its input count then holds whatever it read or wrote.
|
|
211
229
|
*
|
|
212
230
|
* `answered`, given for an answer an OpenAI API key served, takes at the end what the answer stated for its price
|
|
213
231
|
* (`IInferenceResponsesAnswered`): whether its terminal event or its JSON body was read, arrived too large to read, or
|
|
@@ -380,10 +398,11 @@ export class InferenceResponsesUsageTap implements IInferenceUsageTap {
|
|
|
380
398
|
const output = count(usage?.output_tokens);
|
|
381
399
|
if (input === undefined && output === undefined) return undefined;
|
|
382
400
|
const details = record(usage?.input_tokens_details);
|
|
383
|
-
const cacheRead = count(details?.cached_tokens) ??
|
|
384
|
-
const cacheWrite = count(details?.cache_write_tokens) ??
|
|
385
|
-
const counts: IInferenceTokenCounts = { inputTokens: Math.max(0, (input ?? 0) - cacheRead - cacheWrite),
|
|
401
|
+
const cacheRead = count(details?.cached_tokens) ?? null;
|
|
402
|
+
const cacheWrite = count(details?.cache_write_tokens) ?? null;
|
|
403
|
+
const counts: IInferenceTokenCounts = { inputTokens: Math.max(0, (input ?? 0) - (cacheRead ?? 0) - (cacheWrite ?? 0)),
|
|
386
404
|
outputTokens: output ?? 0, cacheReadTokens: cacheRead, cacheWriteTokens: cacheWrite };
|
|
387
|
-
return counts.inputTokens + counts.outputTokens + counts.cacheReadTokens + counts.cacheWriteTokens
|
|
405
|
+
return counts.inputTokens + counts.outputTokens + (counts.cacheReadTokens ?? 0) + (counts.cacheWriteTokens ?? 0) > 0
|
|
406
|
+
? counts : undefined;
|
|
388
407
|
}
|
|
389
408
|
}
|
package/ts/classes.usagefeed.ts
CHANGED
|
@@ -68,7 +68,8 @@ const bucketOrder = (leftArg: IControllerUsageBucket, rightArg: IControllerUsage
|
|
|
68
68
|
|
|
69
69
|
const sameCounts = (leftArg: IControllerUsageBucket, rightArg: IControllerUsageBucket): boolean => leftArg.calls === rightArg.calls
|
|
70
70
|
&& leftArg.inputTokens === rightArg.inputTokens && leftArg.outputTokens === rightArg.outputTokens
|
|
71
|
-
&& leftArg.cacheReadTokens === rightArg.cacheReadTokens && leftArg.cacheWriteTokens === rightArg.cacheWriteTokens
|
|
71
|
+
&& leftArg.cacheReadTokens === rightArg.cacheReadTokens && leftArg.cacheWriteTokens === rightArg.cacheWriteTokens
|
|
72
|
+
&& leftArg.cacheReadUnknownCalls === rightArg.cacheReadUnknownCalls && leftArg.cacheWriteUnknownCalls === rightArg.cacheWriteUnknownCalls;
|
|
72
73
|
|
|
73
74
|
/**
|
|
74
75
|
* The metrics board's usage feed in the controller. The proxy keeps the token usage and has no push, so the feed reads
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
TControllerBrowserPaintAvailability,
|
|
6
6
|
TControllerBrowserPaintTermination,
|
|
7
7
|
} from './browserpaint.js';
|
|
8
|
-
export const controllerProtocolVersion =
|
|
8
|
+
export const controllerProtocolVersion = 47 as const;
|
|
9
9
|
export const controllerUpgradeManagementVersion = 2 as const;
|
|
10
10
|
export const controllerPackageName = 'agl' as const;
|
|
11
11
|
/** The systemd user unit `agl service install` writes; a controller it runs reports it in its status. */
|
package/ts_interfaces/metrics.ts
CHANGED
|
@@ -10,8 +10,27 @@ export type TControllerUsageWindow = '1h' | '24h' | '7d';
|
|
|
10
10
|
export const controllerUsageWindows: readonly TControllerUsageWindow[] = ['1h', '24h', '7d'];
|
|
11
11
|
export type TControllerUsageProvider = 'anthropic' | 'openai';
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* The tokens of the answers one bucket counts. Input counts only the tokens read neither from nor written to the
|
|
15
|
+
* provider's prompt cache. `cacheReadTokens` and `cacheWriteTokens` add up the cache reads and writes the answers
|
|
16
|
+
* stated; `cacheReadUnknownCalls` and `cacheWriteUnknownCalls` (each at most `calls`) count the answers that did not
|
|
17
|
+
* state theirs, which read or wrote an unknown number, not none. With either above 0 the stated sum is not the whole
|
|
18
|
+
* count, and `controllerUsageTotalIsLowerBound` of the bucket's provider says whether its total is then one. A bucket
|
|
19
|
+
* stored before these were counted reads with all its answers unknown.
|
|
20
|
+
*/
|
|
21
|
+
export interface IControllerUsageCounts {
|
|
22
|
+
/** The answers counted. */
|
|
23
|
+
calls: number;
|
|
24
|
+
inputTokens: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
cacheReadTokens: number;
|
|
27
|
+
cacheWriteTokens: number;
|
|
28
|
+
cacheReadUnknownCalls: number;
|
|
29
|
+
cacheWriteUnknownCalls: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
/** One span of one model's tokens on one harness, account and session; never a prompt, a credential or an answer. */
|
|
14
|
-
export interface IControllerUsageBucket {
|
|
33
|
+
export interface IControllerUsageBucket extends IControllerUsageCounts {
|
|
15
34
|
/** The bucket's digest: SHA-256 of its span, start and dimensions. */
|
|
16
35
|
id: string;
|
|
17
36
|
span: TControllerUsageSpan;
|
|
@@ -25,14 +44,22 @@ export interface IControllerUsageBucket {
|
|
|
25
44
|
accountId: string;
|
|
26
45
|
/** The session id the client sent; `null` when it sent none. */
|
|
27
46
|
session: string | null;
|
|
28
|
-
/** The answers counted. */
|
|
29
|
-
calls: number;
|
|
30
|
-
inputTokens: number;
|
|
31
|
-
outputTokens: number;
|
|
32
|
-
cacheReadTokens: number;
|
|
33
|
-
cacheWriteTokens: number;
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Whether a provider's answers state their input without the tokens read from or written to the cache, so that a bucket
|
|
51
|
+
* of that provider with unknown cache counts (`cacheReadUnknownCalls` or `cacheWriteUnknownCalls` above 0) has total
|
|
52
|
+
* tokens (input, output, cache reads and cache writes) that are a lower bound of what its answers used. An Anthropic
|
|
53
|
+
* account's answers leave the cache out of their input, so a cache count such an answer did not state is counted
|
|
54
|
+
* nowhere. An OpenAI account's answers state every input token in their input count, the Messages answers the proxy
|
|
55
|
+
* translates from them included, so their unknown cache counts lie inside the input and their total is exact. Every
|
|
56
|
+
* provider must be named here: a new one fails the type check instead of reading as exact.
|
|
57
|
+
*/
|
|
58
|
+
export const controllerUsageTotalIsLowerBound: Readonly<Record<TControllerUsageProvider, boolean>> = {
|
|
59
|
+
anthropic: true,
|
|
60
|
+
openai: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
36
63
|
const minuteMs = 60_000;
|
|
37
64
|
const hourMs = 60 * minuteMs;
|
|
38
65
|
|
|
@@ -606,7 +606,8 @@ export const isControllerSessionFeedList = (
|
|
|
606
606
|
|
|
607
607
|
const usageBucketKeys = contractKeys<interfaces.IControllerUsageBucket>({
|
|
608
608
|
id: true, span: true, start: true, provider: true, model: true, harness: true, accountId: true, session: true,
|
|
609
|
-
calls: true, inputTokens: true, outputTokens: true, cacheReadTokens: true, cacheWriteTokens: true,
|
|
609
|
+
calls: true, inputTokens: true, outputTokens: true, cacheReadTokens: true, cacheWriteTokens: true, cacheReadUnknownCalls: true,
|
|
610
|
+
cacheWriteUnknownCalls: true,
|
|
610
611
|
});
|
|
611
612
|
const usageHex64 = /^[a-f0-9]{64}$/;
|
|
612
613
|
const usageCount = (valueArg: unknown): boolean => Number.isSafeInteger(valueArg) && (valueArg as number) >= 0;
|
|
@@ -629,7 +630,9 @@ export const isControllerUsageBucket = (valueArg: unknown): valueArg is interfac
|
|
|
629
630
|
&& (candidate.session === null || usageText(candidate.session, 512))
|
|
630
631
|
&& usageCount(candidate.calls) && (candidate.calls as number) >= 1
|
|
631
632
|
&& usageCount(candidate.inputTokens) && usageCount(candidate.outputTokens)
|
|
632
|
-
&& usageCount(candidate.cacheReadTokens) && usageCount(candidate.cacheWriteTokens)
|
|
633
|
+
&& usageCount(candidate.cacheReadTokens) && usageCount(candidate.cacheWriteTokens)
|
|
634
|
+
&& usageCount(candidate.cacheReadUnknownCalls) && (candidate.cacheReadUnknownCalls as number) <= (candidate.calls as number)
|
|
635
|
+
&& usageCount(candidate.cacheWriteUnknownCalls) && (candidate.cacheWriteUnknownCalls as number) <= (candidate.calls as number);
|
|
633
636
|
};
|
|
634
637
|
|
|
635
638
|
export const isControllerUsageDelta = (valueArg: unknown): valueArg is interfaces.TControllerUsageDelta => {
|