@pixelbyte-software/pixcode 1.33.4 → 1.33.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/dist/assets/{index-D0qriZVb.js → index-D6ErCuvV.js} +49 -49
- package/dist/index.html +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +44 -12
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/opencode-cli.js +26 -7
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-response-handler.js +19 -9
- package/dist-server/server/opencode-response-handler.js.map +1 -1
- package/dist-server/server/projects.js +111 -1
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/qwen-code-cli.js +4 -1
- package/dist-server/server/qwen-code-cli.js.map +1 -1
- package/dist-server/server/services/provider-models.js +172 -8
- package/dist-server/server/services/provider-models.js.map +1 -1
- package/dist-server/shared/modelConstants.js +30 -11
- package/dist-server/shared/modelConstants.js.map +1 -1
- package/package.json +1 -1
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +37 -8
- package/server/opencode-cli.js +24 -7
- package/server/opencode-response-handler.js +17 -9
- package/server/projects.js +112 -0
- package/server/qwen-code-cli.js +4 -1
- package/server/services/provider-models.js +164 -8
- package/shared/modelConstants.js +34 -11
package/server/projects.js
CHANGED
|
@@ -699,6 +699,7 @@ async function getProjects(progressCallback = null) {
|
|
|
699
699
|
sessions: [],
|
|
700
700
|
geminiSessions: [],
|
|
701
701
|
qwenSessions: [],
|
|
702
|
+
opencodeSessions: [],
|
|
702
703
|
sessionMeta: {
|
|
703
704
|
hasMore: false,
|
|
704
705
|
total: 0
|
|
@@ -768,6 +769,14 @@ async function getProjects(progressCallback = null) {
|
|
|
768
769
|
}
|
|
769
770
|
applyCustomSessionNames(project.qwenSessions, 'qwen');
|
|
770
771
|
|
|
772
|
+
try {
|
|
773
|
+
project.opencodeSessions = await getOpencodeCliSessions(actualProjectDir);
|
|
774
|
+
} catch (e) {
|
|
775
|
+
console.warn(`Could not load OpenCode sessions for project ${entry.name}:`, e.message);
|
|
776
|
+
project.opencodeSessions = [];
|
|
777
|
+
}
|
|
778
|
+
applyCustomSessionNames(project.opencodeSessions, 'opencode');
|
|
779
|
+
|
|
771
780
|
try {
|
|
772
781
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
773
782
|
project.taskmaster = {
|
|
@@ -831,6 +840,7 @@ async function getProjects(progressCallback = null) {
|
|
|
831
840
|
sessions: [],
|
|
832
841
|
geminiSessions: [],
|
|
833
842
|
qwenSessions: [],
|
|
843
|
+
opencodeSessions: [],
|
|
834
844
|
sessionMeta: {
|
|
835
845
|
hasMore: false,
|
|
836
846
|
total: 0
|
|
@@ -876,6 +886,13 @@ async function getProjects(progressCallback = null) {
|
|
|
876
886
|
}
|
|
877
887
|
applyCustomSessionNames(project.geminiSessions, 'gemini');
|
|
878
888
|
|
|
889
|
+
try {
|
|
890
|
+
project.opencodeSessions = await getOpencodeCliSessions(actualProjectDir);
|
|
891
|
+
} catch (e) {
|
|
892
|
+
console.warn(`Could not load OpenCode sessions for tracked project ${projectName}:`, e.message);
|
|
893
|
+
}
|
|
894
|
+
applyCustomSessionNames(project.opencodeSessions, 'opencode');
|
|
895
|
+
|
|
879
896
|
try {
|
|
880
897
|
const taskMasterResult = await detectTaskMasterFolder(actualProjectDir);
|
|
881
898
|
|
|
@@ -2798,6 +2815,100 @@ async function getQwenCliSessions(projectPath, options = {}) {
|
|
|
2798
2815
|
);
|
|
2799
2816
|
}
|
|
2800
2817
|
|
|
2818
|
+
/**
|
|
2819
|
+
* OpenCode session enumerator. Walks `~/.local/share/opencode/project/*` (the
|
|
2820
|
+
* XDG data dir OpenCode uses on every platform — yes, including Windows under
|
|
2821
|
+
* the user profile) and surfaces every session whose `directory` field matches
|
|
2822
|
+
* the project root we're listing. `<projectID>` segments are git-root commit
|
|
2823
|
+
* hashes for git repos and the literal "global" for non-git dirs, so we
|
|
2824
|
+
* don't try to be clever about the slug — we just walk every project bucket
|
|
2825
|
+
* and filter by the stored `directory`.
|
|
2826
|
+
*
|
|
2827
|
+
* This is best-effort: when OpenCode hasn't written anything yet (brand-new
|
|
2828
|
+
* project, no chats sent) the data dir is missing and we return [] without
|
|
2829
|
+
* surfacing an error to the caller.
|
|
2830
|
+
*/
|
|
2831
|
+
async function getOpencodeCliSessions(projectPath) {
|
|
2832
|
+
const normalizedProjectPath = normalizeComparablePath(projectPath);
|
|
2833
|
+
if (!normalizedProjectPath) return [];
|
|
2834
|
+
|
|
2835
|
+
const sessions = [];
|
|
2836
|
+
const opencodeDataDir = path.join(os.homedir(), '.local', 'share', 'opencode', 'project');
|
|
2837
|
+
|
|
2838
|
+
let projectBuckets;
|
|
2839
|
+
try {
|
|
2840
|
+
projectBuckets = await fs.readdir(opencodeDataDir);
|
|
2841
|
+
} catch {
|
|
2842
|
+
return [];
|
|
2843
|
+
}
|
|
2844
|
+
|
|
2845
|
+
for (const bucket of projectBuckets) {
|
|
2846
|
+
const sessionRoot = path.join(opencodeDataDir, bucket, 'storage', 'session');
|
|
2847
|
+
let projectIdDirs;
|
|
2848
|
+
try {
|
|
2849
|
+
projectIdDirs = await fs.readdir(sessionRoot);
|
|
2850
|
+
} catch {
|
|
2851
|
+
continue;
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
for (const pidDir of projectIdDirs) {
|
|
2855
|
+
const dir = path.join(sessionRoot, pidDir);
|
|
2856
|
+
let sessionFiles;
|
|
2857
|
+
try {
|
|
2858
|
+
sessionFiles = await fs.readdir(dir);
|
|
2859
|
+
} catch {
|
|
2860
|
+
continue;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
for (const file of sessionFiles) {
|
|
2864
|
+
if (!file.startsWith('ses_') || !file.endsWith('.json')) continue;
|
|
2865
|
+
try {
|
|
2866
|
+
const data = await fs.readFile(path.join(dir, file), 'utf8');
|
|
2867
|
+
const sess = JSON.parse(data);
|
|
2868
|
+
const sessDir = typeof sess?.directory === 'string'
|
|
2869
|
+
? normalizeComparablePath(sess.directory)
|
|
2870
|
+
: null;
|
|
2871
|
+
if (!sessDir || sessDir !== normalizedProjectPath) continue;
|
|
2872
|
+
|
|
2873
|
+
const id = typeof sess?.id === 'string' ? sess.id : file.replace(/\.json$/, '');
|
|
2874
|
+
sessions.push({
|
|
2875
|
+
id,
|
|
2876
|
+
summary: sess?.title || 'OpenCode Session',
|
|
2877
|
+
messageCount: typeof sess?.messageCount === 'number' ? sess.messageCount : 0,
|
|
2878
|
+
lastActivity: sess?.time?.updated || sess?.time?.created || null,
|
|
2879
|
+
provider: 'opencode',
|
|
2880
|
+
});
|
|
2881
|
+
} catch {
|
|
2882
|
+
// Skip unreadable session files silently — one corrupt file
|
|
2883
|
+
// shouldn't blank the whole list.
|
|
2884
|
+
continue;
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// Merge in live sessions held by the in-memory sessionManager so a fresh
|
|
2891
|
+
// chat that hasn't been flushed to disk yet still shows in the sidebar.
|
|
2892
|
+
try {
|
|
2893
|
+
const liveSessions = sessionManager.getProjectSessions(projectPath) || [];
|
|
2894
|
+
for (const s of liveSessions) {
|
|
2895
|
+
if (!s.id || !s.id.startsWith('opencode_')) continue;
|
|
2896
|
+
if (sessions.some((existing) => existing.id === s.id)) continue;
|
|
2897
|
+
sessions.push({
|
|
2898
|
+
id: s.id,
|
|
2899
|
+
summary: s.summary || s.title || 'OpenCode Session',
|
|
2900
|
+
messageCount: s.messageCount || 0,
|
|
2901
|
+
lastActivity: s.lastActivity || s.createdAt || null,
|
|
2902
|
+
provider: 'opencode',
|
|
2903
|
+
});
|
|
2904
|
+
}
|
|
2905
|
+
} catch { /* sessionManager not initialised yet — ignore */ }
|
|
2906
|
+
|
|
2907
|
+
return sessions.sort((a, b) =>
|
|
2908
|
+
new Date(b.lastActivity || 0) - new Date(a.lastActivity || 0)
|
|
2909
|
+
);
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2801
2912
|
async function getQwenCliSessionMessages(sessionId) {
|
|
2802
2913
|
const qwenTmpDir = path.join(os.homedir(), '.qwen', 'tmp');
|
|
2803
2914
|
let projectDirs;
|
|
@@ -2989,5 +3100,6 @@ export {
|
|
|
2989
3100
|
getGeminiCliSessionMessages,
|
|
2990
3101
|
getQwenCliSessions,
|
|
2991
3102
|
getQwenCliSessionMessages,
|
|
3103
|
+
getOpencodeCliSessions,
|
|
2992
3104
|
searchConversations
|
|
2993
3105
|
};
|
package/server/qwen-code-cli.js
CHANGED
|
@@ -335,10 +335,13 @@ async function spawnQwen(command, options = {}, ws) {
|
|
|
335
335
|
const installed = await providerAuthService.isProviderInstalled('qwen');
|
|
336
336
|
const errorContent = !installed
|
|
337
337
|
? 'Qwen Code CLI is not installed. Install it first: npm install -g @qwen-code/qwen-code'
|
|
338
|
-
: error
|
|
338
|
+
: (error?.message || String(error));
|
|
339
339
|
|
|
340
340
|
const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
|
|
341
341
|
ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'qwen' }));
|
|
342
|
+
// Always emit `complete` so the UI's "Processing..." state clears
|
|
343
|
+
// even when spawn fails (ENOENT, EACCES) and `close` never fires.
|
|
344
|
+
ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 1, isNewSession: !sessionId && !!command, sessionId: errorSessionId, provider: 'qwen' }));
|
|
342
345
|
notifyTerminalState({ error });
|
|
343
346
|
|
|
344
347
|
reject(error);
|
|
@@ -71,7 +71,9 @@ function normalizeList(list) {
|
|
|
71
71
|
seen.add(value);
|
|
72
72
|
const label = typeof item.label === 'string' && item.label.trim() ? item.label.trim() : value;
|
|
73
73
|
const source = item.source === 'api' ? 'api' : 'static';
|
|
74
|
-
|
|
74
|
+
const entry = { value, label, source };
|
|
75
|
+
if (typeof item.free === 'boolean') entry.free = item.free;
|
|
76
|
+
out.push(entry);
|
|
75
77
|
}
|
|
76
78
|
return out;
|
|
77
79
|
}
|
|
@@ -111,7 +113,18 @@ async function discoverOpenAiCompat(apiKey, baseUrl, fallbackBase) {
|
|
|
111
113
|
const response = await fetch(endpoint, {
|
|
112
114
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
113
115
|
});
|
|
114
|
-
if (!response.ok)
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
// 401 specifically means our key is bad — but for codex/qwen/etc.
|
|
118
|
+
// users often log in via OAuth (`codex login`, `qwen auth`) which
|
|
119
|
+
// doesn't expose an OpenAI-compatible API key. Surface a clean
|
|
120
|
+
// "no live discovery available" rather than a scary 401 trace.
|
|
121
|
+
if (response.status === 401) {
|
|
122
|
+
const err = new Error('OpenAI-compatible /v1/models requires an API key. The static catalog is shown instead — that\'s expected when you signed in via OAuth (e.g. `codex login`).');
|
|
123
|
+
err.code = 'OAUTH_NO_API_KEY';
|
|
124
|
+
throw err;
|
|
125
|
+
}
|
|
126
|
+
throw new Error(`${endpoint} returned ${response.status}`);
|
|
127
|
+
}
|
|
115
128
|
const data = await response.json();
|
|
116
129
|
const rows = Array.isArray(data?.data) ? data.data : [];
|
|
117
130
|
return rows
|
|
@@ -123,6 +136,123 @@ async function discoverOpenAiCompat(apiKey, baseUrl, fallbackBase) {
|
|
|
123
136
|
}));
|
|
124
137
|
}
|
|
125
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Detect whether the user is authenticated via the provider's OAuth flow
|
|
141
|
+
* (codex login / qwen auth) so we can skip live model discovery silently
|
|
142
|
+
* — those flows don't surface a usable OpenAI-compatible API key, and the
|
|
143
|
+
* SDK calls the upstream APIs through its own internal auth path.
|
|
144
|
+
*/
|
|
145
|
+
async function hasProviderOauthAuth(provider) {
|
|
146
|
+
if (provider === 'codex') {
|
|
147
|
+
try {
|
|
148
|
+
await fs.access(path.join(os.homedir(), '.codex', 'auth.json'));
|
|
149
|
+
return true;
|
|
150
|
+
} catch { return false; }
|
|
151
|
+
}
|
|
152
|
+
if (provider === 'qwen') {
|
|
153
|
+
try {
|
|
154
|
+
await fs.access(path.join(os.homedir(), '.qwen', 'oauth_creds.json'));
|
|
155
|
+
return true;
|
|
156
|
+
} catch { return false; }
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* OpenCode is multi-provider — its "model" picker isn't a single API list,
|
|
163
|
+
* it's the union of every provider it can route to (Anthropic, OpenAI,
|
|
164
|
+
* Google, xAI, OpenRouter, OpenCode Zen, Ollama, etc.). The canonical
|
|
165
|
+
* catalog lives at https://models.dev/api.json (no auth, ~1.8 MB JSON, 115
|
|
166
|
+
* providers as of 2026-04). We pull that, filter to providers the user
|
|
167
|
+
* has authenticated with (read `~/.local/share/opencode/auth.json`) plus
|
|
168
|
+
* always include the OpenCode Zen tier (works without explicit auth on
|
|
169
|
+
* the free models), drop deprecated entries, and tag free models.
|
|
170
|
+
*/
|
|
171
|
+
async function discoverOpencode() {
|
|
172
|
+
const url = process.env.OPENCODE_MODELS_URL || 'https://models.dev/api.json';
|
|
173
|
+
const response = await fetch(url, {
|
|
174
|
+
// OpenCode itself caches this for hours; we cache for 6h via the
|
|
175
|
+
// outer wrapper so a single 7s fetch on cold start is acceptable.
|
|
176
|
+
signal: AbortSignal.timeout(15000),
|
|
177
|
+
});
|
|
178
|
+
if (!response.ok) throw new Error(`models.dev/api.json returned ${response.status}`);
|
|
179
|
+
const data = await response.json();
|
|
180
|
+
if (!data || typeof data !== 'object') throw new Error('models.dev returned a non-object payload');
|
|
181
|
+
|
|
182
|
+
// Read OpenCode's auth.json to know which providers the user can
|
|
183
|
+
// actually call. Missing file → only show always-free Zen.
|
|
184
|
+
const authedProviders = new Set(['opencode']);
|
|
185
|
+
try {
|
|
186
|
+
const authPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json');
|
|
187
|
+
const raw = await fs.readFile(authPath, 'utf8');
|
|
188
|
+
const auth = JSON.parse(raw);
|
|
189
|
+
if (auth && typeof auth === 'object') {
|
|
190
|
+
for (const k of Object.keys(auth)) authedProviders.add(k);
|
|
191
|
+
}
|
|
192
|
+
} catch { /* no auth.json → only Zen free models surface */ }
|
|
193
|
+
|
|
194
|
+
// Common env-var providers OpenCode picks up automatically. If the user
|
|
195
|
+
// exported one in their shell, surface those models too even without
|
|
196
|
+
// auth.json. Mirrors the env list in opencode-auth.provider.ts.
|
|
197
|
+
const envProviderHints = {
|
|
198
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
199
|
+
openai: ['OPENAI_API_KEY'],
|
|
200
|
+
google: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
|
|
201
|
+
'google-vertex': ['GOOGLE_APPLICATION_CREDENTIALS'],
|
|
202
|
+
xai: ['XAI_API_KEY'],
|
|
203
|
+
groq: ['GROQ_API_KEY'],
|
|
204
|
+
cerebras: ['CEREBRAS_API_KEY'],
|
|
205
|
+
openrouter: ['OPENROUTER_API_KEY'],
|
|
206
|
+
};
|
|
207
|
+
for (const [providerId, envVars] of Object.entries(envProviderHints)) {
|
|
208
|
+
if (envVars.some((v) => process.env[v]?.trim())) authedProviders.add(providerId);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const out = [];
|
|
212
|
+
for (const [providerId, providerCfg] of Object.entries(data)) {
|
|
213
|
+
if (!authedProviders.has(providerId)) continue;
|
|
214
|
+
if (!providerCfg || typeof providerCfg !== 'object') continue;
|
|
215
|
+
const models = providerCfg.models;
|
|
216
|
+
if (!models || typeof models !== 'object') continue;
|
|
217
|
+
|
|
218
|
+
const providerName = typeof providerCfg.name === 'string' && providerCfg.name.trim()
|
|
219
|
+
? providerCfg.name
|
|
220
|
+
: providerId;
|
|
221
|
+
|
|
222
|
+
for (const [modelId, modelCfg] of Object.entries(models)) {
|
|
223
|
+
if (!modelCfg || typeof modelCfg !== 'object') continue;
|
|
224
|
+
// Skip deprecated entries from the default list — users can
|
|
225
|
+
// still hand-type them if they really need to.
|
|
226
|
+
if (modelCfg.status === 'deprecated') continue;
|
|
227
|
+
const cost = modelCfg.cost && typeof modelCfg.cost === 'object' ? modelCfg.cost : null;
|
|
228
|
+
const free = !cost || (Number(cost.input) === 0 && Number(cost.output) === 0);
|
|
229
|
+
const ctx = modelCfg.limit?.context;
|
|
230
|
+
const ctxLabel = typeof ctx === 'number' && ctx > 0
|
|
231
|
+
? ` · ${ctx >= 1_000_000 ? `${(ctx / 1_000_000).toFixed(1)}M` : `${Math.round(ctx / 1000)}K`}`
|
|
232
|
+
: '';
|
|
233
|
+
const freeLabel = free ? ' · Free' : '';
|
|
234
|
+
const modelName = typeof modelCfg.name === 'string' && modelCfg.name.trim()
|
|
235
|
+
? modelCfg.name
|
|
236
|
+
: modelId;
|
|
237
|
+
|
|
238
|
+
out.push({
|
|
239
|
+
value: `${providerId}/${modelId}`,
|
|
240
|
+
label: `${providerName} · ${modelName}${ctxLabel}${freeLabel}`,
|
|
241
|
+
source: 'api',
|
|
242
|
+
free,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Sort: free first (handy when the user is unauthed), then by label.
|
|
248
|
+
out.sort((a, b) => {
|
|
249
|
+
if (a.free !== b.free) return a.free ? -1 : 1;
|
|
250
|
+
return a.label.localeCompare(b.label);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
126
256
|
async function discoverGoogle(apiKey) {
|
|
127
257
|
// Google Generative Language API — public models list, API key as query.
|
|
128
258
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
@@ -165,6 +295,22 @@ export async function getProviderModels(provider, opts = {}) {
|
|
|
165
295
|
};
|
|
166
296
|
}
|
|
167
297
|
|
|
298
|
+
// OpenCode is the odd one out: its catalog is models.dev, not a per-key
|
|
299
|
+
// API endpoint. Skip the credential plumbing and dispatch straight.
|
|
300
|
+
let liveModels = [];
|
|
301
|
+
let error;
|
|
302
|
+
if (provider === 'opencode') {
|
|
303
|
+
try {
|
|
304
|
+
liveModels = await discoverOpencode();
|
|
305
|
+
} catch (err) {
|
|
306
|
+
error = err?.message || String(err);
|
|
307
|
+
}
|
|
308
|
+
const merged = mergeCatalogs(normalizeList(liveModels), staticCatalog);
|
|
309
|
+
const entry = { models: merged, error };
|
|
310
|
+
await saveCacheEntry(provider, entry).catch(() => { /* non-fatal */ });
|
|
311
|
+
return { models: merged, fetchedAt: new Date().toISOString(), error, fromCache: false };
|
|
312
|
+
}
|
|
313
|
+
|
|
168
314
|
// Pick up credentials from Pixcode's UI store first, then fall back to
|
|
169
315
|
// the native env vars so a user who already exported ANTHROPIC_API_KEY
|
|
170
316
|
// (or authenticated Claude Code via OAuth — the SDK writes the key into
|
|
@@ -185,12 +331,17 @@ export async function getProviderModels(provider, opts = {}) {
|
|
|
185
331
|
const apiKey = creds?.apiKey || envKey;
|
|
186
332
|
const baseUrl = creds?.baseUrl || envBase || undefined;
|
|
187
333
|
|
|
188
|
-
let liveModels = [];
|
|
189
|
-
let error;
|
|
190
334
|
if (!apiKey) {
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
|
|
335
|
+
// Codex and Qwen support OAuth (`codex login`, `qwen auth`) which
|
|
336
|
+
// DOESN'T expose a usable API key — the SDK auths against the
|
|
337
|
+
// upstream API directly. Skip the discovery step silently in that
|
|
338
|
+
// case; the static catalog is the right answer.
|
|
339
|
+
const oauthOnly = await hasProviderOauthAuth(provider);
|
|
340
|
+
if (!oauthOnly) {
|
|
341
|
+
// Be explicit so the UI can surface a useful hint rather than just
|
|
342
|
+
// showing the static baseline with no reason given.
|
|
343
|
+
error = `No ${provider} API key configured. Save one in Settings > Agents > API Key, or sign in via the CLI (e.g. \`codex login\`).`;
|
|
344
|
+
}
|
|
194
345
|
} else {
|
|
195
346
|
try {
|
|
196
347
|
if (provider === 'claude') {
|
|
@@ -207,7 +358,12 @@ export async function getProviderModels(provider, opts = {}) {
|
|
|
207
358
|
liveModels = await discoverGoogle(apiKey);
|
|
208
359
|
}
|
|
209
360
|
} catch (err) {
|
|
210
|
-
|
|
361
|
+
// OAuth users get a clean message instead of a raw 401 stack.
|
|
362
|
+
if (err?.code === 'OAUTH_NO_API_KEY') {
|
|
363
|
+
error = err.message;
|
|
364
|
+
} else {
|
|
365
|
+
error = err?.message || String(err);
|
|
366
|
+
}
|
|
211
367
|
}
|
|
212
368
|
}
|
|
213
369
|
|
package/shared/modelConstants.js
CHANGED
|
@@ -95,25 +95,48 @@ export const QWEN_MODELS = {
|
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* OpenCode Models
|
|
98
|
+
* OpenCode Models — STATIC FALLBACK ONLY.
|
|
99
99
|
*
|
|
100
|
-
* OpenCode is multi-provider
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
100
|
+
* OpenCode is multi-provider and the live model catalog rotates often
|
|
101
|
+
* (Zen free models come and go; new Anthropic/OpenAI/Google models ship
|
|
102
|
+
* every few weeks). The runtime fetches the live catalog from
|
|
103
|
+
* `https://models.dev/api.json` via server/services/provider-models.js
|
|
104
|
+
* and merges it on top of this list — these entries only show when the
|
|
105
|
+
* fetch fails (offline install, firewalled host).
|
|
106
|
+
*
|
|
107
|
+
* Curated current free Zen tier + canonical paid picks. Update when the
|
|
108
|
+
* fallback feels stale, but the live fetch is the source of truth.
|
|
105
109
|
*/
|
|
106
110
|
export const OPENCODE_MODELS = {
|
|
107
111
|
OPTIONS: [
|
|
108
|
-
|
|
109
|
-
|
|
112
|
+
// OpenCode Zen — free tier (no charge, may rate-limit). The "limited
|
|
113
|
+
// time" Zen freebies rotate, so this is the safest small set.
|
|
114
|
+
{ value: "opencode/big-pickle", label: "OpenCode Zen · Big Pickle (Free)", free: true },
|
|
115
|
+
{ value: "opencode/minimax-m2.5-free", label: "OpenCode Zen · MiniMax M2.5 (Free)", free: true },
|
|
116
|
+
{ value: "opencode/hy3-preview-free", label: "OpenCode Zen · Hy3 Preview (Free)", free: true },
|
|
117
|
+
{ value: "opencode/ling-2.6-flash-free", label: "OpenCode Zen · Ling 2.6 Flash (Free)", free: true },
|
|
118
|
+
{ value: "opencode/nemotron-3-super-free", label: "OpenCode Zen · Nemotron 3 Super (Free)", free: true },
|
|
119
|
+
{ value: "opencode/gpt-5-nano", label: "OpenCode Zen · GPT-5 Nano (Free)", free: true },
|
|
120
|
+
|
|
121
|
+
// Anthropic — current flagship + cheap.
|
|
122
|
+
{ value: "anthropic/claude-opus-4-7", label: "Claude Opus 4.7 (Anthropic)" },
|
|
110
123
|
{ value: "anthropic/claude-sonnet-4-6", label: "Claude Sonnet 4.6 (Anthropic)" },
|
|
111
|
-
{ value: "anthropic/claude-
|
|
124
|
+
{ value: "anthropic/claude-haiku-4-5", label: "Claude Haiku 4.5 (Anthropic)" },
|
|
125
|
+
|
|
126
|
+
// OpenAI — current GPT-5 family.
|
|
112
127
|
{ value: "openai/gpt-5.4", label: "GPT-5.4 (OpenAI)" },
|
|
113
|
-
{ value: "
|
|
128
|
+
{ value: "openai/gpt-5.1-codex", label: "GPT-5.1 Codex (OpenAI)" },
|
|
129
|
+
|
|
130
|
+
// Google — current Gemini 3 family.
|
|
131
|
+
{ value: "google/gemini-3.1-pro-preview", label: "Gemini 3.1 Pro (Google)" },
|
|
132
|
+
{ value: "google/gemini-3-flash-preview", label: "Gemini 3 Flash (Google)" },
|
|
133
|
+
|
|
134
|
+
// xAI — fast & cheap coding model.
|
|
135
|
+
{ value: "xai/grok-code-fast-1", label: "Grok Code Fast 1 (xAI)" },
|
|
114
136
|
],
|
|
115
137
|
|
|
116
|
-
|
|
138
|
+
// Free Zen freebie that historically works for unauthed installs.
|
|
139
|
+
DEFAULT: "opencode/big-pickle",
|
|
117
140
|
};
|
|
118
141
|
|
|
119
142
|
/**
|