llm-cli-gateway 2.15.0 → 2.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +78 -1
  3. package/dist/acp/client.js +5 -0
  4. package/dist/acp/flight-redaction.d.ts +2 -0
  5. package/dist/acp/flight-redaction.js +2 -0
  6. package/dist/acp/process-manager.js +2 -1
  7. package/dist/acp/provider-registry.js +8 -8
  8. package/dist/acp/runtime.d.ts +1 -0
  9. package/dist/acp/runtime.js +20 -0
  10. package/dist/acp/types.d.ts +52 -0
  11. package/dist/acp/types.js +8 -0
  12. package/dist/api-provider.d.ts +1 -0
  13. package/dist/api-provider.js +1 -0
  14. package/dist/async-job-manager.d.ts +20 -1
  15. package/dist/async-job-manager.js +85 -28
  16. package/dist/claude-mcp-config.js +1 -1
  17. package/dist/compressor/estimate.d.ts +5 -0
  18. package/dist/compressor/estimate.js +21 -0
  19. package/dist/compressor/index.d.ts +23 -0
  20. package/dist/compressor/index.js +77 -0
  21. package/dist/compressor/router.d.ts +2 -0
  22. package/dist/compressor/router.js +57 -0
  23. package/dist/compressor/transforms/ansi.d.ts +3 -0
  24. package/dist/compressor/transforms/ansi.js +99 -0
  25. package/dist/compressor/transforms/json.d.ts +1 -0
  26. package/dist/compressor/transforms/json.js +156 -0
  27. package/dist/compressor/transforms/log.d.ts +12 -0
  28. package/dist/compressor/transforms/log.js +55 -0
  29. package/dist/compressor/transforms/whitespace.d.ts +8 -0
  30. package/dist/compressor/transforms/whitespace.js +79 -0
  31. package/dist/config.d.ts +39 -0
  32. package/dist/config.js +166 -6
  33. package/dist/db.js +4 -4
  34. package/dist/doctor.d.ts +34 -1
  35. package/dist/doctor.js +85 -3
  36. package/dist/executor.d.ts +6 -0
  37. package/dist/executor.js +16 -3
  38. package/dist/flight-recorder.d.ts +19 -0
  39. package/dist/flight-recorder.js +110 -2
  40. package/dist/http-transport.js +19 -21
  41. package/dist/index.d.ts +58 -2
  42. package/dist/index.js +929 -94
  43. package/dist/job-store.d.ts +14 -2
  44. package/dist/job-store.js +170 -43
  45. package/dist/lcr-priors.d.ts +60 -0
  46. package/dist/lcr-priors.js +190 -0
  47. package/dist/lcr-router-env.d.ts +20 -0
  48. package/dist/lcr-router-env.js +133 -0
  49. package/dist/lcr-telemetry.d.ts +2 -0
  50. package/dist/lcr-telemetry.js +17 -0
  51. package/dist/least-cost-router.d.ts +86 -0
  52. package/dist/least-cost-router.js +296 -0
  53. package/dist/least-cost-types.d.ts +34 -0
  54. package/dist/least-cost-types.js +1 -0
  55. package/dist/migrate-sessions.js +1 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/model-registry.js +1 -1
  58. package/dist/postgres-job-store-worker.js +56 -13
  59. package/dist/pricing.d.ts +17 -0
  60. package/dist/pricing.js +167 -0
  61. package/dist/provider-definitions.d.ts +5 -0
  62. package/dist/provider-definitions.js +56 -10
  63. package/dist/provider-tool-capabilities.js +3 -3
  64. package/dist/request-helpers.d.ts +2 -2
  65. package/dist/request-helpers.js +1 -1
  66. package/dist/resources.d.ts +37 -2
  67. package/dist/resources.js +96 -1
  68. package/dist/retry.d.ts +1 -0
  69. package/dist/retry.js +1 -1
  70. package/dist/spawn-env-isolation.d.ts +10 -0
  71. package/dist/spawn-env-isolation.js +55 -0
  72. package/dist/token-estimator.d.ts +6 -0
  73. package/dist/token-estimator.js +59 -0
  74. package/dist/upstream-contracts.js +48 -28
  75. package/dist/validation-receipt.js +1 -1
  76. package/dist/validation-tools.d.ts +6 -0
  77. package/dist/validation-tools.js +174 -54
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +15 -6
  80. package/setup/status.schema.json +68 -0
package/dist/doctor.js CHANGED
@@ -7,7 +7,11 @@ import { createEndpointExposureReport, redactDiagnosticUrl, } from "./endpoint-e
7
7
  import { getApiProviderStatus, listProviderRuntimeStatuses, } from "./provider-status.js";
8
8
  import { getApiProviderLoginGuidance, } from "./provider-login-guidance.js";
9
9
  import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
10
- import { diagnoseRemoteOAuthConfig, enabledApiProviders, loadCacheAwarenessConfig, loadProvidersConfig, } from "./config.js";
10
+ import { defaultLeastCostConfig, diagnoseRemoteOAuthConfig, enabledApiProviders, loadCacheAwarenessConfig, loadLeastCostConfig, loadProvidersConfig, } from "./config.js";
11
+ import { telemetryTierFor } from "./lcr-telemetry.js";
12
+ import { API_CATALOG_AS_OF, PRICING_AS_OF, getModelCost, modelIdToFamily } from "./pricing.js";
13
+ import { computeLcrPriorsFromDb } from "./lcr-priors.js";
14
+ import { getCliInfo } from "./model-registry.js";
11
15
  import { loadWorkspaceRegistry, remoteSafeWorkspaceSummary, } from "./workspace-registry.js";
12
16
  import { buildRemoteConnectorUrls, resolveConfiguredRemoteOrigin } from "./remote-url.js";
13
17
  import { computeGlobalCacheStats } from "./cache-stats.js";
@@ -58,7 +62,7 @@ export function checkVibeSessionLogging(home = homedir()) {
58
62
  function parseVibeSessionLoggingEnabled(text) {
59
63
  const lines = text.split(/\r?\n/);
60
64
  let inSection = false;
61
- for (let raw of lines) {
65
+ for (const raw of lines) {
62
66
  const line = raw.replace(/#.*$/, "").trim();
63
67
  if (!line)
64
68
  continue;
@@ -426,6 +430,70 @@ function buildCacheAwarenessReport(opts) {
426
430
  per_cli: perCli,
427
431
  };
428
432
  }
433
+ const MS_PER_DAY = 86_400_000;
434
+ function computePricingStaleDays(runIso) {
435
+ const run = Date.parse(runIso);
436
+ const table = Date.parse(PRICING_AS_OF);
437
+ const catalog = Date.parse(API_CATALOG_AS_OF);
438
+ const newer = Math.max(table, catalog);
439
+ if (!Number.isFinite(run) || !Number.isFinite(newer))
440
+ return 0;
441
+ const days = Math.floor((run - newer) / MS_PER_DAY);
442
+ return days > 0 ? days : 0;
443
+ }
444
+ function buildLeastCostReport(cfg, generatedAt, flightRecorder) {
445
+ const pricing = {
446
+ tableAsOf: PRICING_AS_OF,
447
+ apiCatalogAsOf: API_CATALOG_AS_OF,
448
+ staleDays: computePricingStaleDays(generatedAt),
449
+ };
450
+ if (!cfg.enabled) {
451
+ return { enabled: false, pricing, providers: [], untieredModels: [], calibrationQuality: [] };
452
+ }
453
+ const tierMap = new Map(Object.entries(cfg.tiers));
454
+ const cliInfo = getCliInfo();
455
+ const providers = [];
456
+ const untieredModels = [];
457
+ for (const provider of CLI_TYPES) {
458
+ const candidates = Object.keys(cliInfo[provider].models).map(model => {
459
+ const family = modelIdToFamily(model);
460
+ const cost = getModelCost(provider, model, { preferCatalog: cfg.preferCatalogPrice });
461
+ const tier = tierMap.get(`${provider}:${family}`) ?? null;
462
+ if (tier === null) {
463
+ untieredModels.push(`${provider}:${model}`);
464
+ }
465
+ return {
466
+ model,
467
+ family,
468
+ priced: cost.source !== "unknown",
469
+ priceSource: cost.source,
470
+ tier,
471
+ };
472
+ });
473
+ providers.push({
474
+ provider,
475
+ telemetryTier: telemetryTierFor(provider),
476
+ candidates,
477
+ });
478
+ }
479
+ const calibrationQuality = [];
480
+ if (flightRecorder && cfg.priorsScope !== "off") {
481
+ try {
482
+ const priors = computeLcrPriorsFromDb(flightRecorder, { priorsScope: cfg.priorsScope });
483
+ for (const [bucket, entry] of priors.calibration) {
484
+ calibrationQuality.push({
485
+ bucket,
486
+ k: entry.k,
487
+ samples: entry.samples,
488
+ confidence: entry.confidence,
489
+ });
490
+ }
491
+ }
492
+ catch {
493
+ }
494
+ }
495
+ return { enabled: true, pricing, providers, untieredModels, calibrationQuality };
496
+ }
429
497
  function buildProviderCapabilitySummary(providerStatuses) {
430
498
  const capabilities = getProviderToolCapabilities({
431
499
  includeSkills: true,
@@ -502,10 +570,12 @@ export function createDoctorReport(envOrOptions = process.env) {
502
570
  if (probeReport) {
503
571
  upstream.probe_report = probeReport;
504
572
  }
573
+ const generatedAt = new Date().toISOString();
574
+ const leastCostConfig = opts.leastCost ?? defaultLeastCostConfig();
505
575
  const report = {
506
576
  schema_version: "1.0",
507
577
  ok: true,
508
- generated_at: new Date().toISOString(),
578
+ generated_at: generatedAt,
509
579
  system: {
510
580
  os: platform(),
511
581
  arch: arch(),
@@ -564,6 +634,7 @@ export function createDoctorReport(envOrOptions = process.env) {
564
634
  client_config: clientConfigStatus(),
565
635
  cache_awareness: buildCacheAwarenessReport(opts),
566
636
  provider_capabilities: buildProviderCapabilitySummary(providerStatuses),
637
+ least_cost: buildLeastCostReport(leastCostConfig, generatedAt, opts.flightRecorder),
567
638
  upstream,
568
639
  next_actions: [],
569
640
  };
@@ -613,11 +684,17 @@ export async function printDoctorJson(opts = {}) {
613
684
  let cacheAwareness;
614
685
  let flightRecorder;
615
686
  let providersConfig;
687
+ let leastCost;
616
688
  try {
617
689
  cacheAwareness = loadCacheAwarenessConfig();
618
690
  }
619
691
  catch {
620
692
  }
693
+ try {
694
+ leastCost = loadLeastCostConfig();
695
+ }
696
+ catch {
697
+ }
621
698
  try {
622
699
  const dbPath = resolveFlightRecorderDbPath();
623
700
  if (dbPath)
@@ -644,6 +721,7 @@ export async function printDoctorJson(opts = {}) {
644
721
  probeUpstream: opts.probeUpstream,
645
722
  providersConfig,
646
723
  apiReachability,
724
+ leastCost,
647
725
  });
648
726
  process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
649
727
  if (flightRecorder) {
@@ -685,6 +763,10 @@ function isCreateDoctorReportOptions(value) {
685
763
  const candidate = value.flightRecorder;
686
764
  return candidate === undefined || typeof candidate === "object";
687
765
  }
766
+ if (Object.prototype.hasOwnProperty.call(value, "leastCost")) {
767
+ const candidate = value.leastCost;
768
+ return candidate === undefined || typeof candidate === "object";
769
+ }
688
770
  if (Object.prototype.hasOwnProperty.call(value, "env")) {
689
771
  const candidate = value.env;
690
772
  return candidate === undefined || typeof candidate === "object";
@@ -1,5 +1,7 @@
1
1
  import { ChildProcess, type SpawnOptions } from "child_process";
2
+ import { CircuitBreakerState } from "./retry.js";
2
3
  import type { Logger } from "./logger.js";
4
+ import type { CliType } from "./provider-types.js";
3
5
  export interface ExecuteOptions {
4
6
  timeout?: number;
5
7
  idleTimeout?: number;
@@ -14,6 +16,9 @@ export interface ExecuteResult {
14
16
  code: number;
15
17
  }
16
18
  export declare function providerCommandName(command: string): string;
19
+ export declare function cliBreakerState(cli: CliType): CircuitBreakerState;
20
+ export declare function resetCliBreakersForTest(): void;
21
+ export declare function primeCliBreakerStateForTest(command: string, state: CircuitBreakerState): void;
17
22
  export declare function buildExtendedPath(env?: NodeJS.ProcessEnv, home?: string, nodePath?: string, platform?: NodeJS.Platform): string;
18
23
  export declare function getExtendedPath(): string;
19
24
  export declare function envWithExtendedPath(baseEnv?: NodeJS.ProcessEnv, extendedPath?: string, platform?: NodeJS.Platform): NodeJS.ProcessEnv;
@@ -35,5 +40,6 @@ export declare function spawnCliProcess(command: string, args: string[], options
35
40
  cwd?: string;
36
41
  env: NodeJS.ProcessEnv;
37
42
  stdio: SpawnOptions["stdio"];
43
+ logger?: Logger;
38
44
  }): ChildProcess;
39
45
  export declare function executeCli(command: string, args: string[], options?: ExecuteOptions): Promise<ExecuteResult>;
package/dist/executor.js CHANGED
@@ -2,7 +2,8 @@ import { spawn, spawnSync } from "child_process";
2
2
  import { homedir } from "os";
3
3
  import { delimiter, join, dirname, extname, win32 } from "path";
4
4
  import { readdirSync, existsSync } from "fs";
5
- import { createCircuitBreaker, withRetry } from "./retry.js";
5
+ import { createCircuitBreaker, withRetry, CircuitBreakerState } from "./retry.js";
6
+ import { applySpawnEnvIsolation } from "./spawn-env-isolation.js";
6
7
  export function providerCommandName(command) {
7
8
  if (command === "gemini")
8
9
  return "agy";
@@ -24,6 +25,16 @@ function getCircuitBreaker(command) {
24
25
  circuitBreakers.set(command, circuitBreaker);
25
26
  return circuitBreaker;
26
27
  }
28
+ export function cliBreakerState(cli) {
29
+ const command = providerCommandName(cli);
30
+ return circuitBreakers.get(command)?.state ?? CircuitBreakerState.CLOSED;
31
+ }
32
+ export function resetCliBreakersForTest() {
33
+ circuitBreakers.clear();
34
+ }
35
+ export function primeCliBreakerStateForTest(command, state) {
36
+ getCircuitBreaker(command).state = state;
37
+ }
27
38
  function getNvmPath() {
28
39
  if (cachedNvmPath !== undefined) {
29
40
  return cachedNvmPath;
@@ -309,9 +320,10 @@ function killWindowsProcessTree(pid) {
309
320
  return result.status === 0;
310
321
  }
311
322
  export function spawnCliProcess(command, args, options) {
323
+ const env = applySpawnEnvIsolation(options.env, options.logger);
312
324
  const detached = shouldDetachProviderProcess();
313
325
  const resolved = resolveCommandForSpawn(command, args, {
314
- envPath: pathValueFromEnv(options.env, process.platform),
326
+ envPath: pathValueFromEnv(env, process.platform),
315
327
  });
316
328
  const proc = spawn(resolved.command, resolved.args, {
317
329
  cwd: options.cwd,
@@ -319,7 +331,7 @@ export function spawnCliProcess(command, args, options) {
319
331
  windowsHide: true,
320
332
  windowsVerbatimArguments: resolved.windowsVerbatimArguments,
321
333
  stdio: options.stdio,
322
- env: options.env,
334
+ env,
323
335
  });
324
336
  if (proc.pid)
325
337
  registerProcessGroup(proc.pid);
@@ -337,6 +349,7 @@ export async function executeCli(command, args, options = {}) {
337
349
  cwd,
338
350
  stdio,
339
351
  env: { ...baseEnv, ...(extraEnv ?? {}) },
352
+ logger: options.logger,
340
353
  });
341
354
  if (stdin !== undefined && proc.stdin) {
342
355
  proc.stdin.write(stdin);
@@ -23,6 +23,7 @@ export interface FlightLogResult {
23
23
  retryCount: number;
24
24
  circuitBreakerState: string;
25
25
  costUsd?: number;
26
+ costBasis?: string;
26
27
  approvalDecision?: string;
27
28
  optimizationApplied: boolean;
28
29
  thinkingBlocks?: string[];
@@ -37,6 +38,20 @@ interface LoggerLike {
37
38
  info: (message: string, ...args: any[]) => void;
38
39
  error: (message: string, ...args: any[]) => void;
39
40
  }
41
+ export interface CompressionTelemetry {
42
+ route: string;
43
+ transforms: string[];
44
+ originalChars: number;
45
+ compressedChars: number;
46
+ estimatedTokensSaved: number;
47
+ }
48
+ export interface RoutingRecord {
49
+ estCostUsd?: number | null;
50
+ estConfidence?: string | null;
51
+ reason?: string | null;
52
+ considered?: number | null;
53
+ reroutes?: number | null;
54
+ }
40
55
  export declare function resolveFlightRecorderDbPath(): string | null;
41
56
  export declare class FlightRecorder {
42
57
  private db;
@@ -51,6 +66,8 @@ export declare class FlightRecorder {
51
66
  });
52
67
  logStart(entry: FlightLogStart): void;
53
68
  logComplete(correlationId: string, result: FlightLogResult): void;
69
+ recordCompressionTelemetry(correlationId: string, telemetry: CompressionTelemetry): void;
70
+ recordRouting(correlationId: string, routing: RoutingRecord): void;
54
71
  private redactStart;
55
72
  private redactResult;
56
73
  queryRequests<T = Record<string, unknown>>(sql: string, ...params: unknown[]): T[];
@@ -60,6 +77,8 @@ export declare class FlightRecorder {
60
77
  export declare class NoopFlightRecorder {
61
78
  logStart(_entry: FlightLogStart): void;
62
79
  logComplete(_correlationId: string, _result: FlightLogResult): void;
80
+ recordCompressionTelemetry(_correlationId: string, _telemetry: CompressionTelemetry): void;
81
+ recordRouting(_correlationId: string, _routing: RoutingRecord): void;
63
82
  queryRequests<T = Record<string, unknown>>(_sql: string, ..._params: unknown[]): T[];
64
83
  flush(): void;
65
84
  close(): void;
@@ -64,6 +64,54 @@ function ensureMetadataProviderSessionColumns(db) {
64
64
  db.exec("ALTER TABLE gateway_metadata ADD COLUMN stop_reason TEXT");
65
65
  }
66
66
  }
67
+ function ensureCompressionColumns(db) {
68
+ const rows = db.prepare("PRAGMA table_info(gateway_metadata)").all();
69
+ const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
70
+ if (!names.has("compression_route")) {
71
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_route TEXT");
72
+ }
73
+ if (!names.has("compression_transforms")) {
74
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_transforms TEXT");
75
+ }
76
+ if (!names.has("compression_original_chars")) {
77
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_original_chars INTEGER");
78
+ }
79
+ if (!names.has("compression_compressed_chars")) {
80
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_compressed_chars INTEGER");
81
+ }
82
+ if (!names.has("compression_tokens_saved_est")) {
83
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_tokens_saved_est INTEGER");
84
+ }
85
+ }
86
+ function ensureRequestsCostBasisColumn(db) {
87
+ const rows = db.prepare("PRAGMA table_info(requests)").all();
88
+ const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
89
+ if (!names.has("cost_basis")) {
90
+ db.exec("ALTER TABLE requests ADD COLUMN cost_basis TEXT");
91
+ }
92
+ }
93
+ function ensureMetadataRoutingColumns(db) {
94
+ const rows = db.prepare("PRAGMA table_info(gateway_metadata)").all();
95
+ const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
96
+ if (!names.has("routed")) {
97
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN routed INTEGER");
98
+ }
99
+ if (!names.has("route_est_cost_usd")) {
100
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN route_est_cost_usd REAL");
101
+ }
102
+ if (!names.has("route_est_confidence")) {
103
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN route_est_confidence TEXT");
104
+ }
105
+ if (!names.has("route_reason")) {
106
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN route_reason TEXT");
107
+ }
108
+ if (!names.has("route_considered")) {
109
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN route_considered INTEGER");
110
+ }
111
+ if (!names.has("route_reroutes")) {
112
+ db.exec("ALTER TABLE gateway_metadata ADD COLUMN route_reroutes INTEGER");
113
+ }
114
+ }
67
115
  export function resolveFlightRecorderDbPath() {
68
116
  const configured = process.env.LLM_GATEWAY_LOGS_DB;
69
117
  if (configured !== undefined) {
@@ -142,7 +190,8 @@ export class FlightRecorder {
142
190
  output_tokens INTEGER,
143
191
  cache_read_tokens INTEGER,
144
192
  cache_creation_tokens INTEGER,
145
- owner_principal TEXT
193
+ owner_principal TEXT,
194
+ cost_basis TEXT
146
195
  );
147
196
 
148
197
  CREATE TABLE IF NOT EXISTS gateway_metadata (
@@ -159,6 +208,12 @@ export class FlightRecorder {
159
208
  async_job_id TEXT,
160
209
  provider_session_id TEXT,
161
210
  stop_reason TEXT,
211
+ routed INTEGER,
212
+ route_est_cost_usd REAL,
213
+ route_est_confidence TEXT,
214
+ route_reason TEXT,
215
+ route_considered INTEGER,
216
+ route_reroutes INTEGER,
162
217
  status TEXT NOT NULL DEFAULT 'started'
163
218
  );
164
219
 
@@ -196,6 +251,18 @@ export class FlightRecorder {
196
251
  this.db
197
252
  .prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(7, ?)")
198
253
  .run(new Date().toISOString());
254
+ ensureCompressionColumns(this.db);
255
+ this.db
256
+ .prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(8, ?)")
257
+ .run(new Date().toISOString());
258
+ ensureRequestsCostBasisColumn(this.db);
259
+ this.db
260
+ .prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(9, ?)")
261
+ .run(new Date().toISOString());
262
+ ensureMetadataRoutingColumns(this.db);
263
+ this.db
264
+ .prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(10, ?)")
265
+ .run(new Date().toISOString());
199
266
  if (process.platform !== "win32") {
200
267
  try {
201
268
  chmodSync(dbPath, 0o600);
@@ -242,7 +309,8 @@ export class FlightRecorder {
242
309
  input_tokens = @input_tokens,
243
310
  output_tokens = @output_tokens,
244
311
  cache_read_tokens = @cache_read_tokens,
245
- cache_creation_tokens = @cache_creation_tokens
312
+ cache_creation_tokens = @cache_creation_tokens,
313
+ cost_basis = @cost_basis
246
314
  WHERE id = @id
247
315
  `);
248
316
  const updateMetadata = this.db.prepare(`
@@ -273,6 +341,7 @@ export class FlightRecorder {
273
341
  output_tokens: result.outputTokens ?? null,
274
342
  cache_read_tokens: result.cacheReadTokens ?? null,
275
343
  cache_creation_tokens: result.cacheCreationTokens ?? null,
344
+ cost_basis: result.costBasis ?? null,
276
345
  });
277
346
  updateMetadata.run({
278
347
  id: correlationId,
@@ -297,6 +366,43 @@ export class FlightRecorder {
297
366
  logComplete(correlationId, result) {
298
367
  this.updateCompleteTxn(correlationId, this.redactEnabled ? this.redactResult(result) : result);
299
368
  }
369
+ recordCompressionTelemetry(correlationId, telemetry) {
370
+ this.db
371
+ .prepare(`UPDATE gateway_metadata
372
+ SET compression_route = @route,
373
+ compression_transforms = @transforms,
374
+ compression_original_chars = @original_chars,
375
+ compression_compressed_chars = @compressed_chars,
376
+ compression_tokens_saved_est = @tokens_saved_est
377
+ WHERE request_id = @id AND compression_route IS NULL`)
378
+ .run({
379
+ id: correlationId,
380
+ route: telemetry.route,
381
+ transforms: telemetry.transforms.join(","),
382
+ original_chars: telemetry.originalChars,
383
+ compressed_chars: telemetry.compressedChars,
384
+ tokens_saved_est: telemetry.estimatedTokensSaved,
385
+ });
386
+ }
387
+ recordRouting(correlationId, routing) {
388
+ this.db
389
+ .prepare(`UPDATE gateway_metadata
390
+ SET routed = 1,
391
+ route_est_cost_usd = @est_cost_usd,
392
+ route_est_confidence = @est_confidence,
393
+ route_reason = @reason,
394
+ route_considered = @considered,
395
+ route_reroutes = @reroutes
396
+ WHERE request_id = @id`)
397
+ .run({
398
+ id: correlationId,
399
+ est_cost_usd: routing.estCostUsd ?? null,
400
+ est_confidence: routing.estConfidence ?? null,
401
+ reason: routing.reason ?? null,
402
+ considered: routing.considered ?? null,
403
+ reroutes: routing.reroutes ?? null,
404
+ });
405
+ }
300
406
  redactStart(entry) {
301
407
  return {
302
408
  ...entry,
@@ -330,6 +436,8 @@ export class FlightRecorder {
330
436
  export class NoopFlightRecorder {
331
437
  logStart(_entry) { }
332
438
  logComplete(_correlationId, _result) { }
439
+ recordCompressionTelemetry(_correlationId, _telemetry) { }
440
+ recordRouting(_correlationId, _routing) { }
333
441
  queryRequests(_sql, ..._params) {
334
442
  return [];
335
443
  }
@@ -19,12 +19,7 @@ async function readBody(req) {
19
19
  const raw = await readCappedRawBody(req, maxHttpBodyBytes());
20
20
  if (!raw)
21
21
  return undefined;
22
- try {
23
- return JSON.parse(raw);
24
- }
25
- catch (error) {
26
- throw error;
27
- }
22
+ return JSON.parse(raw);
28
23
  }
29
24
  function methodNotAllowed(res) {
30
25
  res.writeHead(405, { allow: "GET, POST, DELETE", "content-type": "application/json" });
@@ -34,10 +29,6 @@ function jsonError(res, status, message) {
34
29
  res.writeHead(status, { "content-type": "application/json" });
35
30
  res.end(JSON.stringify({ error: message }));
36
31
  }
37
- function jsonResponse(res, status, body) {
38
- res.writeHead(status, { "content-type": "application/json" });
39
- res.end(JSON.stringify(body));
40
- }
41
32
  function parseNoAuthPaths(raw, protectedPath) {
42
33
  const paths = new Set();
43
34
  for (const value of (raw ?? "").split(/[,;\s]+/)) {
@@ -133,21 +124,21 @@ export async function startHttpGateway(options) {
133
124
  }
134
125
  async function createSession(releaseInitializeReservation) {
135
126
  const gatewayServer = options.createGatewayServer(options.deps);
136
- let entry;
127
+ const onSessionInitialized = (sessionId) => {
128
+ const initializedAt = Date.now();
129
+ releaseInitializeReservation();
130
+ entry.sessionId = sessionId;
131
+ entry.createdAt = initializedAt;
132
+ entry.lastActivityAt = initializedAt;
133
+ entry.inFlight++;
134
+ sessions.set(sessionId, entry);
135
+ };
137
136
  const transport = new StreamableHTTPServerTransport({
138
137
  sessionIdGenerator: () => randomUUID(),
139
- onsessioninitialized: sessionId => {
140
- const now = Date.now();
141
- releaseInitializeReservation();
142
- entry.sessionId = sessionId;
143
- entry.createdAt = now;
144
- entry.lastActivityAt = now;
145
- entry.inFlight++;
146
- sessions.set(sessionId, entry);
147
- },
138
+ onsessioninitialized: onSessionInitialized,
148
139
  });
149
140
  const now = Date.now();
150
- entry = {
141
+ const entry = {
151
142
  server: gatewayServer,
152
143
  transport,
153
144
  createdAt: now,
@@ -209,6 +200,9 @@ export async function startHttpGateway(options) {
209
200
  if (manager) {
210
201
  const limiter = manager.getLimiterSnapshot();
211
202
  const limits = manager.getConfiguredLimits();
203
+ const durableAdmission = manager.getDurableAdmissionHealth();
204
+ const persistence = options.deps?.persistence;
205
+ const durablePersistenceConfigured = persistence !== undefined && persistence.backend !== "none" && persistence.asyncJobsEnabled;
212
206
  payload.jobs = {
213
207
  running: limiter.running,
214
208
  queued: limiter.queued,
@@ -222,7 +216,11 @@ export async function startHttpGateway(options) {
222
216
  saturated: limiter.saturated,
223
217
  completedJobMemoryTtlMs: limits.completedJobMemoryTtlMs,
224
218
  maxJobOutputBytes: limits.maxJobOutputBytes,
219
+ durableAdmission,
225
220
  };
221
+ payload.ready = durableAdmission.storeAttached
222
+ ? durableAdmission.admitting
223
+ : !durablePersistenceConfigured;
226
224
  }
227
225
  return payload;
228
226
  }
package/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@ import { z } from "zod/v3";
4
4
  import { ISessionManager, type CliType, type ProviderType } from "./session-manager.js";
5
5
  import { ResourceProvider } from "./resources.js";
6
6
  import { PerformanceMetrics } from "./metrics.js";
7
- import { type PersistenceConfig, type CacheAwarenessConfig, type ProvidersConfig, type ApiProviderRuntime, type AcpConfig, type AdminConfig } from "./config.js";
7
+ import { type CompressResult } from "./compressor/index.js";
8
+ import { type PersistenceConfig, type CacheAwarenessConfig, type CompressionConfig, type ProvidersConfig, type ApiProviderRuntime, type AcpConfig, type AdminConfig, type LeastCostConfig } from "./config.js";
9
+ import type { CostBasis } from "./least-cost-types.js";
8
10
  import { DatabaseConnection } from "./db.js";
9
11
  import { type DevinAcpAgentType } from "./provider-definitions.js";
10
12
  import { AsyncJobManager } from "./async-job-manager.js";
@@ -38,6 +40,8 @@ type ExtendedToolResponse = {
38
40
  };
39
41
  reviewIntegrity?: ReviewIntegrityResult;
40
42
  warnings?: WarningEntry[];
43
+ routing?: RouteResponseBlock;
44
+ compression?: CompressResult;
41
45
  };
42
46
  export declare const logger: {
43
47
  info: (message: string, ...args: unknown[]) => void;
@@ -76,10 +80,12 @@ export interface GatewayServerDeps {
76
80
  logger?: GatewayLogger;
77
81
  persistence?: PersistenceConfig;
78
82
  cacheAwareness?: CacheAwarenessConfig;
83
+ compression?: CompressionConfig;
79
84
  providers?: ProvidersConfig;
80
85
  acpConfig?: AcpConfig;
81
86
  adminConfig?: AdminConfig;
82
87
  workspaces?: WorkspaceRegistry;
88
+ leastCost?: LeastCostConfig;
83
89
  }
84
90
  export interface GatewayServerRuntime {
85
91
  sessionManager: ISessionManager;
@@ -92,10 +98,12 @@ export interface GatewayServerRuntime {
92
98
  logger: GatewayLogger;
93
99
  persistence: PersistenceConfig;
94
100
  cacheAwareness: CacheAwarenessConfig;
101
+ compression: CompressionConfig;
95
102
  providers: ProvidersConfig;
96
103
  acpConfig: AcpConfig;
97
104
  adminConfig: AdminConfig;
98
105
  workspaces: WorkspaceRegistry;
106
+ leastCost: LeastCostConfig;
99
107
  }
100
108
  export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
101
109
  isolateState?: boolean;
@@ -169,6 +177,11 @@ export declare function extractUsageAndCost(cli: CliType, output: string, output
169
177
  cacheCreationTokens?: number;
170
178
  costUsd?: number;
171
179
  };
180
+ export declare function resolveEffectiveCompression(compression: CompressionConfig, input: {
181
+ compressResponse?: boolean;
182
+ outputFormat?: string;
183
+ outputSchemaDeclared?: boolean;
184
+ }): boolean;
172
185
  export declare function registerBaseResources(server: McpServer, runtime: GatewayServerRuntime): void;
173
186
  interface CliRequestPrep {
174
187
  corrId: string;
@@ -344,6 +357,7 @@ export declare function prepareGrokRequest(params: {
344
357
  forkSession?: boolean;
345
358
  jsonSchema?: string | Record<string, unknown>;
346
359
  }, runtime?: GatewayServerRuntime): CliRequestPrep | ExtendedToolResponse;
360
+ export declare function resolveMistralAgentMode(approvalStrategy: "legacy" | "mcp_managed" | undefined, callerMode: MistralAgentMode | undefined): MistralAgentMode;
347
361
  export declare function prepareMistralRequest(params: {
348
362
  prompt?: string;
349
363
  promptParts?: PromptParts;
@@ -374,7 +388,7 @@ export declare function buildMistralRetryPrep(params: Pick<MistralRequestParams,
374
388
  env: Record<string, string>;
375
389
  ignoredDisallowedTools: boolean;
376
390
  };
377
- export declare function buildCliResponse(cli: CliType, stdout: string, optimizeResponse: boolean, corrId: string, sessionId: string | undefined, prep: CliRequestPrep, durationMs: number, resumable?: boolean, outputFormat?: string, warnings?: WarningEntry[]): ExtendedToolResponse;
391
+ export declare function buildCliResponse(cli: CliType, stdout: string, optimizeResponse: boolean, corrId: string, sessionId: string | undefined, prep: CliRequestPrep, durationMs: number, resumable?: boolean, outputFormat?: string, warnings?: WarningEntry[], compressResponse?: boolean): ExtendedToolResponse;
378
392
  export interface GrokApiRequestParams {
379
393
  prompt?: string;
380
394
  promptParts?: PromptParts;
@@ -427,6 +441,7 @@ export interface GeminiRequestParams {
427
441
  correlationId?: string;
428
442
  optimizePrompt: boolean;
429
443
  optimizeResponse?: boolean;
444
+ compressResponse?: boolean;
430
445
  idleTimeoutMs?: number;
431
446
  forceRefresh?: boolean;
432
447
  outputFormat?: "text" | "json" | "stream-json";
@@ -482,6 +497,7 @@ export interface GrokRequestParams {
482
497
  correlationId?: string;
483
498
  optimizePrompt: boolean;
484
499
  optimizeResponse?: boolean;
500
+ compressResponse?: boolean;
485
501
  idleTimeoutMs?: number;
486
502
  forceRefresh?: boolean;
487
503
  maxTurns?: number;
@@ -541,6 +557,7 @@ export interface DevinRequestParams {
541
557
  correlationId?: string;
542
558
  optimizePrompt: boolean;
543
559
  optimizeResponse?: boolean;
560
+ compressResponse?: boolean;
544
561
  idleTimeoutMs?: number;
545
562
  forceRefresh?: boolean;
546
563
  }
@@ -580,6 +597,7 @@ export interface CursorRequestParams {
580
597
  correlationId?: string;
581
598
  optimizePrompt: boolean;
582
599
  optimizeResponse?: boolean;
600
+ compressResponse?: boolean;
583
601
  idleTimeoutMs?: number;
584
602
  forceRefresh?: boolean;
585
603
  }
@@ -620,6 +638,7 @@ export interface MistralRequestParams {
620
638
  correlationId?: string;
621
639
  optimizePrompt: boolean;
622
640
  optimizeResponse?: boolean;
641
+ compressResponse?: boolean;
623
642
  idleTimeoutMs?: number;
624
643
  forceRefresh?: boolean;
625
644
  trust?: boolean;
@@ -653,6 +672,7 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
653
672
  createNewSession: boolean;
654
673
  correlationId?: string;
655
674
  optimizePrompt: boolean;
675
+ compressResponse?: boolean;
656
676
  idleTimeoutMs?: number;
657
677
  forceRefresh?: boolean;
658
678
  outputFormat?: "text" | "json";
@@ -680,5 +700,41 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
680
700
  ref?: string;
681
701
  };
682
702
  }): Promise<ExtendedToolResponse>;
703
+ export interface RouteResponseBlock {
704
+ chosen: {
705
+ provider: string;
706
+ model: string;
707
+ } | null;
708
+ tier?: string;
709
+ estCostUsd?: number;
710
+ costBasis?: string;
711
+ confidence?: string;
712
+ nearTie?: boolean;
713
+ estInputTokens?: number;
714
+ estOutputTokens?: number;
715
+ priceAsOf?: string;
716
+ priceSource?: string;
717
+ consideredCount: number;
718
+ rejected: {
719
+ candidate: {
720
+ provider: string;
721
+ model: string;
722
+ };
723
+ reason: string;
724
+ }[];
725
+ reroutes: number;
726
+ error?: string;
727
+ }
728
+ export declare function isTransientRouteFailure(provider: string, response: ExtendedToolResponse): boolean;
729
+ export declare function deriveCostBasis(provider: string, resolvedModel: string, usage: {
730
+ inputTokens?: number;
731
+ outputTokens?: number;
732
+ cacheReadTokens?: number;
733
+ cacheCreationTokens?: number;
734
+ costUsd?: number;
735
+ }): {
736
+ costUsd?: number;
737
+ costBasis?: CostBasis;
738
+ };
683
739
  export declare function createGatewayServer(deps?: GatewayServerDeps): McpServer;
684
740
  export {};