omnigateway 0.14.1 → 0.15.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 (50) hide show
  1. package/README.md +36 -1
  2. package/bin/omni.js +304 -41
  3. package/gateway.js +570 -44
  4. package/package.json +1 -1
  5. package/public/assets/{Chip-D0XSbQUr.js → Chip-C3dIFd8s.js} +1 -1
  6. package/public/assets/{CopyValue-DAlmkISB.js → CopyValue-DG88CWpN.js} +1 -1
  7. package/public/assets/{Field-D9V9N-vb.js → Field-EvdVyEqx.js} +25 -6
  8. package/public/assets/Lamp-CL0i4yaa.js +26 -0
  9. package/public/assets/{Rack-CDUOJkS2.js → Rack-DMrfn4-H.js} +5 -5
  10. package/public/assets/{RequestTable-BF5-R27W.js → RequestTable-CtuuMOT2.js} +3 -3
  11. package/public/assets/{SummaryDeck-DrzTQQo5.js → SummaryDeck-DHsYYpYO.js} +2 -2
  12. package/public/assets/{Toggle-CdMD58sy.js → Toggle-BTie3NY8.js} +1 -1
  13. package/public/assets/{TokenBreakdown-BC6nqo1v.js → TokenBreakdown-Bf0zsKcd.js} +1 -1
  14. package/public/assets/{WindowChart-Bqbl-KFO.js → WindowChart-CaPUJ4GJ.js} +2 -2
  15. package/public/assets/_app.accounts-xCJhRd32.js +70 -0
  16. package/public/assets/{_app.console-BplICGMp.js → _app.console-DS37cfIu.js} +2 -2
  17. package/public/assets/_app.database-BCwEMrEA.js +24 -0
  18. package/public/assets/_app.index-DW6wain4.js +67 -0
  19. package/public/assets/_app.keys-CCU2NB3-.js +120 -0
  20. package/public/assets/_app.logs-CRC5qDK3.js +46 -0
  21. package/public/assets/_app.models-B4brzRdK.js +148 -0
  22. package/public/assets/{_app.plugins._pluginId-BduNUc7R.js → _app.plugins._pluginId-CD9KXknf.js} +1 -1
  23. package/public/assets/_app.settings-NJSb_L2B.js +88 -0
  24. package/public/assets/_app.usage-BR6a92zI.js +78 -0
  25. package/public/assets/chevron-down-3tlYNJqZ.js +1 -0
  26. package/public/assets/chevron-right-kvaJRnfj.js +1 -0
  27. package/public/assets/client-B9t0CsmA.js +46 -0
  28. package/public/assets/{dist-BBGxva0Y.js → dist-B8iuo_hI.js} +1 -1
  29. package/public/assets/index-6rZSWe9j.js +14 -0
  30. package/public/assets/{login-Dj7MQcIc.js → login-NuwPF5SS.js} +2 -2
  31. package/public/assets/plus-CVqm87L-.js +1 -0
  32. package/public/assets/{preload-helper-B7KDmXJ9.js → preload-helper-1xe0b53X.js} +1 -1
  33. package/public/assets/{rotate-ccw-Do43Argv.js → rotate-ccw-Dionx_MW.js} +1 -1
  34. package/public/assets/{shared-DDpLUhCp.js → shared-HVgLEGgl.js} +2 -2
  35. package/public/assets/{trash-00950_yY.js → trash-Bsi60qJc.js} +1 -1
  36. package/public/index.html +4 -4
  37. package/public/assets/Lamp-Dk9XrTCg.js +0 -26
  38. package/public/assets/_app.accounts-C-Y0w4PQ.js +0 -70
  39. package/public/assets/_app.database-CKoZXAHB.js +0 -24
  40. package/public/assets/_app.index-BV1kJJjA.js +0 -67
  41. package/public/assets/_app.keys-TIg-A16v.js +0 -107
  42. package/public/assets/_app.logs-CdF9dkSn.js +0 -46
  43. package/public/assets/_app.models-CPb0eKBe.js +0 -148
  44. package/public/assets/_app.settings-dGgsENlT.js +0 -88
  45. package/public/assets/_app.usage--5qFtU1W.js +0 -76
  46. package/public/assets/chevron-down-Bb5ZGvwk.js +0 -1
  47. package/public/assets/chevron-right-hwF5mD6-.js +0 -1
  48. package/public/assets/client-Bx3ttI-9.js +0 -39
  49. package/public/assets/index-Rh35FNqC.js +0 -14
  50. package/public/assets/plus-BgcHxxIr.js +0 -1
package/bin/omni.js CHANGED
@@ -7751,6 +7751,17 @@ var _006_model_window_index_default = `-- The per-model long-window reads behind
7751
7751
  CREATE INDEX idx_request_logs_key_model_at ON request_logs (api_key_id, requested_model, at);
7752
7752
  `;
7753
7753
 
7754
+ // packages/store/src/postgres/migrations/007_key_guardrail.sql
7755
+ var _007_key_guardrail_default = `-- The Postgres half of the sqlite migration \`019_key_guardrail.sql\`: an
7756
+ -- optional usage policy on a gateway API key, NULL meaning no guardrail.
7757
+ --
7758
+ -- No backfill: every existing key was minted without a policy. Safe under a
7759
+ -- rolling deploy \u2014 a nullable column and nothing dropped, so a replica on the
7760
+ -- previous image keeps serving, ignoring the field (and therefore not judging
7761
+ -- that key's requests until it is replaced, which README's upgrade notes say).
7762
+ ALTER TABLE api_keys ADD COLUMN guardrail_policy TEXT;
7763
+ `;
7764
+
7754
7765
  // packages/store/src/postgres/db.ts
7755
7766
  var num = (v) => Number(v);
7756
7767
  var numOrNull = (v) => v === null ? null : Number(v);
@@ -7761,7 +7772,8 @@ var MIGRATIONS = [
7761
7772
  { id: 3, sql: _003_key_expiry_default },
7762
7773
  { id: 4, sql: _004_log_keyset_default },
7763
7774
  { id: 5, sql: _005_log_filter_indexes_default },
7764
- { id: 6, sql: _006_model_window_index_default }
7775
+ { id: 6, sql: _006_model_window_index_default },
7776
+ { id: 7, sql: _007_key_guardrail_default }
7765
7777
  ];
7766
7778
  async function openPg(url) {
7767
7779
  const sql = new SQL({ url });
@@ -7962,6 +7974,9 @@ function resolvePin(target, accounts) {
7962
7974
  return;
7963
7975
  return accounts.find((account) => servesTarget(target, account));
7964
7976
  }
7977
+ var GUARDRAIL_POLICY_MAX_CHARS = 4000;
7978
+ var GUARDRAIL_TIMEOUT_MAX_MS = 60000;
7979
+ var GUARDRAIL_HISTORY_TURNS_MAX = 10;
7965
7980
  var keyUsable = (key, now) => key.revokedAt === null && (key.expiresAt === null || key.expiresAt > now);
7966
7981
  var credentialPastExpiry = (credential, now) => credential.authType === "oauth" && credential.expiresAt !== null && credential.expiresAt <= now;
7967
7982
  var credentialExpired = (credential, now) => credentialPastExpiry(credential, now) && !credential.hasRefreshToken;
@@ -7981,8 +7996,22 @@ var DEFAULT_SETTINGS = {
7981
7996
  bodyLoggingEnabled: false,
7982
7997
  bodyLoggingCaptureStreamChunks: false,
7983
7998
  snapshotKeepLatest: 5,
7984
- snapshotMaxAgeDays: 30
7999
+ snapshotMaxAgeDays: 30,
8000
+ guardrailModel: null,
8001
+ guardrailTimeoutMs: 1e4,
8002
+ guardrailHistoryTurns: 3
7985
8003
  };
8004
+ var inRange = (value, max) => typeof value === "number" && Number.isInteger(value) && value >= 1 && value <= max;
8005
+ function readGuardrailSettings(stored) {
8006
+ const model = stored.guardrailModel;
8007
+ const timeout = stored.guardrailTimeoutMs;
8008
+ const turns = stored.guardrailHistoryTurns;
8009
+ return {
8010
+ guardrailModel: typeof model === "string" && model.trim() !== "" ? model : null,
8011
+ guardrailTimeoutMs: inRange(timeout, GUARDRAIL_TIMEOUT_MAX_MS) ? timeout : DEFAULT_SETTINGS.guardrailTimeoutMs,
8012
+ guardrailHistoryTurns: inRange(turns, GUARDRAIL_HISTORY_TURNS_MAX) ? turns : DEFAULT_SETTINGS.guardrailHistoryTurns
8013
+ };
8014
+ }
7986
8015
 
7987
8016
  // packages/store/src/postgres/config.ts
7988
8017
  var SETTINGS_KEY = "settings";
@@ -8046,7 +8075,8 @@ function createConfigRepo(sql, emit) {
8046
8075
  bodyRetentionDays: Number.isInteger(stored.bodyRetentionDays) && (stored.bodyRetentionDays ?? 0) >= 1 && (stored.bodyRetentionDays ?? 0) <= 3650 ? stored.bodyRetentionDays : DEFAULT_SETTINGS.bodyRetentionDays,
8047
8076
  bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
8048
8077
  bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
8049
- weights: knownWeights(stored.weights)
8078
+ weights: knownWeights(stored.weights),
8079
+ ...readGuardrailSettings(stored)
8050
8080
  };
8051
8081
  } catch {
8052
8082
  return DEFAULT_SETTINGS;
@@ -8447,7 +8477,8 @@ var toKey = (r, logger) => ({
8447
8477
  bodyLoggingOptOut: r.body_logging_opt_out,
8448
8478
  createdAt: num(r.created_at),
8449
8479
  revokedAt: numOrNull(r.revoked_at),
8450
- expiresAt: numOrNull(r.expires_at)
8480
+ expiresAt: numOrNull(r.expires_at),
8481
+ guardrailPolicy: r.guardrail_policy
8451
8482
  });
8452
8483
  function createKeyRepo(sql, logger) {
8453
8484
  const one = async (where, arg) => {
@@ -8465,8 +8496,9 @@ function createKeyRepo(sql, logger) {
8465
8496
  async create(input) {
8466
8497
  const now = Date.now();
8467
8498
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
8468
- body_logging_opt_out, created_at, revoked_at, expires_at)
8469
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9)`, [
8499
+ body_logging_opt_out, created_at, revoked_at, expires_at,
8500
+ guardrail_policy)
8501
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,NULL,$9,$10)`, [
8470
8502
  input.id,
8471
8503
  input.label,
8472
8504
  input.prefix,
@@ -8475,7 +8507,8 @@ function createKeyRepo(sql, logger) {
8475
8507
  JSON.stringify(parseLimitConfig(input.limits)),
8476
8508
  input.bodyLoggingOptOut,
8477
8509
  now,
8478
- input.expiresAt
8510
+ input.expiresAt,
8511
+ input.guardrailPolicy
8479
8512
  ]);
8480
8513
  return { ...input, createdAt: now, revokedAt: null };
8481
8514
  },
@@ -8483,8 +8516,9 @@ function createKeyRepo(sql, logger) {
8483
8516
  if (row.limits === null)
8484
8517
  throw new Error(`api key ${row.id} has unreadable limits`);
8485
8518
  await sql.unsafe(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
8486
- body_logging_opt_out, created_at, revoked_at, expires_at)
8487
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)`, [
8519
+ body_logging_opt_out, created_at, revoked_at, expires_at,
8520
+ guardrail_policy)
8521
+ VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`, [
8488
8522
  row.id,
8489
8523
  row.label,
8490
8524
  row.prefix,
@@ -8494,7 +8528,8 @@ function createKeyRepo(sql, logger) {
8494
8528
  row.bodyLoggingOptOut,
8495
8529
  row.createdAt,
8496
8530
  row.revokedAt,
8497
- row.expiresAt
8531
+ row.expiresAt,
8532
+ row.guardrailPolicy
8498
8533
  ]);
8499
8534
  },
8500
8535
  async setLimits(id, limits) {
@@ -8512,6 +8547,9 @@ function createKeyRepo(sql, logger) {
8512
8547
  async setExpiry(id, expiresAt) {
8513
8548
  await sql.unsafe("UPDATE api_keys SET expires_at = $1 WHERE id = $2", [expiresAt, id]);
8514
8549
  },
8550
+ async setGuardrailPolicy(id, policy) {
8551
+ await sql.unsafe("UPDATE api_keys SET guardrail_policy = $1 WHERE id = $2", [policy, id]);
8552
+ },
8515
8553
  async revoke(id) {
8516
8554
  await sql.unsafe("UPDATE api_keys SET revoked_at = $1 WHERE id = $2", [Date.now(), id]);
8517
8555
  }
@@ -9281,8 +9319,16 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
9281
9319
  const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
9282
9320
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
9283
9321
  const timeColumn = daily ? "day" : "at";
9284
- const scope = q.apiKeyId;
9285
- const bindings = scope === undefined ? [since, until] : [since, until, scope];
9322
+ const bindings = [since, until];
9323
+ let filters = "";
9324
+ if (q.apiKeyId !== undefined) {
9325
+ bindings.push(q.apiKeyId);
9326
+ filters += ` AND api_key_id = $${bindings.length}`;
9327
+ }
9328
+ if (q.credentialId !== undefined) {
9329
+ bindings.push(q.credentialId);
9330
+ filters += ` AND credential_id = $${bindings.length}`;
9331
+ }
9286
9332
  const rows = await sql.unsafe(`SELECT ${key} AS key,
9287
9333
  ${split ?? "NULL"} AS split,
9288
9334
  ${MEASURES[grain]},
@@ -9294,7 +9340,7 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
9294
9340
  COALESCE(SUM(${daily ? "rtk_applied_requests" : "CASE WHEN rtk_applied THEN 1 ELSE 0 END"}), 0) AS rtk_applied_requests,
9295
9341
  COALESCE(SUM(cost_usd), 0) AS cost_usd
9296
9342
  FROM ${daily ? "usage_daily" : "request_logs"}
9297
- WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= $1 AND ${timeColumn} <= $2${scope === undefined ? "" : " AND api_key_id = $3"}
9343
+ WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= $1 AND ${timeColumn} <= $2${filters}
9298
9344
  GROUP BY 1${split === null ? "" : ", 2"}
9299
9345
  ORDER BY requests DESC`, bindings);
9300
9346
  return rows.map((r) => {
@@ -9577,7 +9623,8 @@ function createConfigRepo2(db, emit = () => {}) {
9577
9623
  bodyRetentionDays: Number.isInteger(stored.bodyRetentionDays) && (stored.bodyRetentionDays ?? 0) >= 1 && (stored.bodyRetentionDays ?? 0) <= 3650 ? stored.bodyRetentionDays : DEFAULT_SETTINGS.bodyRetentionDays,
9578
9624
  bodyLoggingEnabled: stored.bodyLoggingEnabled === true,
9579
9625
  bodyLoggingCaptureStreamChunks: stored.bodyLoggingCaptureStreamChunks === true,
9580
- weights: knownWeights2(stored.weights)
9626
+ weights: knownWeights2(stored.weights),
9627
+ ...readGuardrailSettings(stored)
9581
9628
  };
9582
9629
  } catch {
9583
9630
  return DEFAULT_SETTINGS;
@@ -10588,6 +10635,24 @@ var _018_model_window_index_default = `-- An index for the per-model long-window
10588
10635
  CREATE INDEX idx_request_logs_key_model_at ON request_logs (api_key_id, requested_model, at);
10589
10636
  `;
10590
10637
 
10638
+ // packages/store/src/sqlite/migrations/019_key_guardrail.sql
10639
+ var _019_key_guardrail_default = `-- An optional usage policy on a gateway API key: free text the operator wrote
10640
+ -- about what the key may be used for, judged by a classifier model before each
10641
+ -- inference request (\`apps/gateway/src/guardrail/\`).
10642
+ --
10643
+ -- Nullable, and NULL is the only spelling of "no guardrail". No backfill, for
10644
+ -- the reason migration 14 has none: every existing key was minted without a
10645
+ -- policy, and inventing one would start refusing its client on an upgrade the
10646
+ -- operator never connected to it.
10647
+ --
10648
+ -- Its own column rather than a member of \`limits\`. A policy is not a limit, and
10649
+ -- nesting it there would bind it to the \`@omni/ratelimit/catalog\` storage
10650
+ -- contract, which fails closed on any name it does not know. The cost is
10651
+ -- accepted and documented: a rollback to a build without this column ignores
10652
+ -- it and stops judging \u2014 the guardrail fails open across a downgrade.
10653
+ ALTER TABLE api_keys ADD COLUMN guardrail_policy TEXT;
10654
+ `;
10655
+
10591
10656
  // packages/store/src/sqlite/db.ts
10592
10657
  var MIGRATIONS2 = [
10593
10658
  { id: 1, sql: _001_init_default2 },
@@ -10607,7 +10672,8 @@ var MIGRATIONS2 = [
10607
10672
  { id: 15, sql: _015_log_keyset_default },
10608
10673
  { id: 16, sql: _016_log_filter_indexes_default },
10609
10674
  { id: 17, sql: _017_log_text_search_default },
10610
- { id: 18, sql: _018_model_window_index_default }
10675
+ { id: 18, sql: _018_model_window_index_default },
10676
+ { id: 19, sql: _019_key_guardrail_default }
10611
10677
  ];
10612
10678
  function statements(sql) {
10613
10679
  return sql.split(`
@@ -10664,7 +10730,8 @@ var toKey2 = (r, logger) => ({
10664
10730
  bodyLoggingOptOut: r.body_logging_opt_out === 1,
10665
10731
  createdAt: r.created_at,
10666
10732
  revokedAt: r.revoked_at,
10667
- expiresAt: r.expires_at
10733
+ expiresAt: r.expires_at,
10734
+ guardrailPolicy: r.guardrail_policy
10668
10735
  });
10669
10736
  function createKeyRepo2(db, logger = noopLogger) {
10670
10737
  return {
@@ -10682,8 +10749,9 @@ function createKeyRepo2(db, logger = noopLogger) {
10682
10749
  async create(input) {
10683
10750
  const now = Date.now();
10684
10751
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
10685
- body_logging_opt_out, created_at, revoked_at, expires_at)
10686
- VALUES (?,?,?,?,?,?,?,?,NULL,?)`, [
10752
+ body_logging_opt_out, created_at, revoked_at, expires_at,
10753
+ guardrail_policy)
10754
+ VALUES (?,?,?,?,?,?,?,?,NULL,?,?)`, [
10687
10755
  input.id,
10688
10756
  input.label,
10689
10757
  input.prefix,
@@ -10692,7 +10760,8 @@ function createKeyRepo2(db, logger = noopLogger) {
10692
10760
  JSON.stringify(parseLimitConfig(input.limits)),
10693
10761
  input.bodyLoggingOptOut ? 1 : 0,
10694
10762
  now,
10695
- input.expiresAt
10763
+ input.expiresAt,
10764
+ input.guardrailPolicy
10696
10765
  ]);
10697
10766
  return { ...input, createdAt: now, revokedAt: null };
10698
10767
  },
@@ -10700,8 +10769,9 @@ function createKeyRepo2(db, logger = noopLogger) {
10700
10769
  if (row.limits === null)
10701
10770
  throw new Error(`api key ${row.id} has unreadable limits`);
10702
10771
  db.run(`INSERT INTO api_keys (id, label, prefix, hash, model_allowlist, limits,
10703
- body_logging_opt_out, created_at, revoked_at, expires_at)
10704
- VALUES (?,?,?,?,?,?,?,?,?,?)`, [
10772
+ body_logging_opt_out, created_at, revoked_at, expires_at,
10773
+ guardrail_policy)
10774
+ VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [
10705
10775
  row.id,
10706
10776
  row.label,
10707
10777
  row.prefix,
@@ -10711,7 +10781,8 @@ function createKeyRepo2(db, logger = noopLogger) {
10711
10781
  row.bodyLoggingOptOut ? 1 : 0,
10712
10782
  row.createdAt,
10713
10783
  row.revokedAt,
10714
- row.expiresAt
10784
+ row.expiresAt,
10785
+ row.guardrailPolicy
10715
10786
  ]);
10716
10787
  },
10717
10788
  async setLimits(id, limits) {
@@ -10729,6 +10800,9 @@ function createKeyRepo2(db, logger = noopLogger) {
10729
10800
  async setExpiry(id, expiresAt) {
10730
10801
  db.run("UPDATE api_keys SET expires_at = ? WHERE id = ?", [expiresAt, id]);
10731
10802
  },
10803
+ async setGuardrailPolicy(id, policy) {
10804
+ db.run("UPDATE api_keys SET guardrail_policy = ? WHERE id = ?", [policy, id]);
10805
+ },
10732
10806
  async revoke(id) {
10733
10807
  db.run("UPDATE api_keys SET revoked_at = ? WHERE id = ?", [Date.now(), id]);
10734
10808
  }
@@ -11215,9 +11289,16 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
11215
11289
  const since = daily ? startOfDay(q.since, dayOffsetMinutes) : q.since;
11216
11290
  const until = q.until ?? Number.MAX_SAFE_INTEGER;
11217
11291
  const timeColumn = daily ? "day" : "at";
11218
- const scope = q.apiKeyId;
11219
- const scoped = scope !== undefined;
11220
- const bindings = scope === undefined ? [since, until] : [since, until, scope];
11292
+ const bindings = [since, until];
11293
+ let filters = "";
11294
+ if (q.apiKeyId !== undefined) {
11295
+ filters += " AND api_key_id = ?";
11296
+ bindings.push(q.apiKeyId);
11297
+ }
11298
+ if (q.credentialId !== undefined) {
11299
+ filters += " AND credential_id = ?";
11300
+ bindings.push(q.credentialId);
11301
+ }
11221
11302
  const rows = db.query(`SELECT ${key} AS key,
11222
11303
  ${split ?? "NULL"} AS split,
11223
11304
  ${MEASURES2[grain]},
@@ -11229,7 +11310,7 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
11229
11310
  COALESCE(SUM(${daily ? "rtk_applied_requests" : "CASE WHEN rtk_applied = 1 THEN 1 ELSE 0 END"}), 0) AS rtk_applied_requests,
11230
11311
  COALESCE(SUM(cost_usd), 0) AS cost_usd
11231
11312
  FROM ${daily ? "usage_daily" : "request_logs"}
11232
- WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= ? AND ${timeColumn} <= ?${scoped ? " AND api_key_id = ?" : ""}
11313
+ WHERE ${daily ? "" : "state = 'done' AND "}${timeColumn} >= ? AND ${timeColumn} <= ?${filters}
11233
11314
  GROUP BY key${split === null ? "" : ", split"}
11234
11315
  ORDER BY requests DESC`).all(...bindings);
11235
11316
  return rows.map((r) => {
@@ -11333,6 +11414,7 @@ async function createStore(opts) {
11333
11414
  setLimits: (id, limits) => handle.keys.setLimits(id, limits),
11334
11415
  setModelAllowlist: (id, modelAllowlist) => handle.keys.setModelAllowlist(id, modelAllowlist),
11335
11416
  setExpiry: (id, expiresAt) => handle.keys.setExpiry(id, expiresAt),
11417
+ setGuardrailPolicy: (id, policy) => handle.keys.setGuardrailPolicy(id, policy),
11336
11418
  revoke: (id) => handle.keys.revoke(id)
11337
11419
  },
11338
11420
  usage: {
@@ -20376,18 +20458,21 @@ var modelSchema = object({
20376
20458
  targets: array(targetSchema).min(1, "a virtual model needs at least one target")
20377
20459
  });
20378
20460
  var MAX_INSTANT = 8640000000000000;
20461
+ var guardrailPolicySchema = string2().trim().min(1).max(GUARDRAIL_POLICY_MAX_CHARS);
20379
20462
  var keyCreateSchema = object({
20380
20463
  label: string2().min(1).default("api key"),
20381
20464
  modelAllowlist: array(string2().min(1)).nullable().default(null),
20382
20465
  limits: limitConfigSchema.default({}),
20383
20466
  bodyLoggingOptOut: boolean2().default(false),
20384
- expiresAt: number2().int().positive().max(MAX_INSTANT).nullable().default(null)
20467
+ expiresAt: number2().int().positive().max(MAX_INSTANT).nullable().default(null),
20468
+ guardrailPolicy: guardrailPolicySchema.nullable().default(null)
20385
20469
  }).strict();
20386
20470
  var keyLimitsSchema = object({ limits: limitConfigSchema }).strict();
20387
20471
  var keyModelsSchema = object({
20388
20472
  modelAllowlist: array(string2().min(1)).nullable()
20389
20473
  }).strict();
20390
20474
  var keyExpirySchema = object({ expiresAt: number2().int().positive().max(MAX_INSTANT).nullable() }).strict();
20475
+ var keyGuardrailSchema = object({ policy: guardrailPolicySchema.nullable() }).strict();
20391
20476
  var retentionSchema = object({
20392
20477
  keepLatest: number2().int().min(1).max(100),
20393
20478
  maxAgeDays: number2().int().min(1).max(3650)
@@ -20414,7 +20499,10 @@ var settingsSchema = object({
20414
20499
  bodyLoggingEnabled: boolean2(),
20415
20500
  bodyLoggingCaptureStreamChunks: boolean2(),
20416
20501
  snapshotKeepLatest: retentionSchema.shape.keepLatest.optional(),
20417
- snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional()
20502
+ snapshotMaxAgeDays: retentionSchema.shape.maxAgeDays.optional(),
20503
+ guardrailModel: string2().trim().min(1).max(256).nullable().optional(),
20504
+ guardrailTimeoutMs: number2().int().min(1).max(GUARDRAIL_TIMEOUT_MAX_MS).optional(),
20505
+ guardrailHistoryTurns: number2().int().min(1).max(GUARDRAIL_HISTORY_TURNS_MAX).optional()
20418
20506
  });
20419
20507
  var credentialPatchSchema = object({
20420
20508
  label: string2().min(1).optional(),
@@ -20429,7 +20517,9 @@ var quotaRefreshSchema = discriminatedUnion("kind", [
20429
20517
  }).strict(),
20430
20518
  object({ kind: literal("all") }).strict()
20431
20519
  ]);
20432
- var accountIdSchema = string2().trim().min(1).max(64).regex(/^[A-Za-z0-9_-]+$/, "credentialId must be an account id");
20520
+ var opaqueIdSchema = (message) => string2().trim().min(1, message).max(64, message).regex(/^[A-Za-z0-9_-]+$/, message);
20521
+ var accountIdSchema = opaqueIdSchema("credentialId must be an account id");
20522
+ var keyIdSchema = opaqueIdSchema("apiKeyId must be a key id");
20433
20523
  var redeemResetSchema = object({
20434
20524
  credentialId: accountIdSchema,
20435
20525
  creditId: string2().min(1).max(128).regex(/^[A-Za-z0-9_-]+$/).optional()
@@ -20841,6 +20931,50 @@ function score(pairs, input) {
20841
20931
  };
20842
20932
  });
20843
20933
  }
20934
+ // packages/router/src/resolve.ts
20935
+ function prefixProviders(providers) {
20936
+ return Object.entries(providers).flatMap(([id, descriptor]) => descriptor.modelPrefixes.map((prefix) => [prefix, id, descriptor])).sort(([a], [b]) => b.length - a.length);
20937
+ }
20938
+ function synthesize(provider, model, descriptor) {
20939
+ const listed = entryPricing(descriptor.catalog, model);
20940
+ const limits = entryLimits(descriptor.catalog, model);
20941
+ const target = {
20942
+ provider,
20943
+ model,
20944
+ tier: 1,
20945
+ weight: 1,
20946
+ costPerMTok: listed === null ? { input: 0, output: 0 } : {
20947
+ input: listed.input,
20948
+ output: listed.output,
20949
+ cacheRead: listed.cacheRead,
20950
+ cacheWrite5m: listed.cacheWrite5m,
20951
+ cacheWrite1h: listed.cacheWrite1h
20952
+ },
20953
+ ...limits === null ? {} : { contextWindow: limits.contextWindow, maxOutputTokens: limits.maxOutputTokens },
20954
+ capabilities: descriptor.capabilities
20955
+ };
20956
+ return { id: `${provider}/${model}`, targets: [target], strategy: "score", isAlias: true };
20957
+ }
20958
+ function resolveModel(name, snapshot, providers = PROVIDER_DESCRIPTORS) {
20959
+ const configured = snapshot.models.get(name);
20960
+ if (configured !== undefined)
20961
+ return configured;
20962
+ const sep = name.search(/[/:]/);
20963
+ if (sep > 0) {
20964
+ const prefix = name.slice(0, sep);
20965
+ const rest = name.slice(sep + 1);
20966
+ const descriptor = Object.hasOwn(providers, prefix) ? providers[prefix] : undefined;
20967
+ if (descriptor !== undefined && prefix !== "custom" && rest.length > 0) {
20968
+ return synthesize(prefix, rest, descriptor);
20969
+ }
20970
+ throw new GatewayError("NO_CANDIDATES", `unknown provider "${safeToken(prefix)}" in model "${safeToken(name)}"`);
20971
+ }
20972
+ for (const [prefix, provider, descriptor] of prefixProviders(providers)) {
20973
+ if (name.startsWith(prefix))
20974
+ return synthesize(provider, name, descriptor);
20975
+ }
20976
+ throw new GatewayError("NO_CANDIDATES", `model "${safeToken(name)}" is not a configured virtual model and its provider could not be inferred`);
20977
+ }
20844
20978
 
20845
20979
  // packages/router/src/index.ts
20846
20980
  function weightedShuffle(candidates, rand, headroom, health) {
@@ -21449,6 +21583,22 @@ async function dryRun(deps, modelId, input) {
21449
21583
  }))
21450
21584
  };
21451
21585
  }
21586
+ // packages/control/src/guardrail.ts
21587
+ async function guardrailReadiness(store, providers, now = Date.now()) {
21588
+ const keysWithPolicy = (await store.keys.list()).filter((key) => key.revokedAt === null && key.guardrailPolicy !== null).length;
21589
+ const snapshot = await buildSnapshot(store, now);
21590
+ const model = snapshot.settings.guardrailModel;
21591
+ if (keysWithPolicy === 0)
21592
+ return { keysWithPolicy, model, problem: null };
21593
+ if (model === null)
21594
+ return { keysWithPolicy, model, problem: "unconfigured" };
21595
+ try {
21596
+ resolveModel(model, snapshot, providers);
21597
+ return { keysWithPolicy, model, problem: null };
21598
+ } catch {
21599
+ return { keysWithPolicy, model, problem: "unresolved" };
21600
+ }
21601
+ }
21452
21602
  // packages/control/src/keys.ts
21453
21603
  function configuredWindows(limits) {
21454
21604
  const needed = new Set;
@@ -21518,7 +21668,8 @@ async function toSummary(store, key, now) {
21518
21668
  bodyLoggingOptOut: key.bodyLoggingOptOut,
21519
21669
  createdAt: key.createdAt,
21520
21670
  revokedAt: key.revokedAt,
21521
- expiresAt: key.expiresAt
21671
+ expiresAt: key.expiresAt,
21672
+ guardrailPolicy: key.guardrailPolicy
21522
21673
  };
21523
21674
  }
21524
21675
  async function listKeys(store, now = Date.now()) {
@@ -21536,7 +21687,8 @@ async function createKey(store, input) {
21536
21687
  modelAllowlist: body.modelAllowlist,
21537
21688
  limits: body.limits,
21538
21689
  bodyLoggingOptOut: body.bodyLoggingOptOut,
21539
- expiresAt: body.expiresAt
21690
+ expiresAt: body.expiresAt,
21691
+ guardrailPolicy: body.guardrailPolicy
21540
21692
  });
21541
21693
  return { id: created.id, label: created.label, prefix: created.prefix, key: raw };
21542
21694
  }
@@ -21564,6 +21716,14 @@ async function setKeyExpiry(store, id, input, now = Date.now()) {
21564
21716
  await store.keys.setExpiry(id, body.expiresAt);
21565
21717
  return toSummary(store, { ...key, expiresAt: body.expiresAt }, now);
21566
21718
  }
21719
+ async function setKeyGuardrail(store, id, input, now = Date.now()) {
21720
+ const body = parseOrThrow(keyGuardrailSchema, input);
21721
+ const key = await store.keys.get(id);
21722
+ if (key === null)
21723
+ throw new GatewayError("BAD_REQUEST", "no such api key");
21724
+ await store.keys.setGuardrailPolicy(id, body.policy);
21725
+ return toSummary(store, { ...key, guardrailPolicy: body.policy }, now);
21726
+ }
21567
21727
  async function revokeKey(store, id) {
21568
21728
  await store.keys.revoke(id);
21569
21729
  }
@@ -21589,15 +21749,21 @@ async function queryUsage(deps, input, scope = ALL) {
21589
21749
  throw new GatewayError("BAD_REQUEST", "this dimension is not available on this surface");
21590
21750
  }
21591
21751
  }
21752
+ if (input.credentialId !== undefined || input.apiKeyId !== undefined) {
21753
+ throw new GatewayError("BAD_REQUEST", "this filter is not available on this surface");
21754
+ }
21592
21755
  }
21756
+ const credentialId = input.credentialId === undefined ? undefined : parseOrThrow(accountIdSchema, input.credentialId);
21757
+ const keyFilter = input.apiKeyId === undefined ? undefined : parseOrThrow(keyIdSchema, input.apiKeyId);
21593
21758
  if (readsNothing(scope))
21594
21759
  return [];
21595
- const apiKeyId = scopeKey(scope);
21760
+ const apiKeyId = scope.kind === "all" ? keyFilter : scopeKey(scope);
21596
21761
  return deps.store.usage.aggregate({
21597
21762
  grain,
21598
21763
  groupBy,
21599
21764
  ...splitBy === undefined ? {} : { splitBy },
21600
21765
  ...apiKeyId === undefined ? {} : { apiKeyId },
21766
+ ...credentialId === undefined ? {} : { credentialId },
21601
21767
  since: optionalNumber(input.since, 0),
21602
21768
  until: optionalNumber(input.until, deps.now())
21603
21769
  });
@@ -23598,12 +23764,23 @@ async function getSettings(store) {
23598
23764
  return store.config.getSettings();
23599
23765
  }
23600
23766
  async function putSettings(store, input) {
23601
- const { bodyRetentionDays, snapshotKeepLatest, snapshotMaxAgeDays, ...rest } = parseOrThrow(settingsSchema, input);
23767
+ const {
23768
+ bodyRetentionDays,
23769
+ snapshotKeepLatest,
23770
+ snapshotMaxAgeDays,
23771
+ guardrailModel,
23772
+ guardrailTimeoutMs,
23773
+ guardrailHistoryTurns,
23774
+ ...rest
23775
+ } = parseOrThrow(settingsSchema, input);
23602
23776
  const patch = {
23603
23777
  ...rest,
23604
23778
  ...bodyRetentionDays === undefined ? {} : { bodyRetentionDays },
23605
23779
  ...snapshotKeepLatest === undefined ? {} : { snapshotKeepLatest },
23606
- ...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays }
23780
+ ...snapshotMaxAgeDays === undefined ? {} : { snapshotMaxAgeDays },
23781
+ ...guardrailModel === undefined ? {} : { guardrailModel },
23782
+ ...guardrailTimeoutMs === undefined ? {} : { guardrailTimeoutMs },
23783
+ ...guardrailHistoryTurns === undefined ? {} : { guardrailHistoryTurns }
23607
23784
  };
23608
23785
  return store.config.putSettings(patch);
23609
23786
  }
@@ -25327,6 +25504,7 @@ var keysList = {
25327
25504
  { header: "MODELS" },
25328
25505
  { header: "LIMITS" },
25329
25506
  { header: "BODY CAPTURE" },
25507
+ { header: "POLICY" },
25330
25508
  { header: "STATE" },
25331
25509
  { header: "EXPIRES" },
25332
25510
  { header: "CREATED" }
@@ -25337,6 +25515,7 @@ var keysList = {
25337
25515
  key.modelAllowlist === null ? "all" : key.modelAllowlist.length === 0 ? "none" : key.modelAllowlist.join(","),
25338
25516
  key.limits === null ? paint(ctx, "red", "unreadable") : summarizeLimits(key.limits),
25339
25517
  key.bodyLoggingOptOut ? "no bodies" : "\u2014",
25518
+ key.guardrailPolicy === null ? "no" : "yes",
25340
25519
  ((verdict) => state(ctx, verdict === "active", verdict))(keyState(key, now)),
25341
25520
  formatTime(key.expiresAt),
25342
25521
  formatTime(key.createdAt)
@@ -25345,7 +25524,7 @@ var keysList = {
25345
25524
  }
25346
25525
  };
25347
25526
  var keysCreate = {
25348
- usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--model-limit <model>:<d>:<w>=N ...] [--expires <when>] [--no-bodies]",
25527
+ usage: "keys create [--label L] [--allow <model> ...] [--limit <d>:<w>=N ...] [--model-limit <model>:<d>:<w>=N ...] [--expires <when>] [--policy <text>] [--no-bodies]",
25349
25528
  summary: "Mint a gateway API key, printed once",
25350
25529
  options: {
25351
25530
  label: { type: "string" },
@@ -25353,10 +25532,12 @@ var keysCreate = {
25353
25532
  limit: { type: "string", multiple: true },
25354
25533
  "model-limit": { type: "string", multiple: true },
25355
25534
  expires: { type: "string" },
25535
+ policy: { type: "string" },
25356
25536
  "no-bodies": { type: "boolean" }
25357
25537
  },
25358
25538
  async run(args, { ctx, writer }) {
25359
25539
  const allow = listFlag(args.values, "allow");
25540
+ const policy = stringFlag(args.values, "policy");
25360
25541
  const expires = stringFlag(args.values, "expires");
25361
25542
  const limits = emptyLimits();
25362
25543
  parseLimitFlags(listFlag(args.values, "limit") ?? [], "--limit", limits.scope);
@@ -25366,7 +25547,8 @@ var keysCreate = {
25366
25547
  modelAllowlist: allow ?? null,
25367
25548
  limits: toWire2(limits),
25368
25549
  bodyLoggingOptOut: boolFlag(args.values, "no-bodies"),
25369
- expiresAt: expires === undefined ? null : parseWhen("--expires", expires)
25550
+ expiresAt: expires === undefined ? null : parseWhen("--expires", expires),
25551
+ guardrailPolicy: policy ?? null
25370
25552
  });
25371
25553
  emit(ctx, writer, created, () => {
25372
25554
  note(ctx, writer, paint(ctx, "yellow", "this key is shown once and stored only as a hash"));
@@ -25482,6 +25664,49 @@ expires: ${formatTime(key.expiresAt)}${verdict === "expired" ? ` ${paint(ctx, "r
25482
25664
  });
25483
25665
  }
25484
25666
  };
25667
+ var keysGuardrail = {
25668
+ usage: "keys guardrail <id> [--policy <text> | --policy-file <path> | --clear]",
25669
+ summary: "Show or replace one key's usage policy",
25670
+ options: {
25671
+ policy: { type: "string" },
25672
+ "policy-file": { type: "string" },
25673
+ clear: { type: "boolean" }
25674
+ },
25675
+ async run(args, { ctx, writer }) {
25676
+ const id = requirePositional(args, 0, "key id");
25677
+ const text = stringFlag(args.values, "policy");
25678
+ const file = stringFlag(args.values, "policy-file");
25679
+ const clear = boolFlag(args.values, "clear");
25680
+ const given = [text !== undefined, file !== undefined, clear].filter(Boolean).length;
25681
+ if (given > 1) {
25682
+ throw new UsageError("pass one of --policy, --policy-file or --clear");
25683
+ }
25684
+ const store = await ctx.store();
25685
+ const existing = (await listKeys(store)).find((entry) => entry.id === id);
25686
+ if (existing === undefined)
25687
+ throw new CliError(`no api key "${id}"`);
25688
+ let key = existing;
25689
+ if (given === 1) {
25690
+ const policy = clear ? null : file !== undefined ? await Bun.file(file).text().catch(() => {
25691
+ throw new CliError(`could not read ${file}`);
25692
+ }) : text;
25693
+ key = await setKeyGuardrail(store, id, { policy });
25694
+ }
25695
+ emit(ctx, writer, key, () => {
25696
+ const head = fields([
25697
+ ["id", key.id],
25698
+ ["label", key.label],
25699
+ ["prefix", `${key.prefix}\u2026`]
25700
+ ]);
25701
+ if (key.guardrailPolicy === null)
25702
+ return `${head}
25703
+ policy: none; requests are not judged`;
25704
+ return `${head}
25705
+ policy:
25706
+ ${key.guardrailPolicy}`;
25707
+ });
25708
+ }
25709
+ };
25485
25710
  var keysRevoke = {
25486
25711
  usage: "keys revoke <id>",
25487
25712
  summary: "Revoke a gateway API key, keeping its usage history",
@@ -26081,7 +26306,7 @@ function createServiceDeps(input) {
26081
26306
  }
26082
26307
 
26083
26308
  // apps/cli/src/version.ts
26084
- var VERSION = "0.14.1";
26309
+ var VERSION = "0.15.0";
26085
26310
 
26086
26311
  // apps/cli/src/commands/service.ts
26087
26312
  function gatewayArgv(root) {
@@ -26236,6 +26461,30 @@ async function danglingCredentials(ctx, plugins) {
26236
26461
  return null;
26237
26462
  }
26238
26463
  }
26464
+ async function guardrailState(ctx) {
26465
+ if (ctx.configError !== null || !existsSync5(ctx.databasePath))
26466
+ return null;
26467
+ try {
26468
+ return await guardrailReadiness(await ctx.store(), PROVIDER_DESCRIPTORS);
26469
+ } catch {
26470
+ return null;
26471
+ }
26472
+ }
26473
+ function guardrailLine(ctx, readiness, plugins) {
26474
+ if (readiness === null)
26475
+ return paint(ctx, "dim", "not checked");
26476
+ const { keysWithPolicy: n, model, problem } = readiness;
26477
+ if (n === 0)
26478
+ return paint(ctx, "dim", "no key carries one");
26479
+ const keys = `${n} key${n === 1 ? "" : "s"}`;
26480
+ if (problem === "unconfigured") {
26481
+ return paint(ctx, "yellow", `${keys} carry a policy and no classifier is set (guardrailModel); their requests are refused`);
26482
+ }
26483
+ if (problem === "unresolved") {
26484
+ return plugins.length > 0 ? paint(ctx, "yellow", `${keys} carry a policy; classifier "${model}" is not a built-in model (plugin providers not checked)`) : paint(ctx, "yellow", `${keys} carry a policy and classifier "${model}" does not resolve; their requests are refused`);
26485
+ }
26486
+ return state(ctx, true, `${keys}, judged by ${model}`);
26487
+ }
26239
26488
  var doctor = {
26240
26489
  usage: "doctor",
26241
26490
  summary: "Check what this CLI resolved, and whether it can do anything with it",
@@ -26251,6 +26500,7 @@ var doctor = {
26251
26500
  const pins = await danglingPins(ctx);
26252
26501
  const uninstalled = await missingProviders(ctx, plugins);
26253
26502
  const strandedKeys = await danglingCredentials(ctx, plugins);
26503
+ const guardrail = await guardrailState(ctx);
26254
26504
  const checks = {
26255
26505
  version: VERSION,
26256
26506
  root: deps.root,
@@ -26272,6 +26522,7 @@ var doctor = {
26272
26522
  danglingPins: pins,
26273
26523
  missingProviders: uninstalled,
26274
26524
  danglingCredentials: strandedKeys,
26525
+ guardrail,
26275
26526
  warnings: ctx.warnings
26276
26527
  };
26277
26528
  emit(ctx, writer, checks, () => {
@@ -26311,6 +26562,7 @@ ${fields([
26311
26562
  "stranded credentials",
26312
26563
  strandedKeys === null ? paint(ctx, "dim", "not checked") : strandedKeys.length === 0 ? ok(true, "none") : paint(ctx, "yellow", `${strandedKeys.length}: ${strandedKeys.join(", ")}`)
26313
26564
  ],
26565
+ ["usage policies", guardrailLine(ctx, guardrail, plugins)],
26314
26566
  ...checks.warnings.map((warning) => ["ignored", paint(ctx, "yellow", warning)])
26315
26567
  ])}`;
26316
26568
  });
@@ -26374,9 +26626,15 @@ function asString2(path, raw) {
26374
26626
  throw new UsageError(`${path} must not be blank`);
26375
26627
  return value;
26376
26628
  }
26629
+ function asNullableString(path, raw) {
26630
+ return raw.trim() === "null" ? null : asString2(path, raw);
26631
+ }
26632
+ function nullable2(head) {
26633
+ return has(DEFAULT_SETTINGS, head) && DEFAULT_SETTINGS[head] === null;
26634
+ }
26377
26635
  var settingsSet = {
26378
26636
  usage: "settings set <path> <value>",
26379
- summary: "Change one setting, e.g. weights.cost 0.4 or rtkEnabled true",
26637
+ summary: "Change one setting, e.g. weights.cost 0.4, rtkEnabled true, or guardrailModel null",
26380
26638
  async run(args, { ctx, writer }) {
26381
26639
  const path = requirePositional(args, 0, "setting path");
26382
26640
  const raw = requirePositional(args, 1, "value");
@@ -26389,7 +26647,7 @@ var settingsSet = {
26389
26647
  throw new UsageError(`"${head}" has no sub-settings`);
26390
26648
  }
26391
26649
  const existing = currentValue(current, head, tail);
26392
- const value = typeof existing === "boolean" ? asBoolean(path, raw) : typeof existing === "string" ? asString2(path, raw) : asNumber(path, raw);
26650
+ const value = tail === undefined && nullable2(head) ? asNullableString(path, raw) : typeof existing === "boolean" ? asBoolean(path, raw) : typeof existing === "string" ? asString2(path, raw) : asNumber(path, raw);
26393
26651
  const next = tail === undefined ? { ...current, [head]: value } : { ...current, weights: { ...current.weights, [tail]: value } };
26394
26652
  const saved = await putSettings(store, next);
26395
26653
  emit(ctx, writer, { settings: saved }, () => `${path} = ${value}`);
@@ -26607,7 +26865,9 @@ var usage = {
26607
26865
  by: { type: "string" },
26608
26866
  split: { type: "string" },
26609
26867
  since: { type: "string" },
26610
- until: { type: "string" }
26868
+ until: { type: "string" },
26869
+ account: { type: "string" },
26870
+ key: { type: "string" }
26611
26871
  },
26612
26872
  async run(args, { ctx, writer }) {
26613
26873
  const until = instant(stringFlag(args.values, "until"), ctx.now());
@@ -26617,7 +26877,9 @@ var usage = {
26617
26877
  groupBy: stringFlag(args.values, "by"),
26618
26878
  splitBy: stringFlag(args.values, "split"),
26619
26879
  since,
26620
- until
26880
+ until,
26881
+ credentialId: stringFlag(args.values, "account"),
26882
+ apiKeyId: stringFlag(args.values, "key")
26621
26883
  });
26622
26884
  emit(ctx, writer, { rows }, () => {
26623
26885
  if (rows.length === 0)
@@ -26811,6 +27073,7 @@ var COMMANDS = {
26811
27073
  "keys limits": keysLimits,
26812
27074
  "keys models": keysModels,
26813
27075
  "keys expiry": keysExpiry,
27076
+ "keys guardrail": keysGuardrail,
26814
27077
  "keys revoke": keysRevoke,
26815
27078
  "plugin list": pluginList,
26816
27079
  "plugin verify": pluginVerify,