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,129 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildClientTokenLimitError, extractUsageTotals, getClientTokenLimitStatus, resolveClientTokenWindowStart, } from "./client-token-limits.js";
|
|
4
|
+
const baseConfig = {
|
|
5
|
+
clientRoute: "codex",
|
|
6
|
+
enabled: true,
|
|
7
|
+
tokenLimit: 1000,
|
|
8
|
+
windowType: "daily",
|
|
9
|
+
hardBlock: true,
|
|
10
|
+
createdAt: "2026-04-27T00:00:00.000Z",
|
|
11
|
+
updatedAt: "2026-04-27T00:00:00.000Z",
|
|
12
|
+
};
|
|
13
|
+
const baseUsage = {
|
|
14
|
+
clientRoute: "codex",
|
|
15
|
+
windowStart: "2026-04-27T00:00:00.000Z",
|
|
16
|
+
inputTokens: 100,
|
|
17
|
+
outputTokens: 50,
|
|
18
|
+
totalTokens: 150,
|
|
19
|
+
updatedAt: "2026-04-27T12:00:00.000Z",
|
|
20
|
+
};
|
|
21
|
+
test("resolveClientTokenWindowStart supports daily weekly monthly and fixed windows", () => {
|
|
22
|
+
const now = new Date("2026-04-29T13:45:30.000Z");
|
|
23
|
+
assert.equal(resolveClientTokenWindowStart(now, { windowType: "daily" }), "2026-04-29T00:00:00.000Z");
|
|
24
|
+
assert.equal(resolveClientTokenWindowStart(now, { windowType: "weekly" }), "2026-04-27T00:00:00.000Z");
|
|
25
|
+
assert.equal(resolveClientTokenWindowStart(now, { windowType: "monthly" }), "2026-04-01T00:00:00.000Z");
|
|
26
|
+
assert.equal(resolveClientTokenWindowStart(now, { windowType: "fixed", windowSizeSeconds: 3600 }), "2026-04-29T13:00:00.000Z");
|
|
27
|
+
});
|
|
28
|
+
test("getClientTokenLimitStatus does not block disabled config", () => {
|
|
29
|
+
assert.deepEqual(getClientTokenLimitStatus({ ...baseConfig, enabled: false }, baseUsage), {
|
|
30
|
+
used: 150,
|
|
31
|
+
limit: null,
|
|
32
|
+
remaining: null,
|
|
33
|
+
blocked: false,
|
|
34
|
+
windowStart: "2026-04-27T00:00:00.000Z",
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
test("getClientTokenLimitStatus reports usage under limit", () => {
|
|
38
|
+
assert.deepEqual(getClientTokenLimitStatus(baseConfig, baseUsage), {
|
|
39
|
+
used: 150,
|
|
40
|
+
limit: 1000,
|
|
41
|
+
remaining: 850,
|
|
42
|
+
blocked: false,
|
|
43
|
+
windowStart: "2026-04-27T00:00:00.000Z",
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
test("getClientTokenLimitStatus blocks exactly at limit", () => {
|
|
47
|
+
assert.deepEqual(getClientTokenLimitStatus(baseConfig, {
|
|
48
|
+
...baseUsage,
|
|
49
|
+
totalTokens: 1000,
|
|
50
|
+
}), {
|
|
51
|
+
used: 1000,
|
|
52
|
+
limit: 1000,
|
|
53
|
+
remaining: 0,
|
|
54
|
+
blocked: true,
|
|
55
|
+
windowStart: "2026-04-27T00:00:00.000Z",
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
test("getClientTokenLimitStatus blocks above limit", () => {
|
|
59
|
+
assert.deepEqual(getClientTokenLimitStatus(baseConfig, {
|
|
60
|
+
...baseUsage,
|
|
61
|
+
totalTokens: 1075,
|
|
62
|
+
}), {
|
|
63
|
+
used: 1075,
|
|
64
|
+
limit: 1000,
|
|
65
|
+
remaining: 0,
|
|
66
|
+
blocked: true,
|
|
67
|
+
windowStart: "2026-04-27T00:00:00.000Z",
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
test("buildClientTokenLimitError creates a 429 request error body", () => {
|
|
71
|
+
const status = getClientTokenLimitStatus(baseConfig, {
|
|
72
|
+
...baseUsage,
|
|
73
|
+
totalTokens: 1000,
|
|
74
|
+
});
|
|
75
|
+
assert.deepEqual(buildClientTokenLimitError("codex", status), {
|
|
76
|
+
statusCode: 429,
|
|
77
|
+
body: {
|
|
78
|
+
error: {
|
|
79
|
+
type: "request_error",
|
|
80
|
+
code: "CLIENT_TOKEN_LIMIT_EXCEEDED",
|
|
81
|
+
message: "Client route 'codex' has reached its token limit for the current window.",
|
|
82
|
+
client: "codex",
|
|
83
|
+
client_route: "codex",
|
|
84
|
+
usage: status,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
test("extractUsageTotals reads direct and nested usage totals", () => {
|
|
90
|
+
assert.deepEqual(extractUsageTotals({
|
|
91
|
+
input_tokens: 10,
|
|
92
|
+
output_tokens: 20,
|
|
93
|
+
total_tokens: 30,
|
|
94
|
+
}), {
|
|
95
|
+
inputTokens: 10,
|
|
96
|
+
outputTokens: 20,
|
|
97
|
+
totalTokens: 30,
|
|
98
|
+
});
|
|
99
|
+
assert.deepEqual(extractUsageTotals({
|
|
100
|
+
usage: {
|
|
101
|
+
input_tokens: 5,
|
|
102
|
+
output_tokens: 6,
|
|
103
|
+
total_tokens: 11,
|
|
104
|
+
},
|
|
105
|
+
}), {
|
|
106
|
+
inputTokens: 5,
|
|
107
|
+
outputTokens: 6,
|
|
108
|
+
totalTokens: 11,
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(extractUsageTotals({
|
|
111
|
+
response: {
|
|
112
|
+
usage: {
|
|
113
|
+
input_tokens: 3,
|
|
114
|
+
output_tokens: 4,
|
|
115
|
+
total_tokens: 7,
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
}), {
|
|
119
|
+
inputTokens: 3,
|
|
120
|
+
outputTokens: 4,
|
|
121
|
+
totalTokens: 7,
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
test("extractUsageTotals ignores usage payloads without total_tokens", () => {
|
|
125
|
+
assert.equal(extractUsageTotals({
|
|
126
|
+
input_tokens: 10,
|
|
127
|
+
output_tokens: 20,
|
|
128
|
+
}), undefined);
|
|
129
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
export function resolveDefaultCodexConfigPath() {
|
|
4
|
+
return `${homedir()}/.codex/config.toml`;
|
|
5
|
+
}
|
|
6
|
+
export function readCodexProviderFromConfig(filePath) {
|
|
7
|
+
let raw;
|
|
8
|
+
try {
|
|
9
|
+
raw = readFileSync(filePath, "utf8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const providerName = readTopLevelTomlString(raw, "model_provider");
|
|
15
|
+
if (!providerName) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const section = readTomlSection(raw, `model_providers.${providerName}`);
|
|
19
|
+
if (!section) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const baseUrl = readTomlString(section, "base_url");
|
|
23
|
+
if (!baseUrl) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
name: readTomlString(section, "name") ?? providerName,
|
|
28
|
+
baseUrl,
|
|
29
|
+
wireApi: readTomlString(section, "wire_api"),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function readTopLevelTomlString(raw, key) {
|
|
33
|
+
const match = raw.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
|
|
34
|
+
return match?.[1];
|
|
35
|
+
}
|
|
36
|
+
function readTomlSection(raw, sectionName) {
|
|
37
|
+
const escaped = escapeRegExp(sectionName);
|
|
38
|
+
const match = raw.match(new RegExp(`^\\[${escaped}\\]\\s*$([\\s\\S]*?)(?=^\\[|\\Z)`, "m"));
|
|
39
|
+
return match?.[1];
|
|
40
|
+
}
|
|
41
|
+
function readTomlString(rawSection, key) {
|
|
42
|
+
const match = rawSection.match(new RegExp(`^${escapeRegExp(key)}\\s*=\\s*\"([^\"]+)\"\\s*$`, "m"));
|
|
43
|
+
return match?.[1];
|
|
44
|
+
}
|
|
45
|
+
function escapeRegExp(value) {
|
|
46
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function buildCodexConfigFiles(input) {
|
|
3
|
+
const model = input.model?.trim() || "gpt-5.5";
|
|
4
|
+
const baseUrl = input.baseUrl.trim();
|
|
5
|
+
const apiKey = input.apiKey.trim();
|
|
6
|
+
return {
|
|
7
|
+
configToml: [
|
|
8
|
+
`model = ${tomlString(model)}`,
|
|
9
|
+
`model_provider = "resproxy"`,
|
|
10
|
+
`model_reasoning_effort = "medium"`,
|
|
11
|
+
"",
|
|
12
|
+
`[model_providers.resproxy]`,
|
|
13
|
+
`name = "resproxy"`,
|
|
14
|
+
`base_url = ${tomlString(baseUrl)}`,
|
|
15
|
+
`api_key = ${tomlString(apiKey)}`,
|
|
16
|
+
`wire_api = "responses"`,
|
|
17
|
+
"",
|
|
18
|
+
].join("\n"),
|
|
19
|
+
authJson: `${JSON.stringify({
|
|
20
|
+
auth_mode: "apikey",
|
|
21
|
+
OPENAI_API_KEY: apiKey,
|
|
22
|
+
}, null, 2)}\n`,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export function buildCodexSetupEndpointUrl(publicResponsesBaseUrl) {
|
|
26
|
+
const trimmed = publicResponsesBaseUrl.trim().replace(/\/+$/, "");
|
|
27
|
+
const origin = trimmed.endsWith("/v1") ? trimmed.slice(0, -3) : trimmed;
|
|
28
|
+
return `${origin}/api/customer/codex/setup.sh`;
|
|
29
|
+
}
|
|
30
|
+
export function buildCodexSetupCurlCommand(input) {
|
|
31
|
+
const setupUrl = buildCodexSetupEndpointUrl(input.publicResponsesBaseUrl);
|
|
32
|
+
return [
|
|
33
|
+
"curl -fsSL \\",
|
|
34
|
+
` -H 'Authorization: Bearer ${escapeShellSingleQuote(input.apiKey.trim())}' \\`,
|
|
35
|
+
` '${escapeShellSingleQuote(setupUrl)}' \\`,
|
|
36
|
+
" | sh",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
39
|
+
export function buildCodexConfigSetupScript(input) {
|
|
40
|
+
const targetDir = input.targetDir?.trim() || "$HOME/.codex";
|
|
41
|
+
const configDelimiter = buildHeredocDelimiter("RESPONSES_PROXY_CODEX_CONFIG", input.configToml);
|
|
42
|
+
const authDelimiter = buildHeredocDelimiter("RESPONSES_PROXY_CODEX_AUTH", input.authJson);
|
|
43
|
+
return [
|
|
44
|
+
"#!/usr/bin/env sh",
|
|
45
|
+
"set -eu",
|
|
46
|
+
"umask 077",
|
|
47
|
+
'tmpdir="$(mktemp -d "${TMPDIR:-/tmp}/responses-proxy-codex.XXXXXX")"',
|
|
48
|
+
'trap \'rm -rf "$tmpdir"\' EXIT INT TERM',
|
|
49
|
+
`cat > "$tmpdir/config.toml" <<'${configDelimiter}'`,
|
|
50
|
+
input.configToml,
|
|
51
|
+
configDelimiter,
|
|
52
|
+
`cat > "$tmpdir/auth.json" <<'${authDelimiter}'`,
|
|
53
|
+
input.authJson,
|
|
54
|
+
authDelimiter,
|
|
55
|
+
"install_file() {",
|
|
56
|
+
' src="$1"',
|
|
57
|
+
' dest="$2"',
|
|
58
|
+
' dest_dir="$(dirname "$dest")"',
|
|
59
|
+
' mkdir -p "$dest_dir"',
|
|
60
|
+
' if [ -f "$dest" ] && cmp -s "$src" "$dest"; then',
|
|
61
|
+
' printf "%s\\n" "unchanged: $dest"',
|
|
62
|
+
" return 0",
|
|
63
|
+
" fi",
|
|
64
|
+
' if [ -f "$dest" ]; then',
|
|
65
|
+
' backup="$dest.$(date +%Y%m%d-%H%M%S).bak"',
|
|
66
|
+
' cp "$dest" "$backup"',
|
|
67
|
+
' printf "%s\\n" "backup: $backup"',
|
|
68
|
+
" fi",
|
|
69
|
+
' mv "$src" "$dest"',
|
|
70
|
+
' printf "%s\\n" "updated: $dest"',
|
|
71
|
+
"}",
|
|
72
|
+
`install_file "$tmpdir/config.toml" "${targetDir}/config.toml"`,
|
|
73
|
+
`install_file "$tmpdir/auth.json" "${targetDir}/auth.json"`,
|
|
74
|
+
'printf "%s\\n" "Codex setup applied."',
|
|
75
|
+
"",
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
function buildHeredocDelimiter(prefix, content) {
|
|
79
|
+
const digest = createHash("sha256").update(content).digest("hex").slice(0, 16).toUpperCase();
|
|
80
|
+
return `${prefix}_${digest}`;
|
|
81
|
+
}
|
|
82
|
+
function escapeShellSingleQuote(value) {
|
|
83
|
+
return value.replace(/'/g, `'\\''`);
|
|
84
|
+
}
|
|
85
|
+
function tomlString(value) {
|
|
86
|
+
return JSON.stringify(value);
|
|
87
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildCodexConfigFiles, buildCodexConfigSetupScript, buildCodexSetupCurlCommand, buildCodexSetupEndpointUrl, } from "./codex-setup.js";
|
|
4
|
+
test("buildCodexSetupEndpointUrl strips the public /v1 suffix", () => {
|
|
5
|
+
assert.equal(buildCodexSetupEndpointUrl("https://proxy.example.com/v1/"), "https://proxy.example.com/api/customer/codex/setup.sh");
|
|
6
|
+
});
|
|
7
|
+
test("buildCodexSetupCurlCommand targets the customer setup endpoint", () => {
|
|
8
|
+
const command = buildCodexSetupCurlCommand({
|
|
9
|
+
publicResponsesBaseUrl: "https://proxy.example.com/v1",
|
|
10
|
+
apiKey: "sk-customer-secret",
|
|
11
|
+
});
|
|
12
|
+
assert.match(command, /curl -fsSL/);
|
|
13
|
+
assert.match(command, /Authorization: Bearer sk-customer-secret/);
|
|
14
|
+
assert.match(command, /https:\/\/proxy\.example\.com\/api\/customer\/codex\/setup\.sh/);
|
|
15
|
+
assert.match(command, /\| sh$/);
|
|
16
|
+
});
|
|
17
|
+
test("buildCodexConfigSetupScript writes both Codex config files with backups", () => {
|
|
18
|
+
const files = buildCodexConfigFiles({
|
|
19
|
+
baseUrl: "https://proxy.example.com/v1",
|
|
20
|
+
apiKey: "sk-customer-secret",
|
|
21
|
+
model: "gpt-5.5",
|
|
22
|
+
});
|
|
23
|
+
const script = buildCodexConfigSetupScript(files);
|
|
24
|
+
assert.match(script, /mktemp -d/);
|
|
25
|
+
assert.match(script, /backup="\$dest\.\$\(date \+%Y%m%d-%H%M%S\)\.bak"/);
|
|
26
|
+
assert.match(script, /install_file "\$tmpdir\/config\.toml" "\$HOME\/\.codex\/config\.toml"/);
|
|
27
|
+
assert.match(script, /install_file "\$tmpdir\/auth\.json" "\$HOME\/\.codex\/auth\.json"/);
|
|
28
|
+
assert.match(script, /base_url = "https:\/\/proxy\.example\.com\/v1"/);
|
|
29
|
+
assert.match(script, /OPENAI_API_KEY": "sk-customer-secret"/);
|
|
30
|
+
});
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
function defaultKiroDbPath() {
|
|
5
|
+
return path.join(os.homedir(), ".9router", "db", "data.sqlite");
|
|
6
|
+
}
|
|
7
|
+
function parseIdList(value) {
|
|
8
|
+
return (value ?? "")
|
|
9
|
+
.split(/[,\r?\n]+/g)
|
|
10
|
+
.map((entry) => entry.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
}
|
|
13
|
+
function parseDelimitedList(value) {
|
|
14
|
+
return (value ?? "")
|
|
15
|
+
.split(/[,\r?\n]+/g)
|
|
16
|
+
.map((entry) => entry.trim())
|
|
17
|
+
.filter(Boolean);
|
|
18
|
+
}
|
|
19
|
+
const envSchema = z.object({
|
|
20
|
+
PORT: z.coerce.number().int().positive().default(8318),
|
|
21
|
+
HOST: z.string().min(1).default("0.0.0.0"),
|
|
22
|
+
UPSTREAM_BASE_URL: z.url(),
|
|
23
|
+
UPSTREAM_API_KEY: z.string().optional(),
|
|
24
|
+
PROVIDER_USAGE_CHECK_URL: z
|
|
25
|
+
.string()
|
|
26
|
+
.optional()
|
|
27
|
+
.transform((value) => (value?.trim() ? value.trim() : undefined))
|
|
28
|
+
.pipe(z.url().optional()),
|
|
29
|
+
PROVIDER_USAGE_CHECK_ENABLED: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.transform((value) => value !== "false"),
|
|
33
|
+
REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000),
|
|
34
|
+
SUMMARY_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(900_000),
|
|
35
|
+
STREAM_IDLE_TIMEOUT_MS: z.coerce.number().int().positive().default(330_000),
|
|
36
|
+
HERMES_EXTEND_SUMMARY_TIMEOUT: z
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.transform((value) => value !== "false"),
|
|
40
|
+
REQUEST_BODY_LIMIT_BYTES: z.coerce.number().int().positive().default(25 * 1024 * 1024),
|
|
41
|
+
HTTP_TRUST_PROXY: z
|
|
42
|
+
.string()
|
|
43
|
+
.optional()
|
|
44
|
+
.transform((value) => value === "true"),
|
|
45
|
+
HTTP_RATE_LIMIT_ENABLED: z
|
|
46
|
+
.string()
|
|
47
|
+
.optional()
|
|
48
|
+
.transform((value) => value !== "false"),
|
|
49
|
+
HTTP_RATE_LIMIT_WINDOW_MS: z.coerce.number().int().positive().default(60_000),
|
|
50
|
+
HTTP_RATE_LIMIT_RESPONSES_MAX_REQUESTS: z.coerce.number().int().positive().default(120),
|
|
51
|
+
HTTP_RATE_LIMIT_UNAUTHENTICATED_MAX_REQUESTS: z.coerce.number().int().positive().default(20),
|
|
52
|
+
HTTP_RATE_LIMIT_AUTH_MAX_REQUESTS: z.coerce.number().int().positive().default(30),
|
|
53
|
+
HTTP_RATE_LIMIT_WEBHOOK_MAX_REQUESTS: z.coerce.number().int().positive().default(60),
|
|
54
|
+
HTTP_RATE_LIMIT_HEALTH_MAX_REQUESTS: z.coerce.number().int().positive().default(240),
|
|
55
|
+
LOG_LEVEL: z
|
|
56
|
+
.enum(["fatal", "error", "warn", "info", "debug", "trace", "silent"])
|
|
57
|
+
.default("info"),
|
|
58
|
+
LOG_BODY: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.transform((value) => value === "true"),
|
|
62
|
+
CHATGPT_OAUTH_ENABLED: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.transform((value) => value === "true"),
|
|
66
|
+
MODEL_ROUTING_ENABLED: z
|
|
67
|
+
.string()
|
|
68
|
+
.optional()
|
|
69
|
+
.transform((value) => value === "true"),
|
|
70
|
+
MODEL_ROUTING_CHEAP_MODEL: z.string().optional().default("gpt-4o-mini"),
|
|
71
|
+
MODEL_ROUTING_INPUT_TOKEN_THRESHOLD: z.coerce.number().int().positive().default(2000),
|
|
72
|
+
MODEL_ROUTING_SKIP_IF_TOOLS: z
|
|
73
|
+
.string()
|
|
74
|
+
.optional()
|
|
75
|
+
.transform((value) => value !== "false"),
|
|
76
|
+
MODEL_ROUTING_SKIP_IF_IMAGES: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.transform((value) => value !== "false"),
|
|
80
|
+
MODEL_ROUTING_SKIP_IF_REASONING: z
|
|
81
|
+
.string()
|
|
82
|
+
.optional()
|
|
83
|
+
.transform((value) => value !== "false"),
|
|
84
|
+
CHATGPT_OAUTH_CLIENT_ID: z.string().min(1).default("app_EMoamEEZ73f0CkXaXp7hrann"),
|
|
85
|
+
CHATGPT_OAUTH_REDIRECT_URI: z
|
|
86
|
+
.string()
|
|
87
|
+
.min(1)
|
|
88
|
+
.default("http://localhost:1455/auth/callback"),
|
|
89
|
+
CHATGPT_OAUTH_CALLBACK_PORT: z.coerce.number().int().positive().default(1455),
|
|
90
|
+
CHATGPT_OAUTH_AUTH_URL: z
|
|
91
|
+
.string()
|
|
92
|
+
.min(1)
|
|
93
|
+
.default("https://auth.openai.com/oauth/authorize")
|
|
94
|
+
.pipe(z.url()),
|
|
95
|
+
CHATGPT_OAUTH_TOKEN_URL: z
|
|
96
|
+
.string()
|
|
97
|
+
.min(1)
|
|
98
|
+
.default("https://auth.openai.com/oauth/token")
|
|
99
|
+
.pipe(z.url()),
|
|
100
|
+
CHATGPT_OAUTH_DEVICE_USER_CODE_URL: z
|
|
101
|
+
.string()
|
|
102
|
+
.min(1)
|
|
103
|
+
.default("https://auth.openai.com/api/accounts/deviceauth/usercode")
|
|
104
|
+
.pipe(z.url()),
|
|
105
|
+
CHATGPT_OAUTH_DEVICE_TOKEN_URL: z
|
|
106
|
+
.string()
|
|
107
|
+
.min(1)
|
|
108
|
+
.default("https://auth.openai.com/api/accounts/deviceauth/token")
|
|
109
|
+
.pipe(z.url()),
|
|
110
|
+
CHATGPT_OAUTH_DEVICE_VERIFICATION_URL: z
|
|
111
|
+
.string()
|
|
112
|
+
.min(1)
|
|
113
|
+
.default("https://auth.openai.com/codex/device")
|
|
114
|
+
.pipe(z.url()),
|
|
115
|
+
CHATGPT_CODEX_BASE_URL: z
|
|
116
|
+
.string()
|
|
117
|
+
.min(1)
|
|
118
|
+
.default("https://chatgpt.com/backend-api/codex")
|
|
119
|
+
.pipe(z.url()),
|
|
120
|
+
RESPONSES_PROXY_DEFAULT_MODEL: z.string().default("gpt-5.5"),
|
|
121
|
+
BOT_PUBLIC_RESPONSES_BASE_URL: z.string().optional(),
|
|
122
|
+
CHATGPT_OAUTH_REFRESH_LEAD_DAYS: z.coerce.number().positive().default(5),
|
|
123
|
+
OPENCLAW_TOKEN_OPTIMIZATION_ENABLED: z
|
|
124
|
+
.string()
|
|
125
|
+
.optional()
|
|
126
|
+
.transform((value) => value !== "false"),
|
|
127
|
+
OPENCLAW_DEFAULT_REASONING_SUMMARY: z
|
|
128
|
+
.enum(["auto", "none", "concise", "detailed"])
|
|
129
|
+
.default("auto"),
|
|
130
|
+
OPENCLAW_DEFAULT_REASONING_EFFORT: z
|
|
131
|
+
.enum(["minimal", "low", "medium", "high"])
|
|
132
|
+
.default("low"),
|
|
133
|
+
OPENCLAW_DEFAULT_TEXT_VERBOSITY: z
|
|
134
|
+
.enum(["low", "medium", "high"])
|
|
135
|
+
.default("low"),
|
|
136
|
+
OPENCLAW_DEFAULT_MAX_OUTPUT_TOKENS: z
|
|
137
|
+
.string()
|
|
138
|
+
.optional()
|
|
139
|
+
.transform((value) => {
|
|
140
|
+
if (!value?.trim()) {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
const parsed = Number(value);
|
|
144
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
145
|
+
}),
|
|
146
|
+
OPENCLAW_AUTO_PROMPT_CACHE_KEY: z
|
|
147
|
+
.string()
|
|
148
|
+
.optional()
|
|
149
|
+
.transform((value) => value !== "false"),
|
|
150
|
+
OPENCLAW_PROMPT_CACHE_RETENTION: z.string().min(1).default("24h"),
|
|
151
|
+
RESPONSE_CACHE_ENABLED: z
|
|
152
|
+
.string()
|
|
153
|
+
.optional()
|
|
154
|
+
.transform((value) => value === "true"),
|
|
155
|
+
RESPONSE_CACHE_TTL_MS: z.coerce.number().int().positive().default(5 * 60 * 1000),
|
|
156
|
+
RESPONSE_CACHE_MAX_PAYLOAD_BYTES: z.coerce.number().int().positive().default(512 * 1024),
|
|
157
|
+
PROVIDER_PROMPT_CACHE_REDESIGN_ENABLED: z
|
|
158
|
+
.string()
|
|
159
|
+
.optional()
|
|
160
|
+
.transform((value) => value === "true"),
|
|
161
|
+
PROVIDER_PROMPT_CACHE_STABLE_SUMMARIZATION_ENABLED: z
|
|
162
|
+
.string()
|
|
163
|
+
.optional()
|
|
164
|
+
.transform((value) => value === "true"),
|
|
165
|
+
PROVIDER_PROMPT_CACHE_INFLIGHT_DEDUPE_ENABLED: z
|
|
166
|
+
.string()
|
|
167
|
+
.optional()
|
|
168
|
+
.transform((value) => value !== "false"),
|
|
169
|
+
PROVIDER_PROMPT_CACHE_RETENTION_BY_FAMILY_ENABLED: z
|
|
170
|
+
.string()
|
|
171
|
+
.optional()
|
|
172
|
+
.transform((value) => value === "true"),
|
|
173
|
+
PROVIDER_PROMPT_CACHE_SUMMARY_TRIGGER_ITEMS: z.coerce.number().int().positive().default(14),
|
|
174
|
+
PROVIDER_PROMPT_CACHE_SUMMARY_KEEP_RECENT_ITEMS: z.coerce.number().int().positive().default(6),
|
|
175
|
+
PROVIDER_PROMPT_CACHE_RETENTION_BY_FAMILY: z
|
|
176
|
+
.string()
|
|
177
|
+
.optional()
|
|
178
|
+
.transform(parsePromptCacheFamilyRetentionRules),
|
|
179
|
+
PROVIDER_PROMPT_CACHE_RETENTION_BY_STATIC_KEY_ENABLED: z
|
|
180
|
+
.string()
|
|
181
|
+
.optional()
|
|
182
|
+
.transform((value) => value === "true"),
|
|
183
|
+
PROVIDER_PROMPT_CACHE_RETENTION_BY_STATIC_KEY: z
|
|
184
|
+
.string()
|
|
185
|
+
.optional()
|
|
186
|
+
.transform(parsePromptCacheFamilyRetentionRules),
|
|
187
|
+
RTK_LAYER_ENABLED: z
|
|
188
|
+
.string()
|
|
189
|
+
.optional()
|
|
190
|
+
.transform((value) => value === "true"),
|
|
191
|
+
RTK_LAYER_TOOL_OUTPUT_ENABLED: z
|
|
192
|
+
.string()
|
|
193
|
+
.optional()
|
|
194
|
+
.transform((value) => value !== "false"),
|
|
195
|
+
RTK_LAYER_TOOL_OUTPUT_MAX_CHARS: z.coerce.number().int().positive().default(4000),
|
|
196
|
+
RTK_LAYER_TOOL_OUTPUT_MAX_LINES: z.coerce.number().int().positive().default(120),
|
|
197
|
+
RTK_LAYER_TOOL_OUTPUT_TAIL_LINES: z.coerce.number().int().nonnegative().default(0),
|
|
198
|
+
RTK_LAYER_TOOL_OUTPUT_TAIL_CHARS: z.coerce.number().int().nonnegative().default(0),
|
|
199
|
+
RTK_LAYER_TOOL_OUTPUT_DETECT_FORMAT: z
|
|
200
|
+
.enum(["auto", "plain", "json", "stack", "command"])
|
|
201
|
+
.default("auto"),
|
|
202
|
+
OPENCLAW_DEFAULT_TRUNCATION: z.enum(["auto", "disabled"]).default("auto"),
|
|
203
|
+
MAX_OUTPUT_TOKENS_PARAMETER_MODE_FOR_PROVIDER: z
|
|
204
|
+
.enum(["forward", "strip", "rename"])
|
|
205
|
+
.optional(),
|
|
206
|
+
MAX_OUTPUT_TOKENS_PARAMETER_TARGET_FOR_PROVIDER: z.string().optional(),
|
|
207
|
+
STRIP_MAX_OUTPUT_TOKENS_FOR_PROVIDER: z
|
|
208
|
+
.string()
|
|
209
|
+
.optional()
|
|
210
|
+
.transform((value) => value !== "false"),
|
|
211
|
+
SANITIZE_REASONING_SUMMARY_FOR_PROVIDER: z
|
|
212
|
+
.string()
|
|
213
|
+
.optional()
|
|
214
|
+
.transform((value) => value !== "false"),
|
|
215
|
+
FALLBACK_ENABLED: z
|
|
216
|
+
.string()
|
|
217
|
+
.optional()
|
|
218
|
+
.transform((value) => value !== "false"),
|
|
219
|
+
FALLBACK_STATUS_CODES: z
|
|
220
|
+
.string()
|
|
221
|
+
.default("429,500,502,503,504")
|
|
222
|
+
.transform(parseFallbackStatusCodes),
|
|
223
|
+
RESPONSES_PROXY_CLIENT_API_KEY: z.string().optional(),
|
|
224
|
+
TELEGRAM_BOT_TOKEN: z.string().optional(),
|
|
225
|
+
TELEGRAM_OWNER_USER_IDS: z.string().optional().transform(parseIdList),
|
|
226
|
+
TELEGRAM_ADMIN_USER_IDS: z.string().optional().transform(parseIdList),
|
|
227
|
+
DASHBOARD_AUTH_OTP_TTL_MS: z.coerce.number().int().positive().default(10 * 60 * 1000),
|
|
228
|
+
DASHBOARD_AUTH_SESSION_TTL_MS: z.coerce.number().int().positive().default(12 * 60 * 60 * 1000),
|
|
229
|
+
APP_DB_PATH: z.string().min(1).default("./logs/app.sqlite"),
|
|
230
|
+
CUSTOMER_KEY_DB_PATH: z.string().min(1).default("./logs/telegram-bot.sqlite"),
|
|
231
|
+
SESSION_LOG_DIR: z.string().min(1).default("./logs/sessions"),
|
|
232
|
+
SESSION_LOG_RETENTION_DAYS: z.coerce.number().int().nonnegative().default(14),
|
|
233
|
+
SEPAY_WEBHOOK_ENABLED: z
|
|
234
|
+
.string()
|
|
235
|
+
.optional()
|
|
236
|
+
.transform((value) => {
|
|
237
|
+
const normalized = value?.trim().toLowerCase();
|
|
238
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
239
|
+
}),
|
|
240
|
+
SEPAY_WEBHOOK_SECRET: z.string().optional(),
|
|
241
|
+
SEPAY_WEBHOOK_ALLOWED_IPS: z.string().optional().transform(parseDelimitedList),
|
|
242
|
+
KIRO_ENABLED: z
|
|
243
|
+
.string()
|
|
244
|
+
.optional()
|
|
245
|
+
.transform((value) => value === "true"),
|
|
246
|
+
KIRO_DB_PATH: z
|
|
247
|
+
.string()
|
|
248
|
+
.optional()
|
|
249
|
+
.transform((value) => (value?.trim() ? value.trim() : defaultKiroDbPath())),
|
|
250
|
+
KIRO_DEFAULT_REGION: z.string().min(1).default("us-east-1"),
|
|
251
|
+
KIRO_REFRESH_LEAD_SECONDS: z.coerce.number().int().nonnegative().default(120),
|
|
252
|
+
KIRO_WRITE_BACK_ENABLED: z
|
|
253
|
+
.string()
|
|
254
|
+
.optional()
|
|
255
|
+
.transform((value) => value !== "false"),
|
|
256
|
+
KIRO_DEVICE_CLIENT_NAME: z.string().min(1).default("responses-proxy"),
|
|
257
|
+
KIRO_BUILDER_ID_START_URL: z.string().min(1).default("https://view.awsapps.com/start"),
|
|
258
|
+
KIRO_DEVICE_SCOPES: z
|
|
259
|
+
.string()
|
|
260
|
+
.min(1)
|
|
261
|
+
.default("codewhisperer:completions,codewhisperer:analysis")
|
|
262
|
+
.transform((v) => v.split(",").map((s) => s.trim()).filter(Boolean)),
|
|
263
|
+
// Routing System Configuration
|
|
264
|
+
ROUTING_HEALTH_CHECK_INTERVAL: z.coerce.number().int().positive().default(30000), // 30 seconds
|
|
265
|
+
ROUTING_WEBSOCKET_BROADCAST_INTERVAL: z.coerce.number().int().positive().default(5000), // 5 seconds
|
|
266
|
+
ROUTING_PROVIDER_HEALTH_CACHE_TTL: z.coerce.number().int().positive().default(60000), // 1 minute
|
|
267
|
+
ROUTING_HEALTH_SCORE_THRESHOLD: z.coerce.number().int().min(0).max(100).default(50), // Minimum eligibility score
|
|
268
|
+
ROUTING_MAX_FALLBACK_DELAY: z.coerce.number().int().nonnegative().default(10000), // 10 seconds max delay
|
|
269
|
+
ROUTING_ENABLED: z
|
|
270
|
+
.string()
|
|
271
|
+
.optional()
|
|
272
|
+
.transform((value) => value !== "false"), // Enable routing system by default
|
|
273
|
+
});
|
|
274
|
+
export function readConfig(env) {
|
|
275
|
+
const parsed = envSchema.parse(env);
|
|
276
|
+
const base = parsed.UPSTREAM_BASE_URL.replace(/\/+$/, "");
|
|
277
|
+
const publicResponsesBaseUrl = parsed.BOT_PUBLIC_RESPONSES_BASE_URL?.trim().replace(/\/+$/, "") ||
|
|
278
|
+
`http://127.0.0.1:${parsed.PORT}/v1`;
|
|
279
|
+
return {
|
|
280
|
+
...parsed,
|
|
281
|
+
upstreamResponsesUrl: `${base}/responses`,
|
|
282
|
+
publicResponsesBaseUrl,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function parsePromptCacheFamilyRetentionRules(raw) {
|
|
286
|
+
if (!raw?.trim()) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
return raw
|
|
290
|
+
.split(",")
|
|
291
|
+
.map((entry) => {
|
|
292
|
+
const separatorIndex = entry.indexOf("=");
|
|
293
|
+
if (separatorIndex <= 0) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
const prefix = entry.slice(0, separatorIndex).trim();
|
|
297
|
+
const retention = entry.slice(separatorIndex + 1).trim();
|
|
298
|
+
if (!prefix || !retention) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
prefix,
|
|
303
|
+
retention,
|
|
304
|
+
};
|
|
305
|
+
})
|
|
306
|
+
.filter((entry) => Boolean(entry));
|
|
307
|
+
}
|
|
308
|
+
function parseFallbackStatusCodes(raw) {
|
|
309
|
+
const codes = raw
|
|
310
|
+
.split(",")
|
|
311
|
+
.map((part) => Number(part.trim()))
|
|
312
|
+
.filter((value) => Number.isInteger(value) && value >= 400 && value <= 599);
|
|
313
|
+
return codes.length > 0 ? codes : [429, 500, 502, 503, 504];
|
|
314
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function buildCostSummary(observations) {
|
|
2
|
+
const total = observations.length;
|
|
3
|
+
if (total === 0) {
|
|
4
|
+
const now = new Date().toISOString();
|
|
5
|
+
return {
|
|
6
|
+
window: { from: now, to: now },
|
|
7
|
+
totalRequests: 0,
|
|
8
|
+
promptCacheHits: 0,
|
|
9
|
+
promptCacheHitRate: 0,
|
|
10
|
+
avgCacheSavedPercent: 0,
|
|
11
|
+
estimatedTokensSaved: 0,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const hits = observations.filter((observation) => observation.cacheHit === true).length;
|
|
15
|
+
const savedPcts = observations
|
|
16
|
+
.map((observation) => observation.cacheSavedPercent)
|
|
17
|
+
.filter((value) => typeof value === "number");
|
|
18
|
+
const avgSaved = savedPcts.length > 0 ? savedPcts.reduce((left, right) => left + right, 0) / savedPcts.length : 0;
|
|
19
|
+
const tokensSaved = observations
|
|
20
|
+
.map((observation) => observation.cachedTokens ?? 0)
|
|
21
|
+
.reduce((left, right) => left + right, 0);
|
|
22
|
+
const timestamps = observations.map((observation) => observation.timestamp).sort();
|
|
23
|
+
return {
|
|
24
|
+
window: { from: timestamps[0] ?? "", to: timestamps[timestamps.length - 1] ?? "" },
|
|
25
|
+
totalRequests: total,
|
|
26
|
+
promptCacheHits: hits,
|
|
27
|
+
promptCacheHitRate: total > 0 ? hits / total : 0,
|
|
28
|
+
avgCacheSavedPercent: avgSaved,
|
|
29
|
+
estimatedTokensSaved: tokensSaved,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildCostSummary } from "./cost-analytics.js";
|
|
4
|
+
test("buildCostSummary aggregates prompt cache observations", () => {
|
|
5
|
+
const summary = buildCostSummary([
|
|
6
|
+
{
|
|
7
|
+
cacheHit: true,
|
|
8
|
+
cacheSavedPercent: 50,
|
|
9
|
+
cachedTokens: 100,
|
|
10
|
+
timestamp: "2026-05-11T00:02:00.000Z",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
cacheHit: false,
|
|
14
|
+
cacheSavedPercent: 10,
|
|
15
|
+
cachedTokens: 0,
|
|
16
|
+
timestamp: "2026-05-11T00:01:00.000Z",
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
assert.deepEqual(summary.window, {
|
|
20
|
+
from: "2026-05-11T00:01:00.000Z",
|
|
21
|
+
to: "2026-05-11T00:02:00.000Z",
|
|
22
|
+
});
|
|
23
|
+
assert.equal(summary.totalRequests, 2);
|
|
24
|
+
assert.equal(summary.promptCacheHits, 1);
|
|
25
|
+
assert.equal(summary.promptCacheHitRate, 0.5);
|
|
26
|
+
assert.equal(summary.avgCacheSavedPercent, 30);
|
|
27
|
+
assert.equal(summary.estimatedTokensSaved, 100);
|
|
28
|
+
});
|
|
29
|
+
test("buildCostSummary returns zero summary for empty observations", () => {
|
|
30
|
+
const summary = buildCostSummary([]);
|
|
31
|
+
assert.equal(summary.totalRequests, 0);
|
|
32
|
+
assert.equal(summary.promptCacheHits, 0);
|
|
33
|
+
assert.equal(summary.promptCacheHitRate, 0);
|
|
34
|
+
assert.equal(summary.avgCacheSavedPercent, 0);
|
|
35
|
+
assert.equal(summary.estimatedTokensSaved, 0);
|
|
36
|
+
assert.match(summary.window.from, /^\d{4}-\d{2}-\d{2}T/);
|
|
37
|
+
assert.equal(summary.window.to, summary.window.from);
|
|
38
|
+
});
|