llm-cli-gateway 2.11.1 → 2.12.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/README.md CHANGED
@@ -9,9 +9,9 @@
9
9
  > _"Without consultation, plans are frustrated, but with many counselors they succeed."_
10
10
  > — Proverbs 15:22 (LSB)
11
11
 
12
- A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, Grok, and Mistral (Vibe) CLIs from one MCP endpoint, with durable async jobs, session continuity, cache-aware prompting, observability, and personal-appliance setup tooling.
12
+ A Model Context Protocol (MCP) gateway for running Claude Code, Codex, Gemini, Grok, Mistral (Vibe), and Devin CLIs from one MCP endpoint, with durable async jobs, session continuity, cache-aware prompting, observability, and personal-appliance setup tooling.
13
13
 
14
- **Why developers try it:** one local MCP endpoint for cross-LLM validation, multi-agent coding workflows, and repeatable assistant-led setup across five provider CLIs.
14
+ **Why developers try it:** one local MCP endpoint for cross-LLM validation, multi-agent coding workflows, and repeatable assistant-led setup across six provider CLIs.
15
15
 
16
16
  **Current signals:** CI and security workflows pass on `main`, OpenSSF Scorecard is published, OpenSSF Best Practices is passing, releases use Sigstore signing, and the package is MIT licensed.
17
17
 
@@ -38,7 +38,7 @@ Or use directly with `npx` from an MCP client:
38
38
 
39
39
  `llm-cli-gateway` is a single-user MCP gateway for cross-LLM validation and multi-agent coding workflows. It is more than a thin CLI wrapper:
40
40
 
41
- - Runs five provider CLIs through consistent sync and async MCP tools.
41
+ - Runs six provider CLIs through consistent sync and async MCP tools.
42
42
  - Persists long-running jobs, supports restart-safe result collection, deduplication, cancellation, and sync-to-async deferral.
43
43
  - Tracks sessions, real CLI resume paths, structured response metadata, and cache telemetry.
44
44
  - Supports cache-aware `promptParts`, including explicit Claude `cache_control` when opted in.
@@ -49,7 +49,7 @@ Or use directly with `npx` from an MCP client:
49
49
 
50
50
  ## Workflow Assets
51
51
 
52
- The repo ships agent-ready workflow skills under [`.agents/skills`](.agents/skills) for async orchestration, session continuity, multi-LLM review, implement-review-fix loops, and secure approval-gated dispatch. Machine-readable DAG-TOML plans live under [`docs/plans`](docs/plans) and [`setup/install-plan.dag.toml`](setup/install-plan.dag.toml) for workflows that need deterministic sequencing and verification gates.
52
+ The repo ships agent-ready workflow skills under [`.agents/skills`](.agents/skills) for async orchestration, session continuity, multi-LLM review, implement-review-fix loops, and secure approval-gated dispatch. Six caller-facing skills are bundled in the published npm package: `async-job-orchestration`, `multi-llm-review`, `session-workflow`, `secure-orchestration`, `implement-review-fix`, and `public-demo-session`. Machine-readable DAG-TOML plans live under [`docs/plans`](docs/plans) and [`setup/install-plan.dag.toml`](setup/install-plan.dag.toml) for workflows that need deterministic sequencing and verification gates.
53
53
 
54
54
  The next documentation focus is provider-specific skill and DAG-TOML pairs for each outbound CLI: Claude, Codex, Gemini, Grok, and Mistral Vibe. The implementation plan is tracked in [`docs/plans/provider-workflow-assets.dag.toml`](docs/plans/provider-workflow-assets.dag.toml), with each provider asset expected to cover install/login checks, session behavior, approval modes, cache/telemetry surfaces, failure modes, and a smoke-test gate.
55
55
 
@@ -84,7 +84,7 @@ Current personal-appliance artifacts include:
84
84
  - Docker Compose fallback: [docker/personal.compose.yml](docker/personal.compose.yml) + [docker/Dockerfile.personal](docker/Dockerfile.personal) for users who already manage containers.
85
85
  - Local setup UI artifact: [setup/ui/index.html](setup/ui/index.html)
86
86
  - Provider setup snippets: [setup/providers/](setup/providers/)
87
- - Cross-validation tools: `validate_with_models`, `second_opinion`, `compare_answers`, `red_team_review`, `consensus_check`, `ask_model`, `synthesize_validation`, `job_status`, and `job_result`.
87
+ - Cross-validation tools: `validate_with_models`, `second_opinion`, `compare_answers`, `red_team_review`, `consensus_check`, `ask_model`, `synthesize_validation`, `job_status`, `job_result`, and `validation_receipt` (plus the `validation-receipt://{validationId}` resource).
88
88
 
89
89
  ### Install / Upgrade / Uninstall (single binary)
90
90
 
@@ -157,7 +157,7 @@ docker compose -f docker/personal.compose.yml run --rm doctor
157
157
 
158
158
  ### Core Capabilities
159
159
 
160
- - **Multi-LLM Orchestration**: Unified interface for Claude Code, Codex, Gemini, Grok, and Mistral (Vibe) CLIs
160
+ - **Multi-LLM Orchestration**: Unified interface for Claude Code, Codex, Gemini, Grok, Mistral (Vibe), and Devin CLIs
161
161
  - **Session Management**: Track and resume conversations across all CLIs with persistent storage
162
162
  - **Gateway-owned worktrees**: Run any sync or async provider request inside a managed git worktree, with per-session reuse and cleanup
163
163
  - **Token Optimization**: Automatic 44% reduction on prompts, 37% on responses (opt-in)
@@ -169,11 +169,11 @@ docker compose -f docker/personal.compose.yml run --rm doctor
169
169
  - **SQLite Flight Recorder**: Every request/response logged to `~/.llm-cli-gateway/logs.db` with correlation IDs, token usage, duration, retry counts, and circuit breaker state. Browse with [Datasette](https://datasette.io/): `datasette ~/.llm-cli-gateway/logs.db`
170
170
  - **Structured Metadata**: Tool responses include machine-readable `structuredContent` (model, cli, correlationId, sessionId, durationMs, token counts)
171
171
  - **Cache observability resources**: `cache-state://global`, `cache-state://session/{id}`, and `cache-state://prefix/{hash}` MCP resources return aggregate cache hit/miss/savings — tokens and hashes only, no prompt text. `session_get` includes a `cacheState` block when the session has prior requests.
172
- - **Provider capability inventory**: `provider_tool_capabilities` and `provider-tools://catalog` expose the gateway request fields, supported/degraded provider controls, local skill/tool discovery, and safe config-surface hints for Claude Code, Codex CLI, Gemini/Antigravity, Grok CLI/API, and Mistral Vibe. `doctor --json` includes a compact `provider_capabilities` summary for setup assistants.
172
+ - **Provider capability inventory**: `provider_tool_capabilities` and `provider-tools://catalog` expose the gateway request fields, supported/degraded provider controls, local skill/tool discovery, and safe config-surface hints for Claude Code, Codex CLI, Gemini/Antigravity, Grok CLI/API, Mistral Vibe, and Cognition Devin. `doctor --json` includes a compact `provider_capabilities` summary for setup assistants.
173
173
 
174
174
  ### Cache-aware operation
175
175
 
176
- Every `*_request` and `*_request_async` tool accepts an optional `promptParts` field that structures the prompt for better cache hit rates. The gateway concatenates the parts in canonical order (`system → tools → context → task`) so that the stable prefix bytes precede the volatile task tail unchanged across calls, letting each provider's automatic prompt-caching land on the same content hash each time.
176
+ Every `*_request` and `*_request_async` tool except `devin_request` / `devin_request_async` accepts an optional `promptParts` field that structures the prompt for better cache hit rates (the Devin headless path takes a plain `prompt` only). The gateway concatenates the parts in canonical order (`system → tools → context → task`) so that the stable prefix bytes precede the volatile task tail unchanged across calls, letting each provider's automatic prompt-caching land on the same content hash each time.
177
177
 
178
178
  ```json
179
179
  {
@@ -188,7 +188,7 @@ Every `*_request` and `*_request_async` tool accepts an optional `promptParts` f
188
188
 
189
189
  `prompt` and `promptParts` are mutually exclusive — pass exactly one.
190
190
 
191
- Per-CLI capability matrix (prefix discipline is automatic via `promptParts` for all; explicit levers are provider-specific):
191
+ Per-CLI capability matrix (prefix discipline is automatic via `promptParts` for all providers except Devin, which has no `promptParts` surface; explicit levers are provider-specific):
192
192
 
193
193
  | CLI | Prefix discipline | Explicit lever(s) |
194
194
  | ------- | ----------------- | ----------------- |
@@ -240,7 +240,7 @@ Opt-in flags (all default off) live under `[cache_awareness]` in `~/.llm-cli-gat
240
240
 
241
241
  ### Security & Quality
242
242
 
243
- - **Comprehensive Testing**: 1,000+ tests covering unit, integration, and regression scenarios with real CLI execution
243
+ - **Comprehensive Testing**: 1,700+ tests covering unit, integration, and regression scenarios with real CLI execution
244
244
  - **Input Validation**: Zod schemas prevent injection attacks
245
245
  - **No Secret Leakage**: Generic session descriptions only (file permissions 0o600)
246
246
  - **No ReDoS**: Bounded regex patterns prevent catastrophic backtracking
@@ -387,6 +387,9 @@ The personal-appliance surface exposes simplified validation tools for non-devel
387
387
  - `synthesize_validation`: run an explicit judge model after provider results have been collected.
388
388
  - `list_available_models`: list the models each provider CLI exposes through the simplified surface.
389
389
  - `job_status` and `job_result`: poll and collect validation job outputs.
390
+ - `validation_receipt`: retrieve the immutable receipt of a terminal cross-LLM validation run by `validationId` (returns `minted | pending | expired_unminted | not_found`, own-or-not-found). `format: "markdown"` renders a human-readable report; `includeRawResponses` inlines provider answer text. Registered only under the durable persistence gate (`backend = "sqlite"` with an attached validation-run store).
391
+
392
+ The same receipt is also exposed as the `validation-receipt://{validationId}` MCP resource (same durable gate and own-or-not-found owner scoping).
390
393
 
391
394
  The validation report preserves per-provider disagreement. Optional judge synthesis is explicit about which provider produced the judge job.
392
395
 
@@ -531,6 +534,8 @@ Execute a Google Antigravity CLI (`agy`) request with session support.
531
534
  - `approvalStrategy` (string, optional): `"legacy"` (default) or `"mcp_managed"`
532
535
  - `approvalPolicy` (string, optional): `"strict"`, `"balanced"`, or `"permissive"`
533
536
  - `includeDirs` (string[], optional): Additional workspace directories (passed as `--add-dir`)
537
+ - `project` (string, optional): Select the Antigravity project for this session (`--project <ID>`); mutually exclusive with `newProject`
538
+ - `newProject` (boolean, optional): Create a new Antigravity project for this session (`--new-project`); mutually exclusive with `project`
534
539
  - `sandbox` (boolean, optional): Run Antigravity in sandbox mode (`--sandbox`)
535
540
  - `outputFormat` (string, optional): `text` only. Antigravity print mode emits text; `json` and `stream-json` are rejected.
536
541
  - `mcpServers`, `allowedTools`, `policyFiles`, `adminPolicyFiles`, `attachments` (string[], optional) and `skipTrust` (boolean, optional): **Unsupported by Antigravity CLI** — non-empty values (or `skipTrust: true`) are rejected with an explanatory error. Retained in the schema for caller parity.
@@ -602,6 +607,9 @@ Execute a Grok CLI (xAI) request with session support.
602
607
  - `restoreCode` (boolean, optional): Check out the original session commit when resuming
603
608
  - `leaderSocket` (string, optional): Custom leader socket path (`--leader-socket`, Grok 0.2.32+; default `~/.grok/leader.sock`) — targets an isolated leader process, e.g. a local/branch Grok build
604
609
  - `nativeWorktree` (boolean|string, optional): Grok's own `--worktree` flag (`true` → bare, string → named); distinct from the gateway `worktree` option
610
+ - `worktreeRef` (string, optional): Branch/tag/commit to base the native worktree on (`--worktree-ref`); requires `nativeWorktree`
611
+ - `forkSession` (boolean, optional): Fork the resumed session into a new branch instead of appending to it
612
+ - `jsonSchema` (string|object, optional): JSON Schema (string or object) constraining structured output (`--json-schema`)
605
613
  - `worktree` (boolean|object, optional): Run inside a gateway-owned git worktree (slice λ)
606
614
  - `promptParts` (object, optional): Cache-aware structured prompt `{ system?, tools?, context?, task }`; mutually exclusive with `prompt`
607
615
  - `optimizePrompt` (boolean, optional): Optimize prompt for token efficiency, default: false
@@ -878,9 +886,28 @@ Run a Mistral Vibe agentic coding request. Like `grok_request` in shape, but wit
878
886
  - `idleTimeoutMs` (integer, optional): Kill a stuck process after output inactivity; 30,000 to 3,600,000 ms
879
887
  - `forceRefresh` (boolean, optional): Bypass dedup and force a fresh CLI run, default: false
880
888
 
881
- ##### `claude_request_async` / `codex_request_async` / `gemini_request_async` / `grok_request_async` / `mistral_request_async`
889
+ ##### `devin_request`
890
+
891
+ Run a Cognition Devin CLI request synchronously (headless print mode, `devin -p`). Auto-defers to a pollable job past the sync deadline when async jobs are enabled.
892
+
893
+ **Parameters:**
894
+
895
+ - `prompt` (string, optional*): Prompt text for Devin CLI (1-100,000 chars). Required in practice; `promptFile` is additive
896
+ - `model` (string, optional): Model name or alias (e.g. `opus`, `latest`)
897
+ - `transport` (string, optional): `"cli"` (default) runs the Devin CLI; `"acp"` routes through `devin acp` when `[acp].enabled` and the provider's `runtime_enabled` are set (fails closed otherwise)
898
+ - `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
899
+ - `promptFile` (string, optional): Load the initial prompt from a file (`--prompt-file`)
900
+ - `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`
901
+ - `resumeLatest` (boolean, optional): Resume the most recent Devin session in cwd (`--continue`)
902
+ - `createNewSession` (boolean, optional): Force a new session
903
+ - `optimizePrompt` / `optimizeResponse` (boolean, optional): Token-efficiency optimisation, default: false
904
+ - `correlationId` (string, optional): Request trace ID (auto-generated if omitted)
905
+ - `idleTimeoutMs` (integer, optional): Kill a stuck process after output inactivity; 30,000 to 3,600,000 ms
906
+ - `forceRefresh` (boolean, optional): Bypass dedup and force a fresh CLI run, default: false
907
+
908
+ ##### `claude_request_async` / `codex_request_async` / `gemini_request_async` / `grok_request_async` / `mistral_request_async` / `devin_request_async`
882
909
 
883
- Start a long-running Claude, Codex, Gemini, Grok, or Mistral request without waiting for completion in the same MCP call.
910
+ Start a long-running Claude, Codex, Gemini, Grok, Mistral, or Devin request without waiting for completion in the same MCP call.
884
911
 
885
912
  Use this flow when analysis/runtime can exceed client tool-call limits:
886
913
 
@@ -939,7 +966,7 @@ Return the gateway's declared provider CLI contracts, optionally probing the ins
939
966
 
940
967
  **Parameters:**
941
968
 
942
- - `cli` (string, optional): Filter (`claude|codex|gemini|grok|mistral`)
969
+ - `cli` (string, optional): Filter (`claude|codex|gemini|grok|mistral|devin`)
943
970
  - `probeInstalled` (boolean, optional, default `false`): Run local `--help` probes and compare advertised flags against the declared contract — strongly recommended after any provider CLI upgrade. The probe reports `missingFlags`, `extraFlags`, `acknowledgedExtraFlags` (known upstream-only flags filtered from `extraFlags`), `discoveredFlags`, and stale-marker `warnings`.
944
971
 
945
972
  #### Session Management Tools
@@ -950,7 +977,7 @@ Create a new session for a specific CLI.
950
977
 
951
978
  **Parameters:**
952
979
 
953
- - `cli` (string, required): CLI to create session for ("claude", "codex", "gemini", "grok", "mistral")
980
+ - `cli` (string, required): CLI to create session for ("claude", "codex", "gemini", "grok", "mistral", "devin")
954
981
  - `description` (string, optional): Description for the session
955
982
  - `setAsActive` (boolean, optional): Set as active session, default: true
956
983
 
@@ -970,7 +997,7 @@ List all sessions, optionally filtered by CLI.
970
997
 
971
998
  **Parameters:**
972
999
 
973
- - `cli` (string, optional): Filter by CLI ("claude", "codex", "gemini", "grok", "mistral")
1000
+ - `cli` (string, optional): Filter by CLI ("claude", "codex", "gemini", "grok", "mistral", "devin")
974
1001
 
975
1002
  **Response includes:**
976
1003
 
@@ -1019,7 +1046,7 @@ List available models for each CLI.
1019
1046
 
1020
1047
  **Parameters:**
1021
1048
 
1022
- - `cli` (string, optional): Specific CLI to list models for ("claude", "codex", "gemini", "grok", "mistral")
1049
+ - `cli` (string, optional): Specific CLI to list models for ("claude", "codex", "gemini", "grok", "mistral", "devin")
1023
1050
 
1024
1051
  **Response includes:**
1025
1052
 
@@ -1067,7 +1094,7 @@ inputs.
1067
1094
 
1068
1095
  **Parameters:**
1069
1096
 
1070
- - `cli` (string, optional): Provider filter (`"claude"`, `"codex"`, `"gemini"`, `"grok"`, `"grok_api"`, or `"mistral"`)
1097
+ - `cli` (string, optional): Provider filter (`"claude"`, `"codex"`, `"gemini"`, `"grok"`, `"mistral"`, `"devin"`, or `"grok_api"`)
1071
1098
  - `includeSkills` (boolean, default `true`): Include bounded local skill discovery
1072
1099
  - `includeProviderTools` (boolean, default `true`): Include provider-native tools extracted from discovered skills
1073
1100
  - `includeUnsupported` (boolean, default `true`): Include explicit unsupported/degraded input records
@@ -1087,6 +1114,7 @@ Equivalent MCP resources:
1087
1114
  - `provider-tools://grok`
1088
1115
  - `provider-tools://grok_api`
1089
1116
  - `provider-tools://mistral`
1117
+ - `provider-tools://devin`
1090
1118
 
1091
1119
  `doctor --json` also emits a compact `provider_capabilities` block with the
1092
1120
  same schema version, per-provider request tool names, supported feature names,
@@ -1100,7 +1128,7 @@ Report installed CLI versions.
1100
1128
 
1101
1129
  **Parameters:**
1102
1130
 
1103
- - `cli` (string, optional): Specific CLI to inspect ("claude", "codex", "gemini", "grok", "mistral")
1131
+ - `cli` (string, optional): Specific CLI to inspect ("claude", "codex", "gemini", "grok", "mistral", "devin")
1104
1132
 
1105
1133
  ##### `cli_upgrade`
1106
1134
 
@@ -1108,7 +1136,7 @@ Plan or run an upgrade for one CLI.
1108
1136
 
1109
1137
  **Parameters:**
1110
1138
 
1111
- - `cli` (string, required): CLI to upgrade ("claude", "codex", "gemini", "grok", "mistral")
1139
+ - `cli` (string, required): CLI to upgrade ("claude", "codex", "gemini", "grok", "mistral", "devin")
1112
1140
  - `target` (string, optional): Package tag/version/target, default: `latest`
1113
1141
  - `dryRun` (boolean, optional): Return the upgrade plan without running it, default: `true`
1114
1142
  - `timeoutMs` (number, optional): Upgrade timeout when `dryRun=false`
@@ -1123,6 +1151,7 @@ Plan or run an upgrade for one CLI.
1123
1151
  - Grok latest: `grok update`
1124
1152
  - Grok explicit target: `grok update --version <target>`
1125
1153
  - Mistral (Vibe): dispatches to the detected installer (`pip`/`uv`/`brew`); errors with guidance when none is detected (Vibe ships no self-update command)
1154
+ - Devin latest: `devin update` (self-update; explicit version targets are unsupported)
1126
1155
 
1127
1156
  **Example dry run:**
1128
1157
 
@@ -17,7 +17,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
17
17
  displayName: "xAI Grok CLI",
18
18
  status: "native_smoke_passed",
19
19
  supportKind: "native",
20
- targetVersion: "grok 0.2.60 (474c2bbfc)",
20
+ targetVersion: "grok 0.2.73 (9ff14c43bb)",
21
21
  entrypoint: Object.freeze({ command: "grok", args: Object.freeze(["agent", "stdio"]) }),
22
22
  runtimeEnabledDefault: false,
23
23
  shipRuntimePilot: true,
@@ -30,7 +30,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
30
30
  displayName: "OpenAI Codex CLI",
31
31
  status: "adapter_mediated_deferred",
32
32
  supportKind: "adapter_mediated",
33
- targetVersion: "codex-cli 0.141.0",
33
+ targetVersion: "codex-cli 0.142.4",
34
34
  entrypoint: null,
35
35
  runtimeEnabledDefault: false,
36
36
  shipRuntimePilot: false,
@@ -43,7 +43,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
43
43
  displayName: "Anthropic Claude Code",
44
44
  status: "adapter_mediated_deferred",
45
45
  supportKind: "adapter_mediated",
46
- targetVersion: "claude 2.1.185",
46
+ targetVersion: "claude 2.1.195",
47
47
  entrypoint: null,
48
48
  runtimeEnabledDefault: false,
49
49
  shipRuntimePilot: false,
@@ -56,7 +56,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
56
56
  displayName: "Google Antigravity",
57
57
  status: "absent_watchlist",
58
58
  supportKind: "none",
59
- targetVersion: "agy 1.0.10",
59
+ targetVersion: "agy 1.0.13",
60
60
  entrypoint: null,
61
61
  runtimeEnabledDefault: false,
62
62
  shipRuntimePilot: false,
@@ -69,7 +69,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
69
69
  displayName: "Cognition Devin CLI",
70
70
  status: "native_smoke_passed",
71
71
  supportKind: "native",
72
- targetVersion: "devin 2026.7.23 (3bd47f77)",
72
+ targetVersion: "devin 2026.8.18 (16737566)",
73
73
  entrypoint: Object.freeze({ command: "devin", args: Object.freeze(["acp"]) }),
74
74
  runtimeEnabledDefault: false,
75
75
  shipRuntimePilot: true,
@@ -2,6 +2,7 @@ import type { URL } from "node:url";
2
2
  import type { Logger } from "./logger.js";
3
3
  import { ApiHttpError } from "./api-http.js";
4
4
  export type ApiProviderKind = "openai-compatible" | "anthropic" | "xai-responses";
5
+ export type ApiContinuity = "server-side-id" | "stateless-resend" | "none";
5
6
  export interface ApiChatMessage {
6
7
  role: "system" | "user" | "assistant";
7
8
  content: string;
@@ -17,6 +18,7 @@ export interface ApiRequest {
17
18
  reasoningEffort?: "none" | "low" | "medium" | "high";
18
19
  timeoutMs?: number;
19
20
  previousResponseId?: string;
21
+ usageInclude?: boolean;
20
22
  }
21
23
  export interface ApiUsage {
22
24
  inputTokens?: number;
@@ -32,10 +34,12 @@ export interface ApiResult {
32
34
  raw: unknown;
33
35
  httpStatus: number;
34
36
  responseId?: string | null;
37
+ status?: string | null;
35
38
  }
36
39
  export interface ApiProvider {
37
40
  readonly name: string;
38
41
  readonly kind: ApiProviderKind;
42
+ readonly continuity: ApiContinuity;
39
43
  endpointUrl(baseUrl: string): URL;
40
44
  buildBody(req: ApiRequest): Record<string, unknown>;
41
45
  parseResult(httpStatus: number, body: string): ApiResult;
@@ -45,6 +49,7 @@ export interface ApiProvider {
45
49
  export declare class OpenAiCompatibleProvider implements ApiProvider {
46
50
  readonly name: string;
47
51
  readonly kind: "openai-compatible";
52
+ readonly continuity: "stateless-resend";
48
53
  constructor(name: string);
49
54
  endpointUrl(baseUrl: string): URL;
50
55
  buildBody(req: ApiRequest): Record<string, unknown>;
@@ -57,6 +62,7 @@ export declare class AnthropicProvider implements ApiProvider {
57
62
  readonly name: string;
58
63
  private readonly anthropicVersion;
59
64
  readonly kind: "anthropic";
65
+ readonly continuity: "stateless-resend";
60
66
  constructor(name: string, anthropicVersion?: string);
61
67
  endpointUrl(baseUrl: string): URL;
62
68
  buildBody(req: ApiRequest): Record<string, unknown>;
@@ -67,6 +73,7 @@ export declare class AnthropicProvider implements ApiProvider {
67
73
  export declare class XaiResponsesProvider implements ApiProvider {
68
74
  readonly name: string;
69
75
  readonly kind: "xai-responses";
76
+ readonly continuity: "server-side-id";
70
77
  constructor(name: string);
71
78
  endpointUrl(baseUrl: string): URL;
72
79
  buildBody(req: ApiRequest): Record<string, unknown>;
@@ -15,6 +15,7 @@ function firstNumber(...candidates) {
15
15
  export class OpenAiCompatibleProvider {
16
16
  name;
17
17
  kind = "openai-compatible";
18
+ continuity = "stateless-resend";
18
19
  constructor(name) {
19
20
  this.name = name;
20
21
  }
@@ -32,6 +33,8 @@ export class OpenAiCompatibleProvider {
32
33
  body.temperature = req.temperature;
33
34
  if (req.topP !== undefined)
34
35
  body.top_p = req.topP;
36
+ if (req.usageInclude)
37
+ body.usage = { include: true };
35
38
  return body;
36
39
  }
37
40
  parseResult(httpStatus, body) {
@@ -46,6 +49,7 @@ export class OpenAiCompatibleProvider {
46
49
  inputTokens: firstNumber(usage.prompt_tokens, usage.input_tokens),
47
50
  outputTokens: firstNumber(usage.completion_tokens, usage.output_tokens),
48
51
  cacheReadTokens: firstNumber(usage?.prompt_tokens_details?.cached_tokens),
52
+ costUsd: firstNumber(usage.cost),
49
53
  raw: usage,
50
54
  },
51
55
  raw: parsed,
@@ -65,6 +69,7 @@ export class AnthropicProvider {
65
69
  name;
66
70
  anthropicVersion;
67
71
  kind = "anthropic";
72
+ continuity = "stateless-resend";
68
73
  constructor(name, anthropicVersion = DEFAULT_ANTHROPIC_VERSION) {
69
74
  this.name = name;
70
75
  this.anthropicVersion = anthropicVersion;
@@ -152,6 +157,7 @@ function extractXaiResponseText(parsed) {
152
157
  export class XaiResponsesProvider {
153
158
  name;
154
159
  kind = "xai-responses";
160
+ continuity = "server-side-id";
155
161
  constructor(name) {
156
162
  this.name = name;
157
163
  }
@@ -186,6 +192,7 @@ export class XaiResponsesProvider {
186
192
  const usage = parsed?.usage ?? {};
187
193
  return {
188
194
  responseId: typeof parsed?.id === "string" ? parsed.id : null,
195
+ status: typeof parsed?.status === "string" ? parsed.status : null,
189
196
  model: typeof parsed?.model === "string" ? parsed.model : "unknown",
190
197
  text: extractXaiResponseText(parsed),
191
198
  usage: {
@@ -47,5 +47,6 @@ export function prepareApiRequest(runtime, params) {
47
47
  reasoningEffort: params.reasoningEffort,
48
48
  timeoutMs: params.timeoutMs,
49
49
  previousResponseId: params.previousResponseId,
50
+ usageInclude: runtime.usageInclude,
50
51
  };
51
52
  }
@@ -2,7 +2,10 @@ import type { Logger } from "./logger.js";
2
2
  import { type JobHealth } from "./process-monitor.js";
3
3
  import { JobStore } from "./job-store.js";
4
4
  import { type FlightRecorderLike } from "./flight-recorder.js";
5
- import { type ApiProvider, type ApiRequest } from "./api-provider.js";
5
+ import type { ValidationRunStore } from "./job-store.js";
6
+ import { type ApiProvider, type ApiRequest, type ApiUsage } from "./api-provider.js";
7
+ export declare function extractApiHttpStatus(error: unknown): number | null;
8
+ export declare function extractApiErrorBody(error: unknown): string | undefined;
6
9
  export type LlmCli = "claude" | "codex" | "gemini" | "grok" | "mistral" | "devin";
7
10
  export type JobProvider = LlmCli | (string & {});
8
11
  export type AsyncJobStatus = "running" | "completed" | "failed" | "canceled" | "orphaned";
@@ -41,6 +44,11 @@ export interface AsyncJobResult extends AsyncJobSnapshot {
41
44
  stderr: string;
42
45
  stdoutTruncated: boolean;
43
46
  stderrTruncated: boolean;
47
+ apiUsage?: ApiUsage;
48
+ httpStatus?: number | null;
49
+ responseId?: string | null;
50
+ model?: string;
51
+ errorBody?: string;
44
52
  }
45
53
  export interface StartJobOptions {
46
54
  cwd?: string;
@@ -72,6 +80,7 @@ export declare class AsyncJobManager {
72
80
  private buildOrphanFlightResult;
73
81
  checkStalledJobs(now?: number): void;
74
82
  hasStore(): boolean;
83
+ getValidationRunStore(): ValidationRunStore | null;
75
84
  private emitMetrics;
76
85
  private evictCompletedJobs;
77
86
  private buildRequestKey;
@@ -91,6 +100,7 @@ export declare class AsyncJobManager {
91
100
  private fireOnComplete;
92
101
  private writeFlightComplete;
93
102
  private safeExtractUsage;
103
+ private httpUsage;
94
104
  armFlightCompleteForDeferral(jobId: string): void;
95
105
  private safeStoreCall;
96
106
  private maybeFlushOutput;
@@ -2,12 +2,12 @@ import { randomUUID } from "crypto";
2
2
  import { envWithExtendedPath, getExtendedPath, killProcessGroup, providerCommandName, spawnCliProcess, unregisterProcessGroup, } from "./executor.js";
3
3
  import { noopLogger, logWarn } from "./logger.js";
4
4
  import { ProcessMonitor } from "./process-monitor.js";
5
- import { computeRequestKey } from "./job-store.js";
5
+ import { computeRequestKey, isValidationRunStore } from "./job-store.js";
6
6
  import { NoopFlightRecorder, } from "./flight-recorder.js";
7
7
  import { codexFrResponse } from "./codex-json-parser.js";
8
8
  import { getRequestContext, resolveOwnerPrincipal } from "./request-context.js";
9
9
  import { runApiRequest, ApiHttpError, } from "./api-provider.js";
10
- function extractApiHttpStatus(error) {
10
+ export function extractApiHttpStatus(error) {
11
11
  for (const candidate of [error, error?.cause]) {
12
12
  if (candidate instanceof ApiHttpError && typeof candidate.status === "number") {
13
13
  return candidate.status;
@@ -18,6 +18,14 @@ function extractApiHttpStatus(error) {
18
18
  }
19
19
  return null;
20
20
  }
21
+ export function extractApiErrorBody(error) {
22
+ for (const candidate of [error, error?.cause]) {
23
+ if (candidate instanceof ApiHttpError && candidate.responseText) {
24
+ return candidate.responseText;
25
+ }
26
+ }
27
+ return undefined;
28
+ }
21
29
  const MAX_OUTPUT_SIZE = 50 * 1024 * 1024;
22
30
  const JOB_TTL_MS = 60 * 60 * 1000;
23
31
  const EVICTION_INTERVAL_MS = 5 * 60 * 1000;
@@ -173,6 +181,9 @@ export class AsyncJobManager {
173
181
  hasStore() {
174
182
  return this.store !== null;
175
183
  }
184
+ getValidationRunStore() {
185
+ return this.store && isValidationRunStore(this.store) ? this.store : null;
186
+ }
176
187
  emitMetrics(job) {
177
188
  if (job.metricsRecorded)
178
189
  return;
@@ -404,6 +415,9 @@ export class AsyncJobManager {
404
415
  job.stdout = result.text;
405
416
  job.httpStatus = result.httpStatus;
406
417
  job.exitCode = 0;
418
+ job.apiUsage = result.usage;
419
+ job.apiResponseId = result.responseId ?? null;
420
+ job.apiModel = result.model;
407
421
  }
408
422
  else {
409
423
  job.status = "failed";
@@ -413,6 +427,7 @@ export class AsyncJobManager {
413
427
  job.stderr = message;
414
428
  job.error = message;
415
429
  job.exitCode = 1;
430
+ job.apiErrorBody = extractApiErrorBody(error);
416
431
  }
417
432
  job.finishedAt = new Date().toISOString();
418
433
  job.exited = true;
@@ -443,7 +458,13 @@ export class AsyncJobManager {
443
458
  if (job.flightRecorderComplete)
444
459
  return;
445
460
  const durationMs = Math.max(0, Date.now() - new Date(job.startedAt).getTime());
446
- const usage = finalStatus === "completed" && job.extractUsage ? this.safeExtractUsage(job) : {};
461
+ const usage = finalStatus !== "completed"
462
+ ? {}
463
+ : job.transport === "http"
464
+ ? this.httpUsage(job)
465
+ : job.extractUsage
466
+ ? this.safeExtractUsage(job)
467
+ : {};
447
468
  const isFailure = finalStatus === "failed";
448
469
  let response;
449
470
  if (job.transport === "process" && job.cli === "codex") {
@@ -491,6 +512,17 @@ export class AsyncJobManager {
491
512
  return {};
492
513
  }
493
514
  }
515
+ httpUsage(job) {
516
+ const u = job.apiUsage;
517
+ if (!u)
518
+ return {};
519
+ return {
520
+ inputTokens: u.inputTokens,
521
+ outputTokens: u.outputTokens,
522
+ cacheReadTokens: u.cacheReadTokens,
523
+ costUsd: u.costUsd,
524
+ };
525
+ }
494
526
  armFlightCompleteForDeferral(jobId) {
495
527
  const job = this.jobs.get(jobId);
496
528
  if (!job)
@@ -846,6 +878,15 @@ export class AsyncJobManager {
846
878
  stderr: stderr.text,
847
879
  stdoutTruncated: stdout.truncated,
848
880
  stderrTruncated: stderr.truncated,
881
+ ...(job.transport === "http"
882
+ ? {
883
+ apiUsage: job.apiUsage,
884
+ httpStatus: job.httpStatus,
885
+ responseId: job.apiResponseId,
886
+ model: job.apiModel,
887
+ errorBody: job.apiErrorBody,
888
+ }
889
+ : {}),
849
890
  };
850
891
  }
851
892
  cancelJob(jobId) {
package/dist/config.d.ts CHANGED
@@ -76,6 +76,7 @@ export interface ApiProviderConfig {
76
76
  baseUrl: string;
77
77
  defaultModel: string;
78
78
  models?: string[];
79
+ usageInclude?: boolean;
79
80
  }
80
81
  export interface ApiProviderRuntime {
81
82
  name: string;
@@ -83,6 +84,7 @@ export interface ApiProviderRuntime {
83
84
  baseUrl: string;
84
85
  defaultModel: string;
85
86
  models?: string[];
87
+ usageInclude?: boolean;
86
88
  apiKey: string;
87
89
  }
88
90
  export interface ProvidersConfig {
package/dist/config.js CHANGED
@@ -301,6 +301,7 @@ const ApiProviderSchema = z
301
301
  api_key_env: z.string().min(1).optional(),
302
302
  default_model: z.string().min(1),
303
303
  models: z.array(z.string().min(1)).nonempty().optional(),
304
+ usage_include: z.boolean().optional(),
304
305
  })
305
306
  .strict();
306
307
  function readProvidersFile(configPath, logger) {
@@ -369,6 +370,7 @@ export function loadProvidersConfig(logger = noopLogger) {
369
370
  baseUrl: parsed.data.base_url,
370
371
  defaultModel: parsed.data.default_model,
371
372
  models: parsed.data.models ? [...parsed.data.models] : undefined,
373
+ usageInclude: parsed.data.usage_include,
372
374
  };
373
375
  }
374
376
  return { xai, providers, sources: { configFile: sourcePath } };
@@ -395,6 +397,7 @@ export function enabledApiProviders(config, env = process.env) {
395
397
  baseUrl: provider.baseUrl,
396
398
  defaultModel: provider.defaultModel,
397
399
  models: provider.models,
400
+ usageInclude: provider.usageInclude,
398
401
  apiKey: resolveProviderKey(provider.apiKeyEnv, env) ?? "",
399
402
  });
400
403
  }