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,1177 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import test from "node:test";
6
+ import BetterSqlite3 from "better-sqlite3";
7
+ import { readConfig } from "./config.js";
8
+ import { buildBuiltinProviderPresets, normalizeClientRouteKey, resolveClientTokenWindowStart, RuntimeProviderRepository, } from "./runtime-provider-repository.js";
9
+ test("refreshes unmodified seeded providers from current base config", async () => {
10
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
11
+ const dbFile = path.join(tempDir, "app.sqlite");
12
+ const legacyStateFile = path.join(tempDir, "providers.json");
13
+ const initialBaseProviders = [
14
+ {
15
+ id: "krouter",
16
+ name: "krouter",
17
+ baseUrl: "https://api.krouter.net/v1",
18
+ responsesUrl: "https://api.krouter.net/v1/responses",
19
+ providerApiKeys: ["provider-key"],
20
+ clientApiKeys: ["client-key"],
21
+ capabilities: {
22
+ usageCheckEnabled: false,
23
+ stripMaxOutputTokens: false,
24
+ requestParameterPolicy: {},
25
+ sanitizeReasoningSummary: false,
26
+ stripModelPrefixes: [],
27
+ },
28
+ },
29
+ ];
30
+ const nextBaseProviders = [
31
+ {
32
+ id: "krouter",
33
+ name: "krouter",
34
+ baseUrl: "https://krouter.net/v1",
35
+ responsesUrl: "https://krouter.net/v1/responses",
36
+ providerApiKeys: ["provider-key"],
37
+ clientApiKeys: ["client-key"],
38
+ capabilities: {
39
+ usageCheckEnabled: false,
40
+ stripMaxOutputTokens: true,
41
+ requestParameterPolicy: {
42
+ maxOutputTokens: {
43
+ mode: "strip",
44
+ },
45
+ },
46
+ sanitizeReasoningSummary: false,
47
+ stripModelPrefixes: [],
48
+ },
49
+ },
50
+ ];
51
+ try {
52
+ const firstRepository = await RuntimeProviderRepository.create({
53
+ dbFile,
54
+ legacyStateFile,
55
+ baseProviders: initialBaseProviders,
56
+ });
57
+ assert.equal(firstRepository.getProvider("krouter")?.baseUrl, "https://api.krouter.net/v1");
58
+ assert.equal(firstRepository.getProvider("krouter")?.capabilities.stripMaxOutputTokens, false);
59
+ const secondRepository = await RuntimeProviderRepository.create({
60
+ dbFile,
61
+ legacyStateFile,
62
+ baseProviders: nextBaseProviders,
63
+ });
64
+ const provider = secondRepository.getProvider("krouter");
65
+ assert.equal(provider?.baseUrl, "https://krouter.net/v1");
66
+ assert.equal(provider?.responsesUrl, "https://krouter.net/v1/responses");
67
+ assert.equal(provider?.capabilities.stripMaxOutputTokens, true);
68
+ }
69
+ finally {
70
+ rmSync(tempDir, { recursive: true, force: true });
71
+ }
72
+ });
73
+ test("derives the primary built-in provider identity from the upstream host", () => {
74
+ const config = readConfig({
75
+ PORT: "8318",
76
+ HOST: "0.0.0.0",
77
+ UPSTREAM_BASE_URL: "https://krouter.net/v1",
78
+ UPSTREAM_API_KEY: "provider-key",
79
+ APP_DB_PATH: "./logs/app.sqlite",
80
+ });
81
+ const [provider] = buildBuiltinProviderPresets(config);
82
+ assert.equal(provider?.id, "krouter");
83
+ assert.equal(provider?.name, "krouter");
84
+ });
85
+ test("reads persisted client token limit configuration", async () => {
86
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
87
+ const dbFile = path.join(tempDir, "app.sqlite");
88
+ const legacyStateFile = path.join(tempDir, "providers.json");
89
+ try {
90
+ await RuntimeProviderRepository.create({
91
+ dbFile,
92
+ legacyStateFile,
93
+ baseProviders: [
94
+ {
95
+ id: "provider-a",
96
+ name: "provider-a",
97
+ baseUrl: "https://provider-a.example/v1",
98
+ responsesUrl: "https://provider-a.example/v1/responses",
99
+ providerApiKeys: ["provider-key"],
100
+ clientApiKeys: [],
101
+ capabilities: {
102
+ usageCheckEnabled: false,
103
+ stripMaxOutputTokens: false,
104
+ requestParameterPolicy: {},
105
+ sanitizeReasoningSummary: false,
106
+ stripModelPrefixes: [],
107
+ },
108
+ },
109
+ ],
110
+ });
111
+ const now = "2026-04-27T13:00:00.000Z";
112
+ const db = new BetterSqlite3(dbFile);
113
+ db.prepare(`INSERT INTO client_token_limits (
114
+ client_route, enabled, token_limit, window_type, window_size_seconds, hard_block, created_at, updated_at
115
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run("codex", 1, 500000, "daily", null, 1, now, now);
116
+ db.close();
117
+ const repository = await RuntimeProviderRepository.create({
118
+ dbFile,
119
+ legacyStateFile,
120
+ baseProviders: [],
121
+ });
122
+ assert.deepEqual(repository.getClientTokenLimit("codex"), {
123
+ clientRoute: "codex",
124
+ enabled: true,
125
+ tokenLimit: 500000,
126
+ windowType: "daily",
127
+ windowSizeSeconds: undefined,
128
+ hardBlock: true,
129
+ createdAt: now,
130
+ updatedAt: now,
131
+ });
132
+ }
133
+ finally {
134
+ rmSync(tempDir, { recursive: true, force: true });
135
+ }
136
+ });
137
+ test("upserts and deletes client token limit configuration", async () => {
138
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
139
+ const dbFile = path.join(tempDir, "app.sqlite");
140
+ const legacyStateFile = path.join(tempDir, "providers.json");
141
+ try {
142
+ const repository = await RuntimeProviderRepository.create({
143
+ dbFile,
144
+ legacyStateFile,
145
+ baseProviders: [
146
+ {
147
+ id: "provider-a",
148
+ name: "provider-a",
149
+ baseUrl: "https://provider-a.example/v1",
150
+ responsesUrl: "https://provider-a.example/v1/responses",
151
+ providerApiKeys: ["provider-key"],
152
+ clientApiKeys: [],
153
+ capabilities: {
154
+ usageCheckEnabled: false,
155
+ stripMaxOutputTokens: false,
156
+ requestParameterPolicy: {},
157
+ sanitizeReasoningSummary: false,
158
+ stripModelPrefixes: [],
159
+ },
160
+ },
161
+ ],
162
+ });
163
+ const created = repository.setClientTokenLimit("codex", {
164
+ enabled: true,
165
+ tokenLimit: 1000,
166
+ windowType: "monthly",
167
+ hardBlock: true,
168
+ });
169
+ assert.equal(created.clientRoute, "codex");
170
+ assert.equal(created.windowType, "monthly");
171
+ assert.equal(created.tokenLimit, 1000);
172
+ const updated = repository.setClientTokenLimit("codex", {
173
+ enabled: false,
174
+ tokenLimit: 500,
175
+ windowType: "fixed",
176
+ windowSizeSeconds: 3600,
177
+ hardBlock: false,
178
+ });
179
+ assert.equal(updated.enabled, false);
180
+ assert.equal(updated.tokenLimit, 500);
181
+ assert.equal(updated.windowType, "fixed");
182
+ assert.equal(updated.windowSizeSeconds, 3600);
183
+ assert.equal(updated.hardBlock, false);
184
+ assert.equal(repository.deleteClientTokenLimit("codex"), true);
185
+ assert.equal(repository.getClientTokenLimit("codex"), undefined);
186
+ assert.equal(repository.deleteClientTokenLimit("codex"), false);
187
+ }
188
+ finally {
189
+ rmSync(tempDir, { recursive: true, force: true });
190
+ }
191
+ });
192
+ test("reads empty client token usage as zeros for the current window", async () => {
193
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
194
+ const dbFile = path.join(tempDir, "app.sqlite");
195
+ const legacyStateFile = path.join(tempDir, "providers.json");
196
+ try {
197
+ const repository = await RuntimeProviderRepository.create({
198
+ dbFile,
199
+ legacyStateFile,
200
+ baseProviders: [
201
+ {
202
+ id: "provider-a",
203
+ name: "provider-a",
204
+ baseUrl: "https://provider-a.example/v1",
205
+ responsesUrl: "https://provider-a.example/v1/responses",
206
+ providerApiKeys: ["provider-key"],
207
+ clientApiKeys: [],
208
+ capabilities: {
209
+ usageCheckEnabled: false,
210
+ stripMaxOutputTokens: false,
211
+ requestParameterPolicy: {},
212
+ sanitizeReasoningSummary: false,
213
+ stripModelPrefixes: [],
214
+ },
215
+ },
216
+ ],
217
+ });
218
+ const now = new Date("2026-04-27T13:45:30.000Z");
219
+ assert.deepEqual(repository.getClientTokenUsage("codex", now), {
220
+ clientRoute: "codex",
221
+ windowStart: "2026-04-27T00:00:00.000Z",
222
+ inputTokens: 0,
223
+ outputTokens: 0,
224
+ totalTokens: 0,
225
+ updatedAt: "2026-04-27T00:00:00.000Z",
226
+ });
227
+ }
228
+ finally {
229
+ rmSync(tempDir, { recursive: true, force: true });
230
+ }
231
+ });
232
+ test("increments client token usage within the same window", async () => {
233
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
234
+ const dbFile = path.join(tempDir, "app.sqlite");
235
+ const legacyStateFile = path.join(tempDir, "providers.json");
236
+ try {
237
+ const repository = await RuntimeProviderRepository.create({
238
+ dbFile,
239
+ legacyStateFile,
240
+ baseProviders: [
241
+ {
242
+ id: "provider-a",
243
+ name: "provider-a",
244
+ baseUrl: "https://provider-a.example/v1",
245
+ responsesUrl: "https://provider-a.example/v1/responses",
246
+ providerApiKeys: ["provider-key"],
247
+ clientApiKeys: [],
248
+ capabilities: {
249
+ usageCheckEnabled: false,
250
+ stripMaxOutputTokens: false,
251
+ requestParameterPolicy: {},
252
+ sanitizeReasoningSummary: false,
253
+ stripModelPrefixes: [],
254
+ },
255
+ },
256
+ ],
257
+ });
258
+ repository.setClientTokenLimit("codex", {
259
+ enabled: true,
260
+ tokenLimit: 5000,
261
+ windowType: "daily",
262
+ hardBlock: true,
263
+ });
264
+ const at = new Date("2026-04-27T13:45:30.000Z");
265
+ repository.incrementClientTokenUsage("codex", { inputTokens: 100, outputTokens: 25, totalTokens: 125 }, at);
266
+ const snapshot = repository.incrementClientTokenUsage("codex", { inputTokens: 50, outputTokens: 10, totalTokens: 60 }, at);
267
+ assert.deepEqual(snapshot, {
268
+ clientRoute: "codex",
269
+ windowStart: "2026-04-27T00:00:00.000Z",
270
+ inputTokens: 150,
271
+ outputTokens: 35,
272
+ totalTokens: 185,
273
+ updatedAt: "2026-04-27T13:45:30.000Z",
274
+ });
275
+ }
276
+ finally {
277
+ rmSync(tempDir, { recursive: true, force: true });
278
+ }
279
+ });
280
+ test("increments client token usage into a new window without changing the previous one", async () => {
281
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
282
+ const dbFile = path.join(tempDir, "app.sqlite");
283
+ const legacyStateFile = path.join(tempDir, "providers.json");
284
+ try {
285
+ const repository = await RuntimeProviderRepository.create({
286
+ dbFile,
287
+ legacyStateFile,
288
+ baseProviders: [
289
+ {
290
+ id: "provider-a",
291
+ name: "provider-a",
292
+ baseUrl: "https://provider-a.example/v1",
293
+ responsesUrl: "https://provider-a.example/v1/responses",
294
+ providerApiKeys: ["provider-key"],
295
+ clientApiKeys: [],
296
+ capabilities: {
297
+ usageCheckEnabled: false,
298
+ stripMaxOutputTokens: false,
299
+ requestParameterPolicy: {},
300
+ sanitizeReasoningSummary: false,
301
+ stripModelPrefixes: [],
302
+ },
303
+ },
304
+ ],
305
+ });
306
+ repository.setClientTokenLimit("codex", {
307
+ enabled: true,
308
+ tokenLimit: 5000,
309
+ windowType: "fixed",
310
+ windowSizeSeconds: 3600,
311
+ hardBlock: true,
312
+ });
313
+ repository.incrementClientTokenUsage("codex", { inputTokens: 100, outputTokens: 20, totalTokens: 120 }, new Date("2026-04-27T13:15:00.000Z"));
314
+ repository.incrementClientTokenUsage("codex", { inputTokens: 200, outputTokens: 40, totalTokens: 240 }, new Date("2026-04-27T14:05:00.000Z"));
315
+ assert.deepEqual(repository.getClientTokenUsage("codex", new Date("2026-04-27T13:30:00.000Z")), {
316
+ clientRoute: "codex",
317
+ windowStart: "2026-04-27T13:00:00.000Z",
318
+ inputTokens: 100,
319
+ outputTokens: 20,
320
+ totalTokens: 120,
321
+ updatedAt: "2026-04-27T13:15:00.000Z",
322
+ });
323
+ assert.deepEqual(repository.getClientTokenUsage("codex", new Date("2026-04-27T14:10:00.000Z")), {
324
+ clientRoute: "codex",
325
+ windowStart: "2026-04-27T14:00:00.000Z",
326
+ inputTokens: 200,
327
+ outputTokens: 40,
328
+ totalTokens: 240,
329
+ updatedAt: "2026-04-27T14:05:00.000Z",
330
+ });
331
+ }
332
+ finally {
333
+ rmSync(tempDir, { recursive: true, force: true });
334
+ }
335
+ });
336
+ test("reset clears the current client token usage window", async () => {
337
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
338
+ const dbFile = path.join(tempDir, "app.sqlite");
339
+ const legacyStateFile = path.join(tempDir, "providers.json");
340
+ try {
341
+ const repository = await RuntimeProviderRepository.create({
342
+ dbFile,
343
+ legacyStateFile,
344
+ baseProviders: [
345
+ {
346
+ id: "provider-a",
347
+ name: "provider-a",
348
+ baseUrl: "https://provider-a.example/v1",
349
+ responsesUrl: "https://provider-a.example/v1/responses",
350
+ providerApiKeys: ["provider-key"],
351
+ clientApiKeys: [],
352
+ capabilities: {
353
+ usageCheckEnabled: false,
354
+ stripMaxOutputTokens: false,
355
+ requestParameterPolicy: {},
356
+ sanitizeReasoningSummary: false,
357
+ stripModelPrefixes: [],
358
+ },
359
+ },
360
+ ],
361
+ });
362
+ repository.setClientTokenLimit("codex", {
363
+ enabled: true,
364
+ tokenLimit: 5000,
365
+ windowType: "daily",
366
+ hardBlock: true,
367
+ });
368
+ repository.incrementClientTokenUsage("codex", { inputTokens: 100, outputTokens: 25, totalTokens: 125 }, new Date("2026-04-27T13:00:00.000Z"));
369
+ const reset = repository.resetClientTokenUsage("codex", new Date("2026-04-27T14:00:00.000Z"));
370
+ assert.deepEqual(reset, {
371
+ clientRoute: "codex",
372
+ windowStart: "2026-04-27T00:00:00.000Z",
373
+ inputTokens: 0,
374
+ outputTokens: 0,
375
+ totalTokens: 0,
376
+ updatedAt: "2026-04-27T14:00:00.000Z",
377
+ });
378
+ }
379
+ finally {
380
+ rmSync(tempDir, { recursive: true, force: true });
381
+ }
382
+ });
383
+ test("lists client token limits with current usage snapshots", async () => {
384
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
385
+ const dbFile = path.join(tempDir, "app.sqlite");
386
+ const legacyStateFile = path.join(tempDir, "providers.json");
387
+ try {
388
+ const repository = await RuntimeProviderRepository.create({
389
+ dbFile,
390
+ legacyStateFile,
391
+ baseProviders: [
392
+ {
393
+ id: "provider-a",
394
+ name: "provider-a",
395
+ baseUrl: "https://provider-a.example/v1",
396
+ responsesUrl: "https://provider-a.example/v1/responses",
397
+ providerApiKeys: ["provider-key"],
398
+ clientApiKeys: [],
399
+ capabilities: {
400
+ usageCheckEnabled: false,
401
+ stripMaxOutputTokens: false,
402
+ requestParameterPolicy: {},
403
+ sanitizeReasoningSummary: false,
404
+ stripModelPrefixes: [],
405
+ },
406
+ },
407
+ ],
408
+ });
409
+ repository.setClientRoute("codex", "provider-a");
410
+ const now = "2026-04-27T13:00:00.000Z";
411
+ const db = new BetterSqlite3(dbFile);
412
+ db.prepare(`INSERT INTO client_token_limits (
413
+ client_route, enabled, token_limit, window_type, window_size_seconds, hard_block, created_at, updated_at
414
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run("codex", 1, 1000, "daily", null, 1, now, now);
415
+ db.prepare(`INSERT INTO client_token_usage (
416
+ client_route, window_start, input_tokens, output_tokens, total_tokens, updated_at
417
+ ) VALUES (?, ?, ?, ?, ?, ?)`).run("codex", "2026-04-27T00:00:00.000Z", 100, 50, 150, now);
418
+ db.close();
419
+ const views = repository.listClientTokenLimitsForUi(new Date(now));
420
+ const codexView = views.find((entry) => entry.clientRoute === "codex");
421
+ assert.equal(codexView?.config?.tokenLimit, 1000);
422
+ assert.equal(codexView?.usage.totalTokens, 150);
423
+ assert.equal(codexView?.usage.windowStart, "2026-04-27T00:00:00.000Z");
424
+ }
425
+ finally {
426
+ rmSync(tempDir, { recursive: true, force: true });
427
+ }
428
+ });
429
+ test("deleting a client route clears token limit config and usage", async () => {
430
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
431
+ const dbFile = path.join(tempDir, "app.sqlite");
432
+ const legacyStateFile = path.join(tempDir, "providers.json");
433
+ try {
434
+ const repository = await RuntimeProviderRepository.create({
435
+ dbFile,
436
+ legacyStateFile,
437
+ baseProviders: [
438
+ {
439
+ id: "provider-a",
440
+ name: "provider-a",
441
+ baseUrl: "https://provider-a.example/v1",
442
+ responsesUrl: "https://provider-a.example/v1/responses",
443
+ providerApiKeys: ["provider-key"],
444
+ clientApiKeys: [],
445
+ capabilities: {
446
+ usageCheckEnabled: false,
447
+ stripMaxOutputTokens: false,
448
+ requestParameterPolicy: {},
449
+ sanitizeReasoningSummary: false,
450
+ stripModelPrefixes: [],
451
+ },
452
+ },
453
+ ],
454
+ });
455
+ repository.addClientRoute("codex", "provider-a");
456
+ repository.setClientTokenLimit("codex", {
457
+ enabled: true,
458
+ tokenLimit: 1000,
459
+ windowType: "daily",
460
+ hardBlock: true,
461
+ });
462
+ repository.incrementClientTokenUsage("codex", { inputTokens: 10, outputTokens: 5, totalTokens: 15 }, new Date("2026-04-27T12:00:00.000Z"));
463
+ repository.deleteClientRoute("codex");
464
+ assert.equal(repository.getClientTokenLimit("codex"), undefined);
465
+ assert.deepEqual(repository.getClientTokenUsage("codex", new Date("2026-04-27T12:30:00.000Z")), {
466
+ clientRoute: "codex",
467
+ windowStart: "2026-04-27T00:00:00.000Z",
468
+ inputTokens: 0,
469
+ outputTokens: 0,
470
+ totalTokens: 0,
471
+ updatedAt: "2026-04-27T00:00:00.000Z",
472
+ });
473
+ }
474
+ finally {
475
+ rmSync(tempDir, { recursive: true, force: true });
476
+ }
477
+ });
478
+ test("resolveClientTokenWindowStart supports daily weekly monthly and fixed windows", () => {
479
+ const now = new Date("2026-04-29T13:45:30.000Z");
480
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "daily" }), "2026-04-29T00:00:00.000Z");
481
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "weekly" }), "2026-04-27T00:00:00.000Z");
482
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "monthly" }), "2026-04-01T00:00:00.000Z");
483
+ assert.equal(resolveClientTokenWindowStart(now, { windowType: "fixed", windowSizeSeconds: 3600 }), "2026-04-29T13:00:00.000Z");
484
+ });
485
+ test("backfills legacy request parameter policy rows in the database", async () => {
486
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
487
+ const dbFile = path.join(tempDir, "app.sqlite");
488
+ const legacyStateFile = path.join(tempDir, "providers.json");
489
+ try {
490
+ const repository = await RuntimeProviderRepository.create({
491
+ dbFile,
492
+ legacyStateFile,
493
+ baseProviders: [
494
+ {
495
+ id: "krouter",
496
+ name: "krouter",
497
+ baseUrl: "https://krouter.net/v1",
498
+ responsesUrl: "https://krouter.net/v1/responses",
499
+ providerApiKeys: ["provider-key"],
500
+ clientApiKeys: ["client-key"],
501
+ capabilities: {
502
+ usageCheckEnabled: false,
503
+ stripMaxOutputTokens: true,
504
+ requestParameterPolicy: {
505
+ maxOutputTokens: {
506
+ mode: "strip",
507
+ },
508
+ },
509
+ sanitizeReasoningSummary: false,
510
+ stripModelPrefixes: [],
511
+ },
512
+ },
513
+ ],
514
+ });
515
+ assert.equal(repository.getProvider("krouter")?.capabilities.requestParameterPolicy.maxOutputTokens?.mode, "strip");
516
+ const db = new BetterSqlite3(dbFile);
517
+ db.prepare("UPDATE providers SET request_parameter_policy = '{}' WHERE id = ?").run("krouter");
518
+ db.close();
519
+ await RuntimeProviderRepository.create({
520
+ dbFile,
521
+ legacyStateFile,
522
+ baseProviders: [],
523
+ });
524
+ const verifyDb = new BetterSqlite3(dbFile, { readonly: true });
525
+ const row = verifyDb
526
+ .prepare("SELECT request_parameter_policy FROM providers WHERE id = ?")
527
+ .get("krouter");
528
+ verifyDb.close();
529
+ assert.equal(row.request_parameter_policy, '{"maxOutputTokens":{"mode":"strip"}}');
530
+ }
531
+ finally {
532
+ rmSync(tempDir, { recursive: true, force: true });
533
+ }
534
+ });
535
+ test("persists client-route RTK policy overrides", async () => {
536
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
537
+ const dbFile = path.join(tempDir, "app.sqlite");
538
+ const legacyStateFile = path.join(tempDir, "providers.json");
539
+ try {
540
+ const repository = await RuntimeProviderRepository.create({
541
+ dbFile,
542
+ legacyStateFile,
543
+ baseProviders: [
544
+ {
545
+ id: "provider-a",
546
+ name: "provider-a",
547
+ baseUrl: "https://provider-a.example/v1",
548
+ responsesUrl: "https://provider-a.example/v1/responses",
549
+ providerApiKeys: ["provider-key"],
550
+ clientApiKeys: ["client-key"],
551
+ capabilities: {
552
+ usageCheckEnabled: false,
553
+ stripMaxOutputTokens: false,
554
+ requestParameterPolicy: {},
555
+ sanitizeReasoningSummary: false,
556
+ stripModelPrefixes: [],
557
+ },
558
+ },
559
+ ],
560
+ });
561
+ repository.setClientRouteRtkPolicy("hermes", {
562
+ enabled: true,
563
+ toolOutputEnabled: true,
564
+ maxChars: 1200,
565
+ maxLines: 40,
566
+ tailLines: 8,
567
+ tailChars: 400,
568
+ detectFormat: "command",
569
+ });
570
+ const reloaded = await RuntimeProviderRepository.create({
571
+ dbFile,
572
+ legacyStateFile,
573
+ baseProviders: [],
574
+ });
575
+ assert.deepEqual(reloaded.getClientRouteRtkPolicy("hermes"), {
576
+ enabled: true,
577
+ toolOutputEnabled: true,
578
+ maxChars: 1200,
579
+ maxLines: 40,
580
+ tailLines: 8,
581
+ tailChars: 400,
582
+ detectFormat: "command",
583
+ });
584
+ }
585
+ finally {
586
+ rmSync(tempDir, { recursive: true, force: true });
587
+ }
588
+ });
589
+ test("persists client-route api key bindings and resolves route by key", async () => {
590
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
591
+ const dbFile = path.join(tempDir, "app.sqlite");
592
+ const legacyStateFile = path.join(tempDir, "providers.json");
593
+ try {
594
+ const repository = await RuntimeProviderRepository.create({
595
+ dbFile,
596
+ legacyStateFile,
597
+ baseProviders: [
598
+ {
599
+ id: "provider-a",
600
+ name: "provider-a",
601
+ baseUrl: "https://provider-a.example/v1",
602
+ responsesUrl: "https://provider-a.example/v1/responses",
603
+ providerApiKeys: ["provider-key"],
604
+ clientApiKeys: ["client-key"],
605
+ capabilities: {
606
+ usageCheckEnabled: false,
607
+ stripMaxOutputTokens: false,
608
+ requestParameterPolicy: {},
609
+ sanitizeReasoningSummary: false,
610
+ stripModelPrefixes: [],
611
+ },
612
+ },
613
+ ],
614
+ });
615
+ repository.setClientRoute("hermes", "provider-a");
616
+ repository.setClientRouteApiKeys("hermes", ["sk-hermes-route"]);
617
+ assert.equal(repository.findClientRouteByApiKey("sk-hermes-route"), "hermes");
618
+ assert.deepEqual(repository.getClientRouteApiKeys("hermes"), ["sk-hermes-route"]);
619
+ const reloaded = await RuntimeProviderRepository.create({
620
+ dbFile,
621
+ legacyStateFile,
622
+ baseProviders: [],
623
+ });
624
+ assert.equal(reloaded.findClientRouteByApiKey("sk-hermes-route"), "hermes");
625
+ assert.deepEqual(reloaded.getClientRouteApiKeys("hermes"), ["sk-hermes-route"]);
626
+ }
627
+ finally {
628
+ rmSync(tempDir, { recursive: true, force: true });
629
+ }
630
+ });
631
+ test("routes client CRUD api keys to the bound provider", async () => {
632
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
633
+ const dbFile = path.join(tempDir, "app.sqlite");
634
+ const legacyStateFile = path.join(tempDir, "providers.json");
635
+ try {
636
+ const repository = await RuntimeProviderRepository.create({
637
+ dbFile,
638
+ legacyStateFile,
639
+ baseProviders: [
640
+ {
641
+ id: "provider-a",
642
+ name: "provider-a",
643
+ baseUrl: "https://provider-a.example/v1",
644
+ responsesUrl: "https://provider-a.example/v1/responses",
645
+ providerApiKeys: ["provider-a-key"],
646
+ clientApiKeys: ["legacy-client-a-key"],
647
+ capabilities: {
648
+ usageCheckEnabled: false,
649
+ stripMaxOutputTokens: false,
650
+ requestParameterPolicy: {},
651
+ sanitizeReasoningSummary: false,
652
+ stripModelPrefixes: [],
653
+ },
654
+ },
655
+ {
656
+ id: "provider-b",
657
+ name: "provider-b",
658
+ baseUrl: "https://provider-b.example/v1",
659
+ responsesUrl: "https://provider-b.example/v1/responses",
660
+ providerApiKeys: ["provider-b-key"],
661
+ clientApiKeys: [],
662
+ capabilities: {
663
+ usageCheckEnabled: false,
664
+ stripMaxOutputTokens: false,
665
+ requestParameterPolicy: {},
666
+ sanitizeReasoningSummary: false,
667
+ stripModelPrefixes: [],
668
+ },
669
+ },
670
+ ],
671
+ });
672
+ repository.setClientRoute("mobile-app", "provider-b");
673
+ repository.setClientRouteApiKeys("mobile-app", ["sk-mobile-client"]);
674
+ const matchedProviders = repository.findProvidersByAccessKey("sk-mobile-client");
675
+ assert.equal(repository.findClientRouteByApiKey("sk-mobile-client"), "mobile-app");
676
+ assert.deepEqual(matchedProviders.map((provider) => provider.id), ["provider-b"]);
677
+ }
678
+ finally {
679
+ rmSync(tempDir, { recursive: true, force: true });
680
+ }
681
+ });
682
+ test("does not resolve access by legacy provider-level client API keys", async () => {
683
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
684
+ const dbFile = path.join(tempDir, "app.sqlite");
685
+ const legacyStateFile = path.join(tempDir, "providers.json");
686
+ try {
687
+ const repository = await RuntimeProviderRepository.create({
688
+ dbFile,
689
+ legacyStateFile,
690
+ baseProviders: [
691
+ {
692
+ id: "provider-a",
693
+ name: "provider-a",
694
+ baseUrl: "https://provider-a.example/v1",
695
+ responsesUrl: "https://provider-a.example/v1/responses",
696
+ providerApiKeys: ["provider-a-key"],
697
+ clientApiKeys: ["shared-client-key"],
698
+ capabilities: {
699
+ usageCheckEnabled: false,
700
+ stripMaxOutputTokens: false,
701
+ requestParameterPolicy: {},
702
+ sanitizeReasoningSummary: false,
703
+ stripModelPrefixes: [],
704
+ },
705
+ },
706
+ ],
707
+ });
708
+ repository.createProvider({
709
+ name: "provider-b",
710
+ baseUrl: "https://provider-b.example/v1",
711
+ providerApiKeys: ["provider-b-key"],
712
+ clientApiKeys: ["shared-client-key"],
713
+ });
714
+ const matchedProviders = repository.findProvidersByAccessKey("shared-client-key");
715
+ assert.equal(matchedProviders.length, 0);
716
+ }
717
+ finally {
718
+ rmSync(tempDir, { recursive: true, force: true });
719
+ }
720
+ });
721
+ test("creating and updating a provider does not switch the default route when one already exists", async () => {
722
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
723
+ const dbFile = path.join(tempDir, "app.sqlite");
724
+ const legacyStateFile = path.join(tempDir, "providers.json");
725
+ try {
726
+ const repository = await RuntimeProviderRepository.create({
727
+ dbFile,
728
+ legacyStateFile,
729
+ baseProviders: [
730
+ {
731
+ id: "provider-a",
732
+ name: "provider-a",
733
+ baseUrl: "https://provider-a.example/v1",
734
+ responsesUrl: "https://provider-a.example/v1/responses",
735
+ providerApiKeys: ["provider-a-key"],
736
+ clientApiKeys: ["client-a-key"],
737
+ capabilities: {
738
+ usageCheckEnabled: false,
739
+ stripMaxOutputTokens: false,
740
+ requestParameterPolicy: {},
741
+ sanitizeReasoningSummary: false,
742
+ stripModelPrefixes: [],
743
+ },
744
+ },
745
+ ],
746
+ });
747
+ assert.equal(repository.getActiveProviderId(), "provider-a");
748
+ assert.equal(repository.getProviderIdForClient("default"), "provider-a");
749
+ const created = repository.createProvider({
750
+ name: "provider-b",
751
+ baseUrl: "https://provider-b.example/v1",
752
+ providerApiKeys: ["provider-b-key"],
753
+ clientApiKeys: ["client-b-key"],
754
+ });
755
+ assert.equal(repository.getActiveProviderId(), "provider-a");
756
+ assert.equal(repository.getProviderIdForClient("default"), "provider-a");
757
+ repository.updateProvider(created.id, {
758
+ name: "provider-b-renamed",
759
+ baseUrl: "https://provider-b.example/v1",
760
+ providerApiKeys: ["provider-b-key"],
761
+ clientApiKeys: ["client-b-key"],
762
+ });
763
+ assert.equal(repository.getActiveProviderId(), "provider-a");
764
+ assert.equal(repository.getProviderIdForClient("default"), "provider-a");
765
+ }
766
+ finally {
767
+ rmSync(tempDir, { recursive: true, force: true });
768
+ }
769
+ });
770
+ test("createProvider and updateProvider allow explicit responsesUrl overrides", async () => {
771
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
772
+ const dbFile = path.join(tempDir, "app.sqlite");
773
+ const legacyStateFile = path.join(tempDir, "providers.json");
774
+ try {
775
+ const repository = await RuntimeProviderRepository.create({
776
+ dbFile,
777
+ legacyStateFile,
778
+ baseProviders: [],
779
+ });
780
+ const created = repository.createProvider({
781
+ id: "chatgpt-pool",
782
+ name: "ChatGPT Pool",
783
+ baseUrl: "https://chatgpt.com/backend-api/codex",
784
+ responsesUrl: "https://chatgpt.com/backend-api/codex/responses",
785
+ authMode: "chatgpt_oauth",
786
+ capabilities: {
787
+ usageCheckEnabled: false,
788
+ stripMaxOutputTokens: false,
789
+ requestParameterPolicy: {},
790
+ sanitizeReasoningSummary: false,
791
+ stripModelPrefixes: [],
792
+ },
793
+ });
794
+ assert.equal(created.responsesUrl, "https://chatgpt.com/backend-api/codex/responses");
795
+ const updated = repository.updateProvider(created.id, {
796
+ id: created.id,
797
+ name: created.name,
798
+ baseUrl: created.baseUrl,
799
+ responsesUrl: "https://chatgpt.com/backend-api/codex/responses?via=override",
800
+ authMode: "chatgpt_oauth",
801
+ capabilities: created.capabilities,
802
+ });
803
+ assert.equal(updated.responsesUrl, "https://chatgpt.com/backend-api/codex/responses?via=override");
804
+ const reloaded = await RuntimeProviderRepository.create({
805
+ dbFile,
806
+ legacyStateFile,
807
+ baseProviders: [],
808
+ });
809
+ assert.equal(reloaded.getProviderOrThrow(created.id)?.responsesUrl, "https://chatgpt.com/backend-api/codex/responses?via=override");
810
+ }
811
+ finally {
812
+ rmSync(tempDir, { recursive: true, force: true });
813
+ }
814
+ });
815
+ test("explicit empty client api keys stay empty instead of falling back to provider keys", async () => {
816
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
817
+ const dbFile = path.join(tempDir, "app.sqlite");
818
+ const legacyStateFile = path.join(tempDir, "providers.json");
819
+ try {
820
+ const repository = await RuntimeProviderRepository.create({
821
+ dbFile,
822
+ legacyStateFile,
823
+ baseProviders: [
824
+ {
825
+ id: "provider-a",
826
+ name: "provider-a",
827
+ baseUrl: "https://provider-a.example/v1",
828
+ responsesUrl: "https://provider-a.example/v1/responses",
829
+ providerApiKeys: ["provider-a-key"],
830
+ clientApiKeys: ["client-a-key"],
831
+ capabilities: {
832
+ usageCheckEnabled: false,
833
+ stripMaxOutputTokens: false,
834
+ requestParameterPolicy: {},
835
+ sanitizeReasoningSummary: false,
836
+ stripModelPrefixes: [],
837
+ },
838
+ },
839
+ ],
840
+ });
841
+ repository.updateProvider("provider-a", {
842
+ name: "provider-a",
843
+ baseUrl: "https://provider-a.example/v1",
844
+ providerApiKeys: ["provider-a-key"],
845
+ clientApiKeys: [],
846
+ });
847
+ assert.deepEqual(repository.getProvider("provider-a")?.clientApiKeys, []);
848
+ }
849
+ finally {
850
+ rmSync(tempDir, { recursive: true, force: true });
851
+ }
852
+ });
853
+ test("keeps explicit hermes and codex client routes distinct", () => {
854
+ assert.equal(normalizeClientRouteKey("hermes"), "hermes");
855
+ assert.equal(normalizeClientRouteKey("codex"), "codex");
856
+ assert.equal(normalizeClientRouteKey("openclaw"), "openclaw");
857
+ });
858
+ test("prefers the codex route as fallback for default traffic", async () => {
859
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
860
+ const dbFile = path.join(tempDir, "app.sqlite");
861
+ const legacyStateFile = path.join(tempDir, "providers.json");
862
+ try {
863
+ const repository = await RuntimeProviderRepository.create({
864
+ dbFile,
865
+ legacyStateFile,
866
+ baseProviders: [
867
+ {
868
+ id: "provider-default",
869
+ name: "provider-default",
870
+ baseUrl: "https://default.example/v1",
871
+ responsesUrl: "https://default.example/v1/responses",
872
+ providerApiKeys: ["provider-default-key"],
873
+ clientApiKeys: [],
874
+ capabilities: {
875
+ usageCheckEnabled: false,
876
+ stripMaxOutputTokens: false,
877
+ requestParameterPolicy: {},
878
+ sanitizeReasoningSummary: false,
879
+ stripModelPrefixes: [],
880
+ },
881
+ },
882
+ {
883
+ id: "provider-codex",
884
+ name: "provider-codex",
885
+ baseUrl: "https://codex.example/v1",
886
+ responsesUrl: "https://codex.example/v1/responses",
887
+ providerApiKeys: ["provider-codex-key"],
888
+ clientApiKeys: [],
889
+ capabilities: {
890
+ usageCheckEnabled: false,
891
+ stripMaxOutputTokens: false,
892
+ requestParameterPolicy: {},
893
+ sanitizeReasoningSummary: false,
894
+ stripModelPrefixes: [],
895
+ },
896
+ },
897
+ ],
898
+ });
899
+ repository.setClientRoute("default", "provider-default");
900
+ repository.setClientRoute("codex", "provider-codex");
901
+ assert.equal(repository.getFallbackProvider("default", "provider-default")?.id, "provider-codex");
902
+ }
903
+ finally {
904
+ rmSync(tempDir, { recursive: true, force: true });
905
+ }
906
+ });
907
+ test("uses the default route as fallback for codex traffic", async () => {
908
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
909
+ const dbFile = path.join(tempDir, "app.sqlite");
910
+ const legacyStateFile = path.join(tempDir, "providers.json");
911
+ try {
912
+ const repository = await RuntimeProviderRepository.create({
913
+ dbFile,
914
+ legacyStateFile,
915
+ baseProviders: [
916
+ {
917
+ id: "provider-default",
918
+ name: "provider-default",
919
+ baseUrl: "https://default.example/v1",
920
+ responsesUrl: "https://default.example/v1/responses",
921
+ providerApiKeys: ["provider-default-key"],
922
+ clientApiKeys: [],
923
+ capabilities: {
924
+ usageCheckEnabled: false,
925
+ stripMaxOutputTokens: false,
926
+ requestParameterPolicy: {},
927
+ sanitizeReasoningSummary: false,
928
+ stripModelPrefixes: [],
929
+ },
930
+ },
931
+ {
932
+ id: "provider-codex",
933
+ name: "provider-codex",
934
+ baseUrl: "https://codex.example/v1",
935
+ responsesUrl: "https://codex.example/v1/responses",
936
+ providerApiKeys: ["provider-codex-key"],
937
+ clientApiKeys: [],
938
+ capabilities: {
939
+ usageCheckEnabled: false,
940
+ stripMaxOutputTokens: false,
941
+ requestParameterPolicy: {},
942
+ sanitizeReasoningSummary: false,
943
+ stripModelPrefixes: [],
944
+ },
945
+ },
946
+ ],
947
+ });
948
+ repository.setClientRoute("default", "provider-default");
949
+ repository.setClientRoute("codex", "provider-codex");
950
+ assert.equal(repository.getFallbackProvider("codex", "provider-codex")?.id, "provider-default");
951
+ }
952
+ finally {
953
+ rmSync(tempDir, { recursive: true, force: true });
954
+ }
955
+ });
956
+ test("does not fall back to an unbound provider outside configured client routes", async () => {
957
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
958
+ const dbFile = path.join(tempDir, "app.sqlite");
959
+ const legacyStateFile = path.join(tempDir, "providers.json");
960
+ try {
961
+ const repository = await RuntimeProviderRepository.create({
962
+ dbFile,
963
+ legacyStateFile,
964
+ baseProviders: [
965
+ {
966
+ id: "provider-default",
967
+ name: "provider-default",
968
+ baseUrl: "https://default.example/v1",
969
+ responsesUrl: "https://default.example/v1/responses",
970
+ providerApiKeys: ["provider-default-key"],
971
+ clientApiKeys: [],
972
+ capabilities: {
973
+ usageCheckEnabled: false,
974
+ stripMaxOutputTokens: false,
975
+ requestParameterPolicy: {},
976
+ sanitizeReasoningSummary: false,
977
+ stripModelPrefixes: [],
978
+ },
979
+ },
980
+ {
981
+ id: "provider-extra",
982
+ name: "provider-extra",
983
+ baseUrl: "https://extra.example/v1",
984
+ responsesUrl: "https://extra.example/v1/responses",
985
+ providerApiKeys: ["provider-extra-key"],
986
+ clientApiKeys: [],
987
+ capabilities: {
988
+ usageCheckEnabled: false,
989
+ stripMaxOutputTokens: false,
990
+ requestParameterPolicy: {},
991
+ sanitizeReasoningSummary: false,
992
+ stripModelPrefixes: [],
993
+ },
994
+ },
995
+ ],
996
+ });
997
+ repository.setClientRoute("default", "provider-default");
998
+ assert.equal(repository.getFallbackProvider("default", "provider-default"), undefined);
999
+ }
1000
+ finally {
1001
+ rmSync(tempDir, { recursive: true, force: true });
1002
+ }
1003
+ });
1004
+ test("persists provider error policy rules in the database", async () => {
1005
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
1006
+ const dbFile = path.join(tempDir, "app.sqlite");
1007
+ const legacyStateFile = path.join(tempDir, "providers.json");
1008
+ try {
1009
+ const repository = await RuntimeProviderRepository.create({
1010
+ dbFile,
1011
+ legacyStateFile,
1012
+ baseProviders: [
1013
+ {
1014
+ id: "provider-a",
1015
+ name: "provider-a",
1016
+ baseUrl: "https://provider-a.example/v1",
1017
+ responsesUrl: "https://provider-a.example/v1/responses",
1018
+ providerApiKeys: ["provider-key"],
1019
+ clientApiKeys: ["client-key"],
1020
+ capabilities: {
1021
+ usageCheckEnabled: false,
1022
+ stripMaxOutputTokens: false,
1023
+ requestParameterPolicy: {},
1024
+ sanitizeReasoningSummary: false,
1025
+ stripModelPrefixes: [],
1026
+ errorPolicy: {
1027
+ rules: [
1028
+ {
1029
+ statusCodes: [413],
1030
+ code: "UPSTREAM_REQUEST_TOO_LARGE",
1031
+ message: "Prompt body too large",
1032
+ retryable: false,
1033
+ },
1034
+ ],
1035
+ },
1036
+ },
1037
+ },
1038
+ ],
1039
+ });
1040
+ assert.equal(repository.getProvider("provider-a")?.capabilities.errorPolicy?.rules[0]?.code, "UPSTREAM_REQUEST_TOO_LARGE");
1041
+ const reloaded = await RuntimeProviderRepository.create({
1042
+ dbFile,
1043
+ legacyStateFile,
1044
+ baseProviders: [],
1045
+ });
1046
+ assert.deepEqual(reloaded.getProvider("provider-a")?.capabilities.errorPolicy, {
1047
+ rules: [
1048
+ {
1049
+ statusCodes: [413],
1050
+ upstreamCodes: undefined,
1051
+ upstreamTypes: undefined,
1052
+ messageIncludes: undefined,
1053
+ bodyIncludes: undefined,
1054
+ code: "UPSTREAM_REQUEST_TOO_LARGE",
1055
+ message: "Prompt body too large",
1056
+ retryable: false,
1057
+ },
1058
+ ],
1059
+ });
1060
+ }
1061
+ finally {
1062
+ rmSync(tempDir, { recursive: true, force: true });
1063
+ }
1064
+ });
1065
+ test("persists provider model aliases in the database", async () => {
1066
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
1067
+ const dbFile = path.join(tempDir, "app.sqlite");
1068
+ const legacyStateFile = path.join(tempDir, "providers.json");
1069
+ try {
1070
+ const repository = await RuntimeProviderRepository.create({
1071
+ dbFile,
1072
+ legacyStateFile,
1073
+ baseProviders: [
1074
+ {
1075
+ id: "provider-a",
1076
+ name: "provider-a",
1077
+ baseUrl: "https://provider-a.example/v1",
1078
+ responsesUrl: "https://provider-a.example/v1/responses",
1079
+ providerApiKeys: ["provider-key"],
1080
+ clientApiKeys: ["client-key"],
1081
+ capabilities: {
1082
+ usageCheckEnabled: false,
1083
+ stripMaxOutputTokens: false,
1084
+ requestParameterPolicy: {},
1085
+ sanitizeReasoningSummary: false,
1086
+ stripModelPrefixes: [],
1087
+ modelAliases: {
1088
+ "cheap-default": "gpt-5.4-mini",
1089
+ "quality-default": "gpt-5.4",
1090
+ },
1091
+ },
1092
+ },
1093
+ ],
1094
+ });
1095
+ const reloaded = await RuntimeProviderRepository.create({
1096
+ dbFile,
1097
+ legacyStateFile,
1098
+ baseProviders: [],
1099
+ });
1100
+ assert.deepEqual(reloaded.getProvider("provider-a")?.capabilities.modelAliases, {
1101
+ "cheap-default": "gpt-5.4-mini",
1102
+ "quality-default": "gpt-5.4",
1103
+ });
1104
+ assert.deepEqual(repository.getProvider("provider-a")?.capabilities.modelAliases, {
1105
+ "cheap-default": "gpt-5.4-mini",
1106
+ "quality-default": "gpt-5.4",
1107
+ });
1108
+ }
1109
+ finally {
1110
+ rmSync(tempDir, { recursive: true, force: true });
1111
+ }
1112
+ });
1113
+ test("hides system-managed providers from CRUD list while keeping client selector options", async () => {
1114
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-provider-repo-"));
1115
+ const dbFile = path.join(tempDir, "app.sqlite");
1116
+ const legacyStateFile = path.join(tempDir, "providers.json");
1117
+ try {
1118
+ const repository = await RuntimeProviderRepository.create({
1119
+ dbFile,
1120
+ legacyStateFile,
1121
+ baseProviders: [
1122
+ {
1123
+ id: "manual-provider",
1124
+ name: "manual-provider",
1125
+ baseUrl: "https://manual.example/v1",
1126
+ responsesUrl: "https://manual.example/v1/responses",
1127
+ providerApiKeys: ["provider-key"],
1128
+ clientApiKeys: ["client-key"],
1129
+ capabilities: {
1130
+ usageCheckEnabled: false,
1131
+ stripMaxOutputTokens: false,
1132
+ requestParameterPolicy: {},
1133
+ sanitizeReasoningSummary: false,
1134
+ stripModelPrefixes: [],
1135
+ },
1136
+ },
1137
+ {
1138
+ id: "account-openai-codex",
1139
+ name: "OpenAI / Codex Account Pool",
1140
+ baseUrl: "https://chatgpt.com/backend-api/codex",
1141
+ responsesUrl: "https://chatgpt.com/backend-api/codex/v1/responses",
1142
+ authMode: "chatgpt_oauth",
1143
+ providerApiKeys: [],
1144
+ clientApiKeys: [],
1145
+ capabilities: {
1146
+ usageCheckEnabled: false,
1147
+ stripMaxOutputTokens: false,
1148
+ requestParameterPolicy: {},
1149
+ sanitizeReasoningSummary: false,
1150
+ stripModelPrefixes: [],
1151
+ systemManaged: true,
1152
+ accountPlatform: "openai_codex",
1153
+ accountPoolRequired: true,
1154
+ },
1155
+ },
1156
+ ],
1157
+ });
1158
+ assert.deepEqual(repository.listProvidersForUi().map((provider) => provider.id), ["manual-provider"]);
1159
+ assert.deepEqual(repository.listProviderOptionsForClientSetup().map((provider) => provider.id), ["manual-provider", "account-openai-codex"]);
1160
+ const reloaded = await RuntimeProviderRepository.create({
1161
+ dbFile,
1162
+ legacyStateFile,
1163
+ baseProviders: [],
1164
+ });
1165
+ const accountProvider = reloaded.getProvider("account-openai-codex");
1166
+ assert.equal(accountProvider?.capabilities.systemManaged, true);
1167
+ assert.equal(accountProvider?.capabilities.accountPlatform, "openai_codex");
1168
+ assert.equal(accountProvider?.capabilities.accountPoolRequired, true);
1169
+ assert.deepEqual(reloaded.listProvidersForUi().map((provider) => provider.id), ["manual-provider"]);
1170
+ assert.ok(reloaded
1171
+ .listProviderOptionsForClientSetup()
1172
+ .some((provider) => provider.id === "account-openai-codex"));
1173
+ }
1174
+ finally {
1175
+ rmSync(tempDir, { recursive: true, force: true });
1176
+ }
1177
+ });