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.
- package/.agents/skills/least-cost-routing/SKILL.md +123 -0
- package/CHANGELOG.md +37 -0
- package/README.md +12 -12
- 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/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 +16 -0
- package/dist/async-job-manager.js +70 -22
- package/dist/claude-mcp-config.js +1 -1
- package/dist/compressor/transforms/ansi.js +12 -2
- package/dist/config.d.ts +31 -0
- package/dist/config.js +142 -7
- package/dist/db.js +4 -4
- package/dist/doctor.d.ts +34 -1
- package/dist/doctor.js +85 -3
- package/dist/executor.d.ts +5 -0
- package/dist/executor.js +11 -1
- package/dist/flight-recorder.d.ts +10 -0
- package/dist/flight-recorder.js +68 -2
- package/dist/http-transport.js +19 -21
- package/dist/index.d.ts +42 -3
- package/dist/index.js +666 -41
- package/dist/job-store.d.ts +10 -2
- package/dist/job-store.js +154 -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-admin-tools.js +12 -4
- package/dist/provider-definitions.d.ts +4 -0
- package/dist/provider-definitions.js +33 -5
- package/dist/provider-tool-capabilities.js +6 -9
- package/dist/request-helpers.d.ts +3 -4
- package/dist/request-helpers.js +9 -10
- 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/token-estimator.d.ts +6 -0
- package/dist/token-estimator.js +59 -0
- package/dist/upstream-contracts.d.ts +4 -1
- package/dist/upstream-contracts.js +255 -71
- 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 +14 -5
- 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;
|
|
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
|
|
374
|
-
|
|
375
|
-
-
|
|
376
|
-
`--enabled-tools <tool>` flag per
|
|
377
|
-
|
|
378
|
-
|
|
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
|
|
996
|
-
- `allowedTools` (string[], optional): One `--enabled-tools <tool>` flag per entry
|
|
997
|
-
- `disallowedTools` (string[], optional):
|
|
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
|
|
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
|
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
|
}
|
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;
|
|
@@ -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;
|