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.
- package/README.md +56 -0
- package/cli.js +118 -0
- package/dist/anthropic-messages.js +383 -0
- package/dist/anthropic-messages.test.js +209 -0
- package/dist/audit-log.js +138 -0
- package/dist/audit-log.test.js +480 -0
- package/dist/billing-expiration.js +70 -0
- package/dist/billing-expiration.test.js +114 -0
- package/dist/billing.js +716 -0
- package/dist/billing.test.js +228 -0
- package/dist/chatgpt-oauth-store.js +240 -0
- package/dist/chatgpt-oauth-store.test.js +88 -0
- package/dist/chatgpt-oauth.js +118 -0
- package/dist/chatgpt-oauth.test.js +63 -0
- package/dist/chatgpt-provider-auth.js +60 -0
- package/dist/chatgpt-provider-auth.test.js +101 -0
- package/dist/client/app-icon.svg +17 -0
- package/dist/client/assets/index-C7Vvhst8.js +14 -0
- package/dist/client/assets/index-DpqgYK3L.css +1 -0
- package/dist/client/favicon.svg +17 -0
- package/dist/client/index.html +31 -0
- package/dist/client-config-apply.js +345 -0
- package/dist/client-config-apply.test.js +185 -0
- package/dist/client-token-limits.js +111 -0
- package/dist/client-token-limits.test.js +129 -0
- package/dist/codex-config.js +47 -0
- package/dist/codex-setup.js +87 -0
- package/dist/codex-setup.test.js +30 -0
- package/dist/config.js +314 -0
- package/dist/cost-analytics.js +31 -0
- package/dist/cost-analytics.test.js +38 -0
- package/dist/customer-key-access.js +126 -0
- package/dist/customer-key-access.test.js +178 -0
- package/dist/customer-keys.js +209 -0
- package/dist/customer-keys.test.js +68 -0
- package/dist/customer-usage.js +18 -0
- package/dist/customer-usage.test.js +55 -0
- package/dist/dashboard-auth.js +318 -0
- package/dist/dashboard-auth.test.js +133 -0
- package/dist/dashboard-serving.test.js +235 -0
- package/dist/error-response.js +174 -0
- package/dist/error-response.test.js +88 -0
- package/dist/forward.js +357 -0
- package/dist/health-websocket-manager.js +174 -0
- package/dist/http-rate-limit.js +36 -0
- package/dist/http-rate-limit.test.js +62 -0
- package/dist/kiro-auth.js +136 -0
- package/dist/kiro-auth.test.js +234 -0
- package/dist/kiro-codewhisperer.js +646 -0
- package/dist/kiro-codewhisperer.test.js +219 -0
- package/dist/kiro-device-login.js +338 -0
- package/dist/kiro-eventstream.js +219 -0
- package/dist/kiro-eventstream.test.js +79 -0
- package/dist/kiro-forward.js +401 -0
- package/dist/kiro-import-cli.js +69 -0
- package/dist/kiro-import.js +94 -0
- package/dist/kiro-import.test.js +125 -0
- package/dist/kiro-token-store.js +196 -0
- package/dist/kiro-token-store.test.js +207 -0
- package/dist/krouter-usage.js +243 -0
- package/dist/model-combo-repository.js +147 -0
- package/dist/model-routing.js +69 -0
- package/dist/model-routing.test.js +41 -0
- package/dist/normalize-request.js +531 -0
- package/dist/normalize-request.test.js +277 -0
- package/dist/omv-public-firewall.test.js +11 -0
- package/dist/package.json +17 -0
- package/dist/prompt-cache-state.js +146 -0
- package/dist/prompt-cache-state.test.js +71 -0
- package/dist/prompt-cache.js +229 -0
- package/dist/provider-health-service.js +404 -0
- package/dist/provider-request-parameters.js +107 -0
- package/dist/provider-request-parameters.test.js +26 -0
- package/dist/provider-routing.js +114 -0
- package/dist/provider-routing.test.js +64 -0
- package/dist/provider-usage.js +314 -0
- package/dist/request-timeout-policy.js +61 -0
- package/dist/request-timeout-policy.test.js +40 -0
- package/dist/response-cache.js +69 -0
- package/dist/response-cache.test.js +28 -0
- package/dist/routing-combo-repository.js +300 -0
- package/dist/routing-engine.js +377 -0
- package/dist/routing-integration.js +155 -0
- package/dist/routing-simulation-engine.js +326 -0
- package/dist/rtk-layer.js +483 -0
- package/dist/rtk-layer.test.js +198 -0
- package/dist/runtime-provider-repository.js +1742 -0
- package/dist/runtime-provider-repository.test.js +1177 -0
- package/dist/schema.js +118 -0
- package/dist/schema.test.js +16 -0
- package/dist/sepay-webhook.js +87 -0
- package/dist/sepay-webhook.test.js +142 -0
- package/dist/server-body-limit.test.js +35 -0
- package/dist/server-client-token-limits.test.js +161 -0
- package/dist/server-codex-config-setup.test.js +76 -0
- package/dist/server-http-rate-limit.test.js +80 -0
- package/dist/server-response-cache.test.js +105 -0
- package/dist/server-routes-alias.test.js +39 -0
- package/dist/server-sepay-webhook-security.test.js +59 -0
- package/dist/server.js +5906 -0
- package/dist/session-log.js +178 -0
- package/dist/tailnet-funnel-script.test.js +33 -0
- package/dist/telegram-bot/actions.js +118 -0
- package/dist/telegram-bot/admin-actions.js +103 -0
- package/dist/telegram-bot/auth.js +46 -0
- package/dist/telegram-bot/auth.test.js +1 -0
- package/dist/telegram-bot/bot-identity-repository.js +189 -0
- package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
- package/dist/telegram-bot/callbacks.js +30 -0
- package/dist/telegram-bot/codex-config-delivery.js +38 -0
- package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
- package/dist/telegram-bot/commands/accounts.js +140 -0
- package/dist/telegram-bot/commands/apikey.js +737 -0
- package/dist/telegram-bot/commands/apply.js +265 -0
- package/dist/telegram-bot/commands/clients.js +13 -0
- package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
- package/dist/telegram-bot/commands/grant.js +138 -0
- package/dist/telegram-bot/commands/grant.test.js +217 -0
- package/dist/telegram-bot/commands/help.js +52 -0
- package/dist/telegram-bot/commands/me.js +53 -0
- package/dist/telegram-bot/commands/models.js +6 -0
- package/dist/telegram-bot/commands/oauth.js +64 -0
- package/dist/telegram-bot/commands/plans.js +96 -0
- package/dist/telegram-bot/commands/providers.js +27 -0
- package/dist/telegram-bot/commands/quota.js +10 -0
- package/dist/telegram-bot/commands/renew-user.js +139 -0
- package/dist/telegram-bot/commands/renew-user.test.js +184 -0
- package/dist/telegram-bot/commands/renew.js +1369 -0
- package/dist/telegram-bot/commands/renew.test.js +1633 -0
- package/dist/telegram-bot/commands/start.js +212 -0
- package/dist/telegram-bot/commands/start.test.js +280 -0
- package/dist/telegram-bot/commands/status.js +6 -0
- package/dist/telegram-bot/commands/tailscale.js +15 -0
- package/dist/telegram-bot/commands/tailscale.test.js +76 -0
- package/dist/telegram-bot/commands/test.js +51 -0
- package/dist/telegram-bot/commands/test.test.js +14 -0
- package/dist/telegram-bot/commands/usage.js +10 -0
- package/dist/telegram-bot/config.js +98 -0
- package/dist/telegram-bot/config.test.js +42 -0
- package/dist/telegram-bot/customer-actions.js +160 -0
- package/dist/telegram-bot/customer-api-keys.js +68 -0
- package/dist/telegram-bot/customer-billing.js +72 -0
- package/dist/telegram-bot/customer-workspace-repository.js +134 -0
- package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
- package/dist/telegram-bot/dashboard-login.js +39 -0
- package/dist/telegram-bot/format.js +140 -0
- package/dist/telegram-bot/grants.js +370 -0
- package/dist/telegram-bot/grants.test.js +290 -0
- package/dist/telegram-bot/index.js +85 -0
- package/dist/telegram-bot/message-cleanup.js +55 -0
- package/dist/telegram-bot/message-cleanup.test.js +77 -0
- package/dist/telegram-bot/message-format.js +45 -0
- package/dist/telegram-bot/message-format.test.js +10 -0
- package/dist/telegram-bot/proxy-client.js +174 -0
- package/dist/telegram-bot/rate-limit.js +95 -0
- package/dist/telegram-bot/rate-limit.test.js +58 -0
- package/dist/telegram-bot/sessions.js +171 -0
- package/dist/telegram-bot/sessions.test.js +107 -0
- package/dist/telegram-bot/telegram-adapter.js +126 -0
- package/dist/telegram-bot/worker.js +63 -0
- package/package.json +39 -0
|
@@ -0,0 +1,1742 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
5
|
+
import { cloneProviderRequestParameterPolicy, parseProviderRequestParameterPolicyInput, resolveMaxOutputTokensRule, } from "./provider-request-parameters.js";
|
|
6
|
+
import { cloneRtkLayerPolicy, parseRtkLayerPolicyInput, } from "./rtk-layer.js";
|
|
7
|
+
import { resolveClientTokenWindowStart } from "./client-token-limits.js";
|
|
8
|
+
export { resolveClientTokenWindowStart } from "./client-token-limits.js";
|
|
9
|
+
import { CODEWHISPERER_GENERATE_PATH, DEFAULT_KIRO_MODEL_ALIASES, } from "./kiro-codewhisperer.js";
|
|
10
|
+
export const BUILTIN_CLIENT_ROUTE_KEYS = ["default"];
|
|
11
|
+
export class RuntimeProviderError extends Error {
|
|
12
|
+
statusCode;
|
|
13
|
+
body;
|
|
14
|
+
constructor(statusCode, body) {
|
|
15
|
+
super(body.message);
|
|
16
|
+
this.statusCode = statusCode;
|
|
17
|
+
this.body = body;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class RuntimeProviderRepository {
|
|
21
|
+
dbFile;
|
|
22
|
+
legacyStateFile;
|
|
23
|
+
db;
|
|
24
|
+
providerPresets;
|
|
25
|
+
activeProviderId;
|
|
26
|
+
modelOverrides;
|
|
27
|
+
clientRoutes;
|
|
28
|
+
clientRouteRtkPolicies;
|
|
29
|
+
clientRouteApiKeys;
|
|
30
|
+
constructor(dbFile, legacyStateFile, db, state, baseProviders) {
|
|
31
|
+
this.dbFile = dbFile;
|
|
32
|
+
this.legacyStateFile = legacyStateFile;
|
|
33
|
+
this.db = db;
|
|
34
|
+
const seededProviders = state.providers.length
|
|
35
|
+
? mergeBaseProviders(state.providers, baseProviders)
|
|
36
|
+
: [...baseProviders];
|
|
37
|
+
this.providerPresets = ensureUniqueProviderIds(seededProviders.map((provider) => migrateLegacyProvider(provider)));
|
|
38
|
+
this.activeProviderId = this.resolveActiveProviderId(state.activeProviderId);
|
|
39
|
+
this.modelOverrides = this.resolveModelOverrides(state.modelOverrides, state.modelOverride);
|
|
40
|
+
this.clientRoutes = this.resolveClientRoutes(state.clientRoutes);
|
|
41
|
+
this.clientRouteRtkPolicies = this.resolveClientRouteRtkPolicies(state.clientRouteRtkPolicies);
|
|
42
|
+
this.clientRouteApiKeys = this.resolveClientRouteApiKeys(state.clientRouteApiKeys);
|
|
43
|
+
}
|
|
44
|
+
static async create(options) {
|
|
45
|
+
const db = openDatabase(options.dbFile);
|
|
46
|
+
ensureSchema(db);
|
|
47
|
+
backfillLegacyRequestParameterPolicies(db);
|
|
48
|
+
const stateFromDb = readStateFromDatabase(db);
|
|
49
|
+
const hasDbState = stateFromDb.providers.length > 0;
|
|
50
|
+
const legacyState = hasDbState ? undefined : loadLegacyState(options.legacyStateFile);
|
|
51
|
+
const initialState = hasDbState ? stateFromDb : legacyState ?? { providers: [] };
|
|
52
|
+
const repository = new RuntimeProviderRepository(options.dbFile, options.legacyStateFile, db, initialState, options.baseProviders);
|
|
53
|
+
if (!hasDbState || shouldPersistSeededProviders(initialState.providers, repository.providerPresets)) {
|
|
54
|
+
repository.persistRuntimeState();
|
|
55
|
+
}
|
|
56
|
+
return repository;
|
|
57
|
+
}
|
|
58
|
+
getDatabase() {
|
|
59
|
+
return this.db;
|
|
60
|
+
}
|
|
61
|
+
getActiveProviderId() {
|
|
62
|
+
return this.activeProviderId;
|
|
63
|
+
}
|
|
64
|
+
getModelOverride(client = "default") {
|
|
65
|
+
return this.modelOverrides[client];
|
|
66
|
+
}
|
|
67
|
+
getClientRoutesForUi() {
|
|
68
|
+
return this.listClientRouteKeys().map((key) => {
|
|
69
|
+
const providerId = this.clientRoutes[key] ?? null;
|
|
70
|
+
const provider = providerId ? this.getProvider(providerId) : undefined;
|
|
71
|
+
return {
|
|
72
|
+
key,
|
|
73
|
+
providerId,
|
|
74
|
+
providerName: provider?.name ?? null,
|
|
75
|
+
modelOverride: this.getModelOverride(key) ?? null,
|
|
76
|
+
rtkPolicy: cloneRtkLayerPolicy(this.getClientRouteRtkPolicy(key)) ?? null,
|
|
77
|
+
apiKeys: [...(this.clientRouteApiKeys[key] ?? [])],
|
|
78
|
+
};
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
getProviderIdForClient(client) {
|
|
82
|
+
const preferredId = this.clientRoutes[client] ?? this.activeProviderId;
|
|
83
|
+
return this.resolveActiveProviderId(preferredId);
|
|
84
|
+
}
|
|
85
|
+
getProviderForClient(client) {
|
|
86
|
+
return this.getProvider(this.getProviderIdForClient(client));
|
|
87
|
+
}
|
|
88
|
+
getClientRouteRtkPolicy(client = "default") {
|
|
89
|
+
return this.clientRouteRtkPolicies[normalizeClientRouteKey(client)];
|
|
90
|
+
}
|
|
91
|
+
getClientRouteApiKeys(client = "default") {
|
|
92
|
+
return [...(this.clientRouteApiKeys[normalizeClientRouteKey(client)] ?? [])];
|
|
93
|
+
}
|
|
94
|
+
getClientTokenLimit(client) {
|
|
95
|
+
const row = this.db
|
|
96
|
+
.prepare(`SELECT
|
|
97
|
+
client_route,
|
|
98
|
+
enabled,
|
|
99
|
+
token_limit,
|
|
100
|
+
window_type,
|
|
101
|
+
window_size_seconds,
|
|
102
|
+
hard_block,
|
|
103
|
+
created_at,
|
|
104
|
+
updated_at
|
|
105
|
+
FROM client_token_limits
|
|
106
|
+
WHERE client_route = ?`)
|
|
107
|
+
.get(normalizeClientRouteKey(client));
|
|
108
|
+
return row ? mapClientTokenLimitRow(row) : undefined;
|
|
109
|
+
}
|
|
110
|
+
getClientTokenUsage(client, now = new Date()) {
|
|
111
|
+
const clientRoute = normalizeClientRouteKey(client);
|
|
112
|
+
const config = this.getClientTokenLimit(clientRoute);
|
|
113
|
+
const windowStart = resolveClientTokenWindowStart(now, {
|
|
114
|
+
windowType: config?.windowType ?? "daily",
|
|
115
|
+
windowSizeSeconds: config?.windowSizeSeconds,
|
|
116
|
+
});
|
|
117
|
+
const row = this.db
|
|
118
|
+
.prepare(`SELECT
|
|
119
|
+
client_route,
|
|
120
|
+
window_start,
|
|
121
|
+
input_tokens,
|
|
122
|
+
output_tokens,
|
|
123
|
+
total_tokens,
|
|
124
|
+
updated_at
|
|
125
|
+
FROM client_token_usage
|
|
126
|
+
WHERE client_route = ? AND window_start = ?`)
|
|
127
|
+
.get(clientRoute, windowStart);
|
|
128
|
+
return (row ? mapClientTokenUsageRow(row) : buildEmptyClientTokenUsageSnapshot(clientRoute, windowStart));
|
|
129
|
+
}
|
|
130
|
+
listClientTokenLimitsForUi(now = new Date()) {
|
|
131
|
+
const rows = queryRows(this.db, `SELECT
|
|
132
|
+
client_route,
|
|
133
|
+
enabled,
|
|
134
|
+
token_limit,
|
|
135
|
+
window_type,
|
|
136
|
+
window_size_seconds,
|
|
137
|
+
hard_block,
|
|
138
|
+
created_at,
|
|
139
|
+
updated_at
|
|
140
|
+
FROM client_token_limits
|
|
141
|
+
ORDER BY client_route`);
|
|
142
|
+
const configByClientRoute = new Map(rows.map((row) => {
|
|
143
|
+
const config = mapClientTokenLimitRow(row);
|
|
144
|
+
return [config.clientRoute, config];
|
|
145
|
+
}));
|
|
146
|
+
return this.listClientRouteKeys().map((clientRoute) => {
|
|
147
|
+
const config = configByClientRoute.get(clientRoute) ?? null;
|
|
148
|
+
return {
|
|
149
|
+
clientRoute,
|
|
150
|
+
config,
|
|
151
|
+
usage: this.getClientTokenUsage(clientRoute, now),
|
|
152
|
+
};
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
setClientTokenLimit(client, input) {
|
|
156
|
+
const clientRoute = normalizeClientRouteKey(client);
|
|
157
|
+
const tokenLimit = Math.max(1, Math.floor(input.tokenLimit));
|
|
158
|
+
const windowType = normalizeClientTokenWindowType(input.windowType);
|
|
159
|
+
const windowSizeSeconds = windowType === "fixed" && input.windowSizeSeconds && input.windowSizeSeconds > 0
|
|
160
|
+
? Math.floor(input.windowSizeSeconds)
|
|
161
|
+
: undefined;
|
|
162
|
+
const now = new Date().toISOString();
|
|
163
|
+
const existing = this.getClientTokenLimit(clientRoute);
|
|
164
|
+
this.db
|
|
165
|
+
.prepare(`INSERT INTO client_token_limits (
|
|
166
|
+
client_route,
|
|
167
|
+
enabled,
|
|
168
|
+
token_limit,
|
|
169
|
+
window_type,
|
|
170
|
+
window_size_seconds,
|
|
171
|
+
hard_block,
|
|
172
|
+
created_at,
|
|
173
|
+
updated_at
|
|
174
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
175
|
+
ON CONFLICT(client_route) DO UPDATE SET
|
|
176
|
+
enabled = excluded.enabled,
|
|
177
|
+
token_limit = excluded.token_limit,
|
|
178
|
+
window_type = excluded.window_type,
|
|
179
|
+
window_size_seconds = excluded.window_size_seconds,
|
|
180
|
+
hard_block = excluded.hard_block,
|
|
181
|
+
updated_at = excluded.updated_at`)
|
|
182
|
+
.run(clientRoute, input.enabled ? 1 : 0, tokenLimit, windowType, windowSizeSeconds ?? null, input.hardBlock ? 1 : 0, existing?.createdAt ?? now, now);
|
|
183
|
+
return this.getClientTokenLimit(clientRoute);
|
|
184
|
+
}
|
|
185
|
+
deleteClientTokenLimit(client) {
|
|
186
|
+
const result = this.db
|
|
187
|
+
.prepare("DELETE FROM client_token_limits WHERE client_route = ?")
|
|
188
|
+
.run(normalizeClientRouteKey(client));
|
|
189
|
+
return result.changes > 0;
|
|
190
|
+
}
|
|
191
|
+
incrementClientTokenUsage(client, usage, now = new Date()) {
|
|
192
|
+
const clientRoute = normalizeClientRouteKey(client);
|
|
193
|
+
const config = this.getClientTokenLimit(clientRoute);
|
|
194
|
+
const windowStart = resolveClientTokenWindowStart(now, {
|
|
195
|
+
windowType: config?.windowType ?? "daily",
|
|
196
|
+
windowSizeSeconds: config?.windowSizeSeconds,
|
|
197
|
+
});
|
|
198
|
+
const inputTokens = normalizeNonNegativeInteger(usage.inputTokens);
|
|
199
|
+
const outputTokens = normalizeNonNegativeInteger(usage.outputTokens);
|
|
200
|
+
const totalTokens = normalizeNonNegativeInteger(usage.totalTokens);
|
|
201
|
+
const updatedAt = now.toISOString();
|
|
202
|
+
this.db.transaction(() => {
|
|
203
|
+
this.db
|
|
204
|
+
.prepare(`INSERT INTO client_token_usage (
|
|
205
|
+
client_route,
|
|
206
|
+
window_start,
|
|
207
|
+
input_tokens,
|
|
208
|
+
output_tokens,
|
|
209
|
+
total_tokens,
|
|
210
|
+
updated_at
|
|
211
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
212
|
+
ON CONFLICT(client_route, window_start) DO UPDATE SET
|
|
213
|
+
input_tokens = input_tokens + excluded.input_tokens,
|
|
214
|
+
output_tokens = output_tokens + excluded.output_tokens,
|
|
215
|
+
total_tokens = total_tokens + excluded.total_tokens,
|
|
216
|
+
updated_at = excluded.updated_at`)
|
|
217
|
+
.run(clientRoute, windowStart, inputTokens, outputTokens, totalTokens, updatedAt);
|
|
218
|
+
})();
|
|
219
|
+
return this.getClientTokenUsage(clientRoute, now);
|
|
220
|
+
}
|
|
221
|
+
resetClientTokenUsage(client, now = new Date()) {
|
|
222
|
+
const clientRoute = normalizeClientRouteKey(client);
|
|
223
|
+
const config = this.getClientTokenLimit(clientRoute);
|
|
224
|
+
const windowStart = resolveClientTokenWindowStart(now, {
|
|
225
|
+
windowType: config?.windowType ?? "daily",
|
|
226
|
+
windowSizeSeconds: config?.windowSizeSeconds,
|
|
227
|
+
});
|
|
228
|
+
const updatedAt = now.toISOString();
|
|
229
|
+
this.db.transaction(() => {
|
|
230
|
+
this.db
|
|
231
|
+
.prepare(`DELETE FROM client_token_usage
|
|
232
|
+
WHERE client_route = ? AND window_start = ?`)
|
|
233
|
+
.run(clientRoute, windowStart);
|
|
234
|
+
this.db
|
|
235
|
+
.prepare(`INSERT INTO client_token_usage (
|
|
236
|
+
client_route,
|
|
237
|
+
window_start,
|
|
238
|
+
input_tokens,
|
|
239
|
+
output_tokens,
|
|
240
|
+
total_tokens,
|
|
241
|
+
updated_at
|
|
242
|
+
) VALUES (?, ?, 0, 0, 0, ?)`)
|
|
243
|
+
.run(clientRoute, windowStart, updatedAt);
|
|
244
|
+
})();
|
|
245
|
+
return this.getClientTokenUsage(clientRoute, now);
|
|
246
|
+
}
|
|
247
|
+
findClientRouteByApiKey(apiKey) {
|
|
248
|
+
const normalizedApiKey = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
249
|
+
if (!normalizedApiKey) {
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
252
|
+
for (const clientRoute of this.listClientRouteKeys()) {
|
|
253
|
+
if ((this.clientRouteApiKeys[clientRoute] ?? []).includes(normalizedApiKey)) {
|
|
254
|
+
return clientRoute;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
return undefined;
|
|
258
|
+
}
|
|
259
|
+
setClientRoute(client, providerId) {
|
|
260
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
261
|
+
const normalizedProviderId = typeof providerId === "string" ? providerId.trim() : "";
|
|
262
|
+
if (normalizedProviderId) {
|
|
263
|
+
this.getProviderOrThrow(normalizedProviderId);
|
|
264
|
+
this.clientRoutes[routeKey] = normalizedProviderId;
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
delete this.clientRoutes[routeKey];
|
|
268
|
+
}
|
|
269
|
+
this.persistRuntimeState();
|
|
270
|
+
return this.getProviderIdForClient(routeKey);
|
|
271
|
+
}
|
|
272
|
+
setModelOverride(client, model) {
|
|
273
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
274
|
+
const normalized = model?.trim() ? model.trim() : undefined;
|
|
275
|
+
if (normalized) {
|
|
276
|
+
this.modelOverrides[routeKey] = normalized;
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
delete this.modelOverrides[routeKey];
|
|
280
|
+
}
|
|
281
|
+
this.persistRuntimeState();
|
|
282
|
+
return this.getModelOverride(routeKey);
|
|
283
|
+
}
|
|
284
|
+
setClientRouteRtkPolicy(client, policy) {
|
|
285
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
286
|
+
const normalized = cloneRtkLayerPolicy(policy);
|
|
287
|
+
if (normalized &&
|
|
288
|
+
(normalized.enabled !== undefined ||
|
|
289
|
+
normalized.toolOutputEnabled !== undefined ||
|
|
290
|
+
normalized.maxChars !== undefined ||
|
|
291
|
+
normalized.maxLines !== undefined)) {
|
|
292
|
+
this.clientRouteRtkPolicies[routeKey] = normalized;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
delete this.clientRouteRtkPolicies[routeKey];
|
|
296
|
+
}
|
|
297
|
+
this.persistRuntimeState();
|
|
298
|
+
return this.getClientRouteRtkPolicy(routeKey);
|
|
299
|
+
}
|
|
300
|
+
setClientRouteApiKeys(client, apiKeys) {
|
|
301
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
302
|
+
const normalized = normalizeApiKeys(apiKeys ?? []);
|
|
303
|
+
if (normalized.length > 0) {
|
|
304
|
+
this.clientRouteApiKeys[routeKey] = normalized;
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
delete this.clientRouteApiKeys[routeKey];
|
|
308
|
+
}
|
|
309
|
+
this.persistRuntimeState();
|
|
310
|
+
return this.getClientRouteApiKeys(routeKey);
|
|
311
|
+
}
|
|
312
|
+
addClientRoute(client, providerId) {
|
|
313
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
314
|
+
if (this.listClientRouteKeys().includes(routeKey)) {
|
|
315
|
+
throw new RuntimeProviderError(409, {
|
|
316
|
+
type: "validation_error",
|
|
317
|
+
code: "CLIENT_ROUTE_ALREADY_EXISTS",
|
|
318
|
+
message: "Client route already exists",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
return this.setClientRoute(routeKey, providerId || this.activeProviderId);
|
|
322
|
+
}
|
|
323
|
+
deleteClientRoute(client) {
|
|
324
|
+
const routeKey = normalizeClientRouteKey(client);
|
|
325
|
+
if (routeKey === "default") {
|
|
326
|
+
throw new RuntimeProviderError(400, {
|
|
327
|
+
type: "validation_error",
|
|
328
|
+
code: "DEFAULT_CLIENT_ROUTE_REQUIRED",
|
|
329
|
+
message: "The default client route cannot be deleted",
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
delete this.clientRoutes[routeKey];
|
|
333
|
+
delete this.modelOverrides[routeKey];
|
|
334
|
+
delete this.clientRouteRtkPolicies[routeKey];
|
|
335
|
+
delete this.clientRouteApiKeys[routeKey];
|
|
336
|
+
this.deleteClientTokenLimit(routeKey);
|
|
337
|
+
this.db.prepare("DELETE FROM client_token_usage WHERE client_route = ?").run(routeKey);
|
|
338
|
+
this.persistRuntimeState();
|
|
339
|
+
}
|
|
340
|
+
listClientRouteKeys() {
|
|
341
|
+
return [
|
|
342
|
+
...new Set([
|
|
343
|
+
...BUILTIN_CLIENT_ROUTE_KEYS,
|
|
344
|
+
...Object.keys(this.clientRoutes),
|
|
345
|
+
...Object.keys(this.modelOverrides),
|
|
346
|
+
...Object.keys(this.clientRouteRtkPolicies),
|
|
347
|
+
...Object.keys(this.clientRouteApiKeys),
|
|
348
|
+
]),
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
buildFallbackRouteOrder(currentClient) {
|
|
352
|
+
const normalizedCurrent = normalizeClientRouteKey(currentClient);
|
|
353
|
+
const prioritizedRoutes = normalizedCurrent === "codex"
|
|
354
|
+
? ["default", "hermes", "codex"]
|
|
355
|
+
: normalizedCurrent === "default"
|
|
356
|
+
? ["codex", "hermes", "default"]
|
|
357
|
+
: ["codex", "default", "hermes"];
|
|
358
|
+
return [
|
|
359
|
+
...new Set([...prioritizedRoutes, ...this.listClientRouteKeys()].filter((routeKey) => routeKey !== normalizedCurrent)),
|
|
360
|
+
];
|
|
361
|
+
}
|
|
362
|
+
resolveClientRouteApiKeys(value) {
|
|
363
|
+
const next = sanitizeClientRouteApiKeys(value);
|
|
364
|
+
delete next.default;
|
|
365
|
+
return next;
|
|
366
|
+
}
|
|
367
|
+
listProviders() {
|
|
368
|
+
return [...this.providerPresets];
|
|
369
|
+
}
|
|
370
|
+
listProvidersForUi() {
|
|
371
|
+
return this.providerPresets
|
|
372
|
+
.filter((provider) => !provider.capabilities.systemManaged)
|
|
373
|
+
.map((provider) => this.serializeProviderForUi(provider));
|
|
374
|
+
}
|
|
375
|
+
listProviderOptionsForClientSetup() {
|
|
376
|
+
return this.providerPresets.map((provider) => this.serializeProviderOptionForClientSetup(provider));
|
|
377
|
+
}
|
|
378
|
+
getProvider(id) {
|
|
379
|
+
return this.providerPresets.find((provider) => provider.id === id);
|
|
380
|
+
}
|
|
381
|
+
findProviderByProviderApiKey(apiKey) {
|
|
382
|
+
const normalized = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
383
|
+
if (!normalized) {
|
|
384
|
+
return undefined;
|
|
385
|
+
}
|
|
386
|
+
return this.providerPresets.find((provider) => provider.providerApiKeys.includes(normalized));
|
|
387
|
+
}
|
|
388
|
+
findProviderByAccessKey(apiKey) {
|
|
389
|
+
return this.findProvidersByAccessKey(apiKey)[0];
|
|
390
|
+
}
|
|
391
|
+
findProvidersByAccessKey(apiKey) {
|
|
392
|
+
const normalized = typeof apiKey === "string" ? apiKey.trim() : "";
|
|
393
|
+
if (!normalized) {
|
|
394
|
+
return [];
|
|
395
|
+
}
|
|
396
|
+
const clientRoute = this.findClientRouteByApiKey(normalized);
|
|
397
|
+
if (clientRoute) {
|
|
398
|
+
const provider = this.getProviderForClient(clientRoute);
|
|
399
|
+
return provider ? [provider] : [];
|
|
400
|
+
}
|
|
401
|
+
return this.providerPresets.filter((provider) => provider.providerApiKeys.includes(normalized));
|
|
402
|
+
}
|
|
403
|
+
getProviderOrThrow(id) {
|
|
404
|
+
const provider = this.getProvider(id);
|
|
405
|
+
if (!provider) {
|
|
406
|
+
throw new RuntimeProviderError(404, {
|
|
407
|
+
type: "not_found",
|
|
408
|
+
code: "PROVIDER_NOT_FOUND",
|
|
409
|
+
message: "Provider was not found",
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
return provider;
|
|
413
|
+
}
|
|
414
|
+
getProviderForUiOrThrow(id) {
|
|
415
|
+
return this.serializeProviderForUi(this.getProviderOrThrow(id));
|
|
416
|
+
}
|
|
417
|
+
getActiveProvider() {
|
|
418
|
+
return this.getProviderForClient("default");
|
|
419
|
+
}
|
|
420
|
+
getFallbackProvider(client = "default", primaryProviderId) {
|
|
421
|
+
const preferredRouteOrder = this.buildFallbackRouteOrder(client);
|
|
422
|
+
const excludedProviderIds = new Set();
|
|
423
|
+
const normalizedPrimaryProviderId = typeof primaryProviderId === "string" ? primaryProviderId.trim() : "";
|
|
424
|
+
if (normalizedPrimaryProviderId) {
|
|
425
|
+
excludedProviderIds.add(normalizedPrimaryProviderId);
|
|
426
|
+
}
|
|
427
|
+
for (const routeKey of preferredRouteOrder) {
|
|
428
|
+
const provider = this.getProviderForClient(routeKey);
|
|
429
|
+
if (!provider || excludedProviderIds.has(provider.id)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
return provider;
|
|
433
|
+
}
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
selectProvider(id) {
|
|
437
|
+
const provider = this.getProvider(id);
|
|
438
|
+
if (!provider) {
|
|
439
|
+
throw new RuntimeProviderError(400, {
|
|
440
|
+
type: "validation_error",
|
|
441
|
+
code: "INVALID_PROVIDER_ID",
|
|
442
|
+
message: "providerId must match one of the configured runtime providers",
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
this.activeProviderId = provider.id;
|
|
446
|
+
this.clientRoutes.default = provider.id;
|
|
447
|
+
this.persistRuntimeState();
|
|
448
|
+
return provider;
|
|
449
|
+
}
|
|
450
|
+
createProvider(input) {
|
|
451
|
+
const validated = this.parseProviderInput(input);
|
|
452
|
+
this.ensureNoDuplicate(validated);
|
|
453
|
+
const now = new Date().toISOString();
|
|
454
|
+
const provider = {
|
|
455
|
+
id: validated.id ?? `custom-${randomUUID().slice(0, 8)}`,
|
|
456
|
+
name: validated.name,
|
|
457
|
+
baseUrl: validated.baseUrl,
|
|
458
|
+
responsesUrl: validated.responsesUrl,
|
|
459
|
+
authMode: validated.authMode,
|
|
460
|
+
chatgptAccountId: validated.chatgptAccountId,
|
|
461
|
+
providerApiKeys: validated.providerApiKeys,
|
|
462
|
+
clientApiKeys: validated.clientApiKeys,
|
|
463
|
+
capabilities: validated.capabilities,
|
|
464
|
+
enabled: validated.enabled ?? false,
|
|
465
|
+
createdAt: now,
|
|
466
|
+
updatedAt: now,
|
|
467
|
+
};
|
|
468
|
+
this.providerPresets = [...this.providerPresets, provider];
|
|
469
|
+
if (!this.activeProviderId) {
|
|
470
|
+
this.activeProviderId = provider.id;
|
|
471
|
+
}
|
|
472
|
+
if (!this.clientRoutes.default && this.activeProviderId) {
|
|
473
|
+
this.clientRoutes.default = this.activeProviderId;
|
|
474
|
+
}
|
|
475
|
+
this.persistRuntimeState();
|
|
476
|
+
return provider;
|
|
477
|
+
}
|
|
478
|
+
updateProvider(id, input) {
|
|
479
|
+
const existing = this.getProviderOrThrow(id);
|
|
480
|
+
const validated = this.parseProviderInput(input);
|
|
481
|
+
this.ensureNoDuplicate(validated, id);
|
|
482
|
+
const updated = {
|
|
483
|
+
...existing,
|
|
484
|
+
name: validated.name,
|
|
485
|
+
baseUrl: validated.baseUrl,
|
|
486
|
+
responsesUrl: validated.responsesUrl,
|
|
487
|
+
authMode: validated.authMode,
|
|
488
|
+
chatgptAccountId: validated.chatgptAccountId,
|
|
489
|
+
providerApiKeys: validated.providerApiKeys,
|
|
490
|
+
clientApiKeys: validated.clientApiKeys,
|
|
491
|
+
capabilities: validated.capabilities,
|
|
492
|
+
enabled: validated.enabled ?? existing.enabled,
|
|
493
|
+
updatedAt: new Date().toISOString(),
|
|
494
|
+
};
|
|
495
|
+
this.providerPresets = this.providerPresets.map((provider) => provider.id === id ? updated : provider);
|
|
496
|
+
this.persistRuntimeState();
|
|
497
|
+
return updated;
|
|
498
|
+
}
|
|
499
|
+
deleteProvider(id) {
|
|
500
|
+
this.getProviderOrThrow(id);
|
|
501
|
+
this.providerPresets = this.providerPresets.filter((provider) => provider.id !== id);
|
|
502
|
+
for (const key of this.listClientRouteKeys()) {
|
|
503
|
+
if (this.clientRoutes[key] === id) {
|
|
504
|
+
delete this.clientRoutes[key];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (this.activeProviderId === id) {
|
|
508
|
+
this.activeProviderId = this.resolveActiveProviderId();
|
|
509
|
+
if (this.activeProviderId) {
|
|
510
|
+
this.clientRoutes.default = this.activeProviderId;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
delete this.clientRoutes.default;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
this.persistRuntimeState();
|
|
517
|
+
return this.activeProviderId;
|
|
518
|
+
}
|
|
519
|
+
ensureNoDuplicate(input, ignoreId) {
|
|
520
|
+
const duplicate = this.providerPresets.find((provider) => provider.id !== ignoreId &&
|
|
521
|
+
(provider.baseUrl === input.baseUrl ||
|
|
522
|
+
normalizeProviderName(provider.name) === normalizeProviderName(input.name)));
|
|
523
|
+
if (duplicate) {
|
|
524
|
+
throw new RuntimeProviderError(409, {
|
|
525
|
+
type: "validation_error",
|
|
526
|
+
code: "PROVIDER_ALREADY_EXISTS",
|
|
527
|
+
message: "A provider with the same name or base URL already exists",
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
const conflictingProviderApiKey = input.providerApiKeys.find((apiKey) => this.providerPresets.some((provider) => provider.id !== ignoreId && provider.providerApiKeys.includes(apiKey)));
|
|
531
|
+
if (conflictingProviderApiKey) {
|
|
532
|
+
throw new RuntimeProviderError(409, {
|
|
533
|
+
type: "validation_error",
|
|
534
|
+
code: "PROVIDER_API_KEY_ALREADY_EXISTS",
|
|
535
|
+
message: "A provider API key is already assigned to another provider",
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
parseProviderInput(body) {
|
|
540
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
541
|
+
const id = typeof body.id === "string" && body.id.trim() ? body.id.trim() : undefined;
|
|
542
|
+
const baseUrl = typeof body.baseUrl === "string" ? body.baseUrl.trim() : "";
|
|
543
|
+
const responsesUrl = typeof body.responsesUrl === "string" && body.responsesUrl.trim()
|
|
544
|
+
? body.responsesUrl.trim()
|
|
545
|
+
: undefined;
|
|
546
|
+
const providerApiKeys = normalizeApiKeysInput(body.providerApiKeys, body.apiKeys, body.apiKey);
|
|
547
|
+
const authMode = parseRuntimeProviderAuthMode(body.authMode);
|
|
548
|
+
const chatgptAccountId = typeof body.chatgptAccountId === "string" && body.chatgptAccountId.trim()
|
|
549
|
+
? body.chatgptAccountId.trim()
|
|
550
|
+
: undefined;
|
|
551
|
+
if (!name) {
|
|
552
|
+
throw new RuntimeProviderError(400, {
|
|
553
|
+
type: "validation_error",
|
|
554
|
+
code: "INVALID_NAME",
|
|
555
|
+
message: "name is required",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
let parsedBaseUrl;
|
|
559
|
+
let parsedResponsesUrl;
|
|
560
|
+
try {
|
|
561
|
+
parsedBaseUrl = new URL(baseUrl);
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
throw new RuntimeProviderError(400, {
|
|
565
|
+
type: "validation_error",
|
|
566
|
+
code: "INVALID_BASE_URL",
|
|
567
|
+
message: "baseUrl must be a valid URL",
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (responsesUrl) {
|
|
571
|
+
try {
|
|
572
|
+
parsedResponsesUrl = new URL(responsesUrl);
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
throw new RuntimeProviderError(400, {
|
|
576
|
+
type: "validation_error",
|
|
577
|
+
code: "INVALID_RESPONSES_URL",
|
|
578
|
+
message: "responsesUrl must be a valid URL",
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
name,
|
|
584
|
+
id,
|
|
585
|
+
baseUrl: parsedBaseUrl.toString().replace(/\/+$/, ""),
|
|
586
|
+
responsesUrl: (parsedResponsesUrl ?? new URL(toResponsesUrl(parsedBaseUrl.toString().replace(/\/+$/, ""))))
|
|
587
|
+
.toString()
|
|
588
|
+
.replace(/\/+$/, ""),
|
|
589
|
+
authMode,
|
|
590
|
+
chatgptAccountId,
|
|
591
|
+
providerApiKeys,
|
|
592
|
+
clientApiKeys: [],
|
|
593
|
+
capabilities: parseProviderCapabilitiesInput(body.capabilities),
|
|
594
|
+
enabled: body.enabled === false || body.enabled === "false" ? false : body.enabled === true || body.enabled === "true" ? true : undefined,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
serializeProviderForUi(provider) {
|
|
598
|
+
return {
|
|
599
|
+
id: provider.id,
|
|
600
|
+
name: provider.name,
|
|
601
|
+
baseUrl: provider.baseUrl,
|
|
602
|
+
hasProviderApiKey: provider.providerApiKeys.length > 0,
|
|
603
|
+
providerApiKeys: [...provider.providerApiKeys],
|
|
604
|
+
providerApiKeysCount: provider.providerApiKeys.length,
|
|
605
|
+
authMode: parseRuntimeProviderAuthMode(provider.authMode),
|
|
606
|
+
chatgptAccountId: provider.chatgptAccountId ?? null,
|
|
607
|
+
capabilities: cloneCapabilities(provider.capabilities),
|
|
608
|
+
enabled: provider.enabled !== false,
|
|
609
|
+
createdAt: provider.createdAt ?? null,
|
|
610
|
+
updatedAt: provider.updatedAt ?? null,
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
serializeProviderOptionForClientSetup(provider) {
|
|
614
|
+
const view = this.serializeProviderForUi(provider);
|
|
615
|
+
return {
|
|
616
|
+
...view,
|
|
617
|
+
providerApiKeys: [],
|
|
618
|
+
};
|
|
619
|
+
}
|
|
620
|
+
resolveActiveProviderId(preferredId) {
|
|
621
|
+
if (preferredId && this.providerPresets.some((provider) => provider.id === preferredId)) {
|
|
622
|
+
return preferredId;
|
|
623
|
+
}
|
|
624
|
+
return this.providerPresets[0]?.id ?? "";
|
|
625
|
+
}
|
|
626
|
+
resolveClientRoutes(routes) {
|
|
627
|
+
const resolved = sanitizeClientRoutes(routes);
|
|
628
|
+
if (!resolved.default && this.activeProviderId) {
|
|
629
|
+
resolved.default = this.activeProviderId;
|
|
630
|
+
}
|
|
631
|
+
return Object.fromEntries(Object.entries(resolved).filter(([, providerId]) => typeof providerId === "string" &&
|
|
632
|
+
this.providerPresets.some((provider) => provider.id === providerId)));
|
|
633
|
+
}
|
|
634
|
+
resolveModelOverrides(overrides, legacyDefault) {
|
|
635
|
+
const resolved = sanitizeModelOverrides(overrides);
|
|
636
|
+
if (!resolved.default && typeof legacyDefault === "string" && legacyDefault.trim()) {
|
|
637
|
+
resolved.default = legacyDefault.trim();
|
|
638
|
+
}
|
|
639
|
+
return resolved;
|
|
640
|
+
}
|
|
641
|
+
resolveClientRouteRtkPolicies(policies) {
|
|
642
|
+
if (!policies) {
|
|
643
|
+
return {};
|
|
644
|
+
}
|
|
645
|
+
return Object.fromEntries(Object.entries(policies)
|
|
646
|
+
.map(([clientRoute, policy]) => [normalizeClientRouteKey(clientRoute), cloneRtkLayerPolicy(policy)])
|
|
647
|
+
.filter((entry) => Boolean(entry[1])));
|
|
648
|
+
}
|
|
649
|
+
persistRuntimeState() {
|
|
650
|
+
this.db.exec("BEGIN");
|
|
651
|
+
try {
|
|
652
|
+
this.db.exec("DELETE FROM provider_api_keys");
|
|
653
|
+
this.db.exec("DELETE FROM client_api_keys");
|
|
654
|
+
this.db.exec("DELETE FROM providers");
|
|
655
|
+
this.db.exec("DELETE FROM client_routes");
|
|
656
|
+
this.db.exec("DELETE FROM model_overrides");
|
|
657
|
+
this.db.exec("DELETE FROM client_route_rtk_policies");
|
|
658
|
+
this.db.exec("DELETE FROM app_state");
|
|
659
|
+
const insertProvider = this.db.prepare(`
|
|
660
|
+
INSERT INTO providers (
|
|
661
|
+
id,
|
|
662
|
+
name,
|
|
663
|
+
base_url,
|
|
664
|
+
responses_url,
|
|
665
|
+
auth_mode,
|
|
666
|
+
chatgpt_account_id,
|
|
667
|
+
owned_by,
|
|
668
|
+
usage_check_enabled,
|
|
669
|
+
usage_check_url,
|
|
670
|
+
strip_max_output_tokens,
|
|
671
|
+
request_parameter_policy,
|
|
672
|
+
sanitize_reasoning_summary,
|
|
673
|
+
preserve_messages_payload,
|
|
674
|
+
preserve_raw_request_body,
|
|
675
|
+
transport_mode,
|
|
676
|
+
strip_model_prefixes,
|
|
677
|
+
model_aliases,
|
|
678
|
+
rtk_policy,
|
|
679
|
+
error_policy,
|
|
680
|
+
system_managed,
|
|
681
|
+
account_platform,
|
|
682
|
+
account_pool_required,
|
|
683
|
+
enabled,
|
|
684
|
+
created_at,
|
|
685
|
+
updated_at
|
|
686
|
+
)
|
|
687
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
688
|
+
`);
|
|
689
|
+
const insertApiKey = this.db.prepare(`
|
|
690
|
+
INSERT INTO provider_api_keys (provider_id, api_key, position)
|
|
691
|
+
VALUES (?, ?, ?)
|
|
692
|
+
`);
|
|
693
|
+
const insertClientRoute = this.db.prepare(`
|
|
694
|
+
INSERT INTO client_routes (client_route, provider_id)
|
|
695
|
+
VALUES (?, ?)
|
|
696
|
+
`);
|
|
697
|
+
const insertModelOverride = this.db.prepare(`
|
|
698
|
+
INSERT INTO model_overrides (client_route, model)
|
|
699
|
+
VALUES (?, ?)
|
|
700
|
+
`);
|
|
701
|
+
const insertClientRouteRtkPolicy = this.db.prepare(`
|
|
702
|
+
INSERT INTO client_route_rtk_policies (client_route, policy)
|
|
703
|
+
VALUES (?, ?)
|
|
704
|
+
`);
|
|
705
|
+
const insertAppState = this.db.prepare(`
|
|
706
|
+
INSERT INTO app_state (key, value)
|
|
707
|
+
VALUES (?, ?)
|
|
708
|
+
`);
|
|
709
|
+
for (const provider of this.providerPresets) {
|
|
710
|
+
insertProvider.run(provider.id, provider.name, provider.baseUrl, provider.responsesUrl, parseRuntimeProviderAuthMode(provider.authMode), provider.chatgptAccountId ?? null, provider.capabilities.ownedBy ?? null, provider.capabilities.usageCheckEnabled ? 1 : 0, provider.capabilities.usageCheckUrl ?? null, provider.capabilities.stripMaxOutputTokens ? 1 : 0, JSON.stringify(cloneProviderRequestParameterPolicy(provider.capabilities.requestParameterPolicy)), provider.capabilities.sanitizeReasoningSummary ? 1 : 0, provider.capabilities.preserveMessagesPayload ? 1 : 0, provider.capabilities.preserveRawRequestBody ? 1 : 0, provider.capabilities.transportMode ?? "responses", JSON.stringify(provider.capabilities.stripModelPrefixes), JSON.stringify(provider.capabilities.modelAliases ?? {}), JSON.stringify(cloneRtkLayerPolicy(provider.capabilities.rtkPolicy) ?? {}), JSON.stringify(cloneProviderErrorPolicy(provider.capabilities.errorPolicy) ?? {}), provider.capabilities.systemManaged ? 1 : 0, provider.capabilities.accountPlatform ?? null, provider.capabilities.accountPoolRequired ? 1 : 0, provider.enabled !== false ? 1 : 0, provider.createdAt ?? null, provider.updatedAt ?? null);
|
|
711
|
+
provider.providerApiKeys.forEach((apiKey, index) => {
|
|
712
|
+
insertApiKey.run(provider.id, apiKey, index);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
Object.entries(this.clientRoutes).forEach(([clientRoute, providerId]) => {
|
|
716
|
+
insertClientRoute.run(clientRoute, providerId);
|
|
717
|
+
});
|
|
718
|
+
Object.entries(this.modelOverrides).forEach(([clientRoute, model]) => {
|
|
719
|
+
insertModelOverride.run(clientRoute, model);
|
|
720
|
+
});
|
|
721
|
+
Object.entries(this.clientRouteRtkPolicies).forEach(([clientRoute, policy]) => {
|
|
722
|
+
insertClientRouteRtkPolicy.run(clientRoute, JSON.stringify(cloneRtkLayerPolicy(policy) ?? {}));
|
|
723
|
+
});
|
|
724
|
+
insertAppState.run("active_provider_id", this.activeProviderId);
|
|
725
|
+
insertAppState.run("model_override", this.modelOverrides.default ?? "");
|
|
726
|
+
insertAppState.run("client_route_api_keys", JSON.stringify(this.clientRouteApiKeys));
|
|
727
|
+
this.db.exec("COMMIT");
|
|
728
|
+
}
|
|
729
|
+
catch (error) {
|
|
730
|
+
this.db.exec("ROLLBACK");
|
|
731
|
+
throw error;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
export const KIRO_PROVIDER_ID = "account-kiro";
|
|
736
|
+
export function buildBuiltinProviderPresets(config) {
|
|
737
|
+
const presets = [];
|
|
738
|
+
// Only seed a primary upstream provider if the URL is a real endpoint
|
|
739
|
+
// (skip .invalid, .example, .test reserved domains per RFC 2606)
|
|
740
|
+
const upstreamHostname = (() => {
|
|
741
|
+
try {
|
|
742
|
+
return new URL(config.UPSTREAM_BASE_URL).hostname.toLowerCase();
|
|
743
|
+
}
|
|
744
|
+
catch {
|
|
745
|
+
return "";
|
|
746
|
+
}
|
|
747
|
+
})();
|
|
748
|
+
const isPlaceholder = upstreamHostname.endsWith(".invalid") ||
|
|
749
|
+
upstreamHostname.endsWith(".example") ||
|
|
750
|
+
upstreamHostname.endsWith(".test") ||
|
|
751
|
+
upstreamHostname === "";
|
|
752
|
+
if (!isPlaceholder) {
|
|
753
|
+
const primaryIdentity = inferProviderIdentity(config.UPSTREAM_BASE_URL, "");
|
|
754
|
+
presets.push({
|
|
755
|
+
id: primaryIdentity.id,
|
|
756
|
+
name: primaryIdentity.name,
|
|
757
|
+
baseUrl: config.UPSTREAM_BASE_URL,
|
|
758
|
+
responsesUrl: config.upstreamResponsesUrl,
|
|
759
|
+
authMode: "api_key",
|
|
760
|
+
providerApiKeys: normalizeApiKeys(config.UPSTREAM_API_KEY ? [config.UPSTREAM_API_KEY] : []),
|
|
761
|
+
clientApiKeys: [],
|
|
762
|
+
capabilities: buildDefaultCapabilitiesFromConfig(config),
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
if (config.KIRO_ENABLED) {
|
|
766
|
+
presets.push(buildKiroProviderPreset(config));
|
|
767
|
+
}
|
|
768
|
+
return ensureUniqueProviderIds(presets);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* System-managed provider that serves requests through a Kiro (AWS CodeWhisperer)
|
|
772
|
+
* account from 9router. Accounts/tokens live in 9router's own DB; this preset only
|
|
773
|
+
* declares routing + the CodeWhisperer transport so the forward path can translate.
|
|
774
|
+
*/
|
|
775
|
+
function buildKiroProviderPreset(config) {
|
|
776
|
+
const region = config.KIRO_DEFAULT_REGION;
|
|
777
|
+
const baseUrl = `https://codewhisperer.${region}.amazonaws.com`;
|
|
778
|
+
return {
|
|
779
|
+
id: KIRO_PROVIDER_ID,
|
|
780
|
+
name: "Kiro (9router)",
|
|
781
|
+
baseUrl,
|
|
782
|
+
responsesUrl: `${baseUrl}${CODEWHISPERER_GENERATE_PATH}`,
|
|
783
|
+
authMode: "kiro",
|
|
784
|
+
enabled: false,
|
|
785
|
+
providerApiKeys: [],
|
|
786
|
+
clientApiKeys: [],
|
|
787
|
+
capabilities: {
|
|
788
|
+
systemManaged: true,
|
|
789
|
+
accountPlatform: "kiro",
|
|
790
|
+
accountPoolRequired: true,
|
|
791
|
+
usageCheckEnabled: false,
|
|
792
|
+
stripMaxOutputTokens: false,
|
|
793
|
+
requestParameterPolicy: {},
|
|
794
|
+
sanitizeReasoningSummary: false,
|
|
795
|
+
transportMode: "codewhisperer",
|
|
796
|
+
stripModelPrefixes: [],
|
|
797
|
+
modelAliases: { ...DEFAULT_KIRO_MODEL_ALIASES },
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
function openDatabase(dbFile) {
|
|
802
|
+
mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
803
|
+
return new BetterSqlite3(dbFile);
|
|
804
|
+
}
|
|
805
|
+
function ensureSchema(db) {
|
|
806
|
+
db.pragma("foreign_keys = ON");
|
|
807
|
+
db.exec(`
|
|
808
|
+
CREATE TABLE IF NOT EXISTS providers (
|
|
809
|
+
id TEXT PRIMARY KEY,
|
|
810
|
+
name TEXT NOT NULL,
|
|
811
|
+
base_url TEXT NOT NULL,
|
|
812
|
+
responses_url TEXT NOT NULL,
|
|
813
|
+
auth_mode TEXT NOT NULL DEFAULT 'api_key',
|
|
814
|
+
chatgpt_account_id TEXT,
|
|
815
|
+
owned_by TEXT,
|
|
816
|
+
usage_check_enabled INTEGER NOT NULL DEFAULT 0,
|
|
817
|
+
usage_check_url TEXT,
|
|
818
|
+
strip_max_output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
819
|
+
request_parameter_policy TEXT NOT NULL DEFAULT '{}',
|
|
820
|
+
sanitize_reasoning_summary INTEGER NOT NULL DEFAULT 0,
|
|
821
|
+
preserve_messages_payload INTEGER NOT NULL DEFAULT 0,
|
|
822
|
+
preserve_raw_request_body INTEGER NOT NULL DEFAULT 0,
|
|
823
|
+
transport_mode TEXT NOT NULL DEFAULT 'responses',
|
|
824
|
+
strip_model_prefixes TEXT NOT NULL DEFAULT '[]',
|
|
825
|
+
model_aliases TEXT NOT NULL DEFAULT '{}',
|
|
826
|
+
rtk_policy TEXT NOT NULL DEFAULT '{}',
|
|
827
|
+
error_policy TEXT NOT NULL DEFAULT '{}',
|
|
828
|
+
system_managed INTEGER NOT NULL DEFAULT 0,
|
|
829
|
+
account_platform TEXT,
|
|
830
|
+
account_pool_required INTEGER NOT NULL DEFAULT 0,
|
|
831
|
+
created_at TEXT,
|
|
832
|
+
updated_at TEXT
|
|
833
|
+
);
|
|
834
|
+
|
|
835
|
+
CREATE TABLE IF NOT EXISTS provider_api_keys (
|
|
836
|
+
provider_id TEXT NOT NULL,
|
|
837
|
+
api_key TEXT NOT NULL,
|
|
838
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
839
|
+
PRIMARY KEY (provider_id, api_key),
|
|
840
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
CREATE TABLE IF NOT EXISTS client_api_keys (
|
|
844
|
+
provider_id TEXT NOT NULL,
|
|
845
|
+
api_key TEXT NOT NULL,
|
|
846
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
847
|
+
PRIMARY KEY (provider_id, api_key),
|
|
848
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
849
|
+
);
|
|
850
|
+
|
|
851
|
+
CREATE TABLE IF NOT EXISTS client_routes (
|
|
852
|
+
client_route TEXT PRIMARY KEY,
|
|
853
|
+
provider_id TEXT NOT NULL,
|
|
854
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
CREATE TABLE IF NOT EXISTS model_overrides (
|
|
858
|
+
client_route TEXT PRIMARY KEY,
|
|
859
|
+
model TEXT NOT NULL
|
|
860
|
+
);
|
|
861
|
+
|
|
862
|
+
CREATE TABLE IF NOT EXISTS client_route_rtk_policies (
|
|
863
|
+
client_route TEXT PRIMARY KEY,
|
|
864
|
+
policy TEXT NOT NULL DEFAULT '{}'
|
|
865
|
+
);
|
|
866
|
+
|
|
867
|
+
CREATE TABLE IF NOT EXISTS app_state (
|
|
868
|
+
key TEXT PRIMARY KEY,
|
|
869
|
+
value TEXT NOT NULL
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
CREATE TABLE IF NOT EXISTS client_token_limits (
|
|
873
|
+
client_route TEXT PRIMARY KEY,
|
|
874
|
+
enabled INTEGER NOT NULL DEFAULT 0,
|
|
875
|
+
token_limit INTEGER NOT NULL,
|
|
876
|
+
window_type TEXT NOT NULL,
|
|
877
|
+
window_size_seconds INTEGER,
|
|
878
|
+
hard_block INTEGER NOT NULL DEFAULT 1,
|
|
879
|
+
created_at TEXT NOT NULL,
|
|
880
|
+
updated_at TEXT NOT NULL
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
CREATE TABLE IF NOT EXISTS client_token_usage (
|
|
884
|
+
client_route TEXT NOT NULL,
|
|
885
|
+
window_start TEXT NOT NULL,
|
|
886
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
887
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
888
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
889
|
+
updated_at TEXT NOT NULL,
|
|
890
|
+
PRIMARY KEY (client_route, window_start)
|
|
891
|
+
);
|
|
892
|
+
|
|
893
|
+
-- Routing combo tables
|
|
894
|
+
CREATE TABLE IF NOT EXISTS routing_combos (
|
|
895
|
+
id TEXT PRIMARY KEY,
|
|
896
|
+
name TEXT NOT NULL,
|
|
897
|
+
description TEXT,
|
|
898
|
+
is_active INTEGER NOT NULL DEFAULT 1,
|
|
899
|
+
is_default INTEGER NOT NULL DEFAULT 0,
|
|
900
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
901
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
902
|
+
);
|
|
903
|
+
|
|
904
|
+
CREATE TABLE IF NOT EXISTS routing_tiers (
|
|
905
|
+
id TEXT PRIMARY KEY,
|
|
906
|
+
combo_id TEXT NOT NULL,
|
|
907
|
+
name TEXT NOT NULL,
|
|
908
|
+
priority INTEGER NOT NULL,
|
|
909
|
+
tier TEXT NOT NULL,
|
|
910
|
+
is_enabled INTEGER NOT NULL DEFAULT 1,
|
|
911
|
+
fallback_delay INTEGER NOT NULL DEFAULT 1000,
|
|
912
|
+
max_retries INTEGER NOT NULL DEFAULT 2,
|
|
913
|
+
health_threshold TEXT,
|
|
914
|
+
FOREIGN KEY (combo_id) REFERENCES routing_combos(id) ON DELETE CASCADE
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
CREATE TABLE IF NOT EXISTS routing_tier_providers (
|
|
918
|
+
id TEXT PRIMARY KEY,
|
|
919
|
+
tier_id TEXT NOT NULL,
|
|
920
|
+
provider_id TEXT NOT NULL,
|
|
921
|
+
weight INTEGER NOT NULL DEFAULT 50,
|
|
922
|
+
is_enabled INTEGER NOT NULL DEFAULT 1,
|
|
923
|
+
model_override TEXT,
|
|
924
|
+
FOREIGN KEY (tier_id) REFERENCES routing_tiers(id) ON DELETE CASCADE,
|
|
925
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
CREATE TABLE IF NOT EXISTS routing_combo_policies (
|
|
929
|
+
combo_id TEXT PRIMARY KEY,
|
|
930
|
+
load_balancing TEXT NOT NULL DEFAULT 'weighted',
|
|
931
|
+
failover_strategy TEXT NOT NULL DEFAULT 'immediate',
|
|
932
|
+
token_budget_mode TEXT NOT NULL DEFAULT 'per_route',
|
|
933
|
+
quota_management TEXT,
|
|
934
|
+
cost_optimization TEXT,
|
|
935
|
+
retry_policy TEXT,
|
|
936
|
+
FOREIGN KEY (combo_id) REFERENCES routing_combos(id) ON DELETE CASCADE
|
|
937
|
+
);
|
|
938
|
+
|
|
939
|
+
CREATE TABLE IF NOT EXISTS routing_combo_client_routes (
|
|
940
|
+
combo_id TEXT NOT NULL,
|
|
941
|
+
client_route TEXT NOT NULL,
|
|
942
|
+
PRIMARY KEY (combo_id, client_route),
|
|
943
|
+
FOREIGN KEY (combo_id) REFERENCES routing_combos(id) ON DELETE CASCADE
|
|
944
|
+
);
|
|
945
|
+
`);
|
|
946
|
+
ensureProvidersColumn(db, "owned_by", "TEXT");
|
|
947
|
+
ensureProvidersColumn(db, "auth_mode", "TEXT NOT NULL DEFAULT 'api_key'");
|
|
948
|
+
ensureProvidersColumn(db, "chatgpt_account_id", "TEXT");
|
|
949
|
+
ensureProvidersColumn(db, "usage_check_enabled", "INTEGER NOT NULL DEFAULT 0");
|
|
950
|
+
ensureProvidersColumn(db, "usage_check_url", "TEXT");
|
|
951
|
+
ensureProvidersColumn(db, "strip_max_output_tokens", "INTEGER NOT NULL DEFAULT 0");
|
|
952
|
+
ensureProvidersColumn(db, "request_parameter_policy", "TEXT NOT NULL DEFAULT '{}'");
|
|
953
|
+
ensureProvidersColumn(db, "sanitize_reasoning_summary", "INTEGER NOT NULL DEFAULT 0");
|
|
954
|
+
ensureProvidersColumn(db, "preserve_messages_payload", "INTEGER NOT NULL DEFAULT 0");
|
|
955
|
+
ensureProvidersColumn(db, "preserve_raw_request_body", "INTEGER NOT NULL DEFAULT 0");
|
|
956
|
+
ensureProvidersColumn(db, "transport_mode", "TEXT NOT NULL DEFAULT 'responses'");
|
|
957
|
+
ensureProvidersColumn(db, "strip_model_prefixes", "TEXT NOT NULL DEFAULT '[]'");
|
|
958
|
+
ensureProvidersColumn(db, "model_aliases", "TEXT NOT NULL DEFAULT '{}'");
|
|
959
|
+
ensureProvidersColumn(db, "rtk_policy", "TEXT NOT NULL DEFAULT '{}'");
|
|
960
|
+
ensureProvidersColumn(db, "error_policy", "TEXT NOT NULL DEFAULT '{}'");
|
|
961
|
+
ensureProvidersColumn(db, "system_managed", "INTEGER NOT NULL DEFAULT 0");
|
|
962
|
+
ensureProvidersColumn(db, "account_platform", "TEXT");
|
|
963
|
+
ensureProvidersColumn(db, "account_pool_required", "INTEGER NOT NULL DEFAULT 0");
|
|
964
|
+
ensureProvidersColumn(db, "enabled", "INTEGER NOT NULL DEFAULT 0");
|
|
965
|
+
ensureSharedApiKeyTable(db, "provider_api_keys");
|
|
966
|
+
ensureSharedApiKeyTable(db, "client_api_keys");
|
|
967
|
+
}
|
|
968
|
+
function readStateFromDatabase(db) {
|
|
969
|
+
const providerRows = queryRows(db, `SELECT
|
|
970
|
+
id,
|
|
971
|
+
name,
|
|
972
|
+
base_url,
|
|
973
|
+
responses_url,
|
|
974
|
+
auth_mode,
|
|
975
|
+
chatgpt_account_id,
|
|
976
|
+
owned_by,
|
|
977
|
+
usage_check_enabled,
|
|
978
|
+
usage_check_url,
|
|
979
|
+
strip_max_output_tokens,
|
|
980
|
+
request_parameter_policy,
|
|
981
|
+
sanitize_reasoning_summary,
|
|
982
|
+
preserve_messages_payload,
|
|
983
|
+
preserve_raw_request_body,
|
|
984
|
+
transport_mode,
|
|
985
|
+
strip_model_prefixes,
|
|
986
|
+
model_aliases,
|
|
987
|
+
rtk_policy,
|
|
988
|
+
error_policy,
|
|
989
|
+
system_managed,
|
|
990
|
+
account_platform,
|
|
991
|
+
account_pool_required,
|
|
992
|
+
enabled,
|
|
993
|
+
created_at,
|
|
994
|
+
updated_at
|
|
995
|
+
FROM providers
|
|
996
|
+
ORDER BY name, id`);
|
|
997
|
+
const apiKeyRows = queryRows(db, "SELECT provider_id, api_key FROM provider_api_keys ORDER BY provider_id, position, api_key");
|
|
998
|
+
const clientRouteRows = queryRows(db, "SELECT client_route, provider_id FROM client_routes ORDER BY client_route");
|
|
999
|
+
const modelOverrideRows = queryRows(db, "SELECT client_route, model FROM model_overrides ORDER BY client_route");
|
|
1000
|
+
const clientRouteRtkPolicyRows = queryRows(db, "SELECT client_route, policy FROM client_route_rtk_policies ORDER BY client_route");
|
|
1001
|
+
const appStateRows = queryRows(db, "SELECT key, value FROM app_state ORDER BY key");
|
|
1002
|
+
const providerApiKeysByProvider = new Map();
|
|
1003
|
+
for (const row of apiKeyRows) {
|
|
1004
|
+
const current = providerApiKeysByProvider.get(row.provider_id) ?? [];
|
|
1005
|
+
current.push(row.api_key);
|
|
1006
|
+
providerApiKeysByProvider.set(row.provider_id, current);
|
|
1007
|
+
}
|
|
1008
|
+
const providers = providerRows.map((row) => ({
|
|
1009
|
+
id: row.id,
|
|
1010
|
+
name: row.name,
|
|
1011
|
+
baseUrl: row.base_url,
|
|
1012
|
+
responsesUrl: row.responses_url,
|
|
1013
|
+
authMode: parseRuntimeProviderAuthMode(row.auth_mode),
|
|
1014
|
+
chatgptAccountId: row.chatgpt_account_id?.trim() ? row.chatgpt_account_id.trim() : undefined,
|
|
1015
|
+
providerApiKeys: providerApiKeysByProvider.get(row.id) ?? [],
|
|
1016
|
+
clientApiKeys: [],
|
|
1017
|
+
enabled: row.enabled !== 0,
|
|
1018
|
+
capabilities: {
|
|
1019
|
+
ownedBy: row.owned_by ?? undefined,
|
|
1020
|
+
systemManaged: row.system_managed === 1,
|
|
1021
|
+
accountPlatform: row.account_platform?.trim() ? row.account_platform.trim() : undefined,
|
|
1022
|
+
accountPoolRequired: row.account_pool_required === 1,
|
|
1023
|
+
usageCheckEnabled: row.usage_check_enabled === 1,
|
|
1024
|
+
usageCheckUrl: row.usage_check_url?.trim() ? row.usage_check_url.trim() : undefined,
|
|
1025
|
+
stripMaxOutputTokens: row.strip_max_output_tokens === 1,
|
|
1026
|
+
requestParameterPolicy: normalizeStoredRequestParameterPolicy(row.request_parameter_policy, row.strip_max_output_tokens === 1),
|
|
1027
|
+
sanitizeReasoningSummary: row.sanitize_reasoning_summary === 1,
|
|
1028
|
+
preserveMessagesPayload: row.preserve_messages_payload === 1,
|
|
1029
|
+
preserveRawRequestBody: row.preserve_raw_request_body === 1,
|
|
1030
|
+
transportMode: normalizeTransportMode(row.transport_mode),
|
|
1031
|
+
stripModelPrefixes: normalizeStringList(row.strip_model_prefixes),
|
|
1032
|
+
modelAliases: normalizeStringMap(row.model_aliases),
|
|
1033
|
+
rtkPolicy: parseRtkLayerPolicyInput(safeJsonParse(row.rtk_policy ?? "{}")),
|
|
1034
|
+
errorPolicy: parseProviderErrorPolicyInput(safeJsonParse(row.error_policy ?? "{}")),
|
|
1035
|
+
},
|
|
1036
|
+
createdAt: row.created_at ?? undefined,
|
|
1037
|
+
updatedAt: row.updated_at ?? undefined,
|
|
1038
|
+
}));
|
|
1039
|
+
const appState = new Map(appStateRows.map((row) => [row.key, row.value]));
|
|
1040
|
+
const clientRoutes = Object.fromEntries(clientRouteRows.map((row) => [row.client_route, row.provider_id]));
|
|
1041
|
+
const modelOverrides = Object.fromEntries(modelOverrideRows.map((row) => [row.client_route, row.model]));
|
|
1042
|
+
const clientRouteRtkPolicies = Object.fromEntries(clientRouteRtkPolicyRows
|
|
1043
|
+
.map((row) => [row.client_route, parseRtkLayerPolicyInput(safeJsonParse(row.policy))])
|
|
1044
|
+
.filter((entry) => Boolean(entry[1])));
|
|
1045
|
+
const modelOverride = appState.get("model_override");
|
|
1046
|
+
const clientRouteApiKeys = sanitizeClientRouteApiKeys(safeJsonParse(appState.get("client_route_api_keys") ?? "{}"));
|
|
1047
|
+
return {
|
|
1048
|
+
providers,
|
|
1049
|
+
activeProviderId: appState.get("active_provider_id"),
|
|
1050
|
+
modelOverride: modelOverride?.trim() ? modelOverride : undefined,
|
|
1051
|
+
modelOverrides,
|
|
1052
|
+
clientRoutes,
|
|
1053
|
+
clientRouteRtkPolicies,
|
|
1054
|
+
clientRouteApiKeys,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function queryRows(db, sql) {
|
|
1058
|
+
return db.prepare(sql).all();
|
|
1059
|
+
}
|
|
1060
|
+
function mapClientTokenLimitRow(row) {
|
|
1061
|
+
return {
|
|
1062
|
+
clientRoute: normalizeClientRouteKey(row.client_route),
|
|
1063
|
+
enabled: row.enabled === 1,
|
|
1064
|
+
tokenLimit: Math.max(0, Number(row.token_limit ?? 0)),
|
|
1065
|
+
windowType: normalizeClientTokenWindowType(row.window_type),
|
|
1066
|
+
windowSizeSeconds: typeof row.window_size_seconds === "number" && row.window_size_seconds > 0
|
|
1067
|
+
? row.window_size_seconds
|
|
1068
|
+
: undefined,
|
|
1069
|
+
hardBlock: row.hard_block !== 0,
|
|
1070
|
+
createdAt: row.created_at ?? new Date(0).toISOString(),
|
|
1071
|
+
updatedAt: row.updated_at ?? row.created_at ?? new Date(0).toISOString(),
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
function mapClientTokenUsageRow(row) {
|
|
1075
|
+
return {
|
|
1076
|
+
clientRoute: normalizeClientRouteKey(row.client_route),
|
|
1077
|
+
windowStart: row.window_start,
|
|
1078
|
+
inputTokens: Math.max(0, Number(row.input_tokens ?? 0)),
|
|
1079
|
+
outputTokens: Math.max(0, Number(row.output_tokens ?? 0)),
|
|
1080
|
+
totalTokens: Math.max(0, Number(row.total_tokens ?? 0)),
|
|
1081
|
+
updatedAt: row.updated_at ?? new Date(0).toISOString(),
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function buildEmptyClientTokenUsageSnapshot(clientRoute, windowStart) {
|
|
1085
|
+
return {
|
|
1086
|
+
clientRoute,
|
|
1087
|
+
windowStart,
|
|
1088
|
+
inputTokens: 0,
|
|
1089
|
+
outputTokens: 0,
|
|
1090
|
+
totalTokens: 0,
|
|
1091
|
+
updatedAt: windowStart,
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
function normalizeClientTokenWindowType(value) {
|
|
1095
|
+
switch (value) {
|
|
1096
|
+
case "weekly":
|
|
1097
|
+
case "monthly":
|
|
1098
|
+
case "fixed":
|
|
1099
|
+
return value;
|
|
1100
|
+
case "daily":
|
|
1101
|
+
default:
|
|
1102
|
+
return "daily";
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function normalizeNonNegativeInteger(value) {
|
|
1106
|
+
const parsed = Number(value);
|
|
1107
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1108
|
+
return 0;
|
|
1109
|
+
}
|
|
1110
|
+
return Math.floor(parsed);
|
|
1111
|
+
}
|
|
1112
|
+
function ensureSharedApiKeyTable(db, tableName) {
|
|
1113
|
+
const tableSql = db
|
|
1114
|
+
.prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?")
|
|
1115
|
+
.get(tableName);
|
|
1116
|
+
if (!tableSql?.sql?.includes("api_key TEXT NOT NULL UNIQUE")) {
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
const tempTableName = `${tableName}_legacy_unique`;
|
|
1120
|
+
db.exec(`
|
|
1121
|
+
ALTER TABLE ${tableName} RENAME TO ${tempTableName};
|
|
1122
|
+
|
|
1123
|
+
CREATE TABLE ${tableName} (
|
|
1124
|
+
provider_id TEXT NOT NULL,
|
|
1125
|
+
api_key TEXT NOT NULL,
|
|
1126
|
+
position INTEGER NOT NULL DEFAULT 0,
|
|
1127
|
+
PRIMARY KEY (provider_id, api_key),
|
|
1128
|
+
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
INSERT OR IGNORE INTO ${tableName} (provider_id, api_key, position)
|
|
1132
|
+
SELECT provider_id, api_key, position
|
|
1133
|
+
FROM ${tempTableName};
|
|
1134
|
+
|
|
1135
|
+
DROP TABLE ${tempTableName};
|
|
1136
|
+
`);
|
|
1137
|
+
}
|
|
1138
|
+
function loadLegacyState(stateFile) {
|
|
1139
|
+
try {
|
|
1140
|
+
const raw = readFileSync(stateFile, "utf8");
|
|
1141
|
+
const parsed = JSON.parse(raw);
|
|
1142
|
+
const providers = Array.isArray(parsed.providers)
|
|
1143
|
+
? parsed.providers
|
|
1144
|
+
.map((item) => sanitizeProvider(item))
|
|
1145
|
+
.filter((item) => Boolean(item))
|
|
1146
|
+
: Array.isArray(parsed.customProviders)
|
|
1147
|
+
? (parsed.customProviders ?? [])
|
|
1148
|
+
.map((item) => sanitizeCustomProvider(item))
|
|
1149
|
+
.filter((item) => Boolean(item))
|
|
1150
|
+
: [];
|
|
1151
|
+
return {
|
|
1152
|
+
providers,
|
|
1153
|
+
activeProviderId: typeof parsed.activeProviderId === "string" && parsed.activeProviderId.trim()
|
|
1154
|
+
? parsed.activeProviderId.trim()
|
|
1155
|
+
: undefined,
|
|
1156
|
+
modelOverride: typeof parsed.modelOverride === "string" && parsed.modelOverride.trim()
|
|
1157
|
+
? parsed.modelOverride.trim()
|
|
1158
|
+
: undefined,
|
|
1159
|
+
modelOverrides: sanitizeModelOverrides(parsed.modelOverrides),
|
|
1160
|
+
clientRoutes: sanitizeClientRoutes(parsed.clientRoutes),
|
|
1161
|
+
clientRouteRtkPolicies: sanitizeClientRouteRtkPolicies(parsed.clientRouteRtkPolicies),
|
|
1162
|
+
clientRouteApiKeys: sanitizeClientRouteApiKeys(parsed.clientRouteApiKeys),
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
catch {
|
|
1166
|
+
return undefined;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function sanitizeCustomProvider(value) {
|
|
1170
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1171
|
+
return undefined;
|
|
1172
|
+
}
|
|
1173
|
+
const record = value;
|
|
1174
|
+
const id = typeof record.id === "string" ? record.id.trim() : "";
|
|
1175
|
+
const name = typeof record.name === "string" ? record.name.trim() : "";
|
|
1176
|
+
const baseUrl = typeof record.baseUrl === "string" ? record.baseUrl.trim() : "";
|
|
1177
|
+
const providerApiKeys = normalizeApiKeysInput(record.providerApiKeys, record.apiKeys, record.apiKey);
|
|
1178
|
+
const createdAt = typeof record.createdAt === "string" && record.createdAt.trim()
|
|
1179
|
+
? record.createdAt.trim()
|
|
1180
|
+
: undefined;
|
|
1181
|
+
const updatedAt = typeof record.updatedAt === "string" && record.updatedAt.trim()
|
|
1182
|
+
? record.updatedAt.trim()
|
|
1183
|
+
: undefined;
|
|
1184
|
+
const capabilities = parseProviderCapabilitiesInput(record.capabilities);
|
|
1185
|
+
const authMode = parseRuntimeProviderAuthMode(record.authMode);
|
|
1186
|
+
const chatgptAccountId = typeof record.chatgptAccountId === "string" && record.chatgptAccountId.trim()
|
|
1187
|
+
? record.chatgptAccountId.trim()
|
|
1188
|
+
: undefined;
|
|
1189
|
+
if (!id || !name || !baseUrl) {
|
|
1190
|
+
return undefined;
|
|
1191
|
+
}
|
|
1192
|
+
try {
|
|
1193
|
+
const parsedBaseUrl = new URL(baseUrl);
|
|
1194
|
+
const normalizedBaseUrl = parsedBaseUrl.toString().replace(/\/+$/, "");
|
|
1195
|
+
return {
|
|
1196
|
+
id,
|
|
1197
|
+
name,
|
|
1198
|
+
baseUrl: normalizedBaseUrl,
|
|
1199
|
+
responsesUrl: toResponsesUrl(normalizedBaseUrl),
|
|
1200
|
+
authMode,
|
|
1201
|
+
chatgptAccountId,
|
|
1202
|
+
providerApiKeys,
|
|
1203
|
+
clientApiKeys: [],
|
|
1204
|
+
capabilities,
|
|
1205
|
+
createdAt,
|
|
1206
|
+
updatedAt,
|
|
1207
|
+
};
|
|
1208
|
+
}
|
|
1209
|
+
catch {
|
|
1210
|
+
return undefined;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function sanitizeProvider(value) {
|
|
1214
|
+
return sanitizeCustomProvider(value);
|
|
1215
|
+
}
|
|
1216
|
+
function sanitizeClientRoutes(value) {
|
|
1217
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1218
|
+
return {};
|
|
1219
|
+
}
|
|
1220
|
+
const record = value;
|
|
1221
|
+
const next = {};
|
|
1222
|
+
const legacyDefault = typeof record.default === "string" ? record.default.trim() : "";
|
|
1223
|
+
if (legacyDefault) {
|
|
1224
|
+
next.default = legacyDefault;
|
|
1225
|
+
}
|
|
1226
|
+
for (const [rawKey, rawValue] of Object.entries(record)) {
|
|
1227
|
+
const key = normalizeClientRouteKey(rawKey);
|
|
1228
|
+
const providerId = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
1229
|
+
if (providerId) {
|
|
1230
|
+
next[key] = providerId;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return next;
|
|
1234
|
+
}
|
|
1235
|
+
function sanitizeModelOverrides(value) {
|
|
1236
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1237
|
+
return {};
|
|
1238
|
+
}
|
|
1239
|
+
const record = value;
|
|
1240
|
+
const next = {};
|
|
1241
|
+
const legacyDefault = typeof record.default === "string" ? record.default.trim() : "";
|
|
1242
|
+
if (legacyDefault) {
|
|
1243
|
+
next.default = legacyDefault;
|
|
1244
|
+
}
|
|
1245
|
+
for (const [rawKey, rawValue] of Object.entries(record)) {
|
|
1246
|
+
const key = normalizeClientRouteKey(rawKey);
|
|
1247
|
+
const model = typeof rawValue === "string" ? rawValue.trim() : "";
|
|
1248
|
+
if (model) {
|
|
1249
|
+
next[key] = model;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
return next;
|
|
1253
|
+
}
|
|
1254
|
+
function sanitizeClientRouteRtkPolicies(value) {
|
|
1255
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1256
|
+
return {};
|
|
1257
|
+
}
|
|
1258
|
+
const record = value;
|
|
1259
|
+
const next = {};
|
|
1260
|
+
for (const [rawKey, rawValue] of Object.entries(record)) {
|
|
1261
|
+
const key = normalizeClientRouteKey(rawKey);
|
|
1262
|
+
const policy = parseRtkLayerPolicyInput(rawValue);
|
|
1263
|
+
if (policy) {
|
|
1264
|
+
next[key] = policy;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return next;
|
|
1268
|
+
}
|
|
1269
|
+
function sanitizeClientRouteApiKeys(value) {
|
|
1270
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1271
|
+
return {};
|
|
1272
|
+
}
|
|
1273
|
+
const record = value;
|
|
1274
|
+
const next = {};
|
|
1275
|
+
for (const [rawKey, rawValue] of Object.entries(record)) {
|
|
1276
|
+
const key = normalizeClientRouteKey(rawKey);
|
|
1277
|
+
const apiKeys = normalizeApiKeysInput(rawValue);
|
|
1278
|
+
if (apiKeys.length > 0) {
|
|
1279
|
+
next[key] = apiKeys;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return next;
|
|
1283
|
+
}
|
|
1284
|
+
function normalizeApiKeysInput(...values) {
|
|
1285
|
+
for (const value of values) {
|
|
1286
|
+
if (Array.isArray(value)) {
|
|
1287
|
+
return normalizeApiKeys(value);
|
|
1288
|
+
}
|
|
1289
|
+
if (typeof value === "string") {
|
|
1290
|
+
return normalizeApiKeys(value.split(/\r?\n|,/g));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return [];
|
|
1294
|
+
}
|
|
1295
|
+
function normalizeApiKeys(values) {
|
|
1296
|
+
return [
|
|
1297
|
+
...new Set(values
|
|
1298
|
+
.map((value) => (typeof value === "string" ? value.trim() : ""))
|
|
1299
|
+
.filter(Boolean)),
|
|
1300
|
+
];
|
|
1301
|
+
}
|
|
1302
|
+
function parseRuntimeProviderAuthMode(value) {
|
|
1303
|
+
if (value === "chatgpt_oauth") {
|
|
1304
|
+
return "chatgpt_oauth";
|
|
1305
|
+
}
|
|
1306
|
+
if (value === "kiro") {
|
|
1307
|
+
return "kiro";
|
|
1308
|
+
}
|
|
1309
|
+
return "api_key";
|
|
1310
|
+
}
|
|
1311
|
+
export function normalizeClientRouteKey(value) {
|
|
1312
|
+
const normalized = value
|
|
1313
|
+
.trim()
|
|
1314
|
+
.toLowerCase()
|
|
1315
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
1316
|
+
.replace(/-+/g, "-")
|
|
1317
|
+
.replace(/^-+|-+$/g, "");
|
|
1318
|
+
if (!normalized) {
|
|
1319
|
+
throw new RuntimeProviderError(400, {
|
|
1320
|
+
type: "validation_error",
|
|
1321
|
+
code: "INVALID_CLIENT_ROUTE",
|
|
1322
|
+
message: "client route is required",
|
|
1323
|
+
});
|
|
1324
|
+
}
|
|
1325
|
+
return normalized;
|
|
1326
|
+
}
|
|
1327
|
+
function migrateLegacyProvider(provider) {
|
|
1328
|
+
const migrated = {
|
|
1329
|
+
...provider,
|
|
1330
|
+
authMode: parseRuntimeProviderAuthMode(provider.authMode),
|
|
1331
|
+
chatgptAccountId: provider.chatgptAccountId?.trim() || undefined,
|
|
1332
|
+
};
|
|
1333
|
+
if (provider.id !== "primary" && provider.id !== "fallback") {
|
|
1334
|
+
return migrated;
|
|
1335
|
+
}
|
|
1336
|
+
const identity = inferProviderIdentity(provider.baseUrl, provider.name);
|
|
1337
|
+
return {
|
|
1338
|
+
...migrated,
|
|
1339
|
+
id: identity.id,
|
|
1340
|
+
name: identity.name,
|
|
1341
|
+
capabilities: cloneCapabilities(provider.capabilities),
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
function inferProviderIdentity(baseUrl, fallbackName) {
|
|
1345
|
+
try {
|
|
1346
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
1347
|
+
const normalizedHost = hostname
|
|
1348
|
+
.replace(/^api\./, "")
|
|
1349
|
+
.replace(/^www\./, "")
|
|
1350
|
+
.replace(/\.[a-z]+$/, "")
|
|
1351
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1352
|
+
.replace(/^-+|-+$/g, "");
|
|
1353
|
+
const normalizedName = normalizeProviderName(fallbackName || normalizedHost || "provider");
|
|
1354
|
+
return {
|
|
1355
|
+
id: normalizedName.replace(/[^a-z0-9]+/g, "-"),
|
|
1356
|
+
name: normalizedName,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
catch {
|
|
1360
|
+
const normalizedName = normalizeProviderName(fallbackName || "provider");
|
|
1361
|
+
return {
|
|
1362
|
+
id: normalizedName.replace(/[^a-z0-9]+/g, "-"),
|
|
1363
|
+
name: normalizedName,
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function ensureUniqueProviderIds(providers) {
|
|
1368
|
+
const usedIds = new Map();
|
|
1369
|
+
return providers.map((provider) => {
|
|
1370
|
+
const baseId = provider.id.trim() || "provider";
|
|
1371
|
+
const seen = usedIds.get(baseId) ?? 0;
|
|
1372
|
+
usedIds.set(baseId, seen + 1);
|
|
1373
|
+
if (seen === 0) {
|
|
1374
|
+
return provider;
|
|
1375
|
+
}
|
|
1376
|
+
return {
|
|
1377
|
+
...provider,
|
|
1378
|
+
id: `${baseId}-${seen + 1}`,
|
|
1379
|
+
};
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
function mergeBaseProviders(runtimeProviders, baseProviders) {
|
|
1383
|
+
const merged = [...runtimeProviders];
|
|
1384
|
+
for (const baseProvider of baseProviders) {
|
|
1385
|
+
const matchingIndex = merged.findIndex((provider) => provider.id === baseProvider.id ||
|
|
1386
|
+
provider.baseUrl === baseProvider.baseUrl ||
|
|
1387
|
+
normalizeProviderName(provider.name) === normalizeProviderName(baseProvider.name));
|
|
1388
|
+
if (matchingIndex === -1) {
|
|
1389
|
+
merged.push(baseProvider);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
const existing = merged[matchingIndex];
|
|
1393
|
+
if (isUnmodifiedSeededProvider(existing)) {
|
|
1394
|
+
merged[matchingIndex] = {
|
|
1395
|
+
...baseProvider,
|
|
1396
|
+
createdAt: existing.createdAt,
|
|
1397
|
+
updatedAt: existing.updatedAt,
|
|
1398
|
+
};
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return merged;
|
|
1402
|
+
}
|
|
1403
|
+
function isUnmodifiedSeededProvider(provider) {
|
|
1404
|
+
return !provider.createdAt && !provider.updatedAt;
|
|
1405
|
+
}
|
|
1406
|
+
function shouldPersistSeededProviders(originalProviders, nextProviders) {
|
|
1407
|
+
if (originalProviders.length !== nextProviders.length) {
|
|
1408
|
+
return true;
|
|
1409
|
+
}
|
|
1410
|
+
return nextProviders.some((provider, index) => {
|
|
1411
|
+
const original = originalProviders[index];
|
|
1412
|
+
return (!original ||
|
|
1413
|
+
original.id !== provider.id ||
|
|
1414
|
+
original.name !== provider.name ||
|
|
1415
|
+
original.baseUrl !== provider.baseUrl ||
|
|
1416
|
+
original.authMode !== provider.authMode ||
|
|
1417
|
+
original.chatgptAccountId !== provider.chatgptAccountId ||
|
|
1418
|
+
JSON.stringify(original.providerApiKeys) !== JSON.stringify(provider.providerApiKeys) ||
|
|
1419
|
+
JSON.stringify(cloneCapabilities(original.capabilities)) !==
|
|
1420
|
+
JSON.stringify(cloneCapabilities(provider.capabilities)));
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
function normalizeProviderName(value) {
|
|
1424
|
+
return value.trim().toLowerCase();
|
|
1425
|
+
}
|
|
1426
|
+
function toResponsesUrl(baseUrl) {
|
|
1427
|
+
return baseUrl.endsWith("/v1") ? `${baseUrl}/responses` : `${baseUrl}/v1/responses`;
|
|
1428
|
+
}
|
|
1429
|
+
function buildDefaultCapabilitiesFromConfig(config) {
|
|
1430
|
+
const requestParameterPolicyInput = config.MAX_OUTPUT_TOKENS_PARAMETER_MODE_FOR_PROVIDER
|
|
1431
|
+
? {
|
|
1432
|
+
maxOutputTokens: {
|
|
1433
|
+
mode: config.MAX_OUTPUT_TOKENS_PARAMETER_MODE_FOR_PROVIDER,
|
|
1434
|
+
...(config.MAX_OUTPUT_TOKENS_PARAMETER_MODE_FOR_PROVIDER === "rename" &&
|
|
1435
|
+
config.MAX_OUTPUT_TOKENS_PARAMETER_TARGET_FOR_PROVIDER?.trim()
|
|
1436
|
+
? {
|
|
1437
|
+
target: config.MAX_OUTPUT_TOKENS_PARAMETER_TARGET_FOR_PROVIDER.trim(),
|
|
1438
|
+
}
|
|
1439
|
+
: {}),
|
|
1440
|
+
},
|
|
1441
|
+
}
|
|
1442
|
+
: {};
|
|
1443
|
+
const maxOutputTokensRule = resolveMaxOutputTokensRule({
|
|
1444
|
+
stripMaxOutputTokens: config.STRIP_MAX_OUTPUT_TOKENS_FOR_PROVIDER ?? false,
|
|
1445
|
+
requestParameterPolicy: requestParameterPolicyInput,
|
|
1446
|
+
});
|
|
1447
|
+
const requestParameterPolicy = cloneProviderRequestParameterPolicy({
|
|
1448
|
+
maxOutputTokens: maxOutputTokensRule,
|
|
1449
|
+
});
|
|
1450
|
+
return {
|
|
1451
|
+
usageCheckEnabled: Boolean(config.PROVIDER_USAGE_CHECK_ENABLED && config.PROVIDER_USAGE_CHECK_URL),
|
|
1452
|
+
usageCheckUrl: config.PROVIDER_USAGE_CHECK_URL,
|
|
1453
|
+
stripMaxOutputTokens: maxOutputTokensRule.mode === "strip",
|
|
1454
|
+
requestParameterPolicy,
|
|
1455
|
+
sanitizeReasoningSummary: config.SANITIZE_REASONING_SUMMARY_FOR_PROVIDER ?? false,
|
|
1456
|
+
stripModelPrefixes: [],
|
|
1457
|
+
rtkPolicy: cloneRtkLayerPolicy({
|
|
1458
|
+
enabled: config.RTK_LAYER_ENABLED,
|
|
1459
|
+
toolOutputEnabled: config.RTK_LAYER_TOOL_OUTPUT_ENABLED,
|
|
1460
|
+
maxChars: config.RTK_LAYER_TOOL_OUTPUT_MAX_CHARS,
|
|
1461
|
+
maxLines: config.RTK_LAYER_TOOL_OUTPUT_MAX_LINES,
|
|
1462
|
+
tailLines: config.RTK_LAYER_TOOL_OUTPUT_TAIL_LINES,
|
|
1463
|
+
tailChars: config.RTK_LAYER_TOOL_OUTPUT_TAIL_CHARS,
|
|
1464
|
+
detectFormat: config.RTK_LAYER_TOOL_OUTPUT_DETECT_FORMAT,
|
|
1465
|
+
}),
|
|
1466
|
+
errorPolicy: inferDefaultProviderErrorPolicy(config.UPSTREAM_BASE_URL),
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
function parseProviderCapabilitiesInput(value) {
|
|
1470
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1471
|
+
return {
|
|
1472
|
+
usageCheckEnabled: false,
|
|
1473
|
+
stripMaxOutputTokens: false,
|
|
1474
|
+
requestParameterPolicy: {},
|
|
1475
|
+
sanitizeReasoningSummary: false,
|
|
1476
|
+
stripModelPrefixes: [],
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
const record = value;
|
|
1480
|
+
const ownedBy = typeof record.ownedBy === "string" && record.ownedBy.trim()
|
|
1481
|
+
? record.ownedBy.trim()
|
|
1482
|
+
: undefined;
|
|
1483
|
+
const accountPlatform = typeof record.accountPlatform === "string" && record.accountPlatform.trim()
|
|
1484
|
+
? record.accountPlatform.trim()
|
|
1485
|
+
: typeof record.account_platform === "string" && record.account_platform.trim()
|
|
1486
|
+
? record.account_platform.trim()
|
|
1487
|
+
: undefined;
|
|
1488
|
+
const usageCheckUrl = typeof record.usageCheckUrl === "string" && record.usageCheckUrl.trim()
|
|
1489
|
+
? normalizeOptionalUrl(record.usageCheckUrl)
|
|
1490
|
+
: undefined;
|
|
1491
|
+
const requestParameterPolicy = parseProviderRequestParameterPolicyInput(record.requestParameterPolicy ?? record.request_parameter_policy);
|
|
1492
|
+
const maxOutputTokensRule = resolveMaxOutputTokensRule({
|
|
1493
|
+
stripMaxOutputTokens: coerceBoolean(record.stripMaxOutputTokens),
|
|
1494
|
+
requestParameterPolicy,
|
|
1495
|
+
});
|
|
1496
|
+
return {
|
|
1497
|
+
ownedBy,
|
|
1498
|
+
systemManaged: coerceBoolean(record.systemManaged ?? record.system_managed),
|
|
1499
|
+
accountPlatform,
|
|
1500
|
+
accountPoolRequired: coerceBoolean(record.accountPoolRequired ?? record.account_pool_required),
|
|
1501
|
+
usageCheckEnabled: coerceBoolean(record.usageCheckEnabled),
|
|
1502
|
+
usageCheckUrl,
|
|
1503
|
+
stripMaxOutputTokens: maxOutputTokensRule.mode === "strip",
|
|
1504
|
+
requestParameterPolicy,
|
|
1505
|
+
sanitizeReasoningSummary: coerceBoolean(record.sanitizeReasoningSummary),
|
|
1506
|
+
preserveMessagesPayload: coerceBoolean(record.preserveMessagesPayload ?? record.preserve_messages_payload),
|
|
1507
|
+
preserveRawRequestBody: coerceBoolean(record.preserveRawRequestBody ?? record.preserve_raw_request_body),
|
|
1508
|
+
transportMode: normalizeTransportMode(record.transportMode ?? record.transport_mode),
|
|
1509
|
+
stripModelPrefixes: normalizeStringList(record.stripModelPrefixes),
|
|
1510
|
+
modelAliases: normalizeStringMap(record.modelAliases ?? record.model_aliases),
|
|
1511
|
+
rtkPolicy: parseRtkLayerPolicyInput(record.rtkPolicy ?? record.rtk_policy),
|
|
1512
|
+
errorPolicy: parseProviderErrorPolicyInput(record.errorPolicy ?? record.error_policy),
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
function cloneCapabilities(capabilities) {
|
|
1516
|
+
const maxOutputTokensRule = resolveMaxOutputTokensRule(capabilities);
|
|
1517
|
+
return {
|
|
1518
|
+
ownedBy: capabilities?.ownedBy,
|
|
1519
|
+
systemManaged: capabilities?.systemManaged ?? false,
|
|
1520
|
+
accountPlatform: capabilities?.accountPlatform,
|
|
1521
|
+
accountPoolRequired: capabilities?.accountPoolRequired ?? false,
|
|
1522
|
+
usageCheckEnabled: capabilities?.usageCheckEnabled ?? false,
|
|
1523
|
+
usageCheckUrl: capabilities?.usageCheckUrl,
|
|
1524
|
+
stripMaxOutputTokens: maxOutputTokensRule.mode === "strip",
|
|
1525
|
+
requestParameterPolicy: cloneProviderRequestParameterPolicy({
|
|
1526
|
+
maxOutputTokens: maxOutputTokensRule,
|
|
1527
|
+
}),
|
|
1528
|
+
sanitizeReasoningSummary: capabilities?.sanitizeReasoningSummary ?? false,
|
|
1529
|
+
preserveMessagesPayload: capabilities?.preserveMessagesPayload ?? false,
|
|
1530
|
+
preserveRawRequestBody: capabilities?.preserveRawRequestBody ?? false,
|
|
1531
|
+
transportMode: normalizeTransportMode(capabilities?.transportMode) ?? "responses",
|
|
1532
|
+
stripModelPrefixes: [...(capabilities?.stripModelPrefixes ?? [])],
|
|
1533
|
+
modelAliases: { ...(capabilities?.modelAliases ?? {}) },
|
|
1534
|
+
rtkPolicy: cloneRtkLayerPolicy(capabilities?.rtkPolicy),
|
|
1535
|
+
errorPolicy: cloneProviderErrorPolicy(capabilities?.errorPolicy),
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
function normalizeOptionalUrl(value) {
|
|
1539
|
+
try {
|
|
1540
|
+
return new URL(value.trim()).toString();
|
|
1541
|
+
}
|
|
1542
|
+
catch {
|
|
1543
|
+
throw new RuntimeProviderError(400, {
|
|
1544
|
+
type: "validation_error",
|
|
1545
|
+
code: "INVALID_PROVIDER_CAPABILITIES",
|
|
1546
|
+
message: "capabilities.usageCheckUrl must be a valid URL",
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
function normalizeTransportMode(value) {
|
|
1551
|
+
if (typeof value !== "string") {
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
const normalized = value.trim().toLowerCase();
|
|
1555
|
+
if (normalized === "responses" ||
|
|
1556
|
+
normalized === "chat_completions" ||
|
|
1557
|
+
normalized === "codewhisperer") {
|
|
1558
|
+
return normalized;
|
|
1559
|
+
}
|
|
1560
|
+
return undefined;
|
|
1561
|
+
}
|
|
1562
|
+
function coerceBoolean(value) {
|
|
1563
|
+
return value === true || value === "true" || value === 1 || value === "1";
|
|
1564
|
+
}
|
|
1565
|
+
function normalizeStringList(value) {
|
|
1566
|
+
if (typeof value === "string") {
|
|
1567
|
+
try {
|
|
1568
|
+
const parsed = JSON.parse(value);
|
|
1569
|
+
if (Array.isArray(parsed)) {
|
|
1570
|
+
return normalizeStringList(parsed);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
catch {
|
|
1574
|
+
return normalizeApiKeys(value.split(/\r?\n|,/g));
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
if (!Array.isArray(value)) {
|
|
1578
|
+
return [];
|
|
1579
|
+
}
|
|
1580
|
+
return [
|
|
1581
|
+
...new Set(value
|
|
1582
|
+
.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
1583
|
+
.filter(Boolean)),
|
|
1584
|
+
];
|
|
1585
|
+
}
|
|
1586
|
+
function normalizeStringMap(value) {
|
|
1587
|
+
const parsed = typeof value === "string"
|
|
1588
|
+
? safeJsonParse(value)
|
|
1589
|
+
: value;
|
|
1590
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1591
|
+
return undefined;
|
|
1592
|
+
}
|
|
1593
|
+
const entries = Object.entries(parsed)
|
|
1594
|
+
.map(([key, rawValue]) => [
|
|
1595
|
+
typeof key === "string" ? key.trim() : "",
|
|
1596
|
+
typeof rawValue === "string" ? rawValue.trim() : "",
|
|
1597
|
+
])
|
|
1598
|
+
.filter(([key, mapped]) => key && mapped);
|
|
1599
|
+
if (!entries.length) {
|
|
1600
|
+
return undefined;
|
|
1601
|
+
}
|
|
1602
|
+
return Object.fromEntries(entries);
|
|
1603
|
+
}
|
|
1604
|
+
function normalizeStoredRequestParameterPolicy(raw, stripMaxOutputTokens) {
|
|
1605
|
+
const parsed = raw?.trim() ? safeJsonParse(raw) : undefined;
|
|
1606
|
+
const parsedPolicy = parseProviderRequestParameterPolicyInput(parsed);
|
|
1607
|
+
return cloneProviderRequestParameterPolicy(parsedPolicy.maxOutputTokens
|
|
1608
|
+
? parsedPolicy
|
|
1609
|
+
: parseProviderRequestParameterPolicyInput({
|
|
1610
|
+
maxOutputTokens: stripMaxOutputTokens ? "strip" : "forward",
|
|
1611
|
+
}));
|
|
1612
|
+
}
|
|
1613
|
+
function cloneProviderErrorPolicy(policy) {
|
|
1614
|
+
if (!policy || !Array.isArray(policy.rules) || policy.rules.length === 0) {
|
|
1615
|
+
return undefined;
|
|
1616
|
+
}
|
|
1617
|
+
return {
|
|
1618
|
+
rules: policy.rules.map((rule) => ({
|
|
1619
|
+
statusCodes: Array.isArray(rule.statusCodes) && rule.statusCodes.length > 0
|
|
1620
|
+
? rule.statusCodes.filter((value) => Number.isInteger(value))
|
|
1621
|
+
: undefined,
|
|
1622
|
+
upstreamCodes: normalizeStringList(rule.upstreamCodes),
|
|
1623
|
+
upstreamTypes: normalizeStringList(rule.upstreamTypes),
|
|
1624
|
+
messageIncludes: normalizeStringList(rule.messageIncludes),
|
|
1625
|
+
bodyIncludes: normalizeStringList(rule.bodyIncludes),
|
|
1626
|
+
code: typeof rule.code === "string" && rule.code.trim() ? rule.code.trim() : undefined,
|
|
1627
|
+
message: typeof rule.message === "string" && rule.message.trim() ? rule.message.trim() : undefined,
|
|
1628
|
+
retryable: typeof rule.retryable === "boolean" ? rule.retryable : undefined,
|
|
1629
|
+
})),
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
function parseProviderErrorPolicyInput(value) {
|
|
1633
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1634
|
+
return undefined;
|
|
1635
|
+
}
|
|
1636
|
+
const record = value;
|
|
1637
|
+
const rulesInput = Array.isArray(record.rules) ? record.rules : [];
|
|
1638
|
+
const rules = rulesInput
|
|
1639
|
+
.map((item) => parseProviderErrorPolicyRuleInput(item))
|
|
1640
|
+
.filter((item) => Boolean(item));
|
|
1641
|
+
if (rules.length === 0) {
|
|
1642
|
+
return undefined;
|
|
1643
|
+
}
|
|
1644
|
+
return { rules };
|
|
1645
|
+
}
|
|
1646
|
+
function parseProviderErrorPolicyRuleInput(value) {
|
|
1647
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1648
|
+
return undefined;
|
|
1649
|
+
}
|
|
1650
|
+
const record = value;
|
|
1651
|
+
const statusCodes = normalizeIntegerList(record.statusCodes ?? record.status_codes);
|
|
1652
|
+
const upstreamCodes = normalizeStringList(record.upstreamCodes ?? record.upstream_codes);
|
|
1653
|
+
const upstreamTypes = normalizeStringList(record.upstreamTypes ?? record.upstream_types);
|
|
1654
|
+
const messageIncludes = normalizeStringList(record.messageIncludes ?? record.message_includes);
|
|
1655
|
+
const bodyIncludes = normalizeStringList(record.bodyIncludes ?? record.body_includes);
|
|
1656
|
+
const code = typeof record.code === "string" && record.code.trim() ? record.code.trim() : undefined;
|
|
1657
|
+
const message = typeof record.message === "string" && record.message.trim() ? record.message.trim() : undefined;
|
|
1658
|
+
const retryable = typeof record.retryable === "boolean" ? record.retryable : undefined;
|
|
1659
|
+
if (statusCodes.length === 0 &&
|
|
1660
|
+
upstreamCodes.length === 0 &&
|
|
1661
|
+
upstreamTypes.length === 0 &&
|
|
1662
|
+
messageIncludes.length === 0 &&
|
|
1663
|
+
bodyIncludes.length === 0) {
|
|
1664
|
+
return undefined;
|
|
1665
|
+
}
|
|
1666
|
+
return {
|
|
1667
|
+
statusCodes: statusCodes.length > 0 ? statusCodes : undefined,
|
|
1668
|
+
upstreamCodes: upstreamCodes.length > 0 ? upstreamCodes : undefined,
|
|
1669
|
+
upstreamTypes: upstreamTypes.length > 0 ? upstreamTypes : undefined,
|
|
1670
|
+
messageIncludes: messageIncludes.length > 0 ? messageIncludes : undefined,
|
|
1671
|
+
bodyIncludes: bodyIncludes.length > 0 ? bodyIncludes : undefined,
|
|
1672
|
+
code,
|
|
1673
|
+
message,
|
|
1674
|
+
retryable,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function normalizeIntegerList(value) {
|
|
1678
|
+
if (!Array.isArray(value)) {
|
|
1679
|
+
return [];
|
|
1680
|
+
}
|
|
1681
|
+
return [...new Set(value.filter((item) => Number.isInteger(item)).map((item) => Number(item)))];
|
|
1682
|
+
}
|
|
1683
|
+
function inferDefaultProviderErrorPolicy(baseUrl) {
|
|
1684
|
+
try {
|
|
1685
|
+
const hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
1686
|
+
if (hostname === "krouter.net" || hostname === "api.krouter.net") {
|
|
1687
|
+
return {
|
|
1688
|
+
rules: [
|
|
1689
|
+
{
|
|
1690
|
+
statusCodes: [413],
|
|
1691
|
+
code: "UPSTREAM_REQUEST_TOO_LARGE",
|
|
1692
|
+
message: "Upstream rejected the request because the serialized prompt body is too large",
|
|
1693
|
+
retryable: false,
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
bodyIncludes: ["request body is too large"],
|
|
1697
|
+
code: "UPSTREAM_REQUEST_TOO_LARGE",
|
|
1698
|
+
message: "Upstream rejected the request because the serialized prompt body is too large",
|
|
1699
|
+
retryable: false,
|
|
1700
|
+
},
|
|
1701
|
+
{
|
|
1702
|
+
statusCodes: [429],
|
|
1703
|
+
code: "UPSTREAM_RATE_LIMITED",
|
|
1704
|
+
message: "Upstream rate limit reached",
|
|
1705
|
+
retryable: true,
|
|
1706
|
+
},
|
|
1707
|
+
],
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
catch {
|
|
1712
|
+
return undefined;
|
|
1713
|
+
}
|
|
1714
|
+
return undefined;
|
|
1715
|
+
}
|
|
1716
|
+
function safeJsonParse(value) {
|
|
1717
|
+
try {
|
|
1718
|
+
return JSON.parse(value);
|
|
1719
|
+
}
|
|
1720
|
+
catch {
|
|
1721
|
+
return undefined;
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
function ensureProvidersColumn(db, columnName, sqlDefinition) {
|
|
1725
|
+
const columns = db.prepare("PRAGMA table_info(providers)").all();
|
|
1726
|
+
if (!columns.some((column) => column.name === columnName)) {
|
|
1727
|
+
db.exec(`ALTER TABLE providers ADD COLUMN ${columnName} ${sqlDefinition}`);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
function backfillLegacyRequestParameterPolicies(db) {
|
|
1731
|
+
db.exec(`
|
|
1732
|
+
UPDATE providers
|
|
1733
|
+
SET request_parameter_policy = CASE
|
|
1734
|
+
WHEN strip_max_output_tokens = 1
|
|
1735
|
+
THEN '{"maxOutputTokens":{"mode":"strip"}}'
|
|
1736
|
+
ELSE '{"maxOutputTokens":{"mode":"forward"}}'
|
|
1737
|
+
END
|
|
1738
|
+
WHERE request_parameter_policy IS NULL
|
|
1739
|
+
OR TRIM(request_parameter_policy) = ''
|
|
1740
|
+
OR TRIM(request_parameter_policy) = '{}'
|
|
1741
|
+
`);
|
|
1742
|
+
}
|