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,129 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildClientTokenLimitError, extractUsageTotals, getClientTokenLimitStatus, resolveClientTokenWindowStart, } from "./client-token-limits.js";
4
+ const baseConfig = {
5
+ clientRoute: "codex",
6
+ enabled: true,
7
+ tokenLimit: 1000,
8
+ windowType: "daily",
9
+ hardBlock: true,
10
+ createdAt: "2026-04-27T00:00:00.000Z",
11
+ updatedAt: "2026-04-27T00:00:00.000Z",
12
+ };
13
+ const baseUsage = {
14
+ clientRoute: "codex",
15
+ windowStart: "2026-04-27T00:00:00.000Z",
16
+ inputTokens: 100,
17
+ outputTokens: 50,
18
+ totalTokens: 150,
19
+ updatedAt: "2026-04-27T12:00:00.000Z",
20
+ };
21
+ test("resolveClientTokenWindowStart supports daily weekly monthly and fixed windows", () => {
22
+ const now = new Date("2026-04-29T13:45:30.000Z");
23
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "daily" }), "2026-04-29T00:00:00.000Z");
24
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "weekly" }), "2026-04-27T00:00:00.000Z");
25
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "monthly" }), "2026-04-01T00:00:00.000Z");
26
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "fixed", windowSizeSeconds: 3600 }), "2026-04-29T13:00:00.000Z");
27
+ });
28
+ test("getClientTokenLimitStatus does not block disabled config", () => {
29
+ assert.deepEqual(getClientTokenLimitStatus({ ...baseConfig, enabled: false }, baseUsage), {
30
+ used: 150,
31
+ limit: null,
32
+ remaining: null,
33
+ blocked: false,
34
+ windowStart: "2026-04-27T00:00:00.000Z",
35
+ });
36
+ });
37
+ test("getClientTokenLimitStatus reports usage under limit", () => {
38
+ assert.deepEqual(getClientTokenLimitStatus(baseConfig, baseUsage), {
39
+ used: 150,
40
+ limit: 1000,
41
+ remaining: 850,
42
+ blocked: false,
43
+ windowStart: "2026-04-27T00:00:00.000Z",
44
+ });
45
+ });
46
+ test("getClientTokenLimitStatus blocks exactly at limit", () => {
47
+ assert.deepEqual(getClientTokenLimitStatus(baseConfig, {
48
+ ...baseUsage,
49
+ totalTokens: 1000,
50
+ }), {
51
+ used: 1000,
52
+ limit: 1000,
53
+ remaining: 0,
54
+ blocked: true,
55
+ windowStart: "2026-04-27T00:00:00.000Z",
56
+ });
57
+ });
58
+ test("getClientTokenLimitStatus blocks above limit", () => {
59
+ assert.deepEqual(getClientTokenLimitStatus(baseConfig, {
60
+ ...baseUsage,
61
+ totalTokens: 1075,
62
+ }), {
63
+ used: 1075,
64
+ limit: 1000,
65
+ remaining: 0,
66
+ blocked: true,
67
+ windowStart: "2026-04-27T00:00:00.000Z",
68
+ });
69
+ });
70
+ test("buildClientTokenLimitError creates a 429 request error body", () => {
71
+ const status = getClientTokenLimitStatus(baseConfig, {
72
+ ...baseUsage,
73
+ totalTokens: 1000,
74
+ });
75
+ assert.deepEqual(buildClientTokenLimitError("codex", status), {
76
+ statusCode: 429,
77
+ body: {
78
+ error: {
79
+ type: "request_error",
80
+ code: "CLIENT_TOKEN_LIMIT_EXCEEDED",
81
+ message: "Client route 'codex' has reached its token limit for the current window.",
82
+ client: "codex",
83
+ client_route: "codex",
84
+ usage: status,
85
+ },
86
+ },
87
+ });
88
+ });
89
+ test("extractUsageTotals reads direct and nested usage totals", () => {
90
+ assert.deepEqual(extractUsageTotals({
91
+ input_tokens: 10,
92
+ output_tokens: 20,
93
+ total_tokens: 30,
94
+ }), {
95
+ inputTokens: 10,
96
+ outputTokens: 20,
97
+ totalTokens: 30,
98
+ });
99
+ assert.deepEqual(extractUsageTotals({
100
+ usage: {
101
+ input_tokens: 5,
102
+ output_tokens: 6,
103
+ total_tokens: 11,
104
+ },
105
+ }), {
106
+ inputTokens: 5,
107
+ outputTokens: 6,
108
+ totalTokens: 11,
109
+ });
110
+ assert.deepEqual(extractUsageTotals({
111
+ response: {
112
+ usage: {
113
+ input_tokens: 3,
114
+ output_tokens: 4,
115
+ total_tokens: 7,
116
+ },
117
+ },
118
+ }), {
119
+ inputTokens: 3,
120
+ outputTokens: 4,
121
+ totalTokens: 7,
122
+ });
123
+ });
124
+ test("extractUsageTotals ignores usage payloads without total_tokens", () => {
125
+ assert.equal(extractUsageTotals({
126
+ input_tokens: 10,
127
+ output_tokens: 20,
128
+ }), undefined);
129
+ });
@@ -0,0 +1,47 @@
1
+ import { homedir } from "node:os";
2
+ import { readFileSync } from "node:fs";
3
+ export function resolveDefaultCodexConfigPath() {
4
+ return `${homedir()}/.codex/config.toml`;
5
+ }
6
+ export function readCodexProviderFromConfig(filePath) {
7
+ let raw;
8
+ try {
9
+ raw = readFileSync(filePath, "utf8");
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ const providerName = readTopLevelTomlString(raw, "model_provider");
15
+ if (!providerName) {
16
+ return undefined;
17
+ }
18
+ const section = readTomlSection(raw, `model_providers.${providerName}`);
19
+ if (!section) {
20
+ return undefined;
21
+ }
22
+ const baseUrl = readTomlString(section, "base_url");
23
+ if (!baseUrl) {
24
+ return undefined;
25
+ }
26
+ return {
27
+ name: readTomlString(section, "name") ?? providerName,
28
+ baseUrl,
29
+ wireApi: readTomlString(section, "wire_api"),
30
+ };
31
+ }
32
+ function readTopLevelTomlString(raw, key) {
33
+ const match = raw.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
34
+ return match?.[1];
35
+ }
36
+ function readTomlSection(raw, sectionName) {
37
+ const escaped = escapeRegExp(sectionName);
38
+ const match = raw.match(new RegExp(`^\\[${escaped}\\]\\s*$([\\s\\S]*?)(?=^\\[|\\Z)`, "m"));
39
+ return match?.[1];
40
+ }
41
+ function readTomlString(rawSection, key) {
42
+ const match = rawSection.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
43
+ return match?.[1];
44
+ }
45
+ function escapeRegExp(value) {
46
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
47
+ }
@@ -0,0 +1,87 @@
1
+ import { createHash } from "node:crypto";
2
+ export function buildCodexConfigFiles(input) {
3
+ const model = input.model?.trim() || "gpt-5.5";
4
+ const baseUrl = input.baseUrl.trim();
5
+ const apiKey = input.apiKey.trim();
6
+ return {
7
+ configToml: [
8
+ `model = ${tomlString(model)}`,
9
+ `model_provider = "resproxy"`,
10
+ `model_reasoning_effort = "medium"`,
11
+ "",
12
+ `[model_providers.resproxy]`,
13
+ `name = "resproxy"`,
14
+ `base_url = ${tomlString(baseUrl)}`,
15
+ `api_key = ${tomlString(apiKey)}`,
16
+ `wire_api = "responses"`,
17
+ "",
18
+ ].join("\n"),
19
+ authJson: `${JSON.stringify({
20
+ auth_mode: "apikey",
21
+ OPENAI_API_KEY: apiKey,
22
+ }, null, 2)}\n`,
23
+ };
24
+ }
25
+ export function buildCodexSetupEndpointUrl(publicResponsesBaseUrl) {
26
+ const trimmed = publicResponsesBaseUrl.trim().replace(/\/+$/, "");
27
+ const origin = trimmed.endsWith("/v1") ? trimmed.slice(0, -3) : trimmed;
28
+ return `${origin}/api/customer/codex/setup.sh`;
29
+ }
30
+ export function buildCodexSetupCurlCommand(input) {
31
+ const setupUrl = buildCodexSetupEndpointUrl(input.publicResponsesBaseUrl);
32
+ return [
33
+ "curl -fsSL \\",
34
+ ` -H 'Authorization: Bearer ${escapeShellSingleQuote(input.apiKey.trim())}' \\`,
35
+ ` '${escapeShellSingleQuote(setupUrl)}' \\`,
36
+ " | sh",
37
+ ].join("\n");
38
+ }
39
+ export function buildCodexConfigSetupScript(input) {
40
+ const targetDir = input.targetDir?.trim() || "$HOME/.codex";
41
+ const configDelimiter = buildHeredocDelimiter("RESPONSES_PROXY_CODEX_CONFIG", input.configToml);
42
+ const authDelimiter = buildHeredocDelimiter("RESPONSES_PROXY_CODEX_AUTH", input.authJson);
43
+ return [
44
+ "#!/usr/bin/env sh",
45
+ "set -eu",
46
+ "umask 077",
47
+ 'tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/responses-proxy-codex.XXXXXX")"',
48
+ 'trap \'rm -rf "$tmpdir"\' EXIT INT TERM',
49
+ `cat > "$tmpdir/config.toml" <<'${configDelimiter}'`,
50
+ input.configToml,
51
+ configDelimiter,
52
+ `cat > "$tmpdir/auth.json" <<'${authDelimiter}'`,
53
+ input.authJson,
54
+ authDelimiter,
55
+ "install_file() {",
56
+ ' src="$1"',
57
+ ' dest="$2"',
58
+ ' dest_dir="$(dirname "$dest")"',
59
+ ' mkdir -p "$dest_dir"',
60
+ ' if [ -f "$dest" ] && cmp -s "$src" "$dest"; then',
61
+ ' printf "%s\\n" "unchanged: $dest"',
62
+ " return 0",
63
+ " fi",
64
+ ' if [ -f "$dest" ]; then',
65
+ ' backup="$dest.$(date +%Y%m%d-%H%M%S).bak"',
66
+ ' cp "$dest" "$backup"',
67
+ ' printf "%s\\n" "backup: $backup"',
68
+ " fi",
69
+ ' mv "$src" "$dest"',
70
+ ' printf "%s\\n" "updated: $dest"',
71
+ "}",
72
+ `install_file "$tmpdir/config.toml" "${targetDir}/config.toml"`,
73
+ `install_file "$tmpdir/auth.json" "${targetDir}/auth.json"`,
74
+ 'printf "%s\\n" "Codex setup applied."',
75
+ "",
76
+ ].join("\n");
77
+ }
78
+ function buildHeredocDelimiter(prefix, content) {
79
+ const digest = createHash("sha256").update(content).digest("hex").slice(0, 16).toUpperCase();
80
+ return `${prefix}_${digest}`;
81
+ }
82
+ function escapeShellSingleQuote(value) {
83
+ return value.replace(/'/g, `'\\''`);
84
+ }
85
+ function tomlString(value) {
86
+ return JSON.stringify(value);
87
+ }
@@ -0,0 +1,30 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildCodexConfigFiles, buildCodexConfigSetupScript, buildCodexSetupCurlCommand, buildCodexSetupEndpointUrl, } from "./codex-setup.js";
4
+ test("buildCodexSetupEndpointUrl strips the public /v1 suffix", () => {
5
+ assert.equal(buildCodexSetupEndpointUrl("https://proxy.example.com/v1/"), "https://proxy.example.com/api/customer/codex/setup.sh");
6
+ });
7
+ test("buildCodexSetupCurlCommand targets the customer setup endpoint", () => {
8
+ const command = buildCodexSetupCurlCommand({
9
+ publicResponsesBaseUrl: "https://proxy.example.com/v1",
10
+ apiKey: "sk-customer-secret",
11
+ });
12
+ assert.match(command, /curl -fsSL/);
13
+ assert.match(command, /Authorization: Bearer sk-customer-secret/);
14
+ assert.match(command, /https:\/\/proxy\.example\.com\/api\/customer\/codex\/setup\.sh/);
15
+ assert.match(command, /\| sh$/);
16
+ });
17
+ test("buildCodexConfigSetupScript writes both Codex config files with backups", () => {
18
+ const files = buildCodexConfigFiles({
19
+ baseUrl: "https://proxy.example.com/v1",
20
+ apiKey: "sk-customer-secret",
21
+ model: "gpt-5.5",
22
+ });
23
+ const script = buildCodexConfigSetupScript(files);
24
+ assert.match(script, /mktemp -d/);
25
+ assert.match(script, /backup="\$dest\.\$\(date \+%Y%m%d-%H%M%S\)\.bak"/);
26
+ assert.match(script, /install_file "\$tmpdir\/config\.toml" "\$HOME\/\.codex\/config\.toml"/);
27
+ assert.match(script, /install_file "\$tmpdir\/auth\.json" "\$HOME\/\.codex\/auth\.json"/);
28
+ assert.match(script, /base_url = "https:\/\/proxy\.example\.com\/v1"/);
29
+ assert.match(script, /OPENAI_API_KEY": "sk-customer-secret"/);
30
+ });
package/dist/config.js ADDED
@@ -0,0 +1,314 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { z } from "zod";
4
+ function defaultKiroDbPath() {
5
+ return path.join(os.homedir(), ".9router", "db", "data.sqlite");
6
+ }
7
+ function parseIdList(value) {
8
+ return (value ?? "")
9
+ .split(/[,\r?\n]+/g)
10
+ .map((entry) => entry.trim())
11
+ .filter(Boolean);
12
+ }
13
+ function parseDelimitedList(value) {
14
+ return (value ?? "")
15
+ .split(/[,\r?\n]+/g)
16
+ .map((entry) => entry.trim())
17
+ .filter(Boolean);
18
+ }
19
+ const envSchema = z.object({
20
+ PORT: z.coerce.number().int().positive().default(8318),
21
+ HOST: z.string().min(1).default("0.0.0.0"),
22
+ UPSTREAM_BASE_URL: z.url(),
23
+ UPSTREAM_API_KEY: z.string().optional(),
24
+ PROVIDER_USAGE_CHECK_URL: z
25
+ .string()
26
+ .optional()
27
+ .transform((value) => (value?.trim() ? value.trim() : undefined))
28
+ .pipe(z.url().optional()),
29
+ PROVIDER_USAGE_CHECK_ENABLED: z
30
+ .string()
31
+ .optional()
32
+ .transform((value) => value !== "false"),
33
+ REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000),
34
+ SUMMARY_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(900_000),
35
+ STREAM_IDLE_TIMEOUT_MS: z.coerce.number().int().positive().default(330_000),
36
+ HERMES_EXTEND_SUMMARY_TIMEOUT: z
37
+ .string()
38
+ .optional()
39
+ .transform((value) => value !== "false"),
40
+ REQUEST_BODY_LIMIT_BYTES: z.coerce.number().int().positive().default(25 * 1024 * 1024),
41
+ HTTP_TRUST_PROXY: z
42
+ .string()
43
+ .optional()
44
+ .transform((value) => value === "true"),
45
+ HTTP_RATE_LIMIT_ENABLED: z
46
+ .string()
47
+ .optional()
48
+ .transform((value) => value !== "false"),
49
+ HTTP_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000),
50
+ HTTP_RATE_LIMIT_RESPONSES_MAX_REQUESTS: z.coerce.number().int().positive().default(120),
51
+ HTTP_RATE_LIMIT_UNAUTHENTICATED_MAX_REQUESTS: z.coerce.number().int().positive().default(20),
52
+ HTTP_RATE_LIMIT_AUTH_MAX_REQUESTS: z.coerce.number().int().positive().default(30),
53
+ HTTP_RATE_LIMIT_WEBHOOK_MAX_REQUESTS: z.coerce.number().int().positive().default(60),
54
+ HTTP_RATE_LIMIT_HEALTH_MAX_REQUESTS: z.coerce.number().int().positive().default(240),
55
+ LOG_LEVEL: z
56
+ .enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
57
+ .default("info"),
58
+ LOG_BODY: z
59
+ .string()
60
+ .optional()
61
+ .transform((value) => value === "true"),
62
+ CHATGPT_OAUTH_ENABLED: z
63
+ .string()
64
+ .optional()
65
+ .transform((value) => value === "true"),
66
+ MODEL_ROUTING_ENABLED: z
67
+ .string()
68
+ .optional()
69
+ .transform((value) => value === "true"),
70
+ MODEL_ROUTING_CHEAP_MODEL: z.string().optional().default("gpt-4o-mini"),
71
+ MODEL_ROUTING_INPUT_TOKEN_THRESHOLD: z.coerce.number().int().positive().default(2000),
72
+ MODEL_ROUTING_SKIP_IF_TOOLS: z
73
+ .string()
74
+ .optional()
75
+ .transform((value) => value !== "false"),
76
+ MODEL_ROUTING_SKIP_IF_IMAGES: z
77
+ .string()
78
+ .optional()
79
+ .transform((value) => value !== "false"),
80
+ MODEL_ROUTING_SKIP_IF_REASONING: z
81
+ .string()
82
+ .optional()
83
+ .transform((value) => value !== "false"),
84
+ CHATGPT_OAUTH_CLIENT_ID: z.string().min(1).default("app_EMoamEEZ73f0CkXaXp7hrann"),
85
+ CHATGPT_OAUTH_REDIRECT_URI: z
86
+ .string()
87
+ .min(1)
88
+ .default("http://localhost:1455/auth/callback"),
89
+ CHATGPT_OAUTH_CALLBACK_PORT: z.coerce.number().int().positive().default(1455),
90
+ CHATGPT_OAUTH_AUTH_URL: z
91
+ .string()
92
+ .min(1)
93
+ .default("https://auth.openai.com/oauth/authorize")
94
+ .pipe(z.url()),
95
+ CHATGPT_OAUTH_TOKEN_URL: z
96
+ .string()
97
+ .min(1)
98
+ .default("https://auth.openai.com/oauth/token")
99
+ .pipe(z.url()),
100
+ CHATGPT_OAUTH_DEVICE_USER_CODE_URL: z
101
+ .string()
102
+ .min(1)
103
+ .default("https://auth.openai.com/api/accounts/deviceauth/usercode")
104
+ .pipe(z.url()),
105
+ CHATGPT_OAUTH_DEVICE_TOKEN_URL: z
106
+ .string()
107
+ .min(1)
108
+ .default("https://auth.openai.com/api/accounts/deviceauth/token")
109
+ .pipe(z.url()),
110
+ CHATGPT_OAUTH_DEVICE_VERIFICATION_URL: z
111
+ .string()
112
+ .min(1)
113
+ .default("https://auth.openai.com/codex/device")
114
+ .pipe(z.url()),
115
+ CHATGPT_CODEX_BASE_URL: z
116
+ .string()
117
+ .min(1)
118
+ .default("https://chatgpt.com/backend-api/codex")
119
+ .pipe(z.url()),
120
+ RESPONSES_PROXY_DEFAULT_MODEL: z.string().default("gpt-5.5"),
121
+ BOT_PUBLIC_RESPONSES_BASE_URL: z.string().optional(),
122
+ CHATGPT_OAUTH_REFRESH_LEAD_DAYS: z.coerce.number().positive().default(5),
123
+ OPENCLAW_TOKEN_OPTIMIZATION_ENABLED: z
124
+ .string()
125
+ .optional()
126
+ .transform((value) => value !== "false"),
127
+ OPENCLAW_DEFAULT_REASONING_SUMMARY: z
128
+ .enum(["auto", "none", "concise", "detailed"])
129
+ .default("auto"),
130
+ OPENCLAW_DEFAULT_REASONING_EFFORT: z
131
+ .enum(["minimal", "low", "medium", "high"])
132
+ .default("low"),
133
+ OPENCLAW_DEFAULT_TEXT_VERBOSITY: z
134
+ .enum(["low", "medium", "high"])
135
+ .default("low"),
136
+ OPENCLAW_DEFAULT_MAX_OUTPUT_TOKENS: z
137
+ .string()
138
+ .optional()
139
+ .transform((value) => {
140
+ if (!value?.trim()) {
141
+ return undefined;
142
+ }
143
+ const parsed = Number(value);
144
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
145
+ }),
146
+ OPENCLAW_AUTO_PROMPT_CACHE_KEY: z
147
+ .string()
148
+ .optional()
149
+ .transform((value) => value !== "false"),
150
+ OPENCLAW_PROMPT_CACHE_RETENTION: z.string().min(1).default("24h"),
151
+ RESPONSE_CACHE_ENABLED: z
152
+ .string()
153
+ .optional()
154
+ .transform((value) => value === "true"),
155
+ RESPONSE_CACHE_TTL_MS: z.coerce.number().int().positive().default(5 * 60 * 1000),
156
+ RESPONSE_CACHE_MAX_PAYLOAD_BYTES: z.coerce.number().int().positive().default(512 * 1024),
157
+ PROVIDER_PROMPT_CACHE_REDESIGN_ENABLED: z
158
+ .string()
159
+ .optional()
160
+ .transform((value) => value === "true"),
161
+ PROVIDER_PROMPT_CACHE_STABLE_SUMMARIZATION_ENABLED: z
162
+ .string()
163
+ .optional()
164
+ .transform((value) => value === "true"),
165
+ PROVIDER_PROMPT_CACHE_INFLIGHT_DEDUPE_ENABLED: z
166
+ .string()
167
+ .optional()
168
+ .transform((value) => value !== "false"),
169
+ PROVIDER_PROMPT_CACHE_RETENTION_BY_FAMILY_ENABLED: z
170
+ .string()
171
+ .optional()
172
+ .transform((value) => value === "true"),
173
+ PROVIDER_PROMPT_CACHE_SUMMARY_TRIGGER_ITEMS: z.coerce.number().int().positive().default(14),
174
+ PROVIDER_PROMPT_CACHE_SUMMARY_KEEP_RECENT_ITEMS: z.coerce.number().int().positive().default(6),
175
+ PROVIDER_PROMPT_CACHE_RETENTION_BY_FAMILY: z
176
+ .string()
177
+ .optional()
178
+ .transform(parsePromptCacheFamilyRetentionRules),
179
+ PROVIDER_PROMPT_CACHE_RETENTION_BY_STATIC_KEY_ENABLED: z
180
+ .string()
181
+ .optional()
182
+ .transform((value) => value === "true"),
183
+ PROVIDER_PROMPT_CACHE_RETENTION_BY_STATIC_KEY: z
184
+ .string()
185
+ .optional()
186
+ .transform(parsePromptCacheFamilyRetentionRules),
187
+ RTK_LAYER_ENABLED: z
188
+ .string()
189
+ .optional()
190
+ .transform((value) => value === "true"),
191
+ RTK_LAYER_TOOL_OUTPUT_ENABLED: z
192
+ .string()
193
+ .optional()
194
+ .transform((value) => value !== "false"),
195
+ RTK_LAYER_TOOL_OUTPUT_MAX_CHARS: z.coerce.number().int().positive().default(4000),
196
+ RTK_LAYER_TOOL_OUTPUT_MAX_LINES: z.coerce.number().int().positive().default(120),
197
+ RTK_LAYER_TOOL_OUTPUT_TAIL_LINES: z.coerce.number().int().nonnegative().default(0),
198
+ RTK_LAYER_TOOL_OUTPUT_TAIL_CHARS: z.coerce.number().int().nonnegative().default(0),
199
+ RTK_LAYER_TOOL_OUTPUT_DETECT_FORMAT: z
200
+ .enum(["auto", "plain", "json", "stack", "command"])
201
+ .default("auto"),
202
+ OPENCLAW_DEFAULT_TRUNCATION: z.enum(["auto", "disabled"]).default("auto"),
203
+ MAX_OUTPUT_TOKENS_PARAMETER_MODE_FOR_PROVIDER: z
204
+ .enum(["forward", "strip", "rename"])
205
+ .optional(),
206
+ MAX_OUTPUT_TOKENS_PARAMETER_TARGET_FOR_PROVIDER: z.string().optional(),
207
+ STRIP_MAX_OUTPUT_TOKENS_FOR_PROVIDER: z
208
+ .string()
209
+ .optional()
210
+ .transform((value) => value !== "false"),
211
+ SANITIZE_REASONING_SUMMARY_FOR_PROVIDER: z
212
+ .string()
213
+ .optional()
214
+ .transform((value) => value !== "false"),
215
+ FALLBACK_ENABLED: z
216
+ .string()
217
+ .optional()
218
+ .transform((value) => value !== "false"),
219
+ FALLBACK_STATUS_CODES: z
220
+ .string()
221
+ .default("429,500,502,503,504")
222
+ .transform(parseFallbackStatusCodes),
223
+ RESPONSES_PROXY_CLIENT_API_KEY: z.string().optional(),
224
+ TELEGRAM_BOT_TOKEN: z.string().optional(),
225
+ TELEGRAM_OWNER_USER_IDS: z.string().optional().transform(parseIdList),
226
+ TELEGRAM_ADMIN_USER_IDS: z.string().optional().transform(parseIdList),
227
+ DASHBOARD_AUTH_OTP_TTL_MS: z.coerce.number().int().positive().default(10 * 60 * 1000),
228
+ DASHBOARD_AUTH_SESSION_TTL_MS: z.coerce.number().int().positive().default(12 * 60 * 60 * 1000),
229
+ APP_DB_PATH: z.string().min(1).default("./logs/app.sqlite"),
230
+ CUSTOMER_KEY_DB_PATH: z.string().min(1).default("./logs/telegram-bot.sqlite"),
231
+ SESSION_LOG_DIR: z.string().min(1).default("./logs/sessions"),
232
+ SESSION_LOG_RETENTION_DAYS: z.coerce.number().int().nonnegative().default(14),
233
+ SEPAY_WEBHOOK_ENABLED: z
234
+ .string()
235
+ .optional()
236
+ .transform((value) => {
237
+ const normalized = value?.trim().toLowerCase();
238
+ return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
239
+ }),
240
+ SEPAY_WEBHOOK_SECRET: z.string().optional(),
241
+ SEPAY_WEBHOOK_ALLOWED_IPS: z.string().optional().transform(parseDelimitedList),
242
+ KIRO_ENABLED: z
243
+ .string()
244
+ .optional()
245
+ .transform((value) => value === "true"),
246
+ KIRO_DB_PATH: z
247
+ .string()
248
+ .optional()
249
+ .transform((value) => (value?.trim() ? value.trim() : defaultKiroDbPath())),
250
+ KIRO_DEFAULT_REGION: z.string().min(1).default("us-east-1"),
251
+ KIRO_REFRESH_LEAD_SECONDS: z.coerce.number().int().nonnegative().default(120),
252
+ KIRO_WRITE_BACK_ENABLED: z
253
+ .string()
254
+ .optional()
255
+ .transform((value) => value !== "false"),
256
+ KIRO_DEVICE_CLIENT_NAME: z.string().min(1).default("responses-proxy"),
257
+ KIRO_BUILDER_ID_START_URL: z.string().min(1).default("https://view.awsapps.com/start"),
258
+ KIRO_DEVICE_SCOPES: z
259
+ .string()
260
+ .min(1)
261
+ .default("codewhisperer:completions,codewhisperer:analysis")
262
+ .transform((v) => v.split(",").map((s) => s.trim()).filter(Boolean)),
263
+ // Routing System Configuration
264
+ ROUTING_HEALTH_CHECK_INTERVAL: z.coerce.number().int().positive().default(30000), // 30 seconds
265
+ ROUTING_WEBSOCKET_BROADCAST_INTERVAL: z.coerce.number().int().positive().default(5000), // 5 seconds
266
+ ROUTING_PROVIDER_HEALTH_CACHE_TTL: z.coerce.number().int().positive().default(60000), // 1 minute
267
+ ROUTING_HEALTH_SCORE_THRESHOLD: z.coerce.number().int().min(0).max(100).default(50), // Minimum eligibility score
268
+ ROUTING_MAX_FALLBACK_DELAY: z.coerce.number().int().nonnegative().default(10000), // 10 seconds max delay
269
+ ROUTING_ENABLED: z
270
+ .string()
271
+ .optional()
272
+ .transform((value) => value !== "false"), // Enable routing system by default
273
+ });
274
+ export function readConfig(env) {
275
+ const parsed = envSchema.parse(env);
276
+ const base = parsed.UPSTREAM_BASE_URL.replace(/\/+$/, "");
277
+ const publicResponsesBaseUrl = parsed.BOT_PUBLIC_RESPONSES_BASE_URL?.trim().replace(/\/+$/, "") ||
278
+ `http://127.0.0.1:${parsed.PORT}/v1`;
279
+ return {
280
+ ...parsed,
281
+ upstreamResponsesUrl: `${base}/responses`,
282
+ publicResponsesBaseUrl,
283
+ };
284
+ }
285
+ function parsePromptCacheFamilyRetentionRules(raw) {
286
+ if (!raw?.trim()) {
287
+ return [];
288
+ }
289
+ return raw
290
+ .split(",")
291
+ .map((entry) => {
292
+ const separatorIndex = entry.indexOf("=");
293
+ if (separatorIndex <= 0) {
294
+ return undefined;
295
+ }
296
+ const prefix = entry.slice(0, separatorIndex).trim();
297
+ const retention = entry.slice(separatorIndex + 1).trim();
298
+ if (!prefix || !retention) {
299
+ return undefined;
300
+ }
301
+ return {
302
+ prefix,
303
+ retention,
304
+ };
305
+ })
306
+ .filter((entry) => Boolean(entry));
307
+ }
308
+ function parseFallbackStatusCodes(raw) {
309
+ const codes = raw
310
+ .split(",")
311
+ .map((part) => Number(part.trim()))
312
+ .filter((value) => Number.isInteger(value) && value >= 400 && value <= 599);
313
+ return codes.length > 0 ? codes : [429, 500, 502, 503, 504];
314
+ }
@@ -0,0 +1,31 @@
1
+ export function buildCostSummary(observations) {
2
+ const total = observations.length;
3
+ if (total === 0) {
4
+ const now = new Date().toISOString();
5
+ return {
6
+ window: { from: now, to: now },
7
+ totalRequests: 0,
8
+ promptCacheHits: 0,
9
+ promptCacheHitRate: 0,
10
+ avgCacheSavedPercent: 0,
11
+ estimatedTokensSaved: 0,
12
+ };
13
+ }
14
+ const hits = observations.filter((observation) => observation.cacheHit === true).length;
15
+ const savedPcts = observations
16
+ .map((observation) => observation.cacheSavedPercent)
17
+ .filter((value) => typeof value === "number");
18
+ const avgSaved = savedPcts.length > 0 ? savedPcts.reduce((left, right) => left + right, 0) / savedPcts.length : 0;
19
+ const tokensSaved = observations
20
+ .map((observation) => observation.cachedTokens ?? 0)
21
+ .reduce((left, right) => left + right, 0);
22
+ const timestamps = observations.map((observation) => observation.timestamp).sort();
23
+ return {
24
+ window: { from: timestamps[0] ?? "", to: timestamps[timestamps.length - 1] ?? "" },
25
+ totalRequests: total,
26
+ promptCacheHits: hits,
27
+ promptCacheHitRate: total > 0 ? hits / total : 0,
28
+ avgCacheSavedPercent: avgSaved,
29
+ estimatedTokensSaved: tokensSaved,
30
+ };
31
+ }
@@ -0,0 +1,38 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { buildCostSummary } from "./cost-analytics.js";
4
+ test("buildCostSummary aggregates prompt cache observations", () => {
5
+ const summary = buildCostSummary([
6
+ {
7
+ cacheHit: true,
8
+ cacheSavedPercent: 50,
9
+ cachedTokens: 100,
10
+ timestamp: "2026-05-11T00:02:00.000Z",
11
+ },
12
+ {
13
+ cacheHit: false,
14
+ cacheSavedPercent: 10,
15
+ cachedTokens: 0,
16
+ timestamp: "2026-05-11T00:01:00.000Z",
17
+ },
18
+ ]);
19
+ assert.deepEqual(summary.window, {
20
+ from: "2026-05-11T00:01:00.000Z",
21
+ to: "2026-05-11T00:02:00.000Z",
22
+ });
23
+ assert.equal(summary.totalRequests, 2);
24
+ assert.equal(summary.promptCacheHits, 1);
25
+ assert.equal(summary.promptCacheHitRate, 0.5);
26
+ assert.equal(summary.avgCacheSavedPercent, 30);
27
+ assert.equal(summary.estimatedTokensSaved, 100);
28
+ });
29
+ test("buildCostSummary returns zero summary for empty observations", () => {
30
+ const summary = buildCostSummary([]);
31
+ assert.equal(summary.totalRequests, 0);
32
+ assert.equal(summary.promptCacheHits, 0);
33
+ assert.equal(summary.promptCacheHitRate, 0);
34
+ assert.equal(summary.avgCacheSavedPercent, 0);
35
+ assert.equal(summary.estimatedTokensSaved, 0);
36
+ assert.match(summary.window.from, /^\d{4}-\d{2}-\d{2}T/);
37
+ assert.equal(summary.window.to, summary.window.from);
38
+ });