@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/cli/cmd/auth.ts
CHANGED
|
@@ -359,6 +359,10 @@ export const AuthLoginCommand = cmd({
|
|
|
359
359
|
togetherai: 'https://api.together.xyz/settings/api-keys',
|
|
360
360
|
deepseek: 'https://platform.deepseek.com/api_keys',
|
|
361
361
|
xai: 'https://console.x.ai',
|
|
362
|
+
poe: 'https://poe.com/api_key',
|
|
363
|
+
digitalocean: 'https://cloud.digitalocean.com/gen-ai/model-access-keys',
|
|
364
|
+
gitlab: 'https://gitlab.com/-/user_settings/personal_access_tokens',
|
|
365
|
+
azure: 'https://ai.azure.com/',
|
|
362
366
|
};
|
|
363
367
|
|
|
364
368
|
if (apiKeyUrls[provider]) {
|
package/src/cli/cmd/models.ts
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import type { Argv } from 'yargs';
|
|
2
2
|
import { Instance } from '../../project/instance';
|
|
3
3
|
import { Provider } from '../../provider/provider';
|
|
4
|
+
import { ModelsDev } from '../../provider/models';
|
|
5
|
+
import { ModelDiscovery } from '../../provider/model-discovery';
|
|
6
|
+
import { ExtraProviders } from '../../provider/extra-providers';
|
|
4
7
|
import { cmd } from './cmd';
|
|
5
8
|
import { UI } from '../ui';
|
|
6
9
|
import { EOL } from 'os';
|
|
7
10
|
|
|
11
|
+
function isFree(model: ModelsDev.Model) {
|
|
12
|
+
return model.cost?.input === 0 && model.cost?.output === 0;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export const ModelsCommand = cmd({
|
|
9
16
|
command: 'models [provider]',
|
|
10
17
|
describe: 'list all available models',
|
|
@@ -19,39 +26,87 @@ export const ModelsCommand = cmd({
|
|
|
19
26
|
describe:
|
|
20
27
|
'use more verbose model output (includes metadata like costs)',
|
|
21
28
|
type: 'boolean',
|
|
29
|
+
})
|
|
30
|
+
.option('discover', {
|
|
31
|
+
describe:
|
|
32
|
+
"also list models from each provider's own /models endpoint (cached for an hour)",
|
|
33
|
+
type: 'boolean',
|
|
34
|
+
})
|
|
35
|
+
.option('refresh', {
|
|
36
|
+
describe:
|
|
37
|
+
'refetch the models.dev catalogue and provider model listings, ignoring caches (implies --discover)',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
})
|
|
40
|
+
.option('free', {
|
|
41
|
+
describe: 'only list models that cost nothing to use',
|
|
42
|
+
type: 'boolean',
|
|
22
43
|
});
|
|
23
44
|
},
|
|
24
45
|
handler: async (args) => {
|
|
46
|
+
if (args.refresh) await ModelsDev.refresh();
|
|
47
|
+
const discover = Boolean(args.discover || args.refresh);
|
|
48
|
+
|
|
25
49
|
await Instance.provide({
|
|
26
50
|
directory: process.cwd(),
|
|
27
51
|
async fn() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
async function printModels(providerID: string) {
|
|
53
|
+
const provider = await Provider.getProvider(providerID);
|
|
54
|
+
if (!provider) return false;
|
|
55
|
+
const models: Record<string, ModelsDev.Model> = {
|
|
56
|
+
...provider.info.models,
|
|
57
|
+
};
|
|
58
|
+
const unpriced = new Set<string>();
|
|
59
|
+
const keyless = ExtraProviders.isKeyless(provider.info.id);
|
|
60
|
+
if (discover) {
|
|
61
|
+
const result = await Provider.discoverModels(providerID, {
|
|
62
|
+
force: Boolean(args.refresh),
|
|
63
|
+
});
|
|
64
|
+
if (result.status === 'error') {
|
|
65
|
+
UI.error(
|
|
66
|
+
`${providerID}: model discovery failed: ${result.error}`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
const s = await Provider.state();
|
|
70
|
+
for (const found of result.models) {
|
|
71
|
+
if (models[found.id]) continue;
|
|
72
|
+
const metadata = ModelDiscovery.catalogueMetadata(
|
|
73
|
+
s.database,
|
|
74
|
+
found.model ?? found.id,
|
|
75
|
+
provider.info.id
|
|
76
|
+
);
|
|
77
|
+
// A listing without prices says nothing about the cost, except
|
|
78
|
+
// on keyless providers (gpt4free, local servers), which are free.
|
|
79
|
+
if (!keyless && !found.free && !found.cost && !metadata?.cost) {
|
|
80
|
+
unpriced.add(found.id);
|
|
81
|
+
}
|
|
82
|
+
models[found.id] = ModelDiscovery.toModelInfo(found, {
|
|
83
|
+
metadata,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
for (const [modelID, model] of Object.entries(models)) {
|
|
88
|
+
if (args.free && (unpriced.has(modelID) || !isFree(model)))
|
|
89
|
+
continue;
|
|
90
|
+
process.stdout.write(`${provider.info.id}/${modelID}`);
|
|
34
91
|
process.stdout.write(EOL);
|
|
35
|
-
if (verbose) {
|
|
92
|
+
if (args.verbose) {
|
|
36
93
|
process.stdout.write(JSON.stringify(model, null, 2));
|
|
37
94
|
process.stdout.write(EOL);
|
|
38
95
|
}
|
|
39
96
|
}
|
|
97
|
+
return true;
|
|
40
98
|
}
|
|
41
99
|
|
|
42
100
|
if (args.provider) {
|
|
43
|
-
|
|
44
|
-
if (!provider) {
|
|
101
|
+
if (!(await printModels(args.provider))) {
|
|
45
102
|
UI.error(`Provider not found: ${args.provider}`);
|
|
46
|
-
return;
|
|
47
103
|
}
|
|
48
|
-
|
|
49
|
-
printModels(args.provider, args.verbose);
|
|
50
104
|
return;
|
|
51
105
|
}
|
|
52
106
|
|
|
107
|
+
const providers = await Provider.list();
|
|
53
108
|
for (const providerID of Object.keys(providers)) {
|
|
54
|
-
printModels(providerID
|
|
109
|
+
await printModels(providerID);
|
|
55
110
|
}
|
|
56
111
|
},
|
|
57
112
|
});
|
package/src/cli/model-config.js
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
getCompactionSafetyMarginFromProcessArgv,
|
|
6
6
|
} from './argv.ts';
|
|
7
7
|
import { Log } from '../util/log.ts';
|
|
8
|
+
import { ExtraProviders } from '../provider/extra-providers.ts';
|
|
8
9
|
import {
|
|
9
10
|
DEFAULT_PROVIDER_ID,
|
|
10
11
|
DEFAULT_MODEL_ID,
|
|
@@ -124,7 +125,8 @@ export async function parseModelConfig(
|
|
|
124
125
|
if (modelArg.includes('/')) {
|
|
125
126
|
// Explicit provider/model format - respect user's choice
|
|
126
127
|
const modelParts = modelArg.split('/');
|
|
127
|
-
|
|
128
|
+
// Accept alternative spellings such as `g4f/...` for `gpt4free/...` (#334)
|
|
129
|
+
providerID = ExtraProviders.canonicalID(modelParts[0]);
|
|
128
130
|
modelID = modelParts.slice(1).join('/');
|
|
129
131
|
|
|
130
132
|
// Validate that providerID and modelID are not empty
|
|
@@ -152,9 +154,10 @@ export async function parseModelConfig(
|
|
|
152
154
|
const isDefaultModel = !cliModelArg && modelArg === defaultModel;
|
|
153
155
|
try {
|
|
154
156
|
const { Provider } = await import('../provider/provider.ts');
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
// getProvider also enables keyless providers (gpt4free, local servers)
|
|
158
|
+
// on explicit request; routers among them accept unlisted IDs (#334).
|
|
159
|
+
const provider = await Provider.getProvider(providerID);
|
|
160
|
+
if (provider && !(await Provider.hasModel(providerID, modelID))) {
|
|
158
161
|
const liveInfo = await Provider.refreshLiveModelInfo(
|
|
159
162
|
providerID,
|
|
160
163
|
modelID
|
package/src/index.js
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
} from './json-standard/index.ts';
|
|
27
27
|
import { McpCommand } from './cli/cmd/mcp.ts';
|
|
28
28
|
import { AuthCommand } from './cli/cmd/auth.ts';
|
|
29
|
+
import { ModelsCommand } from './cli/cmd/models.ts';
|
|
29
30
|
import { FormatError } from './cli/error.ts';
|
|
30
31
|
import { UI } from './cli/ui.ts';
|
|
31
32
|
import { errorText } from './util/error-text.ts';
|
|
@@ -654,6 +655,8 @@ async function main() {
|
|
|
654
655
|
.command(McpCommand)
|
|
655
656
|
// Auth subcommand
|
|
656
657
|
.command(AuthCommand)
|
|
658
|
+
// Models subcommand (list, --discover, --refresh, --free)
|
|
659
|
+
.command(ModelsCommand)
|
|
657
660
|
// Default command for agent mode (when no subcommand specified)
|
|
658
661
|
.command({
|
|
659
662
|
command: '$0',
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { ModelsDev } from './models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment handling for models.dev provider entries.
|
|
5
|
+
*
|
|
6
|
+
* models.dev (the catalogue OpenCode uses) describes some providers with
|
|
7
|
+
* templated base URLs such as
|
|
8
|
+
* `https://api.cloudflare.com/client/v4/accounts/${CLOUDFLARE_ACCOUNT_ID}/ai/v1`
|
|
9
|
+
* and lists the template variables next to the real credentials in
|
|
10
|
+
* `provider.env`. The variables configure *where* to send requests; only the
|
|
11
|
+
* remaining variables carry a credential.
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/link-assistant/agent/issues/334
|
|
14
|
+
*/
|
|
15
|
+
export namespace EnvTemplate {
|
|
16
|
+
const PATTERN = /\$\{([A-Za-z0-9_]+)\}/g;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Variable names that configure an endpoint, account, region or project.
|
|
20
|
+
* They must never be sent as an API key.
|
|
21
|
+
*/
|
|
22
|
+
const NON_CREDENTIAL =
|
|
23
|
+
/(_ID|_NAME|_REGION|_PROJECT|_LOCATION|_HOST|_URL|_ACCOUNT|_ENDPOINT|_CREDENTIALS|_RESOURCE_GROUP|_VERSION|_ORG|_ORGANIZATION)$/;
|
|
24
|
+
|
|
25
|
+
/** Part of a SigV4 key pair, not a bearer credential. */
|
|
26
|
+
const PAIRED_SECRETS = new Set(['AWS_SECRET_ACCESS_KEY']);
|
|
27
|
+
|
|
28
|
+
export function variables(template?: string): string[] {
|
|
29
|
+
if (!template) return [];
|
|
30
|
+
return Array.from(template.matchAll(PATTERN), (match) => match[1]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isCredentialEnv(name: string): boolean {
|
|
34
|
+
return !NON_CREDENTIAL.test(name) && !PAIRED_SECRETS.has(name);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The env vars of a provider that hold an API key, in catalogue order.
|
|
39
|
+
*/
|
|
40
|
+
export function credentialEnv(provider: ModelsDev.Provider): string[] {
|
|
41
|
+
const templated = new Set(variables(provider.api));
|
|
42
|
+
for (const model of Object.values(provider.models ?? {})) {
|
|
43
|
+
for (const name of variables(model.provider?.api)) templated.add(name);
|
|
44
|
+
}
|
|
45
|
+
return (provider.env ?? []).filter(
|
|
46
|
+
(name) => !templated.has(name) && isCredentialEnv(name)
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Values for template variables that OpenCode derives instead of reading
|
|
52
|
+
* verbatim (normalized hosts, Vertex endpoint, default AWS region).
|
|
53
|
+
*/
|
|
54
|
+
export function derived(
|
|
55
|
+
env: Record<string, string | undefined> = process.env
|
|
56
|
+
): Record<string, string | undefined> {
|
|
57
|
+
const location =
|
|
58
|
+
env['GOOGLE_VERTEX_LOCATION'] ??
|
|
59
|
+
env['GOOGLE_CLOUD_LOCATION'] ??
|
|
60
|
+
env['VERTEX_LOCATION'] ??
|
|
61
|
+
'global';
|
|
62
|
+
return {
|
|
63
|
+
AWS_REGION: env['AWS_REGION'] ?? env['AWS_DEFAULT_REGION'] ?? 'us-east-1',
|
|
64
|
+
DATABRICKS_HOST: stripHost(env['DATABRICKS_HOST']),
|
|
65
|
+
SNOWFLAKE_ACCOUNT: stripHost(env['SNOWFLAKE_ACCOUNT'])?.replace(
|
|
66
|
+
/\.snowflakecomputing\.com$/,
|
|
67
|
+
''
|
|
68
|
+
),
|
|
69
|
+
NEON_AI_GATEWAY_BASE_URL: env['NEON_AI_GATEWAY_BASE_URL']?.replace(
|
|
70
|
+
/\/+$/,
|
|
71
|
+
''
|
|
72
|
+
),
|
|
73
|
+
GOOGLE_VERTEX_PROJECT:
|
|
74
|
+
env['GOOGLE_VERTEX_PROJECT'] ??
|
|
75
|
+
env['GOOGLE_CLOUD_PROJECT'] ??
|
|
76
|
+
env['GCP_PROJECT'] ??
|
|
77
|
+
env['GCLOUD_PROJECT'],
|
|
78
|
+
GOOGLE_VERTEX_LOCATION: location,
|
|
79
|
+
GOOGLE_VERTEX_ENDPOINT:
|
|
80
|
+
location === 'global'
|
|
81
|
+
? 'aiplatform.googleapis.com'
|
|
82
|
+
: `${location}-aiplatform.googleapis.com`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function stripHost(value?: string) {
|
|
87
|
+
if (!value) return undefined;
|
|
88
|
+
return value.replace(/^[a-z]+:\/\//i, '').replace(/\/+$/, '');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Substitute `${VAR}` placeholders. Returns `undefined` when any variable is
|
|
93
|
+
* missing, so callers never send requests to a half-filled URL.
|
|
94
|
+
*/
|
|
95
|
+
export function resolve(
|
|
96
|
+
template: string | undefined,
|
|
97
|
+
env: Record<string, string | undefined> = process.env,
|
|
98
|
+
values: Record<string, string | undefined> = {}
|
|
99
|
+
): string | undefined {
|
|
100
|
+
if (!template) return undefined;
|
|
101
|
+
if (!template.includes('${')) return template;
|
|
102
|
+
const lookup = { ...derived(env), ...values };
|
|
103
|
+
let missing = false;
|
|
104
|
+
const result = template.replace(PATTERN, (_, name: string) => {
|
|
105
|
+
const value = lookup[name] ?? env[name];
|
|
106
|
+
if (!value) missing = true;
|
|
107
|
+
return value ?? '';
|
|
108
|
+
});
|
|
109
|
+
return missing ? undefined : result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function isTemplate(value?: string): boolean {
|
|
113
|
+
return Boolean(value?.includes('${'));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import type { ModelsDev } from './models';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Providers that models.dev (the catalogue OpenCode uses) does not list:
|
|
5
|
+
* gpt4free (https://github.com/xtekky/gpt4free) and its hosted gateway,
|
|
6
|
+
* Pollinations, other OpenAI-compatible endpoints from gpt4free's provider
|
|
7
|
+
* list, and local inference servers.
|
|
8
|
+
*
|
|
9
|
+
* A definition only fills a gap: if models.dev ever adds the same provider ID,
|
|
10
|
+
* the models.dev entry wins. Seed models are a starting point, the real list
|
|
11
|
+
* comes from the provider's `/models` endpoint (see `ModelDiscovery`).
|
|
12
|
+
*
|
|
13
|
+
* @see https://github.com/link-assistant/agent/issues/334
|
|
14
|
+
*/
|
|
15
|
+
export namespace ExtraProviders {
|
|
16
|
+
export interface Definition {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
/** Default OpenAI-compatible base URL. */
|
|
20
|
+
api: string;
|
|
21
|
+
/** Env vars that override the base URL, first match wins. */
|
|
22
|
+
baseURLEnv?: string[];
|
|
23
|
+
/** Optional or required API key env vars. */
|
|
24
|
+
env: string[];
|
|
25
|
+
doc: string;
|
|
26
|
+
/**
|
|
27
|
+
* Usable without credentials (local servers, anonymous free tiers).
|
|
28
|
+
* Such providers stay hidden until explicitly requested with
|
|
29
|
+
* `--model <provider>/<model>` or configured, so they never take over
|
|
30
|
+
* default or short-name resolution.
|
|
31
|
+
*/
|
|
32
|
+
keyless: boolean;
|
|
33
|
+
/** Router or local server: accept model IDs not present in any list. */
|
|
34
|
+
passthrough: boolean;
|
|
35
|
+
models: Record<string, ModelsDev.Model>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function seed(
|
|
39
|
+
id: string,
|
|
40
|
+
name: string,
|
|
41
|
+
options: {
|
|
42
|
+
context?: number;
|
|
43
|
+
output?: number;
|
|
44
|
+
reasoning?: boolean;
|
|
45
|
+
attachment?: boolean;
|
|
46
|
+
free?: boolean;
|
|
47
|
+
} = {}
|
|
48
|
+
): ModelsDev.Model {
|
|
49
|
+
const context = options.context ?? 128000;
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
name,
|
|
53
|
+
release_date: '',
|
|
54
|
+
attachment: options.attachment ?? false,
|
|
55
|
+
reasoning: options.reasoning ?? false,
|
|
56
|
+
temperature: true,
|
|
57
|
+
tool_call: true,
|
|
58
|
+
cost: { input: 0, output: 0, cache_read: 0, cache_write: 0 },
|
|
59
|
+
limit: { context, output: options.output ?? Math.min(16384, context) },
|
|
60
|
+
modalities: {
|
|
61
|
+
input: options.attachment ? ['text', 'image'] : ['text'],
|
|
62
|
+
output: ['text'],
|
|
63
|
+
},
|
|
64
|
+
options: {},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const LOCAL = { context: 32768, output: 8192 };
|
|
69
|
+
|
|
70
|
+
export const DEFINITIONS: Definition[] = [
|
|
71
|
+
{
|
|
72
|
+
id: 'gpt4free',
|
|
73
|
+
name: 'GPT4Free (local Interference API)',
|
|
74
|
+
api: 'http://localhost:1337/v1',
|
|
75
|
+
baseURLEnv: ['G4F_BASE_URL', 'GPT4FREE_BASE_URL'],
|
|
76
|
+
env: ['G4F_API_KEY'],
|
|
77
|
+
doc: 'https://github.com/xtekky/gpt4free',
|
|
78
|
+
keyless: true,
|
|
79
|
+
passthrough: true,
|
|
80
|
+
models: { auto: seed('auto', 'GPT4Free Auto (provider rotation)') },
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
id: 'g4f-space',
|
|
84
|
+
name: 'GPT4Free hosted gateway (g4f.space)',
|
|
85
|
+
api: 'https://g4f.space/v1',
|
|
86
|
+
baseURLEnv: ['G4F_SPACE_BASE_URL'],
|
|
87
|
+
env: ['G4F_SPACE_API_KEY'],
|
|
88
|
+
doc: 'https://g4f.dev/docs',
|
|
89
|
+
keyless: true,
|
|
90
|
+
passthrough: true,
|
|
91
|
+
models: { auto: seed('auto', 'GPT4Free Auto (random public server)') },
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'pollinations',
|
|
95
|
+
name: 'Pollinations',
|
|
96
|
+
api: 'https://gen.pollinations.ai/v1',
|
|
97
|
+
baseURLEnv: ['POLLINATIONS_BASE_URL'],
|
|
98
|
+
env: ['POLLINATIONS_API_KEY'],
|
|
99
|
+
doc: 'https://pollinations.ai',
|
|
100
|
+
keyless: true,
|
|
101
|
+
passthrough: true,
|
|
102
|
+
models: {
|
|
103
|
+
openai: seed('openai', 'GPT-5.4 Nano (Pollinations alias)', {
|
|
104
|
+
context: 400000,
|
|
105
|
+
reasoning: true,
|
|
106
|
+
attachment: true,
|
|
107
|
+
}),
|
|
108
|
+
'openai-fast': seed('openai-fast', 'GPT-5 Nano (Pollinations alias)', {
|
|
109
|
+
context: 400000,
|
|
110
|
+
reasoning: true,
|
|
111
|
+
attachment: true,
|
|
112
|
+
}),
|
|
113
|
+
deepseek: seed('deepseek', 'DeepSeek V4 Flash (Pollinations alias)', {
|
|
114
|
+
context: 1048576,
|
|
115
|
+
reasoning: true,
|
|
116
|
+
}),
|
|
117
|
+
kimi: seed('kimi', 'Kimi K2.6 (Pollinations alias)', {
|
|
118
|
+
context: 262000,
|
|
119
|
+
reasoning: true,
|
|
120
|
+
}),
|
|
121
|
+
minimax: seed('minimax', 'MiniMax M3 (Pollinations alias)', {
|
|
122
|
+
context: 524288,
|
|
123
|
+
reasoning: true,
|
|
124
|
+
}),
|
|
125
|
+
nemotron: seed('nemotron', 'Nemotron 3 Ultra (Pollinations alias)', {
|
|
126
|
+
context: 262144,
|
|
127
|
+
reasoning: true,
|
|
128
|
+
}),
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'airforce',
|
|
133
|
+
name: 'Airforce',
|
|
134
|
+
api: 'https://api.airforce/v1',
|
|
135
|
+
env: ['AIRFORCE_API_KEY'],
|
|
136
|
+
doc: 'https://api.airforce',
|
|
137
|
+
keyless: false,
|
|
138
|
+
passthrough: true,
|
|
139
|
+
models: {},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'aibadgr',
|
|
143
|
+
name: 'AI Badgr',
|
|
144
|
+
api: 'https://aibadgr.com/api/v1',
|
|
145
|
+
env: ['AIBADGR_API_KEY'],
|
|
146
|
+
doc: 'https://aibadgr.com',
|
|
147
|
+
keyless: false,
|
|
148
|
+
passthrough: false,
|
|
149
|
+
models: {},
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
id: 'api-route',
|
|
153
|
+
name: 'APIRoute',
|
|
154
|
+
api: 'https://global.api-route.com/v1',
|
|
155
|
+
env: ['APIROUTE_API_KEY', 'API_ROUTE_API_KEY'],
|
|
156
|
+
doc: 'https://www.api-route.com',
|
|
157
|
+
keyless: false,
|
|
158
|
+
passthrough: true,
|
|
159
|
+
models: {},
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'cheaperinference',
|
|
163
|
+
name: 'CheaperInference',
|
|
164
|
+
api: 'https://api.cheaperinference.com/v1',
|
|
165
|
+
env: ['CHEAPERINFERENCE_API_KEY'],
|
|
166
|
+
doc: 'https://cheaperinference.com',
|
|
167
|
+
keyless: false,
|
|
168
|
+
passthrough: false,
|
|
169
|
+
models: {},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: 'theb',
|
|
173
|
+
name: 'TheB.AI',
|
|
174
|
+
api: 'https://api.theb.ai/v1',
|
|
175
|
+
env: ['THEB_API_KEY'],
|
|
176
|
+
doc: 'https://theb.ai',
|
|
177
|
+
keyless: false,
|
|
178
|
+
passthrough: false,
|
|
179
|
+
models: {},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: 'ollama',
|
|
183
|
+
name: 'Ollama (local)',
|
|
184
|
+
api: 'http://127.0.0.1:11434/v1',
|
|
185
|
+
baseURLEnv: ['OLLAMA_BASE_URL', 'OLLAMA_HOST'],
|
|
186
|
+
env: [],
|
|
187
|
+
doc: 'https://ollama.com',
|
|
188
|
+
keyless: true,
|
|
189
|
+
passthrough: true,
|
|
190
|
+
models: {},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'vllm',
|
|
194
|
+
name: 'vLLM (local)',
|
|
195
|
+
api: 'http://127.0.0.1:8000/v1',
|
|
196
|
+
baseURLEnv: ['VLLM_BASE_URL'],
|
|
197
|
+
env: ['VLLM_API_KEY'],
|
|
198
|
+
doc: 'https://docs.vllm.ai',
|
|
199
|
+
keyless: true,
|
|
200
|
+
passthrough: true,
|
|
201
|
+
models: {},
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
id: 'llamacpp',
|
|
205
|
+
name: 'llama.cpp server (local)',
|
|
206
|
+
api: 'http://127.0.0.1:8080/v1',
|
|
207
|
+
baseURLEnv: ['LLAMACPP_BASE_URL'],
|
|
208
|
+
env: ['LLAMACPP_API_KEY'],
|
|
209
|
+
doc: 'https://github.com/ggml-org/llama.cpp',
|
|
210
|
+
keyless: true,
|
|
211
|
+
passthrough: true,
|
|
212
|
+
models: {},
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: 'omniroute',
|
|
216
|
+
name: 'OmniRoute (local)',
|
|
217
|
+
api: 'http://localhost:20128/v1',
|
|
218
|
+
baseURLEnv: ['OMNIROUTE_BASE_URL'],
|
|
219
|
+
env: ['OMNIROUTE_API_KEY'],
|
|
220
|
+
doc: 'https://github.com/xtekky/gpt4free',
|
|
221
|
+
keyless: true,
|
|
222
|
+
passthrough: true,
|
|
223
|
+
models: {},
|
|
224
|
+
},
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
/** Alternative spellings accepted in `--model`. */
|
|
228
|
+
export const ALIASES: Record<string, string> = {
|
|
229
|
+
g4f: 'gpt4free',
|
|
230
|
+
'gpt-4-free': 'gpt4free',
|
|
231
|
+
'llama.cpp': 'llamacpp',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
/** Catalogue entries we did not add but treat as local/keyless. */
|
|
235
|
+
const EXTRA_KEYLESS = new Set(['lmstudio']);
|
|
236
|
+
const EXTRA_PASSTHROUGH = new Set(['lmstudio']);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* models.dev packages that do not load with the bundled AI SDK major
|
|
240
|
+
* version, mapped to their OpenAI-compatible endpoint.
|
|
241
|
+
*/
|
|
242
|
+
export const NPM_FALLBACKS: Record<string, { npm: string; api: string }> = {
|
|
243
|
+
'venice-ai-sdk-provider': {
|
|
244
|
+
npm: '@ai-sdk/openai-compatible',
|
|
245
|
+
api: 'https://api.venice.ai/api/v1',
|
|
246
|
+
},
|
|
247
|
+
'@aihubmix/ai-sdk-provider': {
|
|
248
|
+
npm: '@ai-sdk/openai-compatible',
|
|
249
|
+
api: 'https://aihubmix.com/v1',
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const byID = new Map(DEFINITIONS.map((item) => [item.id, item]));
|
|
254
|
+
|
|
255
|
+
export function canonicalID(providerID: string): string {
|
|
256
|
+
return ALIASES[providerID] ?? providerID;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function get(providerID: string): Definition | undefined {
|
|
260
|
+
return byID.get(canonicalID(providerID));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function isKeyless(providerID: string): boolean {
|
|
264
|
+
const id = canonicalID(providerID);
|
|
265
|
+
return Boolean(byID.get(id)?.keyless) || EXTRA_KEYLESS.has(id);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export function allowsUnknownModels(providerID: string): boolean {
|
|
269
|
+
const id = canonicalID(providerID);
|
|
270
|
+
return Boolean(byID.get(id)?.passthrough) || EXTRA_PASSTHROUGH.has(id);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function normalizeBaseURL(id: string, value: string): string {
|
|
274
|
+
let url = value.trim().replace(/\/+$/, '');
|
|
275
|
+
if (!/^[a-z]+:\/\//i.test(url)) url = `http://${url}`;
|
|
276
|
+
// OLLAMA_HOST and similar point at the server root, not the API path.
|
|
277
|
+
if (id === 'ollama' && !/\/v1$/.test(url)) url = `${url}/v1`;
|
|
278
|
+
return url;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Base URL for a definition. `G4F_PROVIDER` pins gpt4free to one of its
|
|
283
|
+
* providers through the `/api/{provider}` route of the Interference API.
|
|
284
|
+
*/
|
|
285
|
+
export function baseURL(
|
|
286
|
+
definition: Definition,
|
|
287
|
+
env: Record<string, string | undefined> = process.env
|
|
288
|
+
): string {
|
|
289
|
+
const fromEnv = definition.baseURLEnv
|
|
290
|
+
?.map((name) => env[name])
|
|
291
|
+
.find((value) => value && value.trim());
|
|
292
|
+
let url = fromEnv
|
|
293
|
+
? normalizeBaseURL(definition.id, fromEnv)
|
|
294
|
+
: definition.api;
|
|
295
|
+
const pinned = env['G4F_PROVIDER']?.trim();
|
|
296
|
+
if (
|
|
297
|
+
pinned &&
|
|
298
|
+
(definition.id === 'gpt4free' || definition.id === 'g4f-space')
|
|
299
|
+
) {
|
|
300
|
+
url = `${url.replace(/\/v1$/, '')}/api/${encodeURIComponent(pinned)}`;
|
|
301
|
+
}
|
|
302
|
+
return url;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function toProvider(
|
|
306
|
+
definition: Definition,
|
|
307
|
+
env: Record<string, string | undefined> = process.env
|
|
308
|
+
): ModelsDev.Provider {
|
|
309
|
+
return {
|
|
310
|
+
id: definition.id,
|
|
311
|
+
name: definition.name,
|
|
312
|
+
api: baseURL(definition, env),
|
|
313
|
+
npm: '@ai-sdk/openai-compatible',
|
|
314
|
+
env: definition.env,
|
|
315
|
+
doc: definition.doc,
|
|
316
|
+
models: structuredClone(definition.models),
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Add the missing providers and patch catalogue entries the bundled SDKs
|
|
322
|
+
* cannot load. Mutates and returns `database`.
|
|
323
|
+
*/
|
|
324
|
+
export function augment(
|
|
325
|
+
database: Record<string, ModelsDev.Provider>,
|
|
326
|
+
env: Record<string, string | undefined> = process.env
|
|
327
|
+
): Record<string, ModelsDev.Provider> {
|
|
328
|
+
for (const definition of DEFINITIONS) {
|
|
329
|
+
if (database[definition.id]) continue;
|
|
330
|
+
database[definition.id] = toProvider(definition, env);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
for (const provider of Object.values(database)) {
|
|
334
|
+
const fallback = provider.npm ? NPM_FALLBACKS[provider.npm] : undefined;
|
|
335
|
+
if (!fallback) continue;
|
|
336
|
+
provider.npm = fallback.npm;
|
|
337
|
+
provider.api = provider.api ?? fallback.api;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const lmstudio = database['lmstudio'];
|
|
341
|
+
const lmstudioURL = env['LMSTUDIO_BASE_URL']?.trim();
|
|
342
|
+
if (lmstudio && lmstudioURL) {
|
|
343
|
+
lmstudio.api = normalizeBaseURL('lmstudio', lmstudioURL);
|
|
344
|
+
}
|
|
345
|
+
return database;
|
|
346
|
+
}
|
|
347
|
+
}
|
package/src/provider/kilo.ts
CHANGED
|
@@ -46,4 +46,23 @@ export namespace Kilo {
|
|
|
46
46
|
65536
|
|
47
47
|
),
|
|
48
48
|
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Text models models.dev lists for Kilo. Their IDs are the same on the
|
|
52
|
+
* OpenRouter-compatible route this provider uses, so the curated aliases
|
|
53
|
+
* above only add short names for the free Nemotron models (#334).
|
|
54
|
+
*/
|
|
55
|
+
export function catalogueModels(
|
|
56
|
+
provider: ModelsDev.Provider | undefined
|
|
57
|
+
): Record<string, ModelsDev.Model> {
|
|
58
|
+
const result: Record<string, ModelsDev.Model> = {};
|
|
59
|
+
for (const [key, model] of Object.entries(provider?.models ?? {})) {
|
|
60
|
+
const output = model.modalities?.output ?? ['text'];
|
|
61
|
+
if (!output.includes('text')) continue;
|
|
62
|
+
// The catalogue points at the gateway route; ours is fixed.
|
|
63
|
+
const { provider: _route, ...rest } = model;
|
|
64
|
+
result[key] = structuredClone(rest);
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
49
68
|
}
|