catence 0.2.0-beta.16 → 0.2.0-beta.18

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.
@@ -1,12 +1,11 @@
1
+ #!/usr/bin/env node
1
2
  // Discover OpenCode Go models and emit Catence Console profiles for them.
2
3
  //
3
- // OpenCode Go serves its catalog through three APIs: most models speak the
4
- // OpenAI Chat Completions API, Grok and GPT-5.6 Luna speak the OpenAI
5
- // Responses API, and MiniMax and Qwen speak the Anthropic Messages API. The
4
+ // Thin CLI wrapper around the runtime's src/core/runtime/opencode-go.ts
5
+ // module, which owns the "live discovery + trusted metadata" logic: the
6
6
  // public `GET /models` endpoint returns only model ids, not which API each
7
- // model needs, so routing comes from the trusted table below and is intersected
8
- // with the live list (the same "live discovery + trusted metadata" approach
9
- // OpenClaw uses).
7
+ // model needs, so routing comes from a trusted table intersected with the
8
+ // live list (the same approach OpenClaw uses).
10
9
  //
11
10
  // Usage:
12
11
  // node scripts/discover-opencode-go.mjs [--base-url <url>] [--write <config.json>] [--set-default]
@@ -17,62 +16,19 @@
17
16
  // --set-default With --write, set console.defaultProfile to opencode-go even when one already exists.
18
17
 
19
18
  import process from 'node:process';
20
- import { readFile, writeFile } from 'node:fs/promises';
21
19
 
22
- const DEFAULT_BASE_URL = 'https://opencode.ai/zen/go/v1';
23
- const API_KEY_ENV = 'OPENCODE_GO_API_KEY';
24
- const API_BASE_ENV = 'OPENCODE_GO_API_BASE';
25
- const MESSAGES_BASE_ENV = 'OPENCODE_GO_MESSAGES_API_BASE';
26
-
27
- // LiteLLM appends a fixed suffix to the profile base URL per provider, so the
28
- // three APIs need different prefixes and two base URLs:
29
- // openai/<id> -> {base}/chat/completions
30
- // openai/responses/<id> -> {base}/responses
31
- // anthropic/<id> -> {base minus /v1}/v1/messages
32
- const RESPONSES_MODELS = new Set(['grok-4.5', 'gpt-5.6-luna']);
33
- const MESSAGES_MODELS = new Set([
34
- 'minimax-m2.5',
35
- 'minimax-m2.7',
36
- 'minimax-m3',
37
- 'qwen3.5-plus',
38
- 'qwen3.6-plus',
39
- 'qwen3.7-plus',
40
- 'qwen3.7-max',
41
- 'qwen3.8-max',
42
- ]);
43
-
44
- const BRAND_NAMES = new Map([
45
- ['deepseek', 'DeepSeek'],
46
- ['glm', 'GLM'],
47
- ['gpt', 'GPT'],
48
- ['grok', 'Grok'],
49
- ['hy3', 'Hy3'],
50
- ['kimi', 'Kimi'],
51
- ['mimo', 'MiMo'],
52
- ['minimax', 'MiniMax'],
53
- ['qwen', 'Qwen'],
54
- ]);
55
-
56
- function classify(id) {
57
- if (RESPONSES_MODELS.has(id)) return { route: 'responses', guessed: false };
58
- if (MESSAGES_MODELS.has(id)) return { route: 'messages', guessed: false };
59
- if (/^(grok-|gpt-)/.test(id)) return { route: 'responses', guessed: true };
60
- if (/^(minimax-|qwen)/.test(id)) return { route: 'messages', guessed: true };
61
- return { route: 'chat', guessed: false };
20
+ let runtime;
21
+ try {
22
+ runtime = await import(new URL('../dist/core/runtime/opencode-go.js', import.meta.url).href);
23
+ } catch (error) {
24
+ if (error?.code !== 'ERR_MODULE_NOT_FOUND') throw error;
25
+ process.stderr.write(
26
+ 'The discovery module is not built yet. Run `npm run build` in the catence checkout first, or use an installed `catence` package.\n',
27
+ );
28
+ process.exit(1);
62
29
  }
63
30
 
64
- function modelReference(id, route) {
65
- if (route === 'responses') return `openai/responses/${id}`;
66
- if (route === 'messages') return `anthropic/${id}`;
67
- return `openai/${id}`;
68
- }
69
-
70
- function labelFor(id) {
71
- return id
72
- .split('-')
73
- .map((segment) => BRAND_NAMES.get(segment) ?? segment.charAt(0).toUpperCase() + segment.slice(1))
74
- .join(' ');
75
- }
31
+ const { OPENCODE_GO_API_BASE_ENV, OPENCODE_GO_API_KEY_ENV, OPENCODE_GO_MESSAGES_API_BASE_ENV } = runtime;
76
32
 
77
33
  function usage() {
78
34
  process.stderr.write(
@@ -82,7 +38,7 @@ function usage() {
82
38
 
83
39
  async function main() {
84
40
  const args = process.argv.slice(2);
85
- let baseUrl = DEFAULT_BASE_URL;
41
+ let baseUrl = runtime.OPENCODE_GO_DEFAULT_BASE_URL;
86
42
  let writePath = null;
87
43
  let setDefault = false;
88
44
  for (let i = 0; i < args.length; i += 1) {
@@ -108,100 +64,36 @@ async function main() {
108
64
  if (!baseUrl.endsWith('/v1')) {
109
65
  process.stderr.write(`Warning: --base-url ${baseUrl} does not end in /v1; chat and responses URLs may be wrong.\n`);
110
66
  }
111
- const modelsUrl = `${baseUrl.replace(/\/$/, '')}/models`;
112
67
  const messagesBase = baseUrl.replace(/\/v1\/?$/, '');
113
68
 
114
- let response;
115
- try {
116
- response = await fetch(modelsUrl, { headers: { accept: 'application/json' } });
117
- } catch (error) {
118
- throw new Error(`Could not reach ${modelsUrl}: ${error.message}`);
119
- }
120
- if (!response.ok) {
121
- throw new Error(`GET ${modelsUrl} returned HTTP ${response.status}.`);
122
- }
123
- const payload = await response.json();
124
- const ids = (payload.data ?? []).map((entry) => entry.id).filter((id) => typeof id === 'string' && id);
125
-
126
- const groups = { chat: [], responses: [], messages: [] };
127
- const guessed = [];
128
- for (const id of ids) {
129
- const { route, guessed: wasGuessed } = classify(id);
130
- if (!(route in groups)) continue;
131
- groups[route].push(id);
132
- if (wasGuessed) guessed.push(`${id} -> ${route}`);
133
- }
134
- for (const route of Object.keys(groups)) groups[route].sort();
135
-
136
- const chatProfileModels = {};
137
- for (const id of groups.chat) {
138
- chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'chat') };
139
- }
140
- for (const id of groups.responses) {
141
- chatProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'responses') };
142
- }
143
- const messagesProfileModels = {};
144
- for (const id of groups.messages) {
145
- messagesProfileModels[id] = { label: labelFor(id), model: modelReference(id, 'messages') };
146
- }
147
-
148
- const chatDefault = groups.chat[0] ?? groups.responses[0];
149
- const profiles = {};
150
- profiles['opencode-go'] = {
151
- label: 'OpenCode Go',
152
- ...(chatDefault ? { defaultModel: chatDefault } : {}),
153
- models: chatProfileModels,
154
- apiKeyEnv: API_KEY_ENV,
155
- apiBaseEnv: API_BASE_ENV,
156
- };
157
- if (messagesProfileModels && groups.messages.length) {
158
- profiles['opencode-go-messages'] = {
159
- label: 'OpenCode Go (Anthropic)',
160
- defaultModel: groups.messages[0],
161
- models: messagesProfileModels,
162
- apiKeyEnv: API_KEY_ENV,
163
- apiBaseEnv: MESSAGES_BASE_ENV,
164
- };
165
- }
166
-
167
- const consoleSection = { defaultProfile: 'opencode-go', profiles };
168
-
169
- if (guessed.length) {
170
- process.stderr.write('Routed by prefix heuristic (verify before use):\n');
171
- for (const line of guessed) process.stderr.write(` ${line}\n`);
172
- }
173
- process.stderr.write(
174
- `Discovered ${ids.length} models (${groups.chat.length} chat, ${groups.responses.length} responses, ${groups.messages.length} messages).\n`,
175
- );
176
-
177
69
  if (writePath) {
178
- let root = {};
179
- try {
180
- root = JSON.parse(await readFile(writePath, 'utf8'));
181
- } catch (error) {
182
- if (error.code !== 'ENOENT') throw new Error(`Could not read ${writePath}: ${error.message}`);
70
+ const result = await runtime.mergeOpenCodeGoConsoleProfiles({ configPath: writePath, baseUrl, setDefault });
71
+ if (result.guessedRoutes.length) {
72
+ process.stderr.write('Routed by prefix heuristic (verify before use):\n');
73
+ for (const line of result.guessedRoutes) process.stderr.write(` ${line}\n`);
183
74
  }
184
- if (typeof root !== 'object' || root === null || Array.isArray(root)) {
185
- throw new Error(`${writePath} must contain a JSON object.`);
186
- }
187
- const existingConsole = typeof root.console === 'object' && root.console !== null ? root.console : {};
188
- const mergedProfiles = { ...(existingConsole.profiles ?? {}), ...profiles };
189
- const defaultProfile =
190
- existingConsole.defaultProfile && !setDefault ? existingConsole.defaultProfile : 'opencode-go';
191
- root.console = { ...existingConsole, defaultProfile, profiles: mergedProfiles };
192
- await writeFile(writePath, `${JSON.stringify(root, null, 2)}\n`);
193
- process.stderr.write(`Merged profiles into ${writePath} (defaultProfile: ${defaultProfile}).\n`);
75
+ process.stderr.write(
76
+ `Merged ${result.counts.chat} chat, ${result.counts.responses} responses, and ${result.counts.messages} messages models into ${result.configPath}` +
77
+ ` as ${result.mergedProfileIds.join(' + ')} (defaultProfile: ${result.defaultProfile}).\n`,
78
+ );
194
79
  return;
195
80
  }
196
81
 
82
+ const ids = await runtime.fetchOpenCodeGoModelIds(baseUrl);
83
+ const built = runtime.buildOpenCodeGoConsoleProfiles(ids, baseUrl);
84
+ if (built.guessedRoutes.length) {
85
+ process.stderr.write('Routed by prefix heuristic (verify before use):\n');
86
+ for (const line of built.guessedRoutes) process.stderr.write(` ${line}\n`);
87
+ }
197
88
  process.stderr.write(
198
- `Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
199
- ` export ${API_KEY_ENV}='sk-…'\n` +
200
- ` export ${API_BASE_ENV}='${baseUrl}'\n` +
201
- ` export ${MESSAGES_BASE_ENV}='${messagesBase}'\n` +
89
+ `Discovered ${ids.length} models (${built.counts.chat} chat, ${built.counts.responses} responses, ${built.counts.messages} messages).\n` +
90
+ `Export the key and base URLs, then add the JSON below under "console" in config.json:\n` +
91
+ ` export ${OPENCODE_GO_API_KEY_ENV}='sk-…'\n` +
92
+ ` export ${OPENCODE_GO_API_BASE_ENV}='${baseUrl}'\n` +
93
+ ` export ${OPENCODE_GO_MESSAGES_API_BASE_ENV}='${messagesBase}'\n` +
202
94
  `(or rerun with --write ~/.catence/config.json to merge into an existing config.)\n`,
203
95
  );
204
- process.stdout.write(`${JSON.stringify(consoleSection, null, 2)}\n`);
96
+ process.stdout.write(`${JSON.stringify({ defaultProfile: built.defaultProfile, profiles: built.profiles }, null, 2)}\n`);
205
97
  }
206
98
 
207
99
  await main().catch((error) => {