@pixelbyte-software/pixcode 1.33.4 → 1.33.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/dist/assets/{index-D0qriZVb.js → index-B2vlFsqI.js} +3 -3
- 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 +22 -6
- 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/services/provider-models.js +117 -3
- 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 +20 -6
- package/server/opencode-response-handler.js +17 -9
- package/server/services/provider-models.js +114 -3
- package/shared/modelConstants.js +34 -11
|
@@ -87,15 +87,44 @@ export class OpencodeSessionsProvider implements IProviderSessions {
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
if (raw.type === 'error') {
|
|
90
|
+
// OpenCode `--format json` emits errors as
|
|
91
|
+
// { type:"error", error:{ name, data:{ message, statusCode?, isRetryable? } } }
|
|
92
|
+
// — `error` is always an object wrapper, never a plain string. Older
|
|
93
|
+
// builds put the message at `error.message`; current builds nest it
|
|
94
|
+
// under `error.data.message`. Map known error class names to friendly
|
|
95
|
+
// copy so the user gets actionable text instead of a class identifier.
|
|
90
96
|
const rawErr = raw.error ?? raw.message;
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const errObj = rawErr && typeof rawErr === 'object' ? rawErr as Record<string, unknown> : null;
|
|
98
|
+
const data = errObj && typeof errObj.data === 'object' && errObj.data
|
|
99
|
+
? errObj.data as Record<string, unknown>
|
|
100
|
+
: null;
|
|
101
|
+
const dataMessage = data && typeof data.message === 'string' ? data.message : null;
|
|
102
|
+
const errMessage = errObj && typeof errObj.message === 'string' ? errObj.message : null;
|
|
103
|
+
const errName = errObj && typeof errObj.name === 'string' ? errObj.name : null;
|
|
104
|
+
const statusCode = data && typeof data.statusCode === 'number' ? data.statusCode : null;
|
|
105
|
+
|
|
106
|
+
let content: string;
|
|
107
|
+
if (typeof rawErr === 'string') {
|
|
108
|
+
content = rawErr;
|
|
109
|
+
} else if (dataMessage) {
|
|
110
|
+
content = dataMessage;
|
|
111
|
+
} else if (errMessage) {
|
|
112
|
+
content = errMessage;
|
|
113
|
+
} else if (errName) {
|
|
114
|
+
const friendly: Record<string, string> = {
|
|
115
|
+
ProviderModelNotFoundError: 'Model not found. Open Settings → Agents → OpenCode and pick a model from the live catalog (or run `opencode models --refresh`).',
|
|
116
|
+
ProviderInitError: 'OpenCode provider config is invalid. Try `opencode auth login` or remove `~/.local/share/opencode/auth.json` and re-authenticate.',
|
|
117
|
+
MessageOutputLengthError: 'OpenCode response was truncated by the model output cap. Try shortening the prompt or pick a model with a larger output limit.',
|
|
118
|
+
AI_APICallError: 'OpenCode upstream API call failed. Clearing `~/.cache/opencode` and retrying usually fixes this.',
|
|
119
|
+
APIError: statusCode === 429
|
|
120
|
+
? 'OpenCode hit a rate limit (429). Wait a few seconds and try again, or switch to a different model.'
|
|
121
|
+
: 'OpenCode upstream API error.',
|
|
122
|
+
};
|
|
123
|
+
content = friendly[errName] ?? errName;
|
|
124
|
+
} else {
|
|
125
|
+
try { content = JSON.stringify(rawErr); }
|
|
126
|
+
catch { content = 'Unknown OpenCode streaming error'; }
|
|
127
|
+
}
|
|
99
128
|
return [createNormalizedMessage({
|
|
100
129
|
id: baseId,
|
|
101
130
|
sessionId,
|
package/server/opencode-cli.js
CHANGED
|
@@ -275,17 +275,31 @@ async function spawnOpencode(command, options = {}, ws) {
|
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
opencodeProcess.stderr.on('data', (data) => {
|
|
278
|
-
const
|
|
278
|
+
const rawMsg = data.toString();
|
|
279
279
|
// Suppress known cosmetic noise.
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
if (rawMsg.includes('[DEP0040]') ||
|
|
281
|
+
rawMsg.includes('DeprecationWarning') ||
|
|
282
|
+
rawMsg.includes('--trace-deprecation') ||
|
|
283
|
+
rawMsg.includes('punycode')) {
|
|
284
284
|
return;
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
// Pre-stream validation errors land on stderr as plain text BEFORE
|
|
288
|
+
// the JSON event stream opens (e.g. "Model must be in the format
|
|
289
|
+
// provider/model"). Map the common ones to actionable copy.
|
|
290
|
+
let friendly = rawMsg.trim();
|
|
291
|
+
if (/Model must be in the format/i.test(friendly)) {
|
|
292
|
+
friendly = 'Model id must use the `provider/model` format (e.g. `opencode/big-pickle` or `anthropic/claude-sonnet-4-5`). Pick one from Settings → Agents → OpenCode → Model.';
|
|
293
|
+
} else if (/ProviderModelNotFoundError/.test(friendly)) {
|
|
294
|
+
friendly = 'OpenCode does not recognize the selected model. Run `opencode models --refresh` or pick a different model from the picker.';
|
|
295
|
+
} else if (/ProviderInitError/.test(friendly)) {
|
|
296
|
+
friendly = 'OpenCode provider config is invalid. Try `opencode auth login`, or clear `~/.local/share/opencode/auth.json` and re-authenticate.';
|
|
297
|
+
} else if (/AI_APICallError/.test(friendly)) {
|
|
298
|
+
friendly = 'OpenCode upstream call failed. Clearing `~/.cache/opencode` and retrying usually resolves this.';
|
|
299
|
+
}
|
|
300
|
+
|
|
287
301
|
const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
|
|
288
|
-
ws.send(createNormalizedMessage({ kind: 'error', content:
|
|
302
|
+
ws.send(createNormalizedMessage({ kind: 'error', content: friendly, sessionId: socketSessionId, provider: 'opencode' }));
|
|
289
303
|
});
|
|
290
304
|
|
|
291
305
|
opencodeProcess.on('close', async (code) => {
|
|
@@ -45,25 +45,33 @@ class OpencodeResponseHandler {
|
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// OpenCode emits
|
|
49
|
-
//
|
|
50
|
-
|
|
48
|
+
// OpenCode emits assistant text as either a top-level `text` event
|
|
49
|
+
// (current `--format json` shape: `{ type: "text", part: { text } }`)
|
|
50
|
+
// or as `message`/`part` legacy shapes from earlier builds. Cover all
|
|
51
|
+
// three so we don't drop tokens on a CLI version we haven't matched.
|
|
52
|
+
if (event.type === 'text' && event.part && typeof event.part.text === 'string') {
|
|
53
|
+
if (this.onContentFragment && event.part.text) this.onContentFragment(event.part.text);
|
|
54
|
+
} else if (event.type === 'message' && event.role === 'assistant') {
|
|
51
55
|
const content = event.content || event.text || '';
|
|
52
56
|
if (this.onContentFragment && content) this.onContentFragment(content);
|
|
53
57
|
} else if (event.type === 'part' && event.part_type === 'text') {
|
|
54
58
|
const content = event.text || event.content || '';
|
|
55
59
|
if (this.onContentFragment && content) this.onContentFragment(content);
|
|
56
60
|
} else if (event.type === 'tool_use' || event.type === 'tool-use' || event.type === 'tool.start') {
|
|
61
|
+
// Tool-use shape on `--format json`: `{ type:"tool_use", part:{ callID, tool, state:{ input } } }`
|
|
62
|
+
const part = event.part || event;
|
|
57
63
|
if (this.onToolUse) this.onToolUse({
|
|
58
|
-
tool_id:
|
|
59
|
-
tool_name:
|
|
60
|
-
parameters:
|
|
64
|
+
tool_id: part.callID || part.id || event.tool_id,
|
|
65
|
+
tool_name: part.tool || part.name || event.tool_name,
|
|
66
|
+
parameters: part.state?.input || part.input || event.parameters || {},
|
|
61
67
|
});
|
|
62
68
|
} else if (event.type === 'tool_result' || event.type === 'tool-result' || event.type === 'tool.end') {
|
|
69
|
+
const part = event.part || event;
|
|
70
|
+
const state = part.state || {};
|
|
63
71
|
if (this.onToolResult) this.onToolResult({
|
|
64
|
-
tool_id:
|
|
65
|
-
output: event.output ?? event.result ?? '',
|
|
66
|
-
status: event.status || (event.isError ? 'error' : 'ok'),
|
|
72
|
+
tool_id: part.callID || part.id || event.tool_id,
|
|
73
|
+
output: state.output ?? part.output ?? event.output ?? event.result ?? '',
|
|
74
|
+
status: state.status || event.status || (event.isError ? 'error' : 'ok'),
|
|
67
75
|
});
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -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
|
}
|
|
@@ -123,6 +125,101 @@ async function discoverOpenAiCompat(apiKey, baseUrl, fallbackBase) {
|
|
|
123
125
|
}));
|
|
124
126
|
}
|
|
125
127
|
|
|
128
|
+
/**
|
|
129
|
+
* OpenCode is multi-provider — its "model" picker isn't a single API list,
|
|
130
|
+
* it's the union of every provider it can route to (Anthropic, OpenAI,
|
|
131
|
+
* Google, xAI, OpenRouter, OpenCode Zen, Ollama, etc.). The canonical
|
|
132
|
+
* catalog lives at https://models.dev/api.json (no auth, ~1.8 MB JSON, 115
|
|
133
|
+
* providers as of 2026-04). We pull that, filter to providers the user
|
|
134
|
+
* has authenticated with (read `~/.local/share/opencode/auth.json`) plus
|
|
135
|
+
* always include the OpenCode Zen tier (works without explicit auth on
|
|
136
|
+
* the free models), drop deprecated entries, and tag free models.
|
|
137
|
+
*/
|
|
138
|
+
async function discoverOpencode() {
|
|
139
|
+
const url = process.env.OPENCODE_MODELS_URL || 'https://models.dev/api.json';
|
|
140
|
+
const response = await fetch(url, {
|
|
141
|
+
// OpenCode itself caches this for hours; we cache for 6h via the
|
|
142
|
+
// outer wrapper so a single 7s fetch on cold start is acceptable.
|
|
143
|
+
signal: AbortSignal.timeout(15000),
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) throw new Error(`models.dev/api.json returned ${response.status}`);
|
|
146
|
+
const data = await response.json();
|
|
147
|
+
if (!data || typeof data !== 'object') throw new Error('models.dev returned a non-object payload');
|
|
148
|
+
|
|
149
|
+
// Read OpenCode's auth.json to know which providers the user can
|
|
150
|
+
// actually call. Missing file → only show always-free Zen.
|
|
151
|
+
const authedProviders = new Set(['opencode']);
|
|
152
|
+
try {
|
|
153
|
+
const authPath = path.join(os.homedir(), '.local', 'share', 'opencode', 'auth.json');
|
|
154
|
+
const raw = await fs.readFile(authPath, 'utf8');
|
|
155
|
+
const auth = JSON.parse(raw);
|
|
156
|
+
if (auth && typeof auth === 'object') {
|
|
157
|
+
for (const k of Object.keys(auth)) authedProviders.add(k);
|
|
158
|
+
}
|
|
159
|
+
} catch { /* no auth.json → only Zen free models surface */ }
|
|
160
|
+
|
|
161
|
+
// Common env-var providers OpenCode picks up automatically. If the user
|
|
162
|
+
// exported one in their shell, surface those models too even without
|
|
163
|
+
// auth.json. Mirrors the env list in opencode-auth.provider.ts.
|
|
164
|
+
const envProviderHints = {
|
|
165
|
+
anthropic: ['ANTHROPIC_API_KEY'],
|
|
166
|
+
openai: ['OPENAI_API_KEY'],
|
|
167
|
+
google: ['GOOGLE_GENERATIVE_AI_API_KEY', 'GEMINI_API_KEY'],
|
|
168
|
+
'google-vertex': ['GOOGLE_APPLICATION_CREDENTIALS'],
|
|
169
|
+
xai: ['XAI_API_KEY'],
|
|
170
|
+
groq: ['GROQ_API_KEY'],
|
|
171
|
+
cerebras: ['CEREBRAS_API_KEY'],
|
|
172
|
+
openrouter: ['OPENROUTER_API_KEY'],
|
|
173
|
+
};
|
|
174
|
+
for (const [providerId, envVars] of Object.entries(envProviderHints)) {
|
|
175
|
+
if (envVars.some((v) => process.env[v]?.trim())) authedProviders.add(providerId);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const out = [];
|
|
179
|
+
for (const [providerId, providerCfg] of Object.entries(data)) {
|
|
180
|
+
if (!authedProviders.has(providerId)) continue;
|
|
181
|
+
if (!providerCfg || typeof providerCfg !== 'object') continue;
|
|
182
|
+
const models = providerCfg.models;
|
|
183
|
+
if (!models || typeof models !== 'object') continue;
|
|
184
|
+
|
|
185
|
+
const providerName = typeof providerCfg.name === 'string' && providerCfg.name.trim()
|
|
186
|
+
? providerCfg.name
|
|
187
|
+
: providerId;
|
|
188
|
+
|
|
189
|
+
for (const [modelId, modelCfg] of Object.entries(models)) {
|
|
190
|
+
if (!modelCfg || typeof modelCfg !== 'object') continue;
|
|
191
|
+
// Skip deprecated entries from the default list — users can
|
|
192
|
+
// still hand-type them if they really need to.
|
|
193
|
+
if (modelCfg.status === 'deprecated') continue;
|
|
194
|
+
const cost = modelCfg.cost && typeof modelCfg.cost === 'object' ? modelCfg.cost : null;
|
|
195
|
+
const free = !cost || (Number(cost.input) === 0 && Number(cost.output) === 0);
|
|
196
|
+
const ctx = modelCfg.limit?.context;
|
|
197
|
+
const ctxLabel = typeof ctx === 'number' && ctx > 0
|
|
198
|
+
? ` · ${ctx >= 1_000_000 ? `${(ctx / 1_000_000).toFixed(1)}M` : `${Math.round(ctx / 1000)}K`}`
|
|
199
|
+
: '';
|
|
200
|
+
const freeLabel = free ? ' · Free' : '';
|
|
201
|
+
const modelName = typeof modelCfg.name === 'string' && modelCfg.name.trim()
|
|
202
|
+
? modelCfg.name
|
|
203
|
+
: modelId;
|
|
204
|
+
|
|
205
|
+
out.push({
|
|
206
|
+
value: `${providerId}/${modelId}`,
|
|
207
|
+
label: `${providerName} · ${modelName}${ctxLabel}${freeLabel}`,
|
|
208
|
+
source: 'api',
|
|
209
|
+
free,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Sort: free first (handy when the user is unauthed), then by label.
|
|
215
|
+
out.sort((a, b) => {
|
|
216
|
+
if (a.free !== b.free) return a.free ? -1 : 1;
|
|
217
|
+
return a.label.localeCompare(b.label);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
return out;
|
|
221
|
+
}
|
|
222
|
+
|
|
126
223
|
async function discoverGoogle(apiKey) {
|
|
127
224
|
// Google Generative Language API — public models list, API key as query.
|
|
128
225
|
const endpoint = `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`;
|
|
@@ -165,6 +262,22 @@ export async function getProviderModels(provider, opts = {}) {
|
|
|
165
262
|
};
|
|
166
263
|
}
|
|
167
264
|
|
|
265
|
+
// OpenCode is the odd one out: its catalog is models.dev, not a per-key
|
|
266
|
+
// API endpoint. Skip the credential plumbing and dispatch straight.
|
|
267
|
+
let liveModels = [];
|
|
268
|
+
let error;
|
|
269
|
+
if (provider === 'opencode') {
|
|
270
|
+
try {
|
|
271
|
+
liveModels = await discoverOpencode();
|
|
272
|
+
} catch (err) {
|
|
273
|
+
error = err?.message || String(err);
|
|
274
|
+
}
|
|
275
|
+
const merged = mergeCatalogs(normalizeList(liveModels), staticCatalog);
|
|
276
|
+
const entry = { models: merged, error };
|
|
277
|
+
await saveCacheEntry(provider, entry).catch(() => { /* non-fatal */ });
|
|
278
|
+
return { models: merged, fetchedAt: new Date().toISOString(), error, fromCache: false };
|
|
279
|
+
}
|
|
280
|
+
|
|
168
281
|
// Pick up credentials from Pixcode's UI store first, then fall back to
|
|
169
282
|
// the native env vars so a user who already exported ANTHROPIC_API_KEY
|
|
170
283
|
// (or authenticated Claude Code via OAuth — the SDK writes the key into
|
|
@@ -185,8 +298,6 @@ export async function getProviderModels(provider, opts = {}) {
|
|
|
185
298
|
const apiKey = creds?.apiKey || envKey;
|
|
186
299
|
const baseUrl = creds?.baseUrl || envBase || undefined;
|
|
187
300
|
|
|
188
|
-
let liveModels = [];
|
|
189
|
-
let error;
|
|
190
301
|
if (!apiKey) {
|
|
191
302
|
// Be explicit so the UI can surface a useful hint rather than just
|
|
192
303
|
// showing the static baseline with no reason given.
|
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
|
/**
|