@chenpu17/cc-gw 0.7.16 → 0.7.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/package.json +1 -1
  2. package/src/server/dist/index.js +420 -159
  3. package/src/web/dist/assets/{About-B0Jdm3Q_.js → About-DiMtBAOL.js} +2 -2
  4. package/src/web/dist/assets/ApiKeys-DcKwRUMu.js +16 -0
  5. package/src/web/dist/assets/Dashboard-C6Uai54P.js +16 -0
  6. package/src/web/dist/assets/{Events-B5zZ5_PB.js → Events-CP1K4Di7.js} +2 -2
  7. package/src/web/dist/assets/{Help-75v5moMf.js → Help-D9w8Z7bi.js} +1 -1
  8. package/src/web/dist/assets/{Login-DwQ6YKZt.js → Login-LyOFmFDM.js} +1 -1
  9. package/src/web/dist/assets/Logs-qMkJ_LnZ.js +6 -0
  10. package/src/web/dist/assets/ModelManagement-JuXIL4nJ.js +6 -0
  11. package/src/web/dist/assets/{PageHeader-Q48thbsH.js → PageHeader-C2Un_Eq9.js} +1 -1
  12. package/src/web/dist/assets/{PageSection-DhdaIOwG.js → PageSection-B9yrPBCN.js} +1 -1
  13. package/src/web/dist/assets/Settings-yxuIaovZ.js +6 -0
  14. package/src/web/dist/assets/Skeleton-BLdvXQnT.js +1 -0
  15. package/src/web/dist/assets/{card-lw0mNbtE.js → card-CtgEZD37.js} +1 -1
  16. package/src/web/dist/assets/{dialog-_9cV4Ss_.js → dialog-DRP_IQxH.js} +3 -3
  17. package/src/web/dist/assets/index-B_ZdlwhD.js +280 -0
  18. package/src/web/dist/assets/{index-BZJu9-Md.js → index-D3x39AFW.js} +1 -1
  19. package/src/web/dist/assets/index-yEbjdbzX.css +1 -0
  20. package/src/web/dist/assets/{info-Ch3uesoo.js → info-B8rDg1M6.js} +1 -1
  21. package/src/web/dist/assets/{input-DuU4V_4o.js → input-BJXToHiR.js} +1 -1
  22. package/src/web/dist/assets/{label-CPyOyofK.js → label-CzVczRHt.js} +1 -1
  23. package/src/web/dist/assets/{refresh-cw-lIS96np_.js → refresh-cw-BvguPOQm.js} +1 -1
  24. package/src/web/dist/assets/select-B1I4kItt.js +11 -0
  25. package/src/web/dist/assets/shield-DceCQI6h.js +11 -0
  26. package/src/web/dist/assets/switch-DteEzH7T.js +1 -0
  27. package/src/web/dist/assets/{useApiQuery-BenuGU61.js → useApiQuery-DNDJWhHp.js} +1 -1
  28. package/src/web/dist/index.html +2 -2
  29. package/src/web/dist/assets/ApiKeys-BJZYNk8v.js +0 -16
  30. package/src/web/dist/assets/Dashboard-Yk4Gcz4p.js +0 -16
  31. package/src/web/dist/assets/Logs-8o1cRVE6.js +0 -6
  32. package/src/web/dist/assets/ModelManagement-C9OoP4WI.js +0 -1
  33. package/src/web/dist/assets/Settings-D7724_w1.js +0 -11
  34. package/src/web/dist/assets/copy-BSxv1vw0.js +0 -6
  35. package/src/web/dist/assets/index-CsJ3GRy4.css +0 -1
  36. package/src/web/dist/assets/index-CwxoiOJ7.js +0 -280
  37. package/src/web/dist/assets/select-Bas7KWYt.js +0 -11
  38. package/src/web/dist/assets/switch-C9znB46C.js +0 -1
@@ -10975,7 +10975,7 @@ function buildProviderBody(payload, options = {}) {
10975
10975
  const body = {
10976
10976
  messages: buildMessages(payload)
10977
10977
  };
10978
- if (options.maxTokens) {
10978
+ if (options.maxTokens != null) {
10979
10979
  if (payload.thinking) {
10980
10980
  body.max_completion_tokens = options.maxTokens;
10981
10981
  } else {
@@ -11094,7 +11094,7 @@ function buildAnthropicBody(payload, options = {}) {
11094
11094
  messages,
11095
11095
  stream: payload.stream
11096
11096
  };
11097
- if (options.maxTokens) {
11097
+ if (options.maxTokens != null) {
11098
11098
  body.max_tokens = options.maxTokens;
11099
11099
  }
11100
11100
  if (typeof options.temperature === "number") {
@@ -12826,6 +12826,7 @@ async function ensureSchema(db) {
12826
12826
  await maybeAddColumn(db, "api_keys", "request_count", "INTEGER DEFAULT 0");
12827
12827
  await maybeAddColumn(db, "api_keys", "total_input_tokens", "INTEGER DEFAULT 0");
12828
12828
  await maybeAddColumn(db, "api_keys", "total_output_tokens", "INTEGER DEFAULT 0");
12829
+ await maybeAddColumn(db, "api_keys", "allowed_endpoints", "TEXT DEFAULT NULL");
12829
12830
  await migrateDailyMetricsTable(db);
12830
12831
  await maybeAddColumn(db, "daily_metrics", "total_cached_tokens", "INTEGER DEFAULT 0");
12831
12832
  await maybeAddColumn(db, "daily_metrics", "total_cache_read_tokens", "INTEGER DEFAULT 0");
@@ -13345,6 +13346,33 @@ function maskKey(prefix, suffix) {
13345
13346
  const safeSuffix = suffix ?? "";
13346
13347
  return `${safePrefix}****${safeSuffix}`;
13347
13348
  }
13349
+ function parseEndpointList(value) {
13350
+ if (!Array.isArray(value)) {
13351
+ return { ok: false };
13352
+ }
13353
+ const normalized = [];
13354
+ for (const raw of value) {
13355
+ if (typeof raw !== "string") {
13356
+ return { ok: false };
13357
+ }
13358
+ const endpointId = raw.trim();
13359
+ if (!endpointId) {
13360
+ return { ok: false };
13361
+ }
13362
+ normalized.push(endpointId);
13363
+ }
13364
+ return { ok: true, value: [...new Set(normalized)] };
13365
+ }
13366
+ function normalizeAllowedEndpointsForStorage(value) {
13367
+ if (value == null) {
13368
+ return null;
13369
+ }
13370
+ const parsed = parseEndpointList(value);
13371
+ if (!parsed.ok) {
13372
+ throw new Error("allowedEndpoints must be an array of strings or null");
13373
+ }
13374
+ return parsed.value.length > 0 ? parsed.value : null;
13375
+ }
13348
13376
  async function recordAuditLog(payload) {
13349
13377
  const { apiKeyId = null, apiKeyName = null, operation, operator = null, details = null, ipAddress = null } = payload;
13350
13378
  const serializedDetails = details ? JSON.stringify(details) : null;
@@ -13364,22 +13392,36 @@ function generateKey() {
13364
13392
  };
13365
13393
  }
13366
13394
  async function listApiKeys() {
13367
- const rows = await getAll("SELECT id, name, description, key_prefix, key_suffix, is_wildcard, enabled, created_at, last_used_at, request_count, total_input_tokens, total_output_tokens FROM api_keys ORDER BY is_wildcard DESC, created_at DESC");
13368
- return rows.map((row) => ({
13369
- id: row.id,
13370
- name: row.name,
13371
- description: row.description ?? null,
13372
- maskedKey: row.is_wildcard ? null : maskKey(row.key_prefix, row.key_suffix),
13373
- isWildcard: Boolean(row.is_wildcard),
13374
- enabled: Boolean(row.enabled),
13375
- createdAt: toIsoOrNull(row.created_at),
13376
- lastUsedAt: toIsoOrNull(row.last_used_at),
13377
- requestCount: row.request_count ?? 0,
13378
- totalInputTokens: row.total_input_tokens ?? 0,
13379
- totalOutputTokens: row.total_output_tokens ?? 0
13380
- }));
13395
+ const rows = await getAll("SELECT id, name, description, key_prefix, key_suffix, is_wildcard, enabled, created_at, last_used_at, request_count, total_input_tokens, total_output_tokens, allowed_endpoints FROM api_keys ORDER BY is_wildcard DESC, created_at DESC");
13396
+ return rows.map((row) => {
13397
+ let allowedEndpoints = null;
13398
+ if (row.allowed_endpoints) {
13399
+ try {
13400
+ const parsed = JSON.parse(row.allowed_endpoints);
13401
+ const endpointList = parseEndpointList(parsed);
13402
+ if (endpointList.ok && endpointList.value.length > 0) {
13403
+ allowedEndpoints = endpointList.value;
13404
+ }
13405
+ } catch {
13406
+ }
13407
+ }
13408
+ return {
13409
+ id: row.id,
13410
+ name: row.name,
13411
+ description: row.description ?? null,
13412
+ maskedKey: row.is_wildcard ? null : maskKey(row.key_prefix, row.key_suffix),
13413
+ isWildcard: Boolean(row.is_wildcard),
13414
+ enabled: Boolean(row.enabled),
13415
+ createdAt: toIsoOrNull(row.created_at),
13416
+ lastUsedAt: toIsoOrNull(row.last_used_at),
13417
+ requestCount: row.request_count ?? 0,
13418
+ totalInputTokens: row.total_input_tokens ?? 0,
13419
+ totalOutputTokens: row.total_output_tokens ?? 0,
13420
+ allowedEndpoints
13421
+ };
13422
+ });
13381
13423
  }
13382
- async function createApiKey(name, description, context) {
13424
+ async function createApiKey(name, description, context, allowedEndpoints) {
13383
13425
  const trimmed = name.trim();
13384
13426
  if (!trimmed) {
13385
13427
  throw new Error("Name is required");
@@ -13396,9 +13438,11 @@ async function createApiKey(name, description, context) {
13396
13438
  const hashed = hashKey(key);
13397
13439
  const encrypted = encryptSecret(key);
13398
13440
  const now = Date.now();
13399
- const columns = ["name", "description", "key_hash", "key_ciphertext", "key_prefix", "key_suffix", "is_wildcard", "enabled", "created_at"];
13400
- const placeholders = ["?", "?", "?", "?", "?", "?", "?", "?", "?"];
13401
- const values = [trimmed, trimmedDescription || null, hashed, encrypted, prefix, suffix, 0, 1, now];
13441
+ const columns = ["name", "description", "key_hash", "key_ciphertext", "key_prefix", "key_suffix", "is_wildcard", "enabled", "created_at", "allowed_endpoints"];
13442
+ const placeholders = ["?", "?", "?", "?", "?", "?", "?", "?", "?", "?"];
13443
+ const normalizedEndpoints = normalizeAllowedEndpointsForStorage(allowedEndpoints);
13444
+ const serializedEndpoints = normalizedEndpoints ? JSON.stringify(normalizedEndpoints) : null;
13445
+ const values = [trimmed, trimmedDescription || null, hashed, encrypted, prefix, suffix, 0, 1, now, serializedEndpoints];
13402
13446
  if (apiKeysHasUpdatedAt) {
13403
13447
  columns.push("updated_at");
13404
13448
  placeholders.push("?");
@@ -13423,24 +13467,55 @@ async function createApiKey(name, description, context) {
13423
13467
  createdAt: new Date(now).toISOString()
13424
13468
  };
13425
13469
  }
13426
- async function setApiKeyEnabled(id, enabled, context) {
13470
+ async function updateApiKeySettings(id, updates, context) {
13427
13471
  await ensureApiKeysMetadataLoaded();
13428
13472
  const existing = await getOne("SELECT id, name, is_wildcard, enabled FROM api_keys WHERE id = ?", [id]);
13429
13473
  if (!existing) {
13430
13474
  throw new Error("API key not found");
13431
13475
  }
13476
+ const setClauses = [];
13477
+ const values = [];
13478
+ const logs = [];
13479
+ if (typeof updates.enabled === "boolean") {
13480
+ setClauses.push("enabled = ?");
13481
+ values.push(updates.enabled ? 1 : 0);
13482
+ logs.push({
13483
+ apiKeyId: existing.id,
13484
+ apiKeyName: existing.name,
13485
+ operation: updates.enabled ? "enable" : "disable",
13486
+ operator: context?.operator ?? null,
13487
+ ipAddress: context?.ipAddress ?? null
13488
+ });
13489
+ }
13490
+ if (updates.allowedEndpointsProvided) {
13491
+ if (existing.is_wildcard) {
13492
+ throw new Error("Cannot set endpoint restrictions on wildcard key");
13493
+ }
13494
+ const normalizedEndpoints = normalizeAllowedEndpointsForStorage(updates.allowedEndpoints);
13495
+ const serialized = normalizedEndpoints ? JSON.stringify(normalizedEndpoints) : null;
13496
+ setClauses.push("allowed_endpoints = ?");
13497
+ values.push(serialized);
13498
+ logs.push({
13499
+ apiKeyId: existing.id,
13500
+ apiKeyName: existing.name,
13501
+ operation: "update_endpoints",
13502
+ details: { allowedEndpoints: normalizedEndpoints },
13503
+ operator: context?.operator ?? null,
13504
+ ipAddress: context?.ipAddress ?? null
13505
+ });
13506
+ }
13507
+ if (setClauses.length === 0) {
13508
+ throw new Error("No updates provided");
13509
+ }
13432
13510
  if (apiKeysHasUpdatedAt) {
13433
- await runQuery("UPDATE api_keys SET enabled = ?, updated_at = ? WHERE id = ?", [enabled ? 1 : 0, Date.now(), id]);
13434
- } else {
13435
- await runQuery("UPDATE api_keys SET enabled = ? WHERE id = ?", [enabled ? 1 : 0, id]);
13511
+ setClauses.push("updated_at = ?");
13512
+ values.push(Date.now());
13513
+ }
13514
+ values.push(id);
13515
+ await runQuery(`UPDATE api_keys SET ${setClauses.join(", ")} WHERE id = ?`, values);
13516
+ for (const payload of logs) {
13517
+ await recordAuditLog(payload);
13436
13518
  }
13437
- await recordAuditLog({
13438
- apiKeyId: existing.id,
13439
- apiKeyName: existing.name,
13440
- operation: enabled ? "enable" : "disable",
13441
- operator: context?.operator ?? null,
13442
- ipAddress: context?.ipAddress ?? null
13443
- });
13444
13519
  }
13445
13520
  async function deleteApiKey(id, context) {
13446
13521
  const existing = await getOne("SELECT id, name, is_wildcard FROM api_keys WHERE id = ?", [id]);
@@ -13483,7 +13558,7 @@ async function resolveApiKey(providedRaw, context) {
13483
13558
  throw new ApiKeyError("API key is required", "missing");
13484
13559
  }
13485
13560
  const hashed = hashKey(provided);
13486
- const existing = await getOne("SELECT id, name, enabled, is_wildcard FROM api_keys WHERE key_hash = ?", [hashed]);
13561
+ const existing = await getOne("SELECT id, name, enabled, is_wildcard, allowed_endpoints FROM api_keys WHERE key_hash = ?", [hashed]);
13487
13562
  if (existing) {
13488
13563
  if (!existing.enabled) {
13489
13564
  await recordAuditLog({
@@ -13495,6 +13570,53 @@ async function resolveApiKey(providedRaw, context) {
13495
13570
  });
13496
13571
  throw new ApiKeyError("API key is disabled", "disabled");
13497
13572
  }
13573
+ if (context?.endpointId && !existing.is_wildcard && existing.allowed_endpoints) {
13574
+ let parsed;
13575
+ try {
13576
+ parsed = JSON.parse(existing.allowed_endpoints);
13577
+ } catch {
13578
+ await recordAuditLog({
13579
+ apiKeyId: existing.id,
13580
+ apiKeyName: existing.name,
13581
+ operation: "auth_failure",
13582
+ details: { reason: "endpoint_policy_invalid", endpoint: context.endpointId },
13583
+ ipAddress: context?.ipAddress ?? null
13584
+ });
13585
+ throw new ApiKeyError("API key endpoint policy is invalid", "forbidden");
13586
+ }
13587
+ if (!Array.isArray(parsed)) {
13588
+ await recordAuditLog({
13589
+ apiKeyId: existing.id,
13590
+ apiKeyName: existing.name,
13591
+ operation: "auth_failure",
13592
+ details: { reason: "endpoint_policy_invalid", endpoint: context.endpointId },
13593
+ ipAddress: context?.ipAddress ?? null
13594
+ });
13595
+ throw new ApiKeyError("API key endpoint policy is invalid", "forbidden");
13596
+ }
13597
+ const endpointList = parseEndpointList(parsed);
13598
+ if (!endpointList.ok) {
13599
+ await recordAuditLog({
13600
+ apiKeyId: existing.id,
13601
+ apiKeyName: existing.name,
13602
+ operation: "auth_failure",
13603
+ details: { reason: "endpoint_policy_invalid", endpoint: context.endpointId },
13604
+ ipAddress: context?.ipAddress ?? null
13605
+ });
13606
+ throw new ApiKeyError("API key endpoint policy is invalid", "forbidden");
13607
+ }
13608
+ const allowed = endpointList.value;
13609
+ if (allowed.length > 0 && !allowed.includes(context.endpointId)) {
13610
+ await recordAuditLog({
13611
+ apiKeyId: existing.id,
13612
+ apiKeyName: existing.name,
13613
+ operation: "auth_failure",
13614
+ details: { reason: "forbidden", endpoint: context.endpointId },
13615
+ ipAddress: context?.ipAddress ?? null
13616
+ });
13617
+ throw new ApiKeyError("API key is not authorized for this endpoint", "forbidden");
13618
+ }
13619
+ }
13498
13620
  return {
13499
13621
  id: existing.id,
13500
13622
  name: existing.name,
@@ -14209,21 +14331,190 @@ function resolveCachedTokens(usage) {
14209
14331
  if (!usage || typeof usage !== "object") {
14210
14332
  return result;
14211
14333
  }
14212
- if (typeof usage.cache_read_input_tokens === "number") {
14334
+ const hasAnthropicRead = Number.isFinite(usage.cache_read_input_tokens);
14335
+ const hasAnthropicCreation = Number.isFinite(usage.cache_creation_input_tokens);
14336
+ if (hasAnthropicRead) {
14213
14337
  result.read = usage.cache_read_input_tokens;
14214
14338
  }
14215
- if (typeof usage.cache_creation_input_tokens === "number") {
14339
+ if (hasAnthropicCreation) {
14216
14340
  result.creation = usage.cache_creation_input_tokens;
14217
14341
  }
14218
- if (typeof usage.cached_tokens === "number") {
14219
- result.read = usage.cached_tokens;
14220
- }
14221
- const promptDetails = usage.prompt_tokens_details;
14222
- if (promptDetails && typeof promptDetails.cached_tokens === "number") {
14223
- result.read = promptDetails.cached_tokens;
14342
+ if (!hasAnthropicRead) {
14343
+ const promptDetails = usage.prompt_tokens_details;
14344
+ const inputDetails = usage.input_tokens_details;
14345
+ if (promptDetails && Number.isFinite(promptDetails.cached_tokens)) {
14346
+ result.read = promptDetails.cached_tokens;
14347
+ } else if (inputDetails && Number.isFinite(inputDetails.cached_tokens)) {
14348
+ result.read = inputDetails.cached_tokens;
14349
+ } else if (typeof usage.cached_tokens === "number") {
14350
+ result.read = usage.cached_tokens;
14351
+ }
14224
14352
  }
14225
14353
  return result;
14226
14354
  }
14355
+ function extractTextForTokenEstimate(value) {
14356
+ if (value == null)
14357
+ return "";
14358
+ if (typeof value === "string")
14359
+ return value;
14360
+ if (Array.isArray(value)) {
14361
+ return value.map((item) => extractTextForTokenEstimate(item)).filter(Boolean).join("\n");
14362
+ }
14363
+ if (typeof value === "object") {
14364
+ const payload = value;
14365
+ if (typeof payload.text === "string")
14366
+ return payload.text;
14367
+ if (typeof payload.content === "string")
14368
+ return payload.content;
14369
+ if (Array.isArray(payload.content))
14370
+ return extractTextForTokenEstimate(payload.content);
14371
+ if (typeof payload.output_text === "string")
14372
+ return payload.output_text;
14373
+ if (typeof payload.value === "string")
14374
+ return payload.value;
14375
+ }
14376
+ return "";
14377
+ }
14378
+ function parseSSEToSummary(chunks, format, modelId) {
14379
+ const allChunks = chunks.join("");
14380
+ const lines = allChunks.split("\n");
14381
+ let accumulatedContent = "";
14382
+ let finishReason = null;
14383
+ const toolCalls = /* @__PURE__ */ new Map();
14384
+ let usagePrompt = null;
14385
+ let usageCompletion = null;
14386
+ let usageCached = null;
14387
+ for (const line of lines) {
14388
+ const trimmed = line.trim();
14389
+ if (!trimmed.startsWith("data:"))
14390
+ continue;
14391
+ const dataStr = trimmed.slice(5).trim();
14392
+ if (dataStr === "[DONE]")
14393
+ continue;
14394
+ try {
14395
+ const data = JSON.parse(dataStr);
14396
+ if (format === "chat") {
14397
+ const choice = data?.choices?.[0];
14398
+ if (choice) {
14399
+ const delta = choice.delta;
14400
+ if (delta?.content) {
14401
+ accumulatedContent += delta.content;
14402
+ }
14403
+ if (delta?.tool_calls) {
14404
+ for (const tc of delta.tool_calls) {
14405
+ const idx = tc.index ?? 0;
14406
+ const existing = toolCalls.get(idx);
14407
+ if (!existing && tc.id) {
14408
+ toolCalls.set(idx, {
14409
+ id: tc.id,
14410
+ type: tc.type || "function",
14411
+ name: tc.function?.name || "",
14412
+ arguments: tc.function?.arguments || ""
14413
+ });
14414
+ } else if (existing) {
14415
+ if (tc.function?.arguments) {
14416
+ existing.arguments += tc.function.arguments;
14417
+ }
14418
+ }
14419
+ }
14420
+ }
14421
+ if (choice.finish_reason) {
14422
+ finishReason = choice.finish_reason;
14423
+ }
14424
+ }
14425
+ if (data?.usage) {
14426
+ usagePrompt = data.usage.prompt_tokens ?? usagePrompt;
14427
+ usageCompletion = data.usage.completion_tokens ?? usageCompletion;
14428
+ const cached = resolveCachedTokens(data.usage);
14429
+ if (cached.read > 0 || cached.creation > 0) {
14430
+ usageCached = cached.read + cached.creation;
14431
+ }
14432
+ }
14433
+ } else {
14434
+ const eventType = data?.type;
14435
+ if (eventType === "response.output_text.delta") {
14436
+ const delta = data?.delta;
14437
+ if (typeof delta === "string") {
14438
+ accumulatedContent += delta;
14439
+ }
14440
+ } else if (eventType === "response.content_part.delta") {
14441
+ const delta = data?.delta?.text;
14442
+ if (typeof delta === "string") {
14443
+ accumulatedContent += delta;
14444
+ }
14445
+ } else if (eventType === "response.function_call_arguments.delta") {
14446
+ const itemId = data?.item_id;
14447
+ const delta = data?.delta;
14448
+ if (itemId && typeof delta === "string") {
14449
+ let found = false;
14450
+ for (const tc of toolCalls.values()) {
14451
+ if (tc.id === itemId) {
14452
+ tc.arguments += delta;
14453
+ found = true;
14454
+ break;
14455
+ }
14456
+ }
14457
+ if (!found) {
14458
+ toolCalls.set(toolCalls.size, {
14459
+ id: itemId,
14460
+ type: "function",
14461
+ name: "",
14462
+ arguments: delta
14463
+ });
14464
+ }
14465
+ }
14466
+ } else if (eventType === "response.output_item.added") {
14467
+ const item = data?.item;
14468
+ if (item?.type === "function_call" && item?.call_id) {
14469
+ toolCalls.set(toolCalls.size, {
14470
+ id: item.call_id,
14471
+ type: "function",
14472
+ name: item.name || "",
14473
+ arguments: ""
14474
+ });
14475
+ }
14476
+ } else if (eventType === "response.completed" || eventType === "response.done") {
14477
+ const response = data?.response;
14478
+ if (response?.status) {
14479
+ finishReason = response.status;
14480
+ }
14481
+ if (response?.usage) {
14482
+ usagePrompt = response.usage.input_tokens ?? response.usage.prompt_tokens ?? usagePrompt;
14483
+ usageCompletion = response.usage.output_tokens ?? response.usage.completion_tokens ?? usageCompletion;
14484
+ const cached = resolveCachedTokens(response.usage);
14485
+ if (cached.read > 0 || cached.creation > 0) {
14486
+ usageCached = cached.read + cached.creation;
14487
+ }
14488
+ }
14489
+ }
14490
+ }
14491
+ } catch {
14492
+ }
14493
+ }
14494
+ const summary = {
14495
+ content: accumulatedContent,
14496
+ model: modelId,
14497
+ finish_reason: finishReason
14498
+ };
14499
+ if (usagePrompt != null || usageCompletion != null || usageCached != null) {
14500
+ summary.usage = {
14501
+ prompt_tokens: usagePrompt,
14502
+ completion_tokens: usageCompletion,
14503
+ cached_tokens: usageCached
14504
+ };
14505
+ }
14506
+ if (toolCalls.size > 0) {
14507
+ summary.tool_calls = Array.from(toolCalls.values()).map((tc) => ({
14508
+ id: tc.id,
14509
+ type: tc.type,
14510
+ function: {
14511
+ name: tc.name,
14512
+ arguments: tc.arguments
14513
+ }
14514
+ }));
14515
+ }
14516
+ return summary;
14517
+ }
14227
14518
  var roundTwoDecimals = (value) => Math.round(value * 100) / 100;
14228
14519
  function cloneOriginalPayload(value) {
14229
14520
  const structuredCloneFn = globalThis.structuredClone;
@@ -14365,13 +14656,13 @@ async function registerModelsHandler(app, path5, endpointId) {
14365
14656
  }
14366
14657
  const providedApiKey = extractApiKeyFromRequest(request);
14367
14658
  try {
14368
- await resolveApiKey(providedApiKey, { ipAddress: request.ip });
14659
+ await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
14369
14660
  } catch (error) {
14370
14661
  if (error instanceof ApiKeyError) {
14371
- reply.code(401);
14662
+ reply.code(error.code === "forbidden" ? 403 : 401);
14372
14663
  return {
14373
14664
  error: {
14374
- code: "invalid_api_key",
14665
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
14375
14666
  message: error.message
14376
14667
  }
14377
14668
  };
@@ -14496,13 +14787,13 @@ async function handleAnthropicProtocol(request, reply, endpoint, endpointId, app
14496
14787
  const providedApiKey = extractApiKeyFromRequest(request);
14497
14788
  let apiKeyContext;
14498
14789
  try {
14499
- apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
14790
+ apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
14500
14791
  } catch (error) {
14501
14792
  if (error instanceof ApiKeyError) {
14502
- reply.code(401);
14793
+ reply.code(error.code === "forbidden" ? 403 : 401);
14503
14794
  return {
14504
14795
  error: {
14505
- code: "invalid_api_key",
14796
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
14506
14797
  message: error.message
14507
14798
  }
14508
14799
  };
@@ -15002,13 +15293,13 @@ async function handleAnthropicCountTokensProtocol(request, reply, endpoint, endp
15002
15293
  }
15003
15294
  const providedApiKey = extractApiKeyFromRequest(request);
15004
15295
  try {
15005
- await resolveApiKey(providedApiKey, { ipAddress: request.ip });
15296
+ await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
15006
15297
  } catch (error) {
15007
15298
  if (error instanceof ApiKeyError) {
15008
- reply.code(401);
15299
+ reply.code(error.code === "forbidden" ? 403 : 401);
15009
15300
  return {
15010
15301
  error: {
15011
- code: "invalid_api_key",
15302
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15012
15303
  message: error.message
15013
15304
  }
15014
15305
  };
@@ -15078,13 +15369,13 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15078
15369
  const providedApiKey = extractApiKeyFromRequest(request);
15079
15370
  let apiKeyContext;
15080
15371
  try {
15081
- apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
15372
+ apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
15082
15373
  } catch (error) {
15083
15374
  if (error instanceof ApiKeyError) {
15084
- reply.code(401);
15375
+ reply.code(error.code === "forbidden" ? 403 : 401);
15085
15376
  return {
15086
15377
  error: {
15087
- code: "invalid_api_key",
15378
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15088
15379
  message: error.message
15089
15380
  }
15090
15381
  };
@@ -15162,10 +15453,6 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15162
15453
  });
15163
15454
  } else {
15164
15455
  providerBody = { ...payload };
15165
- if (providerBody.max_output_tokens == null && typeof providerBody.max_tokens === "number") {
15166
- providerBody.max_output_tokens = providerBody.max_tokens;
15167
- }
15168
- delete providerBody.max_tokens;
15169
15456
  if (typeof providerBody.thinking === "boolean") {
15170
15457
  delete providerBody.thinking;
15171
15458
  }
@@ -15208,7 +15495,7 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15208
15495
  const json = await readProviderBodyJson(upstream.body);
15209
15496
  const usagePayload = json?.usage ?? null;
15210
15497
  const inputTokens2 = usagePayload?.prompt_tokens ?? usagePayload?.input_tokens ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
15211
- const outputTokens2 = usagePayload?.completion_tokens ?? usagePayload?.output_tokens ?? estimateTextTokens(json?.choices?.[0]?.message?.content ?? "", target.modelId);
15498
+ const outputTokens2 = usagePayload?.completion_tokens ?? usagePayload?.output_tokens ?? estimateTextTokens(extractTextForTokenEstimate(json?.choices?.[0]?.message?.content), target.modelId);
15212
15499
  const cached = resolveCachedTokens(usagePayload);
15213
15500
  const cachedTokens = cached.read + cached.creation;
15214
15501
  const latencyMs2 = Date.now() - requestStart;
@@ -15322,54 +15609,14 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15322
15609
  requests: 1,
15323
15610
  inputTokens,
15324
15611
  outputTokens,
15612
+ cachedTokens: usageCached ?? 0,
15613
+ cacheReadTokens: usageCacheRead,
15614
+ cacheCreationTokens: usageCacheCreation,
15325
15615
  latencyMs
15326
15616
  });
15327
15617
  if (storeResponsePayloads && capturedChunks) {
15328
15618
  try {
15329
- const allChunks = capturedChunks.join("");
15330
- let accumulatedText = "";
15331
- let finishReason = null;
15332
- const lines = allChunks.split("\n");
15333
- for (const line of lines) {
15334
- const trimmed = line.trim();
15335
- if (trimmed.startsWith("data:")) {
15336
- const dataStr = trimmed.slice(5).trim();
15337
- if (dataStr !== "[DONE]") {
15338
- try {
15339
- const parsed = JSON.parse(dataStr);
15340
- const choice = parsed?.choices?.[0];
15341
- if (choice) {
15342
- if (choice.delta?.content) {
15343
- accumulatedText += choice.delta.content;
15344
- }
15345
- if (choice.finish_reason) {
15346
- finishReason = choice.finish_reason;
15347
- }
15348
- }
15349
- } catch {
15350
- }
15351
- }
15352
- }
15353
- }
15354
- const responseSummary = {
15355
- id: `chatcmpl_${Date.now()}`,
15356
- object: "chat.completion",
15357
- model: target.modelId,
15358
- choices: [{
15359
- index: 0,
15360
- message: {
15361
- role: "assistant",
15362
- content: accumulatedText
15363
- },
15364
- finish_reason: finishReason ?? "stop"
15365
- }],
15366
- usage: {
15367
- prompt_tokens: inputTokens,
15368
- completion_tokens: outputTokens,
15369
- total_tokens: inputTokens + outputTokens,
15370
- cached_tokens: usageCached ?? 0
15371
- }
15372
- };
15619
+ const responseSummary = parseSSEToSummary(capturedChunks, "chat", target.modelId);
15373
15620
  await upsertLogPayload(logId, { response: JSON.stringify(responseSummary) });
15374
15621
  } catch {
15375
15622
  }
@@ -15442,13 +15689,13 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15442
15689
  const providedApiKey = extractApiKeyFromRequest(request);
15443
15690
  let apiKeyContext;
15444
15691
  try {
15445
- apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
15692
+ apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
15446
15693
  } catch (error) {
15447
15694
  if (error instanceof ApiKeyError) {
15448
- reply.code(401);
15695
+ reply.code(error.code === "forbidden" ? 403 : 401);
15449
15696
  return {
15450
15697
  error: {
15451
- code: "invalid_api_key",
15698
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15452
15699
  message: error.message
15453
15700
  }
15454
15701
  };
@@ -15572,8 +15819,10 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15572
15819
  const json = await readProviderBodyJson(upstream.body);
15573
15820
  const usagePayload = json?.usage ?? null;
15574
15821
  const inputTokens2 = usagePayload?.prompt_tokens ?? usagePayload?.input_tokens ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
15575
- const content = json?.response?.body?.content ?? json?.choices?.[0]?.message?.content ?? "";
15576
- const outputTokens2 = usagePayload?.completion_tokens ?? usagePayload?.output_tokens ?? estimateTextTokens(content, target.modelId);
15822
+ const outputTokens2 = usagePayload?.completion_tokens ?? usagePayload?.output_tokens ?? estimateTextTokens(
15823
+ extractTextForTokenEstimate(json?.response?.body?.content ?? json?.choices?.[0]?.message?.content),
15824
+ target.modelId
15825
+ );
15577
15826
  const cached = resolveCachedTokens(usagePayload);
15578
15827
  const cachedTokens = cached.read + cached.creation;
15579
15828
  const latencyMs2 = Date.now() - requestStart;
@@ -15645,7 +15894,7 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15645
15894
  if (dataStr !== "[DONE]") {
15646
15895
  try {
15647
15896
  const parsed = JSON.parse(dataStr);
15648
- const usage = parsed?.usage || null;
15897
+ const usage = parsed?.usage || parsed?.response?.usage || null;
15649
15898
  if (usage) {
15650
15899
  usagePrompt = usage.prompt_tokens ?? usage.input_tokens ?? usagePrompt;
15651
15900
  usageCompletion = usage.completion_tokens ?? usage.output_tokens ?? usageCompletion;
@@ -15687,54 +15936,14 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15687
15936
  requests: 1,
15688
15937
  inputTokens,
15689
15938
  outputTokens,
15939
+ cachedTokens: usageCached ?? 0,
15940
+ cacheReadTokens: usageCacheRead,
15941
+ cacheCreationTokens: usageCacheCreation,
15690
15942
  latencyMs
15691
15943
  });
15692
15944
  if (storeResponsePayloads && capturedChunks) {
15693
15945
  try {
15694
- const allChunks = capturedChunks.join("");
15695
- let accumulatedText = "";
15696
- let finishReason = null;
15697
- const lines = allChunks.split("\n");
15698
- for (const line of lines) {
15699
- const trimmed = line.trim();
15700
- if (trimmed.startsWith("data:")) {
15701
- const dataStr = trimmed.slice(5).trim();
15702
- if (dataStr !== "[DONE]") {
15703
- try {
15704
- const parsed = JSON.parse(dataStr);
15705
- const choice = parsed?.choices?.[0];
15706
- if (choice) {
15707
- if (choice.delta?.content) {
15708
- accumulatedText += choice.delta.content;
15709
- }
15710
- if (choice.finish_reason) {
15711
- finishReason = choice.finish_reason;
15712
- }
15713
- }
15714
- } catch {
15715
- }
15716
- }
15717
- }
15718
- }
15719
- const responseSummary = {
15720
- id: `chatcmpl_${Date.now()}`,
15721
- object: "chat.completion",
15722
- model: target.modelId,
15723
- choices: [{
15724
- index: 0,
15725
- message: {
15726
- role: "assistant",
15727
- content: accumulatedText
15728
- },
15729
- finish_reason: finishReason ?? "stop"
15730
- }],
15731
- usage: {
15732
- prompt_tokens: inputTokens,
15733
- completion_tokens: outputTokens,
15734
- total_tokens: inputTokens + outputTokens,
15735
- cached_tokens: usageCached ?? 0
15736
- }
15737
- };
15946
+ const responseSummary = parseSSEToSummary(capturedChunks, "responses", target.modelId);
15738
15947
  await upsertLogPayload(logId, { response: JSON.stringify(responseSummary) });
15739
15948
  } catch {
15740
15949
  }
@@ -15808,13 +16017,13 @@ async function registerOpenAiRoutes(app) {
15808
16017
  const handleModels = async (request, reply) => {
15809
16018
  const providedApiKey = extractApiKeyFromRequest2(request);
15810
16019
  try {
15811
- await resolveApiKey(providedApiKey, { ipAddress: request.ip });
16020
+ await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId: "openai" });
15812
16021
  } catch (error) {
15813
16022
  if (error instanceof ApiKeyError) {
15814
- reply.code(401);
16023
+ reply.code(error.code === "forbidden" ? 403 : 401);
15815
16024
  return {
15816
16025
  error: {
15817
- code: "invalid_api_key",
16026
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15818
16027
  message: error.message
15819
16028
  }
15820
16029
  };
@@ -17141,6 +17350,27 @@ async function registerAdminRoutes(app) {
17141
17350
  const endpoint = typeof query.endpoint === "string" && query.endpoint.length > 0 ? query.endpoint : void 0;
17142
17351
  return getApiKeyUsageMetrics(days, limit, endpoint);
17143
17352
  });
17353
+ const normalizeAllowedEndpoints = (value) => {
17354
+ if (value == null) {
17355
+ return { ok: true, value: null };
17356
+ }
17357
+ if (!Array.isArray(value)) {
17358
+ return { ok: false, error: "allowedEndpoints must be an array of strings or null" };
17359
+ }
17360
+ const normalized = [];
17361
+ for (const raw of value) {
17362
+ if (typeof raw !== "string") {
17363
+ return { ok: false, error: "allowedEndpoints must be an array of strings or null" };
17364
+ }
17365
+ const endpointId = raw.trim();
17366
+ if (!endpointId) {
17367
+ return { ok: false, error: "allowedEndpoints must not contain empty endpoint IDs" };
17368
+ }
17369
+ normalized.push(endpointId);
17370
+ }
17371
+ const deduped = [...new Set(normalized)];
17372
+ return { ok: true, value: deduped.length > 0 ? deduped : null };
17373
+ };
17144
17374
  app.get("/api/keys", async () => {
17145
17375
  return listApiKeys();
17146
17376
  });
@@ -17150,8 +17380,19 @@ async function registerAdminRoutes(app) {
17150
17380
  reply.code(400);
17151
17381
  return { error: "Name is required" };
17152
17382
  }
17383
+ const hasAllowedEndpoints = Object.prototype.hasOwnProperty.call(body, "allowedEndpoints");
17384
+ const normalizedAllowedEndpoints = hasAllowedEndpoints ? normalizeAllowedEndpoints(body.allowedEndpoints) : { ok: true, value: null };
17385
+ if (!normalizedAllowedEndpoints.ok) {
17386
+ reply.code(400);
17387
+ return { error: normalizedAllowedEndpoints.error };
17388
+ }
17153
17389
  try {
17154
- return await createApiKey(body.name, body.description, { ipAddress: request.ip });
17390
+ return await createApiKey(
17391
+ body.name,
17392
+ body.description,
17393
+ { ipAddress: request.ip },
17394
+ normalizedAllowedEndpoints.value
17395
+ );
17155
17396
  } catch (error) {
17156
17397
  reply.code(400);
17157
17398
  return { error: error instanceof Error ? error.message : "Failed to create API key" };
@@ -17163,13 +17404,33 @@ async function registerAdminRoutes(app) {
17163
17404
  reply.code(400);
17164
17405
  return { error: "Invalid id" };
17165
17406
  }
17166
- const body = request.body;
17167
- if (typeof body?.enabled !== "boolean") {
17407
+ const rawBody = request.body;
17408
+ if (!rawBody || typeof rawBody !== "object" || Array.isArray(rawBody)) {
17409
+ reply.code(400);
17410
+ return { error: "Invalid request body" };
17411
+ }
17412
+ const body = rawBody;
17413
+ const hasEnabled = typeof body?.enabled === "boolean";
17414
+ const hasAllowedEndpoints = Object.prototype.hasOwnProperty.call(body, "allowedEndpoints");
17415
+ if (!hasEnabled && !hasAllowedEndpoints) {
17168
17416
  reply.code(400);
17169
- return { error: "enabled field is required" };
17417
+ return { error: "At least one of enabled or allowedEndpoints is required" };
17418
+ }
17419
+ const normalizedAllowedEndpoints = hasAllowedEndpoints ? normalizeAllowedEndpoints(body.allowedEndpoints) : { ok: true, value: null };
17420
+ if (!normalizedAllowedEndpoints.ok) {
17421
+ reply.code(400);
17422
+ return { error: normalizedAllowedEndpoints.error };
17170
17423
  }
17171
17424
  try {
17172
- await setApiKeyEnabled(id, body.enabled, { ipAddress: request.ip });
17425
+ await updateApiKeySettings(
17426
+ id,
17427
+ {
17428
+ enabled: hasEnabled ? body.enabled : void 0,
17429
+ allowedEndpoints: normalizedAllowedEndpoints.value,
17430
+ allowedEndpointsProvided: hasAllowedEndpoints
17431
+ },
17432
+ { ipAddress: request.ip }
17433
+ );
17173
17434
  return { success: true };
17174
17435
  } catch (error) {
17175
17436
  if (error instanceof Error && error.message === "API key not found") {