@link-assistant/agent 0.16.12 → 0.16.14
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 +6 -6
- package/package.json +1 -1
- package/src/cli/defaults.ts +15 -0
- package/src/cli/model-config.js +5 -4
- package/src/index.js +2 -1
- package/src/provider/provider.ts +4 -10
- package/src/session/processor.ts +59 -0
- package/src/tool/task.ts +3 -2
package/README.md
CHANGED
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
|
|
21
21
|
> This is the JavaScript/Bun implementation. See also the [Rust implementation](../rust/README.md).
|
|
22
22
|
|
|
23
|
-
This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/
|
|
23
|
+
This is an MVP implementation of an OpenCode-compatible CLI agent, focused on maximum efficiency and unrestricted execution. We reproduce OpenCode's `run --format json --model opencode/minimax-m2.5-free` mode with:
|
|
24
24
|
|
|
25
|
-
- ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/
|
|
25
|
+
- ✅ **JSON Input/Output**: Compatible with `opencode run --format json --model opencode/minimax-m2.5-free`
|
|
26
26
|
- ✅ **Plain Text Input**: Also accepts plain text messages (auto-converted to JSON format)
|
|
27
|
-
- ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen
|
|
27
|
+
- ✅ **Flexible Model Selection**: Defaults to free OpenCode Zen MiniMax M2.5 Free, supports [OpenCode Zen](https://opencode.ai/docs/zen/), [Claude OAuth](../docs/claude-oauth.md), [Groq](../docs/groq.md), and [OpenRouter](../docs/openrouter.md) providers
|
|
28
28
|
- ✅ **No Restrictions**: Fully unrestricted file system and command execution access (no sandbox)
|
|
29
29
|
- ✅ **Minimal Footprint**: Built with Bun.sh for maximum efficiency
|
|
30
30
|
- ✅ **Full Tool Support**: 13 tools including websearch, codesearch, batch - all enabled by default
|
|
@@ -169,7 +169,7 @@ echo '{"message":"hi"}' | agent
|
|
|
169
169
|
**With custom model:**
|
|
170
170
|
|
|
171
171
|
```bash
|
|
172
|
-
echo "hi" | agent --model opencode/
|
|
172
|
+
echo "hi" | agent --model opencode/minimax-m2.5-free
|
|
173
173
|
```
|
|
174
174
|
|
|
175
175
|
### More Examples
|
|
@@ -190,7 +190,7 @@ echo '{"message":"run command","tools":[{"name":"bash","params":{"command":"ls -
|
|
|
190
190
|
**Using different models:**
|
|
191
191
|
|
|
192
192
|
```bash
|
|
193
|
-
# Default model (free
|
|
193
|
+
# Default model (free MiniMax M2.5)
|
|
194
194
|
echo "hi" | agent
|
|
195
195
|
|
|
196
196
|
# Other free models (in order of recommendation)
|
|
@@ -280,7 +280,7 @@ agent [options]
|
|
|
280
280
|
|
|
281
281
|
Options:
|
|
282
282
|
--model Model to use in format providerID/modelID
|
|
283
|
-
Default: opencode/
|
|
283
|
+
Default: opencode/minimax-m2.5-free
|
|
284
284
|
--json-standard JSON output format standard
|
|
285
285
|
Choices: "opencode" (default), "claude" (experimental)
|
|
286
286
|
--use-existing-claude-oauth Use existing Claude OAuth credentials
|
package/package.json
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
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/minimax-m2.5-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('/');
|
package/src/cli/model-config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getModelFromProcessArgv } from './argv.ts';
|
|
2
2
|
import { Log } from '../util/log.ts';
|
|
3
|
+
import { DEFAULT_PROVIDER_ID, DEFAULT_MODEL_ID } from './defaults.ts';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Parse model config from argv. Supports "provider/model" or short "model" format.
|
|
@@ -16,7 +17,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
16
17
|
let modelArg = argv.model;
|
|
17
18
|
|
|
18
19
|
// ALWAYS prefer the CLI value over yargs when available (#196)
|
|
19
|
-
// The yargs default
|
|
20
|
+
// The yargs default (DEFAULT_MODEL) can silently override user's --model argument
|
|
20
21
|
if (cliModelArg) {
|
|
21
22
|
if (cliModelArg !== modelArg) {
|
|
22
23
|
Log.Default.warn(() => ({
|
|
@@ -45,7 +46,7 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
45
46
|
// Do NOT fall back to defaults - if the user provided an invalid format, fail clearly (#196)
|
|
46
47
|
if (!providerID || !modelID) {
|
|
47
48
|
throw new Error(
|
|
48
|
-
`Invalid model format: "${modelArg}". Expected "provider/model" format (e.g., "
|
|
49
|
+
`Invalid model format: "${modelArg}". Expected "provider/model" format (e.g., "${DEFAULT_PROVIDER_ID}/${DEFAULT_MODEL_ID}"). ` +
|
|
49
50
|
`Provider: "${providerID || '(empty)'}", Model: "${modelID || '(empty)'}".`
|
|
50
51
|
);
|
|
51
52
|
}
|
|
@@ -124,9 +125,9 @@ export async function parseModelConfig(argv, outputError, outputStatus) {
|
|
|
124
125
|
// Set environment variable for the provider to use
|
|
125
126
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = creds.accessToken;
|
|
126
127
|
|
|
127
|
-
// If user specified the default model (
|
|
128
|
+
// If user specified the default model (DEFAULT_MODEL), switch to claude-oauth
|
|
128
129
|
// If user explicitly specified kilo or another provider, warn but respect their choice
|
|
129
|
-
if (providerID ===
|
|
130
|
+
if (providerID === DEFAULT_PROVIDER_ID && modelID === DEFAULT_MODEL_ID) {
|
|
130
131
|
providerID = 'claude-oauth';
|
|
131
132
|
modelID = 'claude-sonnet-4-5';
|
|
132
133
|
} else if (!['claude-oauth', 'anthropic'].includes(providerID)) {
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Server } from './server/server.ts';
|
|
|
6
6
|
import { Instance } from './project/instance.ts';
|
|
7
7
|
import { Log } from './util/log.ts';
|
|
8
8
|
import { parseModelConfig } from './cli/model-config.js';
|
|
9
|
+
import { DEFAULT_MODEL } from './cli/defaults.ts';
|
|
9
10
|
// Bus is used via createBusEventSubscription in event-handler.js
|
|
10
11
|
import { Session } from './session/index.ts';
|
|
11
12
|
import { SessionPrompt } from './session/prompt.ts';
|
|
@@ -588,7 +589,7 @@ async function main() {
|
|
|
588
589
|
.option('model', {
|
|
589
590
|
type: 'string',
|
|
590
591
|
description: 'Model to use in format providerID/modelID',
|
|
591
|
-
default:
|
|
592
|
+
default: DEFAULT_MODEL,
|
|
592
593
|
})
|
|
593
594
|
.option('json-standard', {
|
|
594
595
|
type: 'string',
|
package/src/provider/provider.ts
CHANGED
|
@@ -1651,12 +1651,7 @@ export namespace Provider {
|
|
|
1651
1651
|
priority = priority.filter((m) => m !== 'claude-haiku-4.5');
|
|
1652
1652
|
}
|
|
1653
1653
|
if (providerID === 'opencode' || providerID === 'local') {
|
|
1654
|
-
priority = [
|
|
1655
|
-
'kimi-k2.5-free',
|
|
1656
|
-
'minimax-m2.5-free',
|
|
1657
|
-
'gpt-5-nano',
|
|
1658
|
-
'big-pickle',
|
|
1659
|
-
];
|
|
1654
|
+
priority = ['minimax-m2.5-free', 'gpt-5-nano', 'big-pickle'];
|
|
1660
1655
|
}
|
|
1661
1656
|
if (providerID === 'kilo') {
|
|
1662
1657
|
priority = [
|
|
@@ -1684,7 +1679,6 @@ export namespace Provider {
|
|
|
1684
1679
|
|
|
1685
1680
|
const priority = [
|
|
1686
1681
|
'glm-5-free',
|
|
1687
|
-
'kimi-k2.5-free',
|
|
1688
1682
|
'minimax-m2.5-free',
|
|
1689
1683
|
'gpt-5-nano',
|
|
1690
1684
|
'big-pickle',
|
|
@@ -1856,7 +1850,7 @@ export namespace Provider {
|
|
|
1856
1850
|
* Examples:
|
|
1857
1851
|
* - "kilo/glm-5-free" -> { providerID: "kilo", modelID: "glm-5-free" }
|
|
1858
1852
|
* - "glm-5-free" -> { providerID: "kilo", modelID: "glm-5-free" } (resolved)
|
|
1859
|
-
* - "
|
|
1853
|
+
* - "big-pickle" -> { providerID: "opencode", modelID: "big-pickle" } (resolved)
|
|
1860
1854
|
* - "nonexistent-model" -> throws ModelNotFoundError
|
|
1861
1855
|
*
|
|
1862
1856
|
* @param model - Model string with or without provider prefix
|
|
@@ -1916,7 +1910,7 @@ export namespace Provider {
|
|
|
1916
1910
|
* When one provider hits rate limits, the system can try an alternative.
|
|
1917
1911
|
*
|
|
1918
1912
|
* Note: This is only used for models without explicit provider specification.
|
|
1919
|
-
* If user specifies "kilo/
|
|
1913
|
+
* If user specifies "kilo/deepseek-r1-free", no fallback will occur.
|
|
1920
1914
|
*/
|
|
1921
1915
|
const SHARED_FREE_MODELS: Record<string, string[]> = {
|
|
1922
1916
|
// Currently no shared models between OpenCode and Kilo providers.
|
|
@@ -1928,7 +1922,7 @@ export namespace Provider {
|
|
|
1928
1922
|
* This function returns a list of alternative providers that offer the same model.
|
|
1929
1923
|
*
|
|
1930
1924
|
* Note: This only returns alternatives for models without explicit provider specification.
|
|
1931
|
-
* If the original request had an explicit provider (like "kilo/
|
|
1925
|
+
* If the original request had an explicit provider (like "kilo/deepseek-r1-free"), this returns empty array.
|
|
1932
1926
|
*
|
|
1933
1927
|
* @param modelID - The model ID to find alternatives for
|
|
1934
1928
|
* @param failedProviderID - The provider that failed
|
package/src/session/processor.ts
CHANGED
|
@@ -22,6 +22,34 @@ export namespace SessionProcessor {
|
|
|
22
22
|
const DOOM_LOOP_THRESHOLD = 3;
|
|
23
23
|
const log = Log.create({ service: 'session.processor' });
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Detect model-not-supported errors from an API response body.
|
|
27
|
+
*
|
|
28
|
+
* Some providers (e.g., OpenCode, OpenRouter) return HTTP 401 with a body
|
|
29
|
+
* like {"type":"ModelError","message":"Model X not supported"} instead of
|
|
30
|
+
* the semantically correct 400/404. Without this check the error looks like
|
|
31
|
+
* an authentication failure, making diagnostics confusing.
|
|
32
|
+
*
|
|
33
|
+
* See: https://github.com/link-assistant/agent/issues/208
|
|
34
|
+
*/
|
|
35
|
+
export function isModelNotSupportedError(responseBody: string): boolean {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(responseBody);
|
|
38
|
+
// OpenCode/OpenRouter format: {"type":"error","error":{"type":"ModelError","message":"..."}}
|
|
39
|
+
if (parsed?.error?.type === 'ModelError') return true;
|
|
40
|
+
// Flat format: {"type":"ModelError","message":"..."}
|
|
41
|
+
if (parsed?.type === 'ModelError') return true;
|
|
42
|
+
return false;
|
|
43
|
+
} catch {
|
|
44
|
+
// If response body is not JSON, check for common text patterns
|
|
45
|
+
return (
|
|
46
|
+
responseBody.includes('ModelError') ||
|
|
47
|
+
responseBody.toLowerCase().includes('model not supported') ||
|
|
48
|
+
responseBody.toLowerCase().includes('model not found')
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
export type Info = Awaited<ReturnType<typeof create>>;
|
|
26
54
|
export type Result = Awaited<ReturnType<Info['process']>>;
|
|
27
55
|
|
|
@@ -551,6 +579,37 @@ export namespace SessionProcessor {
|
|
|
551
579
|
|
|
552
580
|
// Clear retry state on non-retryable error
|
|
553
581
|
SessionRetry.clearRetryState(input.sessionID);
|
|
582
|
+
|
|
583
|
+
// Detect model-not-supported errors from provider response body.
|
|
584
|
+
// OpenCode and similar proxies return HTTP 401 with
|
|
585
|
+
// {"type":"ModelError","message":"Model X not supported"}.
|
|
586
|
+
// Without this check the error looks like an auth failure (401),
|
|
587
|
+
// but it's actually a model-availability issue.
|
|
588
|
+
// See: https://github.com/link-assistant/agent/issues/208
|
|
589
|
+
if (
|
|
590
|
+
error?.name === 'APIError' &&
|
|
591
|
+
error.data.statusCode === 401 &&
|
|
592
|
+
error.data.responseBody
|
|
593
|
+
) {
|
|
594
|
+
const isModelError = isModelNotSupportedError(
|
|
595
|
+
error.data.responseBody
|
|
596
|
+
);
|
|
597
|
+
if (isModelError) {
|
|
598
|
+
log.error(() => ({
|
|
599
|
+
message:
|
|
600
|
+
'model not supported by provider — this is NOT an auth error',
|
|
601
|
+
hint: 'The model was found in the local cache but the provider rejected it. The model may have been removed or is temporarily unavailable.',
|
|
602
|
+
providerID: input.providerID,
|
|
603
|
+
modelID: input.model.id,
|
|
604
|
+
statusCode: error.data.statusCode,
|
|
605
|
+
responseBody: error.data.responseBody,
|
|
606
|
+
suggestion:
|
|
607
|
+
'Try a different model or check the provider status. Use --model <provider>/<model-id> to specify an alternative.',
|
|
608
|
+
issue: 'https://github.com/link-assistant/agent/issues/208',
|
|
609
|
+
}));
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
554
613
|
input.assistantMessage.error = error;
|
|
555
614
|
Bus.publish(Session.Event.Error, {
|
|
556
615
|
sessionID: input.assistantMessage.sessionID,
|
package/src/tool/task.ts
CHANGED
|
@@ -9,6 +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 { DEFAULT_PROVIDER_ID, DEFAULT_MODEL_ID } from '../cli/defaults';
|
|
12
13
|
|
|
13
14
|
export const TaskTool = Tool.define('task', async () => {
|
|
14
15
|
const agents = await Agent.list().then((x) =>
|
|
@@ -99,8 +100,8 @@ export const TaskTool = Tool.define('task', async () => {
|
|
|
99
100
|
|
|
100
101
|
const model = agent.model ??
|
|
101
102
|
parentModel ?? {
|
|
102
|
-
modelID:
|
|
103
|
-
providerID:
|
|
103
|
+
modelID: DEFAULT_MODEL_ID,
|
|
104
|
+
providerID: DEFAULT_PROVIDER_ID,
|
|
104
105
|
};
|
|
105
106
|
|
|
106
107
|
function cancel() {
|