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,40 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { isHermesSummaryRequest, resolveRequestTimeoutMs } from "./request-timeout-policy.js";
|
|
4
|
+
test("detects Hermes context checkpoint summarization requests", () => {
|
|
5
|
+
assert.equal(isHermesSummaryRequest({
|
|
6
|
+
input: [
|
|
7
|
+
{
|
|
8
|
+
role: "user",
|
|
9
|
+
content: "You are a summarization agent creating a context checkpoint. Summarize the session.",
|
|
10
|
+
},
|
|
11
|
+
],
|
|
12
|
+
}), true);
|
|
13
|
+
});
|
|
14
|
+
test("does not classify normal chat requests as Hermes summaries", () => {
|
|
15
|
+
assert.equal(isHermesSummaryRequest({
|
|
16
|
+
input: [{ role: "user", content: "Implement this Jira ticket." }],
|
|
17
|
+
}), false);
|
|
18
|
+
});
|
|
19
|
+
test("extends timeout only for detected Hermes summary requests when enabled", () => {
|
|
20
|
+
assert.equal(resolveRequestTimeoutMs({
|
|
21
|
+
input: [
|
|
22
|
+
{
|
|
23
|
+
type: "message",
|
|
24
|
+
role: "user",
|
|
25
|
+
content: [{ type: "input_text", text: "You are a summarization agent creating a context checkpoint." }],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}, {
|
|
29
|
+
defaultTimeoutMs: 300_000,
|
|
30
|
+
summaryTimeoutMs: 900_000,
|
|
31
|
+
extendHermesSummaryTimeout: true,
|
|
32
|
+
}), 900_000);
|
|
33
|
+
assert.equal(resolveRequestTimeoutMs({
|
|
34
|
+
input: [{ role: "user", content: "Normal request" }],
|
|
35
|
+
}, {
|
|
36
|
+
defaultTimeoutMs: 300_000,
|
|
37
|
+
summaryTimeoutMs: 900_000,
|
|
38
|
+
extendHermesSummaryTimeout: true,
|
|
39
|
+
}), 300_000);
|
|
40
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import BetterSqlite3 from "better-sqlite3";
|
|
4
|
+
export class ResponseCacheStore {
|
|
5
|
+
db;
|
|
6
|
+
constructor(db) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
}
|
|
9
|
+
static create(dbFile) {
|
|
10
|
+
mkdirSync(path.dirname(dbFile), { recursive: true });
|
|
11
|
+
const db = new BetterSqlite3(dbFile);
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS response_cache (
|
|
14
|
+
request_key TEXT NOT NULL,
|
|
15
|
+
provider_id TEXT NOT NULL,
|
|
16
|
+
payload TEXT NOT NULL,
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
expires_at INTEGER NOT NULL,
|
|
19
|
+
PRIMARY KEY (request_key, provider_id)
|
|
20
|
+
);
|
|
21
|
+
CREATE INDEX IF NOT EXISTS idx_response_cache_expires
|
|
22
|
+
ON response_cache(expires_at);
|
|
23
|
+
`);
|
|
24
|
+
return new ResponseCacheStore(db);
|
|
25
|
+
}
|
|
26
|
+
get(requestKey, providerId) {
|
|
27
|
+
const nowMs = Date.now();
|
|
28
|
+
const row = this.db
|
|
29
|
+
.prepare(`SELECT payload FROM response_cache
|
|
30
|
+
WHERE request_key = ? AND provider_id = ? AND expires_at > ?`)
|
|
31
|
+
.get(requestKey, providerId, nowMs);
|
|
32
|
+
if (!row) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(row.payload);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
set(requestKey, providerId, payload, ttlMs) {
|
|
43
|
+
const nowMs = Date.now();
|
|
44
|
+
this.db
|
|
45
|
+
.prepare(`INSERT INTO response_cache (request_key, provider_id, payload, created_at, expires_at)
|
|
46
|
+
VALUES (?, ?, ?, ?, ?)
|
|
47
|
+
ON CONFLICT(request_key, provider_id) DO UPDATE SET
|
|
48
|
+
payload = excluded.payload,
|
|
49
|
+
created_at = excluded.created_at,
|
|
50
|
+
expires_at = excluded.expires_at`)
|
|
51
|
+
.run(requestKey, providerId, JSON.stringify(payload), nowMs, nowMs + ttlMs);
|
|
52
|
+
}
|
|
53
|
+
prune() {
|
|
54
|
+
const result = this.db
|
|
55
|
+
.prepare("DELETE FROM response_cache WHERE expires_at <= ?")
|
|
56
|
+
.run(Date.now());
|
|
57
|
+
return result.changes;
|
|
58
|
+
}
|
|
59
|
+
stats() {
|
|
60
|
+
const nowMs = Date.now();
|
|
61
|
+
const total = this.db.prepare("SELECT COUNT(*) AS n FROM response_cache").get().n;
|
|
62
|
+
const expired = this.db.prepare("SELECT COUNT(*) AS n FROM response_cache WHERE expires_at <= ?").get(nowMs).n;
|
|
63
|
+
const bytes = this.db.prepare("SELECT COALESCE(SUM(LENGTH(payload)), 0) AS b FROM response_cache").get().b;
|
|
64
|
+
return { totalEntries: total, expiredEntries: expired, estimatedBytes: bytes };
|
|
65
|
+
}
|
|
66
|
+
flush() {
|
|
67
|
+
return this.db.prepare("DELETE FROM response_cache").run().changes;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { ResponseCacheStore } from "./response-cache.js";
|
|
7
|
+
test("ResponseCacheStore stores, expires, reports stats, and flushes entries", () => {
|
|
8
|
+
const tempDir = mkdtempSync(path.join(os.tmpdir(), "responses-proxy-response-cache-"));
|
|
9
|
+
const dbFile = path.join(tempDir, "cache.sqlite");
|
|
10
|
+
try {
|
|
11
|
+
const store = ResponseCacheStore.create(dbFile);
|
|
12
|
+
store.set("request-a", "provider-a", { ok: true, value: 1 }, 60_000);
|
|
13
|
+
assert.deepEqual(store.get("request-a", "provider-a"), { ok: true, value: 1 });
|
|
14
|
+
assert.equal(store.get("request-a", "provider-b"), undefined);
|
|
15
|
+
const stats = store.stats();
|
|
16
|
+
assert.equal(stats.totalEntries, 1);
|
|
17
|
+
assert.equal(stats.expiredEntries, 0);
|
|
18
|
+
assert.equal(stats.estimatedBytes > 0, true);
|
|
19
|
+
store.set("request-expired", "provider-a", { ok: false }, -1);
|
|
20
|
+
assert.equal(store.get("request-expired", "provider-a"), undefined);
|
|
21
|
+
assert.equal(store.prune(), 1);
|
|
22
|
+
assert.equal(store.flush(), 1);
|
|
23
|
+
assert.equal(store.stats().totalEntries, 0);
|
|
24
|
+
}
|
|
25
|
+
finally {
|
|
26
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
export class RoutingComboRepository {
|
|
3
|
+
db;
|
|
4
|
+
constructor(db) {
|
|
5
|
+
this.db = db;
|
|
6
|
+
}
|
|
7
|
+
// Get all routing combos with summary stats
|
|
8
|
+
async getAllCombos() {
|
|
9
|
+
const comboRows = this.db.prepare(`
|
|
10
|
+
SELECT id, name, description, is_active, is_default, created_at, updated_at
|
|
11
|
+
FROM routing_combos
|
|
12
|
+
ORDER BY created_at DESC
|
|
13
|
+
`).all();
|
|
14
|
+
const combos = [];
|
|
15
|
+
for (const row of comboRows) {
|
|
16
|
+
const combo = await this.getComboById(row.id);
|
|
17
|
+
if (combo) {
|
|
18
|
+
combos.push(combo);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return combos;
|
|
22
|
+
}
|
|
23
|
+
// Get specific combo by ID with full configuration
|
|
24
|
+
async getComboById(id) {
|
|
25
|
+
const comboRow = this.db.prepare(`
|
|
26
|
+
SELECT id, name, description, is_active, is_default, created_at, updated_at
|
|
27
|
+
FROM routing_combos
|
|
28
|
+
WHERE id = ?
|
|
29
|
+
`).get(id);
|
|
30
|
+
if (!comboRow) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
// Get tiers
|
|
34
|
+
const tierRows = this.db.prepare(`
|
|
35
|
+
SELECT id, combo_id, name, priority, tier, is_enabled, fallback_delay, max_retries, health_threshold
|
|
36
|
+
FROM routing_tiers
|
|
37
|
+
WHERE combo_id = ?
|
|
38
|
+
ORDER BY priority ASC
|
|
39
|
+
`).all(id);
|
|
40
|
+
const tiers = [];
|
|
41
|
+
for (const tierRow of tierRows) {
|
|
42
|
+
// Get providers for this tier
|
|
43
|
+
const providerRows = this.db.prepare(`
|
|
44
|
+
SELECT id, tier_id, provider_id, weight, is_enabled, model_override
|
|
45
|
+
FROM routing_tier_providers
|
|
46
|
+
WHERE tier_id = ?
|
|
47
|
+
ORDER BY weight DESC
|
|
48
|
+
`).all(tierRow.id);
|
|
49
|
+
const providers = providerRows.map(row => ({
|
|
50
|
+
id: row.id,
|
|
51
|
+
providerId: row.provider_id,
|
|
52
|
+
weight: row.weight,
|
|
53
|
+
isEnabled: Boolean(row.is_enabled),
|
|
54
|
+
modelOverride: row.model_override || undefined
|
|
55
|
+
}));
|
|
56
|
+
tiers.push({
|
|
57
|
+
id: tierRow.id,
|
|
58
|
+
name: tierRow.name,
|
|
59
|
+
priority: tierRow.priority,
|
|
60
|
+
tier: tierRow.tier,
|
|
61
|
+
isEnabled: Boolean(tierRow.is_enabled),
|
|
62
|
+
fallbackDelay: tierRow.fallback_delay,
|
|
63
|
+
maxRetries: tierRow.max_retries,
|
|
64
|
+
healthThreshold: tierRow.health_threshold ? JSON.parse(tierRow.health_threshold) : undefined,
|
|
65
|
+
providers
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
// Get policies
|
|
69
|
+
const policyRow = this.db.prepare(`
|
|
70
|
+
SELECT combo_id, load_balancing, failover_strategy, token_budget_mode, quota_management, cost_optimization, retry_policy
|
|
71
|
+
FROM routing_combo_policies
|
|
72
|
+
WHERE combo_id = ?
|
|
73
|
+
`).get(id);
|
|
74
|
+
const policies = {
|
|
75
|
+
loadBalancing: policyRow?.load_balancing || 'weighted',
|
|
76
|
+
failoverStrategy: policyRow?.failover_strategy || 'immediate',
|
|
77
|
+
tokenBudgetMode: policyRow?.token_budget_mode || 'per_route',
|
|
78
|
+
quotaManagement: policyRow?.quota_management ? JSON.parse(policyRow.quota_management) : undefined,
|
|
79
|
+
costOptimization: policyRow?.cost_optimization ? JSON.parse(policyRow.cost_optimization) : undefined,
|
|
80
|
+
retryPolicy: policyRow?.retry_policy ? JSON.parse(policyRow.retry_policy) : undefined
|
|
81
|
+
};
|
|
82
|
+
// Get client routes
|
|
83
|
+
const clientRouteRows = this.db.prepare(`
|
|
84
|
+
SELECT client_route
|
|
85
|
+
FROM routing_combo_client_routes
|
|
86
|
+
WHERE combo_id = ?
|
|
87
|
+
`).all(id);
|
|
88
|
+
const clientRoutes = clientRouteRows.map(row => row.client_route);
|
|
89
|
+
return {
|
|
90
|
+
id: comboRow.id,
|
|
91
|
+
name: comboRow.name,
|
|
92
|
+
description: comboRow.description || undefined,
|
|
93
|
+
isActive: Boolean(comboRow.is_active),
|
|
94
|
+
isDefault: Boolean(comboRow.is_default),
|
|
95
|
+
tiers,
|
|
96
|
+
policies,
|
|
97
|
+
clientRoutes,
|
|
98
|
+
createdAt: comboRow.created_at,
|
|
99
|
+
updatedAt: comboRow.updated_at
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
// Create new routing combo
|
|
103
|
+
async createCombo(input) {
|
|
104
|
+
const id = randomUUID();
|
|
105
|
+
const now = new Date().toISOString();
|
|
106
|
+
const transaction = this.db.transaction(() => {
|
|
107
|
+
// Insert combo
|
|
108
|
+
this.db.prepare(`
|
|
109
|
+
INSERT INTO routing_combos (id, name, description, is_active, is_default, created_at, updated_at)
|
|
110
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
111
|
+
`).run(id, input.name, input.description || null, input.isActive ? 1 : 0, input.isDefault ? 1 : 0, now, now);
|
|
112
|
+
// Insert tiers
|
|
113
|
+
for (const tier of input.tiers) {
|
|
114
|
+
const tierId = randomUUID();
|
|
115
|
+
this.db.prepare(`
|
|
116
|
+
INSERT INTO routing_tiers (id, combo_id, name, priority, tier, is_enabled, fallback_delay, max_retries, health_threshold)
|
|
117
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
118
|
+
`).run(tierId, id, tier.name, tier.priority, tier.tier, tier.isEnabled ? 1 : 0, tier.fallbackDelay, tier.maxRetries, tier.healthThreshold ? JSON.stringify(tier.healthThreshold) : null);
|
|
119
|
+
// Insert tier providers
|
|
120
|
+
for (const provider of tier.providers) {
|
|
121
|
+
const providerId = randomUUID();
|
|
122
|
+
this.db.prepare(`
|
|
123
|
+
INSERT INTO routing_tier_providers (id, tier_id, provider_id, weight, is_enabled, model_override)
|
|
124
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
125
|
+
`).run(providerId, tierId, provider.providerId, provider.weight, provider.isEnabled ? 1 : 0, provider.modelOverride || null);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Insert policies
|
|
129
|
+
this.db.prepare(`
|
|
130
|
+
INSERT INTO routing_combo_policies (combo_id, load_balancing, failover_strategy, token_budget_mode, quota_management, cost_optimization, retry_policy)
|
|
131
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
132
|
+
`).run(id, input.policies.loadBalancing, input.policies.failoverStrategy, input.policies.tokenBudgetMode, input.policies.quotaManagement ? JSON.stringify(input.policies.quotaManagement) : null, input.policies.costOptimization ? JSON.stringify(input.policies.costOptimization) : null, input.policies.retryPolicy ? JSON.stringify(input.policies.retryPolicy) : null);
|
|
133
|
+
// Insert client routes
|
|
134
|
+
for (const clientRoute of input.clientRoutes) {
|
|
135
|
+
this.db.prepare(`
|
|
136
|
+
INSERT INTO routing_combo_client_routes (combo_id, client_route)
|
|
137
|
+
VALUES (?, ?)
|
|
138
|
+
`).run(id, clientRoute);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
transaction();
|
|
142
|
+
const combo = await this.getComboById(id);
|
|
143
|
+
if (!combo) {
|
|
144
|
+
throw new Error('Failed to create routing combo');
|
|
145
|
+
}
|
|
146
|
+
return combo;
|
|
147
|
+
}
|
|
148
|
+
// Update existing routing combo
|
|
149
|
+
async updateCombo(id, input) {
|
|
150
|
+
const now = new Date().toISOString();
|
|
151
|
+
const transaction = this.db.transaction(() => {
|
|
152
|
+
// Update combo
|
|
153
|
+
this.db.prepare(`
|
|
154
|
+
UPDATE routing_combos
|
|
155
|
+
SET name = ?, description = ?, is_active = ?, is_default = ?, updated_at = ?
|
|
156
|
+
WHERE id = ?
|
|
157
|
+
`).run(input.name, input.description || null, input.isActive ? 1 : 0, input.isDefault ? 1 : 0, now, id);
|
|
158
|
+
// Delete existing tiers and their providers (cascade will handle providers)
|
|
159
|
+
this.db.prepare(`DELETE FROM routing_tiers WHERE combo_id = ?`).run(id);
|
|
160
|
+
// Delete existing policies
|
|
161
|
+
this.db.prepare(`DELETE FROM routing_combo_policies WHERE combo_id = ?`).run(id);
|
|
162
|
+
// Delete existing client routes
|
|
163
|
+
this.db.prepare(`DELETE FROM routing_combo_client_routes WHERE combo_id = ?`).run(id);
|
|
164
|
+
// Insert new tiers
|
|
165
|
+
for (const tier of input.tiers) {
|
|
166
|
+
const tierId = randomUUID();
|
|
167
|
+
this.db.prepare(`
|
|
168
|
+
INSERT INTO routing_tiers (id, combo_id, name, priority, tier, is_enabled, fallback_delay, max_retries, health_threshold)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
170
|
+
`).run(tierId, id, tier.name, tier.priority, tier.tier, tier.isEnabled ? 1 : 0, tier.fallbackDelay, tier.maxRetries, tier.healthThreshold ? JSON.stringify(tier.healthThreshold) : null);
|
|
171
|
+
// Insert tier providers
|
|
172
|
+
for (const provider of tier.providers) {
|
|
173
|
+
const providerId = randomUUID();
|
|
174
|
+
this.db.prepare(`
|
|
175
|
+
INSERT INTO routing_tier_providers (id, tier_id, provider_id, weight, is_enabled, model_override)
|
|
176
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
177
|
+
`).run(providerId, tierId, provider.providerId, provider.weight, provider.isEnabled ? 1 : 0, provider.modelOverride || null);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Insert new policies
|
|
181
|
+
this.db.prepare(`
|
|
182
|
+
INSERT INTO routing_combo_policies (combo_id, load_balancing, failover_strategy, token_budget_mode, quota_management, cost_optimization, retry_policy)
|
|
183
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
184
|
+
`).run(id, input.policies.loadBalancing, input.policies.failoverStrategy, input.policies.tokenBudgetMode, input.policies.quotaManagement ? JSON.stringify(input.policies.quotaManagement) : null, input.policies.costOptimization ? JSON.stringify(input.policies.costOptimization) : null, input.policies.retryPolicy ? JSON.stringify(input.policies.retryPolicy) : null);
|
|
185
|
+
// Insert new client routes
|
|
186
|
+
for (const clientRoute of input.clientRoutes) {
|
|
187
|
+
this.db.prepare(`
|
|
188
|
+
INSERT INTO routing_combo_client_routes (combo_id, client_route)
|
|
189
|
+
VALUES (?, ?)
|
|
190
|
+
`).run(id, clientRoute);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
transaction();
|
|
194
|
+
const combo = await this.getComboById(id);
|
|
195
|
+
if (!combo) {
|
|
196
|
+
throw new Error('Failed to update routing combo');
|
|
197
|
+
}
|
|
198
|
+
return combo;
|
|
199
|
+
}
|
|
200
|
+
// Delete routing combo
|
|
201
|
+
async deleteCombo(id) {
|
|
202
|
+
const combo = await this.getComboById(id);
|
|
203
|
+
if (!combo) {
|
|
204
|
+
throw new Error('Routing combo not found');
|
|
205
|
+
}
|
|
206
|
+
// Safety check: prevent deleting active combos
|
|
207
|
+
if (combo.isActive) {
|
|
208
|
+
throw new Error('Cannot delete active routing combo. Deactivate it first.');
|
|
209
|
+
}
|
|
210
|
+
// Safety check: prevent deleting default combo
|
|
211
|
+
if (combo.isDefault) {
|
|
212
|
+
throw new Error('Cannot delete default routing combo. Set another combo as default first.');
|
|
213
|
+
}
|
|
214
|
+
this.db.prepare(`DELETE FROM routing_combos WHERE id = ?`).run(id);
|
|
215
|
+
}
|
|
216
|
+
// Get combo statistics
|
|
217
|
+
async getComboStats() {
|
|
218
|
+
const totalResult = this.db.prepare(`SELECT COUNT(*) as count FROM routing_combos`).get();
|
|
219
|
+
const activeResult = this.db.prepare(`SELECT COUNT(*) as count FROM routing_combos WHERE is_active = 1`).get();
|
|
220
|
+
const clientRoutesResult = this.db.prepare(`SELECT COUNT(*) as count FROM routing_combo_client_routes`).get();
|
|
221
|
+
const tiersResult = this.db.prepare(`SELECT COUNT(*) as count FROM routing_tiers`).get();
|
|
222
|
+
return {
|
|
223
|
+
total: totalResult.count,
|
|
224
|
+
active: activeResult.count,
|
|
225
|
+
totalClientRoutes: clientRoutesResult.count,
|
|
226
|
+
averageTiersPerCombo: totalResult.count > 0 ? tiersResult.count / totalResult.count : 0
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
// Set default combo
|
|
230
|
+
async setDefaultCombo(id) {
|
|
231
|
+
const transaction = this.db.transaction(() => {
|
|
232
|
+
// Clear existing default
|
|
233
|
+
this.db.prepare(`UPDATE routing_combos SET is_default = 0`).run();
|
|
234
|
+
// Set new default
|
|
235
|
+
this.db.prepare(`UPDATE routing_combos SET is_default = 1 WHERE id = ?`).run(id);
|
|
236
|
+
});
|
|
237
|
+
transaction();
|
|
238
|
+
}
|
|
239
|
+
// Get default combo
|
|
240
|
+
async getDefaultCombo() {
|
|
241
|
+
const row = this.db.prepare(`
|
|
242
|
+
SELECT id FROM routing_combos WHERE is_default = 1 LIMIT 1
|
|
243
|
+
`).get();
|
|
244
|
+
if (!row) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
return this.getComboById(row.id);
|
|
248
|
+
}
|
|
249
|
+
// Client route assignment methods
|
|
250
|
+
// Get routing combo assigned to a client route
|
|
251
|
+
async getClientRouteCombo(clientRoute) {
|
|
252
|
+
const row = this.db.prepare(`
|
|
253
|
+
SELECT combo_id FROM routing_combo_client_routes WHERE client_route = ?
|
|
254
|
+
`).get(clientRoute);
|
|
255
|
+
return row?.combo_id || null;
|
|
256
|
+
}
|
|
257
|
+
// Assign routing combo to client route
|
|
258
|
+
async assignClientRouteCombo(clientRoute, comboId) {
|
|
259
|
+
// Verify combo exists
|
|
260
|
+
const combo = await this.getComboById(comboId);
|
|
261
|
+
if (!combo) {
|
|
262
|
+
throw new Error(`Routing combo ${comboId} not found`);
|
|
263
|
+
}
|
|
264
|
+
// Use INSERT OR REPLACE to handle both new assignments and updates
|
|
265
|
+
this.db.prepare(`
|
|
266
|
+
INSERT OR REPLACE INTO routing_combo_client_routes (combo_id, client_route)
|
|
267
|
+
VALUES (?, ?)
|
|
268
|
+
`).run(comboId, clientRoute);
|
|
269
|
+
}
|
|
270
|
+
// Remove routing combo assignment from client route
|
|
271
|
+
async unassignClientRouteCombo(clientRoute) {
|
|
272
|
+
this.db.prepare(`
|
|
273
|
+
DELETE FROM routing_combo_client_routes WHERE client_route = ?
|
|
274
|
+
`).run(clientRoute);
|
|
275
|
+
}
|
|
276
|
+
// Get all client routes assigned to a combo
|
|
277
|
+
async getComboClientRoutes(comboId) {
|
|
278
|
+
const rows = this.db.prepare(`
|
|
279
|
+
SELECT client_route FROM routing_combo_client_routes WHERE combo_id = ?
|
|
280
|
+
`).all(comboId);
|
|
281
|
+
return rows.map(row => row.client_route);
|
|
282
|
+
}
|
|
283
|
+
// Get all client route assignments
|
|
284
|
+
async getAllClientRouteAssignments() {
|
|
285
|
+
const rows = this.db.prepare(`
|
|
286
|
+
SELECT
|
|
287
|
+
rcr.client_route,
|
|
288
|
+
rcr.combo_id,
|
|
289
|
+
rc.name as combo_name
|
|
290
|
+
FROM routing_combo_client_routes rcr
|
|
291
|
+
JOIN routing_combos rc ON rcr.combo_id = rc.id
|
|
292
|
+
ORDER BY rcr.client_route
|
|
293
|
+
`).all();
|
|
294
|
+
return rows.map(row => ({
|
|
295
|
+
clientRoute: row.client_route,
|
|
296
|
+
comboId: row.combo_id,
|
|
297
|
+
comboName: row.combo_name
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
}
|