pi-free 2.0.2 → 2.0.5
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 +84 -12
- package/README.md +44 -97
- package/banner.svg +132 -0
- package/config.ts +24 -52
- package/constants.ts +6 -0
- package/index.ts +175 -148
- package/lib/built-in-toggle.ts +40 -1
- package/lib/model-detection.ts +176 -139
- package/lib/model-enhancer.ts +20 -20
- package/lib/open-browser.ts +1 -1
- package/lib/provider-compat.ts +46 -0
- package/lib/registry.ts +200 -144
- package/lib/types.ts +101 -108
- package/lib/util.ts +262 -256
- package/package.json +9 -8
- package/provider-failover/benchmark-lookup.ts +191 -140
- package/provider-helper.ts +19 -1
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline.ts +58 -14
- package/providers/crofai/crofai.ts +170 -0
- package/providers/dynamic-built-in/index.ts +260 -308
- package/providers/kilo/kilo-auth.ts +155 -155
- package/providers/kilo/kilo.ts +263 -235
- package/providers/nvidia/nvidia.ts +474 -415
- package/providers/ollama/ollama.ts +295 -280
- package/providers/opencode-session.ts +3 -4
- package/providers/qwen/qwen-models.ts +101 -101
- package/providers/qwen/qwen.ts +47 -49
- package/providers/zenmux/zenmux.ts +176 -0
- package/scripts/check-extensions.mjs +71 -55
- package/provider-factory.ts +0 -207
- package/providers/cloudflare/cloudflare.ts +0 -526
- package/providers/modal/modal.ts +0 -47
|
@@ -1,101 +1,101 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen OAuth model definitions.
|
|
3
|
-
*
|
|
4
|
-
* @deprecated The 1,000 req/day free tier is no longer available. Auth is broken.
|
|
5
|
-
* This provider remains for backward compatibility but should not be used.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
|
|
9
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
10
|
-
|
|
11
|
-
const _logger = createLogger("qwen-models");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* portal.qwen.ai compatibility settings.
|
|
15
|
-
*
|
|
16
|
-
* portal.qwen.ai's OpenAI-compatible API does not support several parameters
|
|
17
|
-
* that the pi framework sends by default.
|
|
18
|
-
*/
|
|
19
|
-
export const PORTAL_COMPAT: NonNullable<ProviderModelConfig["compat"]> = {
|
|
20
|
-
supportsStore: false,
|
|
21
|
-
supportsDeveloperRole: false,
|
|
22
|
-
supportsReasoningEffort: false,
|
|
23
|
-
supportsUsageInStreaming: false,
|
|
24
|
-
supportsStrictMode: false,
|
|
25
|
-
maxTokensField: "max_tokens",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Fallback model used before OAuth completes or if model discovery fails.
|
|
30
|
-
* The real model ID is resolved dynamically via fetchQwenLiveModels() after auth.
|
|
31
|
-
*/
|
|
32
|
-
export const QWEN_FREE_MODELS: ProviderModelConfig[] = [
|
|
33
|
-
{
|
|
34
|
-
id: "coder-model",
|
|
35
|
-
name: "Qwen Coder — DEPRECATED (free tier discontinued)",
|
|
36
|
-
reasoning: false,
|
|
37
|
-
input: ["text"],
|
|
38
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
-
contextWindow: 131_072,
|
|
40
|
-
maxTokens: 16_384,
|
|
41
|
-
compat: PORTAL_COMPAT,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Fetch Qwen models. Returns static model list for backward compatibility.
|
|
47
|
-
* @deprecated Qwen free tier is discontinued.
|
|
48
|
-
*/
|
|
49
|
-
export async function fetchQwenModels(): Promise<ProviderModelConfig[]> {
|
|
50
|
-
_logger.info("Qwen provider is deprecated, returning placeholder models");
|
|
51
|
-
return QWEN_FREE_MODELS;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Fetch live model list from the Qwen API using the OAuth access token.
|
|
56
|
-
* Returns updated models with real IDs from the server, or the original
|
|
57
|
-
* models unchanged if the request fails.
|
|
58
|
-
*/
|
|
59
|
-
export async function fetchQwenLiveModels(
|
|
60
|
-
baseUrl: string,
|
|
61
|
-
accessToken: string,
|
|
62
|
-
templateModels: ProviderModelConfig[],
|
|
63
|
-
): Promise<ProviderModelConfig[]> {
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch(`${baseUrl}/models`, {
|
|
66
|
-
headers: {
|
|
67
|
-
Authorization: `Bearer ${accessToken}`,
|
|
68
|
-
Accept: "application/json",
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
_logger.info("Qwen /v1/models fetch failed, keeping current model IDs", {
|
|
74
|
-
status: response.status,
|
|
75
|
-
});
|
|
76
|
-
return templateModels;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface ModelEntry {
|
|
80
|
-
id: string;
|
|
81
|
-
}
|
|
82
|
-
const data = (await response.json()) as { data?: ModelEntry[] };
|
|
83
|
-
const ids: string[] = (data.data ?? [])
|
|
84
|
-
.map((m: ModelEntry) => m.id)
|
|
85
|
-
.filter(Boolean);
|
|
86
|
-
|
|
87
|
-
_logger.info("Qwen live models discovered", { ids });
|
|
88
|
-
|
|
89
|
-
if (ids.length === 0) return templateModels;
|
|
90
|
-
|
|
91
|
-
// Prefer a coder model if available, otherwise use the first model
|
|
92
|
-
const preferred = ids.find((id) => /coder/i.test(id)) ?? ids[0];
|
|
93
|
-
|
|
94
|
-
return templateModels.map((m) => ({ ...m, id: preferred }));
|
|
95
|
-
} catch (err) {
|
|
96
|
-
_logger.info("Qwen live model fetch error, keeping current model IDs", {
|
|
97
|
-
error: String(err),
|
|
98
|
-
});
|
|
99
|
-
return templateModels;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Qwen OAuth model definitions.
|
|
3
|
+
*
|
|
4
|
+
* @deprecated The 1,000 req/day free tier is no longer available. Auth is broken.
|
|
5
|
+
* This provider remains for backward compatibility but should not be used.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProviderModelConfig } from "@mariozechner/pi-coding-agent";
|
|
9
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
10
|
+
|
|
11
|
+
const _logger = createLogger("qwen-models");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* portal.qwen.ai compatibility settings.
|
|
15
|
+
*
|
|
16
|
+
* portal.qwen.ai's OpenAI-compatible API does not support several parameters
|
|
17
|
+
* that the pi framework sends by default.
|
|
18
|
+
*/
|
|
19
|
+
export const PORTAL_COMPAT: NonNullable<ProviderModelConfig["compat"]> = {
|
|
20
|
+
supportsStore: false,
|
|
21
|
+
supportsDeveloperRole: false,
|
|
22
|
+
supportsReasoningEffort: false,
|
|
23
|
+
supportsUsageInStreaming: false,
|
|
24
|
+
supportsStrictMode: false,
|
|
25
|
+
maxTokensField: "max_tokens",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Fallback model used before OAuth completes or if model discovery fails.
|
|
30
|
+
* The real model ID is resolved dynamically via fetchQwenLiveModels() after auth.
|
|
31
|
+
*/
|
|
32
|
+
export const QWEN_FREE_MODELS: ProviderModelConfig[] = [
|
|
33
|
+
{
|
|
34
|
+
id: "coder-model",
|
|
35
|
+
name: "Qwen Coder — DEPRECATED (free tier discontinued)",
|
|
36
|
+
reasoning: false,
|
|
37
|
+
input: ["text"],
|
|
38
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
+
contextWindow: 131_072,
|
|
40
|
+
maxTokens: 16_384,
|
|
41
|
+
compat: PORTAL_COMPAT,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Fetch Qwen models. Returns static model list for backward compatibility.
|
|
47
|
+
* @deprecated Qwen free tier is discontinued.
|
|
48
|
+
*/
|
|
49
|
+
export async function fetchQwenModels(): Promise<ProviderModelConfig[]> {
|
|
50
|
+
_logger.info("Qwen provider is deprecated, returning placeholder models");
|
|
51
|
+
return QWEN_FREE_MODELS;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Fetch live model list from the Qwen API using the OAuth access token.
|
|
56
|
+
* Returns updated models with real IDs from the server, or the original
|
|
57
|
+
* models unchanged if the request fails.
|
|
58
|
+
*/
|
|
59
|
+
export async function fetchQwenLiveModels(
|
|
60
|
+
baseUrl: string,
|
|
61
|
+
accessToken: string,
|
|
62
|
+
templateModels: ProviderModelConfig[],
|
|
63
|
+
): Promise<ProviderModelConfig[]> {
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetch(`${baseUrl}/models`, {
|
|
66
|
+
headers: {
|
|
67
|
+
Authorization: `Bearer ${accessToken}`,
|
|
68
|
+
Accept: "application/json",
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
_logger.info("Qwen /v1/models fetch failed, keeping current model IDs", {
|
|
74
|
+
status: response.status,
|
|
75
|
+
});
|
|
76
|
+
return templateModels;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface ModelEntry {
|
|
80
|
+
id: string;
|
|
81
|
+
}
|
|
82
|
+
const data = (await response.json()) as { data?: ModelEntry[] };
|
|
83
|
+
const ids: string[] = (data.data ?? [])
|
|
84
|
+
.map((m: ModelEntry) => m.id)
|
|
85
|
+
.filter(Boolean);
|
|
86
|
+
|
|
87
|
+
_logger.info("Qwen live models discovered", { ids });
|
|
88
|
+
|
|
89
|
+
if (ids.length === 0) return templateModels;
|
|
90
|
+
|
|
91
|
+
// Prefer a coder model if available, otherwise use the first model
|
|
92
|
+
const preferred = ids.find((id) => /coder/i.test(id)) ?? ids[0];
|
|
93
|
+
|
|
94
|
+
return templateModels.map((m) => ({ ...m, id: preferred }));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
_logger.info("Qwen live model fetch error, keeping current model IDs", {
|
|
97
|
+
error: String(err),
|
|
98
|
+
});
|
|
99
|
+
return templateModels;
|
|
100
|
+
}
|
|
101
|
+
}
|
package/providers/qwen/qwen.ts
CHANGED
|
@@ -10,18 +10,18 @@
|
|
|
10
10
|
* 1,000 free API calls/day — run /login qwen to authenticate.~~
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import type {
|
|
13
|
+
import type { Api, Model, OAuthCredentials } from "@mariozechner/pi-ai";
|
|
14
14
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
15
15
|
import { PROVIDER_QWEN, URL_QWEN_TOS } from "../../constants.ts";
|
|
16
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
17
|
+
import { logWarning } from "../../lib/util.ts";
|
|
16
18
|
import {
|
|
19
|
+
createReRegister,
|
|
17
20
|
enhanceWithCI,
|
|
18
21
|
type StoredModels,
|
|
19
22
|
setupProvider,
|
|
20
|
-
createReRegister,
|
|
21
23
|
} from "../../provider-helper.ts";
|
|
22
|
-
import {
|
|
23
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
24
|
-
import { loginQwen, refreshQwenToken, getQwenBaseUrl } from "./qwen-auth.ts";
|
|
24
|
+
import { getQwenBaseUrl, loginQwen, refreshQwenToken } from "./qwen-auth.ts";
|
|
25
25
|
import { fetchQwenModels } from "./qwen-models.ts";
|
|
26
26
|
|
|
27
27
|
// =============================================================================
|
|
@@ -61,10 +61,12 @@ const DASHSCOPE_HEADERS = {
|
|
|
61
61
|
|
|
62
62
|
export default async function (pi: ExtensionAPI) {
|
|
63
63
|
// DEPRECATION WARNING
|
|
64
|
-
_logger.warn(
|
|
64
|
+
_logger.warn(
|
|
65
|
+
"Qwen provider is deprecated. The 1,000 req/day free tier is no longer available.",
|
|
66
|
+
);
|
|
65
67
|
|
|
66
68
|
// Fetch static free-tier models
|
|
67
|
-
|
|
69
|
+
const models = await fetchQwenModels().catch((err) => {
|
|
68
70
|
logWarning("qwen", "Failed to load models at startup", err);
|
|
69
71
|
return [];
|
|
70
72
|
});
|
|
@@ -148,55 +150,51 @@ export default async function (pi: ExtensionAPI) {
|
|
|
148
150
|
// getApiKey() call will trigger a token refresh via refreshToken().
|
|
149
151
|
// This mirrors qwen-code's executeWithCredentialManagement() retry logic.
|
|
150
152
|
//
|
|
153
|
+
function isQwenAuthError(msg: {
|
|
154
|
+
role?: string;
|
|
155
|
+
errorMessage?: string;
|
|
156
|
+
}): boolean {
|
|
157
|
+
if (msg.role !== "assistant" || !msg.errorMessage) return false;
|
|
158
|
+
const errLower = msg.errorMessage.toLowerCase();
|
|
159
|
+
return AUTH_ERROR_PATTERNS.some((p) => errLower.includes(p));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function forceExpireQwenToken(
|
|
163
|
+
ctx: any,
|
|
164
|
+
msg: { errorMessage?: string },
|
|
165
|
+
): void {
|
|
166
|
+
_logger.warn("Qwen auth error detected, force-expiring token for refresh", {
|
|
167
|
+
error: (msg.errorMessage ?? "").slice(0, 100),
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const authStorage = (ctx as any).modelRegistry?.authStorage;
|
|
172
|
+
if (authStorage) {
|
|
173
|
+
const cred = authStorage.get(PROVIDER_QWEN);
|
|
174
|
+
if (cred?.type === "oauth" && cred.expires > Date.now()) {
|
|
175
|
+
authStorage.set(PROVIDER_QWEN, { ...cred, expires: 0 });
|
|
176
|
+
_logger.info(
|
|
177
|
+
"Qwen token force-expired; will refresh on next request",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ctx.ui.notify("Qwen: auth error detected, refreshing token…", "warning");
|
|
182
|
+
} catch (e) {
|
|
183
|
+
_logger.warn("Failed to force-expire Qwen token", {
|
|
184
|
+
error: e instanceof Error ? e.message : String(e),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
151
189
|
pi.on("turn_end", async (event, ctx) => {
|
|
152
190
|
if (ctx.model?.provider !== PROVIDER_QWEN) return;
|
|
153
|
-
// NOTE: Request counting removed - usage tracking was deleted in refactor
|
|
154
191
|
|
|
155
192
|
const msg = (
|
|
156
193
|
event as { message?: { role?: string; errorMessage?: string } }
|
|
157
194
|
).message;
|
|
158
195
|
|
|
159
|
-
if (msg
|
|
160
|
-
|
|
161
|
-
const isAuthError = AUTH_ERROR_PATTERNS.some((p) =>
|
|
162
|
-
errLower.includes(p),
|
|
163
|
-
);
|
|
164
|
-
|
|
165
|
-
if (isAuthError) {
|
|
166
|
-
_logger.warn(
|
|
167
|
-
"Qwen auth error detected, force-expiring token for refresh",
|
|
168
|
-
{ error: msg.errorMessage.slice(0, 100) },
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
// Force-expire the stored credential so the next getApiKey() call
|
|
172
|
-
// triggers refreshQwenToken(). The credential object in auth.json
|
|
173
|
-
// is updated with expires = 0 (already past).
|
|
174
|
-
try {
|
|
175
|
-
const authStorage =
|
|
176
|
-
(ctx as any).modelRegistry?.authStorage;
|
|
177
|
-
if (authStorage) {
|
|
178
|
-
const cred = authStorage.get(PROVIDER_QWEN);
|
|
179
|
-
if (cred?.type === "oauth" && cred.expires > Date.now()) {
|
|
180
|
-
// Set expiry to 0 to force refresh on next request
|
|
181
|
-
authStorage.set(PROVIDER_QWEN, {
|
|
182
|
-
...cred,
|
|
183
|
-
expires: 0,
|
|
184
|
-
});
|
|
185
|
-
_logger.info(
|
|
186
|
-
"Qwen token force-expired; will refresh on next request",
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
ctx.ui.notify(
|
|
191
|
-
"Qwen: auth error detected, refreshing token…",
|
|
192
|
-
"warning",
|
|
193
|
-
);
|
|
194
|
-
} catch (e) {
|
|
195
|
-
_logger.warn("Failed to force-expire Qwen token", {
|
|
196
|
-
error: e instanceof Error ? e.message : String(e),
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
}
|
|
196
|
+
if (msg && isQwenAuthError(msg)) {
|
|
197
|
+
forceExpireQwenToken(ctx, msg);
|
|
200
198
|
}
|
|
201
199
|
});
|
|
202
200
|
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ZenMux Provider Extension
|
|
3
|
+
*
|
|
4
|
+
* Provides access to ZenMux AI gateway - unified API for 200+ models from
|
|
5
|
+
* OpenAI, Anthropic, Google, and other providers.
|
|
6
|
+
*
|
|
7
|
+
* Setup:
|
|
8
|
+
* 1. Get API key from https://zenmux.ai
|
|
9
|
+
* 2. Set ZENMUX_API_KEY env var or add to ~/.pi/free.json
|
|
10
|
+
*
|
|
11
|
+
* Responds to global free-only filter.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* pi install git:github.com/apmantza/pi-free
|
|
15
|
+
* # Set ZENMUX_API_KEY env var
|
|
16
|
+
* # Models appear in /model selector
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type {
|
|
20
|
+
ExtensionAPI,
|
|
21
|
+
ProviderModelConfig,
|
|
22
|
+
} from "@mariozechner/pi-coding-agent";
|
|
23
|
+
import { getZenmuxApiKey, getZenmuxShowPaid } from "../../config.ts";
|
|
24
|
+
import {
|
|
25
|
+
BASE_URL_ZENMUX,
|
|
26
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
27
|
+
PROVIDER_ZENMUX,
|
|
28
|
+
} from "../../constants.ts";
|
|
29
|
+
import { createLogger } from "../../lib/logger.ts";
|
|
30
|
+
import {
|
|
31
|
+
getProxyModelCompat,
|
|
32
|
+
isLikelyReasoningModel,
|
|
33
|
+
} from "../../lib/provider-compat.ts";
|
|
34
|
+
import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
|
|
35
|
+
import { fetchWithRetry } from "../../lib/util.ts";
|
|
36
|
+
import { createReRegister, setupProvider } from "../../provider-helper.ts";
|
|
37
|
+
|
|
38
|
+
const _logger = createLogger("zenmux");
|
|
39
|
+
|
|
40
|
+
// =============================================================================
|
|
41
|
+
// Fetch ZenMux models
|
|
42
|
+
// =============================================================================
|
|
43
|
+
|
|
44
|
+
interface ZenMuxModel {
|
|
45
|
+
id: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
context_length?: number;
|
|
48
|
+
pricing?: {
|
|
49
|
+
prompt?: number;
|
|
50
|
+
completion?: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isZenmuxReasoningModel(model: Pick<ZenMuxModel, "id" | "name">) {
|
|
55
|
+
const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
56
|
+
return isLikelyReasoningModel(model) || haystack.includes("claude");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchZenmuxModels(
|
|
60
|
+
apiKey: string,
|
|
61
|
+
): Promise<ProviderModelConfig[]> {
|
|
62
|
+
_logger.info("[zenmux] Fetching models from ZenMux API...");
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const response = await fetchWithRetry(
|
|
66
|
+
`${BASE_URL_ZENMUX}/models`,
|
|
67
|
+
{
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${apiKey}`,
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
3,
|
|
74
|
+
1000,
|
|
75
|
+
DEFAULT_FETCH_TIMEOUT_MS,
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new Error(`ZenMux API error: ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = (await response.json()) as { data?: ZenMuxModel[] };
|
|
83
|
+
const models = data.data ?? [];
|
|
84
|
+
|
|
85
|
+
_logger.info(`[zenmux] Fetched ${models.length} models`);
|
|
86
|
+
|
|
87
|
+
return models.map(
|
|
88
|
+
(m): ProviderModelConfig => ({
|
|
89
|
+
id: m.id,
|
|
90
|
+
name: m.name || m.id,
|
|
91
|
+
reasoning: isZenmuxReasoningModel(m),
|
|
92
|
+
input: ["text"],
|
|
93
|
+
cost: {
|
|
94
|
+
input: m.pricing?.prompt || 0,
|
|
95
|
+
output: m.pricing?.completion || 0,
|
|
96
|
+
cacheRead: 0,
|
|
97
|
+
cacheWrite: 0,
|
|
98
|
+
},
|
|
99
|
+
contextWindow: m.context_length || 128000,
|
|
100
|
+
maxTokens: m.context_length ? Math.floor(m.context_length / 2) : 4096,
|
|
101
|
+
compat: getProxyModelCompat(m),
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
_logger.error("[zenmux] Failed to fetch models:", {
|
|
106
|
+
error: error instanceof Error ? error.message : String(error),
|
|
107
|
+
});
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// =============================================================================
|
|
113
|
+
// Extension Entry Point
|
|
114
|
+
// =============================================================================
|
|
115
|
+
|
|
116
|
+
export default async function zenmuxProvider(pi: ExtensionAPI) {
|
|
117
|
+
const apiKey = getZenmuxApiKey();
|
|
118
|
+
|
|
119
|
+
if (!apiKey) {
|
|
120
|
+
_logger.info(
|
|
121
|
+
"[zenmux] Skipping - ZENMUX_API_KEY not set (env var or ~/.pi/free.json)",
|
|
122
|
+
);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fetch models
|
|
127
|
+
const allModels = await fetchZenmuxModels(apiKey);
|
|
128
|
+
|
|
129
|
+
if (allModels.length === 0) {
|
|
130
|
+
_logger.warn("[zenmux] No models available");
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Use isFreeModel with allModels for proper detection
|
|
135
|
+
// ZenMux exposes pricing, so Route A (OR logic) will be used:
|
|
136
|
+
// FREE if cost=0 OR "free" in name
|
|
137
|
+
const freeModels = allModels.filter((m) =>
|
|
138
|
+
isFreeModel({ ...m, provider: PROVIDER_ZENMUX }, allModels),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const stored = { free: freeModels, all: allModels };
|
|
142
|
+
|
|
143
|
+
_logger.info(
|
|
144
|
+
`[zenmux] Registered ${allModels.length} models (${freeModels.length} free)`,
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Create re-register function
|
|
148
|
+
const reRegister = createReRegister(pi, {
|
|
149
|
+
providerId: PROVIDER_ZENMUX,
|
|
150
|
+
baseUrl: BASE_URL_ZENMUX,
|
|
151
|
+
apiKey,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Register with global toggle
|
|
155
|
+
registerWithGlobalToggle(PROVIDER_ZENMUX, stored, reRegister, true);
|
|
156
|
+
|
|
157
|
+
// Setup provider with toggle command
|
|
158
|
+
setupProvider(
|
|
159
|
+
pi,
|
|
160
|
+
{
|
|
161
|
+
providerId: PROVIDER_ZENMUX,
|
|
162
|
+
initialShowPaid: getZenmuxShowPaid(),
|
|
163
|
+
reRegister: (models, _stored) => {
|
|
164
|
+
if (_stored) {
|
|
165
|
+
stored.free = _stored.free;
|
|
166
|
+
stored.all = _stored.all;
|
|
167
|
+
}
|
|
168
|
+
reRegister(models);
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
stored,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
// Initial registration
|
|
175
|
+
reRegister(freeModels);
|
|
176
|
+
}
|
|
@@ -7,48 +7,58 @@
|
|
|
7
7
|
* node scripts/check-extensions.mjs <dir> # from installed location
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
11
|
-
import { join, resolve, dirname } from "node:path";
|
|
12
10
|
import { execSync } from "node:child_process";
|
|
11
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
12
|
+
import { dirname, join, resolve } from "node:path";
|
|
13
13
|
|
|
14
14
|
const installDir = resolve(process.argv[2] ?? ".");
|
|
15
15
|
const fromSource = process.argv[2] == null;
|
|
16
16
|
|
|
17
17
|
function getFiles() {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
18
|
+
if (fromSource) {
|
|
19
|
+
// Use npm pack --dry-run to get exactly the files that would be published
|
|
20
|
+
const execOptions = { encoding: "utf8" };
|
|
21
|
+
if (process.platform !== "win32") {
|
|
22
|
+
execOptions.env = {
|
|
23
|
+
...process.env,
|
|
24
|
+
PATH: "/usr/local/bin:/usr/bin:/bin",
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const out = execSync("npm pack --dry-run 2>&1", execOptions);
|
|
28
|
+
return out
|
|
29
|
+
.split("\n")
|
|
30
|
+
.map((l) => l.match(/npm notice \S+\s+(.+)/)?.[1]?.trim())
|
|
31
|
+
.filter((f) => f && (f.endsWith(".ts") || f.endsWith(".mjs")))
|
|
32
|
+
.map((f) => join(installDir, f));
|
|
33
|
+
}
|
|
34
|
+
// Installed location: walk all .ts/.mjs files
|
|
35
|
+
const files = [];
|
|
36
|
+
function walk(dir) {
|
|
37
|
+
for (const entry of readdirSync(dir)) {
|
|
38
|
+
const full = join(dir, entry);
|
|
39
|
+
if (entry === "node_modules") continue;
|
|
40
|
+
if (statSync(full).isDirectory()) walk(full);
|
|
41
|
+
else if (entry.endsWith(".ts") || entry.endsWith(".mjs"))
|
|
42
|
+
files.push(full);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
walk(installDir);
|
|
46
|
+
return files;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
function resolveImport(fromFile, importPath) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const base = join(dirname(fromFile), importPath);
|
|
51
|
+
for (const candidate of [
|
|
52
|
+
base,
|
|
53
|
+
base.replace(/\.js$/, ".ts"), // .js → .ts (TypeScript ESM convention)
|
|
54
|
+
base + ".ts",
|
|
55
|
+
join(base, "index.ts"),
|
|
56
|
+
]) {
|
|
57
|
+
try {
|
|
58
|
+
if (statSync(candidate).isFile()) return candidate;
|
|
59
|
+
} catch {}
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
const files = getFiles();
|
|
@@ -59,29 +69,35 @@ let failed = 0;
|
|
|
59
69
|
const seen = new Set();
|
|
60
70
|
|
|
61
71
|
for (const file of files) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
72
|
+
const src = readFileSync(file, "utf8");
|
|
73
|
+
const relFile = file.slice(installDir.length + 1).replace(/\\/g, "/");
|
|
74
|
+
// Strip comments before matching imports
|
|
75
|
+
const stripped = src
|
|
76
|
+
.replace(/\/\/[^\n]*/g, "")
|
|
77
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
78
|
+
const importRe = /from\s+['"](\.[^'"]+)['"]/g;
|
|
79
|
+
let match;
|
|
80
|
+
while ((match = importRe.exec(stripped)) !== null) {
|
|
81
|
+
const importPath = match[1];
|
|
82
|
+
const key = `${relFile}:${importPath}`;
|
|
83
|
+
if (seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
totalImports++;
|
|
86
|
+
if (!resolveImport(file, importPath)) {
|
|
87
|
+
console.error(` ✗ ${relFile}`);
|
|
88
|
+
console.error(` imports '${importPath}' → NOT FOUND`);
|
|
89
|
+
failed++;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
console.log(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
console.log(
|
|
95
|
+
`Checked ${totalImports} relative import(s) across ${files.length} file(s).`,
|
|
96
|
+
);
|
|
97
|
+
console.log(
|
|
98
|
+
failed === 0
|
|
99
|
+
? `\nAll imports resolve OK ✓`
|
|
100
|
+
: `\n${failed} import(s) could not be resolved ✗`,
|
|
101
|
+
);
|
|
86
102
|
|
|
87
103
|
process.exit(failed > 0 ? 1 : 0);
|