@link-assistant/agent 0.12.1 → 0.13.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/package.json +1 -1
- package/src/auth/plugins.ts +168 -0
- package/src/cli/cmd/auth.ts +3 -2
- package/src/index.js +2 -0
- package/src/provider/provider.ts +103 -66
- package/src/session/processor.ts +23 -2
package/package.json
CHANGED
package/src/auth/plugins.ts
CHANGED
|
@@ -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/cli/cmd/auth.ts
CHANGED
|
@@ -128,8 +128,9 @@ export const AuthLoginCommand = cmd({
|
|
|
128
128
|
'github-copilot': 1,
|
|
129
129
|
openai: 2,
|
|
130
130
|
google: 3,
|
|
131
|
-
|
|
132
|
-
|
|
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
|
}));
|
package/src/provider/provider.ts
CHANGED
|
@@ -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
|
|
327
|
+
* Uses OpenRouter-compatible API at https://api.kilo.ai/api/openrouter
|
|
328
328
|
*
|
|
329
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
|
773
|
-
api: 'https://api.kilo.ai/api/
|
|
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
|
|
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:
|
|
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.
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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:
|
|
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
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
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:
|
|
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:
|
|
843
|
-
output:
|
|
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
|
-
//
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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:
|
|
868
|
-
output:
|
|
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
|
-
//
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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:
|
|
893
|
-
output:
|
|
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
|
-
//
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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:
|
|
945
|
+
reasoning: true,
|
|
908
946
|
temperature: true,
|
|
909
|
-
tool_call:
|
|
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:
|
|
918
|
-
output:
|
|
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.
|
|
1297
|
-
'
|
|
1298
|
-
'
|
|
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.
|
|
1395
|
-
* - opencode:
|
|
1396
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
1519
|
-
|
|
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
|
/**
|
package/src/session/processor.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ModelsDev } from '../provider/models';
|
|
2
2
|
import { MessageV2 } from './message-v2';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type StreamTextResult,
|
|
5
|
+
type Tool as AITool,
|
|
6
|
+
APICallError,
|
|
7
|
+
JSONParseError,
|
|
8
|
+
} from 'ai';
|
|
4
9
|
import { Log } from '../util/log';
|
|
5
10
|
import { Identifier } from '../id/id';
|
|
6
11
|
import { Session } from '.';
|
|
@@ -205,6 +210,22 @@ export namespace SessionProcessor {
|
|
|
205
210
|
break;
|
|
206
211
|
}
|
|
207
212
|
case 'error':
|
|
213
|
+
// Skip stream parse errors (malformed SSE from gateway/provider)
|
|
214
|
+
// The AI SDK emits these as error events but continues the stream.
|
|
215
|
+
// Following OpenAI Codex pattern: log and skip bad events.
|
|
216
|
+
// See: https://github.com/link-assistant/agent/issues/169
|
|
217
|
+
if (JSONParseError.isInstance(value.error)) {
|
|
218
|
+
log.warn(() => ({
|
|
219
|
+
message:
|
|
220
|
+
'skipping malformed SSE event (stream parse error)',
|
|
221
|
+
errorName: (value.error as Error)?.name,
|
|
222
|
+
errorMessage: (value.error as Error)?.message?.substring(
|
|
223
|
+
0,
|
|
224
|
+
200
|
|
225
|
+
),
|
|
226
|
+
}));
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
208
229
|
throw value.error;
|
|
209
230
|
|
|
210
231
|
case 'start-step':
|
|
@@ -364,7 +385,7 @@ export namespace SessionProcessor {
|
|
|
364
385
|
providerID: input.providerID,
|
|
365
386
|
});
|
|
366
387
|
|
|
367
|
-
// Check if error is retryable (APIError, SocketConnectionError,
|
|
388
|
+
// Check if error is retryable (APIError, SocketConnectionError, TimeoutError)
|
|
368
389
|
const isRetryableAPIError =
|
|
369
390
|
error?.name === 'APIError' && error.data.isRetryable;
|
|
370
391
|
const isRetryableSocketError =
|