@link-assistant/agent 0.12.3 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@link-assistant/agent",
3
- "version": "0.12.3",
3
+ "version": "0.13.1",
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",
@@ -2630,6 +2630,173 @@ const AlibabaPlugin: AuthPlugin = {
2630
2630
  },
2631
2631
  };
2632
2632
 
2633
+ /**
2634
+ * Kilo Gateway constants
2635
+ * @see https://github.com/Kilo-Org/kilo/blob/main/packages/kilo-gateway/src/api/constants.ts
2636
+ */
2637
+ const KILO_API_BASE = 'https://api.kilo.ai';
2638
+ const KILO_POLL_INTERVAL_MS = 3000;
2639
+
2640
+ /**
2641
+ * Kilo Gateway Auth Plugin
2642
+ * Supports device authorization flow for Kilo Gateway
2643
+ *
2644
+ * @see https://github.com/Kilo-Org/kilo/blob/main/packages/kilo-gateway/src/auth/device-auth.ts
2645
+ */
2646
+ const KiloPlugin: AuthPlugin = {
2647
+ provider: 'kilo',
2648
+ methods: [
2649
+ {
2650
+ label: 'Kilo Gateway (Device Authorization)',
2651
+ type: 'oauth',
2652
+ async authorize() {
2653
+ // Initiate device authorization
2654
+ const initResponse = await fetch(
2655
+ `${KILO_API_BASE}/api/device-auth/codes`,
2656
+ {
2657
+ method: 'POST',
2658
+ headers: { 'Content-Type': 'application/json' },
2659
+ }
2660
+ );
2661
+
2662
+ if (!initResponse.ok) {
2663
+ if (initResponse.status === 429) {
2664
+ log.error(() => ({
2665
+ message:
2666
+ 'kilo device auth rate limited - too many pending requests',
2667
+ }));
2668
+ return {
2669
+ method: 'auto' as const,
2670
+ async callback(): Promise<AuthResult> {
2671
+ return { type: 'failed' };
2672
+ },
2673
+ };
2674
+ }
2675
+ log.error(() => ({
2676
+ message: 'kilo device auth initiation failed',
2677
+ status: initResponse.status,
2678
+ }));
2679
+ return {
2680
+ method: 'auto' as const,
2681
+ async callback(): Promise<AuthResult> {
2682
+ return { type: 'failed' };
2683
+ },
2684
+ };
2685
+ }
2686
+
2687
+ const authData = (await initResponse.json()) as {
2688
+ code: string;
2689
+ verificationUrl: string;
2690
+ expiresIn: number;
2691
+ };
2692
+
2693
+ return {
2694
+ url: authData.verificationUrl,
2695
+ instructions: `Enter code: ${authData.code}\nWaiting for authorization...`,
2696
+ method: 'auto' as const,
2697
+ async callback(): Promise<AuthResult> {
2698
+ const maxAttempts = Math.ceil(
2699
+ (authData.expiresIn * 1000) / KILO_POLL_INTERVAL_MS
2700
+ );
2701
+
2702
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
2703
+ await new Promise((resolve) =>
2704
+ setTimeout(resolve, KILO_POLL_INTERVAL_MS)
2705
+ );
2706
+
2707
+ const pollResponse = await fetch(
2708
+ `${KILO_API_BASE}/api/device-auth/codes/${authData.code}`
2709
+ );
2710
+
2711
+ if (pollResponse.status === 202) {
2712
+ // Still pending
2713
+ continue;
2714
+ }
2715
+
2716
+ if (pollResponse.status === 403) {
2717
+ log.error(() => ({
2718
+ message: 'kilo device auth denied by user',
2719
+ }));
2720
+ return { type: 'failed' };
2721
+ }
2722
+
2723
+ if (pollResponse.status === 410) {
2724
+ log.error(() => ({
2725
+ message: 'kilo device auth code expired',
2726
+ }));
2727
+ return { type: 'failed' };
2728
+ }
2729
+
2730
+ if (!pollResponse.ok) {
2731
+ log.error(() => ({
2732
+ message: 'kilo device auth poll failed',
2733
+ status: pollResponse.status,
2734
+ }));
2735
+ return { type: 'failed' };
2736
+ }
2737
+
2738
+ const data = (await pollResponse.json()) as {
2739
+ status: string;
2740
+ token?: string;
2741
+ userEmail?: string;
2742
+ };
2743
+
2744
+ if (data.status === 'approved' && data.token) {
2745
+ log.info(() => ({
2746
+ message: 'kilo device auth approved',
2747
+ email: data.userEmail,
2748
+ }));
2749
+
2750
+ // Token from Kilo device auth is long-lived (1 year)
2751
+ const TOKEN_EXPIRATION_MS = 365 * 24 * 60 * 60 * 1000;
2752
+ return {
2753
+ type: 'success',
2754
+ provider: 'kilo',
2755
+ refresh: data.token,
2756
+ access: data.token,
2757
+ expires: Date.now() + TOKEN_EXPIRATION_MS,
2758
+ };
2759
+ }
2760
+ }
2761
+
2762
+ log.error(() => ({
2763
+ message: 'kilo device auth timed out',
2764
+ }));
2765
+ return { type: 'failed' };
2766
+ },
2767
+ };
2768
+ },
2769
+ },
2770
+ {
2771
+ label: 'API Key',
2772
+ type: 'api',
2773
+ async authorize(inputs: Record<string, string>) {
2774
+ const key = inputs['key'];
2775
+ if (!key) return { type: 'failed' };
2776
+ return {
2777
+ type: 'success',
2778
+ provider: 'kilo',
2779
+ key,
2780
+ };
2781
+ },
2782
+ },
2783
+ ],
2784
+ async loader(getAuth) {
2785
+ const auth = await getAuth();
2786
+ if (!auth) return {};
2787
+
2788
+ if (auth.type === 'api') {
2789
+ return { apiKey: auth.key };
2790
+ }
2791
+
2792
+ if (auth.type === 'oauth') {
2793
+ return { apiKey: auth.access };
2794
+ }
2795
+
2796
+ return {};
2797
+ },
2798
+ };
2799
+
2633
2800
  /**
2634
2801
  * Registry of all auth plugins
2635
2802
  */
@@ -2640,6 +2807,7 @@ const plugins: Record<string, AuthPlugin> = {
2640
2807
  google: GooglePlugin,
2641
2808
  'qwen-coder': QwenPlugin,
2642
2809
  alibaba: AlibabaPlugin,
2810
+ kilo: KiloPlugin,
2643
2811
  };
2644
2812
 
2645
2813
  /**
package/src/bun/index.ts CHANGED
@@ -13,26 +13,69 @@ export namespace BunProc {
13
13
  // Lock key for serializing package installations to prevent race conditions
14
14
  const INSTALL_LOCK_KEY = 'bun-install';
15
15
 
16
+ // Default timeout for subprocess commands (2 minutes)
17
+ // This prevents indefinite hangs from known Bun issues:
18
+ // - HTTP 304 response handling (https://github.com/oven-sh/bun/issues/5831)
19
+ // - Failed dependency fetch (https://github.com/oven-sh/bun/issues/26341)
20
+ // - IPv6 configuration issues
21
+ const DEFAULT_TIMEOUT_MS = 120000;
22
+
23
+ // Timeout specifically for package installation (60 seconds)
24
+ // Package installations should complete within this time for typical packages
25
+ const INSTALL_TIMEOUT_MS = 60000;
26
+
27
+ export const TimeoutError = NamedError.create(
28
+ 'BunTimeoutError',
29
+ z.object({
30
+ cmd: z.array(z.string()),
31
+ timeoutMs: z.number(),
32
+ })
33
+ );
34
+
16
35
  export async function run(
17
36
  cmd: string[],
18
- options?: Bun.SpawnOptions.OptionsObject<any, any, any>
37
+ options?: Bun.SpawnOptions.OptionsObject<any, any, any> & {
38
+ timeout?: number;
39
+ }
19
40
  ) {
41
+ const timeout = options?.timeout ?? DEFAULT_TIMEOUT_MS;
42
+
20
43
  log.info(() => ({
21
44
  message: 'running',
22
45
  cmd: [which(), ...cmd],
23
- ...options,
46
+ timeout,
47
+ cwd: options?.cwd,
24
48
  }));
49
+
25
50
  const result = Bun.spawn([which(), ...cmd], {
26
51
  ...options,
27
52
  stdout: 'pipe',
28
53
  stderr: 'pipe',
54
+ timeout, // Automatically kills process after timeout
55
+ killSignal: 'SIGTERM', // Graceful termination signal
29
56
  env: {
30
57
  ...process.env,
31
58
  ...options?.env,
32
59
  BUN_BE_BUN: '1',
33
60
  },
34
61
  });
62
+
35
63
  const code = await result.exited;
64
+
65
+ // Check if process was killed due to timeout
66
+ if (result.signalCode === 'SIGTERM' && code !== 0) {
67
+ log.error(() => ({
68
+ message: 'command timed out',
69
+ cmd: [which(), ...cmd],
70
+ timeout,
71
+ signalCode: result.signalCode,
72
+ }));
73
+ throw new TimeoutError({
74
+ cmd: [which(), ...cmd],
75
+ timeoutMs: timeout,
76
+ });
77
+ }
78
+
36
79
  const stdout = result.stdout
37
80
  ? typeof result.stdout === 'number'
38
81
  ? result.stdout
@@ -84,6 +127,13 @@ export namespace BunProc {
84
127
  );
85
128
  }
86
129
 
130
+ /**
131
+ * Check if an error is a timeout error
132
+ */
133
+ function isTimeoutError(error: unknown): boolean {
134
+ return error instanceof TimeoutError;
135
+ }
136
+
87
137
  /**
88
138
  * Wait for a specified duration
89
139
  */
@@ -139,12 +189,13 @@ export namespace BunProc {
139
189
  version,
140
190
  }));
141
191
 
142
- // Retry logic for cache-related errors
192
+ // Retry logic for cache-related errors and timeout errors
143
193
  let lastError: Error | undefined;
144
194
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
145
195
  try {
146
196
  await BunProc.run(args, {
147
197
  cwd: Global.Path.cache,
198
+ timeout: INSTALL_TIMEOUT_MS, // Use specific timeout for package installation
148
199
  });
149
200
 
150
201
  log.info(() => ({
@@ -159,6 +210,7 @@ export namespace BunProc {
159
210
  } catch (e) {
160
211
  const errorMsg = e instanceof Error ? e.message : String(e);
161
212
  const isCacheError = isCacheRelatedError(errorMsg);
213
+ const isTimeout = isTimeoutError(e);
162
214
 
163
215
  log.warn(() => ({
164
216
  message: 'package installation attempt failed',
@@ -168,11 +220,15 @@ export namespace BunProc {
168
220
  maxRetries: MAX_RETRIES,
169
221
  error: errorMsg,
170
222
  isCacheError,
223
+ isTimeout,
171
224
  }));
172
225
 
173
- if (isCacheError && attempt < MAX_RETRIES) {
226
+ // Retry on cache-related errors or timeout errors
227
+ if ((isCacheError || isTimeout) && attempt < MAX_RETRIES) {
174
228
  log.info(() => ({
175
- message: 'retrying installation after cache-related error',
229
+ message: isTimeout
230
+ ? 'retrying installation after timeout (possible network issue)'
231
+ : 'retrying installation after cache-related error',
176
232
  pkg,
177
233
  version,
178
234
  attempt,
@@ -184,7 +240,7 @@ export namespace BunProc {
184
240
  continue;
185
241
  }
186
242
 
187
- // Non-cache error or final attempt - log and throw
243
+ // Non-retriable error or final attempt - log and throw
188
244
  log.error(() => ({
189
245
  message: 'package installation failed',
190
246
  pkg,
@@ -192,10 +248,11 @@ export namespace BunProc {
192
248
  error: errorMsg,
193
249
  stack: e instanceof Error ? e.stack : undefined,
194
250
  possibleCacheCorruption: isCacheError,
251
+ timedOut: isTimeout,
195
252
  attempts: attempt,
196
253
  }));
197
254
 
198
- // Provide helpful recovery instructions for cache-related errors
255
+ // Provide helpful recovery instructions
199
256
  if (isCacheError) {
200
257
  log.error(() => ({
201
258
  message:
@@ -203,6 +260,15 @@ export namespace BunProc {
203
260
  }));
204
261
  }
205
262
 
263
+ if (isTimeout) {
264
+ log.error(() => ({
265
+ message:
266
+ 'Package installation timed out. This may be due to network issues or Bun hanging. ' +
267
+ 'Try: 1) Check network connectivity, 2) Run "bun pm cache rm" to clear cache, ' +
268
+ '3) Check for IPv6 issues (try disabling IPv6)',
269
+ }));
270
+ }
271
+
206
272
  throw new InstallFailedError(
207
273
  { pkg, version, details: errorMsg },
208
274
  {
@@ -128,8 +128,9 @@ export const AuthLoginCommand = cmd({
128
128
  'github-copilot': 1,
129
129
  openai: 2,
130
130
  google: 3,
131
- openrouter: 4,
132
- vercel: 5,
131
+ kilo: 4,
132
+ openrouter: 5,
133
+ vercel: 6,
133
134
  };
134
135
 
135
136
  // Note: Using `select` instead of `autocomplete` because `autocomplete` is only
package/src/index.js CHANGED
@@ -172,8 +172,10 @@ async function parseModelConfig(argv) {
172
172
  modelID = modelID || 'kimi-k2.5-free';
173
173
  }
174
174
 
175
+ // Log raw and parsed values to help diagnose model routing issues (#171)
175
176
  Log.Default.info(() => ({
176
177
  message: 'using explicit provider/model',
178
+ rawModel: modelArg,
177
179
  providerID,
178
180
  modelID,
179
181
  }));
@@ -324,19 +324,21 @@ export namespace Provider {
324
324
  },
325
325
  /**
326
326
  * Kilo provider - access to 500+ AI models through Kilo Gateway
327
- * Uses OpenAI-compatible API at https://api.kilo.ai/api/gateway
327
+ * Uses OpenRouter-compatible API at https://api.kilo.ai/api/openrouter
328
328
  *
329
- * Free models available without API key (using 'public' key):
329
+ * Authentication required: run `agent auth login` and select "Kilo Gateway"
330
+ * For API key authentication, set KILO_API_KEY environment variable
331
+ *
332
+ * Free models available after authentication:
330
333
  * - GLM-5 (z-ai/glm-5) - Free limited time, flagship Z.AI model
331
334
  * - GLM 4.7 (z-ai/glm-4.7:free) - Free, agent-centric model
332
335
  * - Kimi K2.5 (moonshot/kimi-k2.5:free) - Free, agentic capabilities
333
336
  * - MiniMax M2.1 (minimax/m2.1:free) - Free, general-purpose
334
337
  * - Giga Potato (giga-potato:free) - Free evaluation model
335
338
  *
336
- * For paid models, set KILO_API_KEY environment variable
337
- *
338
339
  * @see https://kilo.ai/docs/gateway
339
340
  * @see https://kilo.ai/docs/advanced-usage/free-and-budget-models
341
+ * @see https://github.com/Kilo-Org/kilo/tree/main/packages/kilo-gateway
340
342
  */
341
343
  kilo: async (input) => {
342
344
  const hasKey = await (async () => {
@@ -345,8 +347,7 @@ export namespace Provider {
345
347
  return false;
346
348
  })();
347
349
 
348
- // For free models, we can use 'public' as the API key
349
- // For paid models, user needs to set KILO_API_KEY
350
+ // Filter to only free models when no API key
350
351
  if (!hasKey) {
351
352
  for (const [key, value] of Object.entries(input.models)) {
352
353
  // Keep only free models (cost.input === 0) when no API key
@@ -355,13 +356,44 @@ export namespace Provider {
355
356
  }
356
357
  }
357
358
 
359
+ // Build options
360
+ const options: Record<string, any> = {};
361
+
362
+ // Kilo-specific headers for the OpenRouter-compatible API
363
+ // @see https://github.com/Kilo-Org/kilo/blob/main/packages/kilo-gateway/src/headers.ts
364
+ const headers: Record<string, string> = {
365
+ 'User-Agent': 'opencode-kilo-provider',
366
+ 'X-KILOCODE-EDITORNAME': 'link-assistant-agent',
367
+ };
368
+
369
+ // Pass KILO_ORG_ID if available
370
+ if (process.env['KILO_ORG_ID']) {
371
+ headers['X-KILOCODE-ORGANIZATIONID'] = process.env['KILO_ORG_ID'];
372
+ }
373
+
374
+ options.headers = headers;
375
+
376
+ // Use auth token if available (from `agent auth login` -> Kilo device auth)
377
+ if (!hasKey) {
378
+ // Without authentication, use 'anonymous' key
379
+ // Note: The Kilo API may reject anonymous requests for completions.
380
+ // Users should authenticate via `agent auth login` for reliable access.
381
+ options.apiKey = 'anonymous';
382
+ } else {
383
+ // If we have stored auth, the loader will provide the token
384
+ const auth = await Auth.get(input.id);
385
+ if (auth) {
386
+ if (auth.type === 'api') {
387
+ options.apiKey = auth.key;
388
+ } else if (auth.type === 'oauth') {
389
+ options.apiKey = auth.access;
390
+ }
391
+ }
392
+ }
393
+
358
394
  return {
359
395
  autoload: Object.keys(input.models).length > 0,
360
- options: hasKey
361
- ? {}
362
- : {
363
- apiKey: 'public',
364
- },
396
+ options,
365
397
  };
366
398
  },
367
399
  /**
@@ -769,13 +801,14 @@ export namespace Provider {
769
801
  database['kilo'] = {
770
802
  id: 'kilo',
771
803
  name: 'Kilo Gateway',
772
- npm: '@ai-sdk/openai-compatible',
773
- api: 'https://api.kilo.ai/api/gateway',
804
+ npm: '@openrouter/ai-sdk-provider',
805
+ api: 'https://api.kilo.ai/api/openrouter',
774
806
  env: ['KILO_API_KEY'],
775
807
  models: {
776
- // GLM-5 - Flagship Z.AI model, free for limited time
808
+ // GLM-5 - Flagship Z.AI model, free tier
809
+ // Kilo API model ID: z-ai/glm-5:free (NOT z-ai/glm-5 which is paid)
777
810
  'glm-5-free': {
778
- id: 'z-ai/glm-5',
811
+ id: 'z-ai/glm-5:free',
779
812
  name: 'GLM-5 (Free)',
780
813
  release_date: '2026-02-11',
781
814
  attachment: false,
@@ -789,7 +822,7 @@ export namespace Provider {
789
822
  cache_write: 0,
790
823
  },
791
824
  limit: {
792
- context: 202752,
825
+ context: 202800,
793
826
  output: 131072,
794
827
  },
795
828
  modalities: {
@@ -798,10 +831,11 @@ export namespace Provider {
798
831
  },
799
832
  options: {},
800
833
  },
801
- // GLM 4.7 - Agent-centric model, free
802
- 'glm-4.7-free': {
803
- id: 'z-ai/glm-4.7:free',
804
- name: 'GLM 4.7 (Free)',
834
+ // GLM 4.5 Air - Free Z.AI model (replaces non-existent glm-4.7:free)
835
+ // Kilo API model ID: z-ai/glm-4.5-air:free
836
+ 'glm-4.5-air-free': {
837
+ id: 'z-ai/glm-4.5-air:free',
838
+ name: 'GLM 4.5 Air (Free)',
805
839
  release_date: '2026-01-15',
806
840
  attachment: false,
807
841
  reasoning: true,
@@ -815,7 +849,7 @@ export namespace Provider {
815
849
  },
816
850
  limit: {
817
851
  context: 131072,
818
- output: 65536,
852
+ output: 96000,
819
853
  },
820
854
  modalities: {
821
855
  input: ['text'],
@@ -823,13 +857,14 @@ export namespace Provider {
823
857
  },
824
858
  options: {},
825
859
  },
826
- // Kimi K2.5 - Agentic capabilities, free
827
- 'kimi-k2.5-free': {
828
- id: 'moonshot/kimi-k2.5:free',
829
- name: 'Kimi K2.5 (Free)',
830
- release_date: '2025-12-01',
860
+ // MiniMax M2.5 - General-purpose, free (upgraded from M2.1)
861
+ // Kilo API model ID: minimax/minimax-m2.5:free
862
+ 'minimax-m2.5-free': {
863
+ id: 'minimax/minimax-m2.5:free',
864
+ name: 'MiniMax M2.5 (Free)',
865
+ release_date: '2026-01-01',
831
866
  attachment: false,
832
- reasoning: false,
867
+ reasoning: true,
833
868
  temperature: true,
834
869
  tool_call: true,
835
870
  cost: {
@@ -839,8 +874,8 @@ export namespace Provider {
839
874
  cache_write: 0,
840
875
  },
841
876
  limit: {
842
- context: 131072,
843
- output: 65536,
877
+ context: 204800,
878
+ output: 131072,
844
879
  },
845
880
  modalities: {
846
881
  input: ['text'],
@@ -848,13 +883,14 @@ export namespace Provider {
848
883
  },
849
884
  options: {},
850
885
  },
851
- // MiniMax M2.1 - General-purpose, free
852
- 'minimax-m2.1-free': {
853
- id: 'minimax/m2.1:free',
854
- name: 'MiniMax M2.1 (Free)',
855
- release_date: '2025-11-01',
856
- attachment: false,
857
- reasoning: false,
886
+ // Giga Potato - Free evaluation model
887
+ // Kilo API model ID: giga-potato (no :free suffix)
888
+ 'giga-potato-free': {
889
+ id: 'giga-potato',
890
+ name: 'Giga Potato (Free)',
891
+ release_date: '2026-01-01',
892
+ attachment: true,
893
+ reasoning: true,
858
894
  temperature: true,
859
895
  tool_call: true,
860
896
  cost: {
@@ -864,19 +900,20 @@ export namespace Provider {
864
900
  cache_write: 0,
865
901
  },
866
902
  limit: {
867
- context: 131072,
868
- output: 65536,
903
+ context: 256000,
904
+ output: 32000,
869
905
  },
870
906
  modalities: {
871
- input: ['text'],
907
+ input: ['text', 'image'],
872
908
  output: ['text'],
873
909
  },
874
910
  options: {},
875
911
  },
876
- // Giga Potato - Free evaluation model
877
- 'giga-potato-free': {
878
- id: 'giga-potato:free',
879
- name: 'Giga Potato (Free)',
912
+ // Trinity Large Preview - Preview model from Arcee AI
913
+ // Kilo API model ID: arcee-ai/trinity-large-preview:free
914
+ 'trinity-large-preview': {
915
+ id: 'arcee-ai/trinity-large-preview:free',
916
+ name: 'Trinity Large Preview (Free)',
880
917
  release_date: '2026-01-01',
881
918
  attachment: false,
882
919
  reasoning: false,
@@ -889,8 +926,8 @@ export namespace Provider {
889
926
  cache_write: 0,
890
927
  },
891
928
  limit: {
892
- context: 65536,
893
- output: 32768,
929
+ context: 131000,
930
+ output: 65536,
894
931
  },
895
932
  modalities: {
896
933
  input: ['text'],
@@ -898,15 +935,16 @@ export namespace Provider {
898
935
  },
899
936
  options: {},
900
937
  },
901
- // Trinity Large Preview - Preview model from Arcee AI
902
- 'trinity-large-preview': {
903
- id: 'arcee/trinity-large-preview',
904
- name: 'Trinity Large Preview (Free)',
905
- release_date: '2026-01-01',
938
+ // DeepSeek R1 - Reasoning model, free
939
+ // Kilo API model ID: deepseek/deepseek-r1-0528:free
940
+ 'deepseek-r1-free': {
941
+ id: 'deepseek/deepseek-r1-0528:free',
942
+ name: 'DeepSeek R1 (Free)',
943
+ release_date: '2025-05-28',
906
944
  attachment: false,
907
- reasoning: false,
945
+ reasoning: true,
908
946
  temperature: true,
909
- tool_call: true,
947
+ tool_call: false,
910
948
  cost: {
911
949
  input: 0,
912
950
  output: 0,
@@ -914,8 +952,8 @@ export namespace Provider {
914
952
  cache_write: 0,
915
953
  },
916
954
  limit: {
917
- context: 65536,
918
- output: 32768,
955
+ context: 163840,
956
+ output: 163840,
919
957
  },
920
958
  modalities: {
921
959
  input: ['text'],
@@ -1293,9 +1331,9 @@ export namespace Provider {
1293
1331
  if (providerID === 'kilo') {
1294
1332
  priority = [
1295
1333
  'glm-5-free',
1296
- 'glm-4.7-free',
1297
- 'kimi-k2.5-free',
1298
- 'minimax-m2.1-free',
1334
+ 'glm-4.5-air-free',
1335
+ 'minimax-m2.5-free',
1336
+ 'deepseek-r1-free',
1299
1337
  'giga-potato-free',
1300
1338
  ];
1301
1339
  }
@@ -1391,10 +1429,9 @@ export namespace Provider {
1391
1429
  * Priority for free models:
1392
1430
  * 1. If model is uniquely available in one provider, use that provider
1393
1431
  * 2. If model is available in multiple providers, prioritize based on free model availability:
1394
- * - kilo: glm-5-free, glm-4.7-free, minimax-m2.1-free, giga-potato-free (unique to Kilo)
1395
- * - opencode: minimax-m2.5-free, big-pickle, gpt-5-nano (unique to OpenCode)
1396
- * - SHARED: kimi-k2.5-free (available in both)
1397
- * 3. For shared models like kimi-k2.5-free, prefer OpenCode first, then fall back to Kilo on rate limit
1432
+ * - kilo: glm-5-free, glm-4.5-air-free, minimax-m2.5-free, giga-potato-free, deepseek-r1-free (unique to Kilo)
1433
+ * - opencode: big-pickle, gpt-5-nano (unique to OpenCode)
1434
+ * 3. For shared models, prefer OpenCode first, then fall back to Kilo on rate limit
1398
1435
  *
1399
1436
  * @param modelID - Short model name without provider prefix
1400
1437
  * @returns Provider ID that should handle this model, or undefined if not found
@@ -1408,9 +1445,11 @@ export namespace Provider {
1408
1445
  // Models unique to Kilo (GLM models from Z.AI are only free on Kilo)
1409
1446
  const kiloUniqueModels = [
1410
1447
  'glm-5-free',
1411
- 'glm-4.7-free',
1448
+ 'glm-4.5-air-free',
1449
+ 'minimax-m2.5-free',
1412
1450
  'giga-potato-free',
1413
1451
  'trinity-large-preview',
1452
+ 'deepseek-r1-free',
1414
1453
  ];
1415
1454
 
1416
1455
  // Check if it's a Kilo-unique model
@@ -1515,10 +1554,8 @@ export namespace Provider {
1515
1554
  * If user specifies "kilo/kimi-k2.5-free", no fallback will occur.
1516
1555
  */
1517
1556
  const SHARED_FREE_MODELS: Record<string, string[]> = {
1518
- // kimi-k2.5-free is available in both OpenCode and Kilo
1519
- 'kimi-k2.5-free': ['opencode', 'kilo'],
1520
- // Note: minimax-m2.1-free is Kilo only, minimax-m2.5-free is OpenCode only
1521
- // They are different model versions, not shared
1557
+ // Currently no shared models between OpenCode and Kilo providers.
1558
+ // Kilo models use different IDs than OpenCode models.
1522
1559
  };
1523
1560
 
1524
1561
  /**