responses-proxy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. package/README.md +56 -0
  2. package/cli.js +118 -0
  3. package/dist/anthropic-messages.js +383 -0
  4. package/dist/anthropic-messages.test.js +209 -0
  5. package/dist/audit-log.js +138 -0
  6. package/dist/audit-log.test.js +480 -0
  7. package/dist/billing-expiration.js +70 -0
  8. package/dist/billing-expiration.test.js +114 -0
  9. package/dist/billing.js +716 -0
  10. package/dist/billing.test.js +228 -0
  11. package/dist/chatgpt-oauth-store.js +240 -0
  12. package/dist/chatgpt-oauth-store.test.js +88 -0
  13. package/dist/chatgpt-oauth.js +118 -0
  14. package/dist/chatgpt-oauth.test.js +63 -0
  15. package/dist/chatgpt-provider-auth.js +60 -0
  16. package/dist/chatgpt-provider-auth.test.js +101 -0
  17. package/dist/client/app-icon.svg +17 -0
  18. package/dist/client/assets/index-C7Vvhst8.js +14 -0
  19. package/dist/client/assets/index-DpqgYK3L.css +1 -0
  20. package/dist/client/favicon.svg +17 -0
  21. package/dist/client/index.html +31 -0
  22. package/dist/client-config-apply.js +345 -0
  23. package/dist/client-config-apply.test.js +185 -0
  24. package/dist/client-token-limits.js +111 -0
  25. package/dist/client-token-limits.test.js +129 -0
  26. package/dist/codex-config.js +47 -0
  27. package/dist/codex-setup.js +87 -0
  28. package/dist/codex-setup.test.js +30 -0
  29. package/dist/config.js +314 -0
  30. package/dist/cost-analytics.js +31 -0
  31. package/dist/cost-analytics.test.js +38 -0
  32. package/dist/customer-key-access.js +126 -0
  33. package/dist/customer-key-access.test.js +178 -0
  34. package/dist/customer-keys.js +209 -0
  35. package/dist/customer-keys.test.js +68 -0
  36. package/dist/customer-usage.js +18 -0
  37. package/dist/customer-usage.test.js +55 -0
  38. package/dist/dashboard-auth.js +318 -0
  39. package/dist/dashboard-auth.test.js +133 -0
  40. package/dist/dashboard-serving.test.js +235 -0
  41. package/dist/error-response.js +174 -0
  42. package/dist/error-response.test.js +88 -0
  43. package/dist/forward.js +357 -0
  44. package/dist/health-websocket-manager.js +174 -0
  45. package/dist/http-rate-limit.js +36 -0
  46. package/dist/http-rate-limit.test.js +62 -0
  47. package/dist/kiro-auth.js +136 -0
  48. package/dist/kiro-auth.test.js +234 -0
  49. package/dist/kiro-codewhisperer.js +646 -0
  50. package/dist/kiro-codewhisperer.test.js +219 -0
  51. package/dist/kiro-device-login.js +338 -0
  52. package/dist/kiro-eventstream.js +219 -0
  53. package/dist/kiro-eventstream.test.js +79 -0
  54. package/dist/kiro-forward.js +401 -0
  55. package/dist/kiro-import-cli.js +69 -0
  56. package/dist/kiro-import.js +94 -0
  57. package/dist/kiro-import.test.js +125 -0
  58. package/dist/kiro-token-store.js +196 -0
  59. package/dist/kiro-token-store.test.js +207 -0
  60. package/dist/krouter-usage.js +243 -0
  61. package/dist/model-combo-repository.js +147 -0
  62. package/dist/model-routing.js +69 -0
  63. package/dist/model-routing.test.js +41 -0
  64. package/dist/normalize-request.js +531 -0
  65. package/dist/normalize-request.test.js +277 -0
  66. package/dist/omv-public-firewall.test.js +11 -0
  67. package/dist/package.json +17 -0
  68. package/dist/prompt-cache-state.js +146 -0
  69. package/dist/prompt-cache-state.test.js +71 -0
  70. package/dist/prompt-cache.js +229 -0
  71. package/dist/provider-health-service.js +404 -0
  72. package/dist/provider-request-parameters.js +107 -0
  73. package/dist/provider-request-parameters.test.js +26 -0
  74. package/dist/provider-routing.js +114 -0
  75. package/dist/provider-routing.test.js +64 -0
  76. package/dist/provider-usage.js +314 -0
  77. package/dist/request-timeout-policy.js +61 -0
  78. package/dist/request-timeout-policy.test.js +40 -0
  79. package/dist/response-cache.js +69 -0
  80. package/dist/response-cache.test.js +28 -0
  81. package/dist/routing-combo-repository.js +300 -0
  82. package/dist/routing-engine.js +377 -0
  83. package/dist/routing-integration.js +155 -0
  84. package/dist/routing-simulation-engine.js +326 -0
  85. package/dist/rtk-layer.js +483 -0
  86. package/dist/rtk-layer.test.js +198 -0
  87. package/dist/runtime-provider-repository.js +1742 -0
  88. package/dist/runtime-provider-repository.test.js +1177 -0
  89. package/dist/schema.js +118 -0
  90. package/dist/schema.test.js +16 -0
  91. package/dist/sepay-webhook.js +87 -0
  92. package/dist/sepay-webhook.test.js +142 -0
  93. package/dist/server-body-limit.test.js +35 -0
  94. package/dist/server-client-token-limits.test.js +161 -0
  95. package/dist/server-codex-config-setup.test.js +76 -0
  96. package/dist/server-http-rate-limit.test.js +80 -0
  97. package/dist/server-response-cache.test.js +105 -0
  98. package/dist/server-routes-alias.test.js +39 -0
  99. package/dist/server-sepay-webhook-security.test.js +59 -0
  100. package/dist/server.js +5906 -0
  101. package/dist/session-log.js +178 -0
  102. package/dist/tailnet-funnel-script.test.js +33 -0
  103. package/dist/telegram-bot/actions.js +118 -0
  104. package/dist/telegram-bot/admin-actions.js +103 -0
  105. package/dist/telegram-bot/auth.js +46 -0
  106. package/dist/telegram-bot/auth.test.js +1 -0
  107. package/dist/telegram-bot/bot-identity-repository.js +189 -0
  108. package/dist/telegram-bot/bot-identity-repository.test.js +78 -0
  109. package/dist/telegram-bot/callbacks.js +30 -0
  110. package/dist/telegram-bot/codex-config-delivery.js +38 -0
  111. package/dist/telegram-bot/codex-config-delivery.test.js +75 -0
  112. package/dist/telegram-bot/commands/accounts.js +140 -0
  113. package/dist/telegram-bot/commands/apikey.js +737 -0
  114. package/dist/telegram-bot/commands/apply.js +265 -0
  115. package/dist/telegram-bot/commands/clients.js +13 -0
  116. package/dist/telegram-bot/commands/customer-billing.test.js +271 -0
  117. package/dist/telegram-bot/commands/grant.js +138 -0
  118. package/dist/telegram-bot/commands/grant.test.js +217 -0
  119. package/dist/telegram-bot/commands/help.js +52 -0
  120. package/dist/telegram-bot/commands/me.js +53 -0
  121. package/dist/telegram-bot/commands/models.js +6 -0
  122. package/dist/telegram-bot/commands/oauth.js +64 -0
  123. package/dist/telegram-bot/commands/plans.js +96 -0
  124. package/dist/telegram-bot/commands/providers.js +27 -0
  125. package/dist/telegram-bot/commands/quota.js +10 -0
  126. package/dist/telegram-bot/commands/renew-user.js +139 -0
  127. package/dist/telegram-bot/commands/renew-user.test.js +184 -0
  128. package/dist/telegram-bot/commands/renew.js +1369 -0
  129. package/dist/telegram-bot/commands/renew.test.js +1633 -0
  130. package/dist/telegram-bot/commands/start.js +212 -0
  131. package/dist/telegram-bot/commands/start.test.js +280 -0
  132. package/dist/telegram-bot/commands/status.js +6 -0
  133. package/dist/telegram-bot/commands/tailscale.js +15 -0
  134. package/dist/telegram-bot/commands/tailscale.test.js +76 -0
  135. package/dist/telegram-bot/commands/test.js +51 -0
  136. package/dist/telegram-bot/commands/test.test.js +14 -0
  137. package/dist/telegram-bot/commands/usage.js +10 -0
  138. package/dist/telegram-bot/config.js +98 -0
  139. package/dist/telegram-bot/config.test.js +42 -0
  140. package/dist/telegram-bot/customer-actions.js +160 -0
  141. package/dist/telegram-bot/customer-api-keys.js +68 -0
  142. package/dist/telegram-bot/customer-billing.js +72 -0
  143. package/dist/telegram-bot/customer-workspace-repository.js +134 -0
  144. package/dist/telegram-bot/customer-workspace-repository.test.js +47 -0
  145. package/dist/telegram-bot/dashboard-login.js +39 -0
  146. package/dist/telegram-bot/format.js +140 -0
  147. package/dist/telegram-bot/grants.js +370 -0
  148. package/dist/telegram-bot/grants.test.js +290 -0
  149. package/dist/telegram-bot/index.js +85 -0
  150. package/dist/telegram-bot/message-cleanup.js +55 -0
  151. package/dist/telegram-bot/message-cleanup.test.js +77 -0
  152. package/dist/telegram-bot/message-format.js +45 -0
  153. package/dist/telegram-bot/message-format.test.js +10 -0
  154. package/dist/telegram-bot/proxy-client.js +174 -0
  155. package/dist/telegram-bot/rate-limit.js +95 -0
  156. package/dist/telegram-bot/rate-limit.test.js +58 -0
  157. package/dist/telegram-bot/sessions.js +171 -0
  158. package/dist/telegram-bot/sessions.test.js +107 -0
  159. package/dist/telegram-bot/telegram-adapter.js +126 -0
  160. package/dist/telegram-bot/worker.js +63 -0
  161. package/package.json +39 -0
@@ -0,0 +1,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
+ }