responses-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,107 @@
1
+ export function resolveMaxOutputTokensRule(capabilities) {
2
+ const configured = capabilities?.requestParameterPolicy?.maxOutputTokens;
3
+ if (configured) {
4
+ return normalizeRequestParameterRule(configured, "max_output_tokens");
5
+ }
6
+ if (capabilities?.stripMaxOutputTokens) {
7
+ return { mode: "strip" };
8
+ }
9
+ return { mode: "forward" };
10
+ }
11
+ export function shouldForwardMaxOutputTokens(capabilities) {
12
+ return resolveMaxOutputTokensRule(capabilities).mode !== "strip";
13
+ }
14
+ export function applyProviderRequestParameterPolicy(body, capabilities) {
15
+ const rule = resolveMaxOutputTokensRule(capabilities);
16
+ if (!Object.prototype.hasOwnProperty.call(body, "max_output_tokens")) {
17
+ return body;
18
+ }
19
+ if (rule.mode === "forward") {
20
+ return body;
21
+ }
22
+ const { max_output_tokens: maxOutputTokens, ...rest } = body;
23
+ if (rule.mode === "strip") {
24
+ return rest;
25
+ }
26
+ const target = rule.target?.trim();
27
+ if (!target || target === "max_output_tokens") {
28
+ return body;
29
+ }
30
+ return {
31
+ ...rest,
32
+ [target]: maxOutputTokens,
33
+ };
34
+ }
35
+ export function parseProviderRequestParameterPolicyInput(value) {
36
+ if (!isRecord(value)) {
37
+ return {};
38
+ }
39
+ const maxOutputTokensValue = value.maxOutputTokens ??
40
+ value.max_output_tokens ??
41
+ value.maxOutputTokensRule ??
42
+ value.max_output_tokens_rule;
43
+ if (maxOutputTokensValue === undefined) {
44
+ return {};
45
+ }
46
+ return {
47
+ maxOutputTokens: normalizeRequestParameterRule(maxOutputTokensValue, "max_output_tokens"),
48
+ };
49
+ }
50
+ export function cloneProviderRequestParameterPolicy(policy) {
51
+ return policy?.maxOutputTokens
52
+ ? {
53
+ maxOutputTokens: {
54
+ mode: policy.maxOutputTokens.mode,
55
+ ...(policy.maxOutputTokens.target ? { target: policy.maxOutputTokens.target } : {}),
56
+ },
57
+ }
58
+ : {};
59
+ }
60
+ function normalizeRequestParameterRule(value, fallbackTarget) {
61
+ if (typeof value === "string") {
62
+ return {
63
+ mode: normalizeRequestParameterMode(value),
64
+ };
65
+ }
66
+ if (!isRecord(value)) {
67
+ return { mode: "forward" };
68
+ }
69
+ const rawMode = typeof value.mode === "string"
70
+ ? value.mode
71
+ : typeof value.action === "string"
72
+ ? value.action
73
+ : "forward";
74
+ const mode = normalizeRequestParameterMode(rawMode);
75
+ const rawTarget = typeof value.target === "string"
76
+ ? value.target
77
+ : typeof value.renameTo === "string"
78
+ ? value.renameTo
79
+ : typeof value.rename_to === "string"
80
+ ? value.rename_to
81
+ : undefined;
82
+ const target = rawTarget?.trim() || undefined;
83
+ if (mode !== "rename") {
84
+ return { mode };
85
+ }
86
+ return {
87
+ mode,
88
+ target: normalizeParameterName(target || fallbackTarget),
89
+ };
90
+ }
91
+ function normalizeRequestParameterMode(value) {
92
+ const normalized = value.trim().toLowerCase();
93
+ if (normalized === "forward" || normalized === "strip" || normalized === "rename") {
94
+ return normalized;
95
+ }
96
+ return "forward";
97
+ }
98
+ function normalizeParameterName(value) {
99
+ const normalized = value.trim();
100
+ if (!/^[a-z][a-z0-9_]*$/i.test(normalized)) {
101
+ return "max_output_tokens";
102
+ }
103
+ return normalized;
104
+ }
105
+ function isRecord(value) {
106
+ return typeof value === "object" && value !== null && !Array.isArray(value);
107
+ }
@@ -0,0 +1,26 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { applyProviderRequestParameterPolicy, resolveMaxOutputTokensRule, } from "./provider-request-parameters.js";
4
+ test("request parameter policy can rename max_output_tokens for provider wire compatibility", () => {
5
+ const rewritten = applyProviderRequestParameterPolicy({
6
+ model: "gpt-5.4",
7
+ max_output_tokens: 512,
8
+ }, {
9
+ requestParameterPolicy: {
10
+ maxOutputTokens: {
11
+ mode: "rename",
12
+ target: "max_completion_tokens",
13
+ },
14
+ },
15
+ });
16
+ assert.equal("max_output_tokens" in rewritten, false);
17
+ assert.equal(rewritten.max_completion_tokens, 512);
18
+ });
19
+ test("legacy stripMaxOutputTokens flag resolves to strip mode for backward compatibility", () => {
20
+ const rule = resolveMaxOutputTokensRule({
21
+ stripMaxOutputTokens: true,
22
+ });
23
+ assert.deepEqual(rule, {
24
+ mode: "strip",
25
+ });
26
+ });
@@ -0,0 +1,114 @@
1
+ export function readRequestProviderHint(headers, metadata) {
2
+ const explicitProviderId = readHeaderString(headers["x-provider-id"]) ?? readMetadataString(metadata, "provider_id");
3
+ const explicitProviderName = readHeaderString(headers["x-provider-name"]) ??
4
+ readMetadataString(metadata, "provider_name") ??
5
+ readMetadataString(metadata, "provider");
6
+ return {
7
+ providerId: explicitProviderId,
8
+ providerName: explicitProviderName,
9
+ };
10
+ }
11
+ export function resolveProviderForRequest(args) {
12
+ const { providers } = args;
13
+ if (!providers.length) {
14
+ return {
15
+ error: {
16
+ statusCode: 401,
17
+ type: "authentication_error",
18
+ code: "INVALID_ROUTING_API_KEY",
19
+ message: "Authorization Bearer token must match one of the configured client or provider API keys",
20
+ },
21
+ };
22
+ }
23
+ const explicitMatch = resolveExplicitProviderMatch(providers, args.explicitProviderId, args.explicitProviderName);
24
+ if (explicitMatch && "error" in explicitMatch) {
25
+ return explicitMatch;
26
+ }
27
+ if (explicitMatch?.provider) {
28
+ return explicitMatch;
29
+ }
30
+ if (providers.length === 1) {
31
+ return {
32
+ provider: providers[0],
33
+ matchReason: "single_match",
34
+ };
35
+ }
36
+ return {
37
+ error: {
38
+ statusCode: 409,
39
+ type: "validation_error",
40
+ code: "AMBIGUOUS_PROVIDER_SELECTION",
41
+ message: "This API key is assigned to multiple providers. Set metadata.provider_id, metadata.provider, x-provider-id, or x-provider-name.",
42
+ },
43
+ };
44
+ }
45
+ function resolveExplicitProviderMatch(providers, explicitProviderId, explicitProviderName) {
46
+ const normalizedProviderId = explicitProviderId?.trim();
47
+ if (normalizedProviderId) {
48
+ const matched = providers.find((provider) => provider.id === normalizedProviderId);
49
+ if (!matched) {
50
+ return {
51
+ error: {
52
+ statusCode: 403,
53
+ type: "authentication_error",
54
+ code: "PROVIDER_NOT_ALLOWED_FOR_API_KEY",
55
+ message: "The supplied API key is not allowed to access the requested provider",
56
+ },
57
+ };
58
+ }
59
+ return {
60
+ provider: matched,
61
+ matchReason: "explicit_provider",
62
+ };
63
+ }
64
+ const normalizedProviderName = normalizeProviderName(explicitProviderName);
65
+ if (!normalizedProviderName) {
66
+ return null;
67
+ }
68
+ const matches = providers.filter((provider) => normalizeProviderName(provider.name) === normalizedProviderName ||
69
+ normalizeProviderName(provider.id) === normalizedProviderName);
70
+ if (matches.length === 1) {
71
+ return {
72
+ provider: matches[0],
73
+ matchReason: "explicit_provider",
74
+ };
75
+ }
76
+ if (matches.length > 1) {
77
+ return {
78
+ error: {
79
+ statusCode: 409,
80
+ type: "validation_error",
81
+ code: "AMBIGUOUS_PROVIDER_NAME",
82
+ message: "The supplied provider name matches multiple configured providers",
83
+ },
84
+ };
85
+ }
86
+ return {
87
+ error: {
88
+ statusCode: 403,
89
+ type: "authentication_error",
90
+ code: "PROVIDER_NOT_ALLOWED_FOR_API_KEY",
91
+ message: "The supplied API key is not allowed to access the requested provider",
92
+ },
93
+ };
94
+ }
95
+ function readHeaderString(value) {
96
+ if (typeof value === "string" && value.trim()) {
97
+ return value.trim();
98
+ }
99
+ if (Array.isArray(value)) {
100
+ const first = value.find((entry) => typeof entry === "string" && entry.trim());
101
+ return typeof first === "string" ? first.trim() : undefined;
102
+ }
103
+ return undefined;
104
+ }
105
+ function readMetadataString(value, key) {
106
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
107
+ return undefined;
108
+ }
109
+ const result = value[key];
110
+ return typeof result === "string" && result.trim() ? result.trim() : undefined;
111
+ }
112
+ function normalizeProviderName(value) {
113
+ return typeof value === "string" && value.trim() ? value.trim().toLowerCase() : undefined;
114
+ }
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { readRequestProviderHint, resolveProviderForRequest, } from "./provider-routing.js";
4
+ function createProvider(id, name, clientApiKeys) {
5
+ return {
6
+ id,
7
+ name,
8
+ baseUrl: `https://${id}.example/v1`,
9
+ responsesUrl: `https://${id}.example/v1/responses`,
10
+ providerApiKeys: [],
11
+ clientApiKeys,
12
+ capabilities: {
13
+ usageCheckEnabled: false,
14
+ stripMaxOutputTokens: false,
15
+ requestParameterPolicy: {},
16
+ sanitizeReasoningSummary: false,
17
+ stripModelPrefixes: [],
18
+ },
19
+ };
20
+ }
21
+ test("reads explicit provider hint from headers and metadata", () => {
22
+ assert.deepEqual(readRequestProviderHint({
23
+ "x-provider-id": "cliproxy",
24
+ "x-provider-name": "ClipProxy",
25
+ }, {
26
+ provider_id: "ignored",
27
+ provider: "ignored",
28
+ }), {
29
+ providerId: "cliproxy",
30
+ providerName: "ClipProxy",
31
+ });
32
+ });
33
+ test("resolves a shared API key by explicit provider name", () => {
34
+ const providers = [
35
+ createProvider("cliproxy", "cliproxy", ["shared-key"]),
36
+ createProvider("krouter", "krouter", ["shared-key"]),
37
+ ];
38
+ const result = resolveProviderForRequest({
39
+ providers,
40
+ explicitProviderName: "krouter",
41
+ });
42
+ assert.equal("provider" in result, true);
43
+ if ("provider" in result) {
44
+ assert.equal(result.provider.id, "krouter");
45
+ assert.equal(result.matchReason, "explicit_provider");
46
+ }
47
+ });
48
+ test("returns a clear error when a shared API key is ambiguous", () => {
49
+ const providers = [
50
+ createProvider("cliproxy", "cliproxy", ["shared-key"]),
51
+ createProvider("krouter", "krouter", ["shared-key"]),
52
+ ];
53
+ const result = resolveProviderForRequest({
54
+ providers,
55
+ });
56
+ assert.deepEqual(result, {
57
+ error: {
58
+ statusCode: 409,
59
+ type: "validation_error",
60
+ code: "AMBIGUOUS_PROVIDER_SELECTION",
61
+ message: "This API key is assigned to multiple providers. Set metadata.provider_id, metadata.provider, x-provider-id, or x-provider-name.",
62
+ },
63
+ });
64
+ });
@@ -0,0 +1,314 @@
1
+ export class ProviderUsageLimitError extends Error {
2
+ statusCode = 429;
3
+ code = "PROVIDER_TOKEN_LIMIT_REACHED";
4
+ usage;
5
+ constructor(message, usage) {
6
+ super(message);
7
+ this.name = "ProviderUsageLimitError";
8
+ this.usage = usage;
9
+ }
10
+ }
11
+ export const OPENAI_ORGANIZATION_USAGE_COMPLETIONS_URL = "https://api.openai.com/v1/organization/usage/completions";
12
+ export async function fetchProviderUsage({ apiKey, requestId, logger, timeoutMs, url, onEvent, }) {
13
+ if (!apiKey) {
14
+ return undefined;
15
+ }
16
+ const controller = new AbortController();
17
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
18
+ const startedAt = Date.now();
19
+ try {
20
+ const response = await fetch(url, {
21
+ method: "POST",
22
+ headers: {
23
+ "Content-Type": "application/json",
24
+ },
25
+ body: JSON.stringify({ apiKey }),
26
+ signal: controller.signal,
27
+ });
28
+ const rawText = await response.text();
29
+ const rawPayload = parseJsonSafely(rawText);
30
+ if (!response.ok) {
31
+ logger.warn({
32
+ requestId,
33
+ usageCheckStatus: response.status,
34
+ usageCheckBody: rawText,
35
+ }, "provider usage check request failed");
36
+ await onEvent?.({
37
+ event: "provider_usage_check_failed",
38
+ requestId,
39
+ usageCheckStatus: response.status,
40
+ usageCheckBody: rawText,
41
+ });
42
+ return undefined;
43
+ }
44
+ const usage = extractUsageSnapshot(rawPayload);
45
+ logger.info({
46
+ requestId,
47
+ usageCheckStatus: response.status,
48
+ usageCheckMs: Date.now() - startedAt,
49
+ usageAllowed: usage.allowed,
50
+ usageRemaining: usage.remaining,
51
+ usageLimit: usage.limit,
52
+ usageUsed: usage.used,
53
+ }, "provider usage check completed");
54
+ await onEvent?.({
55
+ event: "provider_usage_checked",
56
+ requestId,
57
+ usageCheckStatus: response.status,
58
+ usageCheckMs: Date.now() - startedAt,
59
+ usageAllowed: usage.allowed,
60
+ usageRemaining: usage.remaining,
61
+ usageLimit: usage.limit,
62
+ usageUsed: usage.used,
63
+ });
64
+ return usage;
65
+ }
66
+ catch (error) {
67
+ logger.warn({
68
+ err: error,
69
+ requestId,
70
+ }, "provider usage check skipped after request error");
71
+ await onEvent?.({
72
+ event: "provider_usage_check_error",
73
+ requestId,
74
+ errorMessage: error instanceof Error ? error.message : "Unknown usage check error",
75
+ });
76
+ return undefined;
77
+ }
78
+ finally {
79
+ clearTimeout(timeout);
80
+ }
81
+ }
82
+ export async function fetchOpenAiCompletionsUsage({ accessToken, requestId, logger, timeoutMs, url = OPENAI_ORGANIZATION_USAGE_COMPLETIONS_URL, now = new Date(), }) {
83
+ if (!accessToken) {
84
+ return undefined;
85
+ }
86
+ const controller = new AbortController();
87
+ const timeout = setTimeout(() => controller.abort(), timeoutMs);
88
+ const startedAt = Date.now();
89
+ const endTime = Math.floor(now.getTime() / 1000);
90
+ const startTime = endTime - 24 * 60 * 60;
91
+ const usageUrl = new URL(url);
92
+ usageUrl.searchParams.set("start_time", String(startTime));
93
+ usageUrl.searchParams.set("end_time", String(endTime));
94
+ usageUrl.searchParams.set("bucket_width", "1d");
95
+ usageUrl.searchParams.set("group_by", "model");
96
+ try {
97
+ const response = await fetch(usageUrl, {
98
+ method: "GET",
99
+ headers: {
100
+ Authorization: `Bearer ${accessToken}`,
101
+ },
102
+ signal: controller.signal,
103
+ });
104
+ const rawText = await response.text();
105
+ const rawPayload = parseJsonSafely(rawText);
106
+ if (!response.ok) {
107
+ logger.warn({
108
+ requestId,
109
+ usageCheckStatus: response.status,
110
+ usageCheckBody: rawText,
111
+ }, "openai usage request failed");
112
+ return {
113
+ allowed: false,
114
+ raw: rawPayload,
115
+ };
116
+ }
117
+ const usage = extractOpenAiUsageSnapshot(rawPayload);
118
+ logger.info({
119
+ requestId,
120
+ usageCheckStatus: response.status,
121
+ usageCheckMs: Date.now() - startedAt,
122
+ usageUsed: usage.used,
123
+ }, "openai usage request completed");
124
+ return usage;
125
+ }
126
+ catch (error) {
127
+ logger.warn({
128
+ err: error,
129
+ requestId,
130
+ }, "openai usage request skipped after request error");
131
+ return undefined;
132
+ }
133
+ finally {
134
+ clearTimeout(timeout);
135
+ }
136
+ }
137
+ export async function ensureProviderUsageAvailable(args) {
138
+ const usage = await fetchProviderUsage(args);
139
+ if (!usage) {
140
+ return undefined;
141
+ }
142
+ if (usage.allowed === false) {
143
+ throw new ProviderUsageLimitError("Provider API key is not allowed to make more requests", usage);
144
+ }
145
+ if (usage.remaining !== undefined && usage.remaining <= 0) {
146
+ throw new ProviderUsageLimitError("Provider token limit has been reached", usage);
147
+ }
148
+ return usage;
149
+ }
150
+ function extractUsageSnapshot(raw) {
151
+ const limit = firstFiniteNumber(raw, [
152
+ ["dailyTokenLimit"],
153
+ ["limit"],
154
+ ["token_limit"],
155
+ ["tokens_limit"],
156
+ ["quota"],
157
+ ["quota_limit"],
158
+ ["data", "dailyTokenLimit"],
159
+ ["data", "limit"],
160
+ ["data", "token_limit"],
161
+ ["data", "tokens_limit"],
162
+ ["data", "quota"],
163
+ ["result", "dailyTokenLimit"],
164
+ ["result", "limit"],
165
+ ["result", "token_limit"],
166
+ ]);
167
+ const used = firstFiniteNumber(raw, [
168
+ ["dailyTokensUsed"],
169
+ ["used"],
170
+ ["used_tokens"],
171
+ ["tokens_used"],
172
+ ["consumed_tokens"],
173
+ ["data", "dailyTokensUsed"],
174
+ ["data", "used"],
175
+ ["data", "used_tokens"],
176
+ ["data", "tokens_used"],
177
+ ["result", "dailyTokensUsed"],
178
+ ["result", "used"],
179
+ ["result", "used_tokens"],
180
+ ]);
181
+ const remaining = firstFiniteNumber(raw, [
182
+ ["remaining_tokens"],
183
+ ["remaining_token"],
184
+ ["tokens_remaining"],
185
+ ["token_remaining"],
186
+ ["remaining"],
187
+ ["quota_remaining"],
188
+ ["remaining_quota"],
189
+ ["dailyTokensRemaining"],
190
+ ["data", "remaining_tokens"],
191
+ ["data", "tokens_remaining"],
192
+ ["data", "remaining"],
193
+ ["data", "quota_remaining"],
194
+ ["data", "dailyTokensRemaining"],
195
+ ["result", "remaining_tokens"],
196
+ ["result", "tokens_remaining"],
197
+ ["result", "remaining"],
198
+ ["result", "dailyTokensRemaining"],
199
+ ]);
200
+ const allowed = firstBoolean(raw, [
201
+ ["allowed"],
202
+ ["isActive"],
203
+ ["isExpired"],
204
+ ["active"],
205
+ ["valid"],
206
+ ["enabled"],
207
+ ["can_use"],
208
+ ["data", "allowed"],
209
+ ["data", "isActive"],
210
+ ["data", "isExpired"],
211
+ ["data", "active"],
212
+ ["data", "valid"],
213
+ ["result", "allowed"],
214
+ ["result", "isActive"],
215
+ ["result", "isExpired"],
216
+ ["result", "active"],
217
+ ]);
218
+ const normalizedAllowed = deriveAllowed(raw, allowed);
219
+ const normalizedRemaining = remaining ?? (limit !== undefined && used !== undefined ? Math.max(limit - used, 0) : undefined);
220
+ return {
221
+ allowed: normalizedAllowed,
222
+ remaining: normalizedRemaining,
223
+ limit,
224
+ used,
225
+ raw,
226
+ };
227
+ }
228
+ function extractOpenAiUsageSnapshot(raw) {
229
+ const buckets = isRecord(raw) && Array.isArray(raw.data) ? raw.data : [];
230
+ let used = 0;
231
+ for (const bucket of buckets) {
232
+ if (!isRecord(bucket) || !Array.isArray(bucket.results)) {
233
+ continue;
234
+ }
235
+ for (const result of bucket.results) {
236
+ if (!isRecord(result)) {
237
+ continue;
238
+ }
239
+ used += toFiniteNumber(result.input_tokens) ?? 0;
240
+ used += toFiniteNumber(result.output_tokens) ?? 0;
241
+ used += toFiniteNumber(result.input_audio_tokens) ?? 0;
242
+ used += toFiniteNumber(result.output_audio_tokens) ?? 0;
243
+ }
244
+ }
245
+ return {
246
+ allowed: true,
247
+ used,
248
+ raw,
249
+ };
250
+ }
251
+ function deriveAllowed(raw, allowed) {
252
+ const isExpired = firstBoolean(raw, [["isExpired"], ["data", "isExpired"], ["result", "isExpired"]]);
253
+ const isActive = firstBoolean(raw, [["isActive"], ["data", "isActive"], ["result", "isActive"]]);
254
+ if (isExpired === true) {
255
+ return false;
256
+ }
257
+ if (isActive !== undefined) {
258
+ return isActive;
259
+ }
260
+ return allowed;
261
+ }
262
+ function parseJsonSafely(value) {
263
+ if (!value.trim()) {
264
+ return undefined;
265
+ }
266
+ try {
267
+ return JSON.parse(value);
268
+ }
269
+ catch {
270
+ return value;
271
+ }
272
+ }
273
+ function isRecord(value) {
274
+ return typeof value === "object" && value !== null && !Array.isArray(value);
275
+ }
276
+ function firstFiniteNumber(payload, paths) {
277
+ for (const path of paths) {
278
+ const value = readPath(payload, path);
279
+ const parsed = toFiniteNumber(value);
280
+ if (parsed !== undefined) {
281
+ return parsed;
282
+ }
283
+ }
284
+ return undefined;
285
+ }
286
+ function firstBoolean(payload, paths) {
287
+ for (const path of paths) {
288
+ const value = readPath(payload, path);
289
+ if (typeof value === "boolean") {
290
+ return value;
291
+ }
292
+ }
293
+ return undefined;
294
+ }
295
+ function readPath(payload, path) {
296
+ let current = payload;
297
+ for (const key of path) {
298
+ if (typeof current !== "object" || current === null || Array.isArray(current)) {
299
+ return undefined;
300
+ }
301
+ current = current[key];
302
+ }
303
+ return current;
304
+ }
305
+ function toFiniteNumber(value) {
306
+ if (typeof value === "number" && Number.isFinite(value)) {
307
+ return value;
308
+ }
309
+ if (typeof value === "string" && value.trim()) {
310
+ const parsed = Number(value);
311
+ return Number.isFinite(parsed) ? parsed : undefined;
312
+ }
313
+ return undefined;
314
+ }
@@ -0,0 +1,61 @@
1
+ const HERMES_SUMMARY_MARKERS = [
2
+ "you are a summarization agent creating a context checkpoint",
3
+ "context checkpoint",
4
+ "generate context summary",
5
+ ];
6
+ export function resolveRequestTimeoutMs(body, options) {
7
+ if (!options.extendHermesSummaryTimeout) {
8
+ return options.defaultTimeoutMs;
9
+ }
10
+ if (!isHermesSummaryRequest(body)) {
11
+ return options.defaultTimeoutMs;
12
+ }
13
+ return Math.max(options.defaultTimeoutMs, options.summaryTimeoutMs);
14
+ }
15
+ export function isHermesSummaryRequest(body) {
16
+ const input = body.input;
17
+ if (!Array.isArray(input) || input.length === 0) {
18
+ return false;
19
+ }
20
+ const preview = input
21
+ .slice(0, 2)
22
+ .map(extractInputText)
23
+ .filter((value) => Boolean(value))
24
+ .join("\n")
25
+ .toLowerCase();
26
+ return HERMES_SUMMARY_MARKERS.some((marker) => preview.includes(marker));
27
+ }
28
+ function extractInputText(value) {
29
+ if (typeof value === "string") {
30
+ return value;
31
+ }
32
+ if (typeof value !== "object" || value === null) {
33
+ return undefined;
34
+ }
35
+ if ("text" in value && typeof value.text === "string") {
36
+ return value.text;
37
+ }
38
+ if ("content" in value) {
39
+ const content = value.content;
40
+ if (typeof content === "string") {
41
+ return content;
42
+ }
43
+ if (Array.isArray(content)) {
44
+ return content
45
+ .map((entry) => {
46
+ if (typeof entry === "string") {
47
+ return entry;
48
+ }
49
+ if (typeof entry === "object" &&
50
+ entry !== null &&
51
+ "text" in entry &&
52
+ typeof entry.text === "string") {
53
+ return entry.text;
54
+ }
55
+ return "";
56
+ })
57
+ .join("\n");
58
+ }
59
+ }
60
+ return undefined;
61
+ }