pi-free 2.2.2 → 2.2.4
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/CHANGELOG.md +18 -39
- package/README.md +41 -532
- package/banner.svg +23 -20
- package/config.ts +774 -702
- package/constants.ts +11 -1
- package/index.ts +432 -419
- package/lib/model-detection.ts +296 -296
- package/lib/model-metadata.ts +10 -3
- package/lib/telemetry.ts +36 -44
- package/package.json +3 -2
- package/provider-failover/benchmark-lookup.ts +30 -15
- package/provider-helper.ts +27 -8
- package/providers/bai/bai.ts +232 -237
- package/providers/cline/cline-xml-bridge.ts +31 -25
- package/providers/cline/cline.ts +17 -8
- package/providers/kilo/kilo.ts +11 -6
- package/providers/model-fetcher.ts +1 -1
- package/providers/opencode-session.ts +2 -2
- package/providers/openmodel/openmodel.ts +525 -0
- package/providers/qoder/auth.ts +548 -0
- package/providers/qoder/cosy.ts +236 -0
- package/providers/qoder/encoding.ts +48 -0
- package/providers/qoder/models.ts +321 -0
- package/providers/qoder/qoder.ts +154 -0
- package/providers/qoder/stream.ts +677 -0
- package/providers/qoder/thinking-parser.ts +251 -0
- package/providers/qoder/transform.ts +189 -0
- package/providers/tokenrouter/tokenrouter.ts +3 -6
package/providers/bai/bai.ts
CHANGED
|
@@ -1,237 +1,232 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* B.AI Provider Extension
|
|
3
|
-
*
|
|
4
|
-
* B.AI (https://b.ai) is an OpenAI-compatible LLM gateway providing access
|
|
5
|
-
* to many models (OpenAI, Anthropic, Google, DeepSeek, Qwen, GLM, Kimi).
|
|
6
|
-
*
|
|
7
|
-
* API: https://api.b.ai/v1
|
|
8
|
-
* Models: /v1/models
|
|
9
|
-
* Chat: /v1/chat/completions
|
|
10
|
-
*
|
|
11
|
-
* Pricing is not exposed via the /v1/models endpoint, so all models
|
|
12
|
-
* default to cost=0. The `isFreeModel` Route B detection (name contains
|
|
13
|
-
* "free") is therefore used. As a result, with `free_only: true` no b.ai
|
|
14
|
-
* models will be visible until you run `/toggle-bai` to enable paid models.
|
|
15
|
-
*
|
|
16
|
-
* A small set of known-promotional models are hardcoded as known-free so
|
|
17
|
-
* they remain visible even when free-only mode is on (mirrors the
|
|
18
|
-
* TokenRouter approach for `MiniMax-M3`).
|
|
19
|
-
*
|
|
20
|
-
* Setup:
|
|
21
|
-
* BAI_API_KEY=sk-...
|
|
22
|
-
* # or add bai_api_key to ~/.pi/free.json
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import type {
|
|
26
|
-
ExtensionAPI,
|
|
27
|
-
ProviderModelConfig,
|
|
28
|
-
} from "@earendil-works/pi-coding-agent";
|
|
29
|
-
import { getBaiApiKey, getBaiShowPaid, applyHidden } from "../../config.ts";
|
|
30
|
-
import {
|
|
31
|
-
BASE_URL_BAI,
|
|
32
|
-
DEFAULT_FETCH_TIMEOUT_MS,
|
|
33
|
-
PROVIDER_BAI,
|
|
34
|
-
} from "../../constants.ts";
|
|
35
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
36
|
-
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
37
|
-
import {
|
|
38
|
-
getProxyModelCompat,
|
|
39
|
-
isLikelyReasoningModel,
|
|
40
|
-
} from "../../lib/provider-compat.ts";
|
|
41
|
-
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
42
|
-
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
43
|
-
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
44
|
-
|
|
45
|
-
const _logger = createLogger("bai");
|
|
46
|
-
|
|
47
|
-
// =============================================================================
|
|
48
|
-
// Known Free Models
|
|
49
|
-
// B.AI doesn't expose pricing via /v1/models, so known-free models are
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
return
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
stored
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
const showPaid = getBaiShowPaid();
|
|
234
|
-
const initialModels =
|
|
235
|
-
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
236
|
-
reRegister(initialModels);
|
|
237
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* B.AI Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* B.AI (https://b.ai) is an OpenAI-compatible LLM gateway providing access
|
|
5
|
+
* to many models (OpenAI, Anthropic, Google, DeepSeek, Qwen, GLM, Kimi).
|
|
6
|
+
*
|
|
7
|
+
* API: https://api.b.ai/v1
|
|
8
|
+
* Models: /v1/models
|
|
9
|
+
* Chat: /v1/chat/completions
|
|
10
|
+
*
|
|
11
|
+
* Pricing is not exposed via the /v1/models endpoint, so all models
|
|
12
|
+
* default to cost=0. The `isFreeModel` Route B detection (name contains
|
|
13
|
+
* "free") is therefore used. As a result, with `free_only: true` no b.ai
|
|
14
|
+
* models will be visible until you run `/toggle-bai` to enable paid models.
|
|
15
|
+
*
|
|
16
|
+
* A small set of known-promotional models are hardcoded as known-free so
|
|
17
|
+
* they remain visible even when free-only mode is on (mirrors the
|
|
18
|
+
* TokenRouter approach for `MiniMax-M3`).
|
|
19
|
+
*
|
|
20
|
+
* Setup:
|
|
21
|
+
* BAI_API_KEY=sk-...
|
|
22
|
+
* # or add bai_api_key to ~/.pi/free.json
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
ExtensionAPI,
|
|
27
|
+
ProviderModelConfig,
|
|
28
|
+
} from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { getBaiApiKey, getBaiShowPaid, applyHidden } from "../../config.ts";
|
|
30
|
+
import {
|
|
31
|
+
BASE_URL_BAI,
|
|
32
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
33
|
+
PROVIDER_BAI,
|
|
34
|
+
} from "../../constants.ts";
|
|
35
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
36
|
+
import { safeEnrichModelsWithModelsDev } from "../../lib/model-metadata.ts";
|
|
37
|
+
import {
|
|
38
|
+
getProxyModelCompat,
|
|
39
|
+
isLikelyReasoningModel,
|
|
40
|
+
} from "../../lib/provider-compat.ts";
|
|
41
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
42
|
+
import { cleanModelName, fetchWithRetry } from "../../lib/util.ts";
|
|
43
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
44
|
+
|
|
45
|
+
const _logger = createLogger("bai");
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Known Free Models
|
|
49
|
+
// B.AI doesn't expose pricing via /v1/models, so known-free models are
|
|
50
|
+
// detected by name suffix. Catches `:free`-tagged models the gateway
|
|
51
|
+
// advertises as promotional.
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
function isBaiKnownFree(modelId: string): boolean {
|
|
55
|
+
return modelId.toLowerCase().endsWith(":free");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// =============================================================================
|
|
59
|
+
// Types
|
|
60
|
+
// =============================================================================
|
|
61
|
+
|
|
62
|
+
interface BaiModel {
|
|
63
|
+
id: string;
|
|
64
|
+
object?: string;
|
|
65
|
+
created?: number;
|
|
66
|
+
owned_by?: string;
|
|
67
|
+
supported_endpoint_types?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Helpers
|
|
72
|
+
// =============================================================================
|
|
73
|
+
|
|
74
|
+
/** Text-capable chat endpoints (excludes image/video/audio-only types) */
|
|
75
|
+
const CHAT_ENDPOINT_TYPES = new Set([
|
|
76
|
+
"openai",
|
|
77
|
+
"openai-response",
|
|
78
|
+
"anthropic",
|
|
79
|
+
"anthropic-compatible",
|
|
80
|
+
"gemini",
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
function isTextChatModel(model: BaiModel): boolean {
|
|
84
|
+
const endpoints = model.supported_endpoint_types ?? [];
|
|
85
|
+
if (endpoints.length === 0) {
|
|
86
|
+
// No endpoint info — assume text chat (matches TokenRouter fallback)
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return endpoints.some((t) => CHAT_ENDPOINT_TYPES.has(t));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mapBaiModel(model: BaiModel): ProviderModelConfig & {
|
|
93
|
+
_pricingKnown?: boolean;
|
|
94
|
+
_freeKnown?: boolean;
|
|
95
|
+
_isFree?: boolean;
|
|
96
|
+
} {
|
|
97
|
+
const name = cleanModelName(model.id);
|
|
98
|
+
const reasoning = isLikelyReasoningModel({ id: model.id, name });
|
|
99
|
+
const isKnownFree = isBaiKnownFree(model.id);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id: model.id,
|
|
103
|
+
name,
|
|
104
|
+
reasoning,
|
|
105
|
+
input: ["text"],
|
|
106
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
107
|
+
contextWindow: 128_000,
|
|
108
|
+
maxTokens: 16_384,
|
|
109
|
+
compat: getProxyModelCompat({ id: model.id, name }),
|
|
110
|
+
// Known-free models bypass name-based detection entirely
|
|
111
|
+
_freeKnown: isKnownFree,
|
|
112
|
+
_isFree: isKnownFree,
|
|
113
|
+
// Non-free models signal no pricing data (name-based detection only)
|
|
114
|
+
_pricingKnown: false,
|
|
115
|
+
} as ProviderModelConfig & {
|
|
116
|
+
_pricingKnown?: boolean;
|
|
117
|
+
_freeKnown?: boolean;
|
|
118
|
+
_isFree?: boolean;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// =============================================================================
|
|
123
|
+
// Fetch Models
|
|
124
|
+
// =============================================================================
|
|
125
|
+
|
|
126
|
+
async function fetchBaiModels(apiKey: string): Promise<ProviderModelConfig[]> {
|
|
127
|
+
_logger.info("[bai] Fetching models from B.AI API...");
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const response = await fetchWithRetry(
|
|
131
|
+
`${BASE_URL_BAI}/models`,
|
|
132
|
+
{
|
|
133
|
+
headers: {
|
|
134
|
+
Authorization: `Bearer ${apiKey}`,
|
|
135
|
+
Accept: "application/json",
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
3,
|
|
140
|
+
1000,
|
|
141
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (!response.ok) {
|
|
145
|
+
throw new Error(`B.AI API error: ${response.status}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const json = (await response.json()) as { data?: BaiModel[] };
|
|
149
|
+
const models = (json.data ?? []).filter(isTextChatModel);
|
|
150
|
+
|
|
151
|
+
_logger.info(`[bai] Fetched ${models.length} text chat models`);
|
|
152
|
+
const enriched = await safeEnrichModelsWithModelsDev(
|
|
153
|
+
models.map(mapBaiModel),
|
|
154
|
+
{ providerId: PROVIDER_BAI },
|
|
155
|
+
);
|
|
156
|
+
return applyHidden(enriched, PROVIDER_BAI);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
_logger.error("[bai] Failed to fetch models", {
|
|
159
|
+
error: error instanceof Error ? error.message : String(error),
|
|
160
|
+
});
|
|
161
|
+
return [];
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// =============================================================================
|
|
166
|
+
// Extension Entry Point
|
|
167
|
+
// =============================================================================
|
|
168
|
+
|
|
169
|
+
export default async function baiProvider(pi: ExtensionAPI) {
|
|
170
|
+
const apiKey = getBaiApiKey();
|
|
171
|
+
|
|
172
|
+
if (!apiKey) {
|
|
173
|
+
_logger.info(
|
|
174
|
+
"[bai] Skipping — BAI_API_KEY not set. Sign up at https://b.ai/",
|
|
175
|
+
);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const allModels = await fetchBaiModels(apiKey);
|
|
180
|
+
|
|
181
|
+
if (allModels.length === 0) {
|
|
182
|
+
// Either the API failed (already logged inside fetchBaiModels) or
|
|
183
|
+
// the API returned zero text chat models. We can't tell the user
|
|
184
|
+
// which, but we can give a hint to check the log / their key.
|
|
185
|
+
_logger.warn(
|
|
186
|
+
"[bai] No text chat models available — verify BAI_API_KEY is valid and see ~/.pi/free.log for details",
|
|
187
|
+
);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Use isFreeModel with allModels for proper detection
|
|
192
|
+
// B.AI doesn't expose pricing, so Route B (name-based) applies:
|
|
193
|
+
// FREE if name contains "free" OR _isFree is true (known-free hardcoded).
|
|
194
|
+
const freeModels = allModels.filter((m) =>
|
|
195
|
+
isFreeModel({ ...m, provider: PROVIDER_BAI }, allModels),
|
|
196
|
+
);
|
|
197
|
+
const stored = { free: freeModels, all: allModels };
|
|
198
|
+
|
|
199
|
+
_logger.info(
|
|
200
|
+
`[bai] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
const reRegister = createReRegister(pi, {
|
|
204
|
+
providerId: PROVIDER_BAI,
|
|
205
|
+
baseUrl: BASE_URL_BAI,
|
|
206
|
+
apiKey,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
registerWithGlobalToggle(PROVIDER_BAI, stored, reRegister, true);
|
|
210
|
+
|
|
211
|
+
setupProvider(
|
|
212
|
+
pi,
|
|
213
|
+
{
|
|
214
|
+
providerId: PROVIDER_BAI,
|
|
215
|
+
initialShowPaid: getBaiShowPaid(),
|
|
216
|
+
tosUrl: "https://b.ai/",
|
|
217
|
+
reRegister: (models, _stored) => {
|
|
218
|
+
if (_stored) {
|
|
219
|
+
stored.free = _stored.free;
|
|
220
|
+
stored.all = _stored.all;
|
|
221
|
+
}
|
|
222
|
+
reRegister(models);
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
stored,
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
const showPaid = getBaiShowPaid();
|
|
229
|
+
const initialModels =
|
|
230
|
+
showPaid && stored.all.length > 0 ? stored.all : freeModels;
|
|
231
|
+
reRegister(initialModels);
|
|
232
|
+
}
|
|
@@ -170,7 +170,9 @@ const CORE_CLINE_TOOL_NAMES = [
|
|
|
170
170
|
|
|
171
171
|
function stringArg(args: Record<string, unknown>, key: string): string {
|
|
172
172
|
const value = args[key];
|
|
173
|
-
|
|
173
|
+
if (typeof value === "string") return value;
|
|
174
|
+
if (value == null) return "";
|
|
175
|
+
return String(value);
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
function booleanArg(args: Record<string, unknown>, key: string): boolean {
|
|
@@ -178,7 +180,8 @@ function booleanArg(args: Record<string, unknown>, key: string): boolean {
|
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
function shellQuote(value: string): string {
|
|
181
|
-
|
|
183
|
+
const escaped = value.replaceAll("'", `'"'"'`);
|
|
184
|
+
return `'${escaped}'`;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
function buildListFilesCommand(args: Record<string, unknown>): string {
|
|
@@ -639,23 +642,25 @@ function buildToolInstructions(tools: Tool[] | undefined): string {
|
|
|
639
642
|
if (bridges.length === 0) return "";
|
|
640
643
|
|
|
641
644
|
const sections = bridges.map((bridge) => {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
645
|
+
let params: string;
|
|
646
|
+
if (bridge.remoteName === "replace_in_file") {
|
|
647
|
+
params = [
|
|
648
|
+
" <path>path/to/file</path>",
|
|
649
|
+
" <diff>",
|
|
650
|
+
"------- SEARCH",
|
|
651
|
+
"exact text to replace",
|
|
652
|
+
"=======",
|
|
653
|
+
"new text",
|
|
654
|
+
"+++++++ REPLACE",
|
|
655
|
+
" </diff>",
|
|
656
|
+
].join("\n");
|
|
657
|
+
} else if (bridge.parameters.length) {
|
|
658
|
+
params = bridge.parameters
|
|
659
|
+
.map((name) => ` <${name}>value</${name}>`)
|
|
660
|
+
.join("\n");
|
|
661
|
+
} else {
|
|
662
|
+
params = " <arguments>{}</arguments>";
|
|
663
|
+
}
|
|
659
664
|
return [
|
|
660
665
|
`Tool: ${bridge.remoteName}`,
|
|
661
666
|
`Description: ${bridge.description ?? bridge.runtimeName}`,
|
|
@@ -1461,12 +1466,13 @@ export function streamClineXml(
|
|
|
1461
1466
|
pushToolCall(assistant, toolCall, stream);
|
|
1462
1467
|
}
|
|
1463
1468
|
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1469
|
+
if (toolCalls.length > 0) {
|
|
1470
|
+
assistant.stopReason = "toolUse";
|
|
1471
|
+
} else if (finishReason === "length") {
|
|
1472
|
+
assistant.stopReason = "length";
|
|
1473
|
+
} else {
|
|
1474
|
+
assistant.stopReason = "stop";
|
|
1475
|
+
}
|
|
1470
1476
|
stream.push({
|
|
1471
1477
|
type: "done",
|
|
1472
1478
|
reason: assistant.stopReason as "stop" | "length" | "toolUse",
|
package/providers/cline/cline.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type {
|
|
|
19
19
|
ExtensionAPI,
|
|
20
20
|
ProviderModelConfig,
|
|
21
21
|
} from "@earendil-works/pi-coding-agent";
|
|
22
|
-
import { getClineShowPaid } from "../../config.ts";
|
|
22
|
+
import { getClineApiKey, getClineShowPaid } from "../../config.ts";
|
|
23
23
|
import { BASE_URL_CLINE, PROVIDER_CLINE } from "../../constants.ts";
|
|
24
24
|
import {
|
|
25
25
|
DEFAULT_PROVIDER_CACHE_TTL_MS,
|
|
@@ -84,6 +84,9 @@ function toApiKey(credentials: OAuthCredentials): string {
|
|
|
84
84
|
// =============================================================================
|
|
85
85
|
|
|
86
86
|
export default async function clineProvider(pi: ExtensionAPI) {
|
|
87
|
+
const clineApiKey = getClineApiKey();
|
|
88
|
+
const useApiKeyAuth = !!clineApiKey;
|
|
89
|
+
|
|
87
90
|
let allModels: ProviderModelConfig[];
|
|
88
91
|
const cachedModels = loadProviderCache(PROVIDER_CLINE);
|
|
89
92
|
if (cachedModels && cachedModels.length > 0) {
|
|
@@ -114,16 +117,21 @@ export default async function clineProvider(pi: ExtensionAPI) {
|
|
|
114
117
|
baseUrl: BASE_URL_CLINE,
|
|
115
118
|
api: "cline-xml-tools" as const,
|
|
116
119
|
authHeader: false,
|
|
120
|
+
apiKey: clineApiKey,
|
|
117
121
|
headers: buildClineHeaders(),
|
|
118
122
|
streamSimple: (model, context, options) =>
|
|
119
123
|
streamClineXml(model as any, context, options, buildClineHeaders()),
|
|
120
124
|
models: enhanceWithCI(m),
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
...(useApiKeyAuth
|
|
126
|
+
? {}
|
|
127
|
+
: {
|
|
128
|
+
oauth: {
|
|
129
|
+
name: "Cline",
|
|
130
|
+
login: loginCline,
|
|
131
|
+
refreshToken: refreshClineToken,
|
|
132
|
+
getApiKey: toApiKey,
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
127
135
|
});
|
|
128
136
|
};
|
|
129
137
|
|
|
@@ -138,7 +146,8 @@ export default async function clineProvider(pi: ExtensionAPI) {
|
|
|
138
146
|
toggleState.applyCurrent(reRegister);
|
|
139
147
|
};
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
// Register with global toggle system (hasKey=true if API key auth configured)
|
|
150
|
+
registerWithGlobalToggle(PROVIDER_CLINE, stored, (m) => reRegister(m), useApiKeyAuth);
|
|
142
151
|
toggleState.applyCurrent(reRegister);
|
|
143
152
|
|
|
144
153
|
pi.registerCommand("toggle-cline", {
|