@f5xc-salesdemos/xcsh 15.5.0 → 15.6.2
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/package.json +7 -7
- package/src/config/auto-config.ts +154 -82
- package/src/config/model-registry.ts +35 -0
- package/src/sdk.ts +9 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@f5xc-salesdemos/xcsh",
|
|
4
|
-
"version": "15.
|
|
4
|
+
"version": "15.6.2",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/f5xc-salesdemos/xcsh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.16.1",
|
|
48
48
|
"@mozilla/readability": "^0.6",
|
|
49
|
-
"@f5xc-salesdemos/xcsh-stats": "15.
|
|
50
|
-
"@f5xc-salesdemos/pi-agent-core": "15.
|
|
51
|
-
"@f5xc-salesdemos/pi-ai": "15.
|
|
52
|
-
"@f5xc-salesdemos/pi-natives": "15.
|
|
53
|
-
"@f5xc-salesdemos/pi-tui": "15.
|
|
54
|
-
"@f5xc-salesdemos/pi-utils": "15.
|
|
49
|
+
"@f5xc-salesdemos/xcsh-stats": "15.6.2",
|
|
50
|
+
"@f5xc-salesdemos/pi-agent-core": "15.6.2",
|
|
51
|
+
"@f5xc-salesdemos/pi-ai": "15.6.2",
|
|
52
|
+
"@f5xc-salesdemos/pi-natives": "15.6.2",
|
|
53
|
+
"@f5xc-salesdemos/pi-tui": "15.6.2",
|
|
54
|
+
"@f5xc-salesdemos/pi-utils": "15.6.2",
|
|
55
55
|
"@sinclair/typebox": "^0.34",
|
|
56
56
|
"@xterm/headless": "^6.0",
|
|
57
57
|
"ajv": "^8.18",
|
|
@@ -43,12 +43,13 @@ function getLiteLLMBaseUrl(): string | undefined {
|
|
|
43
43
|
// ---------------------------------------------------------------------------
|
|
44
44
|
|
|
45
45
|
export interface GenerateModelsYmlOptions {
|
|
46
|
-
/**
|
|
47
|
-
|
|
46
|
+
/** API base path for the litellm provider (e.g. "/v1" or "/api/v1"). Defaults to "/v1". */
|
|
47
|
+
apiBasePath?: string;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/** Generate models.yml content for LiteLLM proxy. */
|
|
51
51
|
export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOptions): string {
|
|
52
|
+
const apiBase = options?.apiBasePath ?? "/v1";
|
|
52
53
|
const lines = [
|
|
53
54
|
"# Auto-generated by xcsh for LiteLLM proxy",
|
|
54
55
|
"# API key resolved from LITELLM_API_KEY env var at runtime",
|
|
@@ -57,22 +58,14 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
|
|
|
57
58
|
" anthropic:",
|
|
58
59
|
` baseUrl: "${baseUrl}/anthropic"`,
|
|
59
60
|
" apiKey: LITELLM_API_KEY",
|
|
61
|
+
" litellm:",
|
|
62
|
+
` baseUrl: "${baseUrl}${apiBase}"`,
|
|
63
|
+
" apiKey: LITELLM_API_KEY",
|
|
64
|
+
" api: openai-completions",
|
|
65
|
+
" discovery:",
|
|
66
|
+
" type: openai-compat",
|
|
60
67
|
];
|
|
61
68
|
|
|
62
|
-
// When the proxy has been probed and models discovered, add a litellm provider
|
|
63
|
-
// with openai-compat discovery so the model registry fetches real model catalog
|
|
64
|
-
const discovered = options?.discoveredModels;
|
|
65
|
-
if (discovered && discovered.length > 0) {
|
|
66
|
-
lines.push(
|
|
67
|
-
" litellm:",
|
|
68
|
-
` baseUrl: "${baseUrl}/v1"`,
|
|
69
|
-
" apiKey: LITELLM_API_KEY",
|
|
70
|
-
" api: openai-completions",
|
|
71
|
-
" discovery:",
|
|
72
|
-
" type: openai-compat",
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
69
|
lines.push("");
|
|
77
70
|
return lines.join("\n");
|
|
78
71
|
}
|
|
@@ -81,6 +74,9 @@ export function generateModelsYml(baseUrl: string, options?: GenerateModelsYmlOp
|
|
|
81
74
|
export function generateConfigYml(): string {
|
|
82
75
|
return [
|
|
83
76
|
"# Auto-generated by xcsh for LiteLLM proxy",
|
|
77
|
+
"modelRoles:",
|
|
78
|
+
" default: anthropic/claude-opus-4-6",
|
|
79
|
+
"",
|
|
84
80
|
"providers:",
|
|
85
81
|
" image: openai",
|
|
86
82
|
"",
|
|
@@ -100,6 +96,25 @@ export function generateConfigYml(): string {
|
|
|
100
96
|
].join("\n");
|
|
101
97
|
}
|
|
102
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Ensure config.yml has a modelRoles.default entry.
|
|
101
|
+
* Existing configs from earlier versions may be missing this, causing xcsh
|
|
102
|
+
* to fall through to the "first available model" fallback which picks stale
|
|
103
|
+
* cached models that the proxy can't serve.
|
|
104
|
+
*/
|
|
105
|
+
function healConfigYmlModelRoles(configPath: string): void {
|
|
106
|
+
try {
|
|
107
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
108
|
+
if (content.includes("modelRoles:")) return; // Already has modelRoles
|
|
109
|
+
// Prepend modelRoles section
|
|
110
|
+
const healed = `modelRoles:\n default: anthropic/claude-opus-4-6\n\n${content}`;
|
|
111
|
+
fs.writeFileSync(configPath, healed);
|
|
112
|
+
logger.debug("Healed config.yml: added default modelRoles", { configPath });
|
|
113
|
+
} catch {
|
|
114
|
+
// Best-effort — don't block startup
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
// ---------------------------------------------------------------------------
|
|
104
119
|
// Backup
|
|
105
120
|
// ---------------------------------------------------------------------------
|
|
@@ -129,6 +144,26 @@ function safeWrite(filePath: string, content: string): boolean {
|
|
|
129
144
|
}
|
|
130
145
|
}
|
|
131
146
|
|
|
147
|
+
/** Remove the model cache database so discovery re-runs fresh. */
|
|
148
|
+
function clearModelCache(modelsPath: string): void {
|
|
149
|
+
const cacheDbPath = path.join(path.dirname(modelsPath), "models.db");
|
|
150
|
+
try {
|
|
151
|
+
if (fs.existsSync(cacheDbPath)) {
|
|
152
|
+
fs.unlinkSync(cacheDbPath);
|
|
153
|
+
logger.debug("Cleared stale model cache", { cacheDbPath });
|
|
154
|
+
}
|
|
155
|
+
// Also remove WAL/SHM files if present
|
|
156
|
+
for (const suffix of ["-wal", "-shm"]) {
|
|
157
|
+
const walPath = `${cacheDbPath}${suffix}`;
|
|
158
|
+
if (fs.existsSync(walPath)) {
|
|
159
|
+
fs.unlinkSync(walPath);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// Best-effort — don't block config repair
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
132
167
|
// ---------------------------------------------------------------------------
|
|
133
168
|
// Proxy connection probing
|
|
134
169
|
// ---------------------------------------------------------------------------
|
|
@@ -137,14 +172,21 @@ export interface ProbeResult {
|
|
|
137
172
|
reachable: boolean;
|
|
138
173
|
models: string[];
|
|
139
174
|
error?: string;
|
|
175
|
+
/** The API base path that worked (e.g. "/v1" or "/api/v1"). */
|
|
176
|
+
apiBasePath?: string;
|
|
140
177
|
}
|
|
141
178
|
|
|
179
|
+
/** Candidate paths to probe for the models endpoint (tried in order). */
|
|
180
|
+
const MODELS_ENDPOINT_PATHS = ["/v1/models", "/api/v1/models"];
|
|
181
|
+
|
|
142
182
|
/**
|
|
143
|
-
* Probe a LiteLLM proxy
|
|
144
|
-
*
|
|
183
|
+
* Probe a LiteLLM proxy to validate connectivity and discover available models.
|
|
184
|
+
*
|
|
185
|
+
* Tries multiple endpoint paths in order (/v1/models, then /api/v1/models) to
|
|
186
|
+
* handle deployments where a frontend like Open WebUI intercepts /v1/*.
|
|
145
187
|
*
|
|
146
188
|
* Returns the list of model IDs on success, or an error on failure.
|
|
147
|
-
* Uses a 3-second timeout to avoid blocking startup.
|
|
189
|
+
* Uses a 3-second timeout per endpoint to avoid blocking startup.
|
|
148
190
|
*/
|
|
149
191
|
export async function probeLiteLLMConnection(
|
|
150
192
|
baseUrl: string,
|
|
@@ -153,60 +195,63 @@ export async function probeLiteLLMConnection(
|
|
|
153
195
|
): Promise<ProbeResult> {
|
|
154
196
|
const fetchImpl = options?.fetch ?? globalThis.fetch;
|
|
155
197
|
const normalizedUrl = baseUrl.replace(/\/+$/, "");
|
|
198
|
+
let lastError = "";
|
|
156
199
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
response
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
}
|
|
200
|
+
for (const endpointPath of MODELS_ENDPOINT_PATHS) {
|
|
201
|
+
const url = `${normalizedUrl}${endpointPath}`;
|
|
202
|
+
let response: Response;
|
|
203
|
+
try {
|
|
204
|
+
response = await fetchImpl(url, {
|
|
205
|
+
method: "GET",
|
|
206
|
+
headers: {
|
|
207
|
+
Accept: "application/json",
|
|
208
|
+
Authorization: `Bearer ${apiKey}`,
|
|
209
|
+
},
|
|
210
|
+
signal: options?.signal ?? AbortSignal.timeout(3000),
|
|
211
|
+
});
|
|
212
|
+
} catch (err) {
|
|
213
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
174
216
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
error: `HTTP ${response.status} ${response.statusText}`,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
217
|
+
if (!response.ok) {
|
|
218
|
+
lastError = `HTTP ${response.status} ${response.statusText} from ${url}`;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
182
221
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
error: "Failed to parse response as JSON",
|
|
191
|
-
};
|
|
192
|
-
}
|
|
222
|
+
let payload: unknown;
|
|
223
|
+
try {
|
|
224
|
+
payload = await response.json();
|
|
225
|
+
} catch {
|
|
226
|
+
lastError = `Non-JSON response from ${url}`;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
193
229
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
230
|
+
// OpenAI-compatible /v1/models returns { data: [{ id: "model-name", ... }] }
|
|
231
|
+
const models: string[] = [];
|
|
232
|
+
if (
|
|
233
|
+
payload &&
|
|
234
|
+
typeof payload === "object" &&
|
|
235
|
+
"data" in payload &&
|
|
236
|
+
Array.isArray((payload as { data: unknown }).data)
|
|
237
|
+
) {
|
|
238
|
+
for (const entry of (payload as { data: Array<{ id?: string }> }).data) {
|
|
239
|
+
if (typeof entry.id === "string" && entry.id.length > 0) {
|
|
240
|
+
models.push(entry.id);
|
|
241
|
+
}
|
|
205
242
|
}
|
|
206
243
|
}
|
|
244
|
+
|
|
245
|
+
if (models.length > 0) {
|
|
246
|
+
// Derive the API base path from the endpoint that worked
|
|
247
|
+
const apiBasePath = endpointPath.replace(/\/models$/, "");
|
|
248
|
+
return { reachable: true, models, apiBasePath };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
lastError = `No models in response from ${url}`;
|
|
207
252
|
}
|
|
208
253
|
|
|
209
|
-
return { reachable:
|
|
254
|
+
return { reachable: false, models: [], error: lastError };
|
|
210
255
|
}
|
|
211
256
|
|
|
212
257
|
// ---------------------------------------------------------------------------
|
|
@@ -227,11 +272,13 @@ export function tryAutoConfigLiteLLM(modelsPath: string): boolean {
|
|
|
227
272
|
if (!safeWrite(modelsPath, generateModelsYml(baseUrl))) return false;
|
|
228
273
|
logger.debug("Auto-configured LiteLLM proxy", { modelsPath, baseUrl });
|
|
229
274
|
|
|
230
|
-
// Write config.yml if it doesn't exist
|
|
275
|
+
// Write config.yml if it doesn't exist, or heal it if it's missing modelRoles
|
|
231
276
|
const configPath = path.join(path.dirname(modelsPath), "config.yml");
|
|
232
277
|
if (!fs.existsSync(configPath)) {
|
|
233
278
|
safeWrite(configPath, generateConfigYml());
|
|
234
279
|
logger.debug("Auto-generated default config", { configPath });
|
|
280
|
+
} else {
|
|
281
|
+
healConfigYmlModelRoles(configPath);
|
|
235
282
|
}
|
|
236
283
|
|
|
237
284
|
return true;
|
|
@@ -360,6 +407,9 @@ export function autoFixModelsConfig(modelsPath: string): FixResult {
|
|
|
360
407
|
return { fixed: false, changes: [`Write failed: could not write to ${modelsPath}`] };
|
|
361
408
|
}
|
|
362
409
|
|
|
410
|
+
// Clear stale model cache so discovery re-runs with the new config
|
|
411
|
+
clearModelCache(modelsPath);
|
|
412
|
+
|
|
363
413
|
logger.debug("Auto-fixed LiteLLM config", { modelsPath, baseUrl });
|
|
364
414
|
return { fixed: true, changes: [`Regenerated models.yml with baseUrl: ${baseUrl}/anthropic`] };
|
|
365
415
|
}
|
|
@@ -416,16 +466,33 @@ export function startupHealthCheck(
|
|
|
416
466
|
return fix.fixed;
|
|
417
467
|
}
|
|
418
468
|
|
|
419
|
-
// Case 4: Config OK and URL matches
|
|
469
|
+
// Case 4: Config OK and URL matches — check for structural issues
|
|
420
470
|
try {
|
|
421
471
|
const content = fs.readFileSync(modelsPath, "utf-8");
|
|
472
|
+
|
|
473
|
+
// 4a: configVersion is missing or outdated
|
|
422
474
|
if (!content.includes(`configVersion: ${CURRENT_CONFIG_VERSION}`)) {
|
|
423
475
|
logger.debug("Upgrading models.yml to configVersion", { version: CURRENT_CONFIG_VERSION });
|
|
424
476
|
const fix = autoFixModelsConfig(modelsPath);
|
|
425
477
|
return fix.fixed;
|
|
426
478
|
}
|
|
479
|
+
|
|
480
|
+
// 4b: litellm discovery provider is missing (legacy v1-style config)
|
|
481
|
+
if (!content.includes("type: openai-compat")) {
|
|
482
|
+
logger.debug("Adding litellm discovery provider to models.yml");
|
|
483
|
+
const fix = autoFixModelsConfig(modelsPath);
|
|
484
|
+
return fix.fixed;
|
|
485
|
+
}
|
|
427
486
|
} catch {
|
|
428
|
-
// File read failed — skip
|
|
487
|
+
// File read failed — skip structural checks, don't block startup
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Always heal config.yml model roles (regardless of models.yml state)
|
|
492
|
+
if (hasLiteLLMEnv()) {
|
|
493
|
+
const configPath = path.join(path.dirname(modelsPath), "config.yml");
|
|
494
|
+
if (fs.existsSync(configPath)) {
|
|
495
|
+
healConfigYmlModelRoles(configPath);
|
|
429
496
|
}
|
|
430
497
|
}
|
|
431
498
|
|
|
@@ -437,12 +504,11 @@ export function startupHealthCheck(
|
|
|
437
504
|
// ---------------------------------------------------------------------------
|
|
438
505
|
|
|
439
506
|
/**
|
|
440
|
-
* Probe the LiteLLM proxy and upgrade
|
|
507
|
+
* Probe the LiteLLM proxy and upgrade config with the correct API base path.
|
|
441
508
|
*
|
|
442
509
|
* This is an async operation that runs during the first ModelRegistry.refresh().
|
|
443
|
-
* It validates proxy connectivity and, if successful,
|
|
444
|
-
*
|
|
445
|
-
* discovers real models at runtime instead of relying on bundled model IDs.
|
|
510
|
+
* It validates proxy connectivity and, if successful, ensures the config uses
|
|
511
|
+
* the correct API base path (e.g. /api/v1 for Open WebUI deployments).
|
|
446
512
|
*
|
|
447
513
|
* Returns true if the config was upgraded (caller should reload).
|
|
448
514
|
*/
|
|
@@ -456,18 +522,15 @@ export async function probeAndUpgradeLiteLLMConfig(
|
|
|
456
522
|
const apiKey = $env.LITELLM_API_KEY?.trim();
|
|
457
523
|
if (!baseUrl || !apiKey) return false;
|
|
458
524
|
|
|
459
|
-
|
|
525
|
+
let content: string;
|
|
460
526
|
try {
|
|
461
|
-
|
|
462
|
-
if (content.includes("type: openai-compat")) {
|
|
463
|
-
return false;
|
|
464
|
-
}
|
|
527
|
+
content = fs.readFileSync(modelsPath, "utf-8");
|
|
465
528
|
} catch {
|
|
466
529
|
// File doesn't exist or is unreadable — nothing to upgrade
|
|
467
530
|
return false;
|
|
468
531
|
}
|
|
469
532
|
|
|
470
|
-
// Probe the proxy
|
|
533
|
+
// Probe the proxy to find the working API base path
|
|
471
534
|
const probe = await probeLiteLLMConnection(baseUrl, apiKey, { fetch: options?.fetch });
|
|
472
535
|
if (!probe.reachable) {
|
|
473
536
|
logger.warn("LiteLLM proxy unreachable during upgrade probe — keeping existing config", {
|
|
@@ -482,17 +545,26 @@ export async function probeAndUpgradeLiteLLMConfig(
|
|
|
482
545
|
return false;
|
|
483
546
|
}
|
|
484
547
|
|
|
485
|
-
//
|
|
548
|
+
// Check if the config already has the correct discovery base path
|
|
549
|
+
const hasDiscovery = content.includes("type: openai-compat");
|
|
550
|
+
const correctBase = `${baseUrl}${probe.apiBasePath}`;
|
|
551
|
+
if (hasDiscovery && content.includes(correctBase)) {
|
|
552
|
+
return false; // Already correct
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Upgrade: backup and regenerate with correct base path
|
|
486
556
|
backupIfExists(modelsPath);
|
|
487
|
-
const newContent = generateModelsYml(baseUrl, {
|
|
557
|
+
const newContent = generateModelsYml(baseUrl, { apiBasePath: probe.apiBasePath });
|
|
488
558
|
if (!safeWrite(modelsPath, newContent)) {
|
|
489
559
|
return false;
|
|
490
560
|
}
|
|
491
561
|
|
|
492
|
-
logger.debug("Upgraded LiteLLM config
|
|
562
|
+
logger.debug("Upgraded LiteLLM config", {
|
|
493
563
|
modelsPath,
|
|
494
564
|
baseUrl,
|
|
495
|
-
|
|
565
|
+
apiBasePath: probe.apiBasePath,
|
|
566
|
+
models: probe.models.length,
|
|
567
|
+
hadDiscovery: hasDiscovery,
|
|
496
568
|
});
|
|
497
569
|
return true;
|
|
498
570
|
}
|
|
@@ -822,6 +822,29 @@ export class ModelRegistry {
|
|
|
822
822
|
this.#backgroundRefresh = refreshPromise;
|
|
823
823
|
}
|
|
824
824
|
|
|
825
|
+
/**
|
|
826
|
+
* Await the in-flight background refresh if one is running.
|
|
827
|
+
* Returns immediately if no background refresh is in progress.
|
|
828
|
+
*/
|
|
829
|
+
async awaitBackgroundRefresh(): Promise<void> {
|
|
830
|
+
if (this.#backgroundRefresh) {
|
|
831
|
+
await this.#backgroundRefresh;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Check if any non-optional discoverable provider has no cached models yet.
|
|
837
|
+
* Returns true on first run when the model cache is empty.
|
|
838
|
+
*/
|
|
839
|
+
hasUncachedDiscoverableProviders(): boolean {
|
|
840
|
+
for (const [, state] of this.#providerDiscoveryStates) {
|
|
841
|
+
if (state.status === "idle" && !state.optional) {
|
|
842
|
+
return true;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
|
|
825
848
|
async refreshProvider(providerId: string, strategy: ModelRefreshStrategy = "online"): Promise<void> {
|
|
826
849
|
this.#reloadStaticModels();
|
|
827
850
|
for (const selector of this.#suppressedSelectors.keys()) {
|
|
@@ -1283,10 +1306,22 @@ export class ModelRegistry {
|
|
|
1283
1306
|
): Promise<Model<Api>[]> {
|
|
1284
1307
|
// Skip providers already handled by configured discovery (e.g. user-configured ollama with discovery.type)
|
|
1285
1308
|
const configuredDiscoveryProviders = new Set(this.#discoverableProviders.map(p => p.provider));
|
|
1309
|
+
|
|
1310
|
+
// When a LiteLLM proxy is configured, providers with overridden baseUrls are
|
|
1311
|
+
// proxied through it. Their built-in discovery would query the proxy's model
|
|
1312
|
+
// listing endpoint, which may return model IDs the proxy can't serve for chat.
|
|
1313
|
+
// Skip them — the litellm discovery provider handles model listing instead.
|
|
1314
|
+
const proxiedProviders = hasLiteLLMEnv()
|
|
1315
|
+
? new Set([...this.#providerOverrides.keys()].filter(id => this.#providerOverrides.get(id)?.baseUrl))
|
|
1316
|
+
: new Set<string>();
|
|
1317
|
+
|
|
1286
1318
|
const managerOptions = (await this.#collectBuiltInModelManagerOptions()).filter(opts => {
|
|
1287
1319
|
if (configuredDiscoveryProviders.has(opts.providerId)) {
|
|
1288
1320
|
return false;
|
|
1289
1321
|
}
|
|
1322
|
+
if (proxiedProviders.has(opts.providerId)) {
|
|
1323
|
+
return false;
|
|
1324
|
+
}
|
|
1290
1325
|
return providerFilter ? providerFilter.has(opts.providerId) : true;
|
|
1291
1326
|
});
|
|
1292
1327
|
if (managerOptions.length === 0) {
|
package/src/sdk.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { AsyncJobManager } from "./async";
|
|
|
28
28
|
import { createAutoresearchExtension } from "./autoresearch";
|
|
29
29
|
import { loadCapability } from "./capability";
|
|
30
30
|
import { type Rule, ruleCapability } from "./capability/rule";
|
|
31
|
+
import { hasLiteLLMEnv } from "./config/auto-config";
|
|
31
32
|
import { ModelRegistry } from "./config/model-registry";
|
|
32
33
|
import { formatModelString, parseModelPattern, parseModelString, resolveModelRoleValue } from "./config/model-resolver";
|
|
33
34
|
import { loadPromptTemplates as loadPromptTemplatesInternal, type PromptTemplate } from "./config/prompt-templates";
|
|
@@ -728,6 +729,14 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
728
729
|
const modelMatchPreferences = {
|
|
729
730
|
usageOrder: settings.getStorage()?.getModelUsageOrder(),
|
|
730
731
|
};
|
|
732
|
+
|
|
733
|
+
// When LiteLLM is configured and no model cache exists yet (first run),
|
|
734
|
+
// await the background refresh so model discovery from the proxy completes
|
|
735
|
+
// before we select a default model. Bounded by the 3s probe timeout.
|
|
736
|
+
if (!options.modelRegistry && hasLiteLLMEnv() && modelRegistry.hasUncachedDiscoverableProviders()) {
|
|
737
|
+
await logger.time("awaitLiteLLMDiscovery", () => modelRegistry.awaitBackgroundRefresh());
|
|
738
|
+
}
|
|
739
|
+
|
|
731
740
|
const defaultRoleSpec = logger.time("resolveDefaultModelRole", () =>
|
|
732
741
|
resolveModelRoleValue(settings.getModelRole("default"), modelRegistry.getAvailable(), {
|
|
733
742
|
settings,
|