@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/project/project.ts
CHANGED
|
@@ -45,7 +45,12 @@ export namespace Project {
|
|
|
45
45
|
}
|
|
46
46
|
let worktree = path.dirname(git);
|
|
47
47
|
const timer = log.time('git.rev-parse');
|
|
48
|
-
|
|
48
|
+
// In a git worktree or submodule `.git` is a file, so the id cache lives in
|
|
49
|
+
// the common git directory instead (as upstream does). Writing to
|
|
50
|
+
// `.git/opencode` there used to reject with ENOTDIR; the rejection was not
|
|
51
|
+
// handled and the CLI exited with an UnhandledRejection (issue #335).
|
|
52
|
+
const cacheDir = await gitCacheDir(git, worktree);
|
|
53
|
+
let id = await Bun.file(path.join(cacheDir, 'opencode'))
|
|
49
54
|
.text()
|
|
50
55
|
.then((x) => x.trim())
|
|
51
56
|
.catch(() => {});
|
|
@@ -63,7 +68,16 @@ export namespace Project {
|
|
|
63
68
|
.toSorted()
|
|
64
69
|
);
|
|
65
70
|
id = roots[0];
|
|
66
|
-
|
|
71
|
+
// The cache is best effort: a read-only or unusual `.git` must not fail
|
|
72
|
+
// project detection.
|
|
73
|
+
if (id)
|
|
74
|
+
await Bun.write(path.join(cacheDir, 'opencode'), id).catch((error) =>
|
|
75
|
+
log.info(() => ({
|
|
76
|
+
message: 'project id cache not written',
|
|
77
|
+
path: path.join(cacheDir, 'opencode'),
|
|
78
|
+
error: String(error),
|
|
79
|
+
}))
|
|
80
|
+
);
|
|
67
81
|
}
|
|
68
82
|
timer.stop();
|
|
69
83
|
if (!id) {
|
|
@@ -95,6 +109,25 @@ export namespace Project {
|
|
|
95
109
|
return project;
|
|
96
110
|
}
|
|
97
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Directory that holds the project id cache: `.git` itself when it is a
|
|
114
|
+
* directory, otherwise the common git directory reported by git (worktrees
|
|
115
|
+
* and submodules have a `.git` file pointing elsewhere).
|
|
116
|
+
*/
|
|
117
|
+
async function gitCacheDir(git: string, worktree: string): Promise<string> {
|
|
118
|
+
const stat = await fs.stat(git).catch(() => undefined);
|
|
119
|
+
if (stat?.isDirectory()) return git;
|
|
120
|
+
const common =
|
|
121
|
+
await $`git rev-parse --path-format=absolute --git-common-dir`
|
|
122
|
+
.quiet()
|
|
123
|
+
.nothrow()
|
|
124
|
+
.cwd(worktree)
|
|
125
|
+
.text()
|
|
126
|
+
.then((x) => x.trim())
|
|
127
|
+
.catch(() => '');
|
|
128
|
+
return common || git;
|
|
129
|
+
}
|
|
130
|
+
|
|
98
131
|
export async function setInitialized(projectID: string) {
|
|
99
132
|
await Storage.update<Info>(['project', projectID], (draft) => {
|
|
100
133
|
draft.time.initialized = Date.now();
|
|
@@ -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
|
+
}
|
|
@@ -13,4 +13,4 @@ export function isFreeTierError(
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export const FREE_TIER_ERROR_MESSAGE =
|
|
16
|
-
"OpenCode Zen's free tier is limited to OpenCode clients. Use kilo/nemotron-3-
|
|
16
|
+
"OpenCode Zen's free tier is limited to OpenCode clients. Use kilo/nemotron-3-ultra-free (authenticate with agent auth login or KILO_API_KEY), or select a paid model.";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Log } from '../util/log';
|
|
2
|
+
import { Kilo } from './kilo';
|
|
3
|
+
|
|
4
|
+
const log = Log.create({ service: 'kilo' });
|
|
5
|
+
|
|
6
|
+
// Aliases already warned about. A stored session or config that names an
|
|
7
|
+
// alias reaches getModel()/parseModel() many times in one process, so warn
|
|
8
|
+
// once per alias; the set is bounded by `Kilo.DEPRECATED_ALIASES`.
|
|
9
|
+
const warned = new Set<string>();
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Map a deprecated Kilo alias (`Kilo.DEPRECATED_ALIASES`) to its replacement
|
|
13
|
+
* and warn about it (once per alias and process); any other model ID is
|
|
14
|
+
* returned unchanged (#335).
|
|
15
|
+
*
|
|
16
|
+
* Kept out of `kilo.ts` so that module has no runtime dependencies.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveKiloDeprecatedAlias(modelID: string): string {
|
|
19
|
+
const replacement = Kilo.deprecatedAliasTarget(modelID);
|
|
20
|
+
if (!replacement) return modelID;
|
|
21
|
+
if (warned.has(modelID)) return replacement;
|
|
22
|
+
warned.add(modelID);
|
|
23
|
+
log.warn(() => ({
|
|
24
|
+
message: `kilo/${modelID} is deprecated and no longer served by Kilo; using kilo/${replacement} instead`,
|
|
25
|
+
deprecatedModel: `kilo/${modelID}`,
|
|
26
|
+
replacement: `kilo/${replacement}`,
|
|
27
|
+
hint: `Pass --model kilo/${replacement} to silence this warning`,
|
|
28
|
+
}));
|
|
29
|
+
return replacement;
|
|
30
|
+
}
|
package/src/provider/kilo.ts
CHANGED
|
@@ -25,14 +25,7 @@ export namespace Kilo {
|
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Super's output cap is deliberately conservative for existing integrations.
|
|
29
28
|
export const FREE_MODELS: Record<string, ModelsDev.Model> = {
|
|
30
|
-
'nemotron-3-super-free': freeModel(
|
|
31
|
-
'nvidia/nemotron-3-super-120b-a12b:free',
|
|
32
|
-
'Nemotron 3 Super (Free)',
|
|
33
|
-
262144,
|
|
34
|
-
32768
|
|
35
|
-
),
|
|
36
29
|
'nemotron-3-ultra-free': freeModel(
|
|
37
30
|
'nvidia/nemotron-3-ultra-550b-a55b:free',
|
|
38
31
|
'Nemotron 3 Ultra (Free)',
|
|
@@ -46,4 +39,48 @@ export namespace Kilo {
|
|
|
46
39
|
65536
|
|
47
40
|
),
|
|
48
41
|
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Withdrawn built-in aliases that keep working temporarily. Each one maps to
|
|
45
|
+
* the served alias in `FREE_MODELS` that replaces it, and every resolution
|
|
46
|
+
* logs a warning naming the replacement. These are not catalog entries, so
|
|
47
|
+
* the drift check (`scripts/check-model-drift.mjs`) does not look them up.
|
|
48
|
+
*
|
|
49
|
+
* Kilo stopped serving `nvidia/nemotron-3-super-120b-a12b:free`; only the
|
|
50
|
+
* paid Super model remains on the gateway (#335).
|
|
51
|
+
*/
|
|
52
|
+
export const DEPRECATED_ALIASES: Readonly<Record<string, string>> = {
|
|
53
|
+
'nemotron-3-super-free': 'nemotron-3-ultra-free',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Replacement for a deprecated alias, or `undefined`. Logs nothing; use
|
|
58
|
+
* `resolveKiloDeprecatedAlias` from `./kilo-deprecation` to also warn.
|
|
59
|
+
* This module stays free of runtime dependencies because
|
|
60
|
+
* `scripts/check-default-models.mjs` imports it without installed packages.
|
|
61
|
+
*/
|
|
62
|
+
export function deprecatedAliasTarget(modelID: string): string | undefined {
|
|
63
|
+
return Object.hasOwn(DEPRECATED_ALIASES, modelID)
|
|
64
|
+
? DEPRECATED_ALIASES[modelID]
|
|
65
|
+
: undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Text models models.dev lists for Kilo. Their IDs are the same on the
|
|
70
|
+
* OpenRouter-compatible route this provider uses, so the curated aliases
|
|
71
|
+
* above only add short names for the free Nemotron models (#334).
|
|
72
|
+
*/
|
|
73
|
+
export function catalogueModels(
|
|
74
|
+
provider: ModelsDev.Provider | undefined
|
|
75
|
+
): Record<string, ModelsDev.Model> {
|
|
76
|
+
const result: Record<string, ModelsDev.Model> = {};
|
|
77
|
+
for (const [key, model] of Object.entries(provider?.models ?? {})) {
|
|
78
|
+
const output = model.modalities?.output ?? ['text'];
|
|
79
|
+
if (!output.includes('text')) continue;
|
|
80
|
+
// The catalogue points at the gateway route; ours is fixed.
|
|
81
|
+
const { provider: _route, ...rest } = model;
|
|
82
|
+
result[key] = structuredClone(rest);
|
|
83
|
+
}
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
49
86
|
}
|