audit-tools 0.33.6 → 0.33.7
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/dist/audit/cli/nextStepCommand.js +3 -3
- package/dist/audit/cli/nextStepCommand.js.map +1 -1
- package/dist/remediate/steps/sessionConfigLoad.js +1 -1
- package/dist/remediate/steps/sessionConfigLoad.js.map +1 -1
- package/dist/shared/dispatch/admissionLoop.d.ts +1 -1
- package/dist/shared/dispatch/admissionLoop.js +1 -1
- package/dist/shared/index.js +1 -1
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/providers/auditorSources.d.ts +39 -44
- package/dist/shared/providers/auditorSources.d.ts.map +1 -1
- package/dist/shared/providers/auditorSources.js +90 -63
- package/dist/shared/providers/auditorSources.js.map +1 -1
- package/dist/shared/providers/claudeWorkerProvider.d.ts +12 -11
- package/dist/shared/providers/claudeWorkerProvider.d.ts.map +1 -1
- package/dist/shared/providers/claudeWorkerProvider.js +23 -16
- package/dist/shared/providers/claudeWorkerProvider.js.map +1 -1
- package/dist/shared/providers/providerConfirmation.d.ts +2 -2
- package/dist/shared/providers/providerConfirmation.js +2 -2
- package/dist/shared/providers/providerConfirmation.js.map +1 -1
- package/dist/shared/providers/proxyCatalog.d.ts +19 -16
- package/dist/shared/providers/proxyCatalog.d.ts.map +1 -1
- package/dist/shared/providers/proxyCatalog.js +272 -181
- package/dist/shared/providers/proxyCatalog.js.map +1 -1
- package/dist/shared/providers/sharedProviderConfirmation.d.ts +1 -1
- package/dist/shared/providers/sharedProviderConfirmation.js +2 -2
- package/dist/shared/providers/sharedProviderConfirmation.js.map +1 -1
- package/dist/shared/quota/apiPool.js +4 -4
- package/dist/shared/quota/apiPool.js.map +1 -1
- package/dist/shared/quota/capacity.d.ts +1 -1
- package/dist/shared/types/providerConfirmation.d.ts +2 -2
- package/dist/shared/types/sessionConfig.d.ts +9 -8
- package/dist/shared/types/sessionConfig.d.ts.map +1 -1
- package/dist/shared/types/sessionConfig.js +1 -1
- package/dist/shared/types/sessionConfig.js.map +1 -1
- package/dist/shared/validation/sessionConfig.js +1 -1
- package/dist/shared/validation/sessionConfig.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,15 +5,15 @@ import { writeJsonFile } from "../io/json.js";
|
|
|
5
5
|
import { resolveAuditCodeStateDir } from "../io/stateDir.js";
|
|
6
6
|
import { validateSessionConfig } from "../validation/sessionConfig.js";
|
|
7
7
|
/**
|
|
8
|
-
* The POPULATE cache for the
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* The POPULATE cache for the proxy lane: discovered via `GET <proxy>/v1/models` +
|
|
9
|
+
* `GET <proxy>/model/info` (OpenAI-compatible surfaces) expanded into ready-to-fold
|
|
10
|
+
* `claude-worker` {@link DispatchableSource}s, written once per populate (Gate-0 build
|
|
11
|
+
* / explicit refresh) and READ by resolve — never fetched mid-resolve.
|
|
12
12
|
*
|
|
13
13
|
* Named `catalog-cache.json`, NOT the `catalog-<auditor-id>.json` the reserved-name
|
|
14
14
|
* comment at `auditorSources.ts` anticipates: populate/resolve run on the AMBIENT
|
|
15
15
|
* path, where no auditor id exists to key on. The cache is machine-level like the
|
|
16
|
-
* declaration beside it — and it is a CACHE of live
|
|
16
|
+
* declaration beside it — and it is a CACHE of live discovery state, not resolved
|
|
17
17
|
* per-auditor capability, so an auditor-id key would assert an isolation the data
|
|
18
18
|
* doesn't have. Per-auditor never-inherit still holds: every resolve re-proves proxy
|
|
19
19
|
* REACH itself; the cache only supplies the expansion.
|
|
@@ -41,34 +41,55 @@ function finiteNonNegative(value) {
|
|
|
41
41
|
: undefined;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
44
|
+
* Edge adapter: proxy `/model/info` response shape → neutral ModelAdvert contract.
|
|
45
|
+
* Maps from proxy-specific field names to a generic contract.
|
|
46
|
+
*
|
|
47
|
+
* Returns:
|
|
48
|
+
* - { advert, filtered: false | undefined } for valid models
|
|
49
|
+
* - { filtered: true } for models filtered by eligibility rules
|
|
50
|
+
* - undefined for invalid data (missing modelName/model_info)
|
|
51
|
+
*/
|
|
52
|
+
function adaptProxyModelInfo(modelInfo) {
|
|
53
|
+
const modelName = modelInfo.model_name;
|
|
54
|
+
if (typeof modelName !== "string" || modelName.trim().length === 0)
|
|
55
|
+
return undefined;
|
|
56
|
+
const info = modelInfo.model_info;
|
|
57
|
+
if (info === null || typeof info !== "object")
|
|
58
|
+
return undefined;
|
|
59
|
+
const infoObj = info;
|
|
60
|
+
const proxyProvider = infoObj.litellm_provider;
|
|
61
|
+
const provider = typeof proxyProvider === "string" ? proxyProvider : undefined;
|
|
62
|
+
const mode = infoObj.mode;
|
|
63
|
+
const supportsToolCalls = infoObj.supports_tool_calls;
|
|
64
|
+
// Check eligibility: mode must be "chat" or absent (unknown ≠ incapable).
|
|
65
|
+
// supports_tool_calls false → skip; absent or true → keep.
|
|
66
|
+
if (typeof mode === "string" && mode !== "chat")
|
|
67
|
+
return { filtered: true };
|
|
68
|
+
if (supportsToolCalls === false)
|
|
69
|
+
return { filtered: true };
|
|
70
|
+
return {
|
|
71
|
+
advert: {
|
|
72
|
+
alias: modelName.trim(),
|
|
73
|
+
...(provider !== undefined ? { provider } : {}),
|
|
74
|
+
context_tokens: finiteNonNegative(infoObj.max_input_tokens),
|
|
75
|
+
input_cost_per_token: finiteNonNegative(infoObj.input_cost_per_token),
|
|
76
|
+
output_cost_per_token: finiteNonNegative(infoObj.output_cost_per_token),
|
|
77
|
+
mode: typeof mode === "string" ? mode : undefined,
|
|
78
|
+
supports_tool_calls: typeof supportsToolCalls === "boolean" ? supportsToolCalls : undefined,
|
|
79
|
+
// Operator-declared rank via advert custom key (consumed as-is when present).
|
|
80
|
+
declared_rank: finiteNonNegative(infoObj.capability_rank),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Best-effort "higher = better" score for ranking. Uses a flat `score` field
|
|
86
|
+
* when present; absent/non-finite → null (unscored models rank last).
|
|
50
87
|
*/
|
|
51
88
|
function deriveScore(entry) {
|
|
52
89
|
if (typeof entry.score === "number" && Number.isFinite(entry.score)) {
|
|
53
90
|
return entry.score;
|
|
54
91
|
}
|
|
55
|
-
|
|
56
|
-
return rank === undefined ? null : -rank;
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* The RAW relative-capability rank (LOWER = better) from a registry entry's
|
|
60
|
-
* capability block, when the active proxy exposes one — best-effort and
|
|
61
|
-
* proxy-agnostic (a registry with no capability data yields undefined; the floor
|
|
62
|
-
* then fails open per the owner decision, [[litellm-replaces-repair-proxy]]).
|
|
63
|
-
* Stamped onto the expanded source as `capability_rank` (unified-routing step C)
|
|
64
|
-
* so the admission floor reads per-model capability with no operator declaration.
|
|
65
|
-
*/
|
|
66
|
-
function deriveRawCapabilityRank(entry) {
|
|
67
|
-
const capability = entry.capability;
|
|
68
|
-
if (capability === null || typeof capability !== "object")
|
|
69
|
-
return undefined;
|
|
70
|
-
return (finiteNonNegative(capability.composite_rank) ??
|
|
71
|
-
finiteNonNegative(capability.arena_rank));
|
|
92
|
+
return null;
|
|
72
93
|
}
|
|
73
94
|
/**
|
|
74
95
|
* Tolerantly extract context-window field from a registry entry or its nested
|
|
@@ -92,98 +113,118 @@ function deriveContextTokens(entry) {
|
|
|
92
113
|
return undefined;
|
|
93
114
|
}
|
|
94
115
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
116
|
+
* Discover models via the neutral proxy contract: `GET /v1/models` (required baseline)
|
|
117
|
+
* returns the alias roster; `GET /model/info` (optional enrichment) provides per-model
|
|
118
|
+
* metadata. Tolerant at every step: missing/unparseable/malformed → graceful degradation.
|
|
119
|
+
* Eligibility filters remove models from the pool entirely (mode != 'chat' or
|
|
120
|
+
* supports_tool_calls === false).
|
|
121
|
+
*
|
|
122
|
+
* Returns { adverts, filtered } where adverts is a map of alias → ModelAdvert,
|
|
123
|
+
* and filtered is a set of aliases that failed the eligibility filter.
|
|
100
124
|
*/
|
|
101
|
-
function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
125
|
+
async function discoverModelAdverts(endpoint, fetchImpl, authHeader) {
|
|
126
|
+
const adverts = new Map();
|
|
127
|
+
const filtered = new Set();
|
|
128
|
+
try {
|
|
129
|
+
const headers = { "content-type": "application/json" };
|
|
130
|
+
if (authHeader)
|
|
131
|
+
headers.authorization = authHeader;
|
|
132
|
+
// Try /model/info first (richer advert); tolerate absent/unparseable
|
|
133
|
+
try {
|
|
134
|
+
const response = await fetchImpl(`${endpoint}/model/info`, { headers });
|
|
135
|
+
if (response.ok) {
|
|
136
|
+
const payload = await response.json();
|
|
137
|
+
// Target the current array form: {data: [...]}
|
|
138
|
+
if (payload !== null && typeof payload === "object") {
|
|
139
|
+
const data = payload.data;
|
|
140
|
+
if (Array.isArray(data)) {
|
|
141
|
+
for (const raw of data) {
|
|
142
|
+
if (raw !== null && typeof raw === "object") {
|
|
143
|
+
const result = adaptProxyModelInfo(raw);
|
|
144
|
+
if (result) {
|
|
145
|
+
if (result.filtered) {
|
|
146
|
+
// Track which aliases failed the eligibility filter
|
|
147
|
+
const modelName = raw.model_name;
|
|
148
|
+
if (typeof modelName === "string" && modelName.trim().length > 0) {
|
|
149
|
+
filtered.add(modelName.trim());
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (result.advert) {
|
|
153
|
+
// Successful adaptation
|
|
154
|
+
adverts.set(result.advert.alias, result.advert);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// If result is undefined, it's invalid data (not eligibility filter)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
123
162
|
}
|
|
124
163
|
}
|
|
164
|
+
catch {
|
|
165
|
+
// /model/info absent/error → degrade to roster-only (no enrichment)
|
|
166
|
+
}
|
|
125
167
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
continue;
|
|
168
|
+
catch {
|
|
169
|
+
// No adverts → degrade to roster-only (carrier of aliases only)
|
|
170
|
+
}
|
|
171
|
+
return { adverts, filtered };
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Discover the model roster via the neutral proxy contract: `GET /v1/models`
|
|
175
|
+
* (OpenAI-compatible list). Returns { aliases, error? }; absent/unparseable → empty aliases + reason.
|
|
176
|
+
*/
|
|
177
|
+
async function discoverModelRoster(endpoint, fetchImpl, authHeader) {
|
|
178
|
+
try {
|
|
179
|
+
const headers = { "content-type": "application/json" };
|
|
180
|
+
if (authHeader)
|
|
181
|
+
headers.authorization = authHeader;
|
|
182
|
+
const response = await fetchImpl(`${endpoint}/v1/models`, { headers });
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
return {
|
|
185
|
+
aliases: [],
|
|
186
|
+
error: `HTTP ${response.status}`,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
const payload = await response.json();
|
|
190
|
+
if (payload === null || typeof payload !== "object")
|
|
191
|
+
return { aliases: [] };
|
|
192
|
+
const data = payload.data;
|
|
193
|
+
if (!Array.isArray(data))
|
|
194
|
+
return { aliases: [] };
|
|
195
|
+
const aliases = [];
|
|
196
|
+
for (const entry of data) {
|
|
197
|
+
if (entry !== null && typeof entry === "object") {
|
|
198
|
+
const id = entry.id;
|
|
199
|
+
if (typeof id === "string" && id.trim().length > 0) {
|
|
200
|
+
aliases.push(id.trim());
|
|
160
201
|
}
|
|
161
|
-
if (typeof rawModel !== "object")
|
|
162
|
-
continue;
|
|
163
|
-
const model = rawModel;
|
|
164
|
-
push({ ...entry, ...model, models: undefined }, provider, model.id ?? model.model);
|
|
165
202
|
}
|
|
166
|
-
continue;
|
|
167
203
|
}
|
|
168
|
-
|
|
204
|
+
return { aliases };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
208
|
+
return { aliases: [], error: reason };
|
|
169
209
|
}
|
|
170
|
-
return models;
|
|
171
210
|
}
|
|
172
211
|
/**
|
|
173
|
-
* Expand
|
|
174
|
-
*
|
|
212
|
+
* Expand discovered models into `claude-worker` sources: per backend provider,
|
|
213
|
+
* the top-K models by best-effort score (higher = better; unscored last).
|
|
175
214
|
* Shape per the plan's identity section: the transport never enters the identity —
|
|
176
|
-
* `backend_provider` + `model` key quota; `endpoint` is the proxy url the
|
|
177
|
-
* transport
|
|
178
|
-
*
|
|
215
|
+
* `backend_provider` + `model` (alias) key quota; `endpoint` is the proxy url the
|
|
216
|
+
* launch transport fronts the spawn with; `worker_kind` is `agentic` by definition.
|
|
217
|
+
*
|
|
218
|
+
* Provider derivation: advert `provider` (from /model/info) > slash-prefix of alias
|
|
219
|
+
* (e.g., `anthropic/claude-*` → `anthropic`) > default shared `"proxy"` bucket
|
|
220
|
+
* (coarse pool identity at degradation rung, revisitable per owner decision).
|
|
179
221
|
*/
|
|
180
|
-
function expandSources(
|
|
222
|
+
function expandSources(discovered, options) {
|
|
181
223
|
const byProvider = new Map();
|
|
182
|
-
// Dedup by (provider,
|
|
183
|
-
// pool identity must expand to exactly one source (first row wins).
|
|
224
|
+
// Dedup by (provider, alias): first row wins.
|
|
184
225
|
const seen = new Set();
|
|
185
|
-
for (const model of
|
|
186
|
-
const identity = `${model.provider}/${model.
|
|
226
|
+
for (const model of discovered) {
|
|
227
|
+
const identity = `${model.provider}/${model.alias}`;
|
|
187
228
|
if (seen.has(identity))
|
|
188
229
|
continue;
|
|
189
230
|
seen.add(identity);
|
|
@@ -192,25 +233,26 @@ function expandSources(models, options) {
|
|
|
192
233
|
byProvider.set(model.provider, bucket);
|
|
193
234
|
}
|
|
194
235
|
const sources = [];
|
|
195
|
-
// Stable, content-derived order (provider, then score desc, then
|
|
196
|
-
// re-populate over identical
|
|
236
|
+
// Stable, content-derived order (provider, then score desc, then alias) so
|
|
237
|
+
// re-populate over identical state emits byte-identical sources.
|
|
197
238
|
for (const provider of [...byProvider.keys()].sort()) {
|
|
198
239
|
const ranked = byProvider
|
|
199
240
|
.get(provider)
|
|
200
241
|
.sort((a, b) => (b.score ?? Number.NEGATIVE_INFINITY) - (a.score ?? Number.NEGATIVE_INFINITY) ||
|
|
201
|
-
a.
|
|
242
|
+
a.alias.localeCompare(b.alias))
|
|
202
243
|
.slice(0, options.topK);
|
|
203
244
|
for (const model of ranked) {
|
|
204
|
-
// Cost precedence: operator-declared (free-to-operator axis) >
|
|
205
|
-
//
|
|
245
|
+
// Cost precedence: operator-declared (free-to-operator axis) > advert price
|
|
246
|
+
// > absent (falls through to models.dev catalog / tier downstream).
|
|
206
247
|
const cost = options.costPerMtok ?? model.costPerMtok;
|
|
207
248
|
sources.push({
|
|
208
|
-
id: `claude-worker:${provider}/${model.
|
|
249
|
+
id: `claude-worker:${provider}/${model.alias}`,
|
|
209
250
|
provider: "claude-worker",
|
|
210
251
|
endpoint: options.endpoint,
|
|
211
252
|
backend_provider: provider,
|
|
212
|
-
model: model.
|
|
253
|
+
model: model.alias, // Alias VERBATIM — the proxy's routing key
|
|
213
254
|
worker_kind: "agentic",
|
|
255
|
+
...(options.apiKeyEnv !== undefined ? { api_key_env: options.apiKeyEnv } : {}),
|
|
214
256
|
...(cost !== undefined ? { cost_per_mtok: cost } : {}),
|
|
215
257
|
// Step C: per-model capability (raw rank, LOWER = better) rides the source →
|
|
216
258
|
// CapacityPool.declaredCapabilityRank → the admission capability floor.
|
|
@@ -226,23 +268,25 @@ function expandSources(models, options) {
|
|
|
226
268
|
return sources;
|
|
227
269
|
}
|
|
228
270
|
/**
|
|
229
|
-
* Probe a single
|
|
230
|
-
* Returns { dropped: true, reason } if the model should be
|
|
231
|
-
* or { dropped: false } to keep
|
|
271
|
+
* Probe a single model via Anthropic-compatible `/v1/messages` to verify it is
|
|
272
|
+
* actually reachable. Returns { dropped: true, reason } if the model should be
|
|
273
|
+
* excluded (404/unavailable), or { dropped: false } to keep it.
|
|
232
274
|
*/
|
|
233
|
-
async function
|
|
275
|
+
async function probeModelViaMessages(alias, backendProvider, endpoint, fetchImpl, timeoutMs, authHeader) {
|
|
234
276
|
const controller = new AbortController();
|
|
235
277
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
236
278
|
try {
|
|
279
|
+
const headers = {
|
|
280
|
+
"anthropic-version": "2023-06-01",
|
|
281
|
+
"content-type": "application/json",
|
|
282
|
+
};
|
|
283
|
+
if (authHeader)
|
|
284
|
+
headers.authorization = authHeader;
|
|
237
285
|
const response = await fetchImpl(`${endpoint}/v1/messages`, {
|
|
238
286
|
method: "POST",
|
|
239
|
-
headers
|
|
240
|
-
"x-api-key": "audit-tools-populate-probe",
|
|
241
|
-
"anthropic-version": "2023-06-01",
|
|
242
|
-
"content-type": "application/json",
|
|
243
|
-
},
|
|
287
|
+
headers,
|
|
244
288
|
body: JSON.stringify({
|
|
245
|
-
model:
|
|
289
|
+
model: alias, // Route on proxy alias, not backend namespace
|
|
246
290
|
max_tokens: 1,
|
|
247
291
|
messages: [{ role: "user", content: "hi" }],
|
|
248
292
|
}),
|
|
@@ -265,11 +309,11 @@ async function probeSource(source, endpoint, fetchImpl, timeoutMs) {
|
|
|
265
309
|
return { dropped: true, reason: `HTTP ${response.status} (model unavailable)` };
|
|
266
310
|
}
|
|
267
311
|
}
|
|
268
|
-
// 200, 401, 429, 5xx, etc. → keep the
|
|
312
|
+
// 200, 401, 429, 5xx, etc. → keep the model
|
|
269
313
|
return { dropped: false };
|
|
270
314
|
}
|
|
271
315
|
catch (error) {
|
|
272
|
-
// Transport failure (timeout, network error) → fail-open, keep the
|
|
316
|
+
// Transport failure (timeout, network error) → fail-open, keep the model
|
|
273
317
|
return { dropped: false };
|
|
274
318
|
}
|
|
275
319
|
finally {
|
|
@@ -277,52 +321,56 @@ async function probeSource(source, endpoint, fetchImpl, timeoutMs) {
|
|
|
277
321
|
}
|
|
278
322
|
}
|
|
279
323
|
/**
|
|
280
|
-
*
|
|
324
|
+
* Probe models with bounded concurrency using a simple worker pool.
|
|
281
325
|
*/
|
|
282
|
-
async function
|
|
326
|
+
async function probeModelsWithConcurrency(aliases, backendProvider, endpoint, fetchImpl, timeoutMs, concurrency, authHeader) {
|
|
283
327
|
const results = [];
|
|
284
328
|
let index = 0;
|
|
285
329
|
const worker = async () => {
|
|
286
|
-
while (index <
|
|
330
|
+
while (index < aliases.length) {
|
|
287
331
|
const currentIndex = index++;
|
|
288
|
-
const
|
|
289
|
-
const probeResult = await
|
|
332
|
+
const alias = aliases[currentIndex];
|
|
333
|
+
const probeResult = await probeModelViaMessages(alias, backendProvider, endpoint, fetchImpl, timeoutMs, authHeader);
|
|
290
334
|
results[currentIndex] = {
|
|
291
|
-
|
|
335
|
+
alias,
|
|
292
336
|
dropped: probeResult.dropped,
|
|
293
337
|
reason: probeResult.reason,
|
|
294
338
|
};
|
|
295
339
|
}
|
|
296
340
|
};
|
|
297
|
-
const workers = Array(Math.min(concurrency,
|
|
341
|
+
const workers = Array(Math.min(concurrency, aliases.length))
|
|
298
342
|
.fill(null)
|
|
299
343
|
.map(() => worker());
|
|
300
344
|
await Promise.all(workers);
|
|
301
|
-
// results is index-assigned
|
|
302
|
-
// the sources' order regardless of probe completion order.
|
|
345
|
+
// results is index-assigned, so already in order.
|
|
303
346
|
return results;
|
|
304
347
|
}
|
|
305
348
|
/**
|
|
306
|
-
* POPULATE:
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
349
|
+
* POPULATE: discover and expand models from an OpenAI-compatible proxy
|
|
350
|
+
* (`GET <endpoint>/v1/models` + `GET <endpoint>/model/info`) into
|
|
351
|
+
* ready-to-fold `claude-worker` sources, then write to the machine-level cache.
|
|
352
|
+
* Network-bound and cacheable — runs at Gate-0 build / explicit refresh, NEVER
|
|
353
|
+
* inside `resolveAmbientSources` (which only READS via {@link readProxyCatalog}).
|
|
354
|
+
* Never throws: a failed fetch returns `{written:false, reason}` and leaves any
|
|
355
|
+
* prior cache untouched; an empty discovery WRITES an empty expansion (fresh
|
|
356
|
+
* knowledge — resolve reports the lane unexpanded with a reason).
|
|
313
357
|
*/
|
|
314
358
|
export async function populateProxyCatalog(options) {
|
|
315
359
|
const endpoint = options.endpoint.replace(/\/+$/u, "");
|
|
316
360
|
const doFetch = options.fetchImpl ?? fetch;
|
|
317
|
-
//
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
//
|
|
361
|
+
// Build auth header when api_key_env is declared and the env var is set.
|
|
362
|
+
let authHeader;
|
|
363
|
+
if (options.apiKeyEnv) {
|
|
364
|
+
const keyValue = process.env[options.apiKeyEnv];
|
|
365
|
+
if (keyValue?.trim()) {
|
|
366
|
+
authHeader = `Bearer ${keyValue}`;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
// Freshness short-circuit: populate carries live per-model probes — real
|
|
370
|
+
// `/v1/messages` POSTs through the proxy that cost seconds and burn quota.
|
|
371
|
+
// A same-endpoint cache younger than the TTL answers instead. This is a
|
|
372
|
+
// REFRESH throttle, not staleness acceptance — the no-TTL-on-READ residual
|
|
373
|
+
// (backlog) is unchanged.
|
|
326
374
|
const cached = readProxyCatalog({ homeDir: options.homeDir });
|
|
327
375
|
if (cached && cached.endpoint === endpoint) {
|
|
328
376
|
const nowMs = (options.now?.() ?? new Date()).getTime();
|
|
@@ -338,47 +386,90 @@ export async function populateProxyCatalog(options) {
|
|
|
338
386
|
};
|
|
339
387
|
}
|
|
340
388
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!response.ok) {
|
|
345
|
-
return {
|
|
346
|
-
sources: [],
|
|
347
|
-
written: false,
|
|
348
|
-
reason: `GET ${endpoint}/registry returned HTTP ${response.status}.`,
|
|
349
|
-
dropped: [],
|
|
350
|
-
};
|
|
351
|
-
}
|
|
352
|
-
payload = await response.json();
|
|
353
|
-
}
|
|
354
|
-
catch (error) {
|
|
389
|
+
// Discover model roster (required baseline: /v1/models).
|
|
390
|
+
const { aliases, error: rosterError } = await discoverModelRoster(endpoint, doFetch, authHeader);
|
|
391
|
+
if (aliases.length === 0) {
|
|
355
392
|
return {
|
|
356
393
|
sources: [],
|
|
357
394
|
written: false,
|
|
358
|
-
reason:
|
|
395
|
+
reason: rosterError
|
|
396
|
+
? `GET ${endpoint}/v1/models failed: ${rosterError}`
|
|
397
|
+
: `GET ${endpoint}/v1/models returned no models or was unreachable.`,
|
|
359
398
|
dropped: [],
|
|
360
399
|
};
|
|
361
400
|
}
|
|
362
|
-
|
|
401
|
+
// Discover model adverts (optional enrichment: /model/info).
|
|
402
|
+
const { adverts, filtered } = await discoverModelAdverts(endpoint, doFetch, authHeader);
|
|
403
|
+
// Build discovered models: join roster + adverts, deriving provider.
|
|
404
|
+
const discovered = [];
|
|
405
|
+
for (const alias of aliases) {
|
|
406
|
+
// Skip models that failed the eligibility filter
|
|
407
|
+
if (filtered.has(alias))
|
|
408
|
+
continue;
|
|
409
|
+
const advert = adverts.get(alias);
|
|
410
|
+
// Provider derivation: advert provider > slash-prefix > default "proxy" bucket.
|
|
411
|
+
let provider;
|
|
412
|
+
if (advert?.provider) {
|
|
413
|
+
provider = advert.provider;
|
|
414
|
+
}
|
|
415
|
+
else if (alias.includes("/")) {
|
|
416
|
+
provider = alias.split("/")[0];
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
provider = "proxy";
|
|
420
|
+
}
|
|
421
|
+
// Cost blend: mean of input/output $/Mtok when both present; otherwise the one present.
|
|
422
|
+
const availableCosts = [];
|
|
423
|
+
if (advert?.input_cost_per_token !== undefined)
|
|
424
|
+
availableCosts.push(advert.input_cost_per_token);
|
|
425
|
+
if (advert?.output_cost_per_token !== undefined)
|
|
426
|
+
availableCosts.push(advert.output_cost_per_token);
|
|
427
|
+
let costPerMtok;
|
|
428
|
+
if (availableCosts.length === 2) {
|
|
429
|
+
costPerMtok = (availableCosts[0] + availableCosts[1]) / 2;
|
|
430
|
+
}
|
|
431
|
+
else if (availableCosts.length === 1) {
|
|
432
|
+
costPerMtok = availableCosts[0];
|
|
433
|
+
}
|
|
434
|
+
discovered.push({
|
|
435
|
+
alias,
|
|
436
|
+
provider,
|
|
437
|
+
score: advert?.declared_rank !== undefined ? -advert.declared_rank : null,
|
|
438
|
+
capabilityRank: advert?.declared_rank,
|
|
439
|
+
costPerMtok,
|
|
440
|
+
contextTokens: advert?.context_tokens,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
// Probe to verify models are reachable and drop 404s.
|
|
444
|
+
// Group by provider for parallel probing per provider.
|
|
445
|
+
const byProvider = new Map();
|
|
446
|
+
for (const model of discovered) {
|
|
447
|
+
const bucket = byProvider.get(model.provider) ?? [];
|
|
448
|
+
bucket.push(model.alias);
|
|
449
|
+
byProvider.set(model.provider, bucket);
|
|
450
|
+
}
|
|
451
|
+
const droppedAliases = new Set();
|
|
452
|
+
const dropped = [];
|
|
453
|
+
for (const [provider, providerAliases] of byProvider) {
|
|
454
|
+
const probeResults = await probeModelsWithConcurrency(providerAliases, provider, endpoint, doFetch, POPULATE_PROBE_TIMEOUT_MS, POPULATE_PROBE_CONCURRENCY, authHeader);
|
|
455
|
+
for (const result of probeResults) {
|
|
456
|
+
if (result.dropped) {
|
|
457
|
+
droppedAliases.add(result.alias);
|
|
458
|
+
dropped.push({
|
|
459
|
+
id: `claude-worker:${provider}/${result.alias}`,
|
|
460
|
+
reason: result.reason ?? "model unavailable",
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
// Filter out dropped models and expand sources.
|
|
466
|
+
const toExpand = discovered.filter((m) => !droppedAliases.has(m.alias));
|
|
467
|
+
let sources = expandSources(toExpand, {
|
|
363
468
|
endpoint,
|
|
364
469
|
topK: options.topK ?? DEFAULT_PROXY_TOP_K,
|
|
365
470
|
costPerMtok: options.costPerMtok,
|
|
471
|
+
apiKeyEnv: options.apiKeyEnv,
|
|
366
472
|
});
|
|
367
|
-
// Probe each source to verify reachability
|
|
368
|
-
const probeResults = await probeSourcesWithConcurrency(sources, endpoint, doFetch, POPULATE_PROBE_TIMEOUT_MS, POPULATE_PROBE_CONCURRENCY);
|
|
369
|
-
const dropped = [];
|
|
370
|
-
sources = probeResults
|
|
371
|
-
.filter((result) => {
|
|
372
|
-
if (result.dropped) {
|
|
373
|
-
dropped.push({
|
|
374
|
-
id: result.source.id ?? `claude-worker:${result.source.backend_provider}/${result.source.model}`,
|
|
375
|
-
reason: result.reason ?? "model unavailable",
|
|
376
|
-
});
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
return true;
|
|
380
|
-
})
|
|
381
|
-
.map((result) => result.source);
|
|
382
473
|
const catalog = {
|
|
383
474
|
fetched_at: (options.now?.() ?? new Date()).toISOString(),
|
|
384
475
|
endpoint,
|