llm-cli-gateway 2.16.0 → 2.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +12 -12
  4. package/dist/acp/client.js +5 -0
  5. package/dist/acp/flight-redaction.d.ts +2 -0
  6. package/dist/acp/flight-redaction.js +2 -0
  7. package/dist/acp/runtime.d.ts +1 -0
  8. package/dist/acp/runtime.js +20 -0
  9. package/dist/acp/types.d.ts +52 -0
  10. package/dist/acp/types.js +8 -0
  11. package/dist/api-provider.d.ts +1 -0
  12. package/dist/api-provider.js +1 -0
  13. package/dist/async-job-manager.d.ts +16 -0
  14. package/dist/async-job-manager.js +70 -22
  15. package/dist/claude-mcp-config.js +1 -1
  16. package/dist/compressor/transforms/ansi.js +12 -2
  17. package/dist/config.d.ts +31 -0
  18. package/dist/config.js +142 -7
  19. package/dist/db.js +4 -4
  20. package/dist/doctor.d.ts +34 -1
  21. package/dist/doctor.js +85 -3
  22. package/dist/executor.d.ts +5 -0
  23. package/dist/executor.js +11 -1
  24. package/dist/flight-recorder.d.ts +10 -0
  25. package/dist/flight-recorder.js +68 -2
  26. package/dist/http-transport.js +19 -21
  27. package/dist/index.d.ts +42 -3
  28. package/dist/index.js +666 -41
  29. package/dist/job-store.d.ts +10 -2
  30. package/dist/job-store.js +154 -43
  31. package/dist/lcr-priors.d.ts +60 -0
  32. package/dist/lcr-priors.js +190 -0
  33. package/dist/lcr-router-env.d.ts +20 -0
  34. package/dist/lcr-router-env.js +133 -0
  35. package/dist/lcr-telemetry.d.ts +2 -0
  36. package/dist/lcr-telemetry.js +17 -0
  37. package/dist/least-cost-router.d.ts +86 -0
  38. package/dist/least-cost-router.js +296 -0
  39. package/dist/least-cost-types.d.ts +34 -0
  40. package/dist/least-cost-types.js +1 -0
  41. package/dist/migrate-sessions.js +1 -1
  42. package/dist/migrate.js +1 -1
  43. package/dist/model-registry.js +1 -1
  44. package/dist/postgres-job-store-worker.js +56 -13
  45. package/dist/pricing.d.ts +17 -0
  46. package/dist/pricing.js +167 -0
  47. package/dist/provider-admin-tools.js +12 -4
  48. package/dist/provider-definitions.d.ts +4 -0
  49. package/dist/provider-definitions.js +33 -5
  50. package/dist/provider-tool-capabilities.js +6 -9
  51. package/dist/request-helpers.d.ts +3 -4
  52. package/dist/request-helpers.js +9 -10
  53. package/dist/resources.d.ts +37 -2
  54. package/dist/resources.js +96 -1
  55. package/dist/retry.d.ts +1 -0
  56. package/dist/retry.js +1 -1
  57. package/dist/token-estimator.d.ts +6 -0
  58. package/dist/token-estimator.js +59 -0
  59. package/dist/upstream-contracts.d.ts +4 -1
  60. package/dist/upstream-contracts.js +255 -71
  61. package/dist/validation-receipt.js +1 -1
  62. package/dist/validation-tools.d.ts +6 -0
  63. package/dist/validation-tools.js +174 -54
  64. package/npm-shrinkwrap.json +2 -2
  65. package/package.json +14 -5
  66. package/setup/status.schema.json +68 -0
package/dist/pricing.js CHANGED
@@ -116,3 +116,170 @@ export function estimateCacheSavingsUsd(cli, model, cacheReadTokens) {
116
116
  const savedPerToken = (p.inputUsd * (1 - p.cacheReadMultiplier)) / 1_000_000;
117
117
  return cacheReadTokens * savedPerToken;
118
118
  }
119
+ export function modelIdToFamily(model) {
120
+ const lower = model.toLowerCase();
121
+ if (lower.includes("sonnet"))
122
+ return "claude-sonnet";
123
+ if (lower.includes("opus"))
124
+ return "claude-opus";
125
+ if (lower.includes("haiku"))
126
+ return "claude-haiku";
127
+ if (lower.includes("gpt-5") || lower.includes("o3"))
128
+ return "openai-gpt5";
129
+ const geminiSpecialty = lower.includes("lite") ||
130
+ lower.includes("image") ||
131
+ lower.includes("audio") ||
132
+ lower.includes("tts");
133
+ if (!geminiSpecialty) {
134
+ if (lower.includes("2.5-flash"))
135
+ return "gemini-2.5-flash";
136
+ if (lower.includes("2.5-pro"))
137
+ return "gemini-2.5-pro";
138
+ if (lower.includes("gemini-3") && lower.includes("pro"))
139
+ return "gemini-3-pro";
140
+ }
141
+ if (lower.includes("grok-build") || lower.includes("grok-code"))
142
+ return "grok-build";
143
+ if (lower.includes("grok-4") || lower.includes("grok-3") || lower.includes("grok-latest")) {
144
+ return "grok-4";
145
+ }
146
+ if (lower.includes("devstral-small"))
147
+ return "mistral-devstral";
148
+ if (lower.includes("mistral-medium-3.5"))
149
+ return "mistral-medium";
150
+ return "unknown";
151
+ }
152
+ const FAMILY_PRICING = new Map([
153
+ ["claude-sonnet", ANTHROPIC_SONNET],
154
+ ["claude-opus", ANTHROPIC_OPUS],
155
+ ["claude-haiku", ANTHROPIC_HAIKU],
156
+ ["openai-gpt5", OPENAI_GPT5],
157
+ ["gemini-2.5-pro", GEMINI_25_PRO],
158
+ ["gemini-2.5-flash", GEMINI_FLASH],
159
+ ["gemini-3-pro", GEMINI_3_PRO],
160
+ ["grok-4", GROK_4],
161
+ ["grok-build", GROK_BUILD],
162
+ ["mistral-medium", MISTRAL_MEDIUM],
163
+ ["mistral-devstral", MISTRAL_DEVSTRAL],
164
+ ]);
165
+ export const API_CATALOG_AS_OF = "2026-07-11";
166
+ export const API_CATALOG = new Map([
167
+ [
168
+ "openai/gpt-5.5",
169
+ {
170
+ inputUsdPerMTok: 1.25,
171
+ outputUsdPerMTok: 10,
172
+ cacheReadMultiplier: 0.5,
173
+ family: "openai-gpt5",
174
+ },
175
+ ],
176
+ [
177
+ "anthropic/claude-sonnet-4.5",
178
+ {
179
+ inputUsdPerMTok: 3,
180
+ outputUsdPerMTok: 15,
181
+ cacheReadMultiplier: 0.1,
182
+ accountingMode: "disjoint",
183
+ family: "claude-sonnet",
184
+ },
185
+ ],
186
+ [
187
+ "google/gemini-2.5-flash",
188
+ {
189
+ inputUsdPerMTok: 0.3,
190
+ outputUsdPerMTok: 2.5,
191
+ cacheReadMultiplier: 0.1,
192
+ family: "gemini-2.5-flash",
193
+ },
194
+ ],
195
+ [
196
+ "grok-build-0.1",
197
+ { inputUsdPerMTok: 1, outputUsdPerMTok: 2, cacheReadMultiplier: 0.2, family: "grok-build" },
198
+ ],
199
+ [
200
+ "grok-4",
201
+ { inputUsdPerMTok: 1.25, outputUsdPerMTok: 2.5, cacheReadMultiplier: 0.16, family: "grok-4" },
202
+ ],
203
+ ]);
204
+ function tableModelCost(family, rate) {
205
+ const accountingMode = family.startsWith("claude-") ? "disjoint" : "inclusive";
206
+ return {
207
+ inputUsdPerMTok: rate.inputUsd,
208
+ outputUsdPerMTok: rate.outputUsd,
209
+ cacheReadMultiplier: rate.cacheReadMultiplier,
210
+ cacheWriteUsdPerMTok: rate.inputUsd,
211
+ accountingMode,
212
+ family,
213
+ source: "table",
214
+ asOf: PRICING_AS_OF,
215
+ };
216
+ }
217
+ function catalogModelCost(model, entry) {
218
+ const family = entry.family ?? modelIdToFamily(model);
219
+ return {
220
+ inputUsdPerMTok: entry.inputUsdPerMTok,
221
+ outputUsdPerMTok: entry.outputUsdPerMTok,
222
+ cacheReadMultiplier: entry.cacheReadMultiplier,
223
+ cacheWriteUsdPerMTok: entry.inputUsdPerMTok,
224
+ accountingMode: entry.accountingMode ?? "inclusive",
225
+ family: family === "unknown" ? model.toLowerCase() : family,
226
+ source: "api-catalog",
227
+ asOf: API_CATALOG_AS_OF,
228
+ };
229
+ }
230
+ const UNKNOWN_MODEL_COST = {
231
+ inputUsdPerMTok: 0,
232
+ outputUsdPerMTok: 0,
233
+ cacheReadMultiplier: 0,
234
+ cacheWriteUsdPerMTok: 0,
235
+ accountingMode: "inclusive",
236
+ family: "unknown",
237
+ source: "unknown",
238
+ asOf: PRICING_AS_OF,
239
+ };
240
+ export function getModelCost(provider, model, opts) {
241
+ const catalog = opts?.catalog ?? API_CATALOG;
242
+ const preferCatalog = opts?.preferCatalog ?? true;
243
+ const catalogEntry = catalog.get(model.toLowerCase());
244
+ const family = modelIdToFamily(model);
245
+ const familyRate = FAMILY_PRICING.get(family);
246
+ if (catalogEntry !== undefined && (preferCatalog || familyRate === undefined)) {
247
+ return catalogModelCost(model, catalogEntry);
248
+ }
249
+ if (familyRate !== undefined) {
250
+ return tableModelCost(family, familyRate);
251
+ }
252
+ return { ...UNKNOWN_MODEL_COST };
253
+ }
254
+ function composeTotalUsd(inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, modelCost, inputIsWholePrompt) {
255
+ const M = 1_000_000;
256
+ const { inputUsdPerMTok, outputUsdPerMTok, cacheWriteUsdPerMTok, cacheReadMultiplier } = modelCost;
257
+ const outputCost = ((outputTokens + reasoningTokens) * outputUsdPerMTok) / M;
258
+ const cacheWriteCost = (cacheWriteTokens * cacheWriteUsdPerMTok) / M;
259
+ if (modelCost.accountingMode === "disjoint") {
260
+ const freshTokens = inputIsWholePrompt
261
+ ? inputTokens - cacheReadTokens - cacheWriteTokens
262
+ : inputTokens;
263
+ const freshCost = (freshTokens * inputUsdPerMTok) / M;
264
+ const cacheReadCost = (cacheReadTokens * inputUsdPerMTok * cacheReadMultiplier) / M;
265
+ return freshCost + cacheWriteCost + cacheReadCost + outputCost;
266
+ }
267
+ const baseCost = (inputTokens * inputUsdPerMTok) / M;
268
+ const cacheReadDiscount = (cacheReadTokens * inputUsdPerMTok * (1 - cacheReadMultiplier)) / M;
269
+ return baseCost + cacheWriteCost + outputCost - cacheReadDiscount;
270
+ }
271
+ export function composeCost(counts, estimate, modelCost) {
272
+ if (counts?.reportedCostUsd != null) {
273
+ return {
274
+ costUsd: counts.reportedCostUsd,
275
+ cost_basis: "provider-reported",
276
+ confidence: "high",
277
+ };
278
+ }
279
+ if (counts != null && modelCost.source !== "unknown") {
280
+ const costUsd = composeTotalUsd(counts.inputTokens, counts.outputTokens, counts.cacheReadTokens ?? 0, counts.cacheCreationTokens ?? 0, counts.reasoningTokens ?? 0, modelCost, false);
281
+ return { costUsd, cost_basis: "derived-from-tokens", confidence: "high" };
282
+ }
283
+ const costUsd = composeTotalUsd(estimate.estInputTokens, estimate.estOutputTokens, estimate.estCacheReadTokens ?? 0, estimate.estCacheWriteTokens ?? 0, 0, modelCost, true);
284
+ return { costUsd, cost_basis: "pre-flight-estimate", confidence: "low" };
285
+ }
@@ -122,6 +122,13 @@ export function familyBaseRisk(provider, fam) {
122
122
  return contract.risk;
123
123
  return fam.safety === "read-only" ? "read_only" : "writes_local_config";
124
124
  }
125
+ function contractAdminProjectionCeiling(provider, commandPath) {
126
+ const exactContract = getCliSubcommandContract(provider, commandPath);
127
+ return exactContract?.adminProjection === "not_exposed" ? "not_exposed" : null;
128
+ }
129
+ function projectedAdminExposure(provider, commandPath, risk) {
130
+ return contractAdminProjectionCeiling(provider, commandPath) ?? adminRiskToExposure(risk);
131
+ }
125
132
  function rootAdvertises(discovered, family) {
126
133
  const wanted = family.toLowerCase();
127
134
  return discovered.rootHelp.subcommands.some(s => s.name.toLowerCase() === wanted);
@@ -142,6 +149,7 @@ export function projectProviderAdminOperations(def, discovered) {
142
149
  if (adminSurfaceKind(fam) !== "cli-subcommand")
143
150
  continue;
144
151
  const baseRisk = familyBaseRisk(def.id, fam);
152
+ const familyExposure = projectedAdminExposure(def.id, [fam.family], baseRisk);
145
153
  if (!discovered) {
146
154
  ops.push({
147
155
  provider: def.id,
@@ -149,8 +157,8 @@ export function projectProviderAdminOperations(def, discovered) {
149
157
  operationId: fam.family,
150
158
  argv: [fam.family],
151
159
  risk: baseRisk,
152
- exposure: adminRiskToExposure(baseRisk),
153
- mutating: adminRiskToExposure(baseRisk) === "mcp_requires_approval",
160
+ exposure: familyExposure,
161
+ mutating: familyExposure === "mcp_requires_approval",
154
162
  available: false,
155
163
  discoverySource: "no-discovery",
156
164
  summary: fam.evidence,
@@ -165,7 +173,7 @@ export function projectProviderAdminOperations(def, discovered) {
165
173
  if (!isSafeAdminToken(sub.name))
166
174
  continue;
167
175
  const risk = classifyOperationRisk(sub.name, baseRisk, { isSubcommand: true });
168
- const exposure = adminRiskToExposure(risk);
176
+ const exposure = projectedAdminExposure(def.id, [fam.family, sub.name], risk);
169
177
  ops.push({
170
178
  provider: def.id,
171
179
  family: fam.family,
@@ -183,7 +191,7 @@ export function projectProviderAdminOperations(def, discovered) {
183
191
  }
184
192
  const baseExposure = adminRiskToExposure(baseRisk);
185
193
  const risk = baseExposure === "not_exposed" ? baseRisk : classifyOperationRisk(fam.family, baseRisk);
186
- const exposure = adminRiskToExposure(risk);
194
+ const exposure = projectedAdminExposure(def.id, [fam.family], risk);
187
195
  ops.push({
188
196
  provider: def.id,
189
197
  family: fam.family,
@@ -97,6 +97,10 @@ export interface ProviderRequestSurface {
97
97
  readonly acpCapable: boolean;
98
98
  readonly syncToolName: string;
99
99
  readonly asyncToolName: string;
100
+ readonly acceptsImages: boolean;
101
+ readonly acceptsAttachments: boolean;
102
+ readonly toolCalling: boolean;
103
+ readonly jsonSchema: boolean;
100
104
  }
101
105
  export interface ProviderDefinition {
102
106
  readonly id: ProviderId;
@@ -5,13 +5,13 @@ export function adminSurfaceKind(family) {
5
5
  }
6
6
  export const DEVIN_ACP_AGENT_TYPES = ["summarizer", "review"];
7
7
  export const PROVIDER_TARGET_VERSIONS = {
8
- claude: "claude 2.1.206",
9
- codex: "codex-cli 0.144.1",
10
- gemini: "agy 1.1.0",
11
- grok: "grok 0.2.93 (f00f96316d)",
8
+ claude: "claude 2.1.207",
9
+ codex: "codex-cli 0.144.3",
10
+ gemini: "agy 1.1.1",
11
+ grok: "grok 0.2.99 (b1b49ccb71)",
12
12
  mistral: "vibe 2.19.1",
13
13
  devin: "devin 3000.1.27 (0d4bf12e)",
14
- cursor: "cursor-agent 2026.07.09-c59fd9a",
14
+ cursor: "cursor-agent 2026.07.09-a3815c0",
15
15
  };
16
16
  const PROVIDER_DEFINITIONS = {
17
17
  claude: {
@@ -28,6 +28,10 @@ const PROVIDER_DEFINITIONS = {
28
28
  acpCapable: false,
29
29
  syncToolName: "claude_request",
30
30
  asyncToolName: "claude_request_async",
31
+ acceptsImages: false,
32
+ acceptsAttachments: false,
33
+ toolCalling: true,
34
+ jsonSchema: true,
31
35
  },
32
36
  docs: { primary: ["https://code.claude.com/docs/en/cli-reference"] },
33
37
  discovery: {
@@ -127,6 +131,10 @@ const PROVIDER_DEFINITIONS = {
127
131
  acpCapable: false,
128
132
  syncToolName: "codex_request",
129
133
  asyncToolName: "codex_request_async",
134
+ acceptsImages: true,
135
+ acceptsAttachments: true,
136
+ toolCalling: true,
137
+ jsonSchema: true,
130
138
  },
131
139
  docs: {
132
140
  primary: [
@@ -264,6 +272,10 @@ const PROVIDER_DEFINITIONS = {
264
272
  acpCapable: false,
265
273
  syncToolName: "gemini_request",
266
274
  asyncToolName: "gemini_request_async",
275
+ acceptsImages: false,
276
+ acceptsAttachments: false,
277
+ toolCalling: true,
278
+ jsonSchema: false,
267
279
  },
268
280
  docs: {
269
281
  primary: [
@@ -363,6 +375,10 @@ const PROVIDER_DEFINITIONS = {
363
375
  acpCapable: true,
364
376
  syncToolName: "grok_request",
365
377
  asyncToolName: "grok_request_async",
378
+ acceptsImages: false,
379
+ acceptsAttachments: false,
380
+ toolCalling: true,
381
+ jsonSchema: true,
366
382
  },
367
383
  docs: {
368
384
  primary: [
@@ -470,6 +486,10 @@ const PROVIDER_DEFINITIONS = {
470
486
  acpCapable: true,
471
487
  syncToolName: "mistral_request",
472
488
  asyncToolName: "mistral_request_async",
489
+ acceptsImages: false,
490
+ acceptsAttachments: false,
491
+ toolCalling: true,
492
+ jsonSchema: false,
473
493
  },
474
494
  docs: { primary: ["https://github.com/mistralai/mistral-vibe"] },
475
495
  discovery: {
@@ -586,6 +606,10 @@ const PROVIDER_DEFINITIONS = {
586
606
  acpCapable: true,
587
607
  syncToolName: "devin_request",
588
608
  asyncToolName: "devin_request_async",
609
+ acceptsImages: false,
610
+ acceptsAttachments: false,
611
+ toolCalling: true,
612
+ jsonSchema: false,
589
613
  },
590
614
  docs: { primary: ["https://docs.devin.ai/cli/reference/commands"] },
591
615
  discovery: {
@@ -697,6 +721,10 @@ const PROVIDER_DEFINITIONS = {
697
721
  acpCapable: true,
698
722
  syncToolName: "cursor_request",
699
723
  asyncToolName: "cursor_request_async",
724
+ acceptsImages: false,
725
+ acceptsAttachments: false,
726
+ toolCalling: true,
727
+ jsonSchema: false,
700
728
  },
701
729
  docs: { primary: ["https://docs.cursor.com/en/cli/overview"] },
702
730
  discovery: {
@@ -615,7 +615,7 @@ const TOOL_CONTROLS = {
615
615
  mistral: {
616
616
  providerKind: "cli",
617
617
  gatewayRequestTools: ["mistral_request", "mistral_request_async"],
618
- summary: "Mistral Vibe owns its runtime tool catalog; the gateway can pass Vibe enabled-tool controls and reports local skills if present.",
618
+ summary: "Mistral Vibe owns its runtime tool catalog; the gateway can pass Vibe enabled- and disabled-tool controls and reports local skills if present.",
619
619
  controls: {
620
620
  allowlist: {
621
621
  supported: true,
@@ -624,9 +624,10 @@ const TOOL_CONTROLS = {
624
624
  behavior: "Each entry is emitted as a separate Vibe enabled-tool flag.",
625
625
  },
626
626
  denylist: {
627
- supported: false,
627
+ supported: true,
628
628
  requestField: "disallowedTools",
629
- behavior: "Accepted for caller parity but ignored because Vibe has no deny-list flag.",
629
+ cliFlag: "--disabled-tools",
630
+ behavior: "Each entry is emitted as a separate Vibe disabled-tool flag.",
630
631
  },
631
632
  mcpServers: {
632
633
  supported: false,
@@ -675,15 +676,11 @@ const TOOL_CONTROLS = {
675
676
  approvalAndSandboxControls: true,
676
677
  costAndLoopControls: true,
677
678
  workspaceAndWorktreeControls: true,
679
+ toolAllowDenyControls: true,
678
680
  enabledToolAllowlist: true,
679
681
  trustControl: true,
680
682
  }),
681
683
  unsupportedInputs: [
682
- {
683
- input: "disallowedTools",
684
- behavior: "ignored",
685
- details: "Accepted for caller parity but ignored because Vibe has no deny-list flag.",
686
- },
687
684
  {
688
685
  input: "mcpServers",
689
686
  behavior: "approval_tracking_only",
@@ -798,7 +795,7 @@ const TOOL_CONTROLS = {
798
795
  supported: true,
799
796
  requestField: "permissionMode",
800
797
  cliFlag: "--permission-mode",
801
- behavior: "Maps to Devin CLI --permission-mode: auto auto-approves read-only tools; smart additionally auto-runs actions a fast model judges safe; dangerous auto-approves all.",
798
+ behavior: "Maps to Devin CLI --permission-mode: auto auto-approves read-only tools; accept-edits also auto-approves workspace edits; smart additionally auto-runs actions a fast model judges safe; dangerous auto-approves all.",
802
799
  },
803
800
  promptControl: {
804
801
  supported: true,
@@ -52,10 +52,9 @@ export interface PrepareMistralRequestInput {
52
52
  export interface PrepareMistralRequestResult {
53
53
  args: string[];
54
54
  env: Record<string, string>;
55
- ignoredDisallowedTools: boolean;
56
55
  }
57
56
  export declare function prepareMistralRequest(input: PrepareMistralRequestInput): PrepareMistralRequestResult;
58
- export declare const CLAUDE_PERMISSION_MODES: readonly ["default", "acceptEdits", "plan", "auto", "dontAsk", "bypassPermissions"];
57
+ export declare const CLAUDE_PERMISSION_MODES: readonly ["default", "acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"];
59
58
  export type ClaudePermissionMode = (typeof CLAUDE_PERMISSION_MODES)[number];
60
59
  export interface ClaudePermissionFlagsInput {
61
60
  permissionMode?: ClaudePermissionMode;
@@ -229,8 +228,8 @@ export declare const CODEX_HIGH_IMPACT_PARAMS_SCHEMA: z.ZodObject<{
229
228
  disable: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
230
229
  }, "strip", z.ZodTypeAny, {
231
230
  search?: boolean | undefined;
232
- outputSchema?: string | Record<string, unknown> | undefined;
233
231
  profile?: string | undefined;
232
+ outputSchema?: string | Record<string, unknown> | undefined;
234
233
  configOverrides?: Record<string, string> | undefined;
235
234
  ephemeral?: boolean | undefined;
236
235
  images?: string[] | undefined;
@@ -240,8 +239,8 @@ export declare const CODEX_HIGH_IMPACT_PARAMS_SCHEMA: z.ZodObject<{
240
239
  disable?: string[] | undefined;
241
240
  }, {
242
241
  search?: boolean | undefined;
243
- outputSchema?: string | Record<string, unknown> | undefined;
244
242
  profile?: string | undefined;
243
+ outputSchema?: string | Record<string, unknown> | undefined;
245
244
  configOverrides?: Record<string, string> | undefined;
246
245
  ephemeral?: boolean | undefined;
247
246
  images?: string[] | undefined;
@@ -3,6 +3,7 @@ import { tmpdir } from "os";
3
3
  import { join, isAbsolute } from "path";
4
4
  import { randomUUID } from "crypto";
5
5
  import { z } from "zod/v3";
6
+ import { CLAUDE_WIRE_PERMISSION_MODES } from "./upstream-contracts.js";
6
7
  export const GATEWAY_SESSION_PREFIX = "gw-";
7
8
  export function validateSessionId(sessionId) {
8
9
  if (sessionId.startsWith(GATEWAY_SESSION_PREFIX)) {
@@ -117,6 +118,12 @@ export function prepareMistralRequest(input) {
117
118
  args.push("--enabled-tools", tool);
118
119
  }
119
120
  }
121
+ if (input.disallowedTools && input.disallowedTools.length > 0) {
122
+ sanitizeCliArgValues(input.disallowedTools, "disallowedTools");
123
+ for (const tool of input.disallowedTools) {
124
+ args.push("--disabled-tools", tool);
125
+ }
126
+ }
120
127
  if (input.trust) {
121
128
  args.push("--trust");
122
129
  }
@@ -137,8 +144,7 @@ export function prepareMistralRequest(input) {
137
144
  args.push("--add-dir", dir);
138
145
  }
139
146
  }
140
- const ignoredDisallowedTools = Boolean(input.disallowedTools && input.disallowedTools.length > 0);
141
- return { args, env, ignoredDisallowedTools };
147
+ return { args, env };
142
148
  }
143
149
  function normalizeMistralOutputFormat(format) {
144
150
  if (format === "plain")
@@ -147,14 +153,7 @@ function normalizeMistralOutputFormat(format) {
147
153
  return "streaming";
148
154
  return format;
149
155
  }
150
- export const CLAUDE_PERMISSION_MODES = [
151
- "default",
152
- "acceptEdits",
153
- "plan",
154
- "auto",
155
- "dontAsk",
156
- "bypassPermissions",
157
- ];
156
+ export const CLAUDE_PERMISSION_MODES = ["default", ...CLAUDE_WIRE_PERMISSION_MODES];
158
157
  export function resolveClaudePermissionFlags(input) {
159
158
  const { permissionMode, dangerouslySkipPermissions } = input;
160
159
  let warning;
@@ -3,7 +3,7 @@ import { type CliType } from "./session-manager.js";
3
3
  import { PerformanceMetrics } from "./metrics.js";
4
4
  import { FlightRecorderQuery } from "./flight-recorder.js";
5
5
  import { type GlobalCacheStats, type PrefixCacheStats, type SessionCacheStats } from "./cache-stats.js";
6
- import { type CacheAwarenessConfig, type ProvidersConfig } from "./config.js";
6
+ import { type CacheAwarenessConfig, type LeastCostConfig, type ProvidersConfig } from "./config.js";
7
7
  import type { AcpConfig } from "./config.js";
8
8
  import { type ResolvedProviderCapability } from "./provider-capability-resolver.js";
9
9
  export interface ResourceDefinition {
@@ -31,7 +31,9 @@ export declare class ResourceProvider {
31
31
  private providers;
32
32
  private capabilityPeek;
33
33
  private acpConfig;
34
- constructor(sessionManager: ISessionManager, performanceMetrics: PerformanceMetrics, flightRecorder?: FlightRecorderQuery, cacheAwareness?: CacheAwarenessConfig | null, providers?: ProvidersConfig | null, capabilityPeek?: (id: CliType) => ResolvedProviderCapability | null, acpConfig?: AcpConfig | null);
34
+ private leastCost;
35
+ constructor(sessionManager: ISessionManager, performanceMetrics: PerformanceMetrics, flightRecorder?: FlightRecorderQuery, cacheAwareness?: CacheAwarenessConfig | null, providers?: ProvidersConfig | null, capabilityPeek?: (id: CliType) => ResolvedProviderCapability | null, acpConfig?: AcpConfig | null, leastCost?: LeastCostConfig | undefined);
36
+ private routingResourcesEnabled;
35
37
  getFlightRecorderQuery(): FlightRecorderQuery;
36
38
  private apiRuntimes;
37
39
  private continuityTrackedApiRuntimes;
@@ -42,7 +44,40 @@ export declare class ResourceProvider {
42
44
  readCacheStateSession(sessionId: string): SessionCacheStats;
43
45
  readCacheStateForPrefix(stablePrefixHash: string): PrefixCacheStats;
44
46
  listResources(): ResourceDefinition[];
47
+ private readRoutingDecisions;
48
+ private readRoutingPriors;
45
49
  private ownedSessions;
46
50
  private ownedActiveId;
47
51
  readResource(uri: string): Promise<ResourceContents | null>;
48
52
  }
53
+ export interface RoutingDecision {
54
+ provider: string;
55
+ model: string;
56
+ tier: string;
57
+ estCostUsd: number | null;
58
+ costBasis: string | null;
59
+ confidence: string | null;
60
+ reason: string | null;
61
+ considered: number | null;
62
+ reroutes: number | null;
63
+ at: string;
64
+ }
65
+ export interface RoutingPriorsPayload {
66
+ priorsScope: string;
67
+ priceAsOf: {
68
+ table: string;
69
+ apiCatalog: string;
70
+ };
71
+ outputPriors: Array<{
72
+ candidate: string;
73
+ median: number;
74
+ p90: number;
75
+ samples: number;
76
+ }>;
77
+ calibration: Array<{
78
+ bucket: string;
79
+ k: number;
80
+ samples: number;
81
+ confidence: string;
82
+ }>;
83
+ }
package/dist/resources.js CHANGED
@@ -4,6 +4,9 @@ import { getRequestContext, principalCanAccess, resolveOwnerPrincipal } from "./
4
4
  import { getAvailableCliInfo } from "./model-registry.js";
5
5
  import { computeGlobalCacheStats, computePrefixCacheStats, computeSessionCacheStats, computeTtlRemaining, } from "./cache-stats.js";
6
6
  import { enabledApiProviders, } from "./config.js";
7
+ import { computeLcrPriorsFromDb } from "./lcr-priors.js";
8
+ import { telemetryTierFor } from "./lcr-telemetry.js";
9
+ import { PRICING_AS_OF, API_CATALOG_AS_OF } from "./pricing.js";
7
10
  import { apiContinuityForKind } from "./api-provider.js";
8
11
  import { apiProviderCatalogEntry } from "./api-request.js";
9
12
  import { buildProviderSubcommandsCompactCatalog, getCliSubcommandContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
@@ -20,7 +23,8 @@ export class ResourceProvider {
20
23
  providers;
21
24
  capabilityPeek;
22
25
  acpConfig;
23
- constructor(sessionManager, performanceMetrics, flightRecorder = { queryRequests: () => [] }, cacheAwareness = null, providers = null, capabilityPeek = peekProviderCapabilitySet, acpConfig = null) {
26
+ leastCost;
27
+ constructor(sessionManager, performanceMetrics, flightRecorder = { queryRequests: () => [] }, cacheAwareness = null, providers = null, capabilityPeek = peekProviderCapabilitySet, acpConfig = null, leastCost = undefined) {
24
28
  this.sessionManager = sessionManager;
25
29
  this.performanceMetrics = performanceMetrics;
26
30
  this.flightRecorder = flightRecorder;
@@ -28,6 +32,10 @@ export class ResourceProvider {
28
32
  this.providers = providers;
29
33
  this.capabilityPeek = capabilityPeek;
30
34
  this.acpConfig = acpConfig;
35
+ this.leastCost = leastCost;
36
+ }
37
+ routingResourcesEnabled() {
38
+ return this.leastCost?.enabled === true;
31
39
  }
32
40
  getFlightRecorderQuery() {
33
41
  return this.flightRecorder;
@@ -172,8 +180,79 @@ export class ResourceProvider {
172
180
  priority: 0.7,
173
181
  },
174
182
  })),
183
+ ...(this.routingResourcesEnabled()
184
+ ? [
185
+ {
186
+ uri: "routing://decisions",
187
+ name: "Routing Decisions",
188
+ title: "Least-Cost Routing Decisions",
189
+ description: "Recent redacted least-cost routing decisions (provider/model/tier/est cost/confidence)",
190
+ mimeType: "application/json",
191
+ annotations: {
192
+ audience: ["user", "assistant"],
193
+ priority: 0.7,
194
+ },
195
+ },
196
+ {
197
+ uri: "routing://priors",
198
+ name: "Routing Priors",
199
+ title: "Least-Cost Routing Priors",
200
+ description: "Learned output-token priors + input-token calibration (k, samples, quality) + price asOf",
201
+ mimeType: "application/json",
202
+ annotations: {
203
+ audience: ["user", "assistant"],
204
+ priority: 0.7,
205
+ },
206
+ },
207
+ ]
208
+ : []),
175
209
  ];
176
210
  }
211
+ readRoutingDecisions() {
212
+ const rows = this.flightRecorder.queryRequests(`SELECT r.cli, r.model, r.datetime_utc, r.cost_basis,
213
+ m.route_est_cost_usd, m.route_est_confidence, m.route_reason,
214
+ m.route_considered, m.route_reroutes
215
+ FROM requests r
216
+ LEFT JOIN gateway_metadata m ON m.request_id = r.id
217
+ WHERE m.routed = 1
218
+ ORDER BY r.datetime_utc DESC
219
+ LIMIT 50`);
220
+ return rows.map(row => ({
221
+ provider: row.cli,
222
+ model: row.model,
223
+ tier: telemetryTierFor(row.cli),
224
+ estCostUsd: row.route_est_cost_usd ?? null,
225
+ costBasis: row.cost_basis ?? null,
226
+ confidence: row.route_est_confidence ?? null,
227
+ reason: row.route_reason ?? null,
228
+ considered: row.route_considered ?? null,
229
+ reroutes: row.route_reroutes ?? null,
230
+ at: row.datetime_utc,
231
+ }));
232
+ }
233
+ readRoutingPriors() {
234
+ const scope = this.leastCost?.priorsScope ?? "off";
235
+ const priors = computeLcrPriorsFromDb(this.flightRecorder, {
236
+ priorsScope: scope,
237
+ ownerPrincipal: resolveOwnerPrincipal(getRequestContext()),
238
+ });
239
+ return {
240
+ priorsScope: scope,
241
+ priceAsOf: { table: PRICING_AS_OF, apiCatalog: API_CATALOG_AS_OF },
242
+ outputPriors: Array.from(priors.outputPriors, ([key, prior]) => ({
243
+ candidate: key,
244
+ median: prior.median,
245
+ p90: prior.p90,
246
+ samples: prior.samples,
247
+ })),
248
+ calibration: Array.from(priors.calibration, ([key, bucket]) => ({
249
+ bucket: key,
250
+ k: bucket.k,
251
+ samples: bucket.samples,
252
+ confidence: bucket.confidence,
253
+ })),
254
+ };
255
+ }
177
256
  ownedSessions(sessions) {
178
257
  const caller = resolveOwnerPrincipal(getRequestContext());
179
258
  const owned = sessions.filter(s => principalCanAccess(s.ownerPrincipal, caller));
@@ -256,6 +335,22 @@ export class ResourceProvider {
256
335
  };
257
336
  }
258
337
  }
338
+ if (this.routingResourcesEnabled()) {
339
+ if (uri === "routing://decisions") {
340
+ return {
341
+ uri,
342
+ mimeType: "application/json",
343
+ text: JSON.stringify({ decisions: this.readRoutingDecisions() }, null, 2),
344
+ };
345
+ }
346
+ if (uri === "routing://priors") {
347
+ return {
348
+ uri,
349
+ mimeType: "application/json",
350
+ text: JSON.stringify(this.readRoutingPriors(), null, 2),
351
+ };
352
+ }
353
+ }
259
354
  if (uri === "metrics://performance") {
260
355
  return {
261
356
  uri,
package/dist/retry.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface RetryOptions {
19
19
  isTransient: (error: any) => boolean;
20
20
  onRetry: (error: any, attempt: number, delay: number) => void;
21
21
  }
22
+ export declare const isDefaultTransient: (error: any) => boolean;
22
23
  export declare function createCircuitBreaker(options?: {
23
24
  resetTimeout?: number;
24
25
  failureThreshold?: number;
package/dist/retry.js CHANGED
@@ -4,7 +4,7 @@ export var CircuitBreakerState;
4
4
  CircuitBreakerState["OPEN"] = "OPEN";
5
5
  CircuitBreakerState["HALF_OPEN"] = "HALF_OPEN";
6
6
  })(CircuitBreakerState || (CircuitBreakerState = {}));
7
- const isDefaultTransient = (error) => {
7
+ export const isDefaultTransient = (error) => {
8
8
  if (!error) {
9
9
  return false;
10
10
  }
@@ -0,0 +1,6 @@
1
+ export type ContentType = "prose" | "code" | "cjk";
2
+ export declare function classifyContent(text: string): ContentType;
3
+ export declare function estimateInputTokens(text: string, opts?: {
4
+ family?: string;
5
+ calibrationK?: number;
6
+ }): number;