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.
@@ -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,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX2ludGVyZmFjZXMvbWV0cmljcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBc0MsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBMEI3RixNQUFNLFFBQVEsR0FBRyxNQUFNLENBQUM7QUFDeEIsTUFBTSxNQUFNLEdBQUcsRUFBRSxHQUFHLFFBQVEsQ0FBQztBQUU3Qix3RkFBd0Y7QUFDeEYsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQWtGO0lBQ3ZILElBQUksRUFBRSxFQUFFLElBQUksRUFBRSxRQUFRLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRTtJQUNuQyxLQUFLLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxFQUFFLEVBQUU7SUFDbEMsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLE1BQU0sRUFBRSxLQUFLLEVBQUUsQ0FBQyxHQUFHLEVBQUUsRUFBRTtDQUN0QyxDQUFDO0FBRUYsTUFBTSxDQUFDLE1BQU0scUJBQXFCLEdBQXlDLEVBQUUsTUFBTSxFQUFFLFFBQVEsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLENBQUM7QUFFOUcsd0dBQXdHO0FBQ3hHLE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLENBQUMsU0FBaUMsRUFBRSxNQUFjLEVBQVUsRUFBRTtJQUN0RyxNQUFNLEVBQUUsSUFBSSxFQUFFLEtBQUssRUFBRSxHQUFHLDBCQUEwQixDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQzlELE1BQU0sTUFBTSxHQUFHLHFCQUFxQixDQUFDLElBQUksQ0FBQyxDQUFDO0lBQzNDLE9BQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxNQUFNLEdBQUcsTUFBTSxDQUFDLEdBQUcsTUFBTSxHQUFHLENBQUMsS0FBSyxHQUFHLENBQUMsQ0FBQyxHQUFHLE1BQU0sQ0FBQztBQUNyRSxDQUFDLENBQUM7QUFFRjs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBRyxFQUFFLE9BQU8sRUFBRSxNQUFNLEVBQUUsWUFBWSxFQUFFLEdBQUcsRUFBVyxDQUFDO0FBcUJyRjs7O0dBR0c7QUFDSCxNQUFNLENBQUMsTUFBTSx5QkFBeUIsR0FBRyxDQUFDLE9BQTZCLEVBQUUsUUFBK0IsRUFBNEIsRUFBRTtJQUNwSSxJQUFJLE9BQU8sSUFBSSxRQUFRLElBQUksUUFBUSxDQUFDLEtBQUssS0FBSyxPQUFPLENBQUMsS0FBSyxJQUFJLFFBQVEsQ0FBQyxNQUFNLEtBQUssT0FBTyxDQUFDLE1BQU07V0FDNUYsUUFBUSxDQUFDLFFBQVEsS0FBSyxPQUFPLENBQUMsUUFBUSxHQUFHLENBQUM7UUFBRSxPQUFPLFlBQVksQ0FBQztJQUNyRSxLQUFLLE1BQU0sTUFBTSxJQUFJLFFBQVEsQ0FBQyxPQUFPO1FBQUUsT0FBTyxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsRUFBRSxNQUFNLENBQUMsQ0FBQztJQUM5RSxPQUFPLENBQUMsUUFBUSxHQUFHLFFBQVEsQ0FBQyxRQUFRLENBQUM7SUFDckMsT0FBTyxTQUFTLENBQUM7QUFDbkIsQ0FBQyxDQUFDIn0=
48
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWV0cmljcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX2ludGVyZmFjZXMvbWV0cmljcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFTQSxNQUFNLENBQUMsTUFBTSxzQkFBc0IsR0FBc0MsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLElBQUksQ0FBQyxDQUFDO0FBdUM3Rjs7Ozs7Ozs7R0FRRztBQUNILE1BQU0sQ0FBQyxNQUFNLGdDQUFnQyxHQUF3RDtJQUNuRyxTQUFTLEVBQUUsSUFBSTtJQUNmLE1BQU0sRUFBRSxLQUFLO0NBQ2QsQ0FBQztBQUVGLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQztBQUN4QixNQUFNLE1BQU0sR0FBRyxFQUFFLEdBQUcsUUFBUSxDQUFDO0FBRTdCLHdGQUF3RjtBQUN4RixNQUFNLENBQUMsTUFBTSwwQkFBMEIsR0FBa0Y7SUFDdkgsSUFBSSxFQUFFLEVBQUUsSUFBSSxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsRUFBRSxFQUFFO0lBQ25DLEtBQUssRUFBRSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRTtJQUNsQyxJQUFJLEVBQUUsRUFBRSxJQUFJLEVBQUUsTUFBTSxFQUFFLEtBQUssRUFBRSxDQUFDLEdBQUcsRUFBRSxFQUFFO0NBQ3RDLENBQUM7QUFFRixNQUFNLENBQUMsTUFBTSxxQkFBcUIsR0FBeUMsRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLElBQUksRUFBRSxNQUFNLEVBQUUsQ0FBQztBQUU5Ryx3R0FBd0c7QUFDeEcsTUFBTSxDQUFDLE1BQU0sMEJBQTBCLEdBQUcsQ0FBQyxTQUFpQyxFQUFFLE1BQWMsRUFBVSxFQUFFO0lBQ3RHLE1BQU0sRUFBRSxJQUFJLEVBQUUsS0FBSyxFQUFFLEdBQUcsMEJBQTBCLENBQUMsU0FBUyxDQUFDLENBQUM7SUFDOUQsTUFBTSxNQUFNLEdBQUcscUJBQXFCLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDM0MsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLE1BQU0sR0FBRyxNQUFNLENBQUMsR0FBRyxNQUFNLEdBQUcsQ0FBQyxLQUFLLEdBQUcsQ0FBQyxDQUFDLEdBQUcsTUFBTSxDQUFDO0FBQ3JFLENBQUMsQ0FBQztBQUVGOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxNQUFNLHFCQUFxQixHQUFHLEVBQUUsT0FBTyxFQUFFLE1BQU0sRUFBRSxZQUFZLEVBQUUsR0FBRyxFQUFXLENBQUM7QUFxQnJGOzs7R0FHRztBQUNILE1BQU0sQ0FBQyxNQUFNLHlCQUF5QixHQUFHLENBQUMsT0FBNkIsRUFBRSxRQUErQixFQUE0QixFQUFFO0lBQ3BJLElBQUksT0FBTyxJQUFJLFFBQVEsSUFBSSxRQUFRLENBQUMsS0FBSyxLQUFLLE9BQU8sQ0FBQyxLQUFLLElBQUksUUFBUSxDQUFDLE1BQU0sS0FBSyxPQUFPLENBQUMsTUFBTTtXQUM1RixRQUFRLENBQUMsUUFBUSxLQUFLLE9BQU8sQ0FBQyxRQUFRLEdBQUcsQ0FBQztRQUFFLE9BQU8sWUFBWSxDQUFDO0lBQ3JFLEtBQUssTUFBTSxNQUFNLElBQUksUUFBUSxDQUFDLE9BQU87UUFBRSxPQUFPLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsRUFBRSxFQUFFLE1BQU0sQ0FBQyxDQUFDO0lBQzlFLE9BQU8sQ0FBQyxRQUFRLEdBQUcsUUFBUSxDQUFDLFFBQVEsQ0FBQztJQUNyQyxPQUFPLFNBQVMsQ0FBQztBQUNuQixDQUFDLENBQUMifQ==
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agl",
3
- "version": "41.1.0",
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": "16.4.0",
38
- "@modelprofile.com/flexharness-agent": "16.4.0",
39
- "@modelprofile.com/flexharness-models": "16.4.0",
40
- "@modelprofile.com/flexharness-providers": "16.4.0",
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. The controller reads these only while a browser watches a usage window:
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; below them a line chart shows the tokens per minute by model, each
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); the backend's response id, its own headers and the rest of its response do
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
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '41.1.0',
6
+ version: '41.2.0',
7
7
  description: 'Agent Gateway Layer for OpenCode, FlexHarness, and Codex sessions'
8
8
  }
@@ -3765,7 +3765,7 @@ export const AGL_API_SURFACE: plugins.flexTypedRequest.IFlexTypedRequestSurface
3765
3765
  },
3766
3766
  "protocolVersion": {
3767
3767
  "type": "number",
3768
- "const": 46
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: 46;\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}"
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: usageArg.cacheReadTokens, cacheWriteTokens: usageArg.cacheWrite5mTokens + usageArg.cacheWrite1hTokens });
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 IInferenceTokenCounts {
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
- export interface IInferenceUsageDocument extends IInferenceUsageBucket {
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
- if (!value || typeof value !== 'object' || Object.keys(value).length !== 15
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, recordArg, 1); continue; }
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, calls: 1, inputTokens: recordArg.inputTokens,
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, bucket.calls);
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
- addCounts(modelArg, bucketArg, bucketArg.calls);
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: IInferenceUsageBucket): IControllerUsageBucket => ({ id: documentArg.id, span: documentArg.span,
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, calls: documentArg.calls, inputTokens: documentArg.inputTokens,
424
- outputTokens: documentArg.outputTokens, cacheReadTokens: documentArg.cacheReadTokens, cacheWriteTokens: documentArg.cacheWriteTokens });
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: IInferenceTokenCounts & { calls: number }, addArg: IInferenceTokenCounts, callsArg: number): void => {
427
- targetArg.calls += callsArg;
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', private readonly report: (usageArg: TTokenUsage) => void,
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) this.report(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) ?? 0;
384
- const cacheWrite = count(details?.cache_write_tokens) ?? 0;
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 > 0 ? counts : undefined;
405
+ return counts.inputTokens + counts.outputTokens + (counts.cacheReadTokens ?? 0) + (counts.cacheWriteTokens ?? 0) > 0
406
+ ? counts : undefined;
388
407
  }
389
408
  }
@@ -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 = 46 as const;
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. */
@@ -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
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: 'agl',
6
- version: '41.1.0',
6
+ version: '41.2.0',
7
7
  description: 'Agent Gateway Layer for OpenCode, FlexHarness, and Codex sessions'
8
8
  }
@@ -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 => {