llm-cli-gateway 2.16.0 → 2.17.1

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.
Files changed (66) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +12 -12
  4. package/dist/acp/client.js +5 -0
  5. package/dist/acp/flight-redaction.d.ts +2 -0
  6. package/dist/acp/flight-redaction.js +2 -0
  7. package/dist/acp/runtime.d.ts +1 -0
  8. package/dist/acp/runtime.js +20 -0
  9. package/dist/acp/types.d.ts +52 -0
  10. package/dist/acp/types.js +8 -0
  11. package/dist/api-provider.d.ts +1 -0
  12. package/dist/api-provider.js +1 -0
  13. package/dist/async-job-manager.d.ts +16 -0
  14. package/dist/async-job-manager.js +70 -22
  15. package/dist/claude-mcp-config.js +1 -1
  16. package/dist/compressor/transforms/ansi.js +12 -2
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.js +142 -7
  19. package/dist/db.js +4 -4
  20. package/dist/doctor.d.ts +34 -1
  21. package/dist/doctor.js +85 -3
  22. package/dist/executor.d.ts +5 -0
  23. package/dist/executor.js +11 -1
  24. package/dist/flight-recorder.d.ts +10 -0
  25. package/dist/flight-recorder.js +68 -2
  26. package/dist/http-transport.js +19 -21
  27. package/dist/index.d.ts +42 -3
  28. package/dist/index.js +666 -41
  29. package/dist/job-store.d.ts +10 -2
  30. package/dist/job-store.js +154 -43
  31. package/dist/lcr-priors.d.ts +60 -0
  32. package/dist/lcr-priors.js +190 -0
  33. package/dist/lcr-router-env.d.ts +20 -0
  34. package/dist/lcr-router-env.js +133 -0
  35. package/dist/lcr-telemetry.d.ts +2 -0
  36. package/dist/lcr-telemetry.js +17 -0
  37. package/dist/least-cost-router.d.ts +86 -0
  38. package/dist/least-cost-router.js +296 -0
  39. package/dist/least-cost-types.d.ts +34 -0
  40. package/dist/least-cost-types.js +1 -0
  41. package/dist/migrate-sessions.js +1 -1
  42. package/dist/migrate.js +1 -1
  43. package/dist/model-registry.js +1 -1
  44. package/dist/postgres-job-store-worker.js +56 -13
  45. package/dist/pricing.d.ts +17 -0
  46. package/dist/pricing.js +167 -0
  47. package/dist/provider-admin-tools.js +12 -4
  48. package/dist/provider-definitions.d.ts +4 -0
  49. package/dist/provider-definitions.js +33 -5
  50. package/dist/provider-tool-capabilities.js +6 -9
  51. package/dist/request-helpers.d.ts +3 -4
  52. package/dist/request-helpers.js +9 -10
  53. package/dist/resources.d.ts +37 -2
  54. package/dist/resources.js +96 -1
  55. package/dist/retry.d.ts +1 -0
  56. package/dist/retry.js +1 -1
  57. package/dist/token-estimator.d.ts +6 -0
  58. package/dist/token-estimator.js +59 -0
  59. package/dist/upstream-contracts.d.ts +4 -1
  60. package/dist/upstream-contracts.js +255 -71
  61. package/dist/validation-receipt.js +1 -1
  62. package/dist/validation-tools.d.ts +6 -0
  63. package/dist/validation-tools.js +174 -54
  64. package/npm-shrinkwrap.json +2 -2
  65. package/package.json +14 -5
  66. package/setup/status.schema.json +68 -0
@@ -0,0 +1,123 @@
1
+ ---
2
+ name: least-cost-routing
3
+ description: Route a model-agnostic request to the cheapest capable (provider, model) via route_request, subject to a quality-tier floor and a hard budget cap. Use when you do not care WHICH model runs a task, only that it is cheap enough and good enough.
4
+ metadata:
5
+ author: verivus-oss
6
+ version: "1.0"
7
+ ---
8
+
9
+ # Least-cost routing (route_request)
10
+
11
+ `route_request` (sync) and `route_request_async` (async) pick the **cheapest
12
+ eligible `(provider, model)`** candidate that meets your constraints, then
13
+ dispatch it through the normal provider path. They are **dormant by default**:
14
+ registered only when `[least_cost].enabled = true` in
15
+ `~/.llm-cli-gateway/config.toml`. If the tools are absent, routing is off; use a
16
+ specific provider tool instead.
17
+
18
+ ## When to use this vs a named provider tool
19
+
20
+ - Use `route_request` when the task is model-agnostic (summarize, answer,
21
+ classify, rewrite) and you want the cheapest model that clears a quality floor.
22
+ - Use `claude_request` / `codex_request` / etc. when you need a SPECIFIC model or
23
+ provider, a provider-specific flag, session resume, or a worktree. In phase_1
24
+ `route_request` is fresh one-shot only (no `sessionId` / `workspace` /
25
+ `worktree`).
26
+
27
+ ## Inputs
28
+
29
+ - `prompt` (or `promptParts`): the request. Exactly one.
30
+ - `minTier`: `economy | standard | frontier` (default `standard`). A hard floor,
31
+ never downgraded to save money.
32
+ - `maxCostUsd`: per-request budget cap. Over-budget fails closed.
33
+ - `expectedOutputTokens` / `maxOutputTokens`: tune the ranking and the
34
+ conservative budget bound.
35
+ - `requiredCapabilities`: `{ images?, attachments?, toolCalling?, jsonSchema?,
36
+ outputFormat?, effort? }`. A candidate missing a required capability is
37
+ excluded.
38
+ - `candidates`: an explicit `(provider, model)[]` to restrict the pool (also
39
+ whitelists otherwise-untiered / maintain-only candidates like cursor/devin).
40
+ - `allowUnpriced` + `budgetWaiver`: BOTH are required to admit an unpriced
41
+ (`source: "unknown"`) candidate. An unpriced candidate always ranks strictly
42
+ last and cannot win over any priced candidate.
43
+ - `fallback`: a `(provider, model)` used ONLY when the eligible pool is empty;
44
+ bypasses cost ranking but still passes eligibility and (unless `budgetWaiver`)
45
+ the budget gate.
46
+
47
+ ## Reading the result
48
+
49
+ Every routed response carries a `routing` block in `structuredContent.routing`,
50
+ and a one-line `[routing] chosen=... est=$... (basis, confidence) considered=N
51
+ reroutes=M` banner prepended to the text. Fields: `chosen`, `tier`,
52
+ `estCostUsd`, `costBasis` (`provider-reported | derived-from-tokens |
53
+ pre-flight-estimate`), `confidence`, `nearTie`, `estInputTokens`,
54
+ `estOutputTokens`, `priceAsOf`, `priceSource`, `consideredCount`,
55
+ `rejected: [{candidate, reason}]`, `reroutes`, and (on failure) `error`.
56
+
57
+ `estCostUsd` is always an ESTIMATE labelled with its inputs, never a billed cost.
58
+
59
+ ## Failure semantics (fail closed)
60
+
61
+ - Over budget: `routing.error = "BudgetExceeded"`, `isError: true`. Raise
62
+ `maxCostUsd` rather than expecting a silent downgrade.
63
+ - No eligible candidate: `routing.error = "NoEligibleCandidate"` with the
64
+ per-candidate rejection reasons (auth / breaker / capacity / capability / tier
65
+ / price / budget). Loosen `minTier`, add `candidates`, or set
66
+ `allowUnpriced` + `budgetWaiver`.
67
+ - Transient dispatch failure (breaker trip, timeout): LCR re-selects over the
68
+ remaining pool up to `[least_cost].max_reroutes`. Non-transient failures drop
69
+ the candidate and continue.
70
+
71
+ ## Observability (phase_2)
72
+
73
+ When routing is enabled, three read-only surfaces expose how it is behaving
74
+ (all economics-only, no prompts/secrets/principal):
75
+
76
+ - `routing://decisions` (MCP resource): the recent routed decisions, each with
77
+ `provider`/`model`, `estCostUsd`, `costBasis`, `confidence`, `reason`,
78
+ `considered`, `reroutes`, `at`.
79
+ - `routing://priors` (MCP resource): the learned per-`(provider,model)`
80
+ output-token priors (median/p90/samples), the per-`(content-type,family)`
81
+ input calibration `k` + sample count + confidence, and `priceAsOf`. Priors are
82
+ anonymized model-level economics; `priors_scope` (`global | principal | off`)
83
+ scopes the learning (or disables it).
84
+ - `llm_process_health` gains a `leastCost` block: per-provider telemetry tier
85
+ (T1..T4) and per-candidate eligibility (priced? / authed? / breaker? / tier?).
86
+ - `doctor --json` gains a `least_cost` block: pricing `asOf` + staleness,
87
+ per-provider telemetry tier, per-candidate eligibility, untiered models, and
88
+ per-`(content-type,family)` calibration quality (`k`/samples/confidence).
89
+
90
+ API-provider models are priced from a published catalog (`prefer_catalog_price`
91
+ picks catalog over the CLI table when both resolve).
92
+
93
+ ## Cheapest-reviewer selection (phase_3)
94
+
95
+ `validate_with_models`, `red_team_review`, `consensus_check`, `second_opinion`,
96
+ and `ask_model` accept an OPT-IN `select: "cheapest" | "cheapest_per_tier"`. When
97
+ omitted, the explicit `models`/`model` list is used verbatim (default unchanged).
98
+ When set, the reviewer target list is filled via the LCR selector:
99
+ `cheapest` picks the single cheapest eligible provider; `cheapest_per_tier` picks
100
+ the cheapest provider in each quality tier (economy/standard/frontier). It fails
101
+ closed (no jobs started) when `[least_cost].enabled` is false or nothing is
102
+ eligible.
103
+
104
+ ## Config (operator, `~/.llm-cli-gateway/config.toml`)
105
+
106
+ ```toml
107
+ [least_cost]
108
+ enabled = true # nothing routes until this is true
109
+ min_tier = "standard"
110
+ max_cost_usd = 0.50
111
+ allow_unpriced = false
112
+ max_reroutes = 2
113
+
114
+ [least_cost.tiers] # (provider:family) -> tier; ships with defaults
115
+ "claude:claude-opus" = "frontier"
116
+
117
+ [least_cost.candidates]
118
+ allow = [] # empty = all eligible
119
+ deny = []
120
+ ```
121
+
122
+ See `docs/least-cost-routing-contract.md` for the frozen decisions and
123
+ invariants.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,43 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.17.1] - 2026-07-13
8
+
9
+ ### Fixed
10
+
11
+ - **Upstream CLI contract refresh.** Updated installed-provider contracts for
12
+ Claude, Codex, Antigravity, Grok, Mistral Vibe, Devin, and Cursor Agent.
13
+ Vibe now emits its supported `--disabled-tools` denylist control instead of
14
+ silently ignoring `disallowedTools`.
15
+
16
+ ## [2.17.0] - 2026-07-13
17
+
18
+ ### Added
19
+
20
+ - **Least-cost routing.** `route_request` and `route_request_async` select the
21
+ cheapest eligible provider and model within a quality floor, budget cap, and
22
+ current capacity constraints. The router now exposes pricing, estimates,
23
+ route decisions, health, and learned cost priors through `routing://`
24
+ resources.
25
+ - **Cheapest eligible validation.** `validate_with_models` can now select a
26
+ least-cost eligible model rather than requiring callers to name one.
27
+ - **Supply-chain release guards.** Dependency drift, tag-along packages,
28
+ unexpected install scripts, licenses, and policy changes are checked before
29
+ release, with fail-closed protection for uncertain findings.
30
+
31
+ ### Fixed
32
+
33
+ - **Durable Postgres job-store recovery.** Replaced temporary-file worker
34
+ result handoff with in-process message channels, preserving failure detail
35
+ and avoiding host `/tmp` inode exhaustion. A failed bridge now retires and
36
+ recreates its worker safely, and durable admission recovers only after a
37
+ successful re-registration.
38
+ - **Async availability reporting and lifecycle.** Readiness and process health
39
+ now reflect durable-admission state, configured-store startup failures exit
40
+ for supervised recovery, and shutdown reliably closes the job store.
41
+ - **Release and test gates.** The lint command now checks the actual source
42
+ tree, and Postgres integration tests work with either Docker or Podman.
43
+
7
44
  ## [2.16.0] - 2026-07-10
8
45
 
9
46
  ### Added
package/README.md CHANGED
@@ -97,7 +97,7 @@ The next documentation focus is provider-specific skill and DAG-TOML pairs for e
97
97
  - CI runs build, lint, format, tests, package checks, and npm audit.
98
98
  - Security CI runs actionlint, zizmor, shellcheck, typos, osv-scanner, gitleaks, and lychee.
99
99
  - GitHub release installer artifacts are checksummed and signed with Sigstore keyless signing.
100
- - npm releases use a generated prod-only shrinkwrap and release security audit; the publish token is fetched at runtime from Azure Key Vault through GitHub OIDC to Entra.
100
+ - npm releases use a generated prod-only shrinkwrap and release security audit; GitHub Actions Trusted Publishing exchanges the job's OIDC identity for short-lived npm publish credentials.
101
101
  - The npm package intentionally ships a generated, prod-only `npm-shrinkwrap.json` so registry installs resolve the audited release tree. Release gates regenerate it from `package-lock.json`, compare for parity, and run a registry-fidelity consumer install before publishing.
102
102
  - Socket behavioural alerts are documented in [`socket.yml`](./socket.yml) and under "Security Considerations" below. `shellAccess` and `shrinkwrap` are reviewed package capabilities/configuration for this CLI appliance, not hidden install behaviour.
103
103
 
@@ -370,12 +370,12 @@ Vibe-specific notes:
370
370
  `default | plan | accept-edits | auto-approve`; Vibe also accepts install-gated
371
371
  builtins (e.g. `lean`) and custom agents from `~/.vibe/agents`, so any name is
372
372
  passed through and Vibe validates availability. The gateway's
373
- programmatic-mode default is `auto-approve`; pick a stricter mode
374
- explicitly if you need approval gates.
375
- - **`allowedTools` is allow-list only** the gateway emits one
376
- `--enabled-tools <tool>` flag per entry. `disallowedTools` is accepted in
377
- the schema for caller-side parity but is silently ignored at the CLI
378
- boundary (a `logger.info` warning records the no-op).
373
+ programmatic-mode default is `accept-edits`; use `auto-approve` only as an
374
+ explicit opt-in.
375
+ - **Tool controls use Vibe's native flags.** The gateway emits one
376
+ `--enabled-tools <tool>` flag per `allowedTools` entry and one
377
+ `--disabled-tools <tool>` flag per `disallowedTools` entry. Vibe applies
378
+ disabled tools after enabled-tool filtering.
379
379
  - **No self-update**: `cli_upgrade --cli mistral` detects whether you used
380
380
  pip / uv / brew and dispatches the matching upgrade command. Running
381
381
  `vibe update` is not a thing.
@@ -992,9 +992,9 @@ Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but wit
992
992
 
993
993
  - `model` (string, optional): Vibe model alias (for example `mistral-medium-3.5` or `latest`). The resolved value is injected via the `VIBE_ACTIVE_MODEL` environment variable; omit it to let the gateway discover Vibe config and avoid stale hardcoded defaults.
994
994
  - `transport` (string, optional): `"cli"` (default) runs the Vibe CLI; `"acp"` routes through Vibe's native `vibe-acp` transport when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise). Sync-only: `mistral_request_async` always runs the CLI transport and does not accept `transport`
995
- - `permissionMode`: the Vibe `--agent` name builtins `default | plan | accept-edits | auto-approve`, or any install-gated/custom agent. Emitted as `--agent <name>`. Defaults to `auto-approve` in programmatic mode.
996
- - `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry (allow-list only).
997
- - `disallowedTools` (string[], optional): Accepted for parity with the other providers; ignored at the CLI boundary with a logged warning.
995
+ - `permissionMode`: the Vibe `--agent` name: builtins `default | plan | accept-edits | auto-approve`, or any install-gated/custom agent. Emitted as `--agent <name>`. Defaults to `accept-edits` in programmatic mode; use `auto-approve` only as an explicit opt-in.
996
+ - `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry.
997
+ - `disallowedTools` (string[], optional): One `--disabled-tools <tool>` flag per entry, applied after the enabled-tool filter.
998
998
  - `outputFormat` (string, optional): Vibe 2.x values are `"text"`, `"json"`, or `"streaming"`; legacy aliases `"plain"` and `"stream-json"` are accepted and normalized before spawn.
999
999
  - `sessionId` / `resumeLatest` / `createNewSession`: standard session controls. Current Vibe defaults session logging to enabled; if an older config has `[session_logging] enabled = false`, `doctor --json` surfaces an actionable next-action.
1000
1000
  - `trust` (boolean, optional): Emit `--trust` so Vibe trusts the cwd for this invocation only (not persisted; skips the interactive trust prompt)
@@ -1023,7 +1023,7 @@ Run a Cognition Devin CLI request synchronously (headless print mode, `devin -p`
1023
1023
  - `model` (string, optional): Model name or alias (e.g. `opus`, `latest`)
1024
1024
  - `transport` (string, optional): `"cli"` (default) runs the Devin CLI; `"acp"` routes through Devin's native `devin acp` transport when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise). Sync-only: `devin_request_async` always runs the CLI transport and accepts neither `transport` nor `agentType`
1025
1025
  - `agentType` (string, optional): ACP agent variant for `transport: "acp"` (`devin acp --agent-type`): `"summarizer"` (no tools, text summary) or `"review"` (read-only plus shell code-review); ignored for the CLI transport
1026
- - `permissionMode` (string, optional): Devin CLI permission mode (`--permission-mode`): `auto` (auto-approves read-only tools), `smart` (also auto-runs actions a fast model judges safe), `dangerous` (auto-approves all). Omit to use Devin's headless default
1026
+ - `permissionMode` (string, optional): Devin CLI permission mode (`--permission-mode`): `auto` (auto-approves read-only tools), `accept-edits` (also auto-approves workspace edits), `smart` (also auto-runs actions a fast model judges safe), `dangerous` (auto-approves all). Omit to use Devin's headless default
1027
1027
  - `promptFile` (string, optional): Load the initial prompt from a file (`--prompt-file`)
1028
1028
  - `sessionId` (string, optional): Devin session ID to resume (`--resume <id>`). The `gw-*` id minted for a brand-new session is not resumable via `sessionId`; continue with `resumeLatest: true`
1029
1029
  - `resumeLatest` (boolean, optional): Resume the most recent Devin session in cwd (`--continue`)
@@ -1653,7 +1653,7 @@ The gateway supports concurrent requests across different CLIs. Each request spa
1653
1653
  - **Command Execution**: Uses `spawn` with separate arguments (not shell execution)
1654
1654
  - **No Eval**: No dynamic code evaluation in our source (see "Socket alerts" below for the transitive `ajv` codegen case)
1655
1655
  - **Sandboxing**: Consider running in containers for production use
1656
- - **npm publish control**: npm releases are gated by the generated prod-only shrinkwrap, release security audit, packed-consumer checks, and a publish token fetched at runtime from Azure Key Vault through GitHub OIDC to Entra
1656
+ - **npm publish control**: npm releases are gated by the generated prod-only shrinkwrap, release security audit, packed-consumer checks, and GitHub Actions Trusted Publishing with short-lived OIDC-derived npm publish credentials
1657
1657
  - **Release signing**: GitHub release installer artifacts are signed with Sigstore keyless signing; verify `SHA256SUMS.sigstore.json` before trusting the checksum file
1658
1658
 
1659
1659
  ### Socket alerts — context for reviewers
@@ -1,6 +1,7 @@
1
1
  import { AcpMethodUnsupportedError, AcpMutatingDisabledError, AcpProtocolError, isAcpError, } from "./errors.js";
2
2
  import { deriveAcpMethodAvailability, parseCloseSessionResponse, parseDeleteSessionResponse, parseInitializeResponse, parseListSessionsResponse, parseReadTextFileRequest, parseRequestPermissionRequest, parseSessionLoadResponse, parseSessionNewResponse, parseSessionPromptResponse, parseSessionResumeResponse, parseSessionUpdateNotification, parseSetSessionConfigOptionResponse, parseSetSessionModeResponse, parseWriteTextFileRequest, sessionResponseMethods, } from "./types.js";
3
3
  import { noopLogger } from "../logger.js";
4
+ import { gatewayVersion } from "../provider-capability-discovery.js";
4
5
  export const DEFAULT_ACP_PROTOCOL_VERSION = 1;
5
6
  export class AcpClient {
6
7
  transport;
@@ -47,6 +48,10 @@ export class AcpClient {
47
48
  }
48
49
  const params = {
49
50
  protocolVersion: this.protocolVersion,
51
+ clientInfo: {
52
+ name: "llm-cli-gateway",
53
+ version: gatewayVersion(),
54
+ },
50
55
  clientCapabilities: {
51
56
  fs: {
52
57
  readTextFile: options.readTextFile ?? false,
@@ -24,5 +24,7 @@ export interface AcpFlightResultParams {
24
24
  readonly errorMessage?: string;
25
25
  readonly providerSessionId?: string;
26
26
  readonly stopReason?: string;
27
+ readonly costUsd?: number;
28
+ readonly costBasis?: string;
27
29
  }
28
30
  export declare function buildAcpFlightResult(params: AcpFlightResultParams): FlightLogResult;
@@ -39,5 +39,7 @@ export function buildAcpFlightResult(params) {
39
39
  errorMessage: params.errorMessage !== undefined ? redactAcpTextForFlight(params.errorMessage) : undefined,
40
40
  providerSessionId: params.providerSessionId,
41
41
  stopReason: params.stopReason,
42
+ costUsd: params.costUsd,
43
+ costBasis: params.costBasis,
42
44
  };
43
45
  }
@@ -38,6 +38,7 @@ export interface AcpPromptUsage {
38
38
  inputTokens?: number;
39
39
  outputTokens?: number;
40
40
  cacheReadTokens?: number;
41
+ reasoningTokens?: number;
41
42
  }
42
43
  export declare function extractAcpPromptUsage(meta: unknown): AcpPromptUsage;
43
44
  export declare function runAcpRequest(deps: AcpRuntimeDeps, req: AcpRunRequest): Promise<AcpRunResult>;
@@ -6,6 +6,7 @@ import { createAcpPermissionDecider } from "./permission-bridge.js";
6
6
  import { AcpProcessManager } from "./process-manager.js";
7
7
  import { createAcpSession, recordAcpSessionInfo, resolveAcpResume } from "./session-map.js";
8
8
  import { DEVIN_ACP_AGENT_TYPES } from "../provider-definitions.js";
9
+ import { composeCost, getModelCost } from "../pricing.js";
9
10
  import { noopLogger } from "../logger.js";
10
11
  export function extractAcpPromptUsage(meta) {
11
12
  if (!meta || typeof meta !== "object" || Array.isArray(meta))
@@ -22,6 +23,9 @@ export function extractAcpPromptUsage(meta) {
22
23
  const cacheReadTokens = num(record.cachedReadTokens);
23
24
  if (cacheReadTokens !== undefined)
24
25
  usage.cacheReadTokens = cacheReadTokens;
26
+ const reasoningTokens = num(record.reasoningTokens);
27
+ if (reasoningTokens !== undefined)
28
+ usage.reasoningTokens = reasoningTokens;
25
29
  return usage;
26
30
  }
27
31
  export async function runAcpRequest(deps, req) {
@@ -120,6 +124,20 @@ export async function runAcpRequest(deps, req) {
120
124
  const text = normalizer.finalText;
121
125
  const durationMs = elapsed();
122
126
  const usage = extractAcpPromptUsage(promptResult._meta);
127
+ let costUsd;
128
+ let costBasis;
129
+ const modelCost = getModelCost(req.provider, req.model ?? "default");
130
+ if (modelCost.source !== "unknown" &&
131
+ (usage.inputTokens !== undefined || usage.outputTokens !== undefined)) {
132
+ const composed = composeCost({
133
+ inputTokens: usage.inputTokens ?? 0,
134
+ outputTokens: usage.outputTokens ?? 0,
135
+ cacheReadTokens: usage.cacheReadTokens,
136
+ reasoningTokens: usage.reasoningTokens,
137
+ }, { estInputTokens: 0, estOutputTokens: 0 }, modelCost);
138
+ costUsd = composed.costUsd;
139
+ costBasis = composed.cost_basis;
140
+ }
123
141
  deps.flightRecorder?.logComplete(req.correlationId, buildAcpFlightResult({
124
142
  responseText: text,
125
143
  durationMs,
@@ -130,6 +148,8 @@ export async function runAcpRequest(deps, req) {
130
148
  inputTokens: usage.inputTokens,
131
149
  outputTokens: usage.outputTokens,
132
150
  cacheReadTokens: usage.cacheReadTokens,
151
+ costUsd,
152
+ costBasis,
133
153
  }));
134
154
  logger.info("acp.request.success", { provider: req.provider, durationMs });
135
155
  return {
@@ -54,8 +54,34 @@ export declare const ClientCapabilitiesSchema: z.ZodObject<{
54
54
  }, z.ZodTypeAny, "passthrough">>>;
55
55
  terminal: z.ZodOptional<z.ZodBoolean>;
56
56
  }, z.ZodTypeAny, "passthrough">>;
57
+ export declare const ClientInfoSchema: z.ZodObject<{
58
+ name: z.ZodString;
59
+ version: z.ZodString;
60
+ title: z.ZodOptional<z.ZodString>;
61
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
62
+ name: z.ZodString;
63
+ version: z.ZodString;
64
+ title: z.ZodOptional<z.ZodString>;
65
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
66
+ name: z.ZodString;
67
+ version: z.ZodString;
68
+ title: z.ZodOptional<z.ZodString>;
69
+ }, z.ZodTypeAny, "passthrough">>;
57
70
  export declare const InitializeRequestSchema: z.ZodObject<{
58
71
  protocolVersion: z.ZodNumber;
72
+ clientInfo: z.ZodOptional<z.ZodObject<{
73
+ name: z.ZodString;
74
+ version: z.ZodString;
75
+ title: z.ZodOptional<z.ZodString>;
76
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
77
+ name: z.ZodString;
78
+ version: z.ZodString;
79
+ title: z.ZodOptional<z.ZodString>;
80
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
81
+ name: z.ZodString;
82
+ version: z.ZodString;
83
+ title: z.ZodOptional<z.ZodString>;
84
+ }, z.ZodTypeAny, "passthrough">>>;
59
85
  clientCapabilities: z.ZodOptional<z.ZodObject<{
60
86
  fs: z.ZodOptional<z.ZodObject<{
61
87
  readTextFile: z.ZodOptional<z.ZodBoolean>;
@@ -96,6 +122,19 @@ export declare const InitializeRequestSchema: z.ZodObject<{
96
122
  _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
97
123
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
98
124
  protocolVersion: z.ZodNumber;
125
+ clientInfo: z.ZodOptional<z.ZodObject<{
126
+ name: z.ZodString;
127
+ version: z.ZodString;
128
+ title: z.ZodOptional<z.ZodString>;
129
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
130
+ name: z.ZodString;
131
+ version: z.ZodString;
132
+ title: z.ZodOptional<z.ZodString>;
133
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
134
+ name: z.ZodString;
135
+ version: z.ZodString;
136
+ title: z.ZodOptional<z.ZodString>;
137
+ }, z.ZodTypeAny, "passthrough">>>;
99
138
  clientCapabilities: z.ZodOptional<z.ZodObject<{
100
139
  fs: z.ZodOptional<z.ZodObject<{
101
140
  readTextFile: z.ZodOptional<z.ZodBoolean>;
@@ -136,6 +175,19 @@ export declare const InitializeRequestSchema: z.ZodObject<{
136
175
  _meta: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
137
176
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
138
177
  protocolVersion: z.ZodNumber;
178
+ clientInfo: z.ZodOptional<z.ZodObject<{
179
+ name: z.ZodString;
180
+ version: z.ZodString;
181
+ title: z.ZodOptional<z.ZodString>;
182
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
183
+ name: z.ZodString;
184
+ version: z.ZodString;
185
+ title: z.ZodOptional<z.ZodString>;
186
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
187
+ name: z.ZodString;
188
+ version: z.ZodString;
189
+ title: z.ZodOptional<z.ZodString>;
190
+ }, z.ZodTypeAny, "passthrough">>>;
139
191
  clientCapabilities: z.ZodOptional<z.ZodObject<{
140
192
  fs: z.ZodOptional<z.ZodObject<{
141
193
  readTextFile: z.ZodOptional<z.ZodBoolean>;
package/dist/acp/types.js CHANGED
@@ -50,9 +50,17 @@ export const ClientCapabilitiesSchema = z
50
50
  terminal: z.boolean().optional(),
51
51
  })
52
52
  .passthrough();
53
+ export const ClientInfoSchema = z
54
+ .object({
55
+ name: z.string().min(1),
56
+ version: z.string().min(1),
57
+ title: z.string().min(1).optional(),
58
+ })
59
+ .passthrough();
53
60
  export const InitializeRequestSchema = z
54
61
  .object({
55
62
  protocolVersion: ProtocolVersionSchema,
63
+ clientInfo: ClientInfoSchema.optional(),
56
64
  clientCapabilities: ClientCapabilitiesSchema.optional(),
57
65
  _meta: MetaSchema,
58
66
  })
@@ -25,6 +25,7 @@ export interface ApiUsage {
25
25
  inputTokens?: number;
26
26
  outputTokens?: number;
27
27
  cacheReadTokens?: number;
28
+ cacheCreationTokens?: number;
28
29
  costUsd?: number;
29
30
  raw?: unknown;
30
31
  }
@@ -126,6 +126,7 @@ export class AnthropicProvider {
126
126
  inputTokens: firstNumber(usage.input_tokens),
127
127
  outputTokens: firstNumber(usage.output_tokens),
128
128
  cacheReadTokens: firstNumber(usage.cache_read_input_tokens),
129
+ cacheCreationTokens: firstNumber(usage.cache_creation_input_tokens),
129
130
  raw: usage,
130
131
  },
131
132
  raw: parsed,
@@ -37,6 +37,7 @@ export interface JobLimiterSnapshot {
37
37
  timedOut: number;
38
38
  saturated: boolean;
39
39
  }
40
+ export declare function providerAtCapacity(snapshot: JobLimiterSnapshot, provider: string): boolean;
40
41
  export interface AsyncJobFlightRecorderEntry {
41
42
  model: string;
42
43
  prompt: string;
@@ -53,6 +54,7 @@ export type AsyncJobUsageExtractor = (stdout: string) => {
53
54
  cacheReadTokens?: number;
54
55
  cacheCreationTokens?: number;
55
56
  costUsd?: number;
57
+ costBasis?: string;
56
58
  };
57
59
  export interface AsyncJobSnapshot {
58
60
  id: string;
@@ -120,12 +122,25 @@ export declare class AsyncJobManager {
120
122
  private sweepTimer;
121
123
  private durableAdmission;
122
124
  private consecutiveHeartbeatFailures;
125
+ private consecutiveHeartbeatSuccesses;
126
+ private lastHeartbeatFailureAt;
127
+ private lastHeartbeatRecoveryAt;
128
+ private lastHeartbeatErrorName;
123
129
  private skipSweepThisCycle;
124
130
  private nextHeartbeatExpectedAt;
125
131
  private disposed;
126
132
  private readonly pendingWrites;
127
133
  constructor(logger?: Logger, onJobComplete?: ((cli: JobProvider, durationMs: number, success: boolean) => void) | undefined, store?: JobStore | null, flightRecorder?: FlightRecorderLike, limits?: JobLimitsConfig, _deprecatedOwnsOrphanRecovery?: boolean, leaseConfig?: LeaseRuntimeConfig);
128
134
  canAdmitDurableJobs(): boolean;
135
+ getDurableAdmissionHealth(): {
136
+ storeAttached: boolean;
137
+ admitting: boolean;
138
+ consecutiveHeartbeatFailures: number;
139
+ consecutiveHeartbeatSuccesses: number;
140
+ lastHeartbeatFailureAt: string | null;
141
+ lastHeartbeatRecoveryAt: string | null;
142
+ lastHeartbeatErrorName: string | null;
143
+ };
129
144
  private assertDurableAdmission;
130
145
  private trackPendingWrite;
131
146
  dispose(opts?: {
@@ -136,6 +151,7 @@ export declare class AsyncJobManager {
136
151
  private onHeartbeatTick;
137
152
  runOrphanSweepNow(): void;
138
153
  private startReaper;
154
+ private restoreDurableAdmission;
139
155
  private runOrphanSweep;
140
156
  private confirmLiveProcessCandidates;
141
157
  private recordStartOrFailClosed;