llm-cli-gateway 2.15.0 → 2.17.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.
Files changed (80) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +78 -1
  3. package/dist/acp/client.js +5 -0
  4. package/dist/acp/flight-redaction.d.ts +2 -0
  5. package/dist/acp/flight-redaction.js +2 -0
  6. package/dist/acp/process-manager.js +2 -1
  7. package/dist/acp/provider-registry.js +8 -8
  8. package/dist/acp/runtime.d.ts +1 -0
  9. package/dist/acp/runtime.js +20 -0
  10. package/dist/acp/types.d.ts +52 -0
  11. package/dist/acp/types.js +8 -0
  12. package/dist/api-provider.d.ts +1 -0
  13. package/dist/api-provider.js +1 -0
  14. package/dist/async-job-manager.d.ts +20 -1
  15. package/dist/async-job-manager.js +85 -28
  16. package/dist/claude-mcp-config.js +1 -1
  17. package/dist/compressor/estimate.d.ts +5 -0
  18. package/dist/compressor/estimate.js +21 -0
  19. package/dist/compressor/index.d.ts +23 -0
  20. package/dist/compressor/index.js +77 -0
  21. package/dist/compressor/router.d.ts +2 -0
  22. package/dist/compressor/router.js +57 -0
  23. package/dist/compressor/transforms/ansi.d.ts +3 -0
  24. package/dist/compressor/transforms/ansi.js +99 -0
  25. package/dist/compressor/transforms/json.d.ts +1 -0
  26. package/dist/compressor/transforms/json.js +156 -0
  27. package/dist/compressor/transforms/log.d.ts +12 -0
  28. package/dist/compressor/transforms/log.js +55 -0
  29. package/dist/compressor/transforms/whitespace.d.ts +8 -0
  30. package/dist/compressor/transforms/whitespace.js +79 -0
  31. package/dist/config.d.ts +39 -0
  32. package/dist/config.js +166 -6
  33. package/dist/db.js +4 -4
  34. package/dist/doctor.d.ts +34 -1
  35. package/dist/doctor.js +85 -3
  36. package/dist/executor.d.ts +6 -0
  37. package/dist/executor.js +16 -3
  38. package/dist/flight-recorder.d.ts +19 -0
  39. package/dist/flight-recorder.js +110 -2
  40. package/dist/http-transport.js +19 -21
  41. package/dist/index.d.ts +58 -2
  42. package/dist/index.js +929 -94
  43. package/dist/job-store.d.ts +14 -2
  44. package/dist/job-store.js +170 -43
  45. package/dist/lcr-priors.d.ts +60 -0
  46. package/dist/lcr-priors.js +190 -0
  47. package/dist/lcr-router-env.d.ts +20 -0
  48. package/dist/lcr-router-env.js +133 -0
  49. package/dist/lcr-telemetry.d.ts +2 -0
  50. package/dist/lcr-telemetry.js +17 -0
  51. package/dist/least-cost-router.d.ts +86 -0
  52. package/dist/least-cost-router.js +296 -0
  53. package/dist/least-cost-types.d.ts +34 -0
  54. package/dist/least-cost-types.js +1 -0
  55. package/dist/migrate-sessions.js +1 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/model-registry.js +1 -1
  58. package/dist/postgres-job-store-worker.js +56 -13
  59. package/dist/pricing.d.ts +17 -0
  60. package/dist/pricing.js +167 -0
  61. package/dist/provider-definitions.d.ts +5 -0
  62. package/dist/provider-definitions.js +56 -10
  63. package/dist/provider-tool-capabilities.js +3 -3
  64. package/dist/request-helpers.d.ts +2 -2
  65. package/dist/request-helpers.js +1 -1
  66. package/dist/resources.d.ts +37 -2
  67. package/dist/resources.js +96 -1
  68. package/dist/retry.d.ts +1 -0
  69. package/dist/retry.js +1 -1
  70. package/dist/spawn-env-isolation.d.ts +10 -0
  71. package/dist/spawn-env-isolation.js +55 -0
  72. package/dist/token-estimator.d.ts +6 -0
  73. package/dist/token-estimator.js +59 -0
  74. package/dist/upstream-contracts.js +48 -28
  75. package/dist/validation-receipt.js +1 -1
  76. package/dist/validation-tools.d.ts +6 -0
  77. package/dist/validation-tools.js +174 -54
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +15 -6
  80. 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,83 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.17.0] - 2026-07-13
8
+
9
+ ### Added
10
+
11
+ - **Least-cost routing.** `route_request` and `route_request_async` select the
12
+ cheapest eligible provider and model within a quality floor, budget cap, and
13
+ current capacity constraints. The router now exposes pricing, estimates,
14
+ route decisions, health, and learned cost priors through `routing://`
15
+ resources.
16
+ - **Cheapest eligible validation.** `validate_with_models` can now select a
17
+ least-cost eligible model rather than requiring callers to name one.
18
+ - **Supply-chain release guards.** Dependency drift, tag-along packages,
19
+ unexpected install scripts, licenses, and policy changes are checked before
20
+ release, with fail-closed protection for uncertain findings.
21
+
22
+ ### Fixed
23
+
24
+ - **Durable Postgres job-store recovery.** Replaced temporary-file worker
25
+ result handoff with in-process message channels, preserving failure detail
26
+ and avoiding host `/tmp` inode exhaustion. A failed bridge now retires and
27
+ recreates its worker safely, and durable admission recovers only after a
28
+ successful re-registration.
29
+ - **Async availability reporting and lifecycle.** Readiness and process health
30
+ now reflect durable-admission state, configured-store startup failures exit
31
+ for supervised recovery, and shutdown reliably closes the job store.
32
+ - **Release and test gates.** The lint command now checks the actual source
33
+ tree, and Postgres integration tests work with either Docker or Podman.
34
+
35
+ ## [2.16.0] - 2026-07-10
36
+
37
+ ### Added
38
+
39
+ - **Native outbound display-text compressor, PR-1 (#151).** A built-in,
40
+ provider-agnostic compressor that shrinks provider display text before it is
41
+ returned to the caller. It is ANSI- and fence-aware (protects code fences,
42
+ inline code, OSC sequences, and wide characters from lossy rewriting) and is
43
+ **off by default**; opt in per the compressor configuration. This is the first
44
+ slice; response-side wiring and API-provider coverage are named follow-ups.
45
+ - **Site machine-discovery generation (#169).** The machine-readable discovery
46
+ artifacts under `site/` (`llms.txt`, `.well-known/*`, `tools.md`,
47
+ `openapi.json`, ...) are now generated from the live MCP tool surface by
48
+ `scripts/generate-site-discovery.mjs`. Regenerate with `npm run site:generate`;
49
+ `npm run site:generate:check` + `npm run site:validate` gate them in
50
+ `npm run check` so the published surface can never silently drift from the
51
+ tools the server actually exposes.
52
+
53
+ ### Changed
54
+
55
+ - **Vibe/Mistral no longer defaults to `--agent auto-approve` (#155).** Under the
56
+ default `legacy` approval strategy, Vibe was spawned in auto-approve ("YOLO"),
57
+ auto-executing every tool call including shell. It now defaults to
58
+ `--agent accept-edits` (auto-accept file edits; dangerous ops such as shell stay
59
+ gated, and Vibe denies rather than blocks on them in programmatic mode), matching
60
+ the `mcp_managed` posture and the other providers' non-bypass defaults. This
61
+ shrinks the blast radius of a prompt-injected tool call. `auto-approve` remains
62
+ reachable deliberately: pass `permissionMode: "auto-approve"` on the request, or
63
+ set `LLM_GATEWAY_APPROVAL_ALLOW_BYPASS` operator-wide. **Behaviour change:**
64
+ `legacy` callers that relied on Vibe running shell unattended will now see the
65
+ gated op denied unless they adopt one of those opt-ins.
66
+ - **Provider contract and target-version refreshes (#157 through #171).**
67
+ `PROVIDER_TARGET_VERSIONS` is now the single source of truth for provider
68
+ target versions (#165), and every provider's declared upstream contract was
69
+ re-probed against the installed CLI and refreshed: codex-cli 0.144.1, claude
70
+ 2.1.206, grok 0.2.93, gemini/agy 1.1.0, mistral vibe 2.19.1 (with
71
+ `--disabled-tools` acknowledged, #170), cursor-agent 2026.07.09, and the devin
72
+ ACP target 3000.1.27. Internal metadata only; no request-time behaviour change.
73
+
74
+ ### Security
75
+
76
+ - **Opt-in isolation of provider child-process environment (#154).** A new
77
+ `LLM_GATEWAY_ISOLATE_SPAWN_ENV` opt-in scrubs the spawned provider CLI's
78
+ environment at the single `spawnCliProcess` chokepoint, shrinking the blast
79
+ radius of a hostile provider attempting to exfiltrate redirected/inherited
80
+ environment secrets.
81
+ - **Installer Go toolchain bumped 1.26.4 to 1.26.5 (#166).** Clears a Go stdlib
82
+ vulnerability (GO-2026-4970) in the shipped installer binaries.
83
+
7
84
  ## [2.15.0] - 2026-07-03
8
85
 
9
86
  ### Added
@@ -32,7 +109,7 @@ All notable changes to the llm-cli-gateway project.
32
109
  - **Durable instance-lease orphan recovery (#139).** The blanket
33
110
  `markOrphanedOnStartup` sweep in the `AsyncJobManager` constructor rewrote
34
111
  every `running` row to `orphaned`, so on a SHARED store (`backend =
35
- "postgres"`) a fresh instance (especially an ephemeral stdio spawn)
112
+ "postgres"`) a fresh instance (especially an ephemeral stdio spawn)
36
113
  transiently orphaned other live instances' in-flight jobs (a `running` ->
37
114
  `orphaned` -> `completed` flap a poller can trip on). The sweep is now a
38
115
  per-job fencing lease: each instance registers a lease and advances a
@@ -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
  }
@@ -6,6 +6,7 @@ import { AcpError, AcpProcessExitError, ProviderUnavailableError } from "./error
6
6
  import { JsonRpcStdioTransport, } from "./json-rpc-stdio.js";
7
7
  import { getAcpProviderEntry } from "./provider-registry.js";
8
8
  import { envWithExtendedPath, getExtendedPath } from "../executor.js";
9
+ import { applySpawnEnvIsolation } from "../spawn-env-isolation.js";
9
10
  import { noopLogger } from "../logger.js";
10
11
  const SHUTDOWN_KILL_GRACE_MS = 5000;
11
12
  const SHELL_METACHARACTERS = /[\s|&;<>(){}$`"'\\*?[\]~#! ]/;
@@ -15,7 +16,7 @@ function assertSafeExecutable(command, provider) {
15
16
  }
16
17
  }
17
18
  export function buildProviderEnv(provider, providerConfig, baseEnv) {
18
- const env = envWithExtendedPath(baseEnv, getExtendedPath());
19
+ const env = applySpawnEnvIsolation(envWithExtendedPath(baseEnv, getExtendedPath()));
19
20
  if (provider === "grok" && providerConfig.isolatedLeaderSocket) {
20
21
  const socketDir = tmpdir();
21
22
  const socketName = `grok-acp-leader-${process.pid}-${Date.now()}.sock`;
@@ -1,4 +1,4 @@
1
- import { getProviderDefinition } from "../provider-definitions.js";
1
+ import { getProviderDefinition, PROVIDER_TARGET_VERSIONS } from "../provider-definitions.js";
2
2
  function acpEntrypointFromDefinition(provider) {
3
3
  const entry = getProviderDefinition(provider).acp.entrypoint;
4
4
  if (!entry) {
@@ -12,7 +12,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
12
12
  displayName: "Mistral Vibe",
13
13
  status: "native_smoke_passed",
14
14
  supportKind: "native",
15
- targetVersion: "vibe 2.18.3",
15
+ targetVersion: PROVIDER_TARGET_VERSIONS.mistral,
16
16
  entrypoint: acpEntrypointFromDefinition("mistral"),
17
17
  runtimeEnabledDefault: false,
18
18
  shipRuntimePilot: true,
@@ -25,7 +25,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
25
25
  displayName: "xAI Grok CLI",
26
26
  status: "native_smoke_passed",
27
27
  supportKind: "native",
28
- targetVersion: "grok 0.2.77 (44e77bec3a)",
28
+ targetVersion: PROVIDER_TARGET_VERSIONS.grok,
29
29
  entrypoint: acpEntrypointFromDefinition("grok"),
30
30
  runtimeEnabledDefault: false,
31
31
  shipRuntimePilot: true,
@@ -38,7 +38,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
38
38
  displayName: "OpenAI Codex CLI",
39
39
  status: "adapter_mediated_deferred",
40
40
  supportKind: "adapter_mediated",
41
- targetVersion: "codex-cli 0.142.4",
41
+ targetVersion: PROVIDER_TARGET_VERSIONS.codex,
42
42
  entrypoint: acpEntrypointFromDefinition("codex"),
43
43
  runtimeEnabledDefault: false,
44
44
  shipRuntimePilot: false,
@@ -51,7 +51,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
51
51
  displayName: "Anthropic Claude Code",
52
52
  status: "adapter_mediated_deferred",
53
53
  supportKind: "adapter_mediated",
54
- targetVersion: "claude 2.1.198",
54
+ targetVersion: PROVIDER_TARGET_VERSIONS.claude,
55
55
  entrypoint: acpEntrypointFromDefinition("claude"),
56
56
  runtimeEnabledDefault: false,
57
57
  shipRuntimePilot: false,
@@ -64,7 +64,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
64
64
  displayName: "Google Antigravity",
65
65
  status: "absent_watchlist",
66
66
  supportKind: "none",
67
- targetVersion: "agy 1.0.14",
67
+ targetVersion: PROVIDER_TARGET_VERSIONS.gemini,
68
68
  entrypoint: acpEntrypointFromDefinition("gemini"),
69
69
  runtimeEnabledDefault: false,
70
70
  shipRuntimePilot: false,
@@ -77,7 +77,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
77
77
  displayName: "Cognition Devin CLI",
78
78
  status: "native_smoke_passed",
79
79
  supportKind: "native",
80
- targetVersion: "devin 2026.8.18 (16737566)",
80
+ targetVersion: PROVIDER_TARGET_VERSIONS.devin,
81
81
  entrypoint: acpEntrypointFromDefinition("devin"),
82
82
  runtimeEnabledDefault: false,
83
83
  shipRuntimePilot: true,
@@ -90,7 +90,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
90
90
  displayName: "Cursor Agent CLI",
91
91
  status: "native_smoke_passed",
92
92
  supportKind: "native",
93
- targetVersion: "cursor-agent 2026.06.29-2ad2186",
93
+ targetVersion: PROVIDER_TARGET_VERSIONS.cursor,
94
94
  entrypoint: acpEntrypointFromDefinition("cursor"),
95
95
  runtimeEnabledDefault: false,
96
96
  shipRuntimePilot: true,
@@ -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;
@@ -45,6 +46,7 @@ export interface AsyncJobFlightRecorderEntry {
45
46
  stablePrefixTokens?: number;
46
47
  cacheControlBlocks?: number;
47
48
  cacheControlTtlSeconds?: number;
49
+ optimizationApplied?: boolean;
48
50
  }
49
51
  export type AsyncJobUsageExtractor = (stdout: string) => {
50
52
  inputTokens?: number;
@@ -52,6 +54,7 @@ export type AsyncJobUsageExtractor = (stdout: string) => {
52
54
  cacheReadTokens?: number;
53
55
  cacheCreationTokens?: number;
54
56
  costUsd?: number;
57
+ costBasis?: string;
55
58
  };
56
59
  export interface AsyncJobSnapshot {
57
60
  id: string;
@@ -91,6 +94,7 @@ export interface StartJobOptions {
91
94
  writeFlightStart?: boolean;
92
95
  flightRecorderEntry?: AsyncJobFlightRecorderEntry;
93
96
  extractUsage?: AsyncJobUsageExtractor;
97
+ compressResponse?: boolean;
94
98
  }
95
99
  export interface StartJobOutcome {
96
100
  snapshot: AsyncJobSnapshot;
@@ -118,12 +122,25 @@ export declare class AsyncJobManager {
118
122
  private sweepTimer;
119
123
  private durableAdmission;
120
124
  private consecutiveHeartbeatFailures;
125
+ private consecutiveHeartbeatSuccesses;
126
+ private lastHeartbeatFailureAt;
127
+ private lastHeartbeatRecoveryAt;
128
+ private lastHeartbeatErrorName;
121
129
  private skipSweepThisCycle;
122
130
  private nextHeartbeatExpectedAt;
123
131
  private disposed;
124
132
  private readonly pendingWrites;
125
133
  constructor(logger?: Logger, onJobComplete?: ((cli: JobProvider, durationMs: number, success: boolean) => void) | undefined, store?: JobStore | null, flightRecorder?: FlightRecorderLike, limits?: JobLimitsConfig, _deprecatedOwnsOrphanRecovery?: boolean, leaseConfig?: LeaseRuntimeConfig);
126
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
+ };
127
144
  private assertDurableAdmission;
128
145
  private trackPendingWrite;
129
146
  dispose(opts?: {
@@ -134,6 +151,7 @@ export declare class AsyncJobManager {
134
151
  private onHeartbeatTick;
135
152
  runOrphanSweepNow(): void;
136
153
  private startReaper;
154
+ private restoreDurableAdmission;
137
155
  private runOrphanSweep;
138
156
  private confirmLiveProcessCandidates;
139
157
  private recordStartOrFailClosed;
@@ -169,7 +187,7 @@ export declare class AsyncJobManager {
169
187
  private persistComplete;
170
188
  private hydrateFromStore;
171
189
  getJobOwner(jobId: string): string | null | undefined;
172
- startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean, env?: Record<string, string>, onComplete?: () => void, flightRecorderEntry?: AsyncJobFlightRecorderEntry, extractUsage?: AsyncJobUsageExtractor, writeFlightStart?: boolean, stdin?: string): AsyncJobSnapshot;
190
+ startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean, env?: Record<string, string>, onComplete?: () => void, flightRecorderEntry?: AsyncJobFlightRecorderEntry, extractUsage?: AsyncJobUsageExtractor, writeFlightStart?: boolean, stdin?: string, compressResponse?: boolean): AsyncJobSnapshot;
173
191
  startJobWithDedup(cli: LlmCli, args: string[], correlationId: string, opts?: StartJobOptions): StartJobOutcome;
174
192
  private launchProcessJob;
175
193
  private failQueuedJob;
@@ -199,6 +217,7 @@ export declare class AsyncJobManager {
199
217
  getLimiterSnapshot(): JobLimiterSnapshot;
200
218
  getConfiguredLimits(): JobLimitsConfig;
201
219
  getJobOutputFormat(jobId: string): string | undefined;
220
+ getJobCompressResponse(jobId: string): boolean;
202
221
  getJobCli(jobId: string): JobProvider | undefined;
203
222
  private snapshot;
204
223
  private appendOutput;