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,345 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { homedir } from "node:os";
5
+ export function resolveQuickApplyPaths(overrides) {
6
+ const home = homedir();
7
+ const codexConfigPath = overrides?.codexConfigPath?.trim() || `${home}/.codex/config.toml`;
8
+ const codexAuthPath = overrides?.codexAuthPath?.trim() ||
9
+ (overrides?.codexConfigPath?.trim() ? join(dirname(codexConfigPath), "auth.json") : `${home}/.codex/auth.json`);
10
+ return {
11
+ hermesConfigPath: overrides?.hermesConfigPath?.trim() || `${home}/.hermes/config.yaml`,
12
+ codexConfigPath,
13
+ codexAuthPath,
14
+ backupDir: overrides?.backupDir?.trim() || `${home}/.responses-proxy/client-config-backups`,
15
+ };
16
+ }
17
+ export function generateRouteApiKey(client) {
18
+ return `sk-${client}-route-${randomBytes(12).toString("hex")}`;
19
+ }
20
+ export function normalizeProxyBaseUrl(value) {
21
+ const trimmed = value?.trim() || "";
22
+ if (!trimmed) {
23
+ return "";
24
+ }
25
+ return trimmed.replace(/\/+$/, "");
26
+ }
27
+ export function readQuickApplyStatus(raw, config, path) {
28
+ if (config.client === "hermes") {
29
+ return readHermesStatus(raw, config, path);
30
+ }
31
+ return readCodexStatus(raw, config, path);
32
+ }
33
+ export function applyQuickConfig(raw, config) {
34
+ if (config.client === "hermes") {
35
+ return applyHermesConfig(raw, config);
36
+ }
37
+ return applyCodexConfig(raw, config);
38
+ }
39
+ export function readCodexAuthStatus(raw, routeApiKey, path, options) {
40
+ const data = parseJsonObject(raw);
41
+ const detectedApiKey = typeof data.OPENAI_API_KEY === "string" ? data.OPENAI_API_KEY : null;
42
+ return {
43
+ path,
44
+ exists: typeof raw === "string",
45
+ configured: detectedApiKey === routeApiKey,
46
+ detectedApiKey,
47
+ backups: listRecentConfigBackups(path, 1, options),
48
+ };
49
+ }
50
+ export function applyCodexAuth(raw, routeApiKey) {
51
+ const data = parseJsonObject(raw);
52
+ data.OPENAI_API_KEY = routeApiKey;
53
+ return `${JSON.stringify(data, null, 2)}\n`;
54
+ }
55
+ export function writeQuickConfigFile(filePath, nextRaw, options) {
56
+ mkdirSync(dirname(filePath), { recursive: true });
57
+ if (existsSync(filePath)) {
58
+ const currentRaw = readFileSync(filePath, "utf8");
59
+ if (currentRaw === nextRaw) {
60
+ return { changed: false, backupCreated: false };
61
+ }
62
+ const backupPath = buildTimestampedBackupPath(filePath, new Date(), options?.backupDir);
63
+ mkdirSync(dirname(backupPath), { recursive: true });
64
+ writeFileSync(backupPath, currentRaw, "utf8");
65
+ writeFileSync(filePath, nextRaw, "utf8");
66
+ return { changed: true, backupCreated: true, backupPath };
67
+ }
68
+ writeFileSync(filePath, nextRaw, "utf8");
69
+ return { changed: true, backupCreated: false };
70
+ }
71
+ export function readQuickConfigFile(filePath) {
72
+ try {
73
+ return readFileSync(filePath, "utf8");
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ }
79
+ export function listRecentConfigBackups(filePath, limit = 5, options) {
80
+ try {
81
+ const directory = options?.backupDir?.trim() || dirname(filePath);
82
+ const fileName = basename(filePath);
83
+ return readdirSync(directory)
84
+ .filter((entry) => entry.startsWith(`${fileName}.`) && entry.endsWith(".bak"))
85
+ .map((entry) => {
86
+ const fullPath = `${directory}/${entry}`;
87
+ const stats = statSync(fullPath);
88
+ return {
89
+ path: fullPath,
90
+ fileName: entry,
91
+ modifiedAt: stats.mtime.toISOString(),
92
+ sizeBytes: stats.size,
93
+ };
94
+ })
95
+ .sort((left, right) => right.modifiedAt.localeCompare(left.modifiedAt))
96
+ .slice(0, limit);
97
+ }
98
+ catch {
99
+ return [];
100
+ }
101
+ }
102
+ function readHermesStatus(raw, config, path) {
103
+ const detected = {
104
+ model: readYamlSectionKey(raw, "model", "default"),
105
+ provider: readYamlSectionKey(raw, "model", "provider"),
106
+ apiKey: readYamlSectionKey(raw, "model", "api_key"),
107
+ baseUrl: readYamlSectionKey(raw, "model", "base_url"),
108
+ apiMode: readYamlSectionKey(raw, "model", "api_mode"),
109
+ };
110
+ return {
111
+ client: "hermes",
112
+ path,
113
+ exists: typeof raw === "string",
114
+ configured: (!config.model?.trim() || detected.model === config.model.trim()) &&
115
+ detected.provider === "custom" &&
116
+ normalizeProxyBaseUrl(detected.baseUrl) === normalizeProxyBaseUrl(config.proxyBaseUrl) &&
117
+ detected.apiKey === config.routeApiKey &&
118
+ detected.apiMode === "codex_responses",
119
+ routeApiKey: config.routeApiKey,
120
+ detected,
121
+ };
122
+ }
123
+ function readCodexStatus(raw, config, path) {
124
+ const providerName = readTomlTopLevelString(raw, "model_provider");
125
+ const section = providerName ? readTomlSection(raw, `model_providers.${providerName}`) : undefined;
126
+ const detected = {
127
+ model: readTomlTopLevelString(raw, "model") ?? null,
128
+ modelProvider: providerName ?? null,
129
+ providerName: section ? readTomlString(section, "name") ?? providerName ?? null : null,
130
+ apiKey: section ? readTomlString(section, "api_key") ?? null : null,
131
+ baseUrl: section ? readTomlString(section, "base_url") ?? null : null,
132
+ wireApi: section ? readTomlString(section, "wire_api") ?? null : null,
133
+ };
134
+ return {
135
+ client: "codex",
136
+ path,
137
+ exists: typeof raw === "string",
138
+ configured: (!config.model?.trim() || detected.model === config.model.trim()) &&
139
+ detected.modelProvider === "responses_proxy" &&
140
+ normalizeProxyBaseUrl(detected.baseUrl) === normalizeProxyBaseUrl(config.proxyBaseUrl) &&
141
+ detected.apiKey === config.routeApiKey &&
142
+ detected.wireApi === "responses",
143
+ routeApiKey: config.routeApiKey,
144
+ detected,
145
+ };
146
+ }
147
+ export function applyHermesConfig(raw, config) {
148
+ const currentModel = readYamlSectionKey(raw, "model", "default");
149
+ const nextModel = config.model?.trim() || currentModel || "gpt-5.4";
150
+ const nextBaseUrl = normalizeProxyBaseUrl(config.proxyBaseUrl);
151
+ let nextRaw = typeof raw === "string" && raw.trim() ? raw : "";
152
+ nextRaw = upsertYamlSectionKey(nextRaw, "model", "default", nextModel);
153
+ nextRaw = upsertYamlSectionKey(nextRaw, "model", "provider", "custom");
154
+ nextRaw = upsertYamlSectionKey(nextRaw, "model", "api_key", config.routeApiKey);
155
+ nextRaw = upsertYamlSectionKey(nextRaw, "model", "base_url", nextBaseUrl);
156
+ nextRaw = upsertYamlSectionKey(nextRaw, "model", "api_mode", "codex_responses");
157
+ return ensureTrailingNewline(nextRaw);
158
+ }
159
+ export function applyCodexConfig(raw, config) {
160
+ const currentModel = readTomlTopLevelString(raw, "model");
161
+ const nextModel = config.model?.trim() || currentModel || "gpt-5.4";
162
+ const nextBaseUrl = normalizeProxyBaseUrl(config.proxyBaseUrl);
163
+ let nextRaw = typeof raw === "string" && raw.trim() ? raw : "";
164
+ nextRaw = upsertTomlTopLevelString(nextRaw, "model", nextModel);
165
+ nextRaw = upsertTomlTopLevelString(nextRaw, "model_provider", "responses_proxy");
166
+ nextRaw = upsertTomlSectionString(nextRaw, "model_providers.responses_proxy", "name", "responses-proxy");
167
+ nextRaw = upsertTomlSectionString(nextRaw, "model_providers.responses_proxy", "base_url", nextBaseUrl);
168
+ nextRaw = upsertTomlSectionString(nextRaw, "model_providers.responses_proxy", "api_key", config.routeApiKey);
169
+ nextRaw = upsertTomlSectionString(nextRaw, "model_providers.responses_proxy", "wire_api", "responses");
170
+ return ensureTrailingNewline(nextRaw);
171
+ }
172
+ function upsertYamlSectionKey(raw, section, key, value) {
173
+ const encodedValue = encodeYamlScalar(value);
174
+ const lines = raw ? raw.split("\n") : [];
175
+ const bounds = findYamlSectionBounds(lines, section);
176
+ if (!bounds) {
177
+ const prefix = raw.trim() ? `${raw.replace(/\s*$/, "\n\n")}` : "";
178
+ return `${prefix}${section}:\n ${key}: ${encodedValue}\n`;
179
+ }
180
+ const nextLines = [...lines];
181
+ let updated = false;
182
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
183
+ if (new RegExp(`^\\s{2}${escapeRegExp(key)}:`).test(nextLines[index])) {
184
+ nextLines[index] = ` ${key}: ${encodedValue}`;
185
+ updated = true;
186
+ break;
187
+ }
188
+ }
189
+ if (!updated) {
190
+ nextLines.splice(bounds.end, 0, ` ${key}: ${encodedValue}`);
191
+ }
192
+ return nextLines.join("\n");
193
+ }
194
+ function readYamlSectionKey(raw, section, key) {
195
+ if (!raw) {
196
+ return null;
197
+ }
198
+ const lines = raw.split("\n");
199
+ const bounds = findYamlSectionBounds(lines, section);
200
+ if (!bounds) {
201
+ return null;
202
+ }
203
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
204
+ const match = lines[index].match(new RegExp(`^\\s{2}${escapeRegExp(key)}:\\s*(.+?)\\s*$`));
205
+ if (match?.[1]) {
206
+ return stripYamlQuotes(match[1].trim());
207
+ }
208
+ }
209
+ return null;
210
+ }
211
+ function encodeYamlScalar(value) {
212
+ if (/^[A-Za-z0-9_./:-]+$/.test(value)) {
213
+ return value;
214
+ }
215
+ return JSON.stringify(value);
216
+ }
217
+ function stripYamlQuotes(value) {
218
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
219
+ return value.slice(1, -1);
220
+ }
221
+ return value;
222
+ }
223
+ function upsertTomlTopLevelString(raw, key, value) {
224
+ const encodedValue = JSON.stringify(value);
225
+ const pattern = new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"[^\"]*\"\\s*$`, "m");
226
+ if (pattern.test(raw)) {
227
+ return raw.replace(pattern, `${key} = ${encodedValue}`);
228
+ }
229
+ const trimmed = raw.trim();
230
+ if (!trimmed) {
231
+ return `${key} = ${encodedValue}\n`;
232
+ }
233
+ return `${key} = ${encodedValue}\n${raw}`;
234
+ }
235
+ function upsertTomlSectionString(raw, sectionName, key, value) {
236
+ const encodedValue = JSON.stringify(value);
237
+ const lines = raw ? raw.split("\n") : [];
238
+ const bounds = findTomlSectionBounds(lines, sectionName);
239
+ if (!bounds) {
240
+ const prefix = raw.trim() ? `${raw.replace(/\s*$/, "\n\n")}` : "";
241
+ return `${prefix}[${sectionName}]\n${key} = ${encodedValue}\n`;
242
+ }
243
+ const nextLines = [...lines];
244
+ let updated = false;
245
+ for (let index = bounds.start + 1; index < bounds.end; index += 1) {
246
+ if (new RegExp(`^${escapeRegExp(key)}\\s*=`).test(nextLines[index])) {
247
+ nextLines[index] = `${key} = ${encodedValue}`;
248
+ updated = true;
249
+ break;
250
+ }
251
+ }
252
+ if (!updated) {
253
+ nextLines.splice(bounds.end, 0, `${key} = ${encodedValue}`);
254
+ }
255
+ return nextLines.join("\n");
256
+ }
257
+ function readTomlTopLevelString(raw, key) {
258
+ if (!raw) {
259
+ return undefined;
260
+ }
261
+ const match = raw.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
262
+ return match?.[1];
263
+ }
264
+ function readTomlSection(raw, sectionName) {
265
+ if (!raw) {
266
+ return undefined;
267
+ }
268
+ const lines = raw.split("\n");
269
+ const bounds = findTomlSectionBounds(lines, sectionName);
270
+ if (!bounds) {
271
+ return undefined;
272
+ }
273
+ return lines.slice(bounds.start + 1, bounds.end).join("\n");
274
+ }
275
+ function readTomlString(rawSection, key) {
276
+ const match = rawSection.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
277
+ return match?.[1];
278
+ }
279
+ function parseJsonObject(raw) {
280
+ if (!raw?.trim()) {
281
+ return {};
282
+ }
283
+ try {
284
+ const parsed = JSON.parse(raw);
285
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
286
+ ? { ...parsed }
287
+ : {};
288
+ }
289
+ catch {
290
+ return {};
291
+ }
292
+ }
293
+ function ensureTrailingNewline(raw) {
294
+ return raw.endsWith("\n") ? raw : `${raw}\n`;
295
+ }
296
+ function findYamlSectionBounds(lines, section) {
297
+ const start = lines.findIndex((line) => line.trim() === `${section}:`);
298
+ if (start === -1) {
299
+ return undefined;
300
+ }
301
+ let end = lines.length;
302
+ for (let index = start + 1; index < lines.length; index += 1) {
303
+ const line = lines[index];
304
+ if (line && !/^\s/.test(line)) {
305
+ end = index;
306
+ break;
307
+ }
308
+ }
309
+ return { start, end };
310
+ }
311
+ function findTomlSectionBounds(lines, sectionName) {
312
+ const start = lines.findIndex((line) => line.trim() === `[${sectionName}]`);
313
+ if (start === -1) {
314
+ return undefined;
315
+ }
316
+ let end = lines.length;
317
+ for (let index = start + 1; index < lines.length; index += 1) {
318
+ if (lines[index].trim().startsWith("[") && lines[index].trim().endsWith("]")) {
319
+ end = index;
320
+ break;
321
+ }
322
+ }
323
+ return { start, end };
324
+ }
325
+ function buildTimestampedBackupPath(filePath, date, backupDir) {
326
+ const pad = (value) => String(value).padStart(2, "0");
327
+ const padMs = (value) => String(value).padStart(3, "0");
328
+ const timestamp = [
329
+ date.getFullYear(),
330
+ pad(date.getMonth() + 1),
331
+ pad(date.getDate()),
332
+ "-",
333
+ pad(date.getHours()),
334
+ pad(date.getMinutes()),
335
+ pad(date.getSeconds()),
336
+ "-",
337
+ padMs(date.getMilliseconds()),
338
+ ].join("");
339
+ const fileName = basename(filePath);
340
+ const targetDir = backupDir?.trim() || dirname(filePath);
341
+ return `${targetDir}/${fileName}.${timestamp}.bak`;
342
+ }
343
+ function escapeRegExp(value) {
344
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
+ }
@@ -0,0 +1,185 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, readdirSync, rmSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import { applyCodexAuth, applyCodexConfig, applyHermesConfig, normalizeProxyBaseUrl, readCodexAuthStatus, readQuickApplyStatus, resolveQuickApplyPaths, writeQuickConfigFile, } from "./client-config-apply.js";
7
+ test("applyHermesConfig points Hermes to proxy while preserving unrelated sections", () => {
8
+ const raw = [
9
+ "model:",
10
+ " default: gpt-5.5",
11
+ " provider: custom",
12
+ " api_key: old-key",
13
+ "browser:",
14
+ " inactivity_timeout: 120",
15
+ "",
16
+ ].join("\n");
17
+ const next = applyHermesConfig(raw, {
18
+ client: "hermes",
19
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
20
+ routeApiKey: "sk-hermes-route-abc",
21
+ });
22
+ assert.match(next, /model:\n default: gpt-5\.5\n provider: custom\n api_key: sk-hermes-route-abc\n base_url: http:\/\/127\.0\.0\.1:8318\/v1\n api_mode: codex_responses\n/);
23
+ assert.match(next, /browser:\n inactivity_timeout: 120/);
24
+ });
25
+ test("readQuickApplyStatus rejects Hermes config when it contains another client's route API key", () => {
26
+ const raw = [
27
+ "model:",
28
+ " default: gpt-5.4",
29
+ " provider: custom",
30
+ " api_key: sk-codex-route-abc",
31
+ " base_url: http://127.0.0.1:8318/v1",
32
+ " api_mode: codex_responses",
33
+ "",
34
+ ].join("\n");
35
+ const status = readQuickApplyStatus(raw, {
36
+ client: "hermes",
37
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
38
+ routeApiKey: "sk-hermes-route-abc",
39
+ }, "/tmp/hermes-config.yaml");
40
+ assert.equal(status.configured, false);
41
+ assert.equal(status.detected.apiKey, "sk-codex-route-abc");
42
+ assert.equal(status.routeApiKey, "sk-hermes-route-abc");
43
+ });
44
+ test("readQuickApplyStatus rejects Hermes config when the configured model differs", () => {
45
+ const raw = [
46
+ "model:",
47
+ " default: cx/gpt-5.4",
48
+ " provider: custom",
49
+ " api_key: sk-hermes-route-abc",
50
+ " base_url: http://127.0.0.1:8318/v1",
51
+ " api_mode: codex_responses",
52
+ "",
53
+ ].join("\n");
54
+ const status = readQuickApplyStatus(raw, {
55
+ client: "hermes",
56
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
57
+ routeApiKey: "sk-hermes-route-abc",
58
+ model: "cx/gpt-5.5",
59
+ }, "/tmp/hermes-config.yaml");
60
+ assert.equal(status.configured, false);
61
+ assert.equal(status.detected.model, "cx/gpt-5.4");
62
+ });
63
+ test("applyCodexConfig switches active provider to responses_proxy and preserves project sections", () => {
64
+ const raw = [
65
+ 'model = "gpt-5.4"',
66
+ 'model_provider = "cliproxy"',
67
+ "",
68
+ "[model_providers.cliproxy]",
69
+ 'name = "cliproxy"',
70
+ 'base_url = "http://100.111.102.37:8317/v1"',
71
+ 'api_key = "sk-old"',
72
+ 'wire_api = "responses"',
73
+ "",
74
+ '[projects."/Volumes/Home_EX/Projects/responses-proxy"]',
75
+ 'trust_level = "trusted"',
76
+ "",
77
+ ].join("\n");
78
+ const next = applyCodexConfig(raw, {
79
+ client: "codex",
80
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
81
+ routeApiKey: "sk-codex-route-abc",
82
+ });
83
+ assert.match(next, /^model = "gpt-5\.4"$/m);
84
+ assert.match(next, /^model_provider = "responses_proxy"$/m);
85
+ assert.match(next, /\[model_providers\.responses_proxy\][\s\S]*base_url = "http:\/\/127\.0\.0\.1:8318\/v1"[\s\S]*api_key = "sk-codex-route-abc"[\s\S]*wire_api = "responses"/m);
86
+ assert.match(next, /\[projects\."\/Volumes\/Home_EX\/Projects\/responses-proxy"\]\ntrust_level = "trusted"/);
87
+ });
88
+ test("applyCodexConfig uses the selected helper model when provided", () => {
89
+ const next = applyCodexConfig('model = "gpt-5.5"\n', {
90
+ client: "codex",
91
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
92
+ routeApiKey: "sk-codex-route-abc",
93
+ model: "gpt-5.4",
94
+ });
95
+ assert.match(next, /^model = "gpt-5\.4"$/m);
96
+ });
97
+ test("applyCodexAuth stores the selected route API key in auth.json", () => {
98
+ const next = applyCodexAuth('{\n "OPENAI_API_KEY": "sk-old"\n}\n', "sk-codex-route-abc");
99
+ assert.equal(JSON.parse(next).OPENAI_API_KEY, "sk-codex-route-abc");
100
+ });
101
+ test("readQuickApplyStatus marks Hermes as configured only when route key and base URL match", () => {
102
+ const raw = [
103
+ "model:",
104
+ " default: cx/gpt-5.4",
105
+ " provider: custom",
106
+ " api_key: sk-hermes-route-abc",
107
+ " base_url: http://127.0.0.1:8318/v1",
108
+ " api_mode: codex_responses",
109
+ "",
110
+ ].join("\n");
111
+ const status = readQuickApplyStatus(raw, {
112
+ client: "hermes",
113
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
114
+ routeApiKey: "sk-hermes-route-abc",
115
+ }, "/tmp/hermes.yaml");
116
+ assert.equal(status.configured, true);
117
+ assert.equal(status.detected.baseUrl, "http://127.0.0.1:8318/v1");
118
+ assert.equal(status.detected.apiKey, "sk-hermes-route-abc");
119
+ });
120
+ test("readCodexAuthStatus detects when auth.json already matches the route key", () => {
121
+ const status = readCodexAuthStatus('{\n "OPENAI_API_KEY": "sk-codex-route-abc"\n}\n', "sk-codex-route-abc", "/tmp/auth.json");
122
+ assert.equal(status.configured, true);
123
+ assert.equal(status.detectedApiKey, "sk-codex-route-abc");
124
+ });
125
+ test("writeQuickConfigFile stores backup in configured backup directory", () => {
126
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-quick-apply-"));
127
+ const configFile = path.join(tempDir, "config.toml");
128
+ const backupDir = path.join(tempDir, "backups");
129
+ try {
130
+ writeQuickConfigFile(configFile, 'model = "gpt-5.4"\n');
131
+ writeQuickConfigFile(configFile, 'model = "gpt-5.5"\n', { backupDir });
132
+ const backupFiles = readdirSync(backupDir);
133
+ assert.equal(backupFiles.length, 1);
134
+ assert.match(backupFiles[0], /^config\.toml\.\d{8}-\d{6}-\d{3}\.bak$/);
135
+ assert.equal(readFileSync(path.join(backupDir, backupFiles[0]), "utf8"), 'model = "gpt-5.4"\n');
136
+ }
137
+ finally {
138
+ rmSync(tempDir, { recursive: true, force: true });
139
+ }
140
+ });
141
+ test("writeQuickConfigFile skips backup when content is unchanged", () => {
142
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-quick-apply-"));
143
+ const configFile = path.join(tempDir, "config.toml");
144
+ const backupDir = path.join(tempDir, "backups");
145
+ try {
146
+ const firstWrite = writeQuickConfigFile(configFile, 'model = "gpt-5.4"\n');
147
+ const secondWrite = writeQuickConfigFile(configFile, 'model = "gpt-5.4"\n', { backupDir });
148
+ assert.equal(firstWrite.changed, true);
149
+ assert.equal(firstWrite.backupCreated, false);
150
+ assert.equal(secondWrite.changed, false);
151
+ assert.equal(secondWrite.backupCreated, false);
152
+ assert.equal(readdirSync(tempDir).includes("backups"), false);
153
+ }
154
+ finally {
155
+ rmSync(tempDir, { recursive: true, force: true });
156
+ }
157
+ });
158
+ test("readQuickApplyStatus treats normalized trailing slash base URLs as configured", () => {
159
+ const raw = [
160
+ "model:",
161
+ " default: gpt-5.5",
162
+ " provider: custom",
163
+ " api_key: sk-hermes-route-abc",
164
+ " base_url: http://127.0.0.1:8318/v1/",
165
+ " api_mode: codex_responses",
166
+ "",
167
+ ].join("\n");
168
+ const status = readQuickApplyStatus(raw, {
169
+ client: "hermes",
170
+ proxyBaseUrl: "http://127.0.0.1:8318/v1",
171
+ routeApiKey: "sk-hermes-route-abc",
172
+ }, "/tmp/hermes.yaml");
173
+ assert.equal(status.configured, true);
174
+ });
175
+ test("normalizeProxyBaseUrl trims trailing slashes", () => {
176
+ assert.equal(normalizeProxyBaseUrl(" http://127.0.0.1:8318/v1/ "), "http://127.0.0.1:8318/v1");
177
+ assert.equal(normalizeProxyBaseUrl(""), "");
178
+ });
179
+ test("resolveQuickApplyPaths keeps Codex auth alongside override config path", () => {
180
+ const paths = resolveQuickApplyPaths({
181
+ codexConfigPath: "/host-home/.codex/config.toml",
182
+ });
183
+ assert.equal(paths.codexConfigPath, "/host-home/.codex/config.toml");
184
+ assert.equal(paths.codexAuthPath, "/host-home/.codex/auth.json");
185
+ });
@@ -0,0 +1,111 @@
1
+ export function resolveClientTokenWindowStart(now, config) {
2
+ const current = new Date(now);
3
+ switch (config.windowType) {
4
+ case "weekly": {
5
+ const windowStart = new Date(current);
6
+ windowStart.setUTCHours(0, 0, 0, 0);
7
+ const day = windowStart.getUTCDay();
8
+ const offset = (day + 6) % 7;
9
+ windowStart.setUTCDate(windowStart.getUTCDate() - offset);
10
+ return windowStart.toISOString();
11
+ }
12
+ case "monthly": {
13
+ return new Date(Date.UTC(current.getUTCFullYear(), current.getUTCMonth(), 1, 0, 0, 0, 0)).toISOString();
14
+ }
15
+ case "fixed": {
16
+ const sizeSeconds = config.windowSizeSeconds && config.windowSizeSeconds > 0
17
+ ? config.windowSizeSeconds
18
+ : 86400;
19
+ const epochSeconds = Math.floor(current.getTime() / 1000);
20
+ const windowStartSeconds = epochSeconds - (epochSeconds % sizeSeconds);
21
+ return new Date(windowStartSeconds * 1000).toISOString();
22
+ }
23
+ case "daily":
24
+ default: {
25
+ const windowStart = new Date(current);
26
+ windowStart.setUTCHours(0, 0, 0, 0);
27
+ return windowStart.toISOString();
28
+ }
29
+ }
30
+ }
31
+ export function getClientTokenLimitStatus(config, usage) {
32
+ const used = normalizeNonNegativeInteger(usage.totalTokens);
33
+ const windowStart = usage.windowStart;
34
+ if (!config?.enabled) {
35
+ return {
36
+ used,
37
+ limit: null,
38
+ remaining: null,
39
+ blocked: false,
40
+ windowStart,
41
+ };
42
+ }
43
+ const limit = normalizeNonNegativeInteger(config.tokenLimit);
44
+ const remaining = Math.max(0, limit - used);
45
+ return {
46
+ used,
47
+ limit,
48
+ remaining,
49
+ blocked: config.hardBlock && limit > 0 && remaining <= 0,
50
+ windowStart,
51
+ };
52
+ }
53
+ export function buildClientTokenLimitError(client, status) {
54
+ return {
55
+ statusCode: 429,
56
+ body: {
57
+ error: {
58
+ type: "request_error",
59
+ code: "CLIENT_TOKEN_LIMIT_EXCEEDED",
60
+ message: `Client route '${client}' has reached its token limit for the current window.`,
61
+ client,
62
+ client_route: client,
63
+ usage: status,
64
+ },
65
+ },
66
+ };
67
+ }
68
+ export function extractUsageTotals(usagePayload) {
69
+ const usage = resolveUsagePayload(usagePayload);
70
+ if (!usage) {
71
+ return undefined;
72
+ }
73
+ const totalTokens = readNonNegativeInteger(usage.total_tokens);
74
+ if (totalTokens === undefined) {
75
+ return undefined;
76
+ }
77
+ return {
78
+ inputTokens: readNonNegativeInteger(usage.input_tokens) ?? 0,
79
+ outputTokens: readNonNegativeInteger(usage.output_tokens) ?? 0,
80
+ totalTokens,
81
+ };
82
+ }
83
+ function resolveUsagePayload(value) {
84
+ if (!isRecord(value)) {
85
+ return undefined;
86
+ }
87
+ if ("total_tokens" in value) {
88
+ return value;
89
+ }
90
+ if (isRecord(value.usage)) {
91
+ return value.usage;
92
+ }
93
+ const response = value.response;
94
+ if (!isRecord(response)) {
95
+ return undefined;
96
+ }
97
+ return isRecord(response.usage) ? response.usage : undefined;
98
+ }
99
+ function readNonNegativeInteger(value) {
100
+ const parsed = Number(value);
101
+ if (!Number.isFinite(parsed) || parsed < 0) {
102
+ return undefined;
103
+ }
104
+ return Math.floor(parsed);
105
+ }
106
+ function normalizeNonNegativeInteger(value) {
107
+ return readNonNegativeInteger(value) ?? 0;
108
+ }
109
+ function isRecord(value) {
110
+ return typeof value === "object" && value !== null && !Array.isArray(value);
111
+ }