saros-proxy 0.7.4 → 0.9.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 +74 -44
- package/dist/cli/daemon.js +4 -0
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/dsh-config.d.ts +32 -0
- package/dist/cli/dsh-config.js +122 -0
- package/dist/cli/dsh-config.js.map +1 -0
- package/dist/cli/harness-models.d.ts +26 -2
- package/dist/cli/harness-models.js +192 -2
- package/dist/cli/harness-models.js.map +1 -1
- package/dist/cli/harness-sync.d.ts +3 -6
- package/dist/cli/harness-sync.js +14 -7
- package/dist/cli/harness-sync.js.map +1 -1
- package/dist/cli/help.js +2 -2
- package/dist/cli/omp-config.js +11 -1
- package/dist/cli/omp-config.js.map +1 -1
- package/dist/cli/overview.js +2 -1
- package/dist/cli/overview.js.map +1 -1
- package/dist/cli/pi-config.js +11 -1
- package/dist/cli/pi-config.js.map +1 -1
- package/dist/cli/setup.d.ts +6 -10
- package/dist/cli/setup.js +100 -232
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/ui.d.ts +0 -1
- package/dist/cli/ui.js +0 -16
- package/dist/cli/ui.js.map +1 -1
- package/dist/cli/usage.d.ts +12 -0
- package/dist/cli/usage.js +116 -0
- package/dist/cli/usage.js.map +1 -0
- package/dist/config.d.ts +13 -17
- package/dist/config.js +29 -78
- package/dist/config.js.map +1 -1
- package/dist/constants.d.ts +6 -5
- package/dist/constants.js +10 -5
- package/dist/constants.js.map +1 -1
- package/dist/index.js +7 -25
- package/dist/index.js.map +1 -1
- package/dist/model-stats.d.ts +20 -0
- package/dist/model-stats.js +135 -0
- package/dist/model-stats.js.map +1 -0
- package/dist/providers/commandcode.d.ts +17 -0
- package/dist/providers/commandcode.js +232 -0
- package/dist/providers/commandcode.js.map +1 -0
- package/dist/providers/index.d.ts +43 -0
- package/dist/providers/index.js +75 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/infer.d.ts +23 -0
- package/dist/providers/infer.js +13 -0
- package/dist/providers/infer.js.map +1 -0
- package/dist/providers/opencode-go.d.ts +4 -0
- package/dist/providers/opencode-go.js +179 -0
- package/dist/providers/opencode-go.js.map +1 -0
- package/dist/providers/types.d.ts +118 -0
- package/dist/providers/types.js +10 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/proxy-logic.d.ts +46 -22
- package/dist/proxy-logic.js +146 -106
- package/dist/proxy-logic.js.map +1 -1
- package/dist/proxy.d.ts +20 -2
- package/dist/proxy.js +231 -57
- package/dist/proxy.js.map +1 -1
- package/dist/usage-client.d.ts +41 -0
- package/dist/usage-client.js +124 -0
- package/dist/usage-client.js.map +1 -0
- package/dist/validation.d.ts +5 -5
- package/dist/validation.js +7 -9
- package/dist/validation.js.map +1 -1
- package/package.json +6 -2
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infer the provider for a key entry.
|
|
3
|
+
* Explicit `provider` field wins; 'user_' keys are commandcode; everything
|
|
4
|
+
* else plausible defaults to opencode-go (back-compat with every existing
|
|
5
|
+
* config.yaml).
|
|
6
|
+
*/
|
|
7
|
+
export function inferProvider(entry) {
|
|
8
|
+
if (entry.provider === 'commandcode' || entry.provider === 'opencode-go') {
|
|
9
|
+
return entry.provider;
|
|
10
|
+
}
|
|
11
|
+
return entry.key.startsWith('user_') ? 'commandcode' : 'opencode-go';
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=infer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"infer.js","sourceRoot":"","sources":["../../src/providers/infer.ts"],"names":[],"mappings":"AAkBA;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,KAAe;IAC3C,IAAI,KAAK,CAAC,QAAQ,KAAK,aAAa,IAAI,KAAK,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QACzE,OAAO,KAAK,CAAC,QAAQ,CAAC;IACxB,CAAC;IACD,OAAO,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,aAAa,CAAC;AACvE,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opencode-go.ts — KeyProvider adapter for the OpenCode-Go API (opencode.ai).
|
|
3
|
+
*
|
|
4
|
+
* Keys: `sk-` prefix (shared with commandcode → 'maybe'), ≥ 20 chars.
|
|
5
|
+
* Usage: GET /zen/go/v1/usage with Bearer = the key itself.
|
|
6
|
+
*/
|
|
7
|
+
import { API_KEY_PREFIX, MIN_KEY_LENGTH, USAGE_PATH, } from '../constants.js';
|
|
8
|
+
/** Bare-name index of the live opencode-go catalog. */
|
|
9
|
+
let catalogCache = null;
|
|
10
|
+
/** Ensures the background refresh fires at most once per process. */
|
|
11
|
+
let refreshStarted = false;
|
|
12
|
+
/**
|
|
13
|
+
* Fetch the public opencode-go model list and index it. The endpoint needs
|
|
14
|
+
* no auth (verified live); failures keep the previous cache.
|
|
15
|
+
*/
|
|
16
|
+
async function refreshCatalog(baseUrl, timeoutMs) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${baseUrl.replace(/\/$/, '')}${USAGE_PATH.replace(/\/usage$/, '/models')}`, {
|
|
21
|
+
signal: controller.signal,
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok)
|
|
24
|
+
return false;
|
|
25
|
+
const body = (await res.json());
|
|
26
|
+
if (!Array.isArray(body.data))
|
|
27
|
+
return false;
|
|
28
|
+
const bareNames = new Set();
|
|
29
|
+
for (const entry of body.data) {
|
|
30
|
+
if (!entry || typeof entry !== 'object')
|
|
31
|
+
continue;
|
|
32
|
+
const id = entry.id;
|
|
33
|
+
if (typeof id !== 'string' || !id)
|
|
34
|
+
continue;
|
|
35
|
+
bareNames.add(id.toLowerCase());
|
|
36
|
+
const last = id.split('/').pop() ?? id;
|
|
37
|
+
if (last)
|
|
38
|
+
bareNames.add(last.toLowerCase());
|
|
39
|
+
}
|
|
40
|
+
catalogCache = { bareNames };
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function parsePercent(window) {
|
|
51
|
+
const percent = window?.percent;
|
|
52
|
+
if (typeof percent !== 'number' || !Number.isFinite(percent))
|
|
53
|
+
return 0;
|
|
54
|
+
return Math.min(100, Math.max(0, percent));
|
|
55
|
+
}
|
|
56
|
+
/** Parse opencode-go usage JSON (moved from usage-client.ts). */
|
|
57
|
+
export function parseOpencodeUsage(body) {
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
parsed = JSON.parse(body);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const usage = parsed?.usage;
|
|
66
|
+
if (!usage || typeof usage !== 'object')
|
|
67
|
+
return null;
|
|
68
|
+
const rolling = parsePercent(usage.rolling);
|
|
69
|
+
const weekly = parsePercent(usage.weekly);
|
|
70
|
+
const monthly = parsePercent(usage.monthly);
|
|
71
|
+
return {
|
|
72
|
+
rollingPercent: rolling,
|
|
73
|
+
weeklyPercent: weekly,
|
|
74
|
+
monthlyPercent: monthly,
|
|
75
|
+
usedPercent: Math.max(rolling, weekly, monthly),
|
|
76
|
+
rollingResetsAt: typeof usage.rolling?.resetsAt === 'string' ? usage.rolling.resetsAt : undefined,
|
|
77
|
+
weeklyResetsAt: typeof usage.weekly?.resetsAt === 'string' ? usage.weekly.resetsAt : undefined,
|
|
78
|
+
monthlyResetsAt: typeof usage.monthly?.resetsAt === 'string' ? usage.monthly.resetsAt : undefined,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
export const opencodeGoProvider = {
|
|
82
|
+
id: 'opencode-go',
|
|
83
|
+
displayName: 'OpenCode Go',
|
|
84
|
+
baseUrl: 'https://opencode.ai',
|
|
85
|
+
chatBasePath: '/zen/go/v1',
|
|
86
|
+
quickMatch(key) {
|
|
87
|
+
// sk- is shared with commandcode → never a definitive 'yes'.
|
|
88
|
+
return key.startsWith(API_KEY_PREFIX) && key.length >= MIN_KEY_LENGTH
|
|
89
|
+
? 'maybe'
|
|
90
|
+
: 'no';
|
|
91
|
+
},
|
|
92
|
+
async smokeTest(key, timeoutMs = 10_000) {
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${this.baseUrl}${USAGE_PATH}`, {
|
|
97
|
+
headers: { Authorization: `Bearer ${key}` },
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
if (res.ok) {
|
|
101
|
+
return { provider: this.id, status: 'valid', httpStatus: res.status };
|
|
102
|
+
}
|
|
103
|
+
// 401/403 = definitive rejection; 5xx/429 = inconclusive
|
|
104
|
+
const definitive = res.status === 401 || res.status === 403;
|
|
105
|
+
return {
|
|
106
|
+
provider: this.id,
|
|
107
|
+
status: definitive ? 'invalid' : 'error',
|
|
108
|
+
httpStatus: res.status,
|
|
109
|
+
detail: definitive ? 'upstream rejected the key' : 'upstream unavailable',
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return {
|
|
114
|
+
provider: this.id,
|
|
115
|
+
status: 'error',
|
|
116
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
usagePath() {
|
|
124
|
+
return USAGE_PATH;
|
|
125
|
+
},
|
|
126
|
+
parseUsageResponse(body) {
|
|
127
|
+
return parseOpencodeUsage(body);
|
|
128
|
+
},
|
|
129
|
+
/**
|
|
130
|
+
* Affinity via the live catalog when loaded; 'maybe' before the first
|
|
131
|
+
* fetch resolves (non-committal, keeps the key as rotation fallback).
|
|
132
|
+
*/
|
|
133
|
+
modelAffinity(modelId) {
|
|
134
|
+
if (!catalogCache) {
|
|
135
|
+
if (!refreshStarted) {
|
|
136
|
+
refreshStarted = true;
|
|
137
|
+
void refreshCatalog(this.baseUrl, 5_000);
|
|
138
|
+
}
|
|
139
|
+
return 'maybe';
|
|
140
|
+
}
|
|
141
|
+
const bare = (modelId.split('/').pop() ?? modelId).toLowerCase();
|
|
142
|
+
return catalogCache.bareNames.has(bare) ? 'yes' : 'no';
|
|
143
|
+
},
|
|
144
|
+
/** Same verdict as modelAffinity — the catalog index is already bare-name based. */
|
|
145
|
+
modelAffinityByName(bareName) {
|
|
146
|
+
if (!catalogCache) {
|
|
147
|
+
if (!refreshStarted) {
|
|
148
|
+
refreshStarted = true;
|
|
149
|
+
void refreshCatalog(this.baseUrl, 5_000);
|
|
150
|
+
}
|
|
151
|
+
return 'maybe';
|
|
152
|
+
}
|
|
153
|
+
return catalogCache.bareNames.has(bareName.toLowerCase()) ? 'yes' : 'no';
|
|
154
|
+
},
|
|
155
|
+
/** opencode-go ids are already bare (1:1 naming) — the input passes through. */
|
|
156
|
+
resolveNativeId(bareName) {
|
|
157
|
+
return catalogCache?.bareNames.has(bareName.toLowerCase()) ? bareName : null;
|
|
158
|
+
},
|
|
159
|
+
/** Public catalog endpoint (no auth required, verified live). */
|
|
160
|
+
async fetchCatalog(timeoutMs = 5_000) {
|
|
161
|
+
const ok = await refreshCatalog(this.baseUrl, timeoutMs);
|
|
162
|
+
if (!ok)
|
|
163
|
+
return null;
|
|
164
|
+
// Re-fetch and return the raw list for catalog consumers (harness sync).
|
|
165
|
+
try {
|
|
166
|
+
const res = await fetch(`${this.baseUrl.replace(/\/$/, '')}${USAGE_PATH.replace(/\/usage$/, '/models')}`);
|
|
167
|
+
if (!res.ok)
|
|
168
|
+
return null;
|
|
169
|
+
const body = (await res.json());
|
|
170
|
+
return Array.isArray(body.data)
|
|
171
|
+
? body.data
|
|
172
|
+
: null;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
//# sourceMappingURL=opencode-go.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"opencode-go.js","sourceRoot":"","sources":["../../src/providers/opencode-go.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Provider abstraction for multi-provider API key support.
|
|
3
|
+
*
|
|
4
|
+
* A KeyProvider encapsulates everything saros needs to know about one
|
|
5
|
+
* upstream API platform: how to recognize its keys structurally, how to
|
|
6
|
+
* verify them with a cheap network ping (smoke test), where its usage
|
|
7
|
+
* endpoint lives, and how to parse its usage response shape.
|
|
8
|
+
*/
|
|
9
|
+
export type ProviderId = 'opencode-go' | 'commandcode';
|
|
10
|
+
/** Result of a provider smoke test (key verification ping). */
|
|
11
|
+
export interface SmokeResult {
|
|
12
|
+
/** Provider that ran the test. */
|
|
13
|
+
provider: ProviderId;
|
|
14
|
+
/**
|
|
15
|
+
* - 'valid' — key authenticated successfully (HTTP 200)
|
|
16
|
+
* - 'invalid' — upstream definitively rejected the key (401/403 auth)
|
|
17
|
+
* - 'error' — network/timeout/5xx; inconclusive, may retry
|
|
18
|
+
*/
|
|
19
|
+
status: 'valid' | 'invalid' | 'error';
|
|
20
|
+
/** HTTP status code when available. */
|
|
21
|
+
httpStatus?: number;
|
|
22
|
+
/** Human-readable detail (safe to show in the TUI — never the key). */
|
|
23
|
+
detail?: string;
|
|
24
|
+
/** Provider-specific extras (e.g. commandcode planId). */
|
|
25
|
+
meta?: Record<string, string>;
|
|
26
|
+
}
|
|
27
|
+
/** Structural prefix check result. */
|
|
28
|
+
export type PrefixMatch = 'yes' | 'no' | 'maybe';
|
|
29
|
+
/**
|
|
30
|
+
* Common interface every key provider implements.
|
|
31
|
+
* All network methods must never log or echo the key itself.
|
|
32
|
+
*/
|
|
33
|
+
export interface KeyProvider {
|
|
34
|
+
readonly id: ProviderId;
|
|
35
|
+
/** Human-readable name for TUI output. */
|
|
36
|
+
readonly displayName: string;
|
|
37
|
+
/** Upstream base URL (HTTPS enforced by callers). */
|
|
38
|
+
readonly baseUrl: string;
|
|
39
|
+
/**
|
|
40
|
+
* Canonical chat route prefix saros accepts from clients. Clients always
|
|
41
|
+
* speak opencode-go shapes (/zen/go/v1/...); providers without a matching
|
|
42
|
+
* upstream path get their requests remapped (see remapChatPath).
|
|
43
|
+
*/
|
|
44
|
+
readonly chatBasePath: string;
|
|
45
|
+
/**
|
|
46
|
+
* Cheap structural check. 'yes' = prefix uniquely identifies this
|
|
47
|
+
* provider, 'maybe' = prefix shared with another provider (empirical
|
|
48
|
+
* smoke test required), 'no' = definitely not this provider's format.
|
|
49
|
+
*/
|
|
50
|
+
quickMatch(key: string): PrefixMatch;
|
|
51
|
+
/**
|
|
52
|
+
* Verify a key with a cheap authenticated GET.
|
|
53
|
+
* Implementations must use a read-only endpoint (no token spend).
|
|
54
|
+
*/
|
|
55
|
+
smokeTest(key: string, timeoutMs?: number): Promise<SmokeResult>;
|
|
56
|
+
/**
|
|
57
|
+
* Usage/quota endpoint path for this provider (empty string when the
|
|
58
|
+
* provider has no per-key usage API saros can query).
|
|
59
|
+
*/
|
|
60
|
+
usagePath(): string;
|
|
61
|
+
/**
|
|
62
|
+
* Parse a usage response body into the shared KeyUsage shape.
|
|
63
|
+
* Returns null when the body doesn't match this provider's contract.
|
|
64
|
+
*/
|
|
65
|
+
parseUsageResponse(body: string): KeyUsage | null;
|
|
66
|
+
/**
|
|
67
|
+
* Extra headers this provider requires on upstream requests
|
|
68
|
+
* (e.g. CommandCode CLI-identity headers). Called per request;
|
|
69
|
+
* must not include Authorization (added by the proxy).
|
|
70
|
+
*/
|
|
71
|
+
extraUpstreamHeaders?(): Record<string, string>;
|
|
72
|
+
/**
|
|
73
|
+
* Fetch this provider's live model catalog (OpenAI list shape).
|
|
74
|
+
* Returns null when unavailable — callers fall back to bundled data.
|
|
75
|
+
* Implementations must not require a valid key (catalogs are public).
|
|
76
|
+
*/
|
|
77
|
+
fetchCatalog?(timeoutMs?: number): Promise<Array<Record<string, unknown>> | null>;
|
|
78
|
+
/**
|
|
79
|
+
* Structural model-id affinity check. Consulted BEFORE dispatching so a
|
|
80
|
+
* request naming a commandcode-only model (e.g. 'claude-*') is not sent
|
|
81
|
+
* with an opencode-go key (and vice versa).
|
|
82
|
+
*/
|
|
83
|
+
modelAffinity(modelId: string): ModelAffinity;
|
|
84
|
+
/**
|
|
85
|
+
* Affinity by bare model name (without vendor prefix), e.g. 'ds-v4-flash'
|
|
86
|
+
* for 'deepseek/ds-v4-flash'. Providers whose catalogs use vendor-prefixed
|
|
87
|
+
* ids match the bare name against their catalog's last path segment;
|
|
88
|
+
* others return the same verdict as modelAffinity(id).
|
|
89
|
+
* Enables cross-provider aliasing so shared models rotate across all keys.
|
|
90
|
+
*/
|
|
91
|
+
modelAffinityByName?(bareName: string): ModelAffinity;
|
|
92
|
+
/**
|
|
93
|
+
* Map a bare model name to this provider's native catalog id
|
|
94
|
+
* ('deepseek-v4-flash' → 'deepseek/deepseek-v4-flash'), or null when the
|
|
95
|
+
* catalog doesn't serve it. Backed by the provider's live catalog;
|
|
96
|
+
* providers with 1:1 naming may return the input unchanged.
|
|
97
|
+
*/
|
|
98
|
+
resolveNativeId?(bareName: string): string | null;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Model → provider affinity.
|
|
102
|
+
*
|
|
103
|
+
* 'yes' = the model id is (almost certainly) served by this provider,
|
|
104
|
+
* 'no' = definitely not, 'maybe' = cannot be decided structurally — the
|
|
105
|
+
* proxy tries matching providers first and falls back to any available key.
|
|
106
|
+
* Used for request routing in mixed-provider key pools.
|
|
107
|
+
*/
|
|
108
|
+
export type ModelAffinity = 'yes' | 'no' | 'maybe';
|
|
109
|
+
/** Shared usage shape used across providers (mirrors usage-client KeyUsage). */
|
|
110
|
+
export interface KeyUsage {
|
|
111
|
+
usedPercent: number;
|
|
112
|
+
rollingPercent: number;
|
|
113
|
+
weeklyPercent: number;
|
|
114
|
+
monthlyPercent: number;
|
|
115
|
+
rollingResetsAt?: string;
|
|
116
|
+
weeklyResetsAt?: string;
|
|
117
|
+
monthlyResetsAt?: string;
|
|
118
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.ts — Provider abstraction for multi-provider API key support.
|
|
3
|
+
*
|
|
4
|
+
* A KeyProvider encapsulates everything saros needs to know about one
|
|
5
|
+
* upstream API platform: how to recognize its keys structurally, how to
|
|
6
|
+
* verify them with a cheap network ping (smoke test), where its usage
|
|
7
|
+
* endpoint lives, and how to parse its usage response shape.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/providers/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG"}
|
package/dist/proxy-logic.d.ts
CHANGED
|
@@ -12,21 +12,37 @@
|
|
|
12
12
|
interface ApiKey {
|
|
13
13
|
label: string;
|
|
14
14
|
key: string;
|
|
15
|
+
/** Provider id ('opencode-go' | 'commandcode' | future extensions). */
|
|
16
|
+
provider: string;
|
|
15
17
|
enabled: boolean;
|
|
16
18
|
consecutiveFailures: number;
|
|
17
19
|
lastUsed: number | null;
|
|
18
20
|
disabledAt: number | null;
|
|
21
|
+
/** Worst-window used percent (0-100) reported by the upstream usage API. */
|
|
22
|
+
usagePercent: number | null;
|
|
23
|
+
/** Smooth weighted round-robin state (nginx-style). */
|
|
24
|
+
currentWeight: number;
|
|
19
25
|
}
|
|
20
26
|
/** Read-only projection returned to callers — cannot mutate internal state. */
|
|
21
27
|
export interface KeySnapshot {
|
|
22
28
|
label: string;
|
|
23
29
|
key: string;
|
|
30
|
+
/** Provider this key belongs to (set at state creation from config). */
|
|
31
|
+
provider: string;
|
|
24
32
|
}
|
|
25
33
|
/** Tracks a single HTTP request's key usage through its lifecycle. */
|
|
26
34
|
interface RequestContext {
|
|
27
35
|
requestId: string;
|
|
28
36
|
triedKeys: string[];
|
|
29
37
|
currentKey: KeySnapshot | null;
|
|
38
|
+
/**
|
|
39
|
+
* Provider filter fixed at selection time (model affinity / @suffix pin).
|
|
40
|
+
* Failover re-applies it so a pinned commandcode request never fails over
|
|
41
|
+
* to an opencode-go key that cannot serve the (already rewritten) model id.
|
|
42
|
+
*/
|
|
43
|
+
providerFilter?: (provider: string) => boolean;
|
|
44
|
+
/** True when the model is provider-pinned (@suffix) — tier-3 cross-provider fallback disabled. */
|
|
45
|
+
pinned?: boolean;
|
|
30
46
|
}
|
|
31
47
|
export interface ProxyState {
|
|
32
48
|
keys: ApiKey[];
|
|
@@ -34,51 +50,56 @@ export interface ProxyState {
|
|
|
34
50
|
circuitBreakerThreshold: number;
|
|
35
51
|
circuitBreakerCooldownMs: number;
|
|
36
52
|
activeRequests: Map<string, RequestContext>;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
rolling: number | null;
|
|
44
|
-
weekly: number | null;
|
|
45
|
-
monthly: number | null;
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Optional parameters for usage-gated key selection.
|
|
49
|
-
*/
|
|
50
|
-
export interface KeySelectionOptions {
|
|
51
|
-
usageMap?: Map<string, UsageInfo>;
|
|
52
|
-
usageThreshold?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Optional model→provider affinity resolver: (providerId, modelId) →
|
|
55
|
+
* 'yes' | 'no' | 'maybe'. Wired from the provider registry at app setup;
|
|
56
|
+
* absence disables affinity routing (single-provider behavior).
|
|
57
|
+
*/
|
|
58
|
+
affinityResolver?: (provider: string, modelId: string) => 'yes' | 'no' | 'maybe';
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Error classification for HTTP responses.
|
|
56
|
-
* - KeyFault: the API key should be penalised (invalid, revoked,
|
|
62
|
+
* - KeyFault: the API key should be penalised (invalid, revoked, quota exhausted)
|
|
57
63
|
* - RequestFault: the request itself is bad — no key penalisation
|
|
58
64
|
* - ServerFault: transient server issue — key penalised (normal circuit-breaker)
|
|
65
|
+
* - NetworkFault: timeout / connection error — no key penalisation. The
|
|
66
|
+
* upstream base URL is shared by every key, so network-level failures are
|
|
67
|
+
* not a per-key fault; disabling keys for them caused total-blackout
|
|
68
|
+
* lockups ("all keys unavailable") when the upstream hung. Failover still
|
|
69
|
+
* tries the next key for this request.
|
|
59
70
|
*/
|
|
60
|
-
export type ErrorType = 'KeyFault' | 'RequestFault' | 'ServerFault';
|
|
71
|
+
export type ErrorType = 'KeyFault' | 'RequestFault' | 'ServerFault' | 'NetworkFault';
|
|
61
72
|
export declare function createProxyState(keys: Array<{
|
|
62
73
|
label: string;
|
|
63
74
|
key: string;
|
|
75
|
+
provider?: string;
|
|
64
76
|
}>, options?: {
|
|
65
77
|
circuitBreakerThreshold?: number;
|
|
66
78
|
circuitBreakerCooldownMs?: number;
|
|
67
79
|
}): ProxyState;
|
|
80
|
+
/**
|
|
81
|
+
* Update per-key usage data (worst-window used percent, 0-100).
|
|
82
|
+
* Keys absent from the map keep their previous value.
|
|
83
|
+
*/
|
|
84
|
+
export declare function updateKeyUsage(state: ProxyState, usage: Map<string, number>): void;
|
|
68
85
|
/**
|
|
69
86
|
* Obtain a key for a new (or in-flight) request.
|
|
70
87
|
* Creates a RequestContext if one does not already exist for this requestId.
|
|
71
88
|
* Skips keys that have already been tried for this request AND keys booked
|
|
72
89
|
* by any other active request.
|
|
90
|
+
*
|
|
91
|
+
* When a modelId is supplied and some providers claim it (modelAffinity
|
|
92
|
+
* 'yes'), keys of other providers are only used after the preferred pool is
|
|
93
|
+
* exhausted (including its booked-share tier).
|
|
73
94
|
*/
|
|
74
|
-
export declare function selectKeyForRequest(state: ProxyState, requestId: string,
|
|
95
|
+
export declare function selectKeyForRequest(state: ProxyState, requestId: string, modelId?: string): KeySnapshot | null;
|
|
75
96
|
/**
|
|
76
97
|
* Failover: record the current key of this request as failed and pick the next
|
|
77
98
|
* available key. The caller should also call markKeyFailed() with the
|
|
78
99
|
* appropriate ErrorType; this function only manages the request-context
|
|
79
100
|
* tracking (adds the current key to triedKeys).
|
|
80
101
|
*/
|
|
81
|
-
export declare function failoverRequest(state: ProxyState, requestId: string
|
|
102
|
+
export declare function failoverRequest(state: ProxyState, requestId: string): KeySnapshot | null;
|
|
82
103
|
/**
|
|
83
104
|
* Complete a request: if successful, mark the current key as succeeded;
|
|
84
105
|
* always clean up the request context.
|
|
@@ -87,7 +108,8 @@ export declare function completeRequest(state: ProxyState, requestId: string, su
|
|
|
87
108
|
/**
|
|
88
109
|
* Mark a key as failed according to the error type (C2).
|
|
89
110
|
* - KeyFault / ServerFault: penalise the key (KeyFault disables immediately)
|
|
90
|
-
* - RequestFault: no penalisation (bad request,
|
|
111
|
+
* - RequestFault / NetworkFault: no penalisation (bad request, or network
|
|
112
|
+
* trouble that is not the key's fault)
|
|
91
113
|
*/
|
|
92
114
|
export declare function markKeyFailed(state: ProxyState, keyLabel: string, errorType: ErrorType): void;
|
|
93
115
|
/**
|
|
@@ -101,7 +123,9 @@ export declare function markKeySucceeded(state: ProxyState, keyLabel: string): v
|
|
|
101
123
|
* 401 → KeyFault (invalid/revoked key — immediate disable)
|
|
102
124
|
* 429 → ServerFault (transient rate-limit — incremental circuit breaker)
|
|
103
125
|
* 500 / 502 / 503 → KeyFault if body mentions quota/balance, else ServerFault
|
|
104
|
-
* 400 / 404 / 422
|
|
126
|
+
* 400 / 403 / 404 / 422 → RequestFault (bad request or permission denial —
|
|
127
|
+
* don't penalise the key; 403 MODEL_NOT_IN_PLAN is a plan
|
|
128
|
+
* restriction, found live on commandcode)
|
|
105
129
|
* everything else → ServerFault
|
|
106
130
|
*/
|
|
107
131
|
export declare function classifyHttpError(status: number, _body?: string): ErrorType;
|