@link-assistant/agent 0.22.8 → 0.22.10
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 +8 -5
- package/package.json +3 -3
- package/src/cli/argv.ts +1 -1
- package/src/cli/continuous-mode.js +2 -1
- package/src/cli/defaults.ts +1 -62
- package/src/cli/model-config.js +70 -34
- package/src/cli/run-options.js +30 -12
- package/src/config/defaults.ts +170 -0
- package/src/index.js +1 -1
- package/src/provider/opencode-zen.ts +138 -0
- package/src/provider/provider.ts +63 -12
- package/src/session/message-v2.ts +46 -0
- package/src/session/processor.ts +56 -25
- package/src/tool/task.ts +4 -3
package/README.md
CHANGED
|
@@ -195,6 +195,9 @@ echo "hi" | agent
|
|
|
195
195
|
|
|
196
196
|
# Other free models (in order of recommendation)
|
|
197
197
|
echo "hi" | agent --model opencode/minimax-m2.5-free
|
|
198
|
+
echo "hi" | agent --model opencode/ling-2.6-flash-free
|
|
199
|
+
echo "hi" | agent --model opencode/hy3-preview-free
|
|
200
|
+
echo "hi" | agent --model opencode/nemotron-3-super-free
|
|
198
201
|
echo "hi" | agent --model opencode/gpt-5-nano
|
|
199
202
|
echo "hi" | agent --model opencode/big-pickle
|
|
200
203
|
|
|
@@ -602,13 +605,13 @@ bun run src/index.js
|
|
|
602
605
|
|
|
603
606
|
```bash
|
|
604
607
|
# Run all tests
|
|
605
|
-
|
|
608
|
+
npm test
|
|
606
609
|
|
|
607
610
|
# Run specific test file
|
|
608
|
-
bun test tests/mcp.
|
|
609
|
-
bun test tests/websearch.tools.
|
|
610
|
-
bun test tests/batch.tools.
|
|
611
|
-
bun test tests/plaintext.input.
|
|
611
|
+
bun test ./tests/integration/mcp.js
|
|
612
|
+
bun test ./tests/integration/websearch.tools.js
|
|
613
|
+
bun test ./tests/integration/batch.tools.js
|
|
614
|
+
bun test ./tests/integration/plaintext.input.js
|
|
612
615
|
```
|
|
613
616
|
|
|
614
617
|
For detailed testing information including how to run tests manually and trigger CI tests, see [TESTING.md](../TESTING.md).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@link-assistant/agent",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.10",
|
|
4
4
|
"description": "A minimal, public domain AI CLI agent compatible with OpenCode's JSON interface. Bun-only runtime.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
11
|
"dev": "bun run src/index.js",
|
|
12
|
-
"test": "bun test tests/*.
|
|
13
|
-
"test:integration": "bun test tests/integration/basic.
|
|
12
|
+
"test": "bun test ./tests/*.js ./tests/*.ts",
|
|
13
|
+
"test:integration": "bun test ./tests/integration/basic.js",
|
|
14
14
|
"lint": "eslint .",
|
|
15
15
|
"lint:fix": "eslint . --fix",
|
|
16
16
|
"format": "prettier --write .",
|
package/src/cli/argv.ts
CHANGED
|
@@ -105,7 +105,7 @@ export function getCompactionSafetyMarginFromProcessArgv(): string | null {
|
|
|
105
105
|
/**
|
|
106
106
|
* Extract --compaction-models argument directly from process.argv
|
|
107
107
|
* The value is a links notation references sequence, e.g.:
|
|
108
|
-
* "(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)"
|
|
108
|
+
* "(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)"
|
|
109
109
|
* @returns The compaction models argument from CLI or null if not found
|
|
110
110
|
* @see https://github.com/link-assistant/agent/issues/232
|
|
111
111
|
*/
|
|
@@ -51,7 +51,8 @@ const log = Log.create({ service: 'resume' });
|
|
|
51
51
|
export async function resolveResumeSession(argv, compactJson) {
|
|
52
52
|
const resumeSessionID = argv.resume;
|
|
53
53
|
const shouldContinue = argv.continue === true;
|
|
54
|
-
const noFork =
|
|
54
|
+
const noFork =
|
|
55
|
+
argv['no-fork'] === true || argv.noFork === true || argv.fork === false;
|
|
55
56
|
|
|
56
57
|
// If neither --resume nor --continue is specified, return null to create new session
|
|
57
58
|
if (!resumeSessionID && !shouldContinue) {
|
package/src/cli/defaults.ts
CHANGED
|
@@ -1,62 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Default CLI configuration values.
|
|
3
|
-
*
|
|
4
|
-
* Centralizing defaults here ensures all code references the same value (#208).
|
|
5
|
-
* When the default model changes, update this file only.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** Default model used when no `--model` CLI argument is provided. */
|
|
9
|
-
export const DEFAULT_MODEL = 'opencode/nemotron-3-super-free';
|
|
10
|
-
|
|
11
|
-
/** Default provider ID extracted from DEFAULT_MODEL. */
|
|
12
|
-
export const DEFAULT_PROVIDER_ID = DEFAULT_MODEL.split('/')[0];
|
|
13
|
-
|
|
14
|
-
/** Default model ID extracted from DEFAULT_MODEL. */
|
|
15
|
-
export const DEFAULT_MODEL_ID = DEFAULT_MODEL.split('/').slice(1).join('/');
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Default compaction model used when no `--compaction-model` CLI argument is provided.
|
|
19
|
-
* gpt-5-nano has a 400K context window, larger than most free base models (~200K),
|
|
20
|
-
* which allows compacting 100% of the base model's context without a safety margin.
|
|
21
|
-
* The special value "same" means use the same model as `--model`.
|
|
22
|
-
* @see https://github.com/link-assistant/agent/issues/219
|
|
23
|
-
*/
|
|
24
|
-
export const DEFAULT_COMPACTION_MODEL = 'opencode/gpt-5-nano';
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Default compaction models cascade, ordered from smallest/cheapest context to largest.
|
|
28
|
-
* During compaction, the system tries each model in order. If the used context exceeds
|
|
29
|
-
* a model's context limit, it skips to the next larger model. If a model's rate limit
|
|
30
|
-
* is reached, it also skips to the next model.
|
|
31
|
-
* The special value "same" means use the same model as `--model`.
|
|
32
|
-
*
|
|
33
|
-
* Parsed as links notation references sequence (single anonymous link):
|
|
34
|
-
* "(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)"
|
|
35
|
-
*
|
|
36
|
-
* Context limits (approximate):
|
|
37
|
-
* big-pickle: ~200K
|
|
38
|
-
* minimax-m2.5-free: ~200K
|
|
39
|
-
* nemotron-3-super-free: ~262K (default model)
|
|
40
|
-
* gpt-5-nano: ~400K
|
|
41
|
-
* same: (base model's context)
|
|
42
|
-
*
|
|
43
|
-
* Note: qwen3.6-plus-free was removed — free promotion ended April 2026.
|
|
44
|
-
* @see https://github.com/link-assistant/agent/issues/242
|
|
45
|
-
* @see https://github.com/link-assistant/agent/issues/232
|
|
46
|
-
*/
|
|
47
|
-
export const DEFAULT_COMPACTION_MODELS =
|
|
48
|
-
'(big-pickle minimax-m2.5-free nemotron-3-super-free gpt-5-nano same)';
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Default compaction safety margin as a percentage of usable context window.
|
|
52
|
-
* Applied only when the compaction model has a context window equal to or smaller
|
|
53
|
-
* than the base model. When the compaction model has a larger context, the margin
|
|
54
|
-
* is automatically set to 0 (allowing 100% context usage).
|
|
55
|
-
*
|
|
56
|
-
* Increased from 15% to 25% to reduce probability of context overflow errors,
|
|
57
|
-
* especially when providers return inaccurate or zero token counts.
|
|
58
|
-
* Matches OpenCode upstream's 75% threshold (25% margin).
|
|
59
|
-
* @see https://github.com/link-assistant/agent/issues/219
|
|
60
|
-
* @see https://github.com/link-assistant/agent/issues/249
|
|
61
|
-
*/
|
|
62
|
-
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT = 25;
|
|
1
|
+
export * from '../config/defaults.ts';
|
package/src/cli/model-config.js
CHANGED
|
@@ -8,9 +8,11 @@ import { Log } from '../util/log.ts';
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_PROVIDER_ID,
|
|
10
10
|
DEFAULT_MODEL_ID,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
getDefaultModel,
|
|
12
|
+
getDefaultModelParts,
|
|
13
|
+
getDefaultCompactionModel,
|
|
14
|
+
getDefaultCompactionModels,
|
|
15
|
+
getDefaultCompactionSafetyMarginPercent,
|
|
14
16
|
} from './defaults.ts';
|
|
15
17
|
|
|
16
18
|
/**
|
|
@@ -20,12 +22,21 @@ import {
|
|
|
20
22
|
* @param {function} outputStatus - Function to output status messages
|
|
21
23
|
* @returns {Promise<{providerID: string, modelID: string}>}
|
|
22
24
|
*/
|
|
23
|
-
export async function parseModelConfig(
|
|
25
|
+
export async function parseModelConfig(
|
|
26
|
+
argv,
|
|
27
|
+
outputError,
|
|
28
|
+
outputStatus,
|
|
29
|
+
defaultOptions = {}
|
|
30
|
+
) {
|
|
31
|
+
const defaultModel = getDefaultModel(defaultOptions);
|
|
32
|
+
const { providerID: defaultProviderID, modelID: defaultModelID } =
|
|
33
|
+
getDefaultModelParts(defaultOptions);
|
|
34
|
+
|
|
24
35
|
// Safeguard: validate argv.model against process.argv to detect yargs/cache mismatch (#192, #196, #239)
|
|
25
36
|
// This is critical because yargs under Bun may fail to parse --model correctly,
|
|
26
37
|
// returning the default value instead of the user's CLI argument.
|
|
27
38
|
const cliModelArg = getModelFromProcessArgv();
|
|
28
|
-
let modelArg = argv.model;
|
|
39
|
+
let modelArg = argv.model ?? defaultModel;
|
|
29
40
|
|
|
30
41
|
// Diagnostic logging: always log raw argv sources when debugging model resolution (#239)
|
|
31
42
|
// Bun global installs may have different process.argv structure (oven-sh/bun#22157)
|
|
@@ -54,7 +65,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
54
65
|
// Always use CLI value when available, even if it matches yargs
|
|
55
66
|
// This ensures we use the actual CLI argument, not a cached/default yargs value
|
|
56
67
|
modelArg = cliModelArg;
|
|
57
|
-
} else if (modelArg ===
|
|
68
|
+
} else if (modelArg === defaultModel) {
|
|
58
69
|
// cliModelArg is null AND yargs returned the default — check if process.argv
|
|
59
70
|
// actually contains --model to detect silent yargs/Bun mismatch (#239)
|
|
60
71
|
const rawArgvStr = process.argv.join(' ');
|
|
@@ -74,7 +85,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
74
85
|
typeof globalThis.Bun !== 'undefined' && globalThis.Bun.argv
|
|
75
86
|
? globalThis.Bun.argv
|
|
76
87
|
: '(not available)',
|
|
77
|
-
defaultModel
|
|
88
|
+
defaultModel,
|
|
78
89
|
}));
|
|
79
90
|
}
|
|
80
91
|
}
|
|
@@ -111,38 +122,54 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
111
122
|
// fail immediately instead of silently falling back to a different model.
|
|
112
123
|
// However, if the model is the default (no --model CLI flag), warn but proceed (#239).
|
|
113
124
|
// The models.dev API may lag behind the provider's actual model availability.
|
|
114
|
-
const isDefaultModel = !cliModelArg;
|
|
125
|
+
const isDefaultModel = !cliModelArg && modelArg === defaultModel;
|
|
115
126
|
try {
|
|
116
127
|
const { Provider } = await import('../provider/provider.ts');
|
|
117
128
|
const s = await Provider.state();
|
|
118
129
|
const provider = s.providers[providerID];
|
|
119
130
|
if (provider && !provider.info.models[modelID]) {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
131
|
+
const liveInfo = await Provider.refreshLiveModelInfo(
|
|
132
|
+
providerID,
|
|
133
|
+
modelID
|
|
134
|
+
);
|
|
135
|
+
if (liveInfo) {
|
|
136
|
+
Log.Default.info(() => ({
|
|
125
137
|
message:
|
|
126
|
-
'
|
|
138
|
+
'model not found in models.dev catalog but found in provider live endpoint',
|
|
127
139
|
providerID,
|
|
128
140
|
modelID,
|
|
129
|
-
availableModels,
|
|
130
141
|
}));
|
|
131
142
|
} else {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
message:
|
|
136
|
-
'model not found in provider — refusing to proceed with explicit provider/model',
|
|
137
|
-
providerID,
|
|
138
|
-
modelID,
|
|
139
|
-
availableModels,
|
|
140
|
-
}));
|
|
141
|
-
throw new Error(
|
|
142
|
-
`Model "${modelID}" not found in provider "${providerID}". ` +
|
|
143
|
-
`Available models include: ${availableModels.join(', ')}. ` +
|
|
144
|
-
`Use --model ${providerID}/<model-id> with a valid model, or omit the provider prefix for auto-resolution.`
|
|
143
|
+
const availableModels = Object.keys(provider.info.models).slice(
|
|
144
|
+
0,
|
|
145
|
+
10
|
|
145
146
|
);
|
|
147
|
+
if (isDefaultModel) {
|
|
148
|
+
// Default model not in models.dev catalog — warn but proceed (#239)
|
|
149
|
+
// The provider may still accept it; models.dev can lag behind actual availability.
|
|
150
|
+
Log.Default.warn(() => ({
|
|
151
|
+
message:
|
|
152
|
+
'default model not found in models.dev catalog — proceeding anyway',
|
|
153
|
+
providerID,
|
|
154
|
+
modelID,
|
|
155
|
+
availableModels,
|
|
156
|
+
}));
|
|
157
|
+
} else {
|
|
158
|
+
// User explicitly specified provider/model — fail with a clear error (#231)
|
|
159
|
+
// Silent fallback caused kimi-k2.5-free to be routed to minimax-m2.5-free
|
|
160
|
+
Log.Default.error(() => ({
|
|
161
|
+
message:
|
|
162
|
+
'model not found in provider — refusing to proceed with explicit provider/model',
|
|
163
|
+
providerID,
|
|
164
|
+
modelID,
|
|
165
|
+
availableModels,
|
|
166
|
+
}));
|
|
167
|
+
throw new Error(
|
|
168
|
+
`Model "${modelID}" not found in provider "${providerID}". ` +
|
|
169
|
+
`Available models include: ${availableModels.join(', ')}. ` +
|
|
170
|
+
`Use --model ${providerID}/<model-id> with a valid model, or omit the provider prefix for auto-resolution.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
146
173
|
}
|
|
147
174
|
}
|
|
148
175
|
} catch (validationError) {
|
|
@@ -177,7 +204,8 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
177
204
|
const compactionModelResult = await parseCompactionModelConfig(
|
|
178
205
|
argv,
|
|
179
206
|
providerID,
|
|
180
|
-
modelID
|
|
207
|
+
modelID,
|
|
208
|
+
defaultOptions
|
|
181
209
|
);
|
|
182
210
|
|
|
183
211
|
// Handle --use-existing-claude-oauth option
|
|
@@ -206,7 +234,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
206
234
|
|
|
207
235
|
// If user specified the default model (DEFAULT_MODEL), switch to claude-oauth
|
|
208
236
|
// If user explicitly specified kilo or another provider, warn but respect their choice
|
|
209
|
-
if (providerID ===
|
|
237
|
+
if (providerID === defaultProviderID && modelID === defaultModelID) {
|
|
210
238
|
providerID = 'claude-oauth';
|
|
211
239
|
modelID = 'claude-sonnet-4-5';
|
|
212
240
|
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
|
@@ -290,20 +318,28 @@ async function resolveCompactionModelEntry(
|
|
|
290
318
|
* @see https://github.com/link-assistant/agent/issues/219
|
|
291
319
|
* @see https://github.com/link-assistant/agent/issues/232
|
|
292
320
|
*/
|
|
293
|
-
async function parseCompactionModelConfig(
|
|
321
|
+
async function parseCompactionModelConfig(
|
|
322
|
+
argv,
|
|
323
|
+
baseProviderID,
|
|
324
|
+
baseModelID,
|
|
325
|
+
defaultOptions = {}
|
|
326
|
+
) {
|
|
327
|
+
const defaultCompactionSafetyMarginPercent =
|
|
328
|
+
getDefaultCompactionSafetyMarginPercent(defaultOptions);
|
|
329
|
+
|
|
294
330
|
// Get safety margin from CLI
|
|
295
331
|
const cliSafetyMarginArg = getCompactionSafetyMarginFromProcessArgv();
|
|
296
332
|
const compactionSafetyMarginPercent = cliSafetyMarginArg
|
|
297
333
|
? parseInt(cliSafetyMarginArg, 10)
|
|
298
334
|
: (argv['compaction-safety-margin'] ??
|
|
299
|
-
|
|
335
|
+
defaultCompactionSafetyMarginPercent);
|
|
300
336
|
|
|
301
337
|
// Check for --compaction-models (cascade) first — it overrides --compaction-model
|
|
302
338
|
const cliCompactionModelsArg = getCompactionModelsFromProcessArgv();
|
|
303
339
|
const compactionModelsArg =
|
|
304
340
|
cliCompactionModelsArg ??
|
|
305
341
|
argv['compaction-models'] ??
|
|
306
|
-
|
|
342
|
+
getDefaultCompactionModels(defaultOptions);
|
|
307
343
|
|
|
308
344
|
// Parse the links notation sequence into an array of model names
|
|
309
345
|
const modelNames = parseLinksNotationSequence(compactionModelsArg);
|
|
@@ -363,7 +399,7 @@ async function parseCompactionModelConfig(argv, baseProviderID, baseModelID) {
|
|
|
363
399
|
const compactionModelArg =
|
|
364
400
|
cliCompactionModelArg ??
|
|
365
401
|
argv['compaction-model'] ??
|
|
366
|
-
|
|
402
|
+
getDefaultCompactionModel(defaultOptions);
|
|
367
403
|
|
|
368
404
|
const resolved = await resolveCompactionModelEntry(
|
|
369
405
|
compactionModelArg,
|
package/src/cli/run-options.js
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
getDefaultModel,
|
|
3
|
+
getDefaultCompactionModel,
|
|
4
|
+
getDefaultCompactionModels,
|
|
5
|
+
getDefaultCompactionSafetyMarginPercent,
|
|
6
6
|
} from './defaults.ts';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Yargs builder for the default `run` command options.
|
|
10
10
|
* Extracted from index.js to keep file size under 1000 lines.
|
|
11
11
|
*/
|
|
12
|
-
export function buildRunOptions(yargs) {
|
|
13
|
-
|
|
12
|
+
export function buildRunOptions(yargs, defaultOptions = {}) {
|
|
13
|
+
const defaultModel = getDefaultModel(defaultOptions);
|
|
14
|
+
const defaultCompactionModel = getDefaultCompactionModel(defaultOptions);
|
|
15
|
+
const defaultCompactionModels = getDefaultCompactionModels(defaultOptions);
|
|
16
|
+
const defaultCompactionSafetyMarginPercent =
|
|
17
|
+
getDefaultCompactionSafetyMarginPercent(defaultOptions);
|
|
18
|
+
|
|
19
|
+
const parser = yargs
|
|
14
20
|
.option('model', {
|
|
15
21
|
type: 'string',
|
|
16
22
|
description: 'Model to use in format providerID/modelID',
|
|
17
|
-
default:
|
|
23
|
+
default: defaultModel,
|
|
18
24
|
})
|
|
19
25
|
.option('json-standard', {
|
|
20
26
|
type: 'string',
|
|
@@ -120,7 +126,19 @@ export function buildRunOptions(yargs) {
|
|
|
120
126
|
description:
|
|
121
127
|
'When used with --resume or --continue, continue in the same session without forking to a new UUID.',
|
|
122
128
|
default: false,
|
|
123
|
-
})
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const normalizedParser =
|
|
132
|
+
typeof parser.middleware === 'function'
|
|
133
|
+
? parser.middleware((argv) => {
|
|
134
|
+
if (argv.fork === false) {
|
|
135
|
+
argv['no-fork'] = true;
|
|
136
|
+
argv.noFork = true;
|
|
137
|
+
}
|
|
138
|
+
}, true)
|
|
139
|
+
: parser;
|
|
140
|
+
|
|
141
|
+
return normalizedParser
|
|
124
142
|
.option('generate-title', {
|
|
125
143
|
type: 'boolean',
|
|
126
144
|
description:
|
|
@@ -153,7 +171,7 @@ export function buildRunOptions(yargs) {
|
|
|
153
171
|
type: 'string',
|
|
154
172
|
description:
|
|
155
173
|
'Model to use for context compaction in format providerID/modelID. Use "same" to use the base model. Default: opencode/gpt-5-nano (free, 400K context). Overridden by --compaction-models if both are specified.',
|
|
156
|
-
default:
|
|
174
|
+
default: defaultCompactionModel,
|
|
157
175
|
})
|
|
158
176
|
.option('compaction-models', {
|
|
159
177
|
type: 'string',
|
|
@@ -161,13 +179,13 @@ export function buildRunOptions(yargs) {
|
|
|
161
179
|
'Ordered cascade of compaction models in links notation sequence format: "(model1 model2 ... same)". ' +
|
|
162
180
|
"Models are tried from smallest/cheapest context to largest. If used context exceeds a model's limit or its rate limit is reached, the next model is tried. " +
|
|
163
181
|
'The special value "same" uses the base model. Overrides --compaction-model when specified.',
|
|
164
|
-
default:
|
|
182
|
+
default: defaultCompactionModels,
|
|
165
183
|
})
|
|
166
184
|
.option('compaction-safety-margin', {
|
|
167
185
|
type: 'number',
|
|
168
186
|
description:
|
|
169
|
-
'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default:
|
|
170
|
-
default:
|
|
187
|
+
'Safety margin (%) of usable context window before triggering compaction. Only applies when the compaction model has equal or smaller context than the base model. Default: 25.',
|
|
188
|
+
default: defaultCompactionSafetyMarginPercent,
|
|
171
189
|
})
|
|
172
190
|
.option('temperature', {
|
|
173
191
|
type: 'number',
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global default model configuration shared by runtime code and tests.
|
|
3
|
+
*
|
|
4
|
+
* Keep the hard-coded defaults here. Runtime helpers allow test runs and
|
|
5
|
+
* local automation to override those defaults without editing source files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type EnvLike = Record<string, string | undefined>;
|
|
9
|
+
|
|
10
|
+
export interface DefaultConfigOptions {
|
|
11
|
+
env?: EnvLike;
|
|
12
|
+
defaultModel?: string | null;
|
|
13
|
+
defaultCompactionModel?: string | null;
|
|
14
|
+
defaultCompactionModels?: string | null;
|
|
15
|
+
defaultCompactionSafetyMarginPercent?: number | string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Default model used when no `--model` CLI argument is provided. */
|
|
19
|
+
export const DEFAULT_MODEL = 'opencode/minimax-m2.5-free';
|
|
20
|
+
|
|
21
|
+
/** Env var for overriding the default model in test runs and automation. */
|
|
22
|
+
export const DEFAULT_MODEL_ENV = 'LINK_ASSISTANT_AGENT_DEFAULT_MODEL';
|
|
23
|
+
|
|
24
|
+
/** Default compaction model used when no `--compaction-model` CLI argument is provided. */
|
|
25
|
+
export const DEFAULT_COMPACTION_MODEL = 'opencode/gpt-5-nano';
|
|
26
|
+
|
|
27
|
+
/** Env var for overriding the default compaction model in test runs and automation. */
|
|
28
|
+
export const DEFAULT_COMPACTION_MODEL_ENV =
|
|
29
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODEL';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default compaction models cascade, ordered from smallest/cheapest context to largest.
|
|
33
|
+
* During compaction, the system tries each model in order. If the used context exceeds
|
|
34
|
+
* a model's context limit, it skips to the next larger model. If a model's rate limit
|
|
35
|
+
* is reached, it also skips to the next model.
|
|
36
|
+
* The special value "same" means use the same model as `--model`.
|
|
37
|
+
*
|
|
38
|
+
* Parsed as links notation references sequence (single anonymous link):
|
|
39
|
+
* "(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)"
|
|
40
|
+
*
|
|
41
|
+
* Context limits (approximate):
|
|
42
|
+
* big-pickle: ~200K
|
|
43
|
+
* minimax-m2.5-free: ~204K
|
|
44
|
+
* nemotron-3-super-free: ~204K
|
|
45
|
+
* hy3-preview-free: ~256K
|
|
46
|
+
* ling-2.6-flash-free: ~262K
|
|
47
|
+
* gpt-5-nano: ~400K
|
|
48
|
+
* same: (base model's context)
|
|
49
|
+
*
|
|
50
|
+
* Note: qwen3.6-plus-free was removed because the free promotion ended in April 2026.
|
|
51
|
+
* Note: minimax-m2.5-free is the default model again as of issue #266.
|
|
52
|
+
* @see https://github.com/link-assistant/agent/issues/266
|
|
53
|
+
* @see https://github.com/link-assistant/agent/issues/242
|
|
54
|
+
* @see https://github.com/link-assistant/agent/issues/232
|
|
55
|
+
*/
|
|
56
|
+
export const DEFAULT_COMPACTION_MODELS =
|
|
57
|
+
'(big-pickle minimax-m2.5-free nemotron-3-super-free hy3-preview-free ling-2.6-flash-free gpt-5-nano same)';
|
|
58
|
+
|
|
59
|
+
/** Env var for overriding the default compaction cascade in test runs and automation. */
|
|
60
|
+
export const DEFAULT_COMPACTION_MODELS_ENV =
|
|
61
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_MODELS';
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Default compaction safety margin as a percentage of usable context window.
|
|
65
|
+
* Applied only when the compaction model has a context window equal to or smaller
|
|
66
|
+
* than the base model. When the compaction model has a larger context, the margin
|
|
67
|
+
* is automatically set to 0 (allowing 100% context usage).
|
|
68
|
+
*
|
|
69
|
+
* Increased from 15% to 25% to reduce probability of context overflow errors,
|
|
70
|
+
* especially when providers return inaccurate or zero token counts.
|
|
71
|
+
* Matches OpenCode upstream's 75% threshold (25% margin).
|
|
72
|
+
* @see https://github.com/link-assistant/agent/issues/219
|
|
73
|
+
* @see https://github.com/link-assistant/agent/issues/249
|
|
74
|
+
*/
|
|
75
|
+
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT = 25;
|
|
76
|
+
|
|
77
|
+
/** Env var for overriding the default compaction safety margin in test runs and automation. */
|
|
78
|
+
export const DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT_ENV =
|
|
79
|
+
'LINK_ASSISTANT_AGENT_DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT';
|
|
80
|
+
|
|
81
|
+
function clean(value: string | null | undefined): string | undefined {
|
|
82
|
+
const trimmed = value?.trim();
|
|
83
|
+
return trimmed ? trimmed : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function optionString(value: string | null | undefined): string | undefined {
|
|
87
|
+
return clean(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function envString(env: EnvLike, key: string): string | undefined {
|
|
91
|
+
return clean(env[key]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function optionNumber(
|
|
95
|
+
value: number | string | null | undefined
|
|
96
|
+
): number | undefined {
|
|
97
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
98
|
+
if (typeof value === 'string') {
|
|
99
|
+
const parsed = Number.parseInt(value.trim(), 10);
|
|
100
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
101
|
+
}
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Resolve the effective default model. CLI `--model` still takes precedence later. */
|
|
106
|
+
export function getDefaultModel(options: DefaultConfigOptions = {}): string {
|
|
107
|
+
const env = options.env ?? process.env;
|
|
108
|
+
return (
|
|
109
|
+
optionString(options.defaultModel) ??
|
|
110
|
+
envString(env, DEFAULT_MODEL_ENV) ??
|
|
111
|
+
DEFAULT_MODEL
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getDefaultCompactionModel(
|
|
116
|
+
options: DefaultConfigOptions = {}
|
|
117
|
+
): string {
|
|
118
|
+
const env = options.env ?? process.env;
|
|
119
|
+
return (
|
|
120
|
+
optionString(options.defaultCompactionModel) ??
|
|
121
|
+
envString(env, DEFAULT_COMPACTION_MODEL_ENV) ??
|
|
122
|
+
DEFAULT_COMPACTION_MODEL
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getDefaultCompactionModels(
|
|
127
|
+
options: DefaultConfigOptions = {}
|
|
128
|
+
): string {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
return (
|
|
131
|
+
optionString(options.defaultCompactionModels) ??
|
|
132
|
+
envString(env, DEFAULT_COMPACTION_MODELS_ENV) ??
|
|
133
|
+
DEFAULT_COMPACTION_MODELS
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function getDefaultCompactionSafetyMarginPercent(
|
|
138
|
+
options: DefaultConfigOptions = {}
|
|
139
|
+
): number {
|
|
140
|
+
const env = options.env ?? process.env;
|
|
141
|
+
return (
|
|
142
|
+
optionNumber(options.defaultCompactionSafetyMarginPercent) ??
|
|
143
|
+
optionNumber(env[DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT_ENV]) ??
|
|
144
|
+
DEFAULT_COMPACTION_SAFETY_MARGIN_PERCENT
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function parseModelParts(model: string): {
|
|
149
|
+
providerID: string;
|
|
150
|
+
modelID: string;
|
|
151
|
+
} {
|
|
152
|
+
const [providerID, ...modelParts] = model.split('/');
|
|
153
|
+
return {
|
|
154
|
+
providerID,
|
|
155
|
+
modelID: modelParts.join('/'),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function getDefaultModelParts(options: DefaultConfigOptions = {}): {
|
|
160
|
+
providerID: string;
|
|
161
|
+
modelID: string;
|
|
162
|
+
} {
|
|
163
|
+
return parseModelParts(getDefaultModel(options));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Default provider ID extracted from DEFAULT_MODEL. */
|
|
167
|
+
export const DEFAULT_PROVIDER_ID = parseModelParts(DEFAULT_MODEL).providerID;
|
|
168
|
+
|
|
169
|
+
/** Default model ID extracted from DEFAULT_MODEL. */
|
|
170
|
+
export const DEFAULT_MODEL_ID = parseModelParts(DEFAULT_MODEL).modelID;
|
package/src/index.js
CHANGED
|
@@ -488,7 +488,7 @@ async function runServerMode(
|
|
|
488
488
|
});
|
|
489
489
|
unsub = eventUnsub;
|
|
490
490
|
|
|
491
|
-
// Send message to session with specified model
|
|
491
|
+
// Send message to session with specified model
|
|
492
492
|
const message = request.message || 'hi';
|
|
493
493
|
const parts = [{ type: 'text', text: message }];
|
|
494
494
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Log } from '../util/log';
|
|
2
|
+
import type { ModelsDev } from './models';
|
|
3
|
+
|
|
4
|
+
export namespace OpenCodeZen {
|
|
5
|
+
const log = Log.create({ service: 'opencode-zen' });
|
|
6
|
+
const MODELS_URL = 'https://opencode.ai/zen/v1/models';
|
|
7
|
+
const DEPRECATED_FREE_MODEL_IDS = new Set(['trinity-large-preview-free']);
|
|
8
|
+
|
|
9
|
+
const FREE_MODEL_OVERRIDES: Record<
|
|
10
|
+
string,
|
|
11
|
+
Partial<ModelsDev.Model> & { name: string }
|
|
12
|
+
> = {
|
|
13
|
+
'minimax-m2.5-free': {
|
|
14
|
+
name: 'MiniMax M2.5 Free',
|
|
15
|
+
reasoning: true,
|
|
16
|
+
release_date: '2026-02-12',
|
|
17
|
+
limit: { context: 204800, output: 131072 },
|
|
18
|
+
provider: { npm: '@ai-sdk/openai-compatible' },
|
|
19
|
+
},
|
|
20
|
+
'ling-2.6-flash-free': {
|
|
21
|
+
name: 'Ling 2.6 Flash Free',
|
|
22
|
+
reasoning: false,
|
|
23
|
+
release_date: '2026-04-21',
|
|
24
|
+
limit: { context: 262100, output: 32800 },
|
|
25
|
+
provider: { npm: '@ai-sdk/openai-compatible' },
|
|
26
|
+
},
|
|
27
|
+
'hy3-preview-free': {
|
|
28
|
+
name: 'Hy3 Preview Free',
|
|
29
|
+
reasoning: true,
|
|
30
|
+
release_date: '2026-04-20',
|
|
31
|
+
limit: { context: 256000, output: 64000 },
|
|
32
|
+
provider: { npm: '@ai-sdk/openai-compatible' },
|
|
33
|
+
},
|
|
34
|
+
'nemotron-3-super-free': {
|
|
35
|
+
name: 'Nemotron 3 Super Free',
|
|
36
|
+
reasoning: true,
|
|
37
|
+
release_date: '2026-03-11',
|
|
38
|
+
limit: { context: 204800, output: 128000 },
|
|
39
|
+
provider: { npm: '@ai-sdk/openai-compatible' },
|
|
40
|
+
},
|
|
41
|
+
'gpt-5-nano': {
|
|
42
|
+
name: 'GPT-5 Nano',
|
|
43
|
+
attachment: true,
|
|
44
|
+
reasoning: true,
|
|
45
|
+
temperature: false,
|
|
46
|
+
release_date: '2025-08-07',
|
|
47
|
+
limit: { context: 400000, output: 128000 },
|
|
48
|
+
provider: { npm: '@ai-sdk/openai' },
|
|
49
|
+
},
|
|
50
|
+
'big-pickle': {
|
|
51
|
+
name: 'Big Pickle',
|
|
52
|
+
reasoning: true,
|
|
53
|
+
release_date: '2025-10-17',
|
|
54
|
+
limit: { context: 200000, output: 128000 },
|
|
55
|
+
provider: { npm: '@ai-sdk/openai-compatible' },
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function displayName(modelID: string) {
|
|
60
|
+
return modelID
|
|
61
|
+
.replace(/:free$/, '')
|
|
62
|
+
.replace(/-free$/, ' Free')
|
|
63
|
+
.split(/[-/]/)
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
66
|
+
.join(' ');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isFreeModelID(modelID: string) {
|
|
70
|
+
if (DEPRECATED_FREE_MODEL_IDS.has(modelID)) return false;
|
|
71
|
+
return (
|
|
72
|
+
modelID.endsWith('-free') ||
|
|
73
|
+
modelID === 'gpt-5-nano' ||
|
|
74
|
+
modelID === 'big-pickle'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function fetchModelIDs(fetchFn: typeof fetch = fetch) {
|
|
79
|
+
const response = await fetchFn(MODELS_URL, {
|
|
80
|
+
headers: { 'User-Agent': 'agent-cli/1.0.0' },
|
|
81
|
+
signal: AbortSignal.timeout(10_000),
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
log.warn(() => ({
|
|
85
|
+
message: 'OpenCode Zen models endpoint returned non-OK response',
|
|
86
|
+
status: response.status,
|
|
87
|
+
statusText: response.statusText,
|
|
88
|
+
}));
|
|
89
|
+
return new Set<string>();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const body = (await response.json().catch(() => undefined)) as
|
|
93
|
+
| { data?: Array<{ id?: unknown }> }
|
|
94
|
+
| undefined;
|
|
95
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
96
|
+
return new Set(
|
|
97
|
+
rows
|
|
98
|
+
.map((row) => (typeof row.id === 'string' ? row.id : undefined))
|
|
99
|
+
.filter((id): id is string => Boolean(id))
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getLiveFreeModelInfo(
|
|
104
|
+
modelID: string,
|
|
105
|
+
fetchFn: typeof fetch = fetch
|
|
106
|
+
): Promise<ModelsDev.Model | undefined> {
|
|
107
|
+
if (!isFreeModelID(modelID)) return undefined;
|
|
108
|
+
|
|
109
|
+
const ids = await fetchModelIDs(fetchFn).catch((error) => {
|
|
110
|
+
log.warn(() => ({
|
|
111
|
+
message: 'failed to fetch OpenCode Zen live model list',
|
|
112
|
+
modelID,
|
|
113
|
+
error: error instanceof Error ? error.message : String(error),
|
|
114
|
+
}));
|
|
115
|
+
return new Set<string>();
|
|
116
|
+
});
|
|
117
|
+
if (!ids.has(modelID)) return undefined;
|
|
118
|
+
|
|
119
|
+
const overrides = FREE_MODEL_OVERRIDES[modelID];
|
|
120
|
+
return {
|
|
121
|
+
id: modelID,
|
|
122
|
+
name: overrides?.name ?? displayName(modelID),
|
|
123
|
+
release_date: overrides?.release_date ?? '',
|
|
124
|
+
attachment: overrides?.attachment ?? false,
|
|
125
|
+
reasoning: overrides?.reasoning ?? modelID.includes('reasoning'),
|
|
126
|
+
temperature: overrides?.temperature ?? true,
|
|
127
|
+
tool_call: overrides?.tool_call ?? true,
|
|
128
|
+
cost: overrides?.cost ?? { input: 0, output: 0, cache_read: 0 },
|
|
129
|
+
limit: overrides?.limit ?? { context: 128000, output: 16384 },
|
|
130
|
+
modalities: overrides?.modalities ?? {
|
|
131
|
+
input: ['text'],
|
|
132
|
+
output: ['text'],
|
|
133
|
+
},
|
|
134
|
+
options: overrides?.options ?? {},
|
|
135
|
+
provider: overrides?.provider ?? { npm: '@ai-sdk/openai-compatible' },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
}
|
package/src/provider/provider.ts
CHANGED
|
@@ -13,11 +13,13 @@ import { AuthPlugins } from '../auth/plugins';
|
|
|
13
13
|
import { Instance } from '../project/instance';
|
|
14
14
|
import { Global } from '../global';
|
|
15
15
|
import { config, isVerbose } from '../config/config';
|
|
16
|
+
import { getDefaultModel, getDefaultModelParts } from '../config/defaults';
|
|
16
17
|
import { iife } from '../util/iife';
|
|
17
18
|
import { createEchoModel } from './echo';
|
|
18
19
|
import { createCacheModel } from './cache';
|
|
19
20
|
import { RetryFetch } from './retry-fetch';
|
|
20
21
|
import { SSEUsageExtractor } from '../util/sse-usage-extractor';
|
|
22
|
+
import { OpenCodeZen } from './opencode-zen';
|
|
21
23
|
|
|
22
24
|
// Direct imports for bundled providers - these are pre-installed to avoid runtime installation hangs
|
|
23
25
|
// @see https://github.com/link-assistant/agent/issues/173
|
|
@@ -1596,6 +1598,31 @@ export namespace Provider {
|
|
|
1596
1598
|
return state().then((s) => s.providers[providerID]);
|
|
1597
1599
|
}
|
|
1598
1600
|
|
|
1601
|
+
async function getLiveModelInfo(providerID: string, modelID: string) {
|
|
1602
|
+
if (providerID !== 'opencode') return undefined;
|
|
1603
|
+
return OpenCodeZen.getLiveFreeModelInfo(modelID);
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
export async function refreshLiveModelInfo(
|
|
1607
|
+
providerID: string,
|
|
1608
|
+
modelID: string
|
|
1609
|
+
) {
|
|
1610
|
+
const info = await getLiveModelInfo(providerID, modelID);
|
|
1611
|
+
if (!info) return undefined;
|
|
1612
|
+
|
|
1613
|
+
const s = await state();
|
|
1614
|
+
const provider = s.providers[providerID];
|
|
1615
|
+
if (provider) {
|
|
1616
|
+
provider.info.models[modelID] = info;
|
|
1617
|
+
log.info(() => ({
|
|
1618
|
+
message: 'model found in provider live model endpoint',
|
|
1619
|
+
providerID,
|
|
1620
|
+
modelID,
|
|
1621
|
+
}));
|
|
1622
|
+
}
|
|
1623
|
+
return info;
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1599
1626
|
export async function getModel(providerID: string, modelID: string) {
|
|
1600
1627
|
const key = `${providerID}/${modelID}`;
|
|
1601
1628
|
const s = await state();
|
|
@@ -1662,15 +1689,28 @@ export namespace Provider {
|
|
|
1662
1689
|
}
|
|
1663
1690
|
}
|
|
1664
1691
|
|
|
1692
|
+
if (!isSyntheticProvider && !info) {
|
|
1693
|
+
const liveInfo = await getLiveModelInfo(providerID, modelID);
|
|
1694
|
+
if (liveInfo) {
|
|
1695
|
+
provider.info.models[modelID] = liveInfo;
|
|
1696
|
+
info = liveInfo;
|
|
1697
|
+
log.info(() => ({
|
|
1698
|
+
message: 'model found in provider live model endpoint',
|
|
1699
|
+
providerID,
|
|
1700
|
+
modelID,
|
|
1701
|
+
}));
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1665
1705
|
if (!isSyntheticProvider && !info) {
|
|
1666
1706
|
// Model not found even after cache refresh.
|
|
1667
1707
|
// Check if this is the default model — if so, create synthetic info and proceed (#239).
|
|
1668
1708
|
// The models.dev API can lag behind the provider's actual model availability.
|
|
1669
1709
|
// For user-specified models, fail with a clear error (#231) to prevent silent substitution.
|
|
1670
|
-
const {
|
|
1671
|
-
|
|
1710
|
+
const { providerID: defaultProviderID, modelID: defaultModelID } =
|
|
1711
|
+
getDefaultModelParts();
|
|
1672
1712
|
const isDefaultModel =
|
|
1673
|
-
providerID ===
|
|
1713
|
+
providerID === defaultProviderID && modelID === defaultModelID;
|
|
1674
1714
|
const availableInProvider = Object.keys(provider.info.models).slice(
|
|
1675
1715
|
0,
|
|
1676
1716
|
10
|
|
@@ -1793,8 +1833,10 @@ export namespace Provider {
|
|
|
1793
1833
|
}
|
|
1794
1834
|
if (providerID === 'opencode' || providerID === 'local') {
|
|
1795
1835
|
priority = [
|
|
1796
|
-
'nemotron-3-super-free',
|
|
1797
1836
|
'minimax-m2.5-free',
|
|
1837
|
+
'ling-2.6-flash-free',
|
|
1838
|
+
'hy3-preview-free',
|
|
1839
|
+
'nemotron-3-super-free',
|
|
1798
1840
|
'gpt-5-nano',
|
|
1799
1841
|
'big-pickle',
|
|
1800
1842
|
];
|
|
@@ -1824,11 +1866,13 @@ export namespace Provider {
|
|
|
1824
1866
|
}
|
|
1825
1867
|
|
|
1826
1868
|
const priority = [
|
|
1869
|
+
'big-pickle',
|
|
1870
|
+
'gpt-5-nano',
|
|
1827
1871
|
'nemotron-3-super-free',
|
|
1828
|
-
'
|
|
1872
|
+
'hy3-preview-free',
|
|
1873
|
+
'ling-2.6-flash-free',
|
|
1829
1874
|
'minimax-m2.5-free',
|
|
1830
|
-
'
|
|
1831
|
-
'big-pickle',
|
|
1875
|
+
'glm-5-free',
|
|
1832
1876
|
'gpt-5',
|
|
1833
1877
|
'claude-sonnet-4',
|
|
1834
1878
|
'gemini-3-pro',
|
|
@@ -1861,6 +1905,15 @@ export namespace Provider {
|
|
|
1861
1905
|
|
|
1862
1906
|
if (cfg.model) return parseModel(cfg.model);
|
|
1863
1907
|
|
|
1908
|
+
const configuredDefaultModel = getDefaultModel();
|
|
1909
|
+
if (configuredDefaultModel) {
|
|
1910
|
+
log.info(() => ({
|
|
1911
|
+
message: 'using configured default model',
|
|
1912
|
+
model: configuredDefaultModel,
|
|
1913
|
+
}));
|
|
1914
|
+
return parseModel(configuredDefaultModel);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1864
1917
|
// Prefer opencode provider if available
|
|
1865
1918
|
const providers = await list().then((val) => Object.values(val));
|
|
1866
1919
|
const opencodeProvider = providers.find((p) => p.info.id === 'opencode');
|
|
@@ -1907,8 +1960,8 @@ export namespace Provider {
|
|
|
1907
1960
|
* Priority for free models:
|
|
1908
1961
|
* 1. If model is uniquely available in one provider, use that provider
|
|
1909
1962
|
* 2. If model is available in multiple providers, prioritize based on free model availability:
|
|
1910
|
-
* - kilo: glm-5-free, glm-4.5-air-free,
|
|
1911
|
-
* - opencode: big-pickle, gpt-5-nano, nemotron-3-super-free
|
|
1963
|
+
* - kilo: glm-5-free, glm-4.5-air-free, giga-potato-free, deepseek-r1-free (unique to Kilo)
|
|
1964
|
+
* - opencode: minimax-m2.5-free, ling-2.6-flash-free, hy3-preview-free, big-pickle, gpt-5-nano, nemotron-3-super-free
|
|
1912
1965
|
* 3. For shared models, prefer OpenCode first, then fall back to Kilo on rate limit
|
|
1913
1966
|
*
|
|
1914
1967
|
* @param modelID - Short model name without provider prefix
|
|
@@ -1924,7 +1977,6 @@ export namespace Provider {
|
|
|
1924
1977
|
const kiloUniqueModels = [
|
|
1925
1978
|
'glm-5-free',
|
|
1926
1979
|
'glm-4.5-air-free',
|
|
1927
|
-
'minimax-m2.5-free',
|
|
1928
1980
|
'giga-potato-free',
|
|
1929
1981
|
'trinity-large-preview',
|
|
1930
1982
|
'deepseek-r1-free',
|
|
@@ -2060,8 +2112,7 @@ export namespace Provider {
|
|
|
2060
2112
|
* If user specifies "kilo/deepseek-r1-free", no fallback will occur.
|
|
2061
2113
|
*/
|
|
2062
2114
|
const SHARED_FREE_MODELS: Record<string, string[]> = {
|
|
2063
|
-
|
|
2064
|
-
// Kilo models use different IDs than OpenCode models.
|
|
2115
|
+
'minimax-m2.5-free': ['opencode', 'kilo'],
|
|
2065
2116
|
};
|
|
2066
2117
|
|
|
2067
2118
|
/**
|
|
@@ -849,6 +849,48 @@ export namespace MessageV2 {
|
|
|
849
849
|
return result;
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
+
function normalizeProviderErrorObject(e: unknown) {
|
|
853
|
+
if (!e || typeof e !== 'object') return;
|
|
854
|
+
|
|
855
|
+
const root = e as Record<string, unknown>;
|
|
856
|
+
const candidate =
|
|
857
|
+
root['error'] && typeof root['error'] === 'object'
|
|
858
|
+
? (root['error'] as Record<string, unknown>)
|
|
859
|
+
: root;
|
|
860
|
+
const rawCode =
|
|
861
|
+
candidate['code'] ?? candidate['statusCode'] ?? candidate['status'];
|
|
862
|
+
const statusCode =
|
|
863
|
+
typeof rawCode === 'number'
|
|
864
|
+
? rawCode
|
|
865
|
+
: typeof rawCode === 'string' && /^\d+$/.test(rawCode)
|
|
866
|
+
? Number.parseInt(rawCode, 10)
|
|
867
|
+
: undefined;
|
|
868
|
+
|
|
869
|
+
if (statusCode === undefined) return;
|
|
870
|
+
|
|
871
|
+
let responseBody: string | undefined;
|
|
872
|
+
try {
|
|
873
|
+
responseBody = JSON.stringify(e);
|
|
874
|
+
} catch {
|
|
875
|
+
responseBody = undefined;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
const message =
|
|
879
|
+
typeof candidate['message'] === 'string'
|
|
880
|
+
? candidate['message']
|
|
881
|
+
: typeof root['message'] === 'string'
|
|
882
|
+
? root['message']
|
|
883
|
+
: `Provider returned HTTP ${statusCode}`;
|
|
884
|
+
|
|
885
|
+
return {
|
|
886
|
+
message,
|
|
887
|
+
statusCode,
|
|
888
|
+
isRetryable:
|
|
889
|
+
statusCode === 429 || (statusCode >= 500 && statusCode < 600),
|
|
890
|
+
responseBody,
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
|
|
852
894
|
export function fromError(e: unknown, ctx: { providerID: string }) {
|
|
853
895
|
switch (true) {
|
|
854
896
|
case e instanceof DOMException && e.name === 'AbortError':
|
|
@@ -884,6 +926,10 @@ export namespace MessageV2 {
|
|
|
884
926
|
},
|
|
885
927
|
{ cause: e }
|
|
886
928
|
).toObject();
|
|
929
|
+
case normalizeProviderErrorObject(e) !== undefined: {
|
|
930
|
+
const providerError = normalizeProviderErrorObject(e)!;
|
|
931
|
+
return new MessageV2.APIError(providerError, { cause: e }).toObject();
|
|
932
|
+
}
|
|
887
933
|
case e instanceof Error: {
|
|
888
934
|
const message = e.message || e.toString();
|
|
889
935
|
// Detect Bun socket connection errors (known Bun issue with 10s idle timeout)
|
package/src/session/processor.ts
CHANGED
|
@@ -52,6 +52,40 @@ export namespace SessionProcessor {
|
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Detect AI SDK/provider usage-shape crashes.
|
|
57
|
+
*
|
|
58
|
+
* Some provider error streams end before a valid finish-step usage object is
|
|
59
|
+
* available. Depending on the provider adapter, that can surface as a
|
|
60
|
+
* TypeError while reading token fields instead of the original provider
|
|
61
|
+
* error. Treat it as retryable provider failure rather than a generic crash.
|
|
62
|
+
*
|
|
63
|
+
* See: https://github.com/link-assistant/agent/issues/264
|
|
64
|
+
*/
|
|
65
|
+
export function isUsageDataTypeError(error: unknown): boolean {
|
|
66
|
+
const message =
|
|
67
|
+
error instanceof Error
|
|
68
|
+
? `${error.name}: ${error.message}`
|
|
69
|
+
: typeof error === 'string'
|
|
70
|
+
? error
|
|
71
|
+
: '';
|
|
72
|
+
|
|
73
|
+
if (!(error instanceof TypeError) && !message.startsWith('TypeError:')) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
'input_tokens',
|
|
79
|
+
'output_tokens',
|
|
80
|
+
'usage.inputTokens',
|
|
81
|
+
'usage.outputTokens',
|
|
82
|
+
'inputTokens.total',
|
|
83
|
+
'outputTokens.total',
|
|
84
|
+
"reading 'inputTokens'",
|
|
85
|
+
"reading 'outputTokens'",
|
|
86
|
+
].some((pattern) => message.includes(pattern));
|
|
87
|
+
}
|
|
88
|
+
|
|
55
89
|
export type Info = Awaited<ReturnType<typeof create>>;
|
|
56
90
|
export type Result = Awaited<ReturnType<Info['process']>>;
|
|
57
91
|
|
|
@@ -542,37 +576,34 @@ export namespace SessionProcessor {
|
|
|
542
576
|
} catch (e) {
|
|
543
577
|
log.error(() => ({ message: 'process', error: e }));
|
|
544
578
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
) {
|
|
579
|
+
let error = MessageV2.fromError(e, {
|
|
580
|
+
providerID: input.providerID,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
// Check for AI SDK usage-related TypeErrors. These usually come
|
|
584
|
+
// from provider adapters after malformed/incomplete usage data, so
|
|
585
|
+
// retry them with normal API-error backoff instead of surfacing a
|
|
586
|
+
// misleading UnknownError.
|
|
587
|
+
if (isUsageDataTypeError(e)) {
|
|
555
588
|
log.warn(() => ({
|
|
556
589
|
message:
|
|
557
|
-
'
|
|
558
|
-
errorMessage: e.message,
|
|
590
|
+
'provider returned invalid usage data; retrying as provider API error',
|
|
591
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
559
592
|
providerID: input.providerID,
|
|
593
|
+
issue: 'https://github.com/link-assistant/agent/issues/264',
|
|
560
594
|
}));
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
595
|
+
error = new MessageV2.APIError(
|
|
596
|
+
{
|
|
597
|
+
message:
|
|
598
|
+
e instanceof Error
|
|
599
|
+
? e.message
|
|
600
|
+
: 'Provider returned invalid usage data',
|
|
601
|
+
isRetryable: true,
|
|
602
|
+
},
|
|
603
|
+
{ cause: e }
|
|
604
|
+
).toObject();
|
|
570
605
|
}
|
|
571
606
|
|
|
572
|
-
const error = MessageV2.fromError(e, {
|
|
573
|
-
providerID: input.providerID,
|
|
574
|
-
});
|
|
575
|
-
|
|
576
607
|
// Check if error is retryable (APIError, SocketConnectionError, TimeoutError)
|
|
577
608
|
const isRetryableAPIError =
|
|
578
609
|
error?.name === 'APIError' && error.data.isRetryable;
|
package/src/tool/task.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { Agent } from '../agent/agent';
|
|
|
9
9
|
import { SessionPrompt } from '../session/prompt';
|
|
10
10
|
import { iife } from '../util/iife';
|
|
11
11
|
import { defer } from '../util/defer';
|
|
12
|
-
import {
|
|
12
|
+
import { getDefaultModelParts } from '../cli/defaults';
|
|
13
13
|
|
|
14
14
|
export const TaskTool = Tool.define('task', async () => {
|
|
15
15
|
const agents = await Agent.list().then((x) =>
|
|
@@ -98,10 +98,11 @@ export const TaskTool = Tool.define('task', async () => {
|
|
|
98
98
|
});
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
const defaultModel = getDefaultModelParts();
|
|
101
102
|
const model = agent.model ??
|
|
102
103
|
parentModel ?? {
|
|
103
|
-
modelID:
|
|
104
|
-
providerID:
|
|
104
|
+
modelID: defaultModel.modelID,
|
|
105
|
+
providerID: defaultModel.providerID,
|
|
105
106
|
};
|
|
106
107
|
|
|
107
108
|
function cancel() {
|