omnigateway 0.13.7 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/gateway.js CHANGED
@@ -11250,12 +11250,43 @@ var spendWindows = object({
11250
11250
  "5h": spendLimit.optional(),
11251
11251
  "1w": spendLimit.optional()
11252
11252
  }).strict();
11253
- var limitConfigSchema = object({
11253
+ var limitConfigBaseSchema = object({
11254
11254
  requests: countWindows.optional(),
11255
11255
  tokens: countWindows.optional(),
11256
11256
  spend: spendWindows.optional(),
11257
11257
  concurrency: countLimit.optional()
11258
11258
  }).strict();
11259
+ var MAX_MODEL_OVERRIDES = 64;
11260
+ var MAX_MODEL_NAME_LENGTH = 256;
11261
+ function setsAnyLimit(scope) {
11262
+ if (scope.concurrency !== undefined && scope.concurrency !== null)
11263
+ return true;
11264
+ for (const dimension of ["requests", "tokens", "spend"]) {
11265
+ const windows = scope[dimension];
11266
+ if (windows === undefined)
11267
+ continue;
11268
+ for (const window2 of WINDOWS) {
11269
+ const limit = windows[window2];
11270
+ if (limit !== undefined && limit !== null)
11271
+ return true;
11272
+ }
11273
+ }
11274
+ return false;
11275
+ }
11276
+ var modelOverrideSchema = limitConfigBaseSchema.refine(setsAnyLimit, {
11277
+ message: "a model override must set at least one limit; omit the model to remove it"
11278
+ });
11279
+ var limitConfigSchema = limitConfigBaseSchema.extend({
11280
+ models: record(string2().min(1).max(MAX_MODEL_NAME_LENGTH), modelOverrideSchema).refine((models) => Object.keys(models).length <= MAX_MODEL_OVERRIDES, {
11281
+ message: `at most ${MAX_MODEL_OVERRIDES} model overrides per key`
11282
+ }).optional()
11283
+ }).strict();
11284
+ function modelOverride(limits, model) {
11285
+ const models = limits.models;
11286
+ if (models === undefined || !Object.hasOwn(models, model))
11287
+ return;
11288
+ return models[model];
11289
+ }
11259
11290
  function assertNoProto(value, path) {
11260
11291
  if (typeof value !== "object" || value === null)
11261
11292
  return;
@@ -11372,7 +11403,22 @@ function evaluate(config, counters, now) {
11372
11403
  }
11373
11404
  }
11374
11405
  const violation = lastToClear(violations);
11375
- return { allowed: violation === null, violation, headroom };
11406
+ return { allowed: violation === null, violation, violations, headroom };
11407
+ }
11408
+ function combine(key, model) {
11409
+ const violations = [...key.violations, ...model.violations];
11410
+ const violation = lastToClear(violations);
11411
+ const headroom = { ...key.headroom };
11412
+ for (const dimension of WINDOWED) {
11413
+ const candidate = model.headroom[dimension];
11414
+ if (candidate === undefined)
11415
+ continue;
11416
+ const current = headroom[dimension];
11417
+ if (current === undefined || candidate.used / candidate.limit > current.used / current.limit) {
11418
+ headroom[dimension] = candidate;
11419
+ }
11420
+ }
11421
+ return { allowed: violation === null, violation, violations, headroom };
11376
11422
  }
11377
11423
  function retryAfterMs(violation, now) {
11378
11424
  return violation === null ? 0 : Math.max(0, violation.resetAt - now);
@@ -13242,6 +13288,31 @@ var _005_log_filter_indexes_default = `-- An index for the provider filter on \`
13242
13288
  CREATE INDEX idx_request_logs_provider ON request_logs (resolved_provider, at DESC, id DESC);
13243
13289
  `;
13244
13290
 
13291
+ // packages/store/src/postgres/migrations/006_model_window_index.sql
13292
+ var _006_model_window_index_default = `-- The per-model long-window reads behind a key's model overrides, on this
13293
+ -- backend: \`usage.sumSince\`, \`sumBuckets\` and \`oldestSince\` narrowed to one
13294
+ -- requested model, read from the log because \`usage_rollup\` has no model
13295
+ -- column. The SQLite copy is \`018_model_window_index.sql\`, same shape.
13296
+ --
13297
+ -- Without it the planner has no prefix to seek and the narrowed sums plan as a
13298
+ -- parallel sequential scan of the table. Measured at 500,000 rows in one key's
13299
+ -- week, a fifth of them the overridden model and a hundred rows of a rare one:
13300
+ --
13301
+ -- read without index with index
13302
+ -- sumSince 1w, rare model 22.6ms 0.22ms
13303
+ -- sumSince 1w, 100k-row model 27.1ms 29.4ms
13304
+ -- sumBuckets 1w hour, rare 78.2ms 0.26ms
13305
+ -- sumBuckets 1w hour, 100k-row 89.4ms 55.5ms
13306
+ -- oldestSince 1w, rare 0.58ms 0.11ms
13307
+ --
13308
+ -- The heavy-model sum is a bitmap heap scan over that model's rows and does not
13309
+ -- beat the sequential scan it replaces: the cost is linear in the model's own
13310
+ -- traffic on either plan, and this index removes the part that was linear in
13311
+ -- everyone else's. 15MB at 500,000 rows, against 19MB for the key/time index.
13312
+ -- Neither partial nor covering, for the reasons the SQLite file records.
13313
+ CREATE INDEX idx_request_logs_key_model_at ON request_logs (api_key_id, requested_model, at);
13314
+ `;
13315
+
13245
13316
  // packages/store/src/postgres/db.ts
13246
13317
  var num = (v) => Number(v);
13247
13318
  var numOrNull = (v) => v === null ? null : Number(v);
@@ -13251,7 +13322,8 @@ var MIGRATIONS = [
13251
13322
  { id: 2, sql: _002_health_measurements_default },
13252
13323
  { id: 3, sql: _003_key_expiry_default },
13253
13324
  { id: 4, sql: _004_log_keyset_default },
13254
- { id: 5, sql: _005_log_filter_indexes_default }
13325
+ { id: 5, sql: _005_log_filter_indexes_default },
13326
+ { id: 6, sql: _006_model_window_index_default }
13255
13327
  ];
13256
13328
  async function openPg(url) {
13257
13329
  const sql = new SQL({ url });
@@ -14604,6 +14676,24 @@ var sum = (row) => ({
14604
14676
  tokens: row === undefined ? 0 : num(row.tokens),
14605
14677
  costUsd: row === undefined ? 0 : row.cost_usd
14606
14678
  });
14679
+ var NARROWED_SQL = {
14680
+ sumSince: `SELECT COUNT(*) AS requests,
14681
+ COALESCE(SUM(input_tokens + output_tokens
14682
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
14683
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
14684
+ FROM request_logs
14685
+ WHERE api_key_id = $1 AND requested_model = $2 AND state = 'done' AND at >= $3`,
14686
+ sumBuckets: `SELECT (at / $1) * $2 AS bucket, COUNT(*) AS requests,
14687
+ COALESCE(SUM(input_tokens + output_tokens
14688
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
14689
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
14690
+ FROM request_logs
14691
+ WHERE api_key_id = $3 AND requested_model = $4 AND state = 'done' AND at >= $5
14692
+ GROUP BY 1`,
14693
+ oldestSince: `SELECT MIN(at) AS at
14694
+ FROM request_logs
14695
+ WHERE api_key_id = $1 AND requested_model = $2 AND state = 'done' AND at >= $3`
14696
+ };
14607
14697
  function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
14608
14698
  const complete = async (conn, log) => {
14609
14699
  const written = (await conn.unsafe(COMPLETE, values(log, "done", nodeId))).count;
@@ -14661,7 +14751,15 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
14661
14751
  ORDER BY at, id LIMIT $3`, [cursor.at, cursor.id, limit]);
14662
14752
  return rows.map(toLog);
14663
14753
  },
14664
- async sumSince(apiKeyId, sinceMs) {
14754
+ async sumSince(apiKeyId, sinceMs, requestedModel) {
14755
+ if (requestedModel !== undefined) {
14756
+ const narrowed = await sql.unsafe(NARROWED_SQL.sumSince, [
14757
+ apiKeyId,
14758
+ requestedModel,
14759
+ sinceMs
14760
+ ]);
14761
+ return sum(narrowed[0]);
14762
+ }
14665
14763
  const boundary = hourOf(sinceMs);
14666
14764
  const whole = await sql.unsafe(`SELECT COALESCE(SUM(requests), 0) AS requests,
14667
14765
  COALESCE(SUM(input_tokens + output_tokens
@@ -14683,7 +14781,18 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
14683
14781
  costUsd: a.costUsd + b.costUsd
14684
14782
  };
14685
14783
  },
14686
- async sumBuckets(apiKeyId, sinceMs, grainMs) {
14784
+ async sumBuckets(apiKeyId, sinceMs, grainMs, requestedModel) {
14785
+ if (requestedModel !== undefined) {
14786
+ const from = grainMs === HOUR_MS ? hourOf(sinceMs) * HOUR_MS : sinceMs;
14787
+ const narrowed = await sql.unsafe(NARROWED_SQL.sumBuckets, [
14788
+ grainMs,
14789
+ grainMs,
14790
+ apiKeyId,
14791
+ requestedModel,
14792
+ from
14793
+ ]);
14794
+ return narrowed.map((row) => [num(row.bucket), sum(row)]);
14795
+ }
14687
14796
  const rows = grainMs === HOUR_MS ? await sql.unsafe(`SELECT hour * ${HOUR_MS} AS bucket, requests,
14688
14797
  input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
14689
14798
  AS tokens,
@@ -14704,10 +14813,14 @@ function createUsageRepo(sql, nodeId, dayOffsetMinutes) {
14704
14813
  async auditRollup() {
14705
14814
  return auditRollup2(sql);
14706
14815
  },
14707
- async oldestSince(apiKeyId, sinceMs) {
14708
- const row = (await sql.unsafe(`SELECT MIN(at) AS at
14709
- FROM request_logs
14710
- WHERE api_key_id = $1 AND state = 'done' AND at >= $2`, [apiKeyId, sinceMs]))[0];
14816
+ async oldestSince(apiKeyId, sinceMs, requestedModel) {
14817
+ const row = (requestedModel === undefined ? await sql.unsafe(`SELECT MIN(at) AS at
14818
+ FROM request_logs
14819
+ WHERE api_key_id = $1 AND state = 'done' AND at >= $2`, [apiKeyId, sinceMs]) : await sql.unsafe(NARROWED_SQL.oldestSince, [
14820
+ apiKeyId,
14821
+ requestedModel,
14822
+ sinceMs
14823
+ ]))[0];
14711
14824
  return row === undefined ? null : numOrNull(row.at);
14712
14825
  },
14713
14826
  async aggregate(q) {
@@ -15989,6 +16102,42 @@ CREATE INDEX idx_request_logs_at
15989
16102
  ON request_logs (at DESC, id DESC, requested_model, resolved_model, error_code);
15990
16103
  `;
15991
16104
 
16105
+ // packages/store/src/sqlite/migrations/018_model_window_index.sql
16106
+ var _018_model_window_index_default = `-- An index for the per-model long-window reads behind a key's model overrides:
16107
+ -- \`usage.sumSince\`, \`sumBuckets\` and \`oldestSince\` narrowed to one requested
16108
+ -- model. \`usage_rollup\` has no model column, so those reads come from the log,
16109
+ -- and without this they walk \`idx_request_logs_key_at\` across the key's whole
16110
+ -- window and reject every other model's row one at a time. \`bun:sqlite\` is
16111
+ -- synchronous, so that walk is the event loop \u2014 and the console's key listing
16112
+ -- repeats it once per override per window on every fetch.
16113
+ --
16114
+ -- Key, then model, then time: one seek lands on one key's one model, and the
16115
+ -- window is a range from there. Measured at 500,000 rows in one key's week, a
16116
+ -- fifth of them the overridden model and a hundred rows of a rare one:
16117
+ --
16118
+ -- read without index with index
16119
+ -- sumSince 1w, rare model 45.9ms 0.03ms
16120
+ -- sumSince 1w, 100k-row model 56.1ms 38.1ms
16121
+ -- sumBuckets 1w hour, rare 46.1ms 0.07ms
16122
+ -- sumBuckets 1w hour, 100k-row 76.8ms 58.9ms
16123
+ -- oldestSince 1w, rare 0.57ms 0.00ms
16124
+ --
16125
+ -- So the read stops costing the key's traffic and starts costing the model's
16126
+ -- own, which is still linear: a model that carries most of a key's traffic pays
16127
+ -- most of what it did before. That remaining cost is the design's, stated in
16128
+ -- \`ARCHITECTURE.md#rate-limiting\`, not something this index claims to remove.
16129
+ --
16130
+ -- Not partial on \`state = 'done'\`, and not covering, both measured. Partial
16131
+ -- bought nothing (37.4ms against 38.1ms) and, once \`ANALYZE\` had run, the
16132
+ -- planner moved the key-wide edge read onto it as a skip-scan over every model \u2014
16133
+ -- a plan that degrades with the number of models a key calls. Covering the four
16134
+ -- token columns and cost cut the heavy case to 9.0ms, for an index nearly three
16135
+ -- times as wide written on every completed request, and took the same key-wide
16136
+ -- plan. This shape leaves every key-wide read on \`idx_request_logs_key_at\`,
16137
+ -- which \`migrations.test.ts\` asserts beside the narrowed plans.
16138
+ CREATE INDEX idx_request_logs_key_model_at ON request_logs (api_key_id, requested_model, at);
16139
+ `;
16140
+
15992
16141
  // packages/store/src/sqlite/db.ts
15993
16142
  var MIGRATIONS2 = [
15994
16143
  { id: 1, sql: _001_init_default2 },
@@ -16007,7 +16156,8 @@ var MIGRATIONS2 = [
16007
16156
  { id: 14, sql: _014_key_expiry_default },
16008
16157
  { id: 15, sql: _015_log_keyset_default },
16009
16158
  { id: 16, sql: _016_log_filter_indexes_default },
16010
- { id: 17, sql: _017_log_text_search_default }
16159
+ { id: 17, sql: _017_log_text_search_default },
16160
+ { id: 18, sql: _018_model_window_index_default }
16011
16161
  ];
16012
16162
  function statements(sql) {
16013
16163
  return sql.split(`
@@ -16451,6 +16601,24 @@ var COMPLETE2 = `INSERT INTO request_logs ${COLUMNS2} VALUES ${PLACEHOLDERS2}
16451
16601
  rtk_estimated_tokens_saved = excluded.rtk_estimated_tokens_saved,
16452
16602
  rtk_filters = excluded.rtk_filters
16453
16603
  WHERE request_logs.state = 'pending'`;
16604
+ var NARROWED_SQL2 = {
16605
+ sumSince: `SELECT COUNT(*) AS requests,
16606
+ COALESCE(SUM(input_tokens + output_tokens
16607
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
16608
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
16609
+ FROM request_logs
16610
+ WHERE api_key_id = ? AND requested_model = ? AND state = 'done' AND at >= ?`,
16611
+ sumBuckets: `SELECT (at / ?) * ? AS bucket, COUNT(*) AS requests,
16612
+ COALESCE(SUM(input_tokens + output_tokens
16613
+ + cache_read_tokens + cache_write_tokens), 0) AS tokens,
16614
+ COALESCE(SUM(cost_usd), 0) AS cost_usd
16615
+ FROM request_logs
16616
+ WHERE api_key_id = ? AND requested_model = ? AND state = 'done' AND at >= ?
16617
+ GROUP BY bucket`,
16618
+ oldestSince: `SELECT MIN(at) AS at
16619
+ FROM request_logs
16620
+ WHERE api_key_id = ? AND requested_model = ? AND state = 'done' AND at >= ?`
16621
+ };
16454
16622
  function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes()) {
16455
16623
  const complete = db.transaction((log) => {
16456
16624
  if (db.run(COMPLETE2, values2(log, "done", nodeId)).changes === 0)
@@ -16523,7 +16691,15 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
16523
16691
  ORDER BY at, id LIMIT ?`).all(cursor.at, cursor.at, cursor.id, limit);
16524
16692
  return rows.map(toLog2);
16525
16693
  },
16526
- async sumSince(apiKeyId, sinceMs) {
16694
+ async sumSince(apiKeyId, sinceMs, requestedModel) {
16695
+ if (requestedModel !== undefined) {
16696
+ const row = db.query(NARROWED_SQL2.sumSince).get(apiKeyId, requestedModel, sinceMs);
16697
+ return {
16698
+ requests: row?.requests ?? 0,
16699
+ tokens: row?.tokens ?? 0,
16700
+ costUsd: row?.cost_usd ?? 0
16701
+ };
16702
+ }
16527
16703
  const boundary = hourOf(sinceMs);
16528
16704
  const whole = db.query(`SELECT COALESCE(SUM(requests), 0) AS requests,
16529
16705
  COALESCE(SUM(input_tokens + output_tokens
@@ -16543,7 +16719,15 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
16543
16719
  costUsd: (whole?.cost_usd ?? 0) + (edge?.cost_usd ?? 0)
16544
16720
  };
16545
16721
  },
16546
- async sumBuckets(apiKeyId, sinceMs, grainMs) {
16722
+ async sumBuckets(apiKeyId, sinceMs, grainMs, requestedModel) {
16723
+ if (requestedModel !== undefined) {
16724
+ const from = grainMs === HOUR_MS ? hourOf(sinceMs) * HOUR_MS : sinceMs;
16725
+ const narrowed = db.query(NARROWED_SQL2.sumBuckets).all(grainMs, grainMs, apiKeyId, requestedModel, from);
16726
+ return narrowed.map((row) => [
16727
+ row.bucket,
16728
+ { requests: row.requests, tokens: row.tokens, costUsd: row.cost_usd }
16729
+ ]);
16730
+ }
16547
16731
  const rows = grainMs === HOUR_MS ? db.query(`SELECT hour * ${HOUR_MS} AS bucket, requests,
16548
16732
  input_tokens + output_tokens + cache_read_tokens + cache_write_tokens
16549
16733
  AS tokens,
@@ -16567,10 +16751,10 @@ function createUsageRepo2(db, nodeId, dayOffsetMinutes = hostDayOffsetMinutes())
16567
16751
  async auditRollup() {
16568
16752
  return auditRollup(db);
16569
16753
  },
16570
- async oldestSince(apiKeyId, sinceMs) {
16571
- const row = db.query(`SELECT MIN(at) AS at
16572
- FROM request_logs
16573
- WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs);
16754
+ async oldestSince(apiKeyId, sinceMs, requestedModel) {
16755
+ const row = requestedModel === undefined ? db.query(`SELECT MIN(at) AS at
16756
+ FROM request_logs
16757
+ WHERE api_key_id = ? AND state = 'done' AND at >= ?`).get(apiKeyId, sinceMs) : db.query(NARROWED_SQL2.oldestSince).get(apiKeyId, requestedModel, sinceMs);
16574
16758
  return row?.at ?? null;
16575
16759
  },
16576
16760
  async aggregate(q) {
@@ -16711,9 +16895,9 @@ async function createStore(opts) {
16711
16895
  lastUsedByCredential: (credentialId) => handle.usage.lastUsedByCredential(credentialId),
16712
16896
  scan: (cursor, limit) => handle.usage.scan(cursor, limit),
16713
16897
  aggregate: (q) => handle.usage.aggregate(q),
16714
- sumSince: (apiKeyId, sinceMs) => handle.usage.sumSince(apiKeyId, sinceMs),
16715
- sumBuckets: (apiKeyId, sinceMs, grainMs) => handle.usage.sumBuckets(apiKeyId, sinceMs, grainMs),
16716
- oldestSince: (apiKeyId, sinceMs) => handle.usage.oldestSince(apiKeyId, sinceMs),
16898
+ sumSince: (apiKeyId, sinceMs, requestedModel) => handle.usage.sumSince(apiKeyId, sinceMs, requestedModel),
16899
+ sumBuckets: (apiKeyId, sinceMs, grainMs, requestedModel) => handle.usage.sumBuckets(apiKeyId, sinceMs, grainMs, requestedModel),
16900
+ oldestSince: (apiKeyId, sinceMs, requestedModel) => handle.usage.oldestSince(apiKeyId, sinceMs, requestedModel),
16717
16901
  rebuildRollup: () => handle.usage.rebuildRollup(),
16718
16902
  auditRollup: () => handle.usage.auditRollup(),
16719
16903
  prune: (olderThan) => handle.usage.prune(olderThan),
@@ -17938,6 +18122,22 @@ function codecFailure(id, hook, what) {
17938
18122
  gatewayAuthored: true
17939
18123
  });
17940
18124
  }
18125
+ function framingAgrees(headers, body, bodyEncoding) {
18126
+ const named = (name) => headers.filter(([n]) => n.toLowerCase() === name);
18127
+ const lengths = named("content-length");
18128
+ const encodings = named("content-encoding");
18129
+ if (bodyEncoding !== undefined) {
18130
+ return lengths.length === 0 && encodings.length === 1 && encodings[0]?.[1].trim().toLowerCase() === bodyEncoding;
18131
+ }
18132
+ if (lengths.length > 1 || encodings.length > 1)
18133
+ return false;
18134
+ const length = lengths[0]?.[1];
18135
+ const bytes = new TextEncoder().encode(body).byteLength;
18136
+ if (length !== undefined && length !== String(bytes))
18137
+ return false;
18138
+ const encoding = encodings[0]?.[1];
18139
+ return encoding === undefined || encoding.trim().toLowerCase() === "identity";
18140
+ }
17941
18141
  function boundedDegradations(entries) {
17942
18142
  if (!Array.isArray(entries))
17943
18143
  return [];
@@ -17994,23 +18194,26 @@ function codecAdapter(id, capabilities, codec, origins) {
17994
18194
  fail,
17995
18195
  ...req.requestId === undefined ? {} : { requestId: req.requestId },
17996
18196
  ...req.autoCache === undefined ? {} : { autoCacheEnabled: req.autoCache },
17997
- ...req.maxOutputTokens === undefined ? {} : { maxOutputTokens: req.maxOutputTokens }
18197
+ ...req.maxOutputTokens === undefined ? {} : { maxOutputTokens: req.maxOutputTokens },
18198
+ ...req.requestStartedAt === undefined ? {} : { requestStartedAt: req.requestStartedAt }
17998
18199
  }));
17999
18200
  const described = built?.request ?? {};
18000
18201
  const request = {
18001
18202
  url: described.url,
18002
18203
  method: described.method,
18003
18204
  headers: Array.isArray(described.headers) ? [...described.headers] : described.headers,
18004
- body: described.body
18205
+ body: described.body,
18206
+ bodyEncoding: described.bodyEncoding
18005
18207
  };
18006
- if (typeof request.body !== "string" || !isSendableUrl(request.url) || !isHttpMethod(request.method) || !Array.isArray(request.headers) || !request.headers.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string")) {
18208
+ if (typeof request.body !== "string" || request.bodyEncoding !== undefined && request.bodyEncoding !== "zstd" || !isSendableUrl(request.url) || !isHttpMethod(request.method) || !Array.isArray(request.headers) || !request.headers.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string") || !framingAgrees(request.headers, request.body, request.bodyEncoding)) {
18007
18209
  throw codecFailure(id, "buildRequest", "did not return a usable request");
18008
18210
  }
18009
18211
  const sendable = {
18010
18212
  url: request.url,
18011
18213
  method: request.method,
18012
18214
  headers: request.headers,
18013
- body: request.body
18215
+ body: request.body,
18216
+ ...request.bodyEncoding === "zstd" ? { bodyEncoding: request.bodyEncoding } : {}
18014
18217
  };
18015
18218
  if (origins !== undefined && !withinOrigins(request.url, origins)) {
18016
18219
  throw codecFailure(id, "buildRequest", "described a request to an origin its manifest does not declare");
@@ -18368,53 +18571,61 @@ var museBodyOrder = [
18368
18571
  ];
18369
18572
 
18370
18573
  // packages/providers/src/openai/profile.ts
18371
- var OPENAI_CLI_VERSION = env("OMNI_OPENAI_CLI_VERSION", "0.159.2");
18574
+ var OPENAI_CLI_VERSION = env("OMNI_OPENAI_CLI_VERSION", "0.163.0");
18372
18575
  var OPENAI_UA_PLATFORM = env("OMNI_OPENAI_UA_PLATFORM", "Windows 10.0.26200");
18373
18576
  var OPENAI_UA_ARCH = env("OMNI_OPENAI_UA_ARCH", "x86_64");
18374
18577
  var OPENAI_UA_TERMINAL = env("OMNI_OPENAI_UA_TERMINAL", "WindowsTerminal");
18375
- var originator = env("OMNI_OPENAI_ORIGINATOR", "codex_cli_rs");
18578
+ var OPENAI_ORIGINATOR = env("OMNI_OPENAI_ORIGINATOR", "codex-tui");
18579
+ var OPENAI_SANDBOX = OPENAI_UA_PLATFORM.startsWith("Windows") ? "windows_elevated" : /^(Mac OS|macOS)/.test(OPENAI_UA_PLATFORM) ? "seatbelt" : "seccomp";
18376
18580
  var openaiProfile = {
18377
18581
  headers: [
18378
18582
  [
18379
18583
  "user-agent",
18380
- env("OMNI_UA_OPENAI", `${originator}/${OPENAI_CLI_VERSION} (${OPENAI_UA_PLATFORM}; ${OPENAI_UA_ARCH}) ${OPENAI_UA_TERMINAL}`)
18584
+ env("OMNI_UA_OPENAI", `${OPENAI_ORIGINATOR}/${OPENAI_CLI_VERSION} (${OPENAI_UA_PLATFORM}; ${OPENAI_UA_ARCH}) ` + `${OPENAI_UA_TERMINAL} (${OPENAI_ORIGINATOR}; ${OPENAI_CLI_VERSION})`)
18381
18585
  ],
18382
- ["originator", originator],
18586
+ ["originator", OPENAI_ORIGINATOR],
18383
18587
  ["version", OPENAI_CLI_VERSION],
18384
18588
  ["x-codex-beta-features", "remote_compaction_v2"],
18385
18589
  ["accept", "text/event-stream"]
18386
18590
  ],
18387
18591
  order: envOrder("OMNI_ORDER_OPENAI", [
18388
- "host",
18389
18592
  "version",
18390
18593
  "x-codex-beta-features",
18391
- "originator",
18594
+ "x-codex-window-id",
18595
+ "x-codex-turn-metadata",
18596
+ "x-openai-internal-codex-responses-lite",
18597
+ "x-codex-routing-hint",
18392
18598
  "x-client-request-id",
18393
18599
  "session-id",
18394
18600
  "thread-id",
18395
18601
  "accept",
18602
+ "content-encoding",
18396
18603
  "content-type",
18397
18604
  "authorization",
18398
18605
  "chatgpt-account-id",
18606
+ "originator",
18399
18607
  "user-agent",
18400
- "accept-encoding",
18608
+ "host",
18401
18609
  "content-length"
18402
18610
  ])
18403
18611
  };
18404
18612
  var openaiBodyOrder = [
18405
18613
  "model",
18406
18614
  "stream",
18615
+ "service_tier",
18407
18616
  "input",
18408
18617
  "instructions",
18409
- "store",
18410
- "reasoning",
18411
- "prompt_cache_key",
18412
18618
  "tools",
18413
18619
  "tool_choice",
18620
+ "parallel_tool_calls",
18621
+ "reasoning",
18622
+ "store",
18623
+ "stream_options",
18414
18624
  "include",
18415
- "service_tier",
18625
+ "prompt_cache_key",
18626
+ "text",
18416
18627
  "client_metadata",
18417
- "parallel_tool_calls",
18628
+ "access_programs",
18418
18629
  "metadata"
18419
18630
  ];
18420
18631
 
@@ -22226,6 +22437,81 @@ function parseMuseUsage(value, now) {
22226
22437
  ]);
22227
22438
  }
22228
22439
 
22440
+ // packages/providers/src/openai/codexIdentity.ts
22441
+ import { createHash as createHash3, createHmac } from "crypto";
22442
+ var NAMESPACE_OID = "6ba7b812-9dad-11d1-80b4-00c04fd430c8";
22443
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
22444
+ var WINDOW_END_MS = Date.UTC(2026, 9, 1);
22445
+ var WINDOW_MS2 = 365 * 24 * 60 * 60 * 1000;
22446
+ function isUuid(value) {
22447
+ return UUID.test(value);
22448
+ }
22449
+ function format(bytes) {
22450
+ const hex = Buffer.from(bytes).toString("hex");
22451
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
22452
+ }
22453
+ function withVersion(bytes, version) {
22454
+ const out = Uint8Array.from(bytes.subarray(0, 16));
22455
+ out[6] = (out[6] ?? 0) & 15 | version << 4;
22456
+ out[8] = (out[8] ?? 0) & 63 | 128;
22457
+ return out;
22458
+ }
22459
+ var sha256 = (seed) => createHash3("sha256").update(seed).digest();
22460
+ function uuidV5(namespace, name) {
22461
+ const digest = createHash3("sha1").update(Buffer.from(namespace.replace(/-/g, ""), "hex")).update(name, "utf8").digest();
22462
+ return format(withVersion(digest, 5));
22463
+ }
22464
+ function uuidV4From(seed) {
22465
+ return format(withVersion(sha256(seed), 4));
22466
+ }
22467
+ function uuidV7From(seed, timestampMs) {
22468
+ const bytes = Uint8Array.from(sha256(seed).subarray(0, 16));
22469
+ let ts = Math.max(0, Math.floor(timestampMs));
22470
+ for (let i = 5;i >= 0; i--) {
22471
+ bytes[i] = ts % 256;
22472
+ ts = Math.floor(ts / 256);
22473
+ }
22474
+ return format(withVersion(bytes, 7));
22475
+ }
22476
+ function threadIdFrom(seed) {
22477
+ const digest = sha256(`thread:${seed}`);
22478
+ const offset = digest.readUIntBE(0, 6) % WINDOW_MS2;
22479
+ return uuidV7From(seed, WINDOW_END_MS - WINDOW_MS2 + offset);
22480
+ }
22481
+ function threadInstant(threadId, now) {
22482
+ const hex = threadId.replace(/-/g, "");
22483
+ const version = hex[12];
22484
+ const ts = Number.parseInt(hex.slice(0, 12), 16);
22485
+ if (version === "7" && Number.isFinite(ts) && ts <= now)
22486
+ return ts;
22487
+ const digest = sha256(`thread:${threadId}`);
22488
+ return WINDOW_END_MS - WINDOW_MS2 + digest.readUIntBE(0, 6) % WINDOW_MS2;
22489
+ }
22490
+ var ID_SALT = /^[0-9a-f]{64}$/;
22491
+ function codexSecretsFrom(providerData) {
22492
+ const { installationId, idSalt } = providerData;
22493
+ return {
22494
+ ...typeof installationId === "string" && isUuid(installationId) ? { installationId } : {},
22495
+ ...typeof idSalt === "string" && ID_SALT.test(idSalt) ? { idSalt } : {}
22496
+ };
22497
+ }
22498
+ function ensureCodexSecrets(providerData) {
22499
+ const stored = codexSecretsFrom(providerData);
22500
+ const salt = () => Buffer.from(globalThis.crypto.getRandomValues(new Uint8Array(32))).toString("hex");
22501
+ return {
22502
+ installationId: stored.installationId ?? globalThis.crypto.randomUUID(),
22503
+ idSalt: stored.idSalt ?? salt()
22504
+ };
22505
+ }
22506
+ function keyed(salt, seed) {
22507
+ if (salt === undefined)
22508
+ return seed;
22509
+ return createHmac("sha256", salt).update(seed).digest("hex");
22510
+ }
22511
+ function prefixNamespace(threadId) {
22512
+ return uuidV5(NAMESPACE_OID, threadId);
22513
+ }
22514
+
22229
22515
  // packages/providers/src/openai/oauth.ts
22230
22516
  var CLIENT_ID6 = "app_EMoamEEZ73f0CkXaXp7hrann";
22231
22517
  var AUTHORIZE_URL3 = "https://auth.openai.com/oauth/authorize";
@@ -22286,7 +22572,7 @@ async function* postToken4(body, fail) {
22286
22572
  }
22287
22573
  return token;
22288
22574
  }
22289
- function toResult6(token, fallbackRefresh, now, fallbackAccountId = null) {
22575
+ function toResult6(token, fallbackRefresh, now, fallbackAccountId, prior) {
22290
22576
  const claims = decodeClaims(token.idToken);
22291
22577
  return {
22292
22578
  secrets: {
@@ -22297,7 +22583,10 @@ function toResult6(token, fallbackRefresh, now, fallbackAccountId = null) {
22297
22583
  },
22298
22584
  expiresAt: token.expiresIn === null ? null : now() + token.expiresIn * 1000,
22299
22585
  accountEmail: claims.email,
22300
- providerData: { accountId: claims.accountId ?? fallbackAccountId }
22586
+ providerData: {
22587
+ accountId: claims.accountId ?? fallbackAccountId,
22588
+ ...ensureCodexSecrets(prior)
22589
+ }
22301
22590
  };
22302
22591
  }
22303
22592
  function windowSecondsOf(value) {
@@ -22354,11 +22643,11 @@ var openaiOAuthFlow = {
22354
22643
  url.searchParams.set("id_token_add_organizations", "true");
22355
22644
  url.searchParams.set("codex_cli_simplified_flow", "true");
22356
22645
  url.searchParams.set("state", state);
22357
- url.searchParams.set("originator", "codex_cli_rs");
22646
+ url.searchParams.set("originator", OPENAI_ORIGINATOR);
22358
22647
  url.searchParams.set("prompt", "login");
22359
22648
  return { authorizeUrl: url.toString(), pending: { verifier, challenge, state, redirectUri } };
22360
22649
  },
22361
- async* exchange({ code, pending, fail, now }) {
22650
+ async* exchange({ code, pending, providerData, fail, now }) {
22362
22651
  const [rawCode, pastedState] = code.split("#");
22363
22652
  if (pastedState !== undefined && pastedState !== pending.state) {
22364
22653
  throw fail("AUTH", "authorization state mismatch");
@@ -22369,11 +22658,11 @@ var openaiOAuthFlow = {
22369
22658
  redirect_uri: pending.redirectUri,
22370
22659
  code_verifier: pending.verifier
22371
22660
  }, fail);
22372
- return toResult6(token, null, now);
22661
+ return toResult6(token, null, now, null, providerData ?? {});
22373
22662
  },
22374
22663
  async* refresh({ refreshToken, providerData, fail, now }) {
22375
22664
  const token = yield* postToken4({ grant_type: "refresh_token", refresh_token: refreshToken, scope: SCOPES4 }, fail);
22376
- return toResult6(token, refreshToken, now, accountIdFromProviderData(providerData));
22665
+ return toResult6(token, refreshToken, now, accountIdFromProviderData(providerData), providerData);
22377
22666
  },
22378
22667
  async* usage({ secrets, providerData, now }) {
22379
22668
  if (secrets.accessToken === null)
@@ -23123,7 +23412,7 @@ var customCodec = {
23123
23412
  // packages/providers/src/custom/index.ts
23124
23413
  var customAdapter = codecAdapter("custom", customDescriptor.capabilities, customCodec);
23125
23414
  // packages/providers/src/grok/codec.ts
23126
- import { createHash as createHash4 } from "crypto";
23415
+ import { createHash as createHash5 } from "crypto";
23127
23416
 
23128
23417
  // packages/providers/src/grok/decode.ts
23129
23418
  var ERROR_CODE2 = {
@@ -23322,7 +23611,7 @@ async function* decodeGrokResponses(messages) {
23322
23611
  }
23323
23612
 
23324
23613
  // packages/providers/src/grok/wire.ts
23325
- import { createHash as createHash3 } from "crypto";
23614
+ import { createHash as createHash4 } from "crypto";
23326
23615
  var MAX_TOOLS = 200;
23327
23616
  function encodeToolChoice3(c) {
23328
23617
  switch (c.type) {
@@ -23338,7 +23627,7 @@ function encodeToolChoice3(c) {
23338
23627
  }
23339
23628
  function promptCacheKey(instructions, firstInput) {
23340
23629
  const stable = JSON.stringify({ instructions, firstInput });
23341
- return createHash3("sha256").update(stable).digest("hex").slice(0, 32);
23630
+ return createHash4("sha256").update(stable).digest("hex").slice(0, 32);
23342
23631
  }
23343
23632
  function toGrokWire(req, model) {
23344
23633
  const degradations = [];
@@ -23478,7 +23767,7 @@ var PROXY_URL = "https://cli-chat-proxy.grok.com/v1/responses";
23478
23767
  var API_URL = "https://api.x.ai/v1/responses";
23479
23768
  var CONV_NAMESPACE = "omni-grok-conv";
23480
23769
  function derivedUuid(value) {
23481
- const h = createHash4("sha256").update(`${CONV_NAMESPACE}:${value}`).digest("hex");
23770
+ const h = createHash5("sha256").update(`${CONV_NAMESPACE}:${value}`).digest("hex");
23482
23771
  const variant = (Number.parseInt(h[16], 16) & 3 | 8).toString(16);
23483
23772
  return [
23484
23773
  h.slice(0, 8),
@@ -23538,11 +23827,12 @@ var grokAdapter = codecAdapter("grok", grokDescriptor.capabilities, grokCodec);
23538
23827
  import { request as httpRequest } from "http";
23539
23828
  import { request as httpsRequest } from "https";
23540
23829
  import { Readable } from "stream";
23830
+ import * as zlib from "zlib";
23541
23831
  var CONNECT_ATTEMPT_TIMEOUT_MS = 3000;
23542
23832
  function nodeHttpClient(options = {}) {
23543
23833
  const logger = options.logger ?? noopLogger;
23544
23834
  const now = options.now ?? (() => Date.now());
23545
- return (req) => new Promise((resolve, reject) => {
23835
+ return (req) => encodeBody(req).then((bodyBytes) => new Promise((resolve, reject) => {
23546
23836
  const url = new URL(req.url);
23547
23837
  const startedAt = now();
23548
23838
  let traced = false;
@@ -23573,7 +23863,6 @@ function nodeHttpClient(options = {}) {
23573
23863
  });
23574
23864
  };
23575
23865
  const send = url.protocol === "https:" ? httpsRequest : httpRequest;
23576
- const bodyBytes = Buffer.from(req.body, "utf8");
23577
23866
  const headers = {};
23578
23867
  for (const [name, value] of req.headers)
23579
23868
  headers[name] = value;
@@ -23631,6 +23920,22 @@ function nodeHttpClient(options = {}) {
23631
23920
  if (bodyBytes.byteLength > 0)
23632
23921
  outgoing.write(bodyBytes);
23633
23922
  outgoing.end();
23923
+ }));
23924
+ }
23925
+ function encodeBody(req) {
23926
+ const raw = Buffer.from(req.body, "utf8");
23927
+ if (req.bodyEncoding !== "zstd")
23928
+ return Promise.resolve(raw);
23929
+ return zstdEncode(raw);
23930
+ }
23931
+ function zstdEncode(raw, create = (options) => zlib.createZstdCompress(options)) {
23932
+ return new Promise((resolve, reject) => {
23933
+ const stream = create({ params: { [zlib.constants.ZSTD_c_compressionLevel]: 3 } });
23934
+ const chunks = [];
23935
+ stream.on("data", (chunk) => chunks.push(chunk));
23936
+ stream.on("end", () => resolve(Buffer.concat(chunks)));
23937
+ stream.on("error", reject);
23938
+ stream.end(raw);
23634
23939
  });
23635
23940
  }
23636
23941
  function hasHeader(req, lowerName) {
@@ -24473,7 +24778,7 @@ async function* decodeMuseResponses(messages, options = {}) {
24473
24778
  }
24474
24779
 
24475
24780
  // packages/providers/src/muse/wire.ts
24476
- import { createHash as createHash5 } from "crypto";
24781
+ import { createHash as createHash6 } from "crypto";
24477
24782
  function suppliedKey(req) {
24478
24783
  const vendor = req.vendor?.openai;
24479
24784
  for (const name of ["prompt_cache_key", "session_id"]) {
@@ -24485,7 +24790,7 @@ function suppliedKey(req) {
24485
24790
  }
24486
24791
  function cacheKey(req, instructions, firstInput) {
24487
24792
  const stable = req.conversationId !== undefined && req.conversationId.length > 0 ? JSON.stringify({ conversation: req.conversationId }) : JSON.stringify({ instructions, firstInput: firstInput ?? null });
24488
- return createHash5("sha256").update(stable).digest("hex").slice(0, 32);
24793
+ return createHash6("sha256").update(stable).digest("hex").slice(0, 32);
24489
24794
  }
24490
24795
  function repairOrphanedCalls(input) {
24491
24796
  const answered = new Set;
@@ -24685,6 +24990,49 @@ var museCodec = {
24685
24990
 
24686
24991
  // packages/providers/src/muse/index.ts
24687
24992
  var museAdapter = codecAdapter("muse", museDescriptor.capabilities, museCodec);
24993
+ // packages/providers/src/openai/codexCatalog.ts
24994
+ var CODEX_EFFORTS = new Set([
24995
+ "none",
24996
+ "minimal",
24997
+ "low",
24998
+ "medium",
24999
+ "high",
25000
+ "xhigh",
25001
+ "max",
25002
+ "ultra",
25003
+ "persistent"
25004
+ ]);
25005
+ function isCodexEffort(value) {
25006
+ return typeof value === "string" && CODEX_EFFORTS.has(value);
25007
+ }
25008
+ var entry = (defaultEffort, nodeReplAutoReviewRequired, over = {}) => ({
25009
+ lite: true,
25010
+ verbosity: "low",
25011
+ defaultEffort,
25012
+ lowestEffort: "low",
25013
+ nodeReplAutoReviewRequired,
25014
+ nodeReplDisabled: false,
25015
+ supportsParallelToolCalls: true,
25016
+ ...over
25017
+ });
25018
+ var UNLISTED = entry("low", false);
25019
+ var MODELS = Object.assign(Object.create(null), {
25020
+ "gpt-6-astra": entry("low", true),
25021
+ "gpt-6.1-sol": entry("low", true),
25022
+ "gpt-6-sol": entry("medium", true),
25023
+ "gpt-6-luna": entry("medium", false),
25024
+ "gpt-5.6-sol": entry("low", false),
25025
+ "gpt-5.6-terra": entry("medium", false),
25026
+ "gpt-5.6-luna": entry("medium", false),
25027
+ "gpt-daybreak-blue-latest": entry("low", false),
25028
+ "gpt-daybreak-red-latest": entry("medium", false, { verbosity: "high" }),
25029
+ "gpt-5.5": entry("medium", false, { lite: false }),
25030
+ "codex-auto-review": entry("medium", false)
25031
+ });
25032
+ function codexModel(model) {
25033
+ return MODELS[model] ?? UNLISTED;
25034
+ }
25035
+
24688
25036
  // packages/providers/src/openai/decode.ts
24689
25037
  var ERROR_CODE4 = {
24690
25038
  rate_limit_exceeded: "RATE_LIMIT",
@@ -24902,8 +25250,170 @@ async function* decodeResponses(messages, options = {}) {
24902
25250
  }
24903
25251
  }
24904
25252
 
25253
+ // packages/providers/src/openai/codexLite.ts
25254
+ var PASSTHROUGH = "internal_chat_message_metadata_passthrough";
25255
+ function turnId(threadId, index, threadAt, salt) {
25256
+ return uuidV7From(keyed(salt, `${threadId}:turn:${index}`), threadAt + index);
25257
+ }
25258
+ function contentKinds(content, role) {
25259
+ if (!Array.isArray(content))
25260
+ return;
25261
+ if (role === "assistant")
25262
+ return content.map(() => "unknown");
25263
+ const kinds = [];
25264
+ for (const part of content) {
25265
+ const type = part.type;
25266
+ if (type === "input_text")
25267
+ kinds.push("user.text");
25268
+ else if (type === "input_image")
25269
+ kinds.push("user.image");
25270
+ else
25271
+ return;
25272
+ }
25273
+ return kinds;
25274
+ }
25275
+ function toLiteInput(args) {
25276
+ const { threadId } = args;
25277
+ const threadAt = threadInstant(threadId, args.startedAt ?? Number.POSITIVE_INFINITY);
25278
+ const startedAt = args.startedAt ?? threadAt;
25279
+ const historySeed = args.requestId ?? threadId;
25280
+ const salt = args.idSalt;
25281
+ const namespace = prefixNamespace(threadId);
25282
+ const prefix = [];
25283
+ if (args.tools !== undefined && args.tools.length > 0) {
25284
+ const tools = [{ type: "namespace", name: "functions", description: "", tools: args.tools }];
25285
+ prefix.push({
25286
+ type: "additional_tools",
25287
+ id: `at_${uuidV5(namespace, JSON.stringify(tools))}`,
25288
+ role: "developer",
25289
+ tools
25290
+ });
25291
+ }
25292
+ if (args.instructions !== undefined && args.instructions.length > 0) {
25293
+ prefix.push({
25294
+ type: "message",
25295
+ id: `msg_${uuidV5(namespace, args.instructions)}`,
25296
+ role: "developer",
25297
+ content: [{ type: "input_text", text: args.instructions }],
25298
+ [PASSTHROUGH]: { content_item_kinds: ["model.base_instructions"] }
25299
+ });
25300
+ }
25301
+ let turn = 0;
25302
+ const history = args.input.map((raw, index) => {
25303
+ const item = raw;
25304
+ turn = args.turnOf.get(item) ?? turn;
25305
+ const turn_id = turnId(threadId, turn, threadAt, salt);
25306
+ const fresh = (prefixName) => `${prefixName}_${uuidV7From(keyed(salt, `${historySeed}:item:${index}`), startedAt)}`;
25307
+ const create_time = (startedAt * 1000 + index) / 1e6;
25308
+ switch (item.type) {
25309
+ case "message": {
25310
+ const role = String(item.role);
25311
+ const kinds = contentKinds(item.content, role);
25312
+ if (role === "assistant") {
25313
+ return {
25314
+ type: "message",
25315
+ status: "completed",
25316
+ id: fresh("msg"),
25317
+ role,
25318
+ content: Array.isArray(item.content) ? item.content.map((part) => part.type === "output_text" ? { ...part, annotations: [] } : part) : item.content,
25319
+ [PASSTHROUGH]: {
25320
+ turn_id,
25321
+ ...kinds === undefined ? {} : { content_item_kinds: kinds }
25322
+ }
25323
+ };
25324
+ }
25325
+ return {
25326
+ type: "message",
25327
+ id: fresh("msg"),
25328
+ role,
25329
+ content: item.content,
25330
+ [PASSTHROUGH]: {
25331
+ turn_id,
25332
+ create_time,
25333
+ ...kinds === undefined ? {} : { content_item_kinds: kinds }
25334
+ }
25335
+ };
25336
+ }
25337
+ case "function_call":
25338
+ return {
25339
+ type: "function_call",
25340
+ status: "completed",
25341
+ id: fresh("fc"),
25342
+ name: item.name,
25343
+ arguments: item.arguments,
25344
+ call_id: item.call_id,
25345
+ [PASSTHROUGH]: { turn_id }
25346
+ };
25347
+ case "function_call_output":
25348
+ return {
25349
+ type: "function_call_output",
25350
+ id: fresh("fco"),
25351
+ call_id: item.call_id,
25352
+ output: item.output,
25353
+ [PASSTHROUGH]: { turn_id, create_time }
25354
+ };
25355
+ case "reasoning": {
25356
+ const { [PASSTHROUGH]: _client, ...rest } = item;
25357
+ return { ...rest, [PASSTHROUGH]: { turn_id } };
25358
+ }
25359
+ default:
25360
+ return item;
25361
+ }
25362
+ });
25363
+ return {
25364
+ input: [...prefix, ...history],
25365
+ turnId: turnId(threadId, turn, threadAt, salt),
25366
+ startedAt
25367
+ };
25368
+ }
25369
+ function asciiJson(value) {
25370
+ return JSON.stringify(value).replace(/[^\x20-\x7e]/g, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, "0")}`);
25371
+ }
25372
+ function codexTurn(args) {
25373
+ const { threadId, turnId: turn } = args;
25374
+ const windowId = `${threadId}:0`;
25375
+ const installationId = args.installationId ?? uuidV4From(`installation:${args.installationSeed}`);
25376
+ const threadAt = threadInstant(threadId, args.startedAt);
25377
+ const catalog = codexModel(args.model);
25378
+ const turnMetadata = asciiJson({
25379
+ installation_id: installationId,
25380
+ session_id: threadId,
25381
+ thread_id: threadId,
25382
+ agent_name: "/root",
25383
+ turn_id: turn,
25384
+ window_id: windowId,
25385
+ window_number: 0,
25386
+ context_window_id: uuidV7From(keyed(args.idSalt, `${threadId}:context-window`), threadAt),
25387
+ request_kind: "turn",
25388
+ history_initialization: "new",
25389
+ root_turn_id: turn,
25390
+ thread_source: "user",
25391
+ turn_trigger: "user",
25392
+ sandbox: OPENAI_SANDBOX,
25393
+ sandbox_mode: "workspace-write",
25394
+ auto_review_enabled: false,
25395
+ node_repl_auto_review_required: catalog.nodeReplAutoReviewRequired,
25396
+ node_repl_disabled: catalog.nodeReplDisabled,
25397
+ turn_started_at_unix_ms: args.startedAt,
25398
+ analytics_enabled: false,
25399
+ model: args.model,
25400
+ reasoning_effort: args.effort
25401
+ });
25402
+ const clientMetadata = {
25403
+ turn_id: turn,
25404
+ thread_id: threadId,
25405
+ "x-codex-turn-metadata": turnMetadata,
25406
+ mcp_attribution: '{"status":"none"}',
25407
+ guardian_credits_requested: "true",
25408
+ "x-codex-installation-id": installationId,
25409
+ "x-codex-window-id": windowId,
25410
+ root_turn_id: turn,
25411
+ session_id: threadId
25412
+ };
25413
+ return { windowId, turnMetadata, clientMetadata };
25414
+ }
25415
+
24905
25416
  // packages/providers/src/openai/wire.ts
24906
- import { createHash as createHash6 } from "crypto";
24907
25417
  var MAX_CACHE_KEY_LENGTH = 1024;
24908
25418
  var HEADER_SAFE_KEY = /^[\x21-\x7e]+$/;
24909
25419
  function suppliedKey2(req) {
@@ -24915,9 +25425,16 @@ function suppliedKey2(req) {
24915
25425
  }
24916
25426
  return;
24917
25427
  }
24918
- function cacheKey2(req, instructions, firstInput) {
24919
- const stable = req.conversationId !== undefined && req.conversationId.length > 0 ? JSON.stringify({ conversation: req.conversationId }) : JSON.stringify({ instructions, firstInput: firstInput ?? null });
24920
- return createHash6("sha256").update(stable).digest("hex").slice(0, 32);
25428
+ function cacheKey2(req, instructions, firstInput, salt) {
25429
+ const supplied = suppliedKey2(req);
25430
+ if (supplied !== undefined) {
25431
+ if (isUuid(supplied))
25432
+ return { key: supplied, seed: supplied };
25433
+ const seed = JSON.stringify({ key: supplied });
25434
+ return { key: threadIdFrom(keyed(salt, seed)), seed };
25435
+ }
25436
+ const seed = req.conversationId !== undefined && req.conversationId.length > 0 ? JSON.stringify({ conversation: req.conversationId }) : JSON.stringify({ instructions, firstInput: firstInput ?? null });
25437
+ return { key: threadIdFrom(keyed(salt, seed)), seed };
24921
25438
  }
24922
25439
  function repairOrphanedCalls2(input) {
24923
25440
  const answered = new Set;
@@ -24948,6 +25465,44 @@ function encodeToolChoice7(c) {
24948
25465
  return { type: "function", name: c.name };
24949
25466
  }
24950
25467
  }
25468
+ function codexReasoning(req, model, note) {
25469
+ const catalog = codexModel(model);
25470
+ const context = catalog.lite ? { context: "all_turns" } : {};
25471
+ const r = req.reasoning;
25472
+ if (r === undefined)
25473
+ return { effort: catalog.defaultEffort, ...context };
25474
+ if (r.mode === "off") {
25475
+ note("openai:reasoning-off-as-lowest");
25476
+ return { effort: catalog.lowestEffort, ...context };
25477
+ }
25478
+ if (r.mode === "betweenTools")
25479
+ return { effort: catalog.lowestEffort, ...context };
25480
+ if (r.mode === "budget") {
25481
+ note("openai:reasoning-budget-dropped");
25482
+ return { effort: catalog.defaultEffort, ...context };
25483
+ }
25484
+ return {
25485
+ effort: r.effort ?? catalog.defaultEffort,
25486
+ ...r.display === "omitted" || r.display === "updates" ? {} : { summary: "auto" },
25487
+ ...context
25488
+ };
25489
+ }
25490
+ var isRecord8 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
25491
+ function mergeCodexVendor(body, vendor, note) {
25492
+ const target = body;
25493
+ for (const [name, value] of Object.entries(vendor)) {
25494
+ if (name === "client_metadata") {
25495
+ note("openai:client-metadata-replaced");
25496
+ continue;
25497
+ }
25498
+ const ours = target[name];
25499
+ if ((name === "text" || name === "reasoning") && isRecord8(ours) && isRecord8(value)) {
25500
+ target[name] = { ...ours, ...value };
25501
+ continue;
25502
+ }
25503
+ target[name] = value;
25504
+ }
25505
+ }
24951
25506
  function toResponsesWire(req, model, opts = { oauth: false }) {
24952
25507
  const degradations = [];
24953
25508
  const input = [];
@@ -24955,6 +25510,12 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24955
25510
  if (!degradations.includes(d))
24956
25511
  degradations.push(d);
24957
25512
  };
25513
+ const turnOf = new WeakMap;
25514
+ let turn = -1;
25515
+ const push = (item) => {
25516
+ input.push(item);
25517
+ turnOf.set(item, Math.max(0, turn));
25518
+ };
24958
25519
  if (req.betas?.includes(CONTEXT_1M_BETA))
24959
25520
  note("openai:context-1m-dropped");
24960
25521
  for (const message of req.messages) {
@@ -24966,10 +25527,15 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24966
25527
  const flush = () => {
24967
25528
  if (parts.length === 0)
24968
25529
  return;
24969
- input.push({ type: "message", role, content: [...parts] });
25530
+ push({ type: "message", role, content: [...parts] });
24970
25531
  parts.length = 0;
24971
25532
  };
25533
+ let opened = false;
24972
25534
  for (const block of message.content) {
25535
+ if (message.role === "user" && !opened && block.type !== "toolResult") {
25536
+ opened = true;
25537
+ turn++;
25538
+ }
24973
25539
  switch (block.type) {
24974
25540
  case "text":
24975
25541
  parts.push({
@@ -24990,7 +25556,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24990
25556
  break;
24991
25557
  case "toolUse":
24992
25558
  flush();
24993
- input.push({
25559
+ push({
24994
25560
  type: "function_call",
24995
25561
  call_id: block.id,
24996
25562
  name: block.name,
@@ -25002,7 +25568,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25002
25568
  note("openai:anthropic-native-block-dropped");
25003
25569
  flush();
25004
25570
  const files = responsesFiles(block.files ?? []);
25005
- input.push({
25571
+ push({
25006
25572
  type: "function_call_output",
25007
25573
  call_id: block.toolUseId,
25008
25574
  output: withFileText(block.content, files.text)
@@ -25030,7 +25596,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25030
25596
  if (block.provider === "openai") {
25031
25597
  flush();
25032
25598
  const { id: _id, ...rest } = block.data;
25033
- input.push({ type: block.blockType, ...rest });
25599
+ push({ type: block.blockType, ...rest });
25034
25600
  break;
25035
25601
  }
25036
25602
  note("openai:anthropic-native-block-dropped");
@@ -25042,8 +25608,10 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25042
25608
  repairOrphanedCalls2(input);
25043
25609
  const body = { model, input, stream: req.stream, store: false };
25044
25610
  const instructions = systemText(req.system, "openai", note);
25045
- if (instructions !== undefined && instructions.length > 0)
25611
+ const lite = opts.oauth && codexModel(model).lite;
25612
+ if (instructions !== undefined && instructions.length > 0 && !opts.oauth) {
25046
25613
  body.instructions = instructions;
25614
+ }
25047
25615
  if (req.maxTokens !== undefined) {
25048
25616
  if (opts.oauth)
25049
25617
  note("openai:max-tokens-dropped");
@@ -25056,22 +25624,45 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25056
25624
  else
25057
25625
  body.temperature = req.temperature;
25058
25626
  }
25627
+ let liteTools;
25059
25628
  if (req.tools !== undefined) {
25060
25629
  const custom = req.tools.filter((t) => t.kind === "portable");
25061
25630
  if (custom.length !== req.tools.length)
25062
25631
  note("openai:anthropic-tool-dropped");
25063
- body.tools = custom.map((t) => ({
25064
- type: "function",
25065
- name: t.name,
25066
- description: t.description,
25067
- parameters: t.inputSchema
25068
- }));
25632
+ if (opts.oauth) {
25633
+ const tools = custom.map((t) => ({
25634
+ type: "function",
25635
+ name: t.name,
25636
+ description: t.description,
25637
+ strict: false,
25638
+ parameters: t.inputSchema
25639
+ }));
25640
+ if (lite)
25641
+ liteTools = tools;
25642
+ else
25643
+ body.tools = tools;
25644
+ } else {
25645
+ body.tools = custom.map((t) => ({
25646
+ type: "function",
25647
+ name: t.name,
25648
+ description: t.description,
25649
+ parameters: t.inputSchema
25650
+ }));
25651
+ }
25069
25652
  }
25070
25653
  if (req.toolChoice !== undefined)
25071
25654
  body.tool_choice = encodeToolChoice7(req.toolChoice);
25072
- if (req.reasoning?.mode === "betweenTools")
25073
- degradations.push("openai:reasoning-between-tools-as-off");
25074
- if (req.reasoning !== undefined && req.reasoning.mode !== "off" && req.reasoning.mode !== "betweenTools") {
25655
+ else if (opts.oauth)
25656
+ body.tool_choice = "auto";
25657
+ if (opts.oauth) {
25658
+ body.parallel_tool_calls = !lite && codexModel(model).supportsParallelToolCalls;
25659
+ }
25660
+ if (req.reasoning?.mode === "betweenTools") {
25661
+ note(opts.oauth ? "openai:reasoning-between-tools-as-lowest" : "openai:reasoning-between-tools-as-off");
25662
+ }
25663
+ if (opts.oauth) {
25664
+ body.reasoning = codexReasoning(req, model, note);
25665
+ } else if (req.reasoning !== undefined && req.reasoning.mode !== "off" && req.reasoning.mode !== "betweenTools") {
25075
25666
  if (req.reasoning.mode === "budget") {
25076
25667
  degradations.push("openai:reasoning-budget-dropped");
25077
25668
  } else {
@@ -25082,36 +25673,98 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25082
25673
  };
25083
25674
  }
25084
25675
  }
25085
- const key = suppliedKey2(req) ?? cacheKey2(req, instructions ?? "", input[0]);
25086
- Object.assign(body, req.vendor?.openai ?? {});
25676
+ const salt = opts.oauth ? opts.idSalt : undefined;
25677
+ const { key, seed } = cacheKey2(req, instructions ?? "", input[0], salt);
25678
+ if (opts.oauth) {
25679
+ body.include = ["reasoning.encrypted_content"];
25680
+ body.text = { verbosity: codexModel(model).verbosity };
25681
+ mergeCodexVendor(body, req.vendor?.openai ?? {}, note);
25682
+ } else {
25683
+ Object.assign(body, req.vendor?.openai ?? {});
25684
+ }
25087
25685
  const moved = moveToResponsesText(req, body, "openai");
25088
25686
  if (moved !== undefined)
25089
25687
  note(moved);
25090
25688
  body.prompt_cache_key = key;
25091
- return { body, degradations, cacheKey: key };
25689
+ if (!opts.oauth)
25690
+ return { body, degradations, cacheKey: key };
25691
+ const built = toLiteInput({
25692
+ threadId: key,
25693
+ input,
25694
+ turnOf,
25695
+ tools: liteTools,
25696
+ instructions,
25697
+ requestId: opts.requestId,
25698
+ startedAt: opts.startedAt,
25699
+ idSalt: salt
25700
+ });
25701
+ body.input = built.input;
25702
+ const sent = body.reasoning?.effort;
25703
+ const codex = codexTurn({
25704
+ threadId: key,
25705
+ turnId: built.turnId,
25706
+ installationId: opts.installationId,
25707
+ installationSeed: opts.accountId ?? seed,
25708
+ idSalt: salt,
25709
+ model,
25710
+ effort: isCodexEffort(sent) ? sent : codexModel(model).defaultEffort,
25711
+ startedAt: built.startedAt
25712
+ });
25713
+ body.client_metadata = codex.clientMetadata;
25714
+ return { body, degradations, cacheKey: key, codex };
25092
25715
  }
25093
25716
 
25094
25717
  // packages/providers/src/openai/codec.ts
25095
25718
  var OAUTH_URL2 = "https://chatgpt.com/backend-api/codex/responses";
25096
25719
  var API_URL3 = "https://api.openai.com/v1/responses";
25720
+ var HEADER_SAFE = /^[\x21-\x7e]+$/;
25721
+ function routingHint(model, tier) {
25722
+ if (!HEADER_SAFE.test(model))
25723
+ return;
25724
+ if (typeof tier === "string" && HEADER_SAFE.test(tier) && !tier.includes(";")) {
25725
+ return `model=${model};tier=${tier}`;
25726
+ }
25727
+ return `model=${model}`;
25728
+ }
25097
25729
  var openaiCodec = {
25098
25730
  buildRequest(input) {
25099
25731
  const oauth = input.credentials.accessToken !== null;
25100
- const { body, degradations, cacheKey } = toResponsesWire(input.request, input.model, { oauth });
25101
- const protocol = [["content-type", "application/json"]];
25732
+ const accountId = input.credentials.providerData.accountId;
25733
+ const secrets = oauth ? codexSecretsFrom(input.credentials.providerData) : {};
25734
+ const { body, degradations, cacheKey, codex } = toResponsesWire(input.request, input.model, {
25735
+ oauth,
25736
+ requestId: input.requestId,
25737
+ startedAt: input.requestStartedAt,
25738
+ accountId: typeof accountId === "string" ? accountId : undefined,
25739
+ installationId: secrets.installationId,
25740
+ idSalt: secrets.idSalt
25741
+ });
25742
+ const protocol = [];
25743
+ if (codex !== undefined) {
25744
+ protocol.push(["x-codex-window-id", codex.windowId], ["x-codex-turn-metadata", codex.turnMetadata]);
25745
+ if (codexModel(input.model).lite) {
25746
+ protocol.push(["x-openai-internal-codex-responses-lite", "true"]);
25747
+ }
25748
+ const hint = routingHint(input.model, body.service_tier);
25749
+ if (hint !== undefined)
25750
+ protocol.push(["x-codex-routing-hint", hint]);
25751
+ }
25752
+ protocol.push(["x-client-request-id", cacheKey], ["session-id", cacheKey], ["thread-id", cacheKey]);
25753
+ if (oauth)
25754
+ protocol.push(["content-encoding", "zstd"]);
25755
+ protocol.push(["content-type", "application/json"]);
25102
25756
  if (oauth) {
25103
25757
  protocol.push(["authorization", `Bearer ${input.credentials.accessToken}`]);
25104
- const accountId = input.credentials.providerData.accountId;
25105
25758
  if (typeof accountId === "string")
25106
25759
  protocol.push(["chatgpt-account-id", accountId]);
25760
+ protocol.push(["host", "chatgpt.com"]);
25107
25761
  } else if (input.credentials.apiKey !== null) {
25108
25762
  protocol.push(["authorization", `Bearer ${input.credentials.apiKey}`]);
25109
25763
  } else {
25110
25764
  throw input.fail("AUTH", "openai credential has no token");
25111
25765
  }
25112
- protocol.push(["x-client-request-id", cacheKey], ["session-id", cacheKey], ["thread-id", cacheKey]);
25113
25766
  const headers = orderHeaders(mergeHeaders(openaiProfile.headers, protocol), openaiProfile.order);
25114
- const include = body.include;
25767
+ const include = input.request.vendor?.openai?.include;
25115
25768
  const nativeReasoning = Array.isArray(include) && include.includes("reasoning.encrypted_content");
25116
25769
  return {
25117
25770
  decodeState: { nativeReasoning },
@@ -25119,7 +25772,8 @@ var openaiCodec = {
25119
25772
  url: oauth ? OAUTH_URL2 : API_URL3,
25120
25773
  method: "POST",
25121
25774
  headers,
25122
- body: JSON.stringify(orderFields({ ...body, stream: true }, openaiBodyOrder))
25775
+ body: JSON.stringify(orderFields({ ...body, stream: true }, openaiBodyOrder)),
25776
+ ...oauth ? { bodyEncoding: "zstd" } : {}
25123
25777
  },
25124
25778
  degradations
25125
25779
  };
@@ -25218,11 +25872,17 @@ function createConnectFlows(deps) {
25218
25872
  const provider = deps.providers[flow.provider];
25219
25873
  if (provider === undefined)
25220
25874
  throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
25221
- const result = await provider.exchange({ code, pending: flow.pending }, { http: deps.http, now: deps.now });
25875
+ const existing = flow.credentialId === undefined ? null : await deps.store.credentials.get(flow.credentialId);
25876
+ if (flow.credentialId !== undefined && existing === null) {
25877
+ throw new GatewayError("BAD_REQUEST", "credential not found");
25878
+ }
25879
+ const prior = existing !== null && existing.provider === flow.provider ? existing.providerData : undefined;
25880
+ const result = await provider.exchange({
25881
+ code,
25882
+ pending: flow.pending,
25883
+ ...prior === undefined ? {} : { providerData: prior }
25884
+ }, { http: deps.http, now: deps.now });
25222
25885
  if (flow.credentialId !== undefined) {
25223
- const existing = await deps.store.credentials.get(flow.credentialId);
25224
- if (existing === null)
25225
- throw new GatewayError("BAD_REQUEST", "credential not found");
25226
25886
  await deps.store.credentials.updateSecrets(flow.credentialId, result.secrets, result.expiresAt);
25227
25887
  await deps.store.credentials.update(flow.credentialId, {
25228
25888
  enabled: true,
@@ -26553,9 +27213,20 @@ function configuredWindows(limits) {
26553
27213
  async function readLimitUsage(store, id, limits, now) {
26554
27214
  if (limits === null)
26555
27215
  return [];
27216
+ const readings = await readScope(store, id, limits, null, now);
27217
+ for (const model of Object.keys(limits.models ?? {})) {
27218
+ const override = modelOverride(limits, model);
27219
+ if (override === undefined)
27220
+ continue;
27221
+ readings.push(...await readScope(store, id, override, model, now));
27222
+ }
27223
+ return readings;
27224
+ }
27225
+ async function readScope(store, id, limits, model, now) {
26556
27226
  const sums = new Map;
26557
27227
  for (const window2 of configuredWindows(limits)) {
26558
- sums.set(window2, await store.usage.sumSince(id, now - WINDOW_MS[window2]));
27228
+ const since = now - WINDOW_MS[window2];
27229
+ sums.set(window2, await store.usage.sumSince(id, since, model ?? undefined));
26559
27230
  }
26560
27231
  const readings = [];
26561
27232
  for (const dimension of ["requests", "tokens", "spend"]) {
@@ -26568,7 +27239,7 @@ async function readLimitUsage(store, id, limits, now) {
26568
27239
  continue;
26569
27240
  const sum = sums.get(window2);
26570
27241
  const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
26571
- readings.push({ dimension, window: window2, limit, used });
27242
+ readings.push({ dimension, window: window2, limit, used, model });
26572
27243
  }
26573
27244
  }
26574
27245
  if (limits.concurrency !== undefined && limits.concurrency !== null) {
@@ -26576,7 +27247,8 @@ async function readLimitUsage(store, id, limits, now) {
26576
27247
  dimension: "concurrency",
26577
27248
  window: null,
26578
27249
  limit: limits.concurrency,
26579
- used: null
27250
+ used: null,
27251
+ model
26580
27252
  });
26581
27253
  }
26582
27254
  return readings;
@@ -27550,7 +28222,7 @@ function createRefresher(deps) {
27550
28222
  };
27551
28223
  }
27552
28224
  // packages/control/src/pluginProviders.ts
27553
- function isRecord8(value) {
28225
+ function isRecord9(value) {
27554
28226
  return typeof value === "object" && value !== null && !Array.isArray(value);
27555
28227
  }
27556
28228
  function isNumber(value) {
@@ -27561,7 +28233,7 @@ function checkAt(ok, id, path, expected) {
27561
28233
  throw new Error(`provider ${id} descriptor: ${path} must be ${expected}`);
27562
28234
  }
27563
28235
  function checkLimits(id, path, value) {
27564
- checkAt(isRecord8(value), id, path, "an object");
28236
+ checkAt(isRecord9(value), id, path, "an object");
27565
28237
  const limits = value;
27566
28238
  checkAt(isNumber(limits.contextWindow), id, `${path}.contextWindow`, "a finite number");
27567
28239
  checkAt(isNumber(limits.maxOutputTokens), id, `${path}.maxOutputTokens`, "a finite number");
@@ -27579,19 +28251,19 @@ function checkDescriptor(id, record) {
27579
28251
  }
27580
28252
  }
27581
28253
  const capabilities = record.capabilities;
27582
- checkAt(isRecord8(capabilities), id, "capabilities", "an object");
28254
+ checkAt(isRecord9(capabilities), id, "capabilities", "an object");
27583
28255
  for (const flag of ["tools", "images", "reasoning"]) {
27584
28256
  const value = capabilities[flag];
27585
28257
  checkAt(typeof value === "boolean", id, `capabilities.${flag}`, "a boolean");
27586
28258
  }
27587
28259
  const write = record.writeOverInput;
27588
- checkAt(isRecord8(write), id, "writeOverInput", "an object");
28260
+ checkAt(isRecord9(write), id, "writeOverInput", "an object");
27589
28261
  for (const ttl of ["fiveMinute", "oneHour"]) {
27590
28262
  const value = write[ttl];
27591
28263
  checkAt(isNumber(value), id, `writeOverInput.${ttl}`, "a finite number");
27592
28264
  }
27593
28265
  const catalog = record.catalog;
27594
- checkAt(isRecord8(catalog), id, "catalog", "an object");
28266
+ checkAt(isRecord9(catalog), id, "catalog", "an object");
27595
28267
  const entry = catalog;
27596
28268
  checkAt(typeof entry.defaultModel === "string", id, "catalog.defaultModel", "a string");
27597
28269
  checkAt(Array.isArray(entry.authTypes), id, "catalog.authTypes", "an array");
@@ -27602,11 +28274,11 @@ function checkDescriptor(id, record) {
27602
28274
  checkAt(Array.isArray(entry.models), id, "catalog.models", "an array");
27603
28275
  for (const [index, model] of entry.models.entries()) {
27604
28276
  const at = `catalog.models[${index}]`;
27605
- checkAt(isRecord8(model), id, at, "an object");
28277
+ checkAt(isRecord9(model), id, at, "an object");
27606
28278
  const choice = model;
27607
28279
  checkAt(typeof choice.id === "string", id, `${at}.id`, "a string");
27608
28280
  checkAt(typeof choice.label === "string", id, `${at}.label`, "a string");
27609
- checkAt(isRecord8(choice.pricing), id, `${at}.pricing`, "an object");
28281
+ checkAt(isRecord9(choice.pricing), id, `${at}.pricing`, "an object");
27610
28282
  const pricing = choice.pricing;
27611
28283
  for (const field of ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h"]) {
27612
28284
  checkAt(isNumber(pricing[field]), id, `${at}.pricing.${field}`, "a finite number");
@@ -27622,13 +28294,13 @@ function checkDescriptor(id, record) {
27622
28294
  checkAt(typeof prefix === "string", id, `modelPrefixes[${index}]`, "a string");
27623
28295
  }
27624
28296
  const presentation = record.presentation;
27625
- checkAt(isRecord8(presentation), id, "presentation", "an object");
28297
+ checkAt(isRecord9(presentation), id, "presentation", "an object");
27626
28298
  const shown = presentation;
27627
28299
  for (const field of ["label", "tone"]) {
27628
28300
  checkAt(typeof shown[field] === "string", id, `presentation.${field}`, "a string");
27629
28301
  }
27630
28302
  checkAt(isNumber(shown.order), id, "presentation.order", "a finite number");
27631
- checkAt(isRecord8(shown.colour), id, "presentation.colour", "an object");
28303
+ checkAt(isRecord9(shown.colour), id, "presentation.colour", "an object");
27632
28304
  for (const scheme of ["light", "dark"]) {
27633
28305
  const value = shown.colour[scheme];
27634
28306
  checkAt(typeof value === "string", id, `presentation.colour.${scheme}`, "a string");
@@ -27640,7 +28312,7 @@ function checkDescriptor(id, record) {
27640
28312
  function validateOAuthFlow(id, value) {
27641
28313
  if (value === undefined)
27642
28314
  return;
27643
- if (!isRecord8(value))
28315
+ if (!isRecord9(value))
27644
28316
  throw new Error(`provider ${id} oauth must be an object`);
27645
28317
  const kind = value.kind;
27646
28318
  if (kind !== "pkce" && kind !== "device") {
@@ -28297,7 +28969,7 @@ function readFrom(path, offset) {
28297
28969
  }
28298
28970
  }
28299
28971
  // packages/plugin-api/src/events.ts
28300
- var WINDOW_MS2 = {
28972
+ var WINDOW_MS3 = {
28301
28973
  "1m": 60000,
28302
28974
  "5h": 5 * 60 * 60 * 1000,
28303
28975
  "1w": 7 * 24 * 60 * 60 * 1000
@@ -41648,8 +42320,26 @@ var SEED_LOCK_MS = 5000;
41648
42320
  var GAUGE_TTL_MS = 300000;
41649
42321
  var RENEW_INTERVAL_MS = 1e5;
41650
42322
  var MAX_RENEWED_MS = 86400000;
41651
- function bucketKey(keyId, window2) {
41652
- return `lim:${keyId}:${window2}`;
42323
+ function bucketKey(subject, window2) {
42324
+ return `lim:${subject}:${window2}`;
42325
+ }
42326
+ function modelSubjectId(keyId, model) {
42327
+ return `${keyId}/m/${encodeURIComponent(model)}`;
42328
+ }
42329
+ function keyWide(limits) {
42330
+ const { models: _models, ...base } = limits;
42331
+ return base;
42332
+ }
42333
+ function subjectsOf(keyId, limits, model) {
42334
+ const subjects = [];
42335
+ const base = keyWide(limits);
42336
+ if (anyLimit(base))
42337
+ subjects.push({ id: keyId, keyId, model: undefined, limits: base });
42338
+ const override = modelOverride(limits, model);
42339
+ if (override !== undefined) {
42340
+ subjects.push({ id: modelSubjectId(keyId, model), keyId, model, limits: override });
42341
+ }
42342
+ return subjects;
41653
42343
  }
41654
42344
 
41655
42345
  class RateLimitExceeded extends GatewayError {
@@ -41704,27 +42394,31 @@ class ApiKeyRateLimiter {
41704
42394
  this.coord = deps.coord ?? memoryCoord();
41705
42395
  this.onRejected = deps.onRejected;
41706
42396
  }
41707
- async admit(keyId, limits, requestId) {
41708
- if (!anyLimit(limits))
42397
+ async admit(keyId, limits, model, requestId) {
42398
+ const subjects = subjectsOf(keyId, limits, model);
42399
+ if (subjects.length === 0)
41709
42400
  return { release: () => {}, renew: async () => {}, headroom: {} };
41710
42401
  const now = this.now();
41711
- this.limits.set(keyId, limits);
41712
- const holder = configured(limits.concurrency) ? crypto.randomUUID() : undefined;
41713
- const claim = await this.claim(keyId, now, true, holder);
42402
+ const claims = [];
41714
42403
  let admitted = false;
41715
42404
  try {
41716
- const counters = await this.counters(keyId, limits, claim, now, requestId);
41717
- const decision = evaluate(limits, counters, now);
41718
- await this.refuse(keyId, decision, now, requestId);
42405
+ for (const subject of subjects) {
42406
+ this.limits.set(subject.id, subject.limits);
42407
+ const holder = configured(subject.limits.concurrency) ? crypto.randomUUID() : undefined;
42408
+ claims.push(await this.claim(subject.id, now, true, holder));
42409
+ }
42410
+ const decision = await this.judge(subjects, claims, (scope) => scope, now, requestId);
41719
42411
  admitted = true;
42412
+ const held = claims.flatMap((claim) => claim.holder === undefined ? [] : [{ subject: claim.subject, holder: claim.holder }]);
41720
42413
  let released = false;
41721
42414
  const renewUntil = now + MAX_RENEWED_MS;
41722
42415
  const renew = async () => {
41723
- if (released || holder === undefined)
42416
+ if (released)
41724
42417
  return;
41725
- await this.coord.gauge.renew(keyId, holder, GAUGE_TTL_MS);
42418
+ for (const slot of held)
42419
+ await this.coord.gauge.renew(slot.subject, slot.holder, GAUGE_TTL_MS);
41726
42420
  };
41727
- const renewal = holder === undefined ? null : setInterval(() => {
42421
+ const renewal = held.length === 0 ? null : setInterval(() => {
41728
42422
  if (this.now() >= renewUntil) {
41729
42423
  clearInterval(renewal ?? undefined);
41730
42424
  return;
@@ -41738,91 +42432,153 @@ class ApiKeyRateLimiter {
41738
42432
  released = true;
41739
42433
  if (renewal !== null)
41740
42434
  clearInterval(renewal);
41741
- this.coord.gauge.release(keyId, holder);
42435
+ for (const claim of claims)
42436
+ this.coord.gauge.release(claim.subject, claim.holder);
41742
42437
  };
41743
42438
  return { release, renew, headroom: decision.headroom };
41744
42439
  } finally {
41745
42440
  if (!admitted)
41746
- await this.rollback(keyId, claim);
42441
+ for (const claim of claims)
42442
+ await this.rollback(claim);
41747
42443
  }
41748
42444
  }
41749
- async consume(keyId, limits, requestId) {
41750
- const requests = limits.requests;
41751
- if (requests === undefined || !anyRequestLimit(limits))
42445
+ async consume(keyId, limits, model, requestId) {
42446
+ const subjects = subjectsOf(keyId, limits, model).filter((subject) => anyRequestLimit(subject.limits));
42447
+ if (subjects.length === 0)
41752
42448
  return;
41753
42449
  const now = this.now();
41754
- this.limits.set(keyId, limits);
41755
- const claim = await this.claim(keyId, now, false);
42450
+ const claims = [];
41756
42451
  let consumed = false;
41757
42452
  try {
41758
- const counters = await this.counters(keyId, { requests }, claim, now, requestId);
41759
- await this.refuse(keyId, evaluate({ requests }, counters, now), now, requestId);
42453
+ for (const subject of subjects) {
42454
+ this.limits.set(subject.id, subject.limits);
42455
+ claims.push(await this.claim(subject.id, now, false));
42456
+ }
42457
+ await this.judge(subjects, claims, requestsOnly, now, requestId);
41760
42458
  consumed = true;
41761
42459
  } finally {
41762
42460
  if (!consumed)
41763
- await this.rollback(keyId, claim);
42461
+ for (const claim of claims)
42462
+ await this.rollback(claim);
42463
+ }
42464
+ }
42465
+ async judge(subjects, claims, scope, now, requestId) {
42466
+ const decisions = [];
42467
+ for (const [index, subject] of subjects.entries()) {
42468
+ const claim = claims[index];
42469
+ if (claim === undefined)
42470
+ throw new Error("rate limit claim missing for a subject");
42471
+ const limits = scope(subject.limits);
42472
+ const counters = await this.counters(subject, limits, claim, now, requestId);
42473
+ decisions.push(evaluate(limits, counters, now));
42474
+ }
42475
+ const decision = combineAll(decisions);
42476
+ if (decision.violation !== null)
42477
+ await this.refuse(subjects, decisions, now, requestId);
42478
+ return decision;
42479
+ }
42480
+ async claim(subject, now, gauge, holder) {
42481
+ const ring = await this.coord.window.claim(subject, WINDOW_MS["1m"], now);
42482
+ let concurrency;
42483
+ try {
42484
+ concurrency = gauge ? await this.coord.gauge.acquire(subject, GAUGE_TTL_MS, holder) : await this.coord.gauge.read(subject);
42485
+ } catch (error) {
42486
+ await this.coord.window.rollback(subject, ring.stamp);
42487
+ throw error;
41764
42488
  }
42489
+ return {
42490
+ subject,
42491
+ stamp: ring.stamp,
42492
+ gauge,
42493
+ holder,
42494
+ before: { requests: ring.before, concurrency }
42495
+ };
41765
42496
  }
41766
- async claim(keyId, now, gauge, holder) {
41767
- const ring = await this.coord.window.claim(keyId, WINDOW_MS["1m"], now);
41768
- const concurrency = gauge ? await this.coord.gauge.acquire(keyId, GAUGE_TTL_MS, holder) : await this.coord.gauge.read(keyId);
41769
- return { stamp: ring.stamp, gauge, holder, before: { requests: ring.before, concurrency } };
41770
- }
41771
- async rollback(keyId, claim) {
41772
- await this.coord.window.rollback(keyId, claim.stamp);
42497
+ async rollback(claim) {
42498
+ await this.coord.window.rollback(claim.subject, claim.stamp);
41773
42499
  if (claim.gauge)
41774
- await this.coord.gauge.release(keyId, claim.holder);
42500
+ await this.coord.gauge.release(claim.subject, claim.holder);
41775
42501
  }
41776
- debit(keyId, usage) {
41777
- const limits = this.limits.get(keyId);
42502
+ debit(keyId, requestedModel, usage) {
42503
+ const delta = { requests: 1, tokens: usage.tokens, costUsd: usage.costUsd };
42504
+ this.debitSubject(keyId, delta);
42505
+ this.debitSubject(modelSubjectId(keyId, requestedModel), delta);
42506
+ }
42507
+ debitSubject(subject, delta) {
42508
+ const limits = this.limits.get(subject);
41778
42509
  if (limits === undefined || !anyLongLimit(limits))
41779
42510
  return;
41780
42511
  const now = this.now();
41781
- const delta = { requests: 1, tokens: usage.tokens, costUsd: usage.costUsd };
41782
42512
  for (const window2 of LONG_WINDOWS) {
41783
- this.coord.buckets.add(bucketKey(keyId, window2), GRAIN_MS[window2], WINDOW_MS[window2], now, delta);
42513
+ this.coord.buckets.add(bucketKey(subject, window2), GRAIN_MS[window2], WINDOW_MS[window2], now, delta);
41784
42514
  }
41785
42515
  }
41786
- inFlight(keyId) {
41787
- return this.coord.gauge.read(keyId);
42516
+ inFlight(subject) {
42517
+ return this.coord.gauge.read(subject);
41788
42518
  }
41789
- async refuse(keyId, decision, now, requestId) {
42519
+ async refuse(subjects, decisions, now, requestId) {
42520
+ const corrected = [];
42521
+ for (const [index, scope] of decisions.entries()) {
42522
+ const subject = subjects[index];
42523
+ if (subject === undefined)
42524
+ throw new Error("rate limit decision from no subject");
42525
+ corrected.push(await this.correct(subject, scope, now, requestId));
42526
+ }
42527
+ const decision = combineAll(corrected);
41790
42528
  const violation = decision.violation;
41791
42529
  if (violation === null)
41792
- return;
41793
- const window2 = violation.window;
41794
- const exact = await this.exactReset(keyId, window2, now, requestId);
41795
- const resolved = exact === null ? violation : { ...violation, resetAt: exact };
41796
- const headroom = exact === null || window2 === null ? decision.headroom : withReset(decision.headroom, window2, exact);
42530
+ throw new Error("rate limit refusal with no violation");
41797
42531
  try {
41798
42532
  this.onRejected?.(violation.dimension, violation.window);
41799
42533
  } catch {}
41800
- throw new RateLimitExceeded(retryAfterMs(resolved, now), headroom);
42534
+ throw new RateLimitExceeded(retryAfterMs(violation, now), refusedHeadroom(decision, violation));
42535
+ }
42536
+ async correct(subject, scope, now, requestId) {
42537
+ if (scope.violations.length === 0)
42538
+ return scope;
42539
+ const exact = new Map;
42540
+ for (const violation of scope.violations) {
42541
+ const window2 = violation.window;
42542
+ if (window2 === null || exact.has(window2))
42543
+ continue;
42544
+ const reset = await this.exactReset(subject, window2, now, requestId);
42545
+ if (reset !== null)
42546
+ exact.set(window2, reset);
42547
+ }
42548
+ const violations = scope.violations.map((violation) => {
42549
+ const reset = violation.window === null ? undefined : exact.get(violation.window);
42550
+ return reset === undefined ? violation : { ...violation, resetAt: reset };
42551
+ });
42552
+ let headroom = scope.headroom;
42553
+ for (const [window2, reset] of exact)
42554
+ headroom = withReset(headroom, window2, reset);
42555
+ return { ...scope, violation: lastToClear(violations), violations, headroom };
41801
42556
  }
41802
- async exactReset(keyId, window2, now, requestId) {
42557
+ async exactReset(subject, window2, now, requestId) {
41803
42558
  if (window2 === null || window2 === "1m")
41804
42559
  return null;
41805
42560
  const length = WINDOW_MS[window2];
41806
42561
  try {
41807
- const oldest = await this.store.usage.oldestSince(keyId, now - length);
42562
+ const oldest = await this.store.usage.oldestSince(subject.keyId, now - length, subject.model);
41808
42563
  if (oldest === null)
41809
42564
  return null;
41810
42565
  return oldest + length;
41811
42566
  } catch (error) {
41812
42567
  this.logger.warn("rate limit reset unavailable", {
41813
42568
  ...requestId === undefined ? {} : { requestId },
41814
- apiKeyId: keyId,
42569
+ apiKeyId: subject.keyId,
42570
+ ...subject.model === undefined ? {} : { model: subject.model },
41815
42571
  reason: describeError(error, "unknown")
41816
42572
  });
41817
42573
  return null;
41818
42574
  }
41819
42575
  }
41820
- async counters(keyId, limits, claim, now, requestId) {
42576
+ async counters(subject, limits, claim, now, requestId) {
41821
42577
  const requests = { "1m": claim.before.requests };
41822
42578
  const snapshot = { requests, concurrency: claim.before.concurrency };
41823
42579
  if (!anyLongLimit(limits))
41824
42580
  return snapshot;
41825
- const counts = await this.longCounts(keyId, now, requestId);
42581
+ const counts = await this.longCounts(subject, now, requestId);
41826
42582
  if (counts === null)
41827
42583
  return snapshot;
41828
42584
  const tokens = {};
@@ -41838,20 +42594,21 @@ class ApiKeyRateLimiter {
41838
42594
  snapshot.spend = spend;
41839
42595
  return snapshot;
41840
42596
  }
41841
- async longCounts(keyId, now, requestId) {
42597
+ async longCounts(subject, now, requestId) {
41842
42598
  const out = {};
41843
42599
  for (const window2 of LONG_WINDOWS) {
41844
- const key = bucketKey(keyId, window2);
42600
+ const key = bucketKey(subject.id, window2);
41845
42601
  const grain = GRAIN_MS[window2];
41846
42602
  const length = WINDOW_MS[window2];
41847
42603
  let sum = await this.coord.buckets.sum(key, grain, length, now);
41848
42604
  if (sum === null) {
41849
42605
  try {
41850
- sum = await this.seed(keyId, window2, now);
42606
+ sum = await this.seed(subject, window2, now);
41851
42607
  } catch (error) {
41852
42608
  this.logger.warn("rate limit counters unavailable", {
41853
42609
  ...requestId === undefined ? {} : { requestId },
41854
- apiKeyId: keyId,
42610
+ apiKeyId: subject.keyId,
42611
+ ...subject.model === undefined ? {} : { model: subject.model },
41855
42612
  reason: describeError(error, "unknown")
41856
42613
  });
41857
42614
  return null;
@@ -41861,15 +42618,15 @@ class ApiKeyRateLimiter {
41861
42618
  }
41862
42619
  return out;
41863
42620
  }
41864
- async seed(keyId, window2, now) {
41865
- const key = bucketKey(keyId, window2);
42621
+ async seed(subject, window2, now) {
42622
+ const key = bucketKey(subject.id, window2);
41866
42623
  const grain = GRAIN_MS[window2];
41867
42624
  const length = WINDOW_MS[window2];
41868
42625
  const install = async () => {
41869
42626
  const already = await this.coord.buckets.sum(key, grain, length, now);
41870
42627
  if (already !== null)
41871
42628
  return already;
41872
- const rows = await this.store.usage.sumBuckets(keyId, now - length, grain);
42629
+ const rows = await this.store.usage.sumBuckets(subject.keyId, now - length, grain, subject.model);
41873
42630
  await this.coord.buckets.seed(key, grain, length, now, rows);
41874
42631
  return await this.coord.buckets.sum(key, grain, length, now) ?? zero();
41875
42632
  };
@@ -41885,6 +42642,9 @@ class ApiKeyRateLimiter {
41885
42642
  function zero() {
41886
42643
  return { requests: 0, tokens: 0, costUsd: 0 };
41887
42644
  }
42645
+ function requestsOnly(limits) {
42646
+ return limits.requests === undefined ? {} : { requests: limits.requests };
42647
+ }
41888
42648
  function withReset(headroom, window2, resetAt) {
41889
42649
  const corrected = { ...headroom };
41890
42650
  for (const dimension of ["requests", "tokens", "spend"]) {
@@ -41894,6 +42654,27 @@ function withReset(headroom, window2, resetAt) {
41894
42654
  }
41895
42655
  return corrected;
41896
42656
  }
42657
+ function refusedHeadroom(decision, violation) {
42658
+ const window2 = violation.window;
42659
+ if (window2 === null || violation.dimension === "concurrency")
42660
+ return decision.headroom;
42661
+ return {
42662
+ ...decision.headroom,
42663
+ [violation.dimension]: {
42664
+ window: window2,
42665
+ limit: violation.limit,
42666
+ used: violation.used,
42667
+ remaining: Math.max(0, violation.limit - violation.used),
42668
+ resetAt: violation.resetAt
42669
+ }
42670
+ };
42671
+ }
42672
+ function combineAll(decisions) {
42673
+ const [first, ...rest] = decisions;
42674
+ if (first === undefined)
42675
+ return { allowed: true, violation: null, violations: [], headroom: {} };
42676
+ return rest.reduce(combine, first);
42677
+ }
41897
42678
 
41898
42679
  // apps/gateway/src/dispatch/loadRegistry.ts
41899
42680
  var PREFIX6 = "load:";
@@ -42811,7 +43592,7 @@ function zodDetail(issue) {
42811
43592
  }
42812
43593
  return issue.message;
42813
43594
  }
42814
- function isRecord9(value) {
43595
+ function isRecord10(value) {
42815
43596
  return typeof value === "object" && value !== null;
42816
43597
  }
42817
43598
  function extraFields(body, known) {
@@ -42954,7 +43735,7 @@ async function readJson(request) {
42954
43735
  }
42955
43736
  async function readJsonRecord(request) {
42956
43737
  const body = await readJson(request);
42957
- return isRecord9(body) ? body : null;
43738
+ return isRecord10(body) ? body : null;
42958
43739
  }
42959
43740
  function apiErrorResponse(error) {
42960
43741
  const gatewayError = error instanceof GatewayError ? error : new GatewayError("INTERNAL", "internal error");
@@ -46041,7 +46822,7 @@ async function finishLog(store, log, keyId, logger = noopLogger, bodies, debit,
46041
46822
  report(logger, "failed to persist request log", log.id, error);
46042
46823
  }
46043
46824
  if (keyId !== null && debit !== undefined) {
46044
- debit(keyId, {
46825
+ debit(keyId, log.requestedModel, {
46045
46826
  tokens: log.inputTokens + log.outputTokens + log.cacheReadTokens + log.cacheWriteTokens,
46046
46827
  costUsd: log.costUsd
46047
46828
  });
@@ -46210,6 +46991,7 @@ async function attempt(opts) {
46210
46991
  signal,
46211
46992
  ...opts.requestId === undefined ? {} : { requestId: opts.requestId },
46212
46993
  ...opts.autoCache === undefined ? {} : { autoCache: opts.autoCache },
46994
+ ...opts.requestStartedAt === undefined ? {} : { requestStartedAt: opts.requestStartedAt },
46213
46995
  ...candidate.target.maxOutputTokens === undefined ? {} : { maxOutputTokens: candidate.target.maxOutputTokens }
46214
46996
  });
46215
46997
  }
@@ -46529,6 +47311,7 @@ async function dispatch(request, deps, signal, requestId) {
46529
47311
  ...retrySecrets === undefined ? {} : { secrets: retrySecrets },
46530
47312
  logger,
46531
47313
  requestId,
47314
+ requestStartedAt: startedAt,
46532
47315
  autoCache: snapshot.settings.autoCacheEnabled
46533
47316
  }), dispatchSignal);
46534
47317
  noteDegradations(result.degradations);
@@ -47374,10 +48157,10 @@ function toIrToolChoice(c) {
47374
48157
  }
47375
48158
  var EFFORTS = REASONING_EFFORTS;
47376
48159
  function readEffort(body) {
47377
- if (!isRecord9(body))
48160
+ if (!isRecord10(body))
47378
48161
  return;
47379
48162
  const outputConfig = body.output_config;
47380
- if (!isRecord9(outputConfig))
48163
+ if (!isRecord10(outputConfig))
47381
48164
  return;
47382
48165
  const effort = outputConfig.effort;
47383
48166
  return EFFORTS.find((level) => level === effort);
@@ -47759,7 +48542,7 @@ function roleOf(raw) {
47759
48542
  return "user";
47760
48543
  }
47761
48544
  function contentPart(part) {
47762
- if (!isRecord9(part))
48545
+ if (!isRecord10(part))
47763
48546
  throw new GatewayError("BAD_REQUEST", "input: content part must be an object");
47764
48547
  const type = part.type;
47765
48548
  if (type === "input_text" || type === "output_text") {
@@ -47783,11 +48566,11 @@ function readToolOutput(output) {
47783
48566
  const images = [];
47784
48567
  const files = [];
47785
48568
  for (const part of output) {
47786
- if (isRecord9(part) && (part.type === "input_text" || part.type === "output_text")) {
48569
+ if (isRecord10(part) && (part.type === "input_text" || part.type === "output_text")) {
47787
48570
  text.push(String(part.text ?? ""));
47788
- } else if (isRecord9(part) && part.type === "input_image" && typeof part.image_url === "string" && part.image_url.startsWith("data:")) {
48571
+ } else if (isRecord10(part) && part.type === "input_image" && typeof part.image_url === "string" && part.image_url.startsWith("data:")) {
47789
48572
  images.push({ type: "image", ...parseDataUrl(part.image_url) });
47790
- } else if (isRecord9(part) && part.type === "input_file" && typeof part.file_data === "string") {
48573
+ } else if (isRecord10(part) && part.type === "input_file" && typeof part.file_data === "string") {
47791
48574
  files.push({
47792
48575
  type: "file",
47793
48576
  ...fileData(part.file_data),
@@ -47838,7 +48621,7 @@ function readInputItems(items) {
47838
48621
  messages.push({ role, content });
47839
48622
  };
47840
48623
  for (const raw of items) {
47841
- if (!isRecord9(raw))
48624
+ if (!isRecord10(raw))
47842
48625
  throw new GatewayError("BAD_REQUEST", "input: item must be an object");
47843
48626
  const type = raw.type ?? (raw.role === undefined ? undefined : "message");
47844
48627
  switch (type) {
@@ -47894,7 +48677,7 @@ function jsonArguments(raw, callId) {
47894
48677
  const text = typeof raw === "string" ? raw : "";
47895
48678
  try {
47896
48679
  const parsed = text === "" ? {} : JSON.parse(text);
47897
- return isRecord9(parsed) ? parsed : { value: parsed };
48680
+ return isRecord10(parsed) ? parsed : { value: parsed };
47898
48681
  } catch {
47899
48682
  throw new GatewayError("BAD_REQUEST", `input: arguments for call ${safeToken(callId)} are not valid JSON`);
47900
48683
  }
@@ -47903,7 +48686,7 @@ var TOOL_NAME = /^[A-Za-z0-9_-]{1,128}$/;
47903
48686
  function readTools(raw) {
47904
48687
  const tools = [];
47905
48688
  for (const entry of raw) {
47906
- if (!isRecord9(entry))
48689
+ if (!isRecord10(entry))
47907
48690
  throw new GatewayError("BAD_REQUEST", "tools: each tool must be an object");
47908
48691
  const type = typeof entry.type === "string" ? entry.type : "";
47909
48692
  const declared = typeof entry.name === "string" ? entry.name : "";
@@ -47916,7 +48699,7 @@ function readTools(raw) {
47916
48699
  kind: "portable",
47917
48700
  name: declared,
47918
48701
  ...description === undefined ? {} : { description },
47919
- inputSchema: type === "custom" ? { type: "object", properties: { input: { type: "string" } }, required: ["input"] } : isRecord9(entry.parameters) ? entry.parameters : { type: "object" }
48702
+ inputSchema: type === "custom" ? { type: "object", properties: { input: { type: "string" } }, required: ["input"] } : isRecord10(entry.parameters) ? entry.parameters : { type: "object" }
47920
48703
  });
47921
48704
  continue;
47922
48705
  }
@@ -47931,10 +48714,10 @@ function readTools(raw) {
47931
48714
  }
47932
48715
  function customToolNames(body) {
47933
48716
  const names = new Set;
47934
- if (!isRecord9(body) || !Array.isArray(body.tools))
48717
+ if (!isRecord10(body) || !Array.isArray(body.tools))
47935
48718
  return names;
47936
48719
  for (const entry of body.tools) {
47937
- if (isRecord9(entry) && entry.type === "custom" && typeof entry.name === "string") {
48720
+ if (isRecord10(entry) && entry.type === "custom" && typeof entry.name === "string") {
47938
48721
  names.add(entry.name);
47939
48722
  }
47940
48723
  }
@@ -48143,8 +48926,8 @@ async function handle(deps, rateLimiter, surface, request) {
48143
48926
  let streaming = false;
48144
48927
  let headroom = {};
48145
48928
  const limitHeaders = () => rateLimitHeaders(surface, release === null ? headroom : afterThisRequest(headroom), startedAt);
48146
- const debit = (id, usage) => {
48147
- rateLimiter.debit(id, usage);
48929
+ const debit = (id, model, usage) => {
48930
+ rateLimiter.debit(id, model, usage);
48148
48931
  };
48149
48932
  try {
48150
48933
  let key;
@@ -48161,24 +48944,6 @@ async function handle(deps, rateLimiter, surface, request) {
48161
48944
  throw error;
48162
48945
  }
48163
48946
  keyId = key.id;
48164
- try {
48165
- const admitted = await rateLimiter.admit(key.id, key.limits, requestId);
48166
- release = admitted.release;
48167
- headroom = admitted.headroom;
48168
- } catch (error) {
48169
- if (error instanceof RateLimitExceeded)
48170
- headroom = error.headroom;
48171
- if (error instanceof GatewayError && error.code === "RATE_LIMIT") {
48172
- deps.logger.warn("rate limit rejected", {
48173
- requestId,
48174
- surface,
48175
- apiKeyId: key.id,
48176
- code: error.code,
48177
- retryAfterMs: error.retryAfterMs
48178
- });
48179
- }
48180
- throw error;
48181
- }
48182
48947
  const captured = await bodyCollectorFor(deps, key);
48183
48948
  collector = captured;
48184
48949
  if (captured !== null) {
@@ -48209,6 +48974,25 @@ async function handle(deps, rateLimiter, surface, request) {
48209
48974
  throw new GatewayError("AUTH", `model "${safeToken(chatRequest.model)}" is not allowed for this API key`);
48210
48975
  }
48211
48976
  requestedModel = chatRequest.model;
48977
+ try {
48978
+ const admitted = await rateLimiter.admit(key.id, key.limits, requestedModel, requestId);
48979
+ release = admitted.release;
48980
+ headroom = admitted.headroom;
48981
+ } catch (error) {
48982
+ if (error instanceof RateLimitExceeded)
48983
+ headroom = error.headroom;
48984
+ if (error instanceof GatewayError && error.code === "RATE_LIMIT") {
48985
+ deps.logger.warn("rate limit rejected", {
48986
+ requestId,
48987
+ surface,
48988
+ apiKeyId: key.id,
48989
+ model: requestedModel,
48990
+ code: error.code,
48991
+ retryAfterMs: error.retryAfterMs
48992
+ });
48993
+ }
48994
+ throw error;
48995
+ }
48212
48996
  let began = false;
48213
48997
  const dispatched = await dispatch(chatRequest, {
48214
48998
  ...deps,
@@ -48344,12 +49128,12 @@ function proxyRoutes(deps) {
48344
49128
  }).post("/v1/messages/count_tokens", async ({ request }) => {
48345
49129
  try {
48346
49130
  const key = await authenticateApiKey(deps.store, apiKeyHeader(request.headers), deps.now(), logger);
48347
- await rateLimiter.consume(key.id, key.limits, deps.requestId());
48348
49131
  const body = await request.json();
48349
49132
  const chatRequest = parseAnthropicRequest(body, request.headers);
48350
49133
  if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
48351
49134
  throw new GatewayError("AUTH", `model "${safeToken(chatRequest.model)}" is not allowed for this API key`);
48352
49135
  }
49136
+ await rateLimiter.consume(key.id, key.limits, chatRequest.model, deps.requestId());
48353
49137
  const { settings } = await dispatchDeps.snapshots.get(deps.now());
48354
49138
  const counted = injectPonytail(chatRequest, { mode: settings.ponytailMode }).request;
48355
49139
  return Response.json({ input_tokens: estimateInputTokens(counted) });
@@ -48570,11 +49354,11 @@ function topicClass(topic) {
48570
49354
  return null;
48571
49355
  }
48572
49356
  var MAX_TOPIC = 128;
48573
- function isRecord10(value) {
49357
+ function isRecord11(value) {
48574
49358
  return typeof value === "object" && value !== null && !Array.isArray(value);
48575
49359
  }
48576
49360
  function parseClientFrame(raw) {
48577
- if (!isRecord10(raw))
49361
+ if (!isRecord11(raw))
48578
49362
  return null;
48579
49363
  const { id, type, topic } = raw;
48580
49364
  if (id !== undefined && typeof id !== "string")
@@ -49258,7 +50042,7 @@ function createRing(limits = DEFAULT_RING_LIMITS) {
49258
50042
  }
49259
50043
 
49260
50044
  // apps/gateway/src/version.ts
49261
- var VERSION = "0.13.7";
50045
+ var VERSION = "0.14.1";
49262
50046
 
49263
50047
  // apps/gateway/src/app.ts
49264
50048
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;