@link-assistant/agent 0.26.15 → 0.27.1
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 +49 -7
- package/package.json +51 -42
- package/src/agent/agent.ts +12 -7
- package/src/auth/azure-login.ts +241 -0
- package/src/auth/plugins.ts +9 -0
- package/src/auth/provider-logins.ts +804 -0
- package/src/cli/argv.ts +1 -1
- package/src/cli/cmd/auth.ts +4 -0
- package/src/cli/cmd/models.ts +68 -13
- package/src/cli/cmd/run.ts +7 -1
- package/src/cli/continuous-mode.js +38 -12
- package/src/cli/event-handler.js +3 -1
- package/src/cli/model-config.js +27 -8
- package/src/cli/run-options.js +144 -78
- package/src/config/config.ts +306 -105
- package/src/config/defaults.ts +13 -5
- package/src/config/file-config.ts +16 -0
- package/src/file/ripgrep.ts +38 -5
- package/src/file/time.ts +86 -9
- package/src/index.js +42 -33
- package/src/mcp/index.ts +370 -106
- package/src/permission/index.ts +22 -0
- package/src/project/instance.ts +5 -1
- package/src/project/project.ts +35 -2
- package/src/provider/env-template.ts +115 -0
- package/src/provider/extra-providers.ts +347 -0
- package/src/provider/free-tier-error.ts +1 -1
- package/src/provider/kilo-deprecation.ts +30 -0
- package/src/provider/kilo.ts +44 -7
- package/src/provider/model-discovery.ts +690 -0
- package/src/provider/models.ts +11 -1
- package/src/provider/parity-loaders.ts +595 -0
- package/src/provider/provider.ts +309 -203
- package/src/provider/retry-fetch.ts +5 -0
- package/src/provider/transform.ts +168 -17
- package/src/session/compaction.ts +18 -2
- package/src/session/index.ts +30 -3
- package/src/session/message-v2.ts +318 -32
- package/src/session/processor.ts +274 -140
- package/src/session/prompt.ts +154 -128
- package/src/session/retry.ts +26 -13
- package/src/session/summary.ts +4 -1
- package/src/tool/bash.ts +232 -45
- package/src/tool/bash.txt +1 -1
- package/src/tool/codesearch.ts +15 -78
- package/src/tool/edit.ts +111 -30
- package/src/tool/glob.ts +2 -1
- package/src/tool/grep.ts +175 -57
- package/src/tool/ls.ts +2 -1
- package/src/tool/multiedit.ts +3 -1
- package/src/tool/read.ts +169 -41
- package/src/tool/task.ts +26 -18
- package/src/tool/tool.ts +4 -1
- package/src/tool/webfetch.ts +68 -56
- package/src/tool/webfetch.txt +4 -4
- package/src/tool/websearch.ts +20 -75
- package/src/tool/websearch.txt +2 -0
- package/src/tool/write.ts +23 -6
- package/src/util/bom.ts +32 -0
- package/src/util/exa-mcp.ts +82 -0
- package/src/util/legacy-error-names.ts +39 -0
- package/src/util/read-bounded.ts +32 -0
- package/src/util/sse-usage-extractor.ts +67 -13
- package/src/util/timeout.ts +3 -4
- package/src/util/verbose-fetch.ts +40 -1
package/src/provider/provider.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { NamedError } from '../util/error';
|
|
|
10
10
|
import { Auth } from '../auth';
|
|
11
11
|
import { ClaudeOAuth } from '../auth/claude-oauth';
|
|
12
12
|
import { AuthPlugins } from '../auth/plugins';
|
|
13
|
+
import { AzureLogin } from '../auth/azure-login';
|
|
13
14
|
import { Instance } from '../project/instance';
|
|
14
15
|
import { Global } from '../global';
|
|
15
16
|
import { config, isVerbose } from '../config/config';
|
|
@@ -19,23 +20,40 @@ import { createEchoModel } from './echo';
|
|
|
19
20
|
import { createCacheModel } from './cache';
|
|
20
21
|
import { RetryFetch } from './retry-fetch';
|
|
21
22
|
import { SSEUsageExtractor } from '../util/sse-usage-extractor';
|
|
23
|
+
import { sanitizeUrl } from '../util/verbose-fetch';
|
|
22
24
|
import { OpenCodeZen } from './opencode-zen';
|
|
23
25
|
import { Kilo } from './kilo';
|
|
26
|
+
import { resolveKiloDeprecatedAlias } from './kilo-deprecation';
|
|
27
|
+
import { EnvTemplate } from './env-template';
|
|
28
|
+
import { ExtraProviders } from './extra-providers';
|
|
29
|
+
import { ModelDiscovery } from './model-discovery';
|
|
30
|
+
import { ParityLoaders } from './parity-loaders';
|
|
24
31
|
|
|
25
32
|
// Direct imports for bundled providers - these are pre-installed to avoid runtime installation hangs
|
|
26
33
|
// @see https://github.com/link-assistant/agent/issues/173
|
|
27
34
|
// @see https://github.com/oven-sh/bun/issues/5831 (bun install hangs sporadically)
|
|
28
35
|
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
36
|
+
import { createBedrockMantle } from '@ai-sdk/amazon-bedrock/mantle';
|
|
29
37
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
30
38
|
import { createAzure } from '@ai-sdk/azure';
|
|
31
39
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
32
40
|
import { createVertex } from '@ai-sdk/google-vertex';
|
|
41
|
+
import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
|
|
33
42
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
34
43
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
35
44
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
36
45
|
import { createXai } from '@ai-sdk/xai';
|
|
37
46
|
import { createMistral } from '@ai-sdk/mistral';
|
|
38
47
|
import { createGroq } from '@ai-sdk/groq';
|
|
48
|
+
import { createAlibaba } from '@ai-sdk/alibaba';
|
|
49
|
+
import { createCerebras } from '@ai-sdk/cerebras';
|
|
50
|
+
import { createCohere } from '@ai-sdk/cohere';
|
|
51
|
+
import { createDeepInfra } from '@ai-sdk/deepinfra';
|
|
52
|
+
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
53
|
+
import { createFireworks } from '@ai-sdk/fireworks';
|
|
54
|
+
import { createGateway } from '@ai-sdk/gateway';
|
|
55
|
+
import { createPerplexity } from '@ai-sdk/perplexity';
|
|
56
|
+
import { createTogetherAI } from '@ai-sdk/togetherai';
|
|
39
57
|
|
|
40
58
|
export namespace Provider {
|
|
41
59
|
const log = Log.create({ service: 'provider' });
|
|
@@ -70,17 +88,24 @@ export namespace Provider {
|
|
|
70
88
|
'@ai-sdk/xai': createXai,
|
|
71
89
|
'@ai-sdk/mistral': createMistral,
|
|
72
90
|
'@ai-sdk/groq': createGroq,
|
|
91
|
+
// Packages models.dev (and OpenCode) use for the remaining providers and
|
|
92
|
+
// per-model overrides (#334).
|
|
93
|
+
'@ai-sdk/amazon-bedrock/mantle': createBedrockMantle,
|
|
94
|
+
'@ai-sdk/google-vertex/anthropic': createVertexAnthropic,
|
|
95
|
+
'@ai-sdk/alibaba': createAlibaba,
|
|
96
|
+
'@ai-sdk/cerebras': createCerebras,
|
|
97
|
+
'@ai-sdk/cohere': createCohere,
|
|
98
|
+
'@ai-sdk/deepinfra': createDeepInfra,
|
|
99
|
+
'@ai-sdk/deepseek': createDeepSeek,
|
|
100
|
+
'@ai-sdk/fireworks': createFireworks,
|
|
101
|
+
'@ai-sdk/gateway': createGateway,
|
|
102
|
+
'@ai-sdk/perplexity': createPerplexity,
|
|
103
|
+
'@ai-sdk/togetherai': createTogetherAI,
|
|
73
104
|
};
|
|
74
105
|
|
|
75
|
-
type CustomLoader = (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
sdk: any,
|
|
79
|
-
modelID: string,
|
|
80
|
-
options?: Record<string, any>
|
|
81
|
-
) => Promise<any>;
|
|
82
|
-
options?: Record<string, any>;
|
|
83
|
-
}>;
|
|
106
|
+
type CustomLoader = (
|
|
107
|
+
provider: ModelsDev.Provider
|
|
108
|
+
) => Promise<ParityLoaders.LoaderResult>;
|
|
84
109
|
|
|
85
110
|
type Source = 'env' | 'config' | 'custom' | 'api';
|
|
86
111
|
|
|
@@ -224,8 +249,14 @@ export namespace Provider {
|
|
|
224
249
|
};
|
|
225
250
|
},
|
|
226
251
|
azure: async () => {
|
|
252
|
+
// `agent auth login azure` with Microsoft Entra ID stores no secret:
|
|
253
|
+
// requests mint bearer tokens from the Azure CLI session (#334).
|
|
254
|
+
const entra = await AzureLogin.options(await Auth.get('azure'));
|
|
255
|
+
if (entra) {
|
|
256
|
+
log.info(() => ({ message: 'using azure entra id credentials' }));
|
|
257
|
+
}
|
|
227
258
|
return {
|
|
228
|
-
autoload:
|
|
259
|
+
autoload: !!entra,
|
|
229
260
|
async getModel(
|
|
230
261
|
sdk: any,
|
|
231
262
|
modelID: string,
|
|
@@ -237,7 +268,7 @@ export namespace Provider {
|
|
|
237
268
|
return sdk.responses(modelID);
|
|
238
269
|
}
|
|
239
270
|
},
|
|
240
|
-
options: {},
|
|
271
|
+
options: entra ?? {},
|
|
241
272
|
};
|
|
242
273
|
},
|
|
243
274
|
'azure-cognitive-services': async () => {
|
|
@@ -263,103 +294,6 @@ export namespace Provider {
|
|
|
263
294
|
},
|
|
264
295
|
};
|
|
265
296
|
},
|
|
266
|
-
'amazon-bedrock': async () => {
|
|
267
|
-
if (
|
|
268
|
-
!process.env['AWS_PROFILE'] &&
|
|
269
|
-
!process.env['AWS_ACCESS_KEY_ID'] &&
|
|
270
|
-
!process.env['AWS_BEARER_TOKEN_BEDROCK']
|
|
271
|
-
)
|
|
272
|
-
return { autoload: false };
|
|
273
|
-
|
|
274
|
-
const region = process.env['AWS_REGION'] ?? 'us-east-1';
|
|
275
|
-
|
|
276
|
-
const { fromNodeProviderChain } = await import(
|
|
277
|
-
await BunProc.install('@aws-sdk/credential-providers')
|
|
278
|
-
);
|
|
279
|
-
return {
|
|
280
|
-
autoload: true,
|
|
281
|
-
options: {
|
|
282
|
-
region,
|
|
283
|
-
credentialProvider: fromNodeProviderChain(),
|
|
284
|
-
},
|
|
285
|
-
async getModel(
|
|
286
|
-
sdk: any,
|
|
287
|
-
modelID: string,
|
|
288
|
-
_options?: Record<string, any>
|
|
289
|
-
) {
|
|
290
|
-
let regionPrefix = region.split('-')[0];
|
|
291
|
-
|
|
292
|
-
switch (regionPrefix) {
|
|
293
|
-
case 'us': {
|
|
294
|
-
const modelRequiresPrefix = [
|
|
295
|
-
'nova-micro',
|
|
296
|
-
'nova-lite',
|
|
297
|
-
'nova-pro',
|
|
298
|
-
'nova-premier',
|
|
299
|
-
'claude',
|
|
300
|
-
'deepseek',
|
|
301
|
-
].some((m) => modelID.includes(m));
|
|
302
|
-
const isGovCloud = region.startsWith('us-gov');
|
|
303
|
-
if (modelRequiresPrefix && !isGovCloud) {
|
|
304
|
-
modelID = `${regionPrefix}.${modelID}`;
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
case 'eu': {
|
|
309
|
-
const regionRequiresPrefix = [
|
|
310
|
-
'eu-west-1',
|
|
311
|
-
'eu-west-2',
|
|
312
|
-
'eu-west-3',
|
|
313
|
-
'eu-north-1',
|
|
314
|
-
'eu-central-1',
|
|
315
|
-
'eu-south-1',
|
|
316
|
-
'eu-south-2',
|
|
317
|
-
].some((r) => region.includes(r));
|
|
318
|
-
const modelRequiresPrefix = [
|
|
319
|
-
'claude',
|
|
320
|
-
'nova-lite',
|
|
321
|
-
'nova-micro',
|
|
322
|
-
'llama3',
|
|
323
|
-
'pixtral',
|
|
324
|
-
].some((m) => modelID.includes(m));
|
|
325
|
-
if (regionRequiresPrefix && modelRequiresPrefix) {
|
|
326
|
-
modelID = `${regionPrefix}.${modelID}`;
|
|
327
|
-
}
|
|
328
|
-
break;
|
|
329
|
-
}
|
|
330
|
-
case 'ap': {
|
|
331
|
-
const isAustraliaRegion = [
|
|
332
|
-
'ap-southeast-2',
|
|
333
|
-
'ap-southeast-4',
|
|
334
|
-
].includes(region);
|
|
335
|
-
if (
|
|
336
|
-
isAustraliaRegion &&
|
|
337
|
-
['anthropic.claude-sonnet-4-5', 'anthropic.claude-haiku'].some(
|
|
338
|
-
(m) => modelID.includes(m)
|
|
339
|
-
)
|
|
340
|
-
) {
|
|
341
|
-
regionPrefix = 'au';
|
|
342
|
-
modelID = `${regionPrefix}.${modelID}`;
|
|
343
|
-
} else {
|
|
344
|
-
const modelRequiresPrefix = [
|
|
345
|
-
'claude',
|
|
346
|
-
'nova-lite',
|
|
347
|
-
'nova-micro',
|
|
348
|
-
'nova-pro',
|
|
349
|
-
].some((m) => modelID.includes(m));
|
|
350
|
-
if (modelRequiresPrefix) {
|
|
351
|
-
regionPrefix = 'apac';
|
|
352
|
-
modelID = `${regionPrefix}.${modelID}`;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
break;
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return sdk.languageModel(modelID);
|
|
360
|
-
},
|
|
361
|
-
};
|
|
362
|
-
},
|
|
363
297
|
openrouter: async () => {
|
|
364
298
|
return {
|
|
365
299
|
autoload: false,
|
|
@@ -382,52 +316,6 @@ export namespace Provider {
|
|
|
382
316
|
},
|
|
383
317
|
};
|
|
384
318
|
},
|
|
385
|
-
'google-vertex': async () => {
|
|
386
|
-
const project =
|
|
387
|
-
process.env['GOOGLE_CLOUD_PROJECT'] ??
|
|
388
|
-
process.env['GCP_PROJECT'] ??
|
|
389
|
-
process.env['GCLOUD_PROJECT'];
|
|
390
|
-
const location =
|
|
391
|
-
process.env['GOOGLE_CLOUD_LOCATION'] ??
|
|
392
|
-
process.env['VERTEX_LOCATION'] ??
|
|
393
|
-
'us-east5';
|
|
394
|
-
const autoload = Boolean(project);
|
|
395
|
-
if (!autoload) return { autoload: false };
|
|
396
|
-
return {
|
|
397
|
-
autoload: true,
|
|
398
|
-
options: {
|
|
399
|
-
project,
|
|
400
|
-
location,
|
|
401
|
-
},
|
|
402
|
-
async getModel(sdk: any, modelID: string) {
|
|
403
|
-
const id = String(modelID).trim();
|
|
404
|
-
return sdk.languageModel(id);
|
|
405
|
-
},
|
|
406
|
-
};
|
|
407
|
-
},
|
|
408
|
-
'google-vertex-anthropic': async () => {
|
|
409
|
-
const project =
|
|
410
|
-
process.env['GOOGLE_CLOUD_PROJECT'] ??
|
|
411
|
-
process.env['GCP_PROJECT'] ??
|
|
412
|
-
process.env['GCLOUD_PROJECT'];
|
|
413
|
-
const location =
|
|
414
|
-
process.env['GOOGLE_CLOUD_LOCATION'] ??
|
|
415
|
-
process.env['VERTEX_LOCATION'] ??
|
|
416
|
-
'global';
|
|
417
|
-
const autoload = Boolean(project);
|
|
418
|
-
if (!autoload) return { autoload: false };
|
|
419
|
-
return {
|
|
420
|
-
autoload: true,
|
|
421
|
-
options: {
|
|
422
|
-
project,
|
|
423
|
-
location,
|
|
424
|
-
},
|
|
425
|
-
async getModel(sdk: any, modelID: string) {
|
|
426
|
-
const id = String(modelID).trim();
|
|
427
|
-
return sdk.languageModel(id);
|
|
428
|
-
},
|
|
429
|
-
};
|
|
430
|
-
},
|
|
431
319
|
zenmux: async () => {
|
|
432
320
|
return {
|
|
433
321
|
autoload: false,
|
|
@@ -764,6 +652,9 @@ export namespace Provider {
|
|
|
764
652
|
options: {},
|
|
765
653
|
};
|
|
766
654
|
},
|
|
655
|
+
// OpenCode parity: Cloudflare, Snowflake, SAP AI Core, Vertex, Bedrock,
|
|
656
|
+
// attribution headers (#334).
|
|
657
|
+
...ParityLoaders.create(),
|
|
767
658
|
};
|
|
768
659
|
|
|
769
660
|
export const state = Instance.state(async () => {
|
|
@@ -780,6 +671,7 @@ export namespace Provider {
|
|
|
780
671
|
modelID: string,
|
|
781
672
|
options?: Record<string, any>
|
|
782
673
|
) => Promise<any>;
|
|
674
|
+
sdkOptions?: ParityLoaders.LoaderResult['sdkOptions'];
|
|
783
675
|
options: Record<string, any>;
|
|
784
676
|
};
|
|
785
677
|
} = {};
|
|
@@ -807,24 +699,29 @@ export namespace Provider {
|
|
|
807
699
|
sdk: any,
|
|
808
700
|
modelID: string,
|
|
809
701
|
options?: Record<string, any>
|
|
810
|
-
) => Promise<any
|
|
702
|
+
) => Promise<any>,
|
|
703
|
+
sdkOptions?: ParityLoaders.LoaderResult['sdkOptions']
|
|
811
704
|
) {
|
|
812
705
|
const provider = providers[id];
|
|
813
706
|
if (!provider) {
|
|
814
707
|
const info = database[id];
|
|
815
708
|
if (!info) return;
|
|
816
|
-
|
|
709
|
+
// models.dev base URLs may contain `${ENV}` placeholders (#334).
|
|
710
|
+
const baseURL = EnvTemplate.resolve(info.api);
|
|
711
|
+
if (baseURL && !options['baseURL']) options['baseURL'] = baseURL;
|
|
817
712
|
providers[id] = {
|
|
818
713
|
source,
|
|
819
714
|
info,
|
|
820
715
|
options,
|
|
821
716
|
getModel,
|
|
717
|
+
sdkOptions,
|
|
822
718
|
};
|
|
823
719
|
return;
|
|
824
720
|
}
|
|
825
721
|
provider.options = mergeDeep(provider.options, options);
|
|
826
722
|
provider.source = source;
|
|
827
723
|
provider.getModel = getModel ?? provider.getModel;
|
|
724
|
+
provider.sdkOptions = sdkOptions ?? provider.sdkOptions;
|
|
828
725
|
}
|
|
829
726
|
|
|
830
727
|
const configProviders = Object.entries(config.provider ?? {});
|
|
@@ -939,15 +836,30 @@ export namespace Provider {
|
|
|
939
836
|
}
|
|
940
837
|
}
|
|
941
838
|
|
|
839
|
+
// Kilo keeps its curated free aliases and gains the rest of its
|
|
840
|
+
// models.dev catalogue (text models only), served through the same
|
|
841
|
+
// OpenRouter-compatible endpoint (#334).
|
|
942
842
|
database.kilo = {
|
|
943
843
|
id: 'kilo',
|
|
944
844
|
name: 'Kilo Gateway',
|
|
945
845
|
npm: '@openrouter/ai-sdk-provider',
|
|
946
846
|
api: 'https://api.kilo.ai/api/openrouter',
|
|
947
847
|
env: ['KILO_API_KEY'],
|
|
948
|
-
models:
|
|
848
|
+
models: {
|
|
849
|
+
...Kilo.catalogueModels(database.kilo),
|
|
850
|
+
...structuredClone(Kilo.FREE_MODELS),
|
|
851
|
+
},
|
|
949
852
|
};
|
|
950
853
|
|
|
854
|
+
// Providers models.dev does not list (gpt4free, Pollinations, local
|
|
855
|
+
// servers) and packages the bundled AI SDK cannot load (#334).
|
|
856
|
+
ExtraProviders.augment(database);
|
|
857
|
+
ParityLoaders.patchCloudflareGateway(
|
|
858
|
+
database,
|
|
859
|
+
process.env,
|
|
860
|
+
config.provider?.['cloudflare-ai-gateway']?.options?.['baseURL']
|
|
861
|
+
);
|
|
862
|
+
|
|
951
863
|
for (const [providerID, provider] of configProviders) {
|
|
952
864
|
const existing = database[providerID];
|
|
953
865
|
const parsed: ModelsDev.Provider = {
|
|
@@ -1030,8 +942,9 @@ export namespace Provider {
|
|
|
1030
942
|
for (const [providerID, provider] of Object.entries(database)) {
|
|
1031
943
|
if (disabled.has(providerID)) continue;
|
|
1032
944
|
// Find the first truthy env var (supports multiple env var options like Google's
|
|
1033
|
-
// GOOGLE_GENERATIVE_AI_API_KEY and GEMINI_API_KEY)
|
|
1034
|
-
|
|
945
|
+
// GOOGLE_GENERATIVE_AI_API_KEY and GEMINI_API_KEY). Variables that only
|
|
946
|
+
// fill a templated URL or name an account/region are not keys (#334).
|
|
947
|
+
const apiKey = EnvTemplate.credentialEnv(provider)
|
|
1035
948
|
.map((item) => process.env[item])
|
|
1036
949
|
.find(Boolean);
|
|
1037
950
|
if (!apiKey) continue;
|
|
@@ -1044,7 +957,31 @@ export namespace Provider {
|
|
|
1044
957
|
if (disabled.has(providerID)) continue;
|
|
1045
958
|
if (provider.type === 'api') {
|
|
1046
959
|
mergeProvider(providerID, { apiKey: provider.key }, 'api');
|
|
960
|
+
continue;
|
|
1047
961
|
}
|
|
962
|
+
// Subscription logins without a dedicated custom loader (xAI,
|
|
963
|
+
// DigitalOcean, GitLab) resolve to a key or fetch through their plugin (#334).
|
|
964
|
+
if (provider.type !== 'oauth' || CUSTOM_LOADERS[providerID]) continue;
|
|
965
|
+
if (!database[providerID]) continue;
|
|
966
|
+
const loaderFn = await AuthPlugins.getLoader(providerID);
|
|
967
|
+
if (!loaderFn) continue;
|
|
968
|
+
const result = await loaderFn(
|
|
969
|
+
() => Auth.get(providerID),
|
|
970
|
+
database[providerID]
|
|
971
|
+
).catch((error) => {
|
|
972
|
+
log.warn(() => ({
|
|
973
|
+
message: 'oauth credentials could not be loaded',
|
|
974
|
+
providerID,
|
|
975
|
+
error: String(error),
|
|
976
|
+
}));
|
|
977
|
+
return undefined;
|
|
978
|
+
});
|
|
979
|
+
if (!result?.apiKey && !result?.fetch) continue;
|
|
980
|
+
const options: Record<string, any> = { ...result.options };
|
|
981
|
+
if (result.apiKey) options['apiKey'] = result.apiKey;
|
|
982
|
+
if (result.baseURL) options['baseURL'] = result.baseURL;
|
|
983
|
+
if (result.fetch) options['fetch'] = result.fetch;
|
|
984
|
+
mergeProvider(providerID, options, 'api');
|
|
1048
985
|
}
|
|
1049
986
|
|
|
1050
987
|
// load custom
|
|
@@ -1056,7 +993,8 @@ export namespace Provider {
|
|
|
1056
993
|
providerID,
|
|
1057
994
|
result.options ?? {},
|
|
1058
995
|
'custom',
|
|
1059
|
-
result.getModel
|
|
996
|
+
result.getModel,
|
|
997
|
+
result.sdkOptions
|
|
1060
998
|
);
|
|
1061
999
|
}
|
|
1062
1000
|
}
|
|
@@ -1095,11 +1033,40 @@ export namespace Provider {
|
|
|
1095
1033
|
available: Object.keys(providers),
|
|
1096
1034
|
}));
|
|
1097
1035
|
|
|
1036
|
+
/**
|
|
1037
|
+
* Enable a keyless provider (gpt4free, Pollinations, local servers) on
|
|
1038
|
+
* first explicit use. They stay out of `providers` until then, so they
|
|
1039
|
+
* never take part in default or short-name resolution (#334).
|
|
1040
|
+
*/
|
|
1041
|
+
async function activate(providerID: string): Promise<boolean> {
|
|
1042
|
+
if (providers[providerID]) return true;
|
|
1043
|
+
if (disabled.has(providerID)) return false;
|
|
1044
|
+
if (!database[providerID] || !ExtraProviders.isKeyless(providerID)) {
|
|
1045
|
+
return false;
|
|
1046
|
+
}
|
|
1047
|
+
const loader = CUSTOM_LOADERS[providerID];
|
|
1048
|
+
const result = loader ? await loader(database[providerID]) : undefined;
|
|
1049
|
+
mergeProvider(
|
|
1050
|
+
providerID,
|
|
1051
|
+
result?.options ?? {},
|
|
1052
|
+
'custom',
|
|
1053
|
+
result?.getModel,
|
|
1054
|
+
result?.sdkOptions
|
|
1055
|
+
);
|
|
1056
|
+
registryLog.info(() => ({
|
|
1057
|
+
message: 'keyless provider enabled on explicit request',
|
|
1058
|
+
provider: providerID,
|
|
1059
|
+
}));
|
|
1060
|
+
return Boolean(providers[providerID]);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1098
1063
|
return {
|
|
1099
1064
|
models,
|
|
1100
1065
|
providers,
|
|
1101
1066
|
sdk,
|
|
1102
1067
|
realIdByKey,
|
|
1068
|
+
database,
|
|
1069
|
+
activate,
|
|
1103
1070
|
};
|
|
1104
1071
|
});
|
|
1105
1072
|
|
|
@@ -1114,7 +1081,38 @@ export namespace Provider {
|
|
|
1114
1081
|
});
|
|
1115
1082
|
const s = await state();
|
|
1116
1083
|
const pkg = model.provider?.npm ?? provider.npm ?? provider.id;
|
|
1117
|
-
const
|
|
1084
|
+
const entry = s.providers[provider.id];
|
|
1085
|
+
let options: Record<string, any> = { ...entry?.options };
|
|
1086
|
+
// Per-model endpoint from models.dev (Vertex partner models, Bedrock
|
|
1087
|
+
// Mantle, Cloudflare passthrough routes). A user-set baseURL still wins
|
|
1088
|
+
// for models served by the provider's own package (#334).
|
|
1089
|
+
const providerURL = EnvTemplate.resolve(provider.api);
|
|
1090
|
+
const modelURL = EnvTemplate.resolve(model.provider?.api);
|
|
1091
|
+
const ownPackage = !model.provider?.npm || pkg === provider.npm;
|
|
1092
|
+
if (
|
|
1093
|
+
modelURL &&
|
|
1094
|
+
/^https?:\/\//.test(modelURL) &&
|
|
1095
|
+
(!ownPackage ||
|
|
1096
|
+
!options['baseURL'] ||
|
|
1097
|
+
options['baseURL'] === providerURL)
|
|
1098
|
+
) {
|
|
1099
|
+
options['baseURL'] = modelURL;
|
|
1100
|
+
}
|
|
1101
|
+
const template = model.provider?.api ?? provider.api;
|
|
1102
|
+
if (
|
|
1103
|
+
!options['baseURL'] &&
|
|
1104
|
+
EnvTemplate.isTemplate(template) &&
|
|
1105
|
+
!EnvTemplate.resolve(template)
|
|
1106
|
+
) {
|
|
1107
|
+
const missing = EnvTemplate.variables(template).filter(
|
|
1108
|
+
(name) => !EnvTemplate.resolve(`\${${name}}`)
|
|
1109
|
+
);
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Provider "${provider.id}" needs ${missing.join(', ')} to build its base URL (${template}). ` +
|
|
1112
|
+
`Set the environment variable(s) or provider.${provider.id}.options.baseURL in the config.`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
if (entry?.sdkOptions) options = entry.sdkOptions(pkg, options);
|
|
1118
1116
|
if (
|
|
1119
1117
|
pkg.includes('@ai-sdk/openai-compatible') &&
|
|
1120
1118
|
options['includeUsage'] === undefined
|
|
@@ -1203,19 +1201,23 @@ export namespace Provider {
|
|
|
1203
1201
|
const [sdkStream, usageStream] = response.body.tee();
|
|
1204
1202
|
const sseReqId = SSEUsageExtractor.nextRequestId();
|
|
1205
1203
|
(async () => {
|
|
1204
|
+
const reader = usageStream.getReader();
|
|
1206
1205
|
try {
|
|
1207
|
-
|
|
1206
|
+
// Read the whole branch: a tee branch that stops being read
|
|
1207
|
+
// keeps buffering the rest of the response (issue #335).
|
|
1208
1208
|
const decoder = new TextDecoder();
|
|
1209
|
-
|
|
1209
|
+
const tracker = SSEUsageExtractor.createTracker();
|
|
1210
1210
|
while (true) {
|
|
1211
1211
|
const { done, value } = await reader.read();
|
|
1212
1212
|
if (done) break;
|
|
1213
|
-
|
|
1214
|
-
if (body.length > 50000) break;
|
|
1213
|
+
tracker.push(decoder.decode(value, { stream: true }));
|
|
1215
1214
|
}
|
|
1216
|
-
|
|
1215
|
+
tracker.push(decoder.decode());
|
|
1216
|
+
const usage = tracker.finish();
|
|
1217
|
+
if (usage) SSEUsageExtractor.storeUsage(sseReqId, usage);
|
|
1217
1218
|
} catch {
|
|
1218
|
-
// Never break the SDK stream
|
|
1219
|
+
// Never break the SDK stream; release the branch instead.
|
|
1220
|
+
reader.cancel().catch(() => {});
|
|
1219
1221
|
}
|
|
1220
1222
|
})();
|
|
1221
1223
|
return new Response(sdkStream, {
|
|
@@ -1241,12 +1243,14 @@ export namespace Provider {
|
|
|
1241
1243
|
});
|
|
1242
1244
|
}
|
|
1243
1245
|
|
|
1244
|
-
|
|
1246
|
+
// Logged only; query-string credentials are masked (issue #335).
|
|
1247
|
+
const url = sanitizeUrl(
|
|
1245
1248
|
typeof input === 'string'
|
|
1246
1249
|
? input
|
|
1247
1250
|
: input instanceof URL
|
|
1248
1251
|
? input.toString()
|
|
1249
|
-
: input.url
|
|
1252
|
+
: input.url
|
|
1253
|
+
);
|
|
1250
1254
|
const method = init?.method ?? 'GET';
|
|
1251
1255
|
|
|
1252
1256
|
// Wrap all verbose logging in try/catch so it never breaks the actual HTTP request.
|
|
@@ -1373,13 +1377,15 @@ export namespace Provider {
|
|
|
1373
1377
|
try {
|
|
1374
1378
|
const reader = logStream.getReader();
|
|
1375
1379
|
const decoder = new TextDecoder();
|
|
1380
|
+
const tracker = SSEUsageExtractor.createTracker();
|
|
1376
1381
|
let bodyPreview = '';
|
|
1377
1382
|
let truncated = false;
|
|
1378
1383
|
while (true) {
|
|
1379
1384
|
const { done, value } = await reader.read();
|
|
1380
1385
|
if (done) break;
|
|
1386
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
1387
|
+
tracker.push(chunk);
|
|
1381
1388
|
if (!truncated) {
|
|
1382
|
-
const chunk = decoder.decode(value, { stream: true });
|
|
1383
1389
|
bodyPreview += chunk;
|
|
1384
1390
|
if (bodyPreview.length > responseBodyMaxChars) {
|
|
1385
1391
|
bodyPreview = bodyPreview.slice(
|
|
@@ -1390,11 +1396,12 @@ export namespace Provider {
|
|
|
1390
1396
|
}
|
|
1391
1397
|
}
|
|
1392
1398
|
}
|
|
1393
|
-
// Extract usage from raw SSE stream
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
)
|
|
1399
|
+
// Extract usage from the whole raw SSE stream (not just
|
|
1400
|
+
// the logged preview) as AI SDK fallback.
|
|
1401
|
+
tracker.push(decoder.decode());
|
|
1402
|
+
const usage = tracker.finish();
|
|
1403
|
+
if (usage)
|
|
1404
|
+
SSEUsageExtractor.storeUsage(sseRequestId, usage);
|
|
1398
1405
|
// Use direct (non-lazy) logging for stream body
|
|
1399
1406
|
// See: https://github.com/link-assistant/agent/issues/211
|
|
1400
1407
|
log.info('HTTP response body (stream)', {
|
|
@@ -1517,17 +1524,9 @@ export namespace Provider {
|
|
|
1517
1524
|
installedPath = pkg;
|
|
1518
1525
|
}
|
|
1519
1526
|
|
|
1520
|
-
//
|
|
1521
|
-
//
|
|
1522
|
-
|
|
1523
|
-
// Ref: https://ai-sdk.dev/providers/ai-sdk-providers/google-vertex#google-vertex-anthropic-provider-usage
|
|
1524
|
-
// In addition, Bun's dynamic import logic does not support subpath imports,
|
|
1525
|
-
// so we patch the import path to load directly from `dist`.
|
|
1526
|
-
const modPath =
|
|
1527
|
-
provider.id === 'google-vertex-anthropic'
|
|
1528
|
-
? `${installedPath}/dist/anthropic/index.mjs`
|
|
1529
|
-
: installedPath;
|
|
1530
|
-
const mod = await import(modPath);
|
|
1527
|
+
// `@ai-sdk/google-vertex/anthropic` and other subpath packages are
|
|
1528
|
+
// bundled above, so no dist path patching is needed here (#334).
|
|
1529
|
+
const mod = await import(installedPath);
|
|
1531
1530
|
|
|
1532
1531
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith('create'))!];
|
|
1533
1532
|
const loaded = fn({
|
|
@@ -1550,27 +1549,98 @@ export namespace Provider {
|
|
|
1550
1549
|
}
|
|
1551
1550
|
|
|
1552
1551
|
export async function getProvider(providerID: string) {
|
|
1553
|
-
|
|
1552
|
+
const s = await state();
|
|
1553
|
+
const id = ExtraProviders.canonicalID(providerID);
|
|
1554
|
+
if (!s.providers[id]) await s.activate(id);
|
|
1555
|
+
return s.providers[id];
|
|
1554
1556
|
}
|
|
1555
1557
|
|
|
1556
1558
|
/**
|
|
1557
1559
|
* Whether a provider's catalog lists a model. Looks at that one provider
|
|
1558
|
-
* only and logs nothing (#313).
|
|
1560
|
+
* only and logs nothing (#313). Routers and local servers that accept any
|
|
1561
|
+
* model ID count as listing every model (#334).
|
|
1559
1562
|
*/
|
|
1560
1563
|
export async function hasModel(providerID: string, modelID: string) {
|
|
1564
|
+
const s = await state();
|
|
1565
|
+
const provider = s.providers[ExtraProviders.canonicalID(providerID)];
|
|
1566
|
+
if (!provider) return false;
|
|
1567
|
+
if (provider.info.models[modelID]) return true;
|
|
1568
|
+
// Deprecated Kilo aliases still resolve to their replacement (#335).
|
|
1569
|
+
const replacement =
|
|
1570
|
+
provider.info.id === 'kilo'
|
|
1571
|
+
? Kilo.deprecatedAliasTarget(modelID)
|
|
1572
|
+
: undefined;
|
|
1573
|
+
if (replacement && provider.info.models[replacement]) return true;
|
|
1574
|
+
return ExtraProviders.allowsUnknownModels(provider.info.id);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* List the models an active provider serves right now, using its
|
|
1579
|
+
* token-free listing endpoint (`/models`). `force` bypasses the cache.
|
|
1580
|
+
*/
|
|
1581
|
+
export async function discoverModels(
|
|
1582
|
+
providerID: string,
|
|
1583
|
+
input: { force?: boolean } = {}
|
|
1584
|
+
): Promise<ModelDiscovery.DiscoveryResult> {
|
|
1561
1585
|
const provider = await getProvider(providerID);
|
|
1562
|
-
|
|
1586
|
+
if (!provider) {
|
|
1587
|
+
return { status: 'skipped', models: [], error: 'provider not active' };
|
|
1588
|
+
}
|
|
1589
|
+
const options = provider.options ?? {};
|
|
1590
|
+
return ModelDiscovery.discover({
|
|
1591
|
+
providerID: provider.info.id,
|
|
1592
|
+
npm: provider.info.npm,
|
|
1593
|
+
baseURL: options['baseURL'] ?? EnvTemplate.resolve(provider.info.api),
|
|
1594
|
+
apiKey:
|
|
1595
|
+
typeof options['apiKey'] === 'string' ? options['apiKey'] : undefined,
|
|
1596
|
+
headers: options['headers'],
|
|
1597
|
+
resourceName:
|
|
1598
|
+
options['resourceName'] ??
|
|
1599
|
+
(provider.info.id === 'azure'
|
|
1600
|
+
? process.env['AZURE_RESOURCE_NAME']
|
|
1601
|
+
: undefined),
|
|
1602
|
+
// Providers that sign requests themselves (OAuth) list models the same way.
|
|
1603
|
+
fetchImpl:
|
|
1604
|
+
typeof options['fetch'] === 'function' ? options['fetch'] : undefined,
|
|
1605
|
+
force: input.force,
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* Look a model up in the provider's own model listing. OpenCode Zen keeps
|
|
1611
|
+
* its dedicated free-model lookup; every other provider with a token-free
|
|
1612
|
+
* listing endpoint goes through `ModelDiscovery` (#334).
|
|
1613
|
+
*/
|
|
1614
|
+
async function getLiveModelInfo(
|
|
1615
|
+
providerID: string,
|
|
1616
|
+
modelID: string
|
|
1617
|
+
): Promise<ModelsDev.Model | undefined> {
|
|
1618
|
+
if (providerID === 'opencode') {
|
|
1619
|
+
return OpenCodeZen.getLiveFreeModelInfo(modelID);
|
|
1620
|
+
}
|
|
1621
|
+
const s = await state();
|
|
1622
|
+
const result = await discoverModels(providerID);
|
|
1623
|
+
const found = ModelDiscovery.findModel(result.models, modelID);
|
|
1624
|
+
if (!found) return undefined;
|
|
1625
|
+
return ModelDiscovery.toModelInfo(found, {
|
|
1626
|
+
metadata: ModelDiscovery.catalogueMetadata(
|
|
1627
|
+
s.database,
|
|
1628
|
+
found.model ?? found.id,
|
|
1629
|
+
providerID
|
|
1630
|
+
),
|
|
1631
|
+
});
|
|
1563
1632
|
}
|
|
1564
1633
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return
|
|
1634
|
+
/** Metadata for a model ID a router or local server accepts unlisted. */
|
|
1635
|
+
function passthroughModelInfo(modelID: string): ModelsDev.Model {
|
|
1636
|
+
return ModelDiscovery.toModelInfo({ id: modelID, free: true });
|
|
1568
1637
|
}
|
|
1569
1638
|
|
|
1570
1639
|
export async function refreshLiveModelInfo(
|
|
1571
1640
|
providerID: string,
|
|
1572
1641
|
modelID: string
|
|
1573
1642
|
) {
|
|
1643
|
+
providerID = ExtraProviders.canonicalID(providerID);
|
|
1574
1644
|
const info = await getLiveModelInfo(providerID, modelID);
|
|
1575
1645
|
if (!info) return undefined;
|
|
1576
1646
|
|
|
@@ -1588,12 +1658,16 @@ export namespace Provider {
|
|
|
1588
1658
|
}
|
|
1589
1659
|
|
|
1590
1660
|
export async function getModel(providerID: string, modelID: string) {
|
|
1661
|
+
providerID = ExtraProviders.canonicalID(providerID);
|
|
1662
|
+
// Sessions and configs may still name a withdrawn Kilo alias (#335).
|
|
1663
|
+
if (providerID === 'kilo') modelID = resolveKiloDeprecatedAlias(modelID);
|
|
1591
1664
|
const key = `${providerID}/${modelID}`;
|
|
1592
1665
|
const s = await state();
|
|
1593
1666
|
if (s.models.has(key)) return s.models.get(key)!;
|
|
1594
1667
|
|
|
1595
1668
|
log.info(() => ({ message: 'getModel', providerID, modelID }));
|
|
1596
1669
|
|
|
1670
|
+
if (!s.providers[providerID]) await s.activate(providerID);
|
|
1597
1671
|
const provider = s.providers[providerID];
|
|
1598
1672
|
if (!provider) {
|
|
1599
1673
|
// Check if this model ID might exist in another provider (e.g., OpenRouter)
|
|
@@ -1680,6 +1754,22 @@ export namespace Provider {
|
|
|
1680
1754
|
}
|
|
1681
1755
|
}
|
|
1682
1756
|
|
|
1757
|
+
if (
|
|
1758
|
+
!isSyntheticProvider &&
|
|
1759
|
+
!info &&
|
|
1760
|
+
ExtraProviders.allowsUnknownModels(providerID)
|
|
1761
|
+
) {
|
|
1762
|
+
// Routers (gpt4free, Pollinations) and local servers accept model IDs
|
|
1763
|
+
// that are not listed anywhere; pass the ID through as-is (#334).
|
|
1764
|
+
info = passthroughModelInfo(modelID);
|
|
1765
|
+
provider.info.models[modelID] = info;
|
|
1766
|
+
log.info(() => ({
|
|
1767
|
+
message: 'passing unlisted model through to router/local server',
|
|
1768
|
+
providerID,
|
|
1769
|
+
modelID,
|
|
1770
|
+
}));
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1683
1773
|
if (!isSyntheticProvider && !info) {
|
|
1684
1774
|
// Model not found even after cache refresh.
|
|
1685
1775
|
// Check if this is the default model — if so, create synthetic info and proceed (#239).
|
|
@@ -1739,9 +1829,13 @@ export namespace Provider {
|
|
|
1739
1829
|
} else {
|
|
1740
1830
|
// For regular providers, load the SDK first
|
|
1741
1831
|
const sdk = await getSDK(provider.info, info!);
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1832
|
+
// Models that need a specific API shape (Bedrock Mantle, models.dev
|
|
1833
|
+
// `provider.shape`) pick the SDK method themselves (#334).
|
|
1834
|
+
language =
|
|
1835
|
+
ParityLoaders.selectByShape(sdk, info!, realID) ??
|
|
1836
|
+
(provider.getModel
|
|
1837
|
+
? await provider.getModel(sdk, realID, provider.options)
|
|
1838
|
+
: sdk.languageModel(realID));
|
|
1745
1839
|
}
|
|
1746
1840
|
log.info(() => ({ message: 'found', providerID, modelID }));
|
|
1747
1841
|
s.models.set(key, {
|
|
@@ -1835,7 +1929,6 @@ export namespace Provider {
|
|
|
1835
1929
|
'gpt-5-nano',
|
|
1836
1930
|
'nemotron-3.5-lightning-free',
|
|
1837
1931
|
'nemotron-3-ultra-free',
|
|
1838
|
-
'nemotron-3-super-free',
|
|
1839
1932
|
'gpt-5',
|
|
1840
1933
|
'claude-sonnet-4',
|
|
1841
1934
|
'gemini-3-pro',
|
|
@@ -1909,10 +2002,14 @@ export namespace Provider {
|
|
|
1909
2002
|
}
|
|
1910
2003
|
|
|
1911
2004
|
export function parseModel(model: string) {
|
|
1912
|
-
const [
|
|
2005
|
+
const [rawProviderID, ...rest] = model.split('/');
|
|
2006
|
+
const providerID = ExtraProviders.canonicalID(rawProviderID);
|
|
2007
|
+
const modelID = rest.join('/');
|
|
1913
2008
|
return {
|
|
1914
|
-
providerID
|
|
1915
|
-
|
|
2009
|
+
providerID,
|
|
2010
|
+
// Withdrawn Kilo aliases keep working with a warning (#335).
|
|
2011
|
+
modelID:
|
|
2012
|
+
providerID === 'kilo' ? resolveKiloDeprecatedAlias(modelID) : modelID,
|
|
1916
2013
|
};
|
|
1917
2014
|
}
|
|
1918
2015
|
|
|
@@ -1932,6 +2029,14 @@ export namespace Provider {
|
|
|
1932
2029
|
): Promise<{ providerID: string; modelID: string } | undefined> {
|
|
1933
2030
|
const s = await state();
|
|
1934
2031
|
|
|
2032
|
+
// A withdrawn built-in alias resolves to its Kilo replacement (#335).
|
|
2033
|
+
if (Kilo.deprecatedAliasTarget(modelID)) {
|
|
2034
|
+
const replacement = resolveKiloDeprecatedAlias(modelID);
|
|
2035
|
+
if (s.providers.kilo?.info.models[replacement]) {
|
|
2036
|
+
return { providerID: 'kilo', modelID: replacement };
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
|
|
1935
2040
|
if (Object.hasOwn(Kilo.FREE_MODELS, modelID)) {
|
|
1936
2041
|
const kiloProvider = s.providers.kilo;
|
|
1937
2042
|
if (kiloProvider?.info.models[modelID]) {
|
|
@@ -2005,8 +2110,9 @@ export namespace Provider {
|
|
|
2005
2110
|
* or a clear error explaining why it's not available.
|
|
2006
2111
|
*
|
|
2007
2112
|
* Examples:
|
|
2008
|
-
* - "kilo/nemotron-3-
|
|
2009
|
-
* - "nemotron-3-
|
|
2113
|
+
* - "kilo/nemotron-3-ultra-free" -> { providerID: "kilo", modelID: "nemotron-3-ultra-free" }
|
|
2114
|
+
* - "nemotron-3-ultra-free" -> { providerID: "kilo", modelID: "nemotron-3-ultra-free" } (resolved)
|
|
2115
|
+
* - "nemotron-3-super-free" -> { providerID: "kilo", modelID: "nemotron-3-ultra-free" } (deprecated alias, warns)
|
|
2010
2116
|
* - "big-pickle" -> { providerID: "opencode", modelID: "big-pickle" } (resolved)
|
|
2011
2117
|
* - "nonexistent-model" -> throws ModelNotFoundError
|
|
2012
2118
|
*
|
|
@@ -2067,7 +2173,7 @@ export namespace Provider {
|
|
|
2067
2173
|
* When one provider hits rate limits, the system can try an alternative.
|
|
2068
2174
|
*
|
|
2069
2175
|
* Note: This is only used for models without explicit provider specification.
|
|
2070
|
-
* If user specifies "kilo/nemotron-3-
|
|
2176
|
+
* If user specifies "kilo/nemotron-3-ultra-free", no fallback will occur.
|
|
2071
2177
|
*/
|
|
2072
2178
|
const SHARED_FREE_MODELS: Record<string, string[]> = {
|
|
2073
2179
|
'nemotron-3.5-lightning-free': ['kilo', 'opencode'],
|
|
@@ -2078,7 +2184,7 @@ export namespace Provider {
|
|
|
2078
2184
|
* This function returns a list of alternative providers that offer the same model.
|
|
2079
2185
|
*
|
|
2080
2186
|
* Note: This only returns alternatives for models without explicit provider specification.
|
|
2081
|
-
* If the original request had an explicit provider (like "kilo/nemotron-3-
|
|
2187
|
+
* If the original request had an explicit provider (like "kilo/nemotron-3-ultra-free"), this returns empty array.
|
|
2082
2188
|
*
|
|
2083
2189
|
* @param modelID - The model ID to find alternatives for
|
|
2084
2190
|
* @param failedProviderID - The provider that failed
|