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.
@@ -0,0 +1,154 @@
1
+ /**
2
+ * Qoder Provider Extension
3
+ *
4
+ * Registers the Qoder provider with Pi, providing free access to top-tier
5
+ * LLM models (DeepSeek V4 Pro/Flash, Qwen3.7 Plus/Max, GLM 5.1, Kimi K2.6,
6
+ * MiniMax M3) through Qoder's proprietary API.
7
+ *
8
+ * Qoder uses a custom authentication protocol (PAT exchange + COSY signing)
9
+ * and a non-standard streaming API. All models are completely free — no
10
+ * paid tier exists.
11
+ *
12
+ * Usage:
13
+ * Install pi-free, then run /login qoder to authenticate
14
+ * (PAT paste or browser OAuth)
15
+ *
16
+ * Environment variables:
17
+ * QODER_PERSONAL_ACCESS_TOKEN — PAT for headless auth (optional)
18
+ * QODER_PAT — Alias for above
19
+ */
20
+
21
+ import type { Api, OAuthCredentials } from "@earendil-works/pi-ai";
22
+ import type {
23
+ ExtensionAPI,
24
+ ProviderModelConfig,
25
+ } from "@earendil-works/pi-coding-agent";
26
+ import { BASE_URL_QODER, PROVIDER_QODER } from "../../constants.ts";
27
+ import {
28
+ getCachedModels,
29
+ isCacheStale,
30
+ updateQoderModelsCache,
31
+ } from "./models.ts";
32
+ import { getCachedCredentials, loginQoder, refreshQoderToken } from "./auth.ts";
33
+ import { streamQoder } from "./stream.ts";
34
+ import { enhanceWithCI } from "../../provider-helper.ts";
35
+ import { isFreeModel, registerWithGlobalToggle } from "../../lib/registry.ts";
36
+
37
+ // =============================================================================
38
+ // Extension Entry Point
39
+ // =============================================================================
40
+
41
+ export default async function qoderProvider(pi: ExtensionAPI) {
42
+ const logger = (await import("../../lib/logger.ts")).createLogger("qoder");
43
+
44
+ // Initial model fetch
45
+ let allModels: ProviderModelConfig[] = getCachedModels();
46
+ let freeModels: ProviderModelConfig[] = allModels.filter((m) =>
47
+ isFreeModel({ ...m, provider: PROVIDER_QODER }, allModels),
48
+ );
49
+ const stored = { free: freeModels, all: allModels };
50
+
51
+ // ── OAuth config (defined before reRegister so it's always available) ──
52
+ const oauthConfig = {
53
+ name: "Qoder (Browser OAuth / PAT)",
54
+ login: async (callbacks: any): Promise<OAuthCredentials> => {
55
+ const cred = await loginQoder(callbacks);
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;
72
+ },
73
+ refreshToken: refreshQoderToken,
74
+ getApiKey: (cred: OAuthCredentials) => cred.access,
75
+ };
76
+
77
+ // Re-register function — called by toggle, session refresh, login
78
+ const reRegister = (models: ProviderModelConfig[]) => {
79
+ const enhanced = enhanceWithCI(models, PROVIDER_QODER);
80
+ pi.registerProvider(PROVIDER_QODER, {
81
+ baseUrl: BASE_URL_QODER,
82
+ api: "qoder-api" as Api,
83
+ models: enhanced,
84
+ oauth: oauthConfig,
85
+ streamSimple: streamQoder,
86
+ });
87
+ };
88
+
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
+ // Register with global toggle system so it participates in /toggle-free
111
+ registerWithGlobalToggle(PROVIDER_QODER, stored, (m) => reRegister(m), false);
112
+
113
+ // If user is already authenticated and cache is stale, refresh at startup
114
+ // (mirrors kilo/cline pattern: check cache freshness before hitting network)
115
+ try {
116
+ const cachedCreds = getCachedCredentials();
117
+ if (cachedCreds?.access && isCacheStale()) {
118
+ await refreshModels(
119
+ cachedCreds.access as string,
120
+ cachedCreds.userID || "qoder-user",
121
+ cachedCreds.name || "Qoder User",
122
+ cachedCreds.email || "user@qoder.com",
123
+ );
124
+ }
125
+ } catch {
126
+ // Best-effort: fall back to cached / static models
127
+ }
128
+
129
+ // Initial registration
130
+ reRegister(allModels);
131
+
132
+ // Refresh models cache on session_start if stale (>1h old)
133
+ pi.on("session_start", async (_event, ctx) => {
134
+ try {
135
+ const accessToken =
136
+ await ctx.modelRegistry.getApiKeyForProvider(PROVIDER_QODER);
137
+ if (!accessToken || !isCacheStale()) return;
138
+ const creds = getCachedCredentials();
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
+ }
148
+ });
149
+
150
+ logger.info(`[qoder] Provider registered with ${allModels.length} models`);
151
+ }
152
+
153
+ // Re-export key symbols for testing
154
+ export { loginQoder, refreshQoderToken, getCachedCredentials, streamQoder };