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.
- package/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +78 -1
- package/dist/acp/client.js +5 -0
- package/dist/acp/flight-redaction.d.ts +2 -0
- package/dist/acp/flight-redaction.js +2 -0
- package/dist/acp/process-manager.js +2 -1
- package/dist/acp/provider-registry.js +8 -8
- package/dist/acp/runtime.d.ts +1 -0
- package/dist/acp/runtime.js +20 -0
- package/dist/acp/types.d.ts +52 -0
- package/dist/acp/types.js +8 -0
- package/dist/api-provider.d.ts +1 -0
- package/dist/api-provider.js +1 -0
- package/dist/async-job-manager.d.ts +20 -1
- package/dist/async-job-manager.js +85 -28
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/estimate.d.ts +5 -0
- package/dist/compressor/estimate.js +21 -0
- package/dist/compressor/index.d.ts +23 -0
- package/dist/compressor/index.js +77 -0
- package/dist/compressor/router.d.ts +2 -0
- package/dist/compressor/router.js +57 -0
- package/dist/compressor/transforms/ansi.d.ts +3 -0
- package/dist/compressor/transforms/ansi.js +99 -0
- package/dist/compressor/transforms/json.d.ts +1 -0
- package/dist/compressor/transforms/json.js +156 -0
- package/dist/compressor/transforms/log.d.ts +12 -0
- package/dist/compressor/transforms/log.js +55 -0
- package/dist/compressor/transforms/whitespace.d.ts +8 -0
- package/dist/compressor/transforms/whitespace.js +79 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +166 -6
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +6 -0
- package/dist/executor.js +16 -3
- package/dist/flight-recorder.d.ts +19 -0
- package/dist/flight-recorder.js +110 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +58 -2
- package/dist/index.js +929 -94
- package/dist/job-store.d.ts +14 -2
- package/dist/job-store.js +170 -43
- package/dist/lcr-priors.d.ts +60 -0
- package/dist/lcr-priors.js +190 -0
- package/dist/lcr-router-env.d.ts +20 -0
- package/dist/lcr-router-env.js +133 -0
- package/dist/lcr-telemetry.d.ts +2 -0
- package/dist/lcr-telemetry.js +17 -0
- package/dist/least-cost-router.d.ts +86 -0
- package/dist/least-cost-router.js +296 -0
- package/dist/least-cost-types.d.ts +34 -0
- package/dist/least-cost-types.js +1 -0
- package/dist/migrate-sessions.js +1 -1
- package/dist/migrate.js +1 -1
- package/dist/model-registry.js +1 -1
- package/dist/postgres-job-store-worker.js +56 -13
- package/dist/pricing.d.ts +17 -0
- package/dist/pricing.js +167 -0
- package/dist/provider-definitions.d.ts +5 -0
- package/dist/provider-definitions.js +56 -10
- package/dist/provider-tool-capabilities.js +3 -3
- package/dist/request-helpers.d.ts +2 -2
- package/dist/request-helpers.js +1 -1
- package/dist/resources.d.ts +37 -2
- package/dist/resources.js +96 -1
- package/dist/retry.d.ts +1 -0
- package/dist/retry.js +1 -1
- package/dist/spawn-env-isolation.d.ts +10 -0
- package/dist/spawn-env-isolation.js +55 -0
- package/dist/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.js +48 -28
- package/dist/validation-receipt.js +1 -1
- package/dist/validation-tools.d.ts +6 -0
- package/dist/validation-tools.js +174 -54
- package/npm-shrinkwrap.json +2 -2
- package/package.json +15 -6
- 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
|
-
|
|
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
|
package/dist/acp/client.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
93
|
+
targetVersion: PROVIDER_TARGET_VERSIONS.cursor,
|
|
94
94
|
entrypoint: acpEntrypointFromDefinition("cursor"),
|
|
95
95
|
runtimeEnabledDefault: false,
|
|
96
96
|
shipRuntimePilot: true,
|
package/dist/acp/runtime.d.ts
CHANGED
|
@@ -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>;
|
package/dist/acp/runtime.js
CHANGED
|
@@ -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 {
|
package/dist/acp/types.d.ts
CHANGED
|
@@ -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
|
})
|
package/dist/api-provider.d.ts
CHANGED
package/dist/api-provider.js
CHANGED
|
@@ -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;
|