@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.
@@ -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
+ }
@@ -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 { DEFAULT_PROVIDER_ID, DEFAULT_MODEL_ID } =
1671
- await import('../cli/defaults.ts');
1710
+ const { providerID: defaultProviderID, modelID: defaultModelID } =
1711
+ getDefaultModelParts();
1672
1712
  const isDefaultModel =
1673
- providerID === DEFAULT_PROVIDER_ID && modelID === DEFAULT_MODEL_ID;
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
- 'glm-5-free',
1872
+ 'hy3-preview-free',
1873
+ 'ling-2.6-flash-free',
1829
1874
  'minimax-m2.5-free',
1830
- 'gpt-5-nano',
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, minimax-m2.5-free, giga-potato-free, deepseek-r1-free (unique to Kilo)
1911
- * - opencode: big-pickle, gpt-5-nano, nemotron-3-super-free (unique to OpenCode)
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
- // Currently no shared models between OpenCode and Kilo providers.
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 { DEFAULT_PROVIDER_ID, DEFAULT_MODEL_ID } from '../cli/defaults';
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: DEFAULT_MODEL_ID,
104
- providerID: DEFAULT_PROVIDER_ID,
104
+ modelID: defaultModel.modelID,
105
+ providerID: defaultModel.providerID,
105
106
  };
106
107
 
107
108
  function cancel() {