@the-open-engine/zeroshot 6.7.2 → 6.8.1

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.
Files changed (38) hide show
  1. package/lib/agent-cli-provider/adapters/claude.d.ts.map +1 -1
  2. package/lib/agent-cli-provider/adapters/claude.js +1 -0
  3. package/lib/agent-cli-provider/adapters/claude.js.map +1 -1
  4. package/lib/agent-cli-provider/adapters/codex.d.ts.map +1 -1
  5. package/lib/agent-cli-provider/adapters/codex.js +3 -0
  6. package/lib/agent-cli-provider/adapters/codex.js.map +1 -1
  7. package/lib/agent-cli-provider/adapters/common.js +1 -1
  8. package/lib/agent-cli-provider/adapters/common.js.map +1 -1
  9. package/lib/agent-cli-provider/adapters/opencode-models.d.ts +7 -0
  10. package/lib/agent-cli-provider/adapters/opencode-models.d.ts.map +1 -0
  11. package/lib/agent-cli-provider/adapters/opencode-models.js +87 -0
  12. package/lib/agent-cli-provider/adapters/opencode-models.js.map +1 -0
  13. package/lib/agent-cli-provider/adapters/opencode.d.ts.map +1 -1
  14. package/lib/agent-cli-provider/adapters/opencode.js +6 -58
  15. package/lib/agent-cli-provider/adapters/opencode.js.map +1 -1
  16. package/lib/agent-cli-provider/single-agent-runtime.d.ts.map +1 -1
  17. package/lib/agent-cli-provider/single-agent-runtime.js +27 -13
  18. package/lib/agent-cli-provider/single-agent-runtime.js.map +1 -1
  19. package/lib/agent-cli-provider/types.d.ts +1 -0
  20. package/lib/agent-cli-provider/types.d.ts.map +1 -1
  21. package/lib/agent-cli-provider/types.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/assert-release-published.js +145 -35
  24. package/src/agent/agent-task-executor.js +23 -10
  25. package/src/agent-cli-provider/adapters/claude.ts +1 -0
  26. package/src/agent-cli-provider/adapters/codex.ts +3 -0
  27. package/src/agent-cli-provider/adapters/common.ts +1 -1
  28. package/src/agent-cli-provider/adapters/opencode-models.ts +100 -0
  29. package/src/agent-cli-provider/adapters/opencode.ts +8 -65
  30. package/src/agent-cli-provider/single-agent-runtime.ts +51 -33
  31. package/src/agent-cli-provider/types.ts +1 -0
  32. package/src/agent-wrapper.js +14 -2
  33. package/src/claude-task-runner.js +121 -37
  34. package/src/config-validator.js +2 -2
  35. package/src/isolation-manager.js +4 -1
  36. package/src/providers/index.js +5 -0
  37. package/src/task-run-model-args.js +155 -0
  38. package/task-lib/runner.js +66 -26
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@the-open-engine/zeroshot",
3
- "version": "6.7.2",
3
+ "version": "6.8.1",
4
4
  "description": "Multi-agent orchestration engine for Claude, Codex, and Gemini",
5
5
  "main": "src/orchestrator.js",
6
6
  "bin": {
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const https = require('https');
5
+ const os = require('os');
5
6
  const path = require('path');
6
7
  const { execFileSync } = require('child_process');
7
8
  const { releaseTypeForMessages } = require('./release-preflight');
@@ -9,8 +10,8 @@ const { releaseTypeForMessages } = require('./release-preflight');
9
10
  const DEFAULT_ATTEMPTS = 24;
10
11
  const DEFAULT_DELAY_MS = 5000;
11
12
 
12
- function run(command, args) {
13
- return execFileSync(command, args, { encoding: 'utf8' }).trim();
13
+ function run(command, args, options = {}) {
14
+ return execFileSync(command, args, { encoding: 'utf8', ...options }).trim();
14
15
  }
15
16
 
16
17
  function packageName() {
@@ -85,21 +86,50 @@ function verifyCuratedNotes(tag, release) {
85
86
  }
86
87
  }
87
88
 
88
- function verifyInstalledCli(name, version) {
89
+ function verifyInstalledCli(name, version, options = {}) {
89
90
  const packageSpec = `${name}@${version}`;
90
- const reported = run('npm', [
91
- 'exec',
92
- '--yes',
93
- `--package=${packageSpec}`,
94
- '--',
95
- 'zeroshot',
96
- '--version',
97
- ]);
98
- if (!reported.split(/\s+/).includes(version)) {
99
- throw new Error(`installed CLI reported ${reported}; expected ${version}`);
91
+ const execute = options.execute || run;
92
+ const makeTempRoot =
93
+ options.makeTempRoot ||
94
+ (() => fs.mkdtempSync(path.join(os.tmpdir(), 'zeroshot-release-cli-')));
95
+ const removeTempRoot =
96
+ options.removeTempRoot ||
97
+ ((root) => {
98
+ fs.rmSync(root, { recursive: true, force: true });
99
+ });
100
+ const platform = options.platform || process.platform;
101
+ const prefix = makeTempRoot();
102
+
103
+ try {
104
+ execute('npm', [
105
+ 'install',
106
+ '--global',
107
+ '--prefix',
108
+ prefix,
109
+ '--no-audit',
110
+ '--no-fund',
111
+ packageSpec,
112
+ ]);
113
+
114
+ const executable = path.join(
115
+ prefix,
116
+ platform === 'win32' ? 'zeroshot.cmd' : 'bin',
117
+ ...(platform === 'win32' ? [] : ['zeroshot'])
118
+ );
119
+ const isolatedEnv = {
120
+ ...process.env,
121
+ HOME: prefix,
122
+ USERPROFILE: prefix,
123
+ };
124
+ const reported = execute(executable, ['--version'], { env: isolatedEnv });
125
+ if (!reported.split(/\s+/).includes(version)) {
126
+ throw new Error(`installed CLI reported ${reported}; expected ${version}`);
127
+ }
128
+ execute(executable, ['--help'], { env: isolatedEnv });
129
+ execute(executable, ['list'], { env: isolatedEnv });
130
+ } finally {
131
+ removeTempRoot(prefix);
100
132
  }
101
- run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', '--help']);
102
- run('npm', ['exec', '--yes', `--package=${packageSpec}`, '--', 'zeroshot', 'list']);
103
133
  }
104
134
 
105
135
  function tagsPointingAtHead() {
@@ -149,28 +179,74 @@ function sleep(ms) {
149
179
  });
150
180
  }
151
181
 
182
+ function nextRetryDelay(attempt, attempts, delayMs, options) {
183
+ if (attempt >= attempts) return null;
184
+ if (options.deadline === undefined) return delayMs;
185
+
186
+ const now = options.now || Date.now;
187
+ const remainingMs = options.deadline - now();
188
+ if (remainingMs <= 0) return null;
189
+ return Math.min(delayMs, remainingMs);
190
+ }
191
+
152
192
  async function waitForNpmLatest(name, expectedVersion, options = {}) {
153
193
  const attempts =
154
194
  options.attempts || Number(process.env.RELEASE_ASSERT_ATTEMPTS || DEFAULT_ATTEMPTS);
155
195
  const delayMs =
156
196
  options.delayMs || Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
197
+ const wait = options.sleep || sleep;
157
198
 
158
199
  let latest = null;
159
200
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
160
201
  latest = npmLatest(name);
161
202
  if (latest === expectedVersion) return latest;
162
203
 
163
- if (attempt < attempts) {
204
+ const retryDelay = nextRetryDelay(attempt, attempts, delayMs, options);
205
+ if (retryDelay !== null) {
164
206
  console.log(
165
207
  `npm latest for ${name} is ${latest}; waiting for ${expectedVersion} (${attempt}/${attempts})`
166
208
  );
167
- await sleep(delayMs);
209
+ await wait(retryDelay);
210
+ } else {
211
+ break;
168
212
  }
169
213
  }
170
214
 
171
215
  throw new Error(`expected npm latest for ${name} to be ${expectedVersion}, got ${latest}`);
172
216
  }
173
217
 
218
+ async function waitForPublishedArtifact(label, check, options = {}) {
219
+ const attempts =
220
+ options.attempts || Number(process.env.RELEASE_ASSERT_ATTEMPTS || DEFAULT_ATTEMPTS);
221
+ const delayMs =
222
+ options.delayMs || Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
223
+ const wait = options.sleep || sleep;
224
+ let lastError = null;
225
+ let attemptsMade = 0;
226
+
227
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
228
+ attemptsMade = attempt;
229
+ try {
230
+ return await check();
231
+ } catch (error) {
232
+ lastError = error;
233
+ const retryDelay = nextRetryDelay(attempt, attempts, delayMs, options);
234
+ if (retryDelay !== null) {
235
+ console.log(
236
+ `${label} is not ready: ${error.message}; retrying (${attempt}/${attempts})`
237
+ );
238
+ await wait(retryDelay);
239
+ } else {
240
+ break;
241
+ }
242
+ }
243
+ }
244
+
245
+ throw new Error(
246
+ `${label} did not become ready after ${attemptsMade} attempts: ${lastError?.message || 'unknown error'}`
247
+ );
248
+ }
249
+
174
250
  async function main() {
175
251
  const name = packageName();
176
252
  const headTags = tagsPointingAtHead();
@@ -188,30 +264,62 @@ async function main() {
188
264
 
189
265
  console.log(`tags on HEAD: ${headTags.join(', ') || '(none)'}`);
190
266
  const expectedVersion = expectedTag.slice(1);
191
- const latest = await waitForNpmLatest(name, expectedVersion);
267
+ const retryAttempts = Number(process.env.RELEASE_ASSERT_ATTEMPTS || DEFAULT_ATTEMPTS);
268
+ const retryDelayMs = Number(process.env.RELEASE_ASSERT_DELAY_MS || DEFAULT_DELAY_MS);
269
+ const retryOptions = {
270
+ attempts: retryAttempts,
271
+ delayMs: retryDelayMs,
272
+ deadline: Date.now() + retryAttempts * retryDelayMs,
273
+ };
274
+ const latest = await waitForNpmLatest(name, expectedVersion, retryOptions);
192
275
 
193
276
  console.log(`npm latest for ${name}: ${latest}`);
194
277
 
195
278
  const expectedCommit = run('git', ['rev-parse', 'HEAD']);
196
- const metadata = npmReleaseMetadata(name, expectedVersion);
197
- if (metadata.version !== expectedVersion) {
198
- throw new Error(`npm metadata returned ${metadata.version}; expected ${expectedVersion}`);
199
- }
200
- if (metadata.gitHead !== expectedCommit) {
201
- throw new Error(`npm gitHead ${metadata.gitHead || '(missing)'} does not match HEAD`);
202
- }
279
+ const metadata = await waitForPublishedArtifact(
280
+ 'npm release metadata',
281
+ () => {
282
+ const result = npmReleaseMetadata(name, expectedVersion);
283
+ if (result.version !== expectedVersion) {
284
+ throw new Error(`npm metadata returned ${result.version}; expected ${expectedVersion}`);
285
+ }
286
+ if (result.gitHead !== expectedCommit) {
287
+ throw new Error(`npm gitHead ${result.gitHead || '(missing)'} does not match HEAD`);
288
+ }
289
+ if (!result['dist.attestations']?.url) {
290
+ throw new Error('npm attestation URL is missing');
291
+ }
292
+ return result;
293
+ },
294
+ retryOptions
295
+ );
203
296
 
204
- const attestationUrl = metadata['dist.attestations']?.url;
205
- if (!attestationUrl) throw new Error('npm attestation URL is missing');
206
- const attestations = await httpsJson(attestationUrl);
207
- verifyProvenance(provenanceStatement(attestations), expectedCommit);
297
+ await waitForPublishedArtifact(
298
+ 'npm provenance',
299
+ async () => {
300
+ const attestations = await httpsJson(metadata['dist.attestations'].url);
301
+ verifyProvenance(provenanceStatement(attestations), expectedCommit);
302
+ },
303
+ retryOptions
304
+ );
208
305
 
209
- const release = githubRelease(expectedTag);
210
- if (release.tagName !== expectedTag) {
211
- throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
212
- }
213
- verifyCuratedNotes(expectedTag, release);
214
- verifyInstalledCli(name, expectedVersion);
306
+ await waitForPublishedArtifact(
307
+ 'GitHub Release',
308
+ () => {
309
+ const release = githubRelease(expectedTag);
310
+ if (release.tagName !== expectedTag) {
311
+ throw new Error(`GitHub Release tag ${release.tagName} does not match ${expectedTag}`);
312
+ }
313
+ verifyCuratedNotes(expectedTag, release);
314
+ },
315
+ retryOptions
316
+ );
317
+
318
+ await waitForPublishedArtifact(
319
+ 'installed CLI',
320
+ () => verifyInstalledCli(name, expectedVersion),
321
+ retryOptions
322
+ );
215
323
 
216
324
  console.log(`Release publication verified: ${name}@${latest}`);
217
325
  }
@@ -230,6 +338,8 @@ module.exports = {
230
338
  npmLatest,
231
339
  provenanceStatement,
232
340
  tagsPointingAtHead,
341
+ verifyInstalledCli,
233
342
  verifyProvenance,
234
343
  waitForNpmLatest,
344
+ waitForPublishedArtifact,
235
345
  };
@@ -23,6 +23,10 @@ const {
23
23
  prepareClaudeConfigDir,
24
24
  resolveRepoMcpConfigPath,
25
25
  } = require('../worktree-claude-config.js');
26
+ const {
27
+ appendTaskRunModelArgs,
28
+ wrapTaskRunWithIsolatedSettings,
29
+ } = require('../task-run-model-args.js');
26
30
  const { buildRawLogOnlyMetadata } = require('./context-replay-policy');
27
31
 
28
32
  function runCommandWithTimeout(command, args, options = {}, callback = null) {
@@ -722,14 +726,10 @@ function resolveOutputFormatConfig(agent) {
722
726
 
723
727
  function buildTaskRunArgs({ agent, providerName, modelSpec, runOutputFormat }) {
724
728
  const args = ['task', 'run', '--output-format', runOutputFormat, '--provider', providerName];
725
-
726
- if (modelSpec?.model) {
727
- args.push('--model', modelSpec.model);
728
- }
729
-
730
- if (modelSpec?.reasoningEffort) {
731
- args.push('--reasoning-effort', modelSpec.reasoningEffort);
732
- }
729
+ const modelSpecSource = agent._resolveModelSpecSource
730
+ ? agent._resolveModelSpecSource()
731
+ : 'direct';
732
+ appendTaskRunModelArgs(args, modelSpec, modelSpecSource);
733
733
 
734
734
  // Add verification mode flag if configured
735
735
  if (agent.config.verificationMode) {
@@ -875,8 +875,11 @@ function spawnTaskProcess({ agent, ctPath, args, cwd, spawnEnv }) {
875
875
  // Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it
876
876
  const SPAWN_TIMEOUT_MS = 30000; // 30 seconds to spawn task
877
877
 
878
+ // spawn() throws on null bytes in argv; strip them before they get there.
879
+ const safeArgs = args.map((arg) => (typeof arg === 'string' ? arg.replace(/\0/g, '') : arg));
880
+
878
881
  return new Promise((resolve, reject) => {
879
- const proc = spawn(ctPath, args, {
882
+ const proc = spawn(ctPath, safeArgs, {
880
883
  cwd,
881
884
  stdio: ['ignore', 'pipe', 'pipe'],
882
885
  env: spawnEnv,
@@ -1509,11 +1512,14 @@ async function spawnClaudeTaskIsolated(agent, context) {
1509
1512
  const { manager, clusterId } = agent.isolation;
1510
1513
  const providerName = agent._resolveProvider ? agent._resolveProvider() : 'claude';
1511
1514
  const modelSpec = resolveAgentModelSpec(agent);
1515
+ const modelSpecSource = agent._resolveModelSpecSource
1516
+ ? agent._resolveModelSpecSource()
1517
+ : 'direct';
1512
1518
 
1513
1519
  agent._log(`📦 Agent ${agent.id}: Running task in isolated container using zeroshot task run...`);
1514
1520
 
1515
1521
  const { desiredOutputFormat, runOutputFormat } = resolveOutputFormatConfig(agent);
1516
- const command = [
1522
+ let command = [
1517
1523
  'zeroshot',
1518
1524
  ...buildTaskRunArgs({
1519
1525
  agent,
@@ -1531,6 +1537,12 @@ async function spawnClaudeTaskIsolated(agent, context) {
1531
1537
  });
1532
1538
 
1533
1539
  command.push(finalContext);
1540
+ command = wrapTaskRunWithIsolatedSettings(command, {
1541
+ providerName,
1542
+ settings: loadSettings(),
1543
+ modelSpecSource,
1544
+ modelSpec,
1545
+ });
1534
1546
 
1535
1547
  // STEP 1: Spawn task and extract task ID (same as non-isolated mode)
1536
1548
  // Timeout for spawn phase - if CLI hangs during init (e.g., opencode 429 bug), kill it
@@ -2357,6 +2369,7 @@ async function killIsolatedTask(agent, currentTask, taskId, reason, code) {
2357
2369
  module.exports = {
2358
2370
  ensureAskUserQuestionHook,
2359
2371
  spawnClaudeTask,
2372
+ spawnTaskProcess,
2360
2373
  followClaudeTaskLogs,
2361
2374
  followClaudeTaskLogsIsolated,
2362
2375
  waitForTaskReady,
@@ -40,6 +40,7 @@ const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
40
40
  'claude-opus-4-6': { rank: 3 },
41
41
  'claude-opus-4-7': { rank: 3 },
42
42
  'claude-opus-4-8': { rank: 3 },
43
+ 'claude-opus-5': { rank: 3 },
43
44
  fable: { rank: 3 },
44
45
  'claude-fable-5': { rank: 3 },
45
46
  'claude-mythos-5': { rank: 3 },
@@ -29,8 +29,11 @@ const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
29
29
  'gpt-5.5': { rank: 3 },
30
30
  'gpt-5.6': { rank: 3 },
31
31
  'gpt-5.6-sol': { rank: 3 },
32
+ 'openai.gpt-5.6-sol': { rank: 3 },
32
33
  'gpt-5.6-terra': { rank: 2 },
34
+ 'openai.gpt-5.6-terra': { rank: 2 },
33
35
  'gpt-5.6-luna': { rank: 1 },
36
+ 'openai.gpt-5.6-luna': { rank: 1 },
34
37
  };
35
38
 
36
39
  const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
@@ -99,7 +99,7 @@ export function validateModelIdFromCatalog(
99
99
  modelId: string | null | undefined
100
100
  ): string | null | undefined {
101
101
  if (!modelId) return modelId;
102
- if (catalog[modelId] !== undefined) return modelId;
102
+ if (Object.prototype.hasOwnProperty.call(catalog, modelId)) return modelId;
103
103
  const validModels = Object.keys(catalog).join(', ');
104
104
  throw new InvalidProviderModelError(
105
105
  `Invalid model "${modelId}" for provider "${provider}". Valid models: ${validModels}.`
@@ -0,0 +1,100 @@
1
+ import { unknownToMessage } from '../json';
2
+ import {
3
+ InvalidProviderModelError,
4
+ type LevelModelSpec,
5
+ type LevelOverrides,
6
+ type ModelCatalogEntry,
7
+ type ModelLevel,
8
+ type ResolvedModelSpec,
9
+ } from '../types';
10
+ import { validateModelIdFromCatalog } from './common';
11
+
12
+ export const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
13
+ 'opencode/big-pickle': { rank: 1 },
14
+ 'opencode/glm-4.7-free': { rank: 1 },
15
+ 'opencode/gpt-5-nano': { rank: 1 },
16
+ 'opencode/grok-code': { rank: 1 },
17
+ 'opencode/minimax-m2.1-free': { rank: 1 },
18
+ 'google/gemini-1.5-flash': { rank: 1 },
19
+ 'google/gemini-1.5-flash-8b': { rank: 1 },
20
+ 'google/gemini-1.5-pro': { rank: 1 },
21
+ 'google/gemini-2.0-flash': { rank: 1 },
22
+ 'google/gemini-2.0-flash-lite': { rank: 1 },
23
+ 'google/gemini-2.5-flash': { rank: 1 },
24
+ 'google/gemini-2.5-flash-image': { rank: 1 },
25
+ 'google/gemini-2.5-flash-image-preview': { rank: 1 },
26
+ 'google/gemini-2.5-flash-lite': { rank: 1 },
27
+ 'google/gemini-2.5-flash-lite-preview-06-17': { rank: 1 },
28
+ 'google/gemini-2.5-flash-lite-preview-09-2025': { rank: 1 },
29
+ 'google/gemini-2.5-flash-preview-04-17': { rank: 1 },
30
+ 'google/gemini-2.5-flash-preview-05-20': { rank: 1 },
31
+ 'google/gemini-2.5-flash-preview-09-2025': { rank: 1 },
32
+ 'google/gemini-2.5-flash-preview-tts': { rank: 1 },
33
+ 'google/gemini-2.5-pro': { rank: 1 },
34
+ 'google/gemini-2.5-pro-preview-05-06': { rank: 1 },
35
+ 'google/gemini-2.5-pro-preview-06-05': { rank: 1 },
36
+ 'google/gemini-2.5-pro-preview-tts': { rank: 1 },
37
+ 'google/gemini-3-flash-preview': { rank: 1 },
38
+ 'google/gemini-3-pro-preview': { rank: 1 },
39
+ 'google/gemini-embedding-001': { rank: 1 },
40
+ 'google/gemini-flash-latest': { rank: 1 },
41
+ 'google/gemini-flash-lite-latest': { rank: 1 },
42
+ 'google/gemini-live-2.5-flash': { rank: 1 },
43
+ 'google/gemini-live-2.5-flash-preview-native-audio': { rank: 1 },
44
+ 'openai/gpt-5.1-codex-max': { rank: 1 },
45
+ 'openai/gpt-5.1-codex-mini': { rank: 1 },
46
+ 'openai/gpt-5.2': { rank: 1 },
47
+ 'openai/gpt-5.2-codex': { rank: 1 },
48
+ };
49
+
50
+ export const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
51
+ level1: { rank: 1, model: null, reasoningEffort: 'low' },
52
+ level2: { rank: 2, model: null, reasoningEffort: 'medium' },
53
+ level3: { rank: 3, model: null, reasoningEffort: 'high' },
54
+ };
55
+
56
+ function containsControlCharacter(value: string): boolean {
57
+ return Array.from(value).some((character) => {
58
+ const codePoint = character.codePointAt(0);
59
+ return codePoint !== undefined && (codePoint <= 0x1f || codePoint === 0x7f);
60
+ });
61
+ }
62
+
63
+ export function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
64
+ const base = LEVEL_MAPPING[level] ?? LEVEL_MAPPING.level2;
65
+ const override = overrides?.[level];
66
+ const selectedModel = override?.model ?? base.model;
67
+ return {
68
+ level,
69
+ model:
70
+ override?.model === undefined || override.model === null
71
+ ? (validateModelId(selectedModel) ?? null)
72
+ : (validateConfiguredModelId(selectedModel) ?? null),
73
+ reasoningEffort: override?.reasoningEffort ?? base.reasoningEffort,
74
+ };
75
+ }
76
+
77
+ export function validateConfiguredModelId(
78
+ modelId: string | null | undefined
79
+ ): string | null | undefined {
80
+ if (modelId && Object.prototype.hasOwnProperty.call(MODEL_CATALOG, modelId)) return modelId;
81
+ if (typeof modelId !== 'string') return validateModelId(modelId);
82
+ const segments = modelId.split('/');
83
+ if (
84
+ segments.length >= 2 &&
85
+ segments.every(Boolean) &&
86
+ !/\s/u.test(modelId) &&
87
+ !containsControlCharacter(modelId)
88
+ ) {
89
+ return modelId;
90
+ }
91
+ throw new InvalidProviderModelError(
92
+ `Invalid configured model "${unknownToMessage(
93
+ modelId
94
+ )}" for provider "opencode". Expected "provider/model" with no whitespace, control characters, or empty path segments.`
95
+ );
96
+ }
97
+
98
+ export function validateModelId(modelId: string | null | undefined): string | null | undefined {
99
+ return validateModelIdFromCatalog('opencode', MODEL_CATALOG, modelId);
100
+ }
@@ -11,14 +11,9 @@ import {
11
11
  type BuildProviderCommandOptions,
12
12
  type CommandSpec,
13
13
  type ErrorClassification,
14
- type LevelModelSpec,
15
- type LevelOverrides,
16
- type ModelCatalogEntry,
17
- type ModelLevel,
18
14
  type OpencodeCliFeatures,
19
15
  type OutputEvent,
20
16
  type ProviderAdapter,
21
- type ResolvedModelSpec,
22
17
  type WarningMetadata,
23
18
  } from '../types';
24
19
  import {
@@ -26,55 +21,16 @@ import {
26
21
  commandSpec,
27
22
  createParserState,
28
23
  optionFeatures,
29
- resolveModelSpecWithConfig,
30
24
  unsupportedSessionControlWarnings,
31
- validateModelIdFromCatalog,
32
25
  warning,
33
26
  } from './common';
34
-
35
- const MODEL_CATALOG: Readonly<Record<string, ModelCatalogEntry>> = {
36
- 'opencode/big-pickle': { rank: 1 },
37
- 'opencode/glm-4.7-free': { rank: 1 },
38
- 'opencode/gpt-5-nano': { rank: 1 },
39
- 'opencode/grok-code': { rank: 1 },
40
- 'opencode/minimax-m2.1-free': { rank: 1 },
41
- 'google/gemini-1.5-flash': { rank: 1 },
42
- 'google/gemini-1.5-flash-8b': { rank: 1 },
43
- 'google/gemini-1.5-pro': { rank: 1 },
44
- 'google/gemini-2.0-flash': { rank: 1 },
45
- 'google/gemini-2.0-flash-lite': { rank: 1 },
46
- 'google/gemini-2.5-flash': { rank: 1 },
47
- 'google/gemini-2.5-flash-image': { rank: 1 },
48
- 'google/gemini-2.5-flash-image-preview': { rank: 1 },
49
- 'google/gemini-2.5-flash-lite': { rank: 1 },
50
- 'google/gemini-2.5-flash-lite-preview-06-17': { rank: 1 },
51
- 'google/gemini-2.5-flash-lite-preview-09-2025': { rank: 1 },
52
- 'google/gemini-2.5-flash-preview-04-17': { rank: 1 },
53
- 'google/gemini-2.5-flash-preview-05-20': { rank: 1 },
54
- 'google/gemini-2.5-flash-preview-09-2025': { rank: 1 },
55
- 'google/gemini-2.5-flash-preview-tts': { rank: 1 },
56
- 'google/gemini-2.5-pro': { rank: 1 },
57
- 'google/gemini-2.5-pro-preview-05-06': { rank: 1 },
58
- 'google/gemini-2.5-pro-preview-06-05': { rank: 1 },
59
- 'google/gemini-2.5-pro-preview-tts': { rank: 1 },
60
- 'google/gemini-3-flash-preview': { rank: 1 },
61
- 'google/gemini-3-pro-preview': { rank: 1 },
62
- 'google/gemini-embedding-001': { rank: 1 },
63
- 'google/gemini-flash-latest': { rank: 1 },
64
- 'google/gemini-flash-lite-latest': { rank: 1 },
65
- 'google/gemini-live-2.5-flash': { rank: 1 },
66
- 'google/gemini-live-2.5-flash-preview-native-audio': { rank: 1 },
67
- 'openai/gpt-5.1-codex-max': { rank: 1 },
68
- 'openai/gpt-5.1-codex-mini': { rank: 1 },
69
- 'openai/gpt-5.2': { rank: 1 },
70
- 'openai/gpt-5.2-codex': { rank: 1 },
71
- };
72
-
73
- const LEVEL_MAPPING: Readonly<Record<ModelLevel, LevelModelSpec>> = {
74
- level1: { rank: 1, model: null, reasoningEffort: 'low' },
75
- level2: { rank: 2, model: null, reasoningEffort: 'medium' },
76
- level3: { rank: 3, model: null, reasoningEffort: 'high' },
77
- };
27
+ import {
28
+ LEVEL_MAPPING,
29
+ MODEL_CATALOG,
30
+ resolveModelSpec,
31
+ validateConfiguredModelId,
32
+ validateModelId,
33
+ } from './opencode-models';
78
34
 
79
35
  function detectCliFeatures(helpText?: string | null): OpencodeCliFeatures {
80
36
  const help = helpText ?? '';
@@ -244,20 +200,6 @@ function parseEvent(line: string): OutputEvent | null {
244
200
  return null;
245
201
  }
246
202
 
247
- function resolveModelSpec(level: ModelLevel, overrides?: LevelOverrides): ResolvedModelSpec {
248
- return resolveModelSpecWithConfig({
249
- mapping: LEVEL_MAPPING,
250
- defaultLevel: 'level2',
251
- level,
252
- overrides,
253
- validateModelId,
254
- });
255
- }
256
-
257
- function validateModelId(modelId: string | null | undefined): string | null | undefined {
258
- return validateModelIdFromCatalog('opencode', MODEL_CATALOG, modelId);
259
- }
260
-
261
203
  function classifyError(error: unknown): ErrorClassification {
262
204
  return classifyBaseProviderError(error, [], []);
263
205
  }
@@ -285,5 +227,6 @@ export const opencodeAdapter: ProviderAdapter = {
285
227
  createParserState: () => createParserState('opencode'),
286
228
  resolveModelSpec,
287
229
  validateModelId,
230
+ validateConfiguredModelId,
288
231
  classifyError,
289
232
  };