omnigateway 0.13.6 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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",
@@ -24716,6 +25064,7 @@ var KNOWN_EVENTS4 = new Set([
24716
25064
  "response.reasoning_text.done",
24717
25065
  "response.function_call_arguments.delta",
24718
25066
  "response.function_call_arguments.done",
25067
+ "response.metadata",
24719
25068
  "response.completed",
24720
25069
  "response.incomplete",
24721
25070
  "response.failed",
@@ -24901,8 +25250,170 @@ async function* decodeResponses(messages, options = {}) {
24901
25250
  }
24902
25251
  }
24903
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
+
24904
25416
  // packages/providers/src/openai/wire.ts
24905
- import { createHash as createHash6 } from "crypto";
24906
25417
  var MAX_CACHE_KEY_LENGTH = 1024;
24907
25418
  var HEADER_SAFE_KEY = /^[\x21-\x7e]+$/;
24908
25419
  function suppliedKey2(req) {
@@ -24914,9 +25425,16 @@ function suppliedKey2(req) {
24914
25425
  }
24915
25426
  return;
24916
25427
  }
24917
- function cacheKey2(req, instructions, firstInput) {
24918
- const stable = req.conversationId !== undefined && req.conversationId.length > 0 ? JSON.stringify({ conversation: req.conversationId }) : JSON.stringify({ instructions, firstInput: firstInput ?? null });
24919
- 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 };
24920
25438
  }
24921
25439
  function repairOrphanedCalls2(input) {
24922
25440
  const answered = new Set;
@@ -24947,6 +25465,44 @@ function encodeToolChoice7(c) {
24947
25465
  return { type: "function", name: c.name };
24948
25466
  }
24949
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
+ }
24950
25506
  function toResponsesWire(req, model, opts = { oauth: false }) {
24951
25507
  const degradations = [];
24952
25508
  const input = [];
@@ -24954,6 +25510,12 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24954
25510
  if (!degradations.includes(d))
24955
25511
  degradations.push(d);
24956
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
+ };
24957
25519
  if (req.betas?.includes(CONTEXT_1M_BETA))
24958
25520
  note("openai:context-1m-dropped");
24959
25521
  for (const message of req.messages) {
@@ -24965,10 +25527,15 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24965
25527
  const flush = () => {
24966
25528
  if (parts.length === 0)
24967
25529
  return;
24968
- input.push({ type: "message", role, content: [...parts] });
25530
+ push({ type: "message", role, content: [...parts] });
24969
25531
  parts.length = 0;
24970
25532
  };
25533
+ let opened = false;
24971
25534
  for (const block of message.content) {
25535
+ if (message.role === "user" && !opened && block.type !== "toolResult") {
25536
+ opened = true;
25537
+ turn++;
25538
+ }
24972
25539
  switch (block.type) {
24973
25540
  case "text":
24974
25541
  parts.push({
@@ -24989,7 +25556,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
24989
25556
  break;
24990
25557
  case "toolUse":
24991
25558
  flush();
24992
- input.push({
25559
+ push({
24993
25560
  type: "function_call",
24994
25561
  call_id: block.id,
24995
25562
  name: block.name,
@@ -25001,7 +25568,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25001
25568
  note("openai:anthropic-native-block-dropped");
25002
25569
  flush();
25003
25570
  const files = responsesFiles(block.files ?? []);
25004
- input.push({
25571
+ push({
25005
25572
  type: "function_call_output",
25006
25573
  call_id: block.toolUseId,
25007
25574
  output: withFileText(block.content, files.text)
@@ -25029,7 +25596,7 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25029
25596
  if (block.provider === "openai") {
25030
25597
  flush();
25031
25598
  const { id: _id, ...rest } = block.data;
25032
- input.push({ type: block.blockType, ...rest });
25599
+ push({ type: block.blockType, ...rest });
25033
25600
  break;
25034
25601
  }
25035
25602
  note("openai:anthropic-native-block-dropped");
@@ -25041,8 +25608,10 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25041
25608
  repairOrphanedCalls2(input);
25042
25609
  const body = { model, input, stream: req.stream, store: false };
25043
25610
  const instructions = systemText(req.system, "openai", note);
25044
- if (instructions !== undefined && instructions.length > 0)
25611
+ const lite = opts.oauth && codexModel(model).lite;
25612
+ if (instructions !== undefined && instructions.length > 0 && !opts.oauth) {
25045
25613
  body.instructions = instructions;
25614
+ }
25046
25615
  if (req.maxTokens !== undefined) {
25047
25616
  if (opts.oauth)
25048
25617
  note("openai:max-tokens-dropped");
@@ -25055,22 +25624,45 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25055
25624
  else
25056
25625
  body.temperature = req.temperature;
25057
25626
  }
25627
+ let liteTools;
25058
25628
  if (req.tools !== undefined) {
25059
25629
  const custom = req.tools.filter((t) => t.kind === "portable");
25060
25630
  if (custom.length !== req.tools.length)
25061
25631
  note("openai:anthropic-tool-dropped");
25062
- body.tools = custom.map((t) => ({
25063
- type: "function",
25064
- name: t.name,
25065
- description: t.description,
25066
- parameters: t.inputSchema
25067
- }));
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
+ }
25068
25652
  }
25069
25653
  if (req.toolChoice !== undefined)
25070
25654
  body.tool_choice = encodeToolChoice7(req.toolChoice);
25071
- if (req.reasoning?.mode === "betweenTools")
25072
- degradations.push("openai:reasoning-between-tools-as-off");
25073
- 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") {
25074
25666
  if (req.reasoning.mode === "budget") {
25075
25667
  degradations.push("openai:reasoning-budget-dropped");
25076
25668
  } else {
@@ -25081,36 +25673,98 @@ function toResponsesWire(req, model, opts = { oauth: false }) {
25081
25673
  };
25082
25674
  }
25083
25675
  }
25084
- const key = suppliedKey2(req) ?? cacheKey2(req, instructions ?? "", input[0]);
25085
- 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
+ }
25086
25685
  const moved = moveToResponsesText(req, body, "openai");
25087
25686
  if (moved !== undefined)
25088
25687
  note(moved);
25089
25688
  body.prompt_cache_key = key;
25090
- 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 };
25091
25715
  }
25092
25716
 
25093
25717
  // packages/providers/src/openai/codec.ts
25094
25718
  var OAUTH_URL2 = "https://chatgpt.com/backend-api/codex/responses";
25095
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
+ }
25096
25729
  var openaiCodec = {
25097
25730
  buildRequest(input) {
25098
25731
  const oauth = input.credentials.accessToken !== null;
25099
- const { body, degradations, cacheKey } = toResponsesWire(input.request, input.model, { oauth });
25100
- 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"]);
25101
25756
  if (oauth) {
25102
25757
  protocol.push(["authorization", `Bearer ${input.credentials.accessToken}`]);
25103
- const accountId = input.credentials.providerData.accountId;
25104
25758
  if (typeof accountId === "string")
25105
25759
  protocol.push(["chatgpt-account-id", accountId]);
25760
+ protocol.push(["host", "chatgpt.com"]);
25106
25761
  } else if (input.credentials.apiKey !== null) {
25107
25762
  protocol.push(["authorization", `Bearer ${input.credentials.apiKey}`]);
25108
25763
  } else {
25109
25764
  throw input.fail("AUTH", "openai credential has no token");
25110
25765
  }
25111
- protocol.push(["x-client-request-id", cacheKey], ["session-id", cacheKey], ["thread-id", cacheKey]);
25112
25766
  const headers = orderHeaders(mergeHeaders(openaiProfile.headers, protocol), openaiProfile.order);
25113
- const include = body.include;
25767
+ const include = input.request.vendor?.openai?.include;
25114
25768
  const nativeReasoning = Array.isArray(include) && include.includes("reasoning.encrypted_content");
25115
25769
  return {
25116
25770
  decodeState: { nativeReasoning },
@@ -25118,7 +25772,8 @@ var openaiCodec = {
25118
25772
  url: oauth ? OAUTH_URL2 : API_URL3,
25119
25773
  method: "POST",
25120
25774
  headers,
25121
- body: JSON.stringify(orderFields({ ...body, stream: true }, openaiBodyOrder))
25775
+ body: JSON.stringify(orderFields({ ...body, stream: true }, openaiBodyOrder)),
25776
+ ...oauth ? { bodyEncoding: "zstd" } : {}
25122
25777
  },
25123
25778
  degradations
25124
25779
  };
@@ -25217,11 +25872,17 @@ function createConnectFlows(deps) {
25217
25872
  const provider = deps.providers[flow.provider];
25218
25873
  if (provider === undefined)
25219
25874
  throw new GatewayError("BAD_REQUEST", "provider does not support OAuth");
25220
- 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 });
25221
25885
  if (flow.credentialId !== undefined) {
25222
- const existing = await deps.store.credentials.get(flow.credentialId);
25223
- if (existing === null)
25224
- throw new GatewayError("BAD_REQUEST", "credential not found");
25225
25886
  await deps.store.credentials.updateSecrets(flow.credentialId, result.secrets, result.expiresAt);
25226
25887
  await deps.store.credentials.update(flow.credentialId, {
25227
25888
  enabled: true,
@@ -26552,9 +27213,20 @@ function configuredWindows(limits) {
26552
27213
  async function readLimitUsage(store, id, limits, now) {
26553
27214
  if (limits === null)
26554
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) {
26555
27226
  const sums = new Map;
26556
27227
  for (const window2 of configuredWindows(limits)) {
26557
- 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));
26558
27230
  }
26559
27231
  const readings = [];
26560
27232
  for (const dimension of ["requests", "tokens", "spend"]) {
@@ -26567,7 +27239,7 @@ async function readLimitUsage(store, id, limits, now) {
26567
27239
  continue;
26568
27240
  const sum = sums.get(window2);
26569
27241
  const used = sum === undefined ? null : dimension === "requests" ? sum.requests : dimension === "tokens" ? sum.tokens : sum.costUsd;
26570
- readings.push({ dimension, window: window2, limit, used });
27242
+ readings.push({ dimension, window: window2, limit, used, model });
26571
27243
  }
26572
27244
  }
26573
27245
  if (limits.concurrency !== undefined && limits.concurrency !== null) {
@@ -26575,7 +27247,8 @@ async function readLimitUsage(store, id, limits, now) {
26575
27247
  dimension: "concurrency",
26576
27248
  window: null,
26577
27249
  limit: limits.concurrency,
26578
- used: null
27250
+ used: null,
27251
+ model
26579
27252
  });
26580
27253
  }
26581
27254
  return readings;
@@ -27549,7 +28222,7 @@ function createRefresher(deps) {
27549
28222
  };
27550
28223
  }
27551
28224
  // packages/control/src/pluginProviders.ts
27552
- function isRecord8(value) {
28225
+ function isRecord9(value) {
27553
28226
  return typeof value === "object" && value !== null && !Array.isArray(value);
27554
28227
  }
27555
28228
  function isNumber(value) {
@@ -27560,7 +28233,7 @@ function checkAt(ok, id, path, expected) {
27560
28233
  throw new Error(`provider ${id} descriptor: ${path} must be ${expected}`);
27561
28234
  }
27562
28235
  function checkLimits(id, path, value) {
27563
- checkAt(isRecord8(value), id, path, "an object");
28236
+ checkAt(isRecord9(value), id, path, "an object");
27564
28237
  const limits = value;
27565
28238
  checkAt(isNumber(limits.contextWindow), id, `${path}.contextWindow`, "a finite number");
27566
28239
  checkAt(isNumber(limits.maxOutputTokens), id, `${path}.maxOutputTokens`, "a finite number");
@@ -27578,19 +28251,19 @@ function checkDescriptor(id, record) {
27578
28251
  }
27579
28252
  }
27580
28253
  const capabilities = record.capabilities;
27581
- checkAt(isRecord8(capabilities), id, "capabilities", "an object");
28254
+ checkAt(isRecord9(capabilities), id, "capabilities", "an object");
27582
28255
  for (const flag of ["tools", "images", "reasoning"]) {
27583
28256
  const value = capabilities[flag];
27584
28257
  checkAt(typeof value === "boolean", id, `capabilities.${flag}`, "a boolean");
27585
28258
  }
27586
28259
  const write = record.writeOverInput;
27587
- checkAt(isRecord8(write), id, "writeOverInput", "an object");
28260
+ checkAt(isRecord9(write), id, "writeOverInput", "an object");
27588
28261
  for (const ttl of ["fiveMinute", "oneHour"]) {
27589
28262
  const value = write[ttl];
27590
28263
  checkAt(isNumber(value), id, `writeOverInput.${ttl}`, "a finite number");
27591
28264
  }
27592
28265
  const catalog = record.catalog;
27593
- checkAt(isRecord8(catalog), id, "catalog", "an object");
28266
+ checkAt(isRecord9(catalog), id, "catalog", "an object");
27594
28267
  const entry = catalog;
27595
28268
  checkAt(typeof entry.defaultModel === "string", id, "catalog.defaultModel", "a string");
27596
28269
  checkAt(Array.isArray(entry.authTypes), id, "catalog.authTypes", "an array");
@@ -27601,11 +28274,11 @@ function checkDescriptor(id, record) {
27601
28274
  checkAt(Array.isArray(entry.models), id, "catalog.models", "an array");
27602
28275
  for (const [index, model] of entry.models.entries()) {
27603
28276
  const at = `catalog.models[${index}]`;
27604
- checkAt(isRecord8(model), id, at, "an object");
28277
+ checkAt(isRecord9(model), id, at, "an object");
27605
28278
  const choice = model;
27606
28279
  checkAt(typeof choice.id === "string", id, `${at}.id`, "a string");
27607
28280
  checkAt(typeof choice.label === "string", id, `${at}.label`, "a string");
27608
- checkAt(isRecord8(choice.pricing), id, `${at}.pricing`, "an object");
28281
+ checkAt(isRecord9(choice.pricing), id, `${at}.pricing`, "an object");
27609
28282
  const pricing = choice.pricing;
27610
28283
  for (const field of ["input", "output", "cacheRead", "cacheWrite5m", "cacheWrite1h"]) {
27611
28284
  checkAt(isNumber(pricing[field]), id, `${at}.pricing.${field}`, "a finite number");
@@ -27621,13 +28294,13 @@ function checkDescriptor(id, record) {
27621
28294
  checkAt(typeof prefix === "string", id, `modelPrefixes[${index}]`, "a string");
27622
28295
  }
27623
28296
  const presentation = record.presentation;
27624
- checkAt(isRecord8(presentation), id, "presentation", "an object");
28297
+ checkAt(isRecord9(presentation), id, "presentation", "an object");
27625
28298
  const shown = presentation;
27626
28299
  for (const field of ["label", "tone"]) {
27627
28300
  checkAt(typeof shown[field] === "string", id, `presentation.${field}`, "a string");
27628
28301
  }
27629
28302
  checkAt(isNumber(shown.order), id, "presentation.order", "a finite number");
27630
- checkAt(isRecord8(shown.colour), id, "presentation.colour", "an object");
28303
+ checkAt(isRecord9(shown.colour), id, "presentation.colour", "an object");
27631
28304
  for (const scheme of ["light", "dark"]) {
27632
28305
  const value = shown.colour[scheme];
27633
28306
  checkAt(typeof value === "string", id, `presentation.colour.${scheme}`, "a string");
@@ -27639,7 +28312,7 @@ function checkDescriptor(id, record) {
27639
28312
  function validateOAuthFlow(id, value) {
27640
28313
  if (value === undefined)
27641
28314
  return;
27642
- if (!isRecord8(value))
28315
+ if (!isRecord9(value))
27643
28316
  throw new Error(`provider ${id} oauth must be an object`);
27644
28317
  const kind = value.kind;
27645
28318
  if (kind !== "pkce" && kind !== "device") {
@@ -28296,7 +28969,7 @@ function readFrom(path, offset) {
28296
28969
  }
28297
28970
  }
28298
28971
  // packages/plugin-api/src/events.ts
28299
- var WINDOW_MS2 = {
28972
+ var WINDOW_MS3 = {
28300
28973
  "1m": 60000,
28301
28974
  "5h": 5 * 60 * 60 * 1000,
28302
28975
  "1w": 7 * 24 * 60 * 60 * 1000
@@ -41647,8 +42320,26 @@ var SEED_LOCK_MS = 5000;
41647
42320
  var GAUGE_TTL_MS = 300000;
41648
42321
  var RENEW_INTERVAL_MS = 1e5;
41649
42322
  var MAX_RENEWED_MS = 86400000;
41650
- function bucketKey(keyId, window2) {
41651
- 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;
41652
42343
  }
41653
42344
 
41654
42345
  class RateLimitExceeded extends GatewayError {
@@ -41703,27 +42394,31 @@ class ApiKeyRateLimiter {
41703
42394
  this.coord = deps.coord ?? memoryCoord();
41704
42395
  this.onRejected = deps.onRejected;
41705
42396
  }
41706
- async admit(keyId, limits, requestId) {
41707
- if (!anyLimit(limits))
42397
+ async admit(keyId, limits, model, requestId) {
42398
+ const subjects = subjectsOf(keyId, limits, model);
42399
+ if (subjects.length === 0)
41708
42400
  return { release: () => {}, renew: async () => {}, headroom: {} };
41709
42401
  const now = this.now();
41710
- this.limits.set(keyId, limits);
41711
- const holder = configured(limits.concurrency) ? crypto.randomUUID() : undefined;
41712
- const claim = await this.claim(keyId, now, true, holder);
42402
+ const claims = [];
41713
42403
  let admitted = false;
41714
42404
  try {
41715
- const counters = await this.counters(keyId, limits, claim, now, requestId);
41716
- const decision = evaluate(limits, counters, now);
41717
- 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);
41718
42411
  admitted = true;
42412
+ const held = claims.flatMap((claim) => claim.holder === undefined ? [] : [{ subject: claim.subject, holder: claim.holder }]);
41719
42413
  let released = false;
41720
42414
  const renewUntil = now + MAX_RENEWED_MS;
41721
42415
  const renew = async () => {
41722
- if (released || holder === undefined)
42416
+ if (released)
41723
42417
  return;
41724
- 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);
41725
42420
  };
41726
- const renewal = holder === undefined ? null : setInterval(() => {
42421
+ const renewal = held.length === 0 ? null : setInterval(() => {
41727
42422
  if (this.now() >= renewUntil) {
41728
42423
  clearInterval(renewal ?? undefined);
41729
42424
  return;
@@ -41737,91 +42432,153 @@ class ApiKeyRateLimiter {
41737
42432
  released = true;
41738
42433
  if (renewal !== null)
41739
42434
  clearInterval(renewal);
41740
- this.coord.gauge.release(keyId, holder);
42435
+ for (const claim of claims)
42436
+ this.coord.gauge.release(claim.subject, claim.holder);
41741
42437
  };
41742
42438
  return { release, renew, headroom: decision.headroom };
41743
42439
  } finally {
41744
42440
  if (!admitted)
41745
- await this.rollback(keyId, claim);
42441
+ for (const claim of claims)
42442
+ await this.rollback(claim);
41746
42443
  }
41747
42444
  }
41748
- async consume(keyId, limits, requestId) {
41749
- const requests = limits.requests;
41750
- 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)
41751
42448
  return;
41752
42449
  const now = this.now();
41753
- this.limits.set(keyId, limits);
41754
- const claim = await this.claim(keyId, now, false);
42450
+ const claims = [];
41755
42451
  let consumed = false;
41756
42452
  try {
41757
- const counters = await this.counters(keyId, { requests }, claim, now, requestId);
41758
- 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);
41759
42458
  consumed = true;
41760
42459
  } finally {
41761
42460
  if (!consumed)
41762
- 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;
41763
42488
  }
42489
+ return {
42490
+ subject,
42491
+ stamp: ring.stamp,
42492
+ gauge,
42493
+ holder,
42494
+ before: { requests: ring.before, concurrency }
42495
+ };
41764
42496
  }
41765
- async claim(keyId, now, gauge, holder) {
41766
- const ring = await this.coord.window.claim(keyId, WINDOW_MS["1m"], now);
41767
- const concurrency = gauge ? await this.coord.gauge.acquire(keyId, GAUGE_TTL_MS, holder) : await this.coord.gauge.read(keyId);
41768
- return { stamp: ring.stamp, gauge, holder, before: { requests: ring.before, concurrency } };
41769
- }
41770
- async rollback(keyId, claim) {
41771
- await this.coord.window.rollback(keyId, claim.stamp);
42497
+ async rollback(claim) {
42498
+ await this.coord.window.rollback(claim.subject, claim.stamp);
41772
42499
  if (claim.gauge)
41773
- await this.coord.gauge.release(keyId, claim.holder);
42500
+ await this.coord.gauge.release(claim.subject, claim.holder);
41774
42501
  }
41775
- debit(keyId, usage) {
41776
- 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);
41777
42509
  if (limits === undefined || !anyLongLimit(limits))
41778
42510
  return;
41779
42511
  const now = this.now();
41780
- const delta = { requests: 1, tokens: usage.tokens, costUsd: usage.costUsd };
41781
42512
  for (const window2 of LONG_WINDOWS) {
41782
- 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);
41783
42514
  }
41784
42515
  }
41785
- inFlight(keyId) {
41786
- return this.coord.gauge.read(keyId);
42516
+ inFlight(subject) {
42517
+ return this.coord.gauge.read(subject);
41787
42518
  }
41788
- 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);
41789
42528
  const violation = decision.violation;
41790
42529
  if (violation === null)
41791
- return;
41792
- const window2 = violation.window;
41793
- const exact = await this.exactReset(keyId, window2, now, requestId);
41794
- const resolved = exact === null ? violation : { ...violation, resetAt: exact };
41795
- const headroom = exact === null || window2 === null ? decision.headroom : withReset(decision.headroom, window2, exact);
42530
+ throw new Error("rate limit refusal with no violation");
41796
42531
  try {
41797
42532
  this.onRejected?.(violation.dimension, violation.window);
41798
42533
  } catch {}
41799
- 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 };
41800
42556
  }
41801
- async exactReset(keyId, window2, now, requestId) {
42557
+ async exactReset(subject, window2, now, requestId) {
41802
42558
  if (window2 === null || window2 === "1m")
41803
42559
  return null;
41804
42560
  const length = WINDOW_MS[window2];
41805
42561
  try {
41806
- const oldest = await this.store.usage.oldestSince(keyId, now - length);
42562
+ const oldest = await this.store.usage.oldestSince(subject.keyId, now - length, subject.model);
41807
42563
  if (oldest === null)
41808
42564
  return null;
41809
42565
  return oldest + length;
41810
42566
  } catch (error) {
41811
42567
  this.logger.warn("rate limit reset unavailable", {
41812
42568
  ...requestId === undefined ? {} : { requestId },
41813
- apiKeyId: keyId,
42569
+ apiKeyId: subject.keyId,
42570
+ ...subject.model === undefined ? {} : { model: subject.model },
41814
42571
  reason: describeError(error, "unknown")
41815
42572
  });
41816
42573
  return null;
41817
42574
  }
41818
42575
  }
41819
- async counters(keyId, limits, claim, now, requestId) {
42576
+ async counters(subject, limits, claim, now, requestId) {
41820
42577
  const requests = { "1m": claim.before.requests };
41821
42578
  const snapshot = { requests, concurrency: claim.before.concurrency };
41822
42579
  if (!anyLongLimit(limits))
41823
42580
  return snapshot;
41824
- const counts = await this.longCounts(keyId, now, requestId);
42581
+ const counts = await this.longCounts(subject, now, requestId);
41825
42582
  if (counts === null)
41826
42583
  return snapshot;
41827
42584
  const tokens = {};
@@ -41837,20 +42594,21 @@ class ApiKeyRateLimiter {
41837
42594
  snapshot.spend = spend;
41838
42595
  return snapshot;
41839
42596
  }
41840
- async longCounts(keyId, now, requestId) {
42597
+ async longCounts(subject, now, requestId) {
41841
42598
  const out = {};
41842
42599
  for (const window2 of LONG_WINDOWS) {
41843
- const key = bucketKey(keyId, window2);
42600
+ const key = bucketKey(subject.id, window2);
41844
42601
  const grain = GRAIN_MS[window2];
41845
42602
  const length = WINDOW_MS[window2];
41846
42603
  let sum = await this.coord.buckets.sum(key, grain, length, now);
41847
42604
  if (sum === null) {
41848
42605
  try {
41849
- sum = await this.seed(keyId, window2, now);
42606
+ sum = await this.seed(subject, window2, now);
41850
42607
  } catch (error) {
41851
42608
  this.logger.warn("rate limit counters unavailable", {
41852
42609
  ...requestId === undefined ? {} : { requestId },
41853
- apiKeyId: keyId,
42610
+ apiKeyId: subject.keyId,
42611
+ ...subject.model === undefined ? {} : { model: subject.model },
41854
42612
  reason: describeError(error, "unknown")
41855
42613
  });
41856
42614
  return null;
@@ -41860,15 +42618,15 @@ class ApiKeyRateLimiter {
41860
42618
  }
41861
42619
  return out;
41862
42620
  }
41863
- async seed(keyId, window2, now) {
41864
- const key = bucketKey(keyId, window2);
42621
+ async seed(subject, window2, now) {
42622
+ const key = bucketKey(subject.id, window2);
41865
42623
  const grain = GRAIN_MS[window2];
41866
42624
  const length = WINDOW_MS[window2];
41867
42625
  const install = async () => {
41868
42626
  const already = await this.coord.buckets.sum(key, grain, length, now);
41869
42627
  if (already !== null)
41870
42628
  return already;
41871
- 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);
41872
42630
  await this.coord.buckets.seed(key, grain, length, now, rows);
41873
42631
  return await this.coord.buckets.sum(key, grain, length, now) ?? zero();
41874
42632
  };
@@ -41884,6 +42642,9 @@ class ApiKeyRateLimiter {
41884
42642
  function zero() {
41885
42643
  return { requests: 0, tokens: 0, costUsd: 0 };
41886
42644
  }
42645
+ function requestsOnly(limits) {
42646
+ return limits.requests === undefined ? {} : { requests: limits.requests };
42647
+ }
41887
42648
  function withReset(headroom, window2, resetAt) {
41888
42649
  const corrected = { ...headroom };
41889
42650
  for (const dimension of ["requests", "tokens", "spend"]) {
@@ -41893,6 +42654,27 @@ function withReset(headroom, window2, resetAt) {
41893
42654
  }
41894
42655
  return corrected;
41895
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
+ }
41896
42678
 
41897
42679
  // apps/gateway/src/dispatch/loadRegistry.ts
41898
42680
  var PREFIX6 = "load:";
@@ -42810,7 +43592,7 @@ function zodDetail(issue) {
42810
43592
  }
42811
43593
  return issue.message;
42812
43594
  }
42813
- function isRecord9(value) {
43595
+ function isRecord10(value) {
42814
43596
  return typeof value === "object" && value !== null;
42815
43597
  }
42816
43598
  function extraFields(body, known) {
@@ -42953,7 +43735,7 @@ async function readJson(request) {
42953
43735
  }
42954
43736
  async function readJsonRecord(request) {
42955
43737
  const body = await readJson(request);
42956
- return isRecord9(body) ? body : null;
43738
+ return isRecord10(body) ? body : null;
42957
43739
  }
42958
43740
  function apiErrorResponse(error) {
42959
43741
  const gatewayError = error instanceof GatewayError ? error : new GatewayError("INTERNAL", "internal error");
@@ -46040,7 +46822,7 @@ async function finishLog(store, log, keyId, logger = noopLogger, bodies, debit,
46040
46822
  report(logger, "failed to persist request log", log.id, error);
46041
46823
  }
46042
46824
  if (keyId !== null && debit !== undefined) {
46043
- debit(keyId, {
46825
+ debit(keyId, log.requestedModel, {
46044
46826
  tokens: log.inputTokens + log.outputTokens + log.cacheReadTokens + log.cacheWriteTokens,
46045
46827
  costUsd: log.costUsd
46046
46828
  });
@@ -46209,6 +46991,7 @@ async function attempt(opts) {
46209
46991
  signal,
46210
46992
  ...opts.requestId === undefined ? {} : { requestId: opts.requestId },
46211
46993
  ...opts.autoCache === undefined ? {} : { autoCache: opts.autoCache },
46994
+ ...opts.requestStartedAt === undefined ? {} : { requestStartedAt: opts.requestStartedAt },
46212
46995
  ...candidate.target.maxOutputTokens === undefined ? {} : { maxOutputTokens: candidate.target.maxOutputTokens }
46213
46996
  });
46214
46997
  }
@@ -46528,6 +47311,7 @@ async function dispatch(request, deps, signal, requestId) {
46528
47311
  ...retrySecrets === undefined ? {} : { secrets: retrySecrets },
46529
47312
  logger,
46530
47313
  requestId,
47314
+ requestStartedAt: startedAt,
46531
47315
  autoCache: snapshot.settings.autoCacheEnabled
46532
47316
  }), dispatchSignal);
46533
47317
  noteDegradations(result.degradations);
@@ -47373,10 +48157,10 @@ function toIrToolChoice(c) {
47373
48157
  }
47374
48158
  var EFFORTS = REASONING_EFFORTS;
47375
48159
  function readEffort(body) {
47376
- if (!isRecord9(body))
48160
+ if (!isRecord10(body))
47377
48161
  return;
47378
48162
  const outputConfig = body.output_config;
47379
- if (!isRecord9(outputConfig))
48163
+ if (!isRecord10(outputConfig))
47380
48164
  return;
47381
48165
  const effort = outputConfig.effort;
47382
48166
  return EFFORTS.find((level) => level === effort);
@@ -47758,7 +48542,7 @@ function roleOf(raw) {
47758
48542
  return "user";
47759
48543
  }
47760
48544
  function contentPart(part) {
47761
- if (!isRecord9(part))
48545
+ if (!isRecord10(part))
47762
48546
  throw new GatewayError("BAD_REQUEST", "input: content part must be an object");
47763
48547
  const type = part.type;
47764
48548
  if (type === "input_text" || type === "output_text") {
@@ -47782,11 +48566,11 @@ function readToolOutput(output) {
47782
48566
  const images = [];
47783
48567
  const files = [];
47784
48568
  for (const part of output) {
47785
- if (isRecord9(part) && (part.type === "input_text" || part.type === "output_text")) {
48569
+ if (isRecord10(part) && (part.type === "input_text" || part.type === "output_text")) {
47786
48570
  text.push(String(part.text ?? ""));
47787
- } 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:")) {
47788
48572
  images.push({ type: "image", ...parseDataUrl(part.image_url) });
47789
- } 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") {
47790
48574
  files.push({
47791
48575
  type: "file",
47792
48576
  ...fileData(part.file_data),
@@ -47837,7 +48621,7 @@ function readInputItems(items) {
47837
48621
  messages.push({ role, content });
47838
48622
  };
47839
48623
  for (const raw of items) {
47840
- if (!isRecord9(raw))
48624
+ if (!isRecord10(raw))
47841
48625
  throw new GatewayError("BAD_REQUEST", "input: item must be an object");
47842
48626
  const type = raw.type ?? (raw.role === undefined ? undefined : "message");
47843
48627
  switch (type) {
@@ -47893,7 +48677,7 @@ function jsonArguments(raw, callId) {
47893
48677
  const text = typeof raw === "string" ? raw : "";
47894
48678
  try {
47895
48679
  const parsed = text === "" ? {} : JSON.parse(text);
47896
- return isRecord9(parsed) ? parsed : { value: parsed };
48680
+ return isRecord10(parsed) ? parsed : { value: parsed };
47897
48681
  } catch {
47898
48682
  throw new GatewayError("BAD_REQUEST", `input: arguments for call ${safeToken(callId)} are not valid JSON`);
47899
48683
  }
@@ -47902,7 +48686,7 @@ var TOOL_NAME = /^[A-Za-z0-9_-]{1,128}$/;
47902
48686
  function readTools(raw) {
47903
48687
  const tools = [];
47904
48688
  for (const entry of raw) {
47905
- if (!isRecord9(entry))
48689
+ if (!isRecord10(entry))
47906
48690
  throw new GatewayError("BAD_REQUEST", "tools: each tool must be an object");
47907
48691
  const type = typeof entry.type === "string" ? entry.type : "";
47908
48692
  const declared = typeof entry.name === "string" ? entry.name : "";
@@ -47915,7 +48699,7 @@ function readTools(raw) {
47915
48699
  kind: "portable",
47916
48700
  name: declared,
47917
48701
  ...description === undefined ? {} : { description },
47918
- 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" }
47919
48703
  });
47920
48704
  continue;
47921
48705
  }
@@ -47930,10 +48714,10 @@ function readTools(raw) {
47930
48714
  }
47931
48715
  function customToolNames(body) {
47932
48716
  const names = new Set;
47933
- if (!isRecord9(body) || !Array.isArray(body.tools))
48717
+ if (!isRecord10(body) || !Array.isArray(body.tools))
47934
48718
  return names;
47935
48719
  for (const entry of body.tools) {
47936
- if (isRecord9(entry) && entry.type === "custom" && typeof entry.name === "string") {
48720
+ if (isRecord10(entry) && entry.type === "custom" && typeof entry.name === "string") {
47937
48721
  names.add(entry.name);
47938
48722
  }
47939
48723
  }
@@ -48142,8 +48926,8 @@ async function handle(deps, rateLimiter, surface, request) {
48142
48926
  let streaming = false;
48143
48927
  let headroom = {};
48144
48928
  const limitHeaders = () => rateLimitHeaders(surface, release === null ? headroom : afterThisRequest(headroom), startedAt);
48145
- const debit = (id, usage) => {
48146
- rateLimiter.debit(id, usage);
48929
+ const debit = (id, model, usage) => {
48930
+ rateLimiter.debit(id, model, usage);
48147
48931
  };
48148
48932
  try {
48149
48933
  let key;
@@ -48160,24 +48944,6 @@ async function handle(deps, rateLimiter, surface, request) {
48160
48944
  throw error;
48161
48945
  }
48162
48946
  keyId = key.id;
48163
- try {
48164
- const admitted = await rateLimiter.admit(key.id, key.limits, requestId);
48165
- release = admitted.release;
48166
- headroom = admitted.headroom;
48167
- } catch (error) {
48168
- if (error instanceof RateLimitExceeded)
48169
- headroom = error.headroom;
48170
- if (error instanceof GatewayError && error.code === "RATE_LIMIT") {
48171
- deps.logger.warn("rate limit rejected", {
48172
- requestId,
48173
- surface,
48174
- apiKeyId: key.id,
48175
- code: error.code,
48176
- retryAfterMs: error.retryAfterMs
48177
- });
48178
- }
48179
- throw error;
48180
- }
48181
48947
  const captured = await bodyCollectorFor(deps, key);
48182
48948
  collector = captured;
48183
48949
  if (captured !== null) {
@@ -48208,6 +48974,25 @@ async function handle(deps, rateLimiter, surface, request) {
48208
48974
  throw new GatewayError("AUTH", `model "${safeToken(chatRequest.model)}" is not allowed for this API key`);
48209
48975
  }
48210
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
+ }
48211
48996
  let began = false;
48212
48997
  const dispatched = await dispatch(chatRequest, {
48213
48998
  ...deps,
@@ -48343,12 +49128,12 @@ function proxyRoutes(deps) {
48343
49128
  }).post("/v1/messages/count_tokens", async ({ request }) => {
48344
49129
  try {
48345
49130
  const key = await authenticateApiKey(deps.store, apiKeyHeader(request.headers), deps.now(), logger);
48346
- await rateLimiter.consume(key.id, key.limits, deps.requestId());
48347
49131
  const body = await request.json();
48348
49132
  const chatRequest = parseAnthropicRequest(body, request.headers);
48349
49133
  if (key.modelAllowlist !== null && !key.modelAllowlist.includes(chatRequest.model)) {
48350
49134
  throw new GatewayError("AUTH", `model "${safeToken(chatRequest.model)}" is not allowed for this API key`);
48351
49135
  }
49136
+ await rateLimiter.consume(key.id, key.limits, chatRequest.model, deps.requestId());
48352
49137
  const { settings } = await dispatchDeps.snapshots.get(deps.now());
48353
49138
  const counted = injectPonytail(chatRequest, { mode: settings.ponytailMode }).request;
48354
49139
  return Response.json({ input_tokens: estimateInputTokens(counted) });
@@ -48569,11 +49354,11 @@ function topicClass(topic) {
48569
49354
  return null;
48570
49355
  }
48571
49356
  var MAX_TOPIC = 128;
48572
- function isRecord10(value) {
49357
+ function isRecord11(value) {
48573
49358
  return typeof value === "object" && value !== null && !Array.isArray(value);
48574
49359
  }
48575
49360
  function parseClientFrame(raw) {
48576
- if (!isRecord10(raw))
49361
+ if (!isRecord11(raw))
48577
49362
  return null;
48578
49363
  const { id, type, topic } = raw;
48579
49364
  if (id !== undefined && typeof id !== "string")
@@ -49257,7 +50042,7 @@ function createRing(limits = DEFAULT_RING_LIMITS) {
49257
50042
  }
49258
50043
 
49259
50044
  // apps/gateway/src/version.ts
49260
- var VERSION = "0.13.6";
50045
+ var VERSION = "0.14.0";
49261
50046
 
49262
50047
  // apps/gateway/src/app.ts
49263
50048
  var ADMIN_SESSION_TTL_MS = 12 * 60 * 60 * 1000;