@chenpu17/cc-gw 0.7.17 → 0.7.19

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 -155
  3. package/src/web/dist/assets/{About-T8buLoTf.js → About-BA_VxEEw.js} +2 -2
  4. package/src/web/dist/assets/ApiKeys-9iuHXiuE.js +16 -0
  5. package/src/web/dist/assets/Dashboard-BV9q_ZEH.js +16 -0
  6. package/src/web/dist/assets/{Events-BY6QDHf6.js → Events-CVDX7Sdl.js} +2 -2
  7. package/src/web/dist/assets/{Help-CXlj66Zk.js → Help-Q8aahW1e.js} +1 -1
  8. package/src/web/dist/assets/{Login-C227e592.js → Login-Cc9PcXNz.js} +1 -1
  9. package/src/web/dist/assets/Logs-EvCIbsmH.js +6 -0
  10. package/src/web/dist/assets/ModelManagement-Bb8NWZJD.js +6 -0
  11. package/src/web/dist/assets/{PageHeader-BatC8G-O.js → PageHeader-DE87PlB_.js} +1 -1
  12. package/src/web/dist/assets/{PageSection-HTrZH-H0.js → PageSection-BW4UtaTE.js} +1 -1
  13. package/src/web/dist/assets/Settings-hk35KMN5.js +6 -0
  14. package/src/web/dist/assets/Skeleton-DKmc2Oau.js +1 -0
  15. package/src/web/dist/assets/{card-DQb2ediN.js → card-B5QaMdIj.js} +1 -1
  16. package/src/web/dist/assets/{dialog-B9ZBo_5P.js → dialog-Bl93c96O.js} +3 -3
  17. package/src/web/dist/assets/{index-CprjNJ8x.js → index-Bkl9YQUf.js} +1 -1
  18. package/src/web/dist/assets/index-DDEa11GU.css +1 -0
  19. package/src/web/dist/assets/index-iaYfODK6.js +280 -0
  20. package/src/web/dist/assets/{info-BSxQbQAp.js → info-Dtepvi_0.js} +1 -1
  21. package/src/web/dist/assets/{input-x0jTfvyD.js → input-CRfONI49.js} +1 -1
  22. package/src/web/dist/assets/{label-t5tthgxG.js → label-sf3QNge5.js} +1 -1
  23. package/src/web/dist/assets/{refresh-cw-DHAoO4El.js → refresh-cw-ZDTq8wum.js} +1 -1
  24. package/src/web/dist/assets/select-DfAJfUCL.js +11 -0
  25. package/src/web/dist/assets/shield-wbL9EF4o.js +11 -0
  26. package/src/web/dist/assets/switch-DTxmu4vA.js +1 -0
  27. package/src/web/dist/assets/{useApiQuery-CkT-JtpV.js → useApiQuery-LZxVJXA3.js} +1 -1
  28. package/src/web/dist/index.html +2 -2
  29. package/src/web/dist/assets/ApiKeys-kkOS5njK.js +0 -16
  30. package/src/web/dist/assets/Dashboard-BuFP7xgE.js +0 -16
  31. package/src/web/dist/assets/Logs-A1_44nYh.js +0 -6
  32. package/src/web/dist/assets/ModelManagement-CZsts9T9.js +0 -1
  33. package/src/web/dist/assets/Settings-DDaFQ7RV.js +0 -11
  34. package/src/web/dist/assets/copy-8Dwvprhw.js +0 -6
  35. package/src/web/dist/assets/index-CsJ3GRy4.css +0 -1
  36. package/src/web/dist/assets/index-D-F4C6n_.js +0 -280
  37. package/src/web/dist/assets/select-fri8gRof.js +0 -11
  38. package/src/web/dist/assets/switch-BrV6LX33.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
  };
@@ -15204,7 +15495,7 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15204
15495
  const json = await readProviderBodyJson(upstream.body);
15205
15496
  const usagePayload = json?.usage ?? null;
15206
15497
  const inputTokens2 = usagePayload?.prompt_tokens ?? usagePayload?.input_tokens ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
15207
- 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);
15208
15499
  const cached = resolveCachedTokens(usagePayload);
15209
15500
  const cachedTokens = cached.read + cached.creation;
15210
15501
  const latencyMs2 = Date.now() - requestStart;
@@ -15318,54 +15609,14 @@ async function handleOpenAIChatProtocol(request, reply, endpoint, endpointId, ap
15318
15609
  requests: 1,
15319
15610
  inputTokens,
15320
15611
  outputTokens,
15612
+ cachedTokens: usageCached ?? 0,
15613
+ cacheReadTokens: usageCacheRead,
15614
+ cacheCreationTokens: usageCacheCreation,
15321
15615
  latencyMs
15322
15616
  });
15323
15617
  if (storeResponsePayloads && capturedChunks) {
15324
15618
  try {
15325
- const allChunks = capturedChunks.join("");
15326
- let accumulatedText = "";
15327
- let finishReason = null;
15328
- const lines = allChunks.split("\n");
15329
- for (const line of lines) {
15330
- const trimmed = line.trim();
15331
- if (trimmed.startsWith("data:")) {
15332
- const dataStr = trimmed.slice(5).trim();
15333
- if (dataStr !== "[DONE]") {
15334
- try {
15335
- const parsed = JSON.parse(dataStr);
15336
- const choice = parsed?.choices?.[0];
15337
- if (choice) {
15338
- if (choice.delta?.content) {
15339
- accumulatedText += choice.delta.content;
15340
- }
15341
- if (choice.finish_reason) {
15342
- finishReason = choice.finish_reason;
15343
- }
15344
- }
15345
- } catch {
15346
- }
15347
- }
15348
- }
15349
- }
15350
- const responseSummary = {
15351
- id: `chatcmpl_${Date.now()}`,
15352
- object: "chat.completion",
15353
- model: target.modelId,
15354
- choices: [{
15355
- index: 0,
15356
- message: {
15357
- role: "assistant",
15358
- content: accumulatedText
15359
- },
15360
- finish_reason: finishReason ?? "stop"
15361
- }],
15362
- usage: {
15363
- prompt_tokens: inputTokens,
15364
- completion_tokens: outputTokens,
15365
- total_tokens: inputTokens + outputTokens,
15366
- cached_tokens: usageCached ?? 0
15367
- }
15368
- };
15619
+ const responseSummary = parseSSEToSummary(capturedChunks, "chat", target.modelId);
15369
15620
  await upsertLogPayload(logId, { response: JSON.stringify(responseSummary) });
15370
15621
  } catch {
15371
15622
  }
@@ -15438,13 +15689,13 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15438
15689
  const providedApiKey = extractApiKeyFromRequest(request);
15439
15690
  let apiKeyContext;
15440
15691
  try {
15441
- apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip });
15692
+ apiKeyContext = await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId });
15442
15693
  } catch (error) {
15443
15694
  if (error instanceof ApiKeyError) {
15444
- reply.code(401);
15695
+ reply.code(error.code === "forbidden" ? 403 : 401);
15445
15696
  return {
15446
15697
  error: {
15447
- code: "invalid_api_key",
15698
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15448
15699
  message: error.message
15449
15700
  }
15450
15701
  };
@@ -15568,8 +15819,10 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15568
15819
  const json = await readProviderBodyJson(upstream.body);
15569
15820
  const usagePayload = json?.usage ?? null;
15570
15821
  const inputTokens2 = usagePayload?.prompt_tokens ?? usagePayload?.input_tokens ?? target.tokenEstimate ?? estimateTokens(normalized, target.modelId);
15571
- const content = json?.response?.body?.content ?? json?.choices?.[0]?.message?.content ?? "";
15572
- 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
+ );
15573
15826
  const cached = resolveCachedTokens(usagePayload);
15574
15827
  const cachedTokens = cached.read + cached.creation;
15575
15828
  const latencyMs2 = Date.now() - requestStart;
@@ -15641,7 +15894,7 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15641
15894
  if (dataStr !== "[DONE]") {
15642
15895
  try {
15643
15896
  const parsed = JSON.parse(dataStr);
15644
- const usage = parsed?.usage || null;
15897
+ const usage = parsed?.usage || parsed?.response?.usage || null;
15645
15898
  if (usage) {
15646
15899
  usagePrompt = usage.prompt_tokens ?? usage.input_tokens ?? usagePrompt;
15647
15900
  usageCompletion = usage.completion_tokens ?? usage.output_tokens ?? usageCompletion;
@@ -15683,54 +15936,14 @@ async function handleOpenAIResponsesProtocol(request, reply, endpoint, endpointI
15683
15936
  requests: 1,
15684
15937
  inputTokens,
15685
15938
  outputTokens,
15939
+ cachedTokens: usageCached ?? 0,
15940
+ cacheReadTokens: usageCacheRead,
15941
+ cacheCreationTokens: usageCacheCreation,
15686
15942
  latencyMs
15687
15943
  });
15688
15944
  if (storeResponsePayloads && capturedChunks) {
15689
15945
  try {
15690
- const allChunks = capturedChunks.join("");
15691
- let accumulatedText = "";
15692
- let finishReason = null;
15693
- const lines = allChunks.split("\n");
15694
- for (const line of lines) {
15695
- const trimmed = line.trim();
15696
- if (trimmed.startsWith("data:")) {
15697
- const dataStr = trimmed.slice(5).trim();
15698
- if (dataStr !== "[DONE]") {
15699
- try {
15700
- const parsed = JSON.parse(dataStr);
15701
- const choice = parsed?.choices?.[0];
15702
- if (choice) {
15703
- if (choice.delta?.content) {
15704
- accumulatedText += choice.delta.content;
15705
- }
15706
- if (choice.finish_reason) {
15707
- finishReason = choice.finish_reason;
15708
- }
15709
- }
15710
- } catch {
15711
- }
15712
- }
15713
- }
15714
- }
15715
- const responseSummary = {
15716
- id: `chatcmpl_${Date.now()}`,
15717
- object: "chat.completion",
15718
- model: target.modelId,
15719
- choices: [{
15720
- index: 0,
15721
- message: {
15722
- role: "assistant",
15723
- content: accumulatedText
15724
- },
15725
- finish_reason: finishReason ?? "stop"
15726
- }],
15727
- usage: {
15728
- prompt_tokens: inputTokens,
15729
- completion_tokens: outputTokens,
15730
- total_tokens: inputTokens + outputTokens,
15731
- cached_tokens: usageCached ?? 0
15732
- }
15733
- };
15946
+ const responseSummary = parseSSEToSummary(capturedChunks, "responses", target.modelId);
15734
15947
  await upsertLogPayload(logId, { response: JSON.stringify(responseSummary) });
15735
15948
  } catch {
15736
15949
  }
@@ -15804,13 +16017,13 @@ async function registerOpenAiRoutes(app) {
15804
16017
  const handleModels = async (request, reply) => {
15805
16018
  const providedApiKey = extractApiKeyFromRequest2(request);
15806
16019
  try {
15807
- await resolveApiKey(providedApiKey, { ipAddress: request.ip });
16020
+ await resolveApiKey(providedApiKey, { ipAddress: request.ip, endpointId: "openai" });
15808
16021
  } catch (error) {
15809
16022
  if (error instanceof ApiKeyError) {
15810
- reply.code(401);
16023
+ reply.code(error.code === "forbidden" ? 403 : 401);
15811
16024
  return {
15812
16025
  error: {
15813
- code: "invalid_api_key",
16026
+ code: error.code === "forbidden" ? "endpoint_forbidden" : "invalid_api_key",
15814
16027
  message: error.message
15815
16028
  }
15816
16029
  };
@@ -17137,6 +17350,27 @@ async function registerAdminRoutes(app) {
17137
17350
  const endpoint = typeof query.endpoint === "string" && query.endpoint.length > 0 ? query.endpoint : void 0;
17138
17351
  return getApiKeyUsageMetrics(days, limit, endpoint);
17139
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
+ };
17140
17374
  app.get("/api/keys", async () => {
17141
17375
  return listApiKeys();
17142
17376
  });
@@ -17146,8 +17380,19 @@ async function registerAdminRoutes(app) {
17146
17380
  reply.code(400);
17147
17381
  return { error: "Name is required" };
17148
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
+ }
17149
17389
  try {
17150
- 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
+ );
17151
17396
  } catch (error) {
17152
17397
  reply.code(400);
17153
17398
  return { error: error instanceof Error ? error.message : "Failed to create API key" };
@@ -17159,13 +17404,33 @@ async function registerAdminRoutes(app) {
17159
17404
  reply.code(400);
17160
17405
  return { error: "Invalid id" };
17161
17406
  }
17162
- const body = request.body;
17163
- 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) {
17164
17416
  reply.code(400);
17165
- 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 };
17166
17423
  }
17167
17424
  try {
17168
- 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
+ );
17169
17434
  return { success: true };
17170
17435
  } catch (error) {
17171
17436
  if (error instanceof Error && error.message === "API key not found") {