pi-free 2.2.4 → 2.2.6
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 +24 -0
- package/README.md +1 -1
- package/banner.svg +1 -1
- package/config.ts +58 -29
- package/constants.ts +1 -4
- package/index.ts +30 -24
- package/lib/built-in-toggle.ts +185 -18
- package/lib/provider-cache.ts +22 -0
- package/package.json +74 -74
- package/provider-failover/benchmark-lookup.ts +4 -1
- package/provider-failover/benchmarks-chunk-0.ts +570 -570
- package/provider-failover/benchmarks-chunk-1.ts +676 -676
- package/provider-failover/benchmarks-chunk-2.ts +673 -673
- package/provider-failover/benchmarks-chunk-3.ts +680 -680
- package/provider-failover/benchmarks-chunk-4.ts +683 -683
- package/provider-failover/benchmarks-chunk-5.ts +816 -474
- package/provider-helper.ts +64 -0
- package/providers/bai/bai.ts +8 -2
- package/providers/crofai/crofai.ts +8 -2
- package/providers/deepinfra/deepinfra.ts +8 -2
- package/providers/dynamic-built-in/index.ts +36 -26
- package/providers/kilo/kilo.ts +41 -22
- package/providers/novita/novita.ts +8 -2
- package/providers/openmodel/openmodel.ts +8 -2
- package/providers/qoder/models.ts +63 -175
- package/providers/qoder/qoder.ts +49 -84
- package/providers/qoder/stream.ts +182 -274
- package/providers/qoder/transform.ts +5 -2
- package/providers/routeway/routeway.ts +8 -2
- package/providers/sambanova/sambanova.ts +12 -6
- package/providers/together/together.ts +8 -2
- package/providers/tokenrouter/tokenrouter.ts +8 -2
- package/providers/zenmux/zenmux.ts +8 -2
- package/providers/codestral/codestral.ts +0 -128
- package/providers/qoder/encoding.ts +0 -48
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qoder model definitions and cache management.
|
|
3
3
|
*
|
|
4
|
-
* Qoder
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* a 1-hour TTL and falls back to the static models on cache miss or API error.
|
|
4
|
+
* Qoder operates on a credits-based pricing model:
|
|
5
|
+
* - Community Edition (free): basic models with daily message limits
|
|
6
|
+
* - Pro / Pro+ / Ultra (paid): premium models via monthly credits
|
|
8
7
|
*
|
|
9
|
-
*
|
|
8
|
+
* The dynamic model list API is currently unavailable (legacy api3 endpoint
|
|
9
|
+
* is decommissioned). We keep a static curated list and classify models as
|
|
10
|
+
* basic (free tier) or premium (paid credits) by model ID.
|
|
11
|
+
*
|
|
12
|
+
* Dynamic model discovery is disabled until Qoder publishes a model-list
|
|
13
|
+
* endpoint on api2-v2. Stale legacy cache entries are ignored and static
|
|
14
|
+
* models in `staticModels` remain the source of truth.
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
|
-
import { existsSync,
|
|
17
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
13
18
|
import { homedir } from "node:os";
|
|
14
|
-
import {
|
|
19
|
+
import { join } from "node:path";
|
|
15
20
|
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import {
|
|
21
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
22
|
+
|
|
23
|
+
const _logger = createLogger("qoder");
|
|
24
|
+
|
|
25
|
+
export type QoderModelConfig = ProviderModelConfig;
|
|
17
26
|
|
|
18
27
|
// ─── Cache ───────────────────────────────────────────────────────────────────
|
|
19
28
|
|
|
@@ -26,14 +35,29 @@ const ZERO_COST = Object.freeze({
|
|
|
26
35
|
cacheWrite: 0,
|
|
27
36
|
});
|
|
28
37
|
|
|
38
|
+
// ─── Basic (free-tier) model IDs ─────────────────────────────────────────────
|
|
39
|
+
// These are the Qoder-branded router models available on Community Edition.
|
|
40
|
+
// Named models (DeepSeek, Qwen, GLM, Kimi, MiniMax) are premium and cost credits.
|
|
41
|
+
// This set is the single source of truth for basic-model classification.
|
|
42
|
+
const BASIC_MODEL_IDS = new Set([
|
|
43
|
+
"auto",
|
|
44
|
+
"ultimate",
|
|
45
|
+
"performance",
|
|
46
|
+
"efficient",
|
|
47
|
+
"lite",
|
|
48
|
+
]);
|
|
49
|
+
|
|
29
50
|
// ─── Static model list ───────────────────────────────────────────────────────
|
|
30
51
|
|
|
31
52
|
/**
|
|
32
53
|
* Static model definitions for Qoder.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
54
|
+
* Basic models (free tier) are identified by membership in BASIC_MODEL_IDS.
|
|
55
|
+
* Premium models consume credits and require a paid plan.
|
|
56
|
+
*
|
|
57
|
+
* Model IDs are validated against the live api2-v2 endpoint; invalid IDs
|
|
58
|
+
* (dfmodel, gm51model, qmodel_latest) are excluded here.
|
|
35
59
|
*/
|
|
36
|
-
export const staticModels:
|
|
60
|
+
export const staticModels: QoderModelConfig[] = [
|
|
37
61
|
{
|
|
38
62
|
id: "auto",
|
|
39
63
|
name: "Qoder Auto",
|
|
@@ -88,15 +112,6 @@ export const staticModels: ProviderModelConfig[] = [
|
|
|
88
112
|
contextWindow: 1_000_000,
|
|
89
113
|
maxTokens: 32_768,
|
|
90
114
|
},
|
|
91
|
-
{
|
|
92
|
-
id: "qmodel_latest",
|
|
93
|
-
name: "Qwen3.7 Max (Qoder)",
|
|
94
|
-
reasoning: false,
|
|
95
|
-
input: ["text", "image"] as ("text" | "image")[],
|
|
96
|
-
cost: ZERO_COST,
|
|
97
|
-
contextWindow: 1_000_000,
|
|
98
|
-
maxTokens: 32_768,
|
|
99
|
-
},
|
|
100
115
|
{
|
|
101
116
|
id: "dmodel",
|
|
102
117
|
name: "DeepSeek V4 Pro (Qoder)",
|
|
@@ -106,24 +121,6 @@ export const staticModels: ProviderModelConfig[] = [
|
|
|
106
121
|
contextWindow: 1_000_000,
|
|
107
122
|
maxTokens: 32_768,
|
|
108
123
|
},
|
|
109
|
-
{
|
|
110
|
-
id: "dfmodel",
|
|
111
|
-
name: "DeepSeek V4 Flash (Qoder)",
|
|
112
|
-
reasoning: true,
|
|
113
|
-
input: ["text", "image"] as ("text" | "image")[],
|
|
114
|
-
cost: ZERO_COST,
|
|
115
|
-
contextWindow: 1_000_000,
|
|
116
|
-
maxTokens: 32_768,
|
|
117
|
-
},
|
|
118
|
-
{
|
|
119
|
-
id: "gm51model",
|
|
120
|
-
name: "GLM 5.1 (Qoder)",
|
|
121
|
-
reasoning: true,
|
|
122
|
-
input: ["text", "image"] as ("text" | "image")[],
|
|
123
|
-
cost: ZERO_COST,
|
|
124
|
-
contextWindow: 180_000,
|
|
125
|
-
maxTokens: 32_768,
|
|
126
|
-
},
|
|
127
124
|
{
|
|
128
125
|
id: "kmodel",
|
|
129
126
|
name: "Kimi K2.6 (Qoder)",
|
|
@@ -144,74 +141,30 @@ export const staticModels: ProviderModelConfig[] = [
|
|
|
144
141
|
},
|
|
145
142
|
];
|
|
146
143
|
|
|
147
|
-
// ───
|
|
144
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
148
145
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
display_name?: string;
|
|
153
|
-
max_input_tokens?: number;
|
|
154
|
-
max_output_tokens?: number;
|
|
155
|
-
context_config?: Record<string, { token_count?: number }>;
|
|
156
|
-
is_vl?: boolean;
|
|
157
|
-
is_reasoning?: boolean;
|
|
158
|
-
thinking_config?: { enabled?: { efforts?: unknown } };
|
|
159
|
-
source?: string;
|
|
160
|
-
[key: string]: unknown;
|
|
146
|
+
/** Check if a model is a basic (free-tier) model. */
|
|
147
|
+
export function isBasicModel(model: ProviderModelConfig): boolean {
|
|
148
|
+
return BASIC_MODEL_IDS.has(model.id);
|
|
161
149
|
}
|
|
162
150
|
|
|
163
151
|
// ─── Cache management ────────────────────────────────────────────────────────
|
|
164
152
|
|
|
165
|
-
function modelEntryToConfig(
|
|
166
|
-
entry: QoderModelEntry,
|
|
167
|
-
): ProviderModelConfig | null {
|
|
168
|
-
const key = entry.key;
|
|
169
|
-
if (!key || !entry.enable) return null;
|
|
170
|
-
|
|
171
|
-
const display = entry.display_name || key;
|
|
172
|
-
const ctxLen = resolveContextLength(entry);
|
|
173
|
-
const isVL = Boolean(entry.is_vl);
|
|
174
|
-
const isReasoning = Boolean(entry.is_reasoning) || Boolean(entry.thinking_config);
|
|
175
|
-
const input: ("text" | "image")[] = isVL ? ["text", "image"] : ["text"];
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
id: key,
|
|
179
|
-
name: display,
|
|
180
|
-
reasoning: isReasoning,
|
|
181
|
-
input,
|
|
182
|
-
cost: ZERO_COST,
|
|
183
|
-
contextWindow: ctxLen,
|
|
184
|
-
maxTokens: entry.max_output_tokens || 32_768,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function resolveContextLength(entry: QoderModelEntry): number {
|
|
189
|
-
let ctxLen = entry.max_input_tokens || 180_000;
|
|
190
|
-
if (entry.context_config && typeof entry.context_config === "object") {
|
|
191
|
-
for (const val of Object.values(entry.context_config)) {
|
|
192
|
-
if (
|
|
193
|
-
val &&
|
|
194
|
-
typeof val === "object" &&
|
|
195
|
-
typeof (val as Record<string, unknown>).token_count === "number"
|
|
196
|
-
) {
|
|
197
|
-
const tc = (val as Record<string, number>).token_count;
|
|
198
|
-
if (tc > ctxLen) ctxLen = tc;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return ctxLen;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
153
|
/** Get models from cache, falling back to static models. */
|
|
206
|
-
export function getCachedModels():
|
|
207
|
-
if (existsSync(CACHE_PATH)) {
|
|
154
|
+
export function getCachedModels(): QoderModelConfig[] {
|
|
155
|
+
if (existsSync(CACHE_PATH) && !isCacheStale()) {
|
|
208
156
|
try {
|
|
209
157
|
const data = JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
210
158
|
if (data && Array.isArray(data.models)) {
|
|
211
|
-
return data.models as
|
|
159
|
+
return data.models as QoderModelConfig[];
|
|
212
160
|
}
|
|
213
|
-
} catch {
|
|
214
|
-
|
|
161
|
+
} catch (err) {
|
|
162
|
+
_logger.warn(
|
|
163
|
+
"Failed to read Qoder model cache; falling back to static models",
|
|
164
|
+
{
|
|
165
|
+
error: err instanceof Error ? err.message : String(err),
|
|
166
|
+
},
|
|
167
|
+
);
|
|
215
168
|
}
|
|
216
169
|
}
|
|
217
170
|
return staticModels;
|
|
@@ -224,81 +177,11 @@ export function isCacheStale(): boolean {
|
|
|
224
177
|
const data = JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
225
178
|
if (!data || typeof data.updatedAt !== "number") return true;
|
|
226
179
|
return Date.now() - data.updatedAt > 3_600_000; // 1 hour
|
|
227
|
-
} catch {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
/**
|
|
233
|
-
* Fetch available models from Qoder's dynamic model list API and cache them.
|
|
234
|
-
* Falls back silently if the API is unreachable.
|
|
235
|
-
*/
|
|
236
|
-
export async function updateQoderModelsCache(
|
|
237
|
-
authToken: string,
|
|
238
|
-
userID: string,
|
|
239
|
-
name: string,
|
|
240
|
-
email: string,
|
|
241
|
-
): Promise<void> {
|
|
242
|
-
const modelListURL = "https://api3.qoder.sh/algo/api/v2/model/list";
|
|
243
|
-
try {
|
|
244
|
-
const headers = buildAuthHeaders(null, modelListURL, {
|
|
245
|
-
userID,
|
|
246
|
-
authToken,
|
|
247
|
-
name,
|
|
248
|
-
email,
|
|
180
|
+
} catch (err) {
|
|
181
|
+
_logger.warn("Failed to check Qoder cache staleness; treating as stale", {
|
|
182
|
+
error: err instanceof Error ? err.message : String(err),
|
|
249
183
|
});
|
|
250
|
-
|
|
251
|
-
const response = await fetch(modelListURL, {
|
|
252
|
-
method: "GET",
|
|
253
|
-
headers: {
|
|
254
|
-
Accept: "application/json",
|
|
255
|
-
...headers,
|
|
256
|
-
},
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
if (!response.ok) return;
|
|
260
|
-
|
|
261
|
-
const resData = (await response.json()) as {
|
|
262
|
-
chat?: QoderModelEntry[];
|
|
263
|
-
};
|
|
264
|
-
const chatModels = resData.chat || [];
|
|
265
|
-
if (chatModels.length === 0) return;
|
|
266
|
-
|
|
267
|
-
const newModels: ProviderModelConfig[] = [];
|
|
268
|
-
const configs: Record<string, QoderModelEntry> = {};
|
|
269
|
-
|
|
270
|
-
for (const entry of chatModels) {
|
|
271
|
-
const model = modelEntryToConfig(entry);
|
|
272
|
-
if (!model) continue;
|
|
273
|
-
configs[model.id] = entry;
|
|
274
|
-
newModels.push(model);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (newModels.length === 0) return;
|
|
278
|
-
|
|
279
|
-
// Ensure the auto router model is present
|
|
280
|
-
if (!newModels.some((m) => m.id === "auto")) {
|
|
281
|
-
newModels.unshift({
|
|
282
|
-
id: "auto",
|
|
283
|
-
name: "Qoder Auto",
|
|
284
|
-
reasoning: true,
|
|
285
|
-
input: ["text", "image"] as ("text" | "image")[],
|
|
286
|
-
cost: ZERO_COST,
|
|
287
|
-
contextWindow: 180_000,
|
|
288
|
-
maxTokens: 32_768,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const cacheData = {
|
|
293
|
-
updatedAt: Date.now(),
|
|
294
|
-
models: newModels,
|
|
295
|
-
configs,
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
mkdirSync(dirname(CACHE_PATH), { recursive: true });
|
|
299
|
-
writeFileSync(CACHE_PATH, JSON.stringify(cacheData, null, 2), "utf-8");
|
|
300
|
-
} catch {
|
|
301
|
-
// Best-effort
|
|
184
|
+
return true;
|
|
302
185
|
}
|
|
303
186
|
}
|
|
304
187
|
|
|
@@ -306,15 +189,20 @@ export async function updateQoderModelsCache(
|
|
|
306
189
|
* Get the cached model config for a specific model key.
|
|
307
190
|
* Used to determine per-model settings (reasoning, max tokens, etc.) at stream time.
|
|
308
191
|
*/
|
|
309
|
-
export function getCachedModelConfig(
|
|
310
|
-
|
|
192
|
+
export function getCachedModelConfig(
|
|
193
|
+
modelKey: string,
|
|
194
|
+
): Record<string, unknown> | null {
|
|
195
|
+
if (existsSync(CACHE_PATH) && !isCacheStale()) {
|
|
311
196
|
try {
|
|
312
197
|
const data = JSON.parse(readFileSync(CACHE_PATH, "utf8"));
|
|
313
198
|
if (data?.configs?.[modelKey]) {
|
|
314
|
-
return data.configs[modelKey] as
|
|
199
|
+
return data.configs[modelKey] as Record<string, unknown>;
|
|
315
200
|
}
|
|
316
|
-
} catch {
|
|
317
|
-
|
|
201
|
+
} catch (err) {
|
|
202
|
+
_logger.warn("Failed to read Qoder model config cache", {
|
|
203
|
+
modelKey,
|
|
204
|
+
error: err instanceof Error ? err.message : String(err),
|
|
205
|
+
});
|
|
318
206
|
}
|
|
319
207
|
}
|
|
320
208
|
return null;
|
package/providers/qoder/qoder.ts
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers the Qoder provider with Pi, providing free access to top-tier
|
|
5
5
|
* LLM models (DeepSeek V4 Pro/Flash, Qwen3.7 Plus/Max, GLM 5.1, Kimi K2.6,
|
|
6
|
-
* MiniMax M3) through Qoder's
|
|
6
|
+
* MiniMax M3) through Qoder's OpenAI-compatible API.
|
|
7
7
|
*
|
|
8
8
|
* Qoder uses a custom authentication protocol (PAT exchange + COSY signing)
|
|
9
|
-
* and a
|
|
10
|
-
*
|
|
9
|
+
* and a credits-based pricing model:
|
|
10
|
+
* - Community Edition (free): basic models with daily message limits
|
|
11
|
+
* - Pro / Pro+ / Ultra (paid): premium models via monthly credits
|
|
11
12
|
*
|
|
12
13
|
* Usage:
|
|
13
14
|
* Install pi-free, then run /login qoder to authenticate
|
|
14
15
|
* (PAT paste or browser OAuth)
|
|
16
|
+
* /toggle-qoder switches between basic (free-tier) and all models
|
|
15
17
|
*
|
|
16
18
|
* Environment variables:
|
|
17
19
|
* QODER_PERSONAL_ACCESS_TOKEN — PAT for headless auth (optional)
|
|
@@ -24,15 +26,17 @@ import type {
|
|
|
24
26
|
ProviderModelConfig,
|
|
25
27
|
} from "@earendil-works/pi-coding-agent";
|
|
26
28
|
import { BASE_URL_QODER, PROVIDER_QODER } from "../../constants.ts";
|
|
29
|
+
import { getProviderShowPaid } from "../../config.ts";
|
|
27
30
|
import {
|
|
28
31
|
getCachedModels,
|
|
29
|
-
|
|
30
|
-
|
|
32
|
+
isBasicModel,
|
|
33
|
+
type QoderModelConfig,
|
|
31
34
|
} from "./models.ts";
|
|
32
35
|
import { getCachedCredentials, loginQoder, refreshQoderToken } from "./auth.ts";
|
|
33
36
|
import { streamQoder } from "./stream.ts";
|
|
34
37
|
import { enhanceWithCI } from "../../provider-helper.ts";
|
|
35
|
-
import {
|
|
38
|
+
import { registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
39
|
+
import { createToggleState } from "../../lib/toggle-state.ts";
|
|
36
40
|
|
|
37
41
|
// =============================================================================
|
|
38
42
|
// Extension Entry Point
|
|
@@ -42,33 +46,21 @@ export default async function qoderProvider(pi: ExtensionAPI) {
|
|
|
42
46
|
const logger = (await import("../../lib/logger.ts")).createLogger("qoder");
|
|
43
47
|
|
|
44
48
|
// Initial model fetch
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const
|
|
49
|
+
const allModels: QoderModelConfig[] = getCachedModels();
|
|
50
|
+
const basicModels: QoderModelConfig[] = allModels.filter(isBasicModel);
|
|
51
|
+
const stored = { free: basicModels, all: allModels };
|
|
52
|
+
|
|
53
|
+
const toggleState = createToggleState({
|
|
54
|
+
providerId: PROVIDER_QODER,
|
|
55
|
+
initialShowPaid: getProviderShowPaid(PROVIDER_QODER),
|
|
56
|
+
initialModels: stored,
|
|
57
|
+
});
|
|
50
58
|
|
|
51
59
|
// ── OAuth config (defined before reRegister so it's always available) ──
|
|
52
60
|
const oauthConfig = {
|
|
53
61
|
name: "Qoder (Browser OAuth / PAT)",
|
|
54
62
|
login: async (callbacks: any): Promise<OAuthCredentials> => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// After login, refresh models from API
|
|
58
|
-
try {
|
|
59
|
-
const accessToken = cred.access as string;
|
|
60
|
-
const creds = getCachedCredentials();
|
|
61
|
-
await refreshModels(
|
|
62
|
-
accessToken,
|
|
63
|
-
creds?.userID || "qoder-user",
|
|
64
|
-
creds?.name || "Qoder User",
|
|
65
|
-
creds?.email || "user@qoder.com",
|
|
66
|
-
);
|
|
67
|
-
} catch {
|
|
68
|
-
// Best-effort
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return cred;
|
|
63
|
+
return loginQoder(callbacks);
|
|
72
64
|
},
|
|
73
65
|
refreshToken: refreshQoderToken,
|
|
74
66
|
getApiKey: (cred: OAuthCredentials) => cred.access,
|
|
@@ -86,67 +78,40 @@ export default async function qoderProvider(pi: ExtensionAPI) {
|
|
|
86
78
|
});
|
|
87
79
|
};
|
|
88
80
|
|
|
89
|
-
// ── Helper: refresh models from API and re-register ──
|
|
90
|
-
const refreshModels = async (
|
|
91
|
-
accessToken: string,
|
|
92
|
-
userID: string,
|
|
93
|
-
name: string,
|
|
94
|
-
email: string,
|
|
95
|
-
) => {
|
|
96
|
-
await updateQoderModelsCache(accessToken, userID, name, email);
|
|
97
|
-
const fresh = getCachedModels();
|
|
98
|
-
if (fresh.length > 0) {
|
|
99
|
-
allModels = fresh;
|
|
100
|
-
freeModels = fresh.filter((m) =>
|
|
101
|
-
isFreeModel({ ...m, provider: PROVIDER_QODER }, fresh),
|
|
102
|
-
);
|
|
103
|
-
stored.all = allModels;
|
|
104
|
-
stored.free = freeModels;
|
|
105
|
-
reRegister(allModels);
|
|
106
|
-
logger.info(`[qoder] Models refreshed: ${allModels.length}`);
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
81
|
// Register with global toggle system so it participates in /toggle-free
|
|
111
82
|
registerWithGlobalToggle(PROVIDER_QODER, stored, (m) => reRegister(m), false);
|
|
112
83
|
|
|
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
|
-
await refreshModels(
|
|
140
|
-
accessToken,
|
|
141
|
-
creds?.userID || "qoder-user",
|
|
142
|
-
creds?.name || "Qoder User",
|
|
143
|
-
creds?.email || "user@qoder.com",
|
|
144
|
-
);
|
|
145
|
-
} catch {
|
|
146
|
-
// Best-effort: fall back to existing cache / static models
|
|
147
|
-
}
|
|
84
|
+
// Per-provider toggle: /toggle-qoder (basic free-tier ↔ all models)
|
|
85
|
+
pi.registerCommand("toggle-qoder", {
|
|
86
|
+
description: "Toggle between basic (free-tier) and all Qoder models",
|
|
87
|
+
handler: async (_args, ctx) => {
|
|
88
|
+
try {
|
|
89
|
+
const applied = toggleState.toggle(reRegister);
|
|
90
|
+
const basicCount = stored.free.length;
|
|
91
|
+
const premiumCount = stored.all.length - basicCount;
|
|
92
|
+
if (applied.mode === "all") {
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
`qoder: showing all ${stored.all.length} models (${basicCount} basic, ${premiumCount} premium)`,
|
|
95
|
+
"info",
|
|
96
|
+
);
|
|
97
|
+
} else {
|
|
98
|
+
ctx.ui.notify(
|
|
99
|
+
`qoder: showing ${stored.free.length} basic (free-tier) models`,
|
|
100
|
+
"info",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
} catch (err) {
|
|
104
|
+
logger.error("[qoder] toggle failed", {
|
|
105
|
+
error: err instanceof Error ? err.message : String(err),
|
|
106
|
+
});
|
|
107
|
+
ctx.ui.notify("qoder: toggle failed", "error");
|
|
108
|
+
}
|
|
109
|
+
},
|
|
148
110
|
});
|
|
149
111
|
|
|
112
|
+
// Initial registration respects the configured show-paid mode.
|
|
113
|
+
toggleState.applyCurrent(reRegister);
|
|
114
|
+
|
|
150
115
|
logger.info(`[qoder] Provider registered with ${allModels.length} models`);
|
|
151
116
|
}
|
|
152
117
|
|