@link-assistant/agent 0.22.9 → 0.23.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 +8 -5
- package/package.json +3 -3
- package/src/cli/argv.ts +1 -1
- package/src/cli/continuous-mode.js +117 -28
- package/src/cli/defaults.ts +1 -62
- package/src/cli/input-queue.js +169 -5
- package/src/cli/model-config.js +70 -34
- package/src/cli/output.ts +1 -1
- package/src/cli/run-options.js +50 -12
- package/src/config/defaults.ts +170 -0
- package/src/index.js +84 -24
- package/src/provider/opencode-zen.ts +138 -0
- package/src/provider/provider.ts +63 -12
- package/src/tool/task.ts +4 -3
|
@@ -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
|
/**
|
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() {
|