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,147 @@
1
+ /**
2
+ * Simple model combo repository — 9Router-style.
3
+ *
4
+ * A "model combo" is a named, ordered list of model strings (e.g.
5
+ * "cc/claude-opus-4-7", "kr/claude-sonnet-4.5") with simple fallback semantics.
6
+ * When a request specifies a combo name as the model, the proxy tries each model
7
+ * in order until one succeeds.
8
+ *
9
+ * This is intentionally simpler than the advanced routing combo system
10
+ * (RoutingComboRepository) which uses tiers, weights, health checks, etc.
11
+ */
12
+ import { randomUUID } from "node:crypto";
13
+ // Validate combo name: only a-z, A-Z, 0-9, -, _, .
14
+ const VALID_NAME_REGEX = /^[a-zA-Z0-9_.\-]+$/;
15
+ export class ModelComboRepository {
16
+ db;
17
+ constructor(db) {
18
+ this.db = db;
19
+ this.ensureTable();
20
+ }
21
+ ensureTable() {
22
+ this.db.exec(`
23
+ CREATE TABLE IF NOT EXISTS model_combos (
24
+ id TEXT PRIMARY KEY,
25
+ name TEXT UNIQUE NOT NULL,
26
+ kind TEXT,
27
+ models TEXT NOT NULL DEFAULT '[]',
28
+ round_robin INTEGER NOT NULL DEFAULT 0,
29
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
30
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
31
+ );
32
+ CREATE INDEX IF NOT EXISTS idx_model_combos_name ON model_combos(name);
33
+ `);
34
+ }
35
+ /** Get all model combos, optionally filtered by kind */
36
+ getAll(kind) {
37
+ let rows;
38
+ if (kind === undefined || kind === null) {
39
+ rows = this.db.prepare(`SELECT * FROM model_combos ORDER BY created_at ASC`).all();
40
+ }
41
+ else {
42
+ rows = this.db.prepare(`SELECT * FROM model_combos WHERE kind = ? ORDER BY created_at ASC`).all(kind);
43
+ }
44
+ return rows.map(rowToCombo);
45
+ }
46
+ /** Get combos without a kind (LLM combos) */
47
+ getLlmCombos() {
48
+ const rows = this.db.prepare(`SELECT * FROM model_combos WHERE kind IS NULL ORDER BY created_at ASC`).all();
49
+ return rows.map(rowToCombo);
50
+ }
51
+ getById(id) {
52
+ const row = this.db.prepare(`SELECT * FROM model_combos WHERE id = ?`).get(id);
53
+ return row ? rowToCombo(row) : null;
54
+ }
55
+ getByName(name) {
56
+ const row = this.db.prepare(`SELECT * FROM model_combos WHERE name = ?`).get(name);
57
+ return row ? rowToCombo(row) : null;
58
+ }
59
+ create(input) {
60
+ if (!input.name || !input.name.trim()) {
61
+ throw new ModelComboValidationError("Name is required");
62
+ }
63
+ if (!VALID_NAME_REGEX.test(input.name)) {
64
+ throw new ModelComboValidationError("Name can only contain letters, numbers, -, _ and .");
65
+ }
66
+ const existing = this.getByName(input.name);
67
+ if (existing) {
68
+ throw new ModelComboValidationError("Combo name already exists");
69
+ }
70
+ const id = randomUUID();
71
+ const now = new Date().toISOString();
72
+ const models = input.models || [];
73
+ const roundRobin = input.roundRobin ? 1 : 0;
74
+ this.db.prepare(`
75
+ INSERT INTO model_combos (id, name, kind, models, round_robin, created_at, updated_at)
76
+ VALUES (?, ?, ?, ?, ?, ?, ?)
77
+ `).run(id, input.name, input.kind || null, JSON.stringify(models), roundRobin, now, now);
78
+ return this.getById(id);
79
+ }
80
+ update(id, input) {
81
+ const existing = this.getById(id);
82
+ if (!existing) {
83
+ throw new ModelComboNotFoundError(id);
84
+ }
85
+ if (input.name !== undefined) {
86
+ if (!input.name.trim()) {
87
+ throw new ModelComboValidationError("Name is required");
88
+ }
89
+ if (!VALID_NAME_REGEX.test(input.name)) {
90
+ throw new ModelComboValidationError("Name can only contain letters, numbers, -, _ and .");
91
+ }
92
+ // Check uniqueness if name changed
93
+ if (input.name !== existing.name) {
94
+ const conflict = this.getByName(input.name);
95
+ if (conflict) {
96
+ throw new ModelComboValidationError("Combo name already exists");
97
+ }
98
+ }
99
+ }
100
+ const now = new Date().toISOString();
101
+ const name = input.name ?? existing.name;
102
+ const kind = input.kind !== undefined ? input.kind : existing.kind;
103
+ const models = input.models ?? existing.models;
104
+ const roundRobin = input.roundRobin !== undefined ? (input.roundRobin ? 1 : 0) : (existing.roundRobin ? 1 : 0);
105
+ this.db.prepare(`
106
+ UPDATE model_combos
107
+ SET name = ?, kind = ?, models = ?, round_robin = ?, updated_at = ?
108
+ WHERE id = ?
109
+ `).run(name, kind || null, JSON.stringify(models), roundRobin, now, id);
110
+ return this.getById(id);
111
+ }
112
+ delete(id) {
113
+ const result = this.db.prepare(`DELETE FROM model_combos WHERE id = ?`).run(id);
114
+ return (result.changes ?? 0) > 0;
115
+ }
116
+ }
117
+ function rowToCombo(row) {
118
+ let models;
119
+ try {
120
+ models = JSON.parse(row.models);
121
+ }
122
+ catch {
123
+ models = [];
124
+ }
125
+ return {
126
+ id: row.id,
127
+ name: row.name,
128
+ kind: row.kind,
129
+ models,
130
+ roundRobin: Boolean(row.round_robin),
131
+ createdAt: row.created_at,
132
+ updatedAt: row.updated_at,
133
+ };
134
+ }
135
+ // ─── Errors ───────────────────────────────────────────────────────────────────
136
+ export class ModelComboValidationError extends Error {
137
+ constructor(message) {
138
+ super(message);
139
+ this.name = "ModelComboValidationError";
140
+ }
141
+ }
142
+ export class ModelComboNotFoundError extends Error {
143
+ constructor(id) {
144
+ super(`Model combo not found: ${id}`);
145
+ this.name = "ModelComboNotFoundError";
146
+ }
147
+ }
@@ -0,0 +1,69 @@
1
+ export function resolveModelRouting(requestBody, policy) {
2
+ if (!policy.enabled) {
3
+ return { downgraded: false };
4
+ }
5
+ const model = typeof requestBody.model === "string" ? requestBody.model : "";
6
+ const isExpensiveModel = /gpt-4|o1|o3|claude-3-5|claude-opus/i.test(model);
7
+ if (!isExpensiveModel) {
8
+ return { downgraded: false };
9
+ }
10
+ if (policy.skipIfTools && Array.isArray(requestBody.tools) && requestBody.tools.length > 0) {
11
+ return { downgraded: false };
12
+ }
13
+ if (policy.skipIfReasoning && requestBody.reasoning !== undefined) {
14
+ return { downgraded: false };
15
+ }
16
+ if (policy.skipIfImages && hasImageInput(requestBody)) {
17
+ return { downgraded: false };
18
+ }
19
+ const estimatedTokens = estimateInputTokens(requestBody);
20
+ if (estimatedTokens > policy.inputTokenThreshold) {
21
+ return { downgraded: false };
22
+ }
23
+ return {
24
+ downgraded: true,
25
+ originalModel: model,
26
+ resolvedModel: policy.cheapModel,
27
+ reason: `estimated_tokens:${estimatedTokens}<threshold:${policy.inputTokenThreshold}`,
28
+ };
29
+ }
30
+ function estimateInputTokens(body) {
31
+ const instructions = typeof body.instructions === "string" ? body.instructions : "";
32
+ const input = Array.isArray(body.input) ? body.input : [];
33
+ const inputText = input
34
+ .map((item) => {
35
+ if (typeof item !== "object" || item === null) {
36
+ return typeof item === "string" ? item : "";
37
+ }
38
+ const content = item.content;
39
+ if (typeof content === "string") {
40
+ return content;
41
+ }
42
+ if (Array.isArray(content)) {
43
+ return content
44
+ .map((part) => typeof part === "object" && part !== null
45
+ ? String(part.text ?? "")
46
+ : "")
47
+ .join(" ");
48
+ }
49
+ return "";
50
+ })
51
+ .join(" ");
52
+ return Math.ceil((instructions.length + inputText.length) / 4);
53
+ }
54
+ function hasImageInput(body) {
55
+ const input = Array.isArray(body.input) ? body.input : [];
56
+ return input.some((item) => {
57
+ if (typeof item !== "object" || item === null) {
58
+ return false;
59
+ }
60
+ const content = item.content;
61
+ if (!Array.isArray(content)) {
62
+ return false;
63
+ }
64
+ return content.some((part) => typeof part === "object" &&
65
+ part !== null &&
66
+ (part.type === "image_url" ||
67
+ part.type === "input_image"));
68
+ });
69
+ }
@@ -0,0 +1,41 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { resolveModelRouting } from "./model-routing.js";
4
+ const policy = {
5
+ enabled: true,
6
+ inputTokenThreshold: 2000,
7
+ cheapModel: "gpt-4o-mini",
8
+ skipIfTools: true,
9
+ skipIfImages: true,
10
+ skipIfReasoning: true,
11
+ };
12
+ test("resolveModelRouting downgrades small expensive text requests", () => {
13
+ const decision = resolveModelRouting({
14
+ model: "gpt-4.1",
15
+ instructions: "Be concise.",
16
+ input: [{ role: "user", content: "Hello" }],
17
+ }, policy);
18
+ assert.equal(decision.downgraded, true);
19
+ if (decision.downgraded) {
20
+ assert.equal(decision.originalModel, "gpt-4.1");
21
+ assert.equal(decision.resolvedModel, "gpt-4o-mini");
22
+ assert.match(decision.reason, /^estimated_tokens:/);
23
+ }
24
+ });
25
+ test("resolveModelRouting skips tools images reasoning and large requests", () => {
26
+ assert.deepEqual(resolveModelRouting({ model: "gpt-4.1", tools: [{ type: "function" }], input: [] }, policy), { downgraded: false });
27
+ assert.deepEqual(resolveModelRouting({
28
+ model: "gpt-4.1",
29
+ input: [
30
+ {
31
+ role: "user",
32
+ content: [{ type: "input_image", image_url: "https://example.test/image.png" }],
33
+ },
34
+ ],
35
+ }, policy), { downgraded: false });
36
+ assert.deepEqual(resolveModelRouting({ model: "gpt-4.1", reasoning: { effort: "high" }, input: [] }, policy), { downgraded: false });
37
+ assert.deepEqual(resolveModelRouting({
38
+ model: "gpt-4.1",
39
+ input: [{ role: "user", content: "x".repeat(20_000) }],
40
+ }, policy), { downgraded: false });
41
+ });