saros-proxy 0.11.0 → 1.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 +149 -60
- package/assets/models.json +3349 -0
- package/dist/cli/autostart.d.ts +13 -6
- package/dist/cli/autostart.js +317 -60
- package/dist/cli/autostart.js.map +1 -1
- package/dist/cli/daemon.js +66 -8
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/harness-models.d.ts +5 -4
- package/dist/cli/harness-models.js +77 -30
- package/dist/cli/harness-models.js.map +1 -1
- package/dist/cli/help.js +5 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/opencode-config.d.ts +1 -0
- package/dist/cli/opencode-config.js +7 -6
- package/dist/cli/opencode-config.js.map +1 -1
- package/dist/cli/usage.js +13 -10
- package/dist/cli/usage.js.map +1 -1
- package/dist/constants.d.ts +2 -0
- package/dist/constants.js +2 -0
- package/dist/constants.js.map +1 -1
- package/dist/index.js +29 -36
- package/dist/index.js.map +1 -1
- package/dist/model-ladder.d.ts +30 -0
- package/dist/model-ladder.js +45 -0
- package/dist/model-ladder.js.map +1 -0
- package/dist/models-catalog.d.ts +25 -4
- package/dist/models-catalog.js +41 -12
- package/dist/models-catalog.js.map +1 -1
- package/dist/models-dev.d.ts +72 -0
- package/dist/models-dev.js +196 -0
- package/dist/models-dev.js.map +1 -0
- package/dist/models-fetcher.js +2 -0
- package/dist/models-fetcher.js.map +1 -1
- package/dist/models-refresh.d.ts +144 -0
- package/dist/models-refresh.js +522 -0
- package/dist/models-refresh.js.map +1 -0
- package/dist/models-sync.d.ts +6 -10
- package/dist/models-sync.js +26 -60
- package/dist/models-sync.js.map +1 -1
- package/dist/providers/commandcode.d.ts +20 -5
- package/dist/providers/commandcode.js +115 -7
- package/dist/providers/commandcode.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.js +2 -2
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/opencode-go.d.ts +8 -0
- package/dist/providers/opencode-go.js +35 -7
- package/dist/providers/opencode-go.js.map +1 -1
- package/dist/providers/types.d.ts +10 -2
- package/dist/proxy-logic.d.ts +27 -4
- package/dist/proxy-logic.js +86 -32
- package/dist/proxy-logic.js.map +1 -1
- package/dist/proxy.js +41 -22
- package/dist/proxy.js.map +1 -1
- package/dist/token-usage.d.ts +9 -2
- package/dist/token-usage.js +20 -4
- package/dist/token-usage.js.map +1 -1
- package/dist/usage-client.d.ts +43 -13
- package/dist/usage-client.js +37 -54
- package/dist/usage-client.js.map +1 -1
- package/package.json +5 -4
- package/models.json +0 -1440
package/dist/token-usage.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Storage mirrors model-stats.ts: a tiny JSON file in the saros config dir,
|
|
12
12
|
* written behind a debounce so the request path never blocks on I/O.
|
|
13
13
|
*/
|
|
14
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
15
15
|
import { dirname } from 'node:path';
|
|
16
16
|
import { TOKEN_USAGE_SAVE_DEBOUNCE_MS } from './constants.js';
|
|
17
17
|
import { getSarosDir } from './config.js';
|
|
@@ -60,7 +60,10 @@ export function parseTokenCounts(usage) {
|
|
|
60
60
|
return {
|
|
61
61
|
promptTokens,
|
|
62
62
|
completionTokens,
|
|
63
|
-
|
|
63
|
+
// A stream merges counts per field, so a chunk can contribute a stale
|
|
64
|
+
// total while a later chunk raises prompt/completion. Never report a
|
|
65
|
+
// total below the parts the upstream itself reported.
|
|
66
|
+
totalTokens: Math.max(total, promptTokens + completionTokens),
|
|
64
67
|
};
|
|
65
68
|
}
|
|
66
69
|
function load() {
|
|
@@ -188,8 +191,13 @@ export function getKeyTokenUsage() {
|
|
|
188
191
|
load();
|
|
189
192
|
return { since, keys: Object.fromEntries(counters) };
|
|
190
193
|
}
|
|
191
|
-
/**
|
|
192
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Test seam: drop in-memory state and any pending debounced write. By default
|
|
196
|
+
* the persisted file is removed too, so counters cannot leak between tests
|
|
197
|
+
* that share a config dir; `keepFile` simulates a process restart that
|
|
198
|
+
* reloads existing history.
|
|
199
|
+
*/
|
|
200
|
+
export function resetTokenUsage(options = {}) {
|
|
193
201
|
if (saveTimer !== undefined) {
|
|
194
202
|
clearTimeout(saveTimer);
|
|
195
203
|
saveTimer = undefined;
|
|
@@ -197,5 +205,13 @@ export function resetTokenUsage() {
|
|
|
197
205
|
counters.clear();
|
|
198
206
|
since = 0;
|
|
199
207
|
loaded = false;
|
|
208
|
+
if (options.keepFile)
|
|
209
|
+
return;
|
|
210
|
+
try {
|
|
211
|
+
rmSync(getTokenUsagePath(), { force: true });
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
logger.debug({ err }, 'failed to remove token usage file');
|
|
215
|
+
}
|
|
200
216
|
}
|
|
201
217
|
//# sourceMappingURL=token-usage.js.map
|
package/dist/token-usage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"token-usage.js","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"token-usage.js","sourceRoot":"","sources":["../src/token-usage.ts"],"names":[],"mappings":"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"}
|
package/dist/usage-client.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* usage-client.ts — Per-key quota usage from
|
|
2
|
+
* usage-client.ts — Per-key quota usage from each provider's usage API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every provider owns its endpoint path, response parser and identity
|
|
5
|
+
* headers (opencode-go: `/zen/go/v1/usage` behind Cloudflare, which needs the
|
|
6
|
+
* CLI User-Agent; commandcode: `/alpha/billing/credits`, gated on the same
|
|
7
|
+
* CLI identity). `fetchKeyUsage` only adds the bearer credential.
|
|
7
8
|
*
|
|
8
9
|
* Refresh strategy: stale-while-revalidate on demand. `maybeRefreshUsage`
|
|
9
10
|
* is called on the request path; when data is older than USAGE_REFRESH_TTL_MS
|
|
@@ -13,9 +14,36 @@
|
|
|
13
14
|
*/
|
|
14
15
|
import { type ProxyState } from './proxy-logic.js';
|
|
15
16
|
import type { ProxyConfig } from './config.js';
|
|
17
|
+
import type { KeyProvider } from './providers/index.js';
|
|
18
|
+
/**
|
|
19
|
+
* Per-key quota usage, normalized as a percentage *of the provider's own
|
|
20
|
+
* budget* — never absolute capacity. `usedPercent` is the **monthly** window:
|
|
21
|
+
* 0 = untouched, 100 = monthly allowance spent. `exhausted` is true when any
|
|
22
|
+
* window (5h, weekly or monthly) reached 100%, which takes the key out of
|
|
23
|
+
* rotation regardless of how much of the month remains — otherwise a key with
|
|
24
|
+
* 50% monthly and a spent weekly window would keep receiving traffic and
|
|
25
|
+
* failing upstream.
|
|
26
|
+
*
|
|
27
|
+
* This normalization is the contract mixed-provider pools rely on:
|
|
28
|
+
* `pickWeighted` (`proxy-logic.ts`) compares the numbers from every provider
|
|
29
|
+
* directly, so "30%" must always mean "30% of that provider's monthly budget".
|
|
30
|
+
* Percent is the most saros can honestly compare — opencode-go's
|
|
31
|
+
* `/zen/go/v1/usage` publishes only percentages
|
|
32
|
+
* (`{"usage":{"rolling":{"percent":1},"weekly":{"percent":15},
|
|
33
|
+
* "monthly":{"percent":19}}}`), with no token counts or absolute caps, while
|
|
34
|
+
* CommandCode publishes `used`/`cap` per window plus the remaining monthly
|
|
35
|
+
* credits, and its parser converts them to the same percent scale.
|
|
36
|
+
*/
|
|
16
37
|
export interface KeyUsage {
|
|
17
|
-
/**
|
|
18
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Monthly used percent, 0-100, of this provider's own budget; null when the
|
|
40
|
+
* payload has no derivable monthly metric (the previous value must survive).
|
|
41
|
+
*/
|
|
42
|
+
usedPercent: number | null;
|
|
43
|
+
/** True when any window is at 100% — rotation weighs the key 0. */
|
|
44
|
+
exhausted: boolean;
|
|
45
|
+
/** False when the payload carries no monthly percentage (CLI shows n/a). */
|
|
46
|
+
monthlyAvailable: boolean;
|
|
19
47
|
rollingPercent: number;
|
|
20
48
|
weeklyPercent: number;
|
|
21
49
|
monthlyPercent: number;
|
|
@@ -43,15 +71,17 @@ export interface UsageFetchResult {
|
|
|
43
71
|
* problem, not an account one, and must not match.
|
|
44
72
|
*/
|
|
45
73
|
export declare function isEntitlementError(status: number, body: string): boolean;
|
|
46
|
-
/** Parse a usage API response body into KeyUsage (throws on contract violation). */
|
|
47
|
-
export declare function parseUsageResponse(body: string): KeyUsage;
|
|
48
74
|
/**
|
|
49
|
-
* Fetch current usage for one key
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
75
|
+
* Fetch current usage for one key from its provider's endpoint. The provider
|
|
76
|
+
* supplies the path, the identity headers (CommandCode and opencode-go both
|
|
77
|
+
* reject requests without a CLI User-Agent) and the response parser; this
|
|
78
|
+
* function only adds the bearer credential and the timeout.
|
|
79
|
+
*
|
|
80
|
+
* Failures (network, timeout, non-200, contract violation) yield
|
|
81
|
+
* `usage: null` — callers treat that as "no fresh data". A 403 entitlement
|
|
82
|
+
* rejection is reported separately because it is definitive, not transient.
|
|
53
83
|
*/
|
|
54
|
-
export declare function fetchKeyUsage(upstreamBaseUrl: string, apiKey: string,
|
|
84
|
+
export declare function fetchKeyUsage(upstreamBaseUrl: string, apiKey: string, provider?: KeyProvider): Promise<UsageFetchResult>;
|
|
55
85
|
/** Test seam: reset coordinator state. */
|
|
56
86
|
export declare function resetUsageRefreshState(): void;
|
|
57
87
|
/**
|
package/dist/usage-client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* usage-client.ts — Per-key quota usage from
|
|
2
|
+
* usage-client.ts — Per-key quota usage from each provider's usage API.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Every provider owns its endpoint path, response parser and identity
|
|
5
|
+
* headers (opencode-go: `/zen/go/v1/usage` behind Cloudflare, which needs the
|
|
6
|
+
* CLI User-Agent; commandcode: `/alpha/billing/credits`, gated on the same
|
|
7
|
+
* CLI identity). `fetchKeyUsage` only adds the bearer credential.
|
|
7
8
|
*
|
|
8
9
|
* Refresh strategy: stale-while-revalidate on demand. `maybeRefreshUsage`
|
|
9
10
|
* is called on the request path; when data is older than USAGE_REFRESH_TTL_MS
|
|
@@ -12,9 +13,9 @@
|
|
|
12
13
|
* with slightly stale percentages).
|
|
13
14
|
*/
|
|
14
15
|
import { logger } from './logger.js';
|
|
15
|
-
import { USAGE_FETCH_TIMEOUT_MS,
|
|
16
|
-
import { updateKeyUsage, setKeyEntitlement } from './proxy-logic.js';
|
|
17
|
-
import { getProvider, inferProvider } from './providers/index.js';
|
|
16
|
+
import { USAGE_FETCH_TIMEOUT_MS, USAGE_REFRESH_TTL_MS } from './constants.js';
|
|
17
|
+
import { updateKeyUsage, setKeyEntitlement, } from './proxy-logic.js';
|
|
18
|
+
import { getProvider, inferProvider, opencodeGoProvider } from './providers/index.js';
|
|
18
19
|
/**
|
|
19
20
|
* 403 bodies that mean "this account has no plan for this provider":
|
|
20
21
|
* opencode-go → `{"error":{"type":"EntitlementError","message":"OpenCode Go
|
|
@@ -25,50 +26,29 @@ import { getProvider, inferProvider } from './providers/index.js';
|
|
|
25
26
|
export function isEntitlementError(status, body) {
|
|
26
27
|
return status === 403 && /subscription|entitlement|upgrade_required/i.test(body);
|
|
27
28
|
}
|
|
28
|
-
function parsePercent(window) {
|
|
29
|
-
const percent = window?.percent;
|
|
30
|
-
if (typeof percent !== 'number' || !Number.isFinite(percent))
|
|
31
|
-
return 0;
|
|
32
|
-
return Math.min(100, Math.max(0, percent));
|
|
33
|
-
}
|
|
34
|
-
/** Parse a usage API response body into KeyUsage (throws on contract violation). */
|
|
35
|
-
export function parseUsageResponse(body) {
|
|
36
|
-
let parsed;
|
|
37
|
-
try {
|
|
38
|
-
parsed = JSON.parse(body);
|
|
39
|
-
}
|
|
40
|
-
catch {
|
|
41
|
-
throw new Error('usage response is not valid JSON');
|
|
42
|
-
}
|
|
43
|
-
const usage = parsed?.usage;
|
|
44
|
-
if (!usage || typeof usage !== 'object') {
|
|
45
|
-
throw new Error('usage response missing "usage" object');
|
|
46
|
-
}
|
|
47
|
-
const rolling = parsePercent(usage.rolling);
|
|
48
|
-
const weekly = parsePercent(usage.weekly);
|
|
49
|
-
const monthly = parsePercent(usage.monthly);
|
|
50
|
-
return {
|
|
51
|
-
rollingPercent: rolling,
|
|
52
|
-
weeklyPercent: weekly,
|
|
53
|
-
monthlyPercent: monthly,
|
|
54
|
-
usedPercent: Math.max(rolling, weekly, monthly),
|
|
55
|
-
rollingResetsAt: typeof usage.rolling?.resetsAt === 'string' ? usage.rolling.resetsAt : undefined,
|
|
56
|
-
weeklyResetsAt: typeof usage.weekly?.resetsAt === 'string' ? usage.weekly.resetsAt : undefined,
|
|
57
|
-
monthlyResetsAt: typeof usage.monthly?.resetsAt === 'string' ? usage.monthly.resetsAt : undefined,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
29
|
/**
|
|
61
|
-
* Fetch current usage for one key
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
30
|
+
* Fetch current usage for one key from its provider's endpoint. The provider
|
|
31
|
+
* supplies the path, the identity headers (CommandCode and opencode-go both
|
|
32
|
+
* reject requests without a CLI User-Agent) and the response parser; this
|
|
33
|
+
* function only adds the bearer credential and the timeout.
|
|
34
|
+
*
|
|
35
|
+
* Failures (network, timeout, non-200, contract violation) yield
|
|
36
|
+
* `usage: null` — callers treat that as "no fresh data". A 403 entitlement
|
|
37
|
+
* rejection is reported separately because it is definitive, not transient.
|
|
65
38
|
*/
|
|
66
|
-
export async function fetchKeyUsage(upstreamBaseUrl, apiKey,
|
|
39
|
+
export async function fetchKeyUsage(upstreamBaseUrl, apiKey, provider = opencodeGoProvider) {
|
|
40
|
+
const usagePath = provider.usagePath();
|
|
41
|
+
if (!usagePath)
|
|
42
|
+
return { usage: null, noEntitlement: false };
|
|
67
43
|
const controller = new AbortController();
|
|
68
44
|
const timer = setTimeout(() => controller.abort(), USAGE_FETCH_TIMEOUT_MS);
|
|
69
45
|
try {
|
|
70
46
|
const response = await fetch(new URL(usagePath, upstreamBaseUrl), {
|
|
71
|
-
headers: {
|
|
47
|
+
headers: {
|
|
48
|
+
...provider.extraUpstreamHeaders?.(),
|
|
49
|
+
Authorization: `Bearer ${apiKey}`,
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
},
|
|
72
52
|
signal: controller.signal,
|
|
73
53
|
});
|
|
74
54
|
// Keep the abort armed through the body read: a stalled body after a 200
|
|
@@ -79,7 +59,7 @@ export async function fetchKeyUsage(upstreamBaseUrl, apiKey, usagePath = USAGE_P
|
|
|
79
59
|
return { usage: null, noEntitlement: isEntitlementError(response.status, body) };
|
|
80
60
|
}
|
|
81
61
|
try {
|
|
82
|
-
return { usage: parseUsageResponse(body), noEntitlement: false };
|
|
62
|
+
return { usage: provider.parseUsageResponse(body), noEntitlement: false };
|
|
83
63
|
}
|
|
84
64
|
catch {
|
|
85
65
|
return { usage: null, noEntitlement: false };
|
|
@@ -117,20 +97,23 @@ export function maybeRefreshUsage(state, config) {
|
|
|
117
97
|
try {
|
|
118
98
|
const entries = await Promise.all(state.keys.map(async (k) => {
|
|
119
99
|
const provider = getProvider(inferProvider({ label: k.label, key: k.key, provider: k.provider }));
|
|
120
|
-
// Providers without a
|
|
121
|
-
//
|
|
100
|
+
// Providers without a usage endpoint are skipped — weighted
|
|
101
|
+
// rotation simply treats them as having no fresh data.
|
|
122
102
|
if (!provider || !provider.usagePath())
|
|
123
103
|
return [k.label, null];
|
|
124
104
|
const base = config.upstreams?.[provider.id] ?? (provider.id === 'opencode-go' ? config.upstreamBaseUrl : provider.baseUrl);
|
|
125
|
-
const usage = await fetchKeyUsage(base, k.key, provider
|
|
105
|
+
const usage = await fetchKeyUsage(base, k.key, provider);
|
|
126
106
|
return [k.label, usage];
|
|
127
107
|
}));
|
|
128
|
-
const
|
|
108
|
+
const metrics = new Map();
|
|
129
109
|
for (const [label, result] of entries) {
|
|
130
110
|
if (!result)
|
|
131
111
|
continue;
|
|
132
112
|
if (result.usage) {
|
|
133
|
-
|
|
113
|
+
metrics.set(label, {
|
|
114
|
+
usedPercent: result.usage.usedPercent,
|
|
115
|
+
exhausted: result.usage.exhausted,
|
|
116
|
+
});
|
|
134
117
|
// A key that answers the usage API is entitled again.
|
|
135
118
|
setKeyEntitlement(state, label, true);
|
|
136
119
|
}
|
|
@@ -139,9 +122,9 @@ export function maybeRefreshUsage(state, config) {
|
|
|
139
122
|
}
|
|
140
123
|
// Transient failures leave the previous entitlement untouched.
|
|
141
124
|
}
|
|
142
|
-
if (
|
|
143
|
-
updateKeyUsage(state,
|
|
144
|
-
logger.debug({ keys: [...
|
|
125
|
+
if (metrics.size > 0) {
|
|
126
|
+
updateKeyUsage(state, metrics);
|
|
127
|
+
logger.debug({ keys: [...metrics.keys()] }, 'key usage refreshed');
|
|
145
128
|
}
|
|
146
129
|
}
|
|
147
130
|
catch (err) {
|
package/dist/usage-client.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"usage-client.js","sourceRoot":"","sources":["../src/usage-client.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"usage-client.js","sourceRoot":"","sources":["../src/usage-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "saros-proxy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Saros — predictive multi-account proxy for the OpenCode-Go API. Cycles between accounts before they exhaust.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
-
"models.json",
|
|
12
|
+
"assets/models.json",
|
|
13
13
|
"README.md",
|
|
14
14
|
"LICENSE"
|
|
15
15
|
],
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"setup": "tsx src/cli/setup.ts",
|
|
18
18
|
"dev": "tsx src/index.ts serve",
|
|
19
19
|
"build": "tsc",
|
|
20
|
-
"lint": "tsc --noEmit",
|
|
20
|
+
"lint": "tsc --noEmit && tsc --noEmit -p tsconfig.scripts.json",
|
|
21
21
|
"prepublishOnly": "npm run build",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"coverage": "vitest run --coverage",
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"docker:build": "docker compose build proxy",
|
|
31
31
|
"docker:run": "docker compose up proxy",
|
|
32
32
|
"docker:test": "docker compose run --rm test",
|
|
33
|
-
"test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts"
|
|
33
|
+
"test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts",
|
|
34
|
+
"models:refresh": "tsx scripts/refresh-models.ts"
|
|
34
35
|
},
|
|
35
36
|
"keywords": [
|
|
36
37
|
"opencode",
|