@link-assistant/agent 0.26.15 → 0.27.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 +3 -3
- package/package.json +43 -33
- 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/cmd/auth.ts +4 -0
- package/src/cli/cmd/models.ts +68 -13
- package/src/cli/model-config.js +7 -4
- package/src/index.js +3 -0
- package/src/provider/env-template.ts +115 -0
- package/src/provider/extra-providers.ts +347 -0
- package/src/provider/kilo.ts +19 -0
- 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 +257 -182
- package/src/provider/transform.ts +29 -0
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';
|
|
@@ -21,21 +22,36 @@ import { RetryFetch } from './retry-fetch';
|
|
|
21
22
|
import { SSEUsageExtractor } from '../util/sse-usage-extractor';
|
|
22
23
|
import { OpenCodeZen } from './opencode-zen';
|
|
23
24
|
import { Kilo } from './kilo';
|
|
25
|
+
import { EnvTemplate } from './env-template';
|
|
26
|
+
import { ExtraProviders } from './extra-providers';
|
|
27
|
+
import { ModelDiscovery } from './model-discovery';
|
|
28
|
+
import { ParityLoaders } from './parity-loaders';
|
|
24
29
|
|
|
25
30
|
// Direct imports for bundled providers - these are pre-installed to avoid runtime installation hangs
|
|
26
31
|
// @see https://github.com/link-assistant/agent/issues/173
|
|
27
32
|
// @see https://github.com/oven-sh/bun/issues/5831 (bun install hangs sporadically)
|
|
28
33
|
import { createAmazonBedrock } from '@ai-sdk/amazon-bedrock';
|
|
34
|
+
import { createBedrockMantle } from '@ai-sdk/amazon-bedrock/mantle';
|
|
29
35
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
30
36
|
import { createAzure } from '@ai-sdk/azure';
|
|
31
37
|
import { createGoogleGenerativeAI } from '@ai-sdk/google';
|
|
32
38
|
import { createVertex } from '@ai-sdk/google-vertex';
|
|
39
|
+
import { createVertexAnthropic } from '@ai-sdk/google-vertex/anthropic';
|
|
33
40
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
34
41
|
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
|
|
35
42
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
|
|
36
43
|
import { createXai } from '@ai-sdk/xai';
|
|
37
44
|
import { createMistral } from '@ai-sdk/mistral';
|
|
38
45
|
import { createGroq } from '@ai-sdk/groq';
|
|
46
|
+
import { createAlibaba } from '@ai-sdk/alibaba';
|
|
47
|
+
import { createCerebras } from '@ai-sdk/cerebras';
|
|
48
|
+
import { createCohere } from '@ai-sdk/cohere';
|
|
49
|
+
import { createDeepInfra } from '@ai-sdk/deepinfra';
|
|
50
|
+
import { createDeepSeek } from '@ai-sdk/deepseek';
|
|
51
|
+
import { createFireworks } from '@ai-sdk/fireworks';
|
|
52
|
+
import { createGateway } from '@ai-sdk/gateway';
|
|
53
|
+
import { createPerplexity } from '@ai-sdk/perplexity';
|
|
54
|
+
import { createTogetherAI } from '@ai-sdk/togetherai';
|
|
39
55
|
|
|
40
56
|
export namespace Provider {
|
|
41
57
|
const log = Log.create({ service: 'provider' });
|
|
@@ -70,17 +86,24 @@ export namespace Provider {
|
|
|
70
86
|
'@ai-sdk/xai': createXai,
|
|
71
87
|
'@ai-sdk/mistral': createMistral,
|
|
72
88
|
'@ai-sdk/groq': createGroq,
|
|
89
|
+
// Packages models.dev (and OpenCode) use for the remaining providers and
|
|
90
|
+
// per-model overrides (#334).
|
|
91
|
+
'@ai-sdk/amazon-bedrock/mantle': createBedrockMantle,
|
|
92
|
+
'@ai-sdk/google-vertex/anthropic': createVertexAnthropic,
|
|
93
|
+
'@ai-sdk/alibaba': createAlibaba,
|
|
94
|
+
'@ai-sdk/cerebras': createCerebras,
|
|
95
|
+
'@ai-sdk/cohere': createCohere,
|
|
96
|
+
'@ai-sdk/deepinfra': createDeepInfra,
|
|
97
|
+
'@ai-sdk/deepseek': createDeepSeek,
|
|
98
|
+
'@ai-sdk/fireworks': createFireworks,
|
|
99
|
+
'@ai-sdk/gateway': createGateway,
|
|
100
|
+
'@ai-sdk/perplexity': createPerplexity,
|
|
101
|
+
'@ai-sdk/togetherai': createTogetherAI,
|
|
73
102
|
};
|
|
74
103
|
|
|
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
|
-
}>;
|
|
104
|
+
type CustomLoader = (
|
|
105
|
+
provider: ModelsDev.Provider
|
|
106
|
+
) => Promise<ParityLoaders.LoaderResult>;
|
|
84
107
|
|
|
85
108
|
type Source = 'env' | 'config' | 'custom' | 'api';
|
|
86
109
|
|
|
@@ -224,8 +247,14 @@ export namespace Provider {
|
|
|
224
247
|
};
|
|
225
248
|
},
|
|
226
249
|
azure: async () => {
|
|
250
|
+
// `agent auth login azure` with Microsoft Entra ID stores no secret:
|
|
251
|
+
// requests mint bearer tokens from the Azure CLI session (#334).
|
|
252
|
+
const entra = await AzureLogin.options(await Auth.get('azure'));
|
|
253
|
+
if (entra) {
|
|
254
|
+
log.info(() => ({ message: 'using azure entra id credentials' }));
|
|
255
|
+
}
|
|
227
256
|
return {
|
|
228
|
-
autoload:
|
|
257
|
+
autoload: !!entra,
|
|
229
258
|
async getModel(
|
|
230
259
|
sdk: any,
|
|
231
260
|
modelID: string,
|
|
@@ -237,7 +266,7 @@ export namespace Provider {
|
|
|
237
266
|
return sdk.responses(modelID);
|
|
238
267
|
}
|
|
239
268
|
},
|
|
240
|
-
options: {},
|
|
269
|
+
options: entra ?? {},
|
|
241
270
|
};
|
|
242
271
|
},
|
|
243
272
|
'azure-cognitive-services': async () => {
|
|
@@ -263,103 +292,6 @@ export namespace Provider {
|
|
|
263
292
|
},
|
|
264
293
|
};
|
|
265
294
|
},
|
|
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
295
|
openrouter: async () => {
|
|
364
296
|
return {
|
|
365
297
|
autoload: false,
|
|
@@ -382,52 +314,6 @@ export namespace Provider {
|
|
|
382
314
|
},
|
|
383
315
|
};
|
|
384
316
|
},
|
|
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
317
|
zenmux: async () => {
|
|
432
318
|
return {
|
|
433
319
|
autoload: false,
|
|
@@ -764,6 +650,9 @@ export namespace Provider {
|
|
|
764
650
|
options: {},
|
|
765
651
|
};
|
|
766
652
|
},
|
|
653
|
+
// OpenCode parity: Cloudflare, Snowflake, SAP AI Core, Vertex, Bedrock,
|
|
654
|
+
// attribution headers (#334).
|
|
655
|
+
...ParityLoaders.create(),
|
|
767
656
|
};
|
|
768
657
|
|
|
769
658
|
export const state = Instance.state(async () => {
|
|
@@ -780,6 +669,7 @@ export namespace Provider {
|
|
|
780
669
|
modelID: string,
|
|
781
670
|
options?: Record<string, any>
|
|
782
671
|
) => Promise<any>;
|
|
672
|
+
sdkOptions?: ParityLoaders.LoaderResult['sdkOptions'];
|
|
783
673
|
options: Record<string, any>;
|
|
784
674
|
};
|
|
785
675
|
} = {};
|
|
@@ -807,24 +697,29 @@ export namespace Provider {
|
|
|
807
697
|
sdk: any,
|
|
808
698
|
modelID: string,
|
|
809
699
|
options?: Record<string, any>
|
|
810
|
-
) => Promise<any
|
|
700
|
+
) => Promise<any>,
|
|
701
|
+
sdkOptions?: ParityLoaders.LoaderResult['sdkOptions']
|
|
811
702
|
) {
|
|
812
703
|
const provider = providers[id];
|
|
813
704
|
if (!provider) {
|
|
814
705
|
const info = database[id];
|
|
815
706
|
if (!info) return;
|
|
816
|
-
|
|
707
|
+
// models.dev base URLs may contain `${ENV}` placeholders (#334).
|
|
708
|
+
const baseURL = EnvTemplate.resolve(info.api);
|
|
709
|
+
if (baseURL && !options['baseURL']) options['baseURL'] = baseURL;
|
|
817
710
|
providers[id] = {
|
|
818
711
|
source,
|
|
819
712
|
info,
|
|
820
713
|
options,
|
|
821
714
|
getModel,
|
|
715
|
+
sdkOptions,
|
|
822
716
|
};
|
|
823
717
|
return;
|
|
824
718
|
}
|
|
825
719
|
provider.options = mergeDeep(provider.options, options);
|
|
826
720
|
provider.source = source;
|
|
827
721
|
provider.getModel = getModel ?? provider.getModel;
|
|
722
|
+
provider.sdkOptions = sdkOptions ?? provider.sdkOptions;
|
|
828
723
|
}
|
|
829
724
|
|
|
830
725
|
const configProviders = Object.entries(config.provider ?? {});
|
|
@@ -939,15 +834,30 @@ export namespace Provider {
|
|
|
939
834
|
}
|
|
940
835
|
}
|
|
941
836
|
|
|
837
|
+
// Kilo keeps its curated free aliases and gains the rest of its
|
|
838
|
+
// models.dev catalogue (text models only), served through the same
|
|
839
|
+
// OpenRouter-compatible endpoint (#334).
|
|
942
840
|
database.kilo = {
|
|
943
841
|
id: 'kilo',
|
|
944
842
|
name: 'Kilo Gateway',
|
|
945
843
|
npm: '@openrouter/ai-sdk-provider',
|
|
946
844
|
api: 'https://api.kilo.ai/api/openrouter',
|
|
947
845
|
env: ['KILO_API_KEY'],
|
|
948
|
-
models:
|
|
846
|
+
models: {
|
|
847
|
+
...Kilo.catalogueModels(database.kilo),
|
|
848
|
+
...structuredClone(Kilo.FREE_MODELS),
|
|
849
|
+
},
|
|
949
850
|
};
|
|
950
851
|
|
|
852
|
+
// Providers models.dev does not list (gpt4free, Pollinations, local
|
|
853
|
+
// servers) and packages the bundled AI SDK cannot load (#334).
|
|
854
|
+
ExtraProviders.augment(database);
|
|
855
|
+
ParityLoaders.patchCloudflareGateway(
|
|
856
|
+
database,
|
|
857
|
+
process.env,
|
|
858
|
+
config.provider?.['cloudflare-ai-gateway']?.options?.['baseURL']
|
|
859
|
+
);
|
|
860
|
+
|
|
951
861
|
for (const [providerID, provider] of configProviders) {
|
|
952
862
|
const existing = database[providerID];
|
|
953
863
|
const parsed: ModelsDev.Provider = {
|
|
@@ -1030,8 +940,9 @@ export namespace Provider {
|
|
|
1030
940
|
for (const [providerID, provider] of Object.entries(database)) {
|
|
1031
941
|
if (disabled.has(providerID)) continue;
|
|
1032
942
|
// 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
|
-
|
|
943
|
+
// GOOGLE_GENERATIVE_AI_API_KEY and GEMINI_API_KEY). Variables that only
|
|
944
|
+
// fill a templated URL or name an account/region are not keys (#334).
|
|
945
|
+
const apiKey = EnvTemplate.credentialEnv(provider)
|
|
1035
946
|
.map((item) => process.env[item])
|
|
1036
947
|
.find(Boolean);
|
|
1037
948
|
if (!apiKey) continue;
|
|
@@ -1044,7 +955,31 @@ export namespace Provider {
|
|
|
1044
955
|
if (disabled.has(providerID)) continue;
|
|
1045
956
|
if (provider.type === 'api') {
|
|
1046
957
|
mergeProvider(providerID, { apiKey: provider.key }, 'api');
|
|
958
|
+
continue;
|
|
1047
959
|
}
|
|
960
|
+
// Subscription logins without a dedicated custom loader (xAI,
|
|
961
|
+
// DigitalOcean, GitLab) resolve to a key or fetch through their plugin (#334).
|
|
962
|
+
if (provider.type !== 'oauth' || CUSTOM_LOADERS[providerID]) continue;
|
|
963
|
+
if (!database[providerID]) continue;
|
|
964
|
+
const loaderFn = await AuthPlugins.getLoader(providerID);
|
|
965
|
+
if (!loaderFn) continue;
|
|
966
|
+
const result = await loaderFn(
|
|
967
|
+
() => Auth.get(providerID),
|
|
968
|
+
database[providerID]
|
|
969
|
+
).catch((error) => {
|
|
970
|
+
log.warn(() => ({
|
|
971
|
+
message: 'oauth credentials could not be loaded',
|
|
972
|
+
providerID,
|
|
973
|
+
error: String(error),
|
|
974
|
+
}));
|
|
975
|
+
return undefined;
|
|
976
|
+
});
|
|
977
|
+
if (!result?.apiKey && !result?.fetch) continue;
|
|
978
|
+
const options: Record<string, any> = { ...result.options };
|
|
979
|
+
if (result.apiKey) options['apiKey'] = result.apiKey;
|
|
980
|
+
if (result.baseURL) options['baseURL'] = result.baseURL;
|
|
981
|
+
if (result.fetch) options['fetch'] = result.fetch;
|
|
982
|
+
mergeProvider(providerID, options, 'api');
|
|
1048
983
|
}
|
|
1049
984
|
|
|
1050
985
|
// load custom
|
|
@@ -1056,7 +991,8 @@ export namespace Provider {
|
|
|
1056
991
|
providerID,
|
|
1057
992
|
result.options ?? {},
|
|
1058
993
|
'custom',
|
|
1059
|
-
result.getModel
|
|
994
|
+
result.getModel,
|
|
995
|
+
result.sdkOptions
|
|
1060
996
|
);
|
|
1061
997
|
}
|
|
1062
998
|
}
|
|
@@ -1095,11 +1031,40 @@ export namespace Provider {
|
|
|
1095
1031
|
available: Object.keys(providers),
|
|
1096
1032
|
}));
|
|
1097
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* Enable a keyless provider (gpt4free, Pollinations, local servers) on
|
|
1036
|
+
* first explicit use. They stay out of `providers` until then, so they
|
|
1037
|
+
* never take part in default or short-name resolution (#334).
|
|
1038
|
+
*/
|
|
1039
|
+
async function activate(providerID: string): Promise<boolean> {
|
|
1040
|
+
if (providers[providerID]) return true;
|
|
1041
|
+
if (disabled.has(providerID)) return false;
|
|
1042
|
+
if (!database[providerID] || !ExtraProviders.isKeyless(providerID)) {
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
const loader = CUSTOM_LOADERS[providerID];
|
|
1046
|
+
const result = loader ? await loader(database[providerID]) : undefined;
|
|
1047
|
+
mergeProvider(
|
|
1048
|
+
providerID,
|
|
1049
|
+
result?.options ?? {},
|
|
1050
|
+
'custom',
|
|
1051
|
+
result?.getModel,
|
|
1052
|
+
result?.sdkOptions
|
|
1053
|
+
);
|
|
1054
|
+
registryLog.info(() => ({
|
|
1055
|
+
message: 'keyless provider enabled on explicit request',
|
|
1056
|
+
provider: providerID,
|
|
1057
|
+
}));
|
|
1058
|
+
return Boolean(providers[providerID]);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1098
1061
|
return {
|
|
1099
1062
|
models,
|
|
1100
1063
|
providers,
|
|
1101
1064
|
sdk,
|
|
1102
1065
|
realIdByKey,
|
|
1066
|
+
database,
|
|
1067
|
+
activate,
|
|
1103
1068
|
};
|
|
1104
1069
|
});
|
|
1105
1070
|
|
|
@@ -1114,7 +1079,38 @@ export namespace Provider {
|
|
|
1114
1079
|
});
|
|
1115
1080
|
const s = await state();
|
|
1116
1081
|
const pkg = model.provider?.npm ?? provider.npm ?? provider.id;
|
|
1117
|
-
const
|
|
1082
|
+
const entry = s.providers[provider.id];
|
|
1083
|
+
let options: Record<string, any> = { ...entry?.options };
|
|
1084
|
+
// Per-model endpoint from models.dev (Vertex partner models, Bedrock
|
|
1085
|
+
// Mantle, Cloudflare passthrough routes). A user-set baseURL still wins
|
|
1086
|
+
// for models served by the provider's own package (#334).
|
|
1087
|
+
const providerURL = EnvTemplate.resolve(provider.api);
|
|
1088
|
+
const modelURL = EnvTemplate.resolve(model.provider?.api);
|
|
1089
|
+
const ownPackage = !model.provider?.npm || pkg === provider.npm;
|
|
1090
|
+
if (
|
|
1091
|
+
modelURL &&
|
|
1092
|
+
/^https?:\/\//.test(modelURL) &&
|
|
1093
|
+
(!ownPackage ||
|
|
1094
|
+
!options['baseURL'] ||
|
|
1095
|
+
options['baseURL'] === providerURL)
|
|
1096
|
+
) {
|
|
1097
|
+
options['baseURL'] = modelURL;
|
|
1098
|
+
}
|
|
1099
|
+
const template = model.provider?.api ?? provider.api;
|
|
1100
|
+
if (
|
|
1101
|
+
!options['baseURL'] &&
|
|
1102
|
+
EnvTemplate.isTemplate(template) &&
|
|
1103
|
+
!EnvTemplate.resolve(template)
|
|
1104
|
+
) {
|
|
1105
|
+
const missing = EnvTemplate.variables(template).filter(
|
|
1106
|
+
(name) => !EnvTemplate.resolve(`\${${name}}`)
|
|
1107
|
+
);
|
|
1108
|
+
throw new Error(
|
|
1109
|
+
`Provider "${provider.id}" needs ${missing.join(', ')} to build its base URL (${template}). ` +
|
|
1110
|
+
`Set the environment variable(s) or provider.${provider.id}.options.baseURL in the config.`
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (entry?.sdkOptions) options = entry.sdkOptions(pkg, options);
|
|
1118
1114
|
if (
|
|
1119
1115
|
pkg.includes('@ai-sdk/openai-compatible') &&
|
|
1120
1116
|
options['includeUsage'] === undefined
|
|
@@ -1517,17 +1513,9 @@ export namespace Provider {
|
|
|
1517
1513
|
installedPath = pkg;
|
|
1518
1514
|
}
|
|
1519
1515
|
|
|
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);
|
|
1516
|
+
// `@ai-sdk/google-vertex/anthropic` and other subpath packages are
|
|
1517
|
+
// bundled above, so no dist path patching is needed here (#334).
|
|
1518
|
+
const mod = await import(installedPath);
|
|
1531
1519
|
|
|
1532
1520
|
const fn = mod[Object.keys(mod).find((key) => key.startsWith('create'))!];
|
|
1533
1521
|
const loaded = fn({
|
|
@@ -1550,27 +1538,92 @@ export namespace Provider {
|
|
|
1550
1538
|
}
|
|
1551
1539
|
|
|
1552
1540
|
export async function getProvider(providerID: string) {
|
|
1553
|
-
|
|
1541
|
+
const s = await state();
|
|
1542
|
+
const id = ExtraProviders.canonicalID(providerID);
|
|
1543
|
+
if (!s.providers[id]) await s.activate(id);
|
|
1544
|
+
return s.providers[id];
|
|
1554
1545
|
}
|
|
1555
1546
|
|
|
1556
1547
|
/**
|
|
1557
1548
|
* Whether a provider's catalog lists a model. Looks at that one provider
|
|
1558
|
-
* only and logs nothing (#313).
|
|
1549
|
+
* only and logs nothing (#313). Routers and local servers that accept any
|
|
1550
|
+
* model ID count as listing every model (#334).
|
|
1559
1551
|
*/
|
|
1560
1552
|
export async function hasModel(providerID: string, modelID: string) {
|
|
1553
|
+
const s = await state();
|
|
1554
|
+
const provider = s.providers[ExtraProviders.canonicalID(providerID)];
|
|
1555
|
+
if (!provider) return false;
|
|
1556
|
+
if (provider.info.models[modelID]) return true;
|
|
1557
|
+
return ExtraProviders.allowsUnknownModels(provider.info.id);
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* List the models an active provider serves right now, using its
|
|
1562
|
+
* token-free listing endpoint (`/models`). `force` bypasses the cache.
|
|
1563
|
+
*/
|
|
1564
|
+
export async function discoverModels(
|
|
1565
|
+
providerID: string,
|
|
1566
|
+
input: { force?: boolean } = {}
|
|
1567
|
+
): Promise<ModelDiscovery.DiscoveryResult> {
|
|
1561
1568
|
const provider = await getProvider(providerID);
|
|
1562
|
-
|
|
1569
|
+
if (!provider) {
|
|
1570
|
+
return { status: 'skipped', models: [], error: 'provider not active' };
|
|
1571
|
+
}
|
|
1572
|
+
const options = provider.options ?? {};
|
|
1573
|
+
return ModelDiscovery.discover({
|
|
1574
|
+
providerID: provider.info.id,
|
|
1575
|
+
npm: provider.info.npm,
|
|
1576
|
+
baseURL: options['baseURL'] ?? EnvTemplate.resolve(provider.info.api),
|
|
1577
|
+
apiKey:
|
|
1578
|
+
typeof options['apiKey'] === 'string' ? options['apiKey'] : undefined,
|
|
1579
|
+
headers: options['headers'],
|
|
1580
|
+
resourceName:
|
|
1581
|
+
options['resourceName'] ??
|
|
1582
|
+
(provider.info.id === 'azure'
|
|
1583
|
+
? process.env['AZURE_RESOURCE_NAME']
|
|
1584
|
+
: undefined),
|
|
1585
|
+
// Providers that sign requests themselves (OAuth) list models the same way.
|
|
1586
|
+
fetchImpl:
|
|
1587
|
+
typeof options['fetch'] === 'function' ? options['fetch'] : undefined,
|
|
1588
|
+
force: input.force,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
/**
|
|
1593
|
+
* Look a model up in the provider's own model listing. OpenCode Zen keeps
|
|
1594
|
+
* its dedicated free-model lookup; every other provider with a token-free
|
|
1595
|
+
* listing endpoint goes through `ModelDiscovery` (#334).
|
|
1596
|
+
*/
|
|
1597
|
+
async function getLiveModelInfo(
|
|
1598
|
+
providerID: string,
|
|
1599
|
+
modelID: string
|
|
1600
|
+
): Promise<ModelsDev.Model | undefined> {
|
|
1601
|
+
if (providerID === 'opencode') {
|
|
1602
|
+
return OpenCodeZen.getLiveFreeModelInfo(modelID);
|
|
1603
|
+
}
|
|
1604
|
+
const s = await state();
|
|
1605
|
+
const result = await discoverModels(providerID);
|
|
1606
|
+
const found = ModelDiscovery.findModel(result.models, modelID);
|
|
1607
|
+
if (!found) return undefined;
|
|
1608
|
+
return ModelDiscovery.toModelInfo(found, {
|
|
1609
|
+
metadata: ModelDiscovery.catalogueMetadata(
|
|
1610
|
+
s.database,
|
|
1611
|
+
found.model ?? found.id,
|
|
1612
|
+
providerID
|
|
1613
|
+
),
|
|
1614
|
+
});
|
|
1563
1615
|
}
|
|
1564
1616
|
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
return
|
|
1617
|
+
/** Metadata for a model ID a router or local server accepts unlisted. */
|
|
1618
|
+
function passthroughModelInfo(modelID: string): ModelsDev.Model {
|
|
1619
|
+
return ModelDiscovery.toModelInfo({ id: modelID, free: true });
|
|
1568
1620
|
}
|
|
1569
1621
|
|
|
1570
1622
|
export async function refreshLiveModelInfo(
|
|
1571
1623
|
providerID: string,
|
|
1572
1624
|
modelID: string
|
|
1573
1625
|
) {
|
|
1626
|
+
providerID = ExtraProviders.canonicalID(providerID);
|
|
1574
1627
|
const info = await getLiveModelInfo(providerID, modelID);
|
|
1575
1628
|
if (!info) return undefined;
|
|
1576
1629
|
|
|
@@ -1588,12 +1641,14 @@ export namespace Provider {
|
|
|
1588
1641
|
}
|
|
1589
1642
|
|
|
1590
1643
|
export async function getModel(providerID: string, modelID: string) {
|
|
1644
|
+
providerID = ExtraProviders.canonicalID(providerID);
|
|
1591
1645
|
const key = `${providerID}/${modelID}`;
|
|
1592
1646
|
const s = await state();
|
|
1593
1647
|
if (s.models.has(key)) return s.models.get(key)!;
|
|
1594
1648
|
|
|
1595
1649
|
log.info(() => ({ message: 'getModel', providerID, modelID }));
|
|
1596
1650
|
|
|
1651
|
+
if (!s.providers[providerID]) await s.activate(providerID);
|
|
1597
1652
|
const provider = s.providers[providerID];
|
|
1598
1653
|
if (!provider) {
|
|
1599
1654
|
// Check if this model ID might exist in another provider (e.g., OpenRouter)
|
|
@@ -1680,6 +1735,22 @@ export namespace Provider {
|
|
|
1680
1735
|
}
|
|
1681
1736
|
}
|
|
1682
1737
|
|
|
1738
|
+
if (
|
|
1739
|
+
!isSyntheticProvider &&
|
|
1740
|
+
!info &&
|
|
1741
|
+
ExtraProviders.allowsUnknownModels(providerID)
|
|
1742
|
+
) {
|
|
1743
|
+
// Routers (gpt4free, Pollinations) and local servers accept model IDs
|
|
1744
|
+
// that are not listed anywhere; pass the ID through as-is (#334).
|
|
1745
|
+
info = passthroughModelInfo(modelID);
|
|
1746
|
+
provider.info.models[modelID] = info;
|
|
1747
|
+
log.info(() => ({
|
|
1748
|
+
message: 'passing unlisted model through to router/local server',
|
|
1749
|
+
providerID,
|
|
1750
|
+
modelID,
|
|
1751
|
+
}));
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1683
1754
|
if (!isSyntheticProvider && !info) {
|
|
1684
1755
|
// Model not found even after cache refresh.
|
|
1685
1756
|
// Check if this is the default model — if so, create synthetic info and proceed (#239).
|
|
@@ -1739,9 +1810,13 @@ export namespace Provider {
|
|
|
1739
1810
|
} else {
|
|
1740
1811
|
// For regular providers, load the SDK first
|
|
1741
1812
|
const sdk = await getSDK(provider.info, info!);
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1813
|
+
// Models that need a specific API shape (Bedrock Mantle, models.dev
|
|
1814
|
+
// `provider.shape`) pick the SDK method themselves (#334).
|
|
1815
|
+
language =
|
|
1816
|
+
ParityLoaders.selectByShape(sdk, info!, realID) ??
|
|
1817
|
+
(provider.getModel
|
|
1818
|
+
? await provider.getModel(sdk, realID, provider.options)
|
|
1819
|
+
: sdk.languageModel(realID));
|
|
1745
1820
|
}
|
|
1746
1821
|
log.info(() => ({ message: 'found', providerID, modelID }));
|
|
1747
1822
|
s.models.set(key, {
|
|
@@ -1911,7 +1986,7 @@ export namespace Provider {
|
|
|
1911
1986
|
export function parseModel(model: string) {
|
|
1912
1987
|
const [providerID, ...rest] = model.split('/');
|
|
1913
1988
|
return {
|
|
1914
|
-
providerID: providerID,
|
|
1989
|
+
providerID: ExtraProviders.canonicalID(providerID),
|
|
1915
1990
|
modelID: rest.join('/'),
|
|
1916
1991
|
};
|
|
1917
1992
|
}
|
|
@@ -159,6 +159,27 @@ export namespace ProviderTransform {
|
|
|
159
159
|
return undefined;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Catalogue providers whose chat-completions endpoint accepts an unscoped
|
|
164
|
+
* `prompt_cache_key`, as audited by OpenCode
|
|
165
|
+
* (packages/core/src/plugin/provider/prompt-cache-key.ts).
|
|
166
|
+
* @see https://github.com/link-assistant/agent/issues/334
|
|
167
|
+
*/
|
|
168
|
+
export const PROMPT_CACHE_KEY_PROVIDERS = new Set([
|
|
169
|
+
'abliteration-ai',
|
|
170
|
+
'aihubmix',
|
|
171
|
+
'anyapi',
|
|
172
|
+
'auriko',
|
|
173
|
+
'edenai',
|
|
174
|
+
'jalapeno',
|
|
175
|
+
'llama',
|
|
176
|
+
'morph',
|
|
177
|
+
'snowflake-cortex',
|
|
178
|
+
'tencent-tokenhub',
|
|
179
|
+
'upstage',
|
|
180
|
+
'venice',
|
|
181
|
+
]);
|
|
182
|
+
|
|
162
183
|
export function options(
|
|
163
184
|
providerID: string,
|
|
164
185
|
modelID: string,
|
|
@@ -167,6 +188,14 @@ export namespace ProviderTransform {
|
|
|
167
188
|
): Record<string, any> | undefined {
|
|
168
189
|
const result: Record<string, any> = {};
|
|
169
190
|
|
|
191
|
+
if (
|
|
192
|
+
npm === '@ai-sdk/openai-compatible' &&
|
|
193
|
+
PROMPT_CACHE_KEY_PROVIDERS.has(providerID)
|
|
194
|
+
) {
|
|
195
|
+
// Unknown openai-compatible options are sent verbatim in the body.
|
|
196
|
+
result['prompt_cache_key'] = sessionID;
|
|
197
|
+
}
|
|
198
|
+
|
|
170
199
|
if (providerID === 'openai') {
|
|
171
200
|
result['promptCacheKey'] = sessionID;
|
|
172
201
|
}
|