llm-cli-gateway 2.16.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 (61) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +28 -0
  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/runtime.d.ts +1 -0
  7. package/dist/acp/runtime.js +20 -0
  8. package/dist/acp/types.d.ts +52 -0
  9. package/dist/acp/types.js +8 -0
  10. package/dist/api-provider.d.ts +1 -0
  11. package/dist/api-provider.js +1 -0
  12. package/dist/async-job-manager.d.ts +16 -0
  13. package/dist/async-job-manager.js +70 -22
  14. package/dist/claude-mcp-config.js +1 -1
  15. package/dist/compressor/transforms/ansi.js +12 -2
  16. package/dist/config.d.ts +31 -0
  17. package/dist/config.js +142 -7
  18. package/dist/db.js +4 -4
  19. package/dist/doctor.d.ts +34 -1
  20. package/dist/doctor.js +85 -3
  21. package/dist/executor.d.ts +5 -0
  22. package/dist/executor.js +11 -1
  23. package/dist/flight-recorder.d.ts +10 -0
  24. package/dist/flight-recorder.js +68 -2
  25. package/dist/http-transport.js +19 -21
  26. package/dist/index.d.ts +41 -1
  27. package/dist/index.js +658 -30
  28. package/dist/job-store.d.ts +10 -2
  29. package/dist/job-store.js +154 -43
  30. package/dist/lcr-priors.d.ts +60 -0
  31. package/dist/lcr-priors.js +190 -0
  32. package/dist/lcr-router-env.d.ts +20 -0
  33. package/dist/lcr-router-env.js +133 -0
  34. package/dist/lcr-telemetry.d.ts +2 -0
  35. package/dist/lcr-telemetry.js +17 -0
  36. package/dist/least-cost-router.d.ts +86 -0
  37. package/dist/least-cost-router.js +296 -0
  38. package/dist/least-cost-types.d.ts +34 -0
  39. package/dist/least-cost-types.js +1 -0
  40. package/dist/migrate-sessions.js +1 -1
  41. package/dist/migrate.js +1 -1
  42. package/dist/model-registry.js +1 -1
  43. package/dist/postgres-job-store-worker.js +56 -13
  44. package/dist/pricing.d.ts +17 -0
  45. package/dist/pricing.js +167 -0
  46. package/dist/provider-definitions.d.ts +4 -0
  47. package/dist/provider-definitions.js +28 -0
  48. package/dist/request-helpers.d.ts +2 -2
  49. package/dist/resources.d.ts +37 -2
  50. package/dist/resources.js +96 -1
  51. package/dist/retry.d.ts +1 -0
  52. package/dist/retry.js +1 -1
  53. package/dist/token-estimator.d.ts +6 -0
  54. package/dist/token-estimator.js +59 -0
  55. package/dist/upstream-contracts.js +1 -1
  56. package/dist/validation-receipt.js +1 -1
  57. package/dist/validation-tools.d.ts +6 -0
  58. package/dist/validation-tools.js +174 -54
  59. package/npm-shrinkwrap.json +2 -2
  60. package/package.json +9 -4
  61. package/setup/status.schema.json +68 -0
@@ -83,6 +83,35 @@ function ensureCompressionColumns(db) {
83
83
  db.exec("ALTER TABLE gateway_metadata ADD COLUMN compression_tokens_saved_est INTEGER");
84
84
  }
85
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
+ }
86
115
  export function resolveFlightRecorderDbPath() {
87
116
  const configured = process.env.LLM_GATEWAY_LOGS_DB;
88
117
  if (configured !== undefined) {
@@ -161,7 +190,8 @@ export class FlightRecorder {
161
190
  output_tokens INTEGER,
162
191
  cache_read_tokens INTEGER,
163
192
  cache_creation_tokens INTEGER,
164
- owner_principal TEXT
193
+ owner_principal TEXT,
194
+ cost_basis TEXT
165
195
  );
166
196
 
167
197
  CREATE TABLE IF NOT EXISTS gateway_metadata (
@@ -178,6 +208,12 @@ export class FlightRecorder {
178
208
  async_job_id TEXT,
179
209
  provider_session_id TEXT,
180
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,
181
217
  status TEXT NOT NULL DEFAULT 'started'
182
218
  );
183
219
 
@@ -219,6 +255,14 @@ export class FlightRecorder {
219
255
  this.db
220
256
  .prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(8, ?)")
221
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());
222
266
  if (process.platform !== "win32") {
223
267
  try {
224
268
  chmodSync(dbPath, 0o600);
@@ -265,7 +309,8 @@ export class FlightRecorder {
265
309
  input_tokens = @input_tokens,
266
310
  output_tokens = @output_tokens,
267
311
  cache_read_tokens = @cache_read_tokens,
268
- cache_creation_tokens = @cache_creation_tokens
312
+ cache_creation_tokens = @cache_creation_tokens,
313
+ cost_basis = @cost_basis
269
314
  WHERE id = @id
270
315
  `);
271
316
  const updateMetadata = this.db.prepare(`
@@ -296,6 +341,7 @@ export class FlightRecorder {
296
341
  output_tokens: result.outputTokens ?? null,
297
342
  cache_read_tokens: result.cacheReadTokens ?? null,
298
343
  cache_creation_tokens: result.cacheCreationTokens ?? null,
344
+ cost_basis: result.costBasis ?? null,
299
345
  });
300
346
  updateMetadata.run({
301
347
  id: correlationId,
@@ -338,6 +384,25 @@ export class FlightRecorder {
338
384
  tokens_saved_est: telemetry.estimatedTokensSaved,
339
385
  });
340
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
+ }
341
406
  redactStart(entry) {
342
407
  return {
343
408
  ...entry,
@@ -372,6 +437,7 @@ export class NoopFlightRecorder {
372
437
  logStart(_entry) { }
373
438
  logComplete(_correlationId, _result) { }
374
439
  recordCompressionTelemetry(_correlationId, _telemetry) { }
440
+ recordRouting(_correlationId, _routing) { }
375
441
  queryRequests(_sql, ..._params) {
376
442
  return [];
377
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
@@ -5,7 +5,8 @@ import { ISessionManager, type CliType, type ProviderType } from "./session-mana
5
5
  import { ResourceProvider } from "./resources.js";
6
6
  import { PerformanceMetrics } from "./metrics.js";
7
7
  import { type CompressResult } from "./compressor/index.js";
8
- import { type PersistenceConfig, type CacheAwarenessConfig, type CompressionConfig, type ProvidersConfig, type ApiProviderRuntime, type AcpConfig, type AdminConfig } from "./config.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";
9
10
  import { DatabaseConnection } from "./db.js";
10
11
  import { type DevinAcpAgentType } from "./provider-definitions.js";
11
12
  import { AsyncJobManager } from "./async-job-manager.js";
@@ -39,6 +40,7 @@ type ExtendedToolResponse = {
39
40
  };
40
41
  reviewIntegrity?: ReviewIntegrityResult;
41
42
  warnings?: WarningEntry[];
43
+ routing?: RouteResponseBlock;
42
44
  compression?: CompressResult;
43
45
  };
44
46
  export declare const logger: {
@@ -83,6 +85,7 @@ export interface GatewayServerDeps {
83
85
  acpConfig?: AcpConfig;
84
86
  adminConfig?: AdminConfig;
85
87
  workspaces?: WorkspaceRegistry;
88
+ leastCost?: LeastCostConfig;
86
89
  }
87
90
  export interface GatewayServerRuntime {
88
91
  sessionManager: ISessionManager;
@@ -100,6 +103,7 @@ export interface GatewayServerRuntime {
100
103
  acpConfig: AcpConfig;
101
104
  adminConfig: AdminConfig;
102
105
  workspaces: WorkspaceRegistry;
106
+ leastCost: LeastCostConfig;
103
107
  }
104
108
  export declare function resolveGatewayServerRuntime(deps?: GatewayServerDeps, options?: {
105
109
  isolateState?: boolean;
@@ -696,5 +700,41 @@ export declare function handleCodexRequestAsync(deps: AsyncHandlerDeps, params:
696
700
  ref?: string;
697
701
  };
698
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
+ };
699
739
  export declare function createGatewayServer(deps?: GatewayServerDeps): McpServer;
700
740
  export {};