subconscious-cli 7.0.0 → 8.0.0
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/bin/agent-spawn.js +2 -2
- package/bin/agents.js +25 -3
- package/bin/cli.js +1 -0
- package/bin/model-capabilities.js +45 -5
- package/bin/models.js +123 -35
- package/bin/native/subc-tui-darwin-amd64 +0 -0
- package/bin/native/subc-tui-darwin-arm64 +0 -0
- package/bin/native/subc-tui-linux-amd64 +0 -0
- package/bin/native/subc-tui-linux-arm64 +0 -0
- package/bin/native/subc-tui-windows-amd64.exe +0 -0
- package/bin/native/subc-tui-windows-arm64.exe +0 -0
- package/bin/opencode-provider.js +17 -5
- package/bin/profiles.js +39 -1
- package/bin/tui.js +1 -1
- package/bin/usage.js +66 -59
- package/package.json +1 -1
package/bin/agent-spawn.js
CHANGED
|
@@ -3,7 +3,7 @@ import os from 'node:os';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { RUNBOOK_DIR } from './agent-data.js';
|
|
5
5
|
import { modelIds } from './launch-values.js';
|
|
6
|
-
import {
|
|
6
|
+
import { visionModelListFromMetadata } from './model-capabilities.js';
|
|
7
7
|
|
|
8
8
|
/** Resolve a script inside the agent's own runbook directory. */
|
|
9
9
|
export function runbookScriptPath(agent, relativeScript) {
|
|
@@ -95,7 +95,7 @@ export function scriptEnv(env, { strict = true } = {}) {
|
|
|
95
95
|
return {
|
|
96
96
|
...env,
|
|
97
97
|
...(ids === undefined ? {} : { SUBC_MODEL_IDS: ids }),
|
|
98
|
-
SUBC_VISION_MODELS:
|
|
98
|
+
SUBC_VISION_MODELS: visionModelListFromMetadata(env.SUBC_MODEL_METADATA),
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
package/bin/agents.js
CHANGED
|
@@ -327,6 +327,7 @@ export function runbookEnv(
|
|
|
327
327
|
profile,
|
|
328
328
|
agent,
|
|
329
329
|
models = PACKAGED_MODELS,
|
|
330
|
+
entries = [],
|
|
330
331
|
) {
|
|
331
332
|
const ctx = buildContext(apiKey, model, profile);
|
|
332
333
|
const extraDirs = [binDir, ...candidateBinDirs()].filter(Boolean);
|
|
@@ -343,6 +344,7 @@ export function runbookEnv(
|
|
|
343
344
|
...(keyName ? { [keyName]: apiKey } : {}),
|
|
344
345
|
MODEL: model,
|
|
345
346
|
SUBCONSCIOUS_MODELS: models.join('\n'),
|
|
347
|
+
SUBC_MODEL_METADATA: JSON.stringify(entries),
|
|
346
348
|
...(claude
|
|
347
349
|
? {
|
|
348
350
|
SUBC_CLAUDE_SETTINGS: JSON.stringify(
|
|
@@ -378,7 +380,11 @@ async function resolvedModelsForLaunch(profile, apiKey, selectedModel) {
|
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
export function selectLaunchModel(requestedModel, modelSource, catalog) {
|
|
381
|
-
const first =
|
|
383
|
+
const first =
|
|
384
|
+
catalog.defaultModels?.[0] ||
|
|
385
|
+
catalog.models[0] ||
|
|
386
|
+
requestedModel ||
|
|
387
|
+
DEFAULTS.model;
|
|
382
388
|
if (modelSource === 'catalog' || !requestedModel) {
|
|
383
389
|
return first;
|
|
384
390
|
}
|
|
@@ -427,7 +433,15 @@ async function runRunbookSetup(agent, argv, profile) {
|
|
|
427
433
|
// Setup scripts read no strict-choice setting; only the model list matters.
|
|
428
434
|
const env = resolveInputs(
|
|
429
435
|
agent,
|
|
430
|
-
runbookEnv(
|
|
436
|
+
runbookEnv(
|
|
437
|
+
apiKey,
|
|
438
|
+
model,
|
|
439
|
+
undefined,
|
|
440
|
+
profile,
|
|
441
|
+
agent,
|
|
442
|
+
catalog.defaultModels,
|
|
443
|
+
catalog.entries,
|
|
444
|
+
),
|
|
431
445
|
{},
|
|
432
446
|
{ strict: false },
|
|
433
447
|
);
|
|
@@ -542,7 +556,15 @@ async function launchAgent(agent, argv, profile, headless) {
|
|
|
542
556
|
(headless ? console.error : console.log)(
|
|
543
557
|
` ${c.dim}Launching ${c.reset}${c.bold}${agent.name}${c.reset} ${c.dim}on Subconscious ${c.reset}${c.dim}(${model})${c.reset}\n`,
|
|
544
558
|
);
|
|
545
|
-
const env = runbookEnv(
|
|
559
|
+
const env = runbookEnv(
|
|
560
|
+
apiKey,
|
|
561
|
+
model,
|
|
562
|
+
binDir,
|
|
563
|
+
profile,
|
|
564
|
+
agent,
|
|
565
|
+
catalog.defaultModels,
|
|
566
|
+
catalog.entries,
|
|
567
|
+
);
|
|
546
568
|
// The headless endpoint is the one endpoint for every harness, Claude too.
|
|
547
569
|
const launchEnv = headless ? { ...env, CLAUDE_GATEWAY_URL: '' } : env;
|
|
548
570
|
const plan = launchPlan(agent, { env: launchEnv, ...headlessParts(rest) });
|
package/bin/cli.js
CHANGED
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { MODEL_CAPABILITIES } from './agent-data.js';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
3
|
+
// Capabilities are listed by exact model ID: similarly named models inherit
|
|
4
|
+
// nothing. The canonical catalog's per-model modality metadata is preferred
|
|
5
|
+
// when a launch carries it; the packaged map is the offline fallback.
|
|
6
|
+
export function modelSupportsVision(modelId, entries = []) {
|
|
7
|
+
const entry = entries.find((candidate) => candidate?.id === modelId);
|
|
8
|
+
if (entry?.modalities) {
|
|
9
|
+
const input = Array.isArray(entry.modalities.input)
|
|
10
|
+
? entry.modalities.input
|
|
11
|
+
: [];
|
|
12
|
+
return input.includes('image');
|
|
13
|
+
}
|
|
6
14
|
return (
|
|
7
15
|
Object.hasOwn(MODEL_CAPABILITIES, modelId) &&
|
|
8
16
|
MODEL_CAPABILITIES[modelId].vision === true
|
|
@@ -10,6 +18,38 @@ export function modelSupportsVision(modelId) {
|
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
/** Vision model IDs, newline-separated, for the runbooks' shell lookup. */
|
|
13
|
-
export function visionModelList() {
|
|
14
|
-
|
|
21
|
+
export function visionModelList(entries = []) {
|
|
22
|
+
const ids = entries.length
|
|
23
|
+
? entries
|
|
24
|
+
.map((entry) => entry.id)
|
|
25
|
+
.filter((id) => modelSupportsVision(id, entries))
|
|
26
|
+
: Object.keys(MODEL_CAPABILITIES).filter((id) =>
|
|
27
|
+
modelSupportsVision(id, entries),
|
|
28
|
+
);
|
|
29
|
+
return ids.join('\n');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Canonical catalog entries from the launch env's SUBC_MODEL_METADATA (a
|
|
34
|
+
* JSON array), or an empty list when discovery did not run, was skipped,
|
|
35
|
+
* or wrote malformed JSON.
|
|
36
|
+
*/
|
|
37
|
+
export function catalogEntriesFromMetadata(metadataJson) {
|
|
38
|
+
if (typeof metadataJson !== 'string' || !metadataJson.trim()) return [];
|
|
39
|
+
try {
|
|
40
|
+
const entries = JSON.parse(metadataJson);
|
|
41
|
+
return Array.isArray(entries) ? entries : [];
|
|
42
|
+
} catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The vision list for a runbook script, from the launch env's catalog
|
|
49
|
+
* metadata with the packaged map as the fallback when discovery did not
|
|
50
|
+
* run or failed.
|
|
51
|
+
*/
|
|
52
|
+
export function visionModelListFromMetadata(metadataJson) {
|
|
53
|
+
const entries = catalogEntriesFromMetadata(metadataJson);
|
|
54
|
+
return entries.length ? visionModelList(entries) : visionModelList();
|
|
15
55
|
}
|
package/bin/models.js
CHANGED
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Resolve the model catalog exposed by an OpenAI-compatible gateway.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A key-scoped GET /v1/models is the canonical source: the gateway filters
|
|
5
|
+
* it by the key's model access and describes each model (lifecycle, limits,
|
|
6
|
+
* modalities, capabilities). Older gateways without the canonical catalog
|
|
7
|
+
* answer with a minimal list, which still names the fleet. Launches keep
|
|
8
|
+
* working with the packaged registry when the gateway is offline or returns
|
|
9
|
+
* nothing usable.
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
import { proxyAwareFetch } from './network.js';
|
|
11
13
|
|
|
12
14
|
const MODEL_ID_PATTERN = /^[-A-Za-z0-9._:/+]+$/;
|
|
13
15
|
export const DEFAULT_MODEL_FETCH_TIMEOUT_MS = 3000;
|
|
14
|
-
export const AVAILABLE_MODELS_PATH = '/v1/models/available';
|
|
15
16
|
export const PUBLIC_MODELS_PATH = '/v1/models';
|
|
16
17
|
export const PUBLIC_CATALOG_FALLBACK_MESSAGE =
|
|
17
18
|
'Provisioned catalog unavailable; showing the public model list.';
|
|
18
19
|
|
|
19
20
|
export function isLiveModelSource(source) {
|
|
20
|
-
return source === '
|
|
21
|
+
return source === 'catalog' || source === 'public';
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export function isAbortError(error) {
|
|
@@ -60,6 +61,34 @@ export function normalizeModelIds(modelIds = [], selectedModel) {
|
|
|
60
61
|
return models;
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Rich entry fields from a canonical /v1/models response: lifecycle
|
|
66
|
+
* (status, deprecation_date, replacement), limits, modalities, and
|
|
67
|
+
* capabilities. Minimal legacy responses produce entries that carry only
|
|
68
|
+
* the id; every optional field is absent, never guessed.
|
|
69
|
+
*/
|
|
70
|
+
export function parseCatalogEntries(data = []) {
|
|
71
|
+
const entries = [];
|
|
72
|
+
for (const model of data) {
|
|
73
|
+
if (model == null || typeof model !== 'object') continue;
|
|
74
|
+
const id = typeof model.id === 'string' ? model.id.trim() : '';
|
|
75
|
+
if (!id || !MODEL_ID_PATTERN.test(id)) continue;
|
|
76
|
+
entries.push({
|
|
77
|
+
id,
|
|
78
|
+
lifecycle: model.lifecycle ?? null,
|
|
79
|
+
limits: model.limits ?? null,
|
|
80
|
+
modalities: model.modalities ?? null,
|
|
81
|
+
capabilities: model.capabilities ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return entries;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** True when the response is the canonical catalog, not a minimal list. */
|
|
88
|
+
function isCanonicalResponse(data = []) {
|
|
89
|
+
return data.some((model) => model?.schema_version === '1');
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
function gatewayOrigin(baseUrl) {
|
|
64
93
|
if (!baseUrl?.trim()) throw new Error('Gateway URL is not configured');
|
|
65
94
|
return baseUrl.trim().replace(/\/+$/, '');
|
|
@@ -70,12 +99,12 @@ function modelsEndpoint(baseUrl, path) {
|
|
|
70
99
|
return `${gatewayOrigin(baseUrl)}${normalizedPath}`;
|
|
71
100
|
}
|
|
72
101
|
|
|
73
|
-
|
|
102
|
+
async function fetchModelsPayload({
|
|
74
103
|
baseUrl,
|
|
75
104
|
apiKey,
|
|
76
|
-
path
|
|
77
|
-
fetchImpl
|
|
78
|
-
timeoutMs
|
|
105
|
+
path,
|
|
106
|
+
fetchImpl,
|
|
107
|
+
timeoutMs,
|
|
79
108
|
signal,
|
|
80
109
|
}) {
|
|
81
110
|
if (typeof fetchImpl !== 'function') {
|
|
@@ -125,7 +154,7 @@ export async function fetchGatewayModels({
|
|
|
125
154
|
throw new Error('Model discovery returned an invalid response');
|
|
126
155
|
}
|
|
127
156
|
|
|
128
|
-
return
|
|
157
|
+
return payload.data;
|
|
129
158
|
};
|
|
130
159
|
return await Promise.race([discover(), aborted]);
|
|
131
160
|
} catch (error) {
|
|
@@ -140,16 +169,35 @@ export async function fetchGatewayModels({
|
|
|
140
169
|
}
|
|
141
170
|
}
|
|
142
171
|
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Fetch one models listing. Returns the id list, the parsed catalog
|
|
174
|
+
* entries, and whether the gateway answered with the canonical catalog.
|
|
175
|
+
*/
|
|
176
|
+
export async function fetchGatewayCatalog({
|
|
177
|
+
baseUrl,
|
|
178
|
+
apiKey,
|
|
179
|
+
path = PUBLIC_MODELS_PATH,
|
|
180
|
+
fetchImpl = proxyAwareFetch,
|
|
181
|
+
timeoutMs = DEFAULT_MODEL_FETCH_TIMEOUT_MS,
|
|
182
|
+
signal,
|
|
183
|
+
}) {
|
|
184
|
+
const data = await fetchModelsPayload({
|
|
145
185
|
baseUrl,
|
|
146
|
-
|
|
186
|
+
apiKey,
|
|
187
|
+
path,
|
|
147
188
|
fetchImpl,
|
|
148
189
|
timeoutMs,
|
|
149
190
|
signal,
|
|
150
191
|
});
|
|
192
|
+
const entries = parseCatalogEntries(data);
|
|
193
|
+
const models = normalizeModelIds(entries.map((entry) => entry.id));
|
|
151
194
|
if (!models.length)
|
|
152
195
|
throw new Error('Model discovery returned no usable model IDs');
|
|
196
|
+
return { models, entries, canonical: isCanonicalResponse(data) };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function fetchGatewayModels(args) {
|
|
200
|
+
const { models } = await fetchGatewayCatalog(args);
|
|
153
201
|
return models;
|
|
154
202
|
}
|
|
155
203
|
|
|
@@ -159,14 +207,33 @@ function packagedCatalog(selectedModel, fallbackModels, error) {
|
|
|
159
207
|
[selectedModel, ...fallbackModels],
|
|
160
208
|
selectedModel,
|
|
161
209
|
),
|
|
210
|
+
defaultModels: normalizeModelIds(
|
|
211
|
+
[selectedModel, ...fallbackModels],
|
|
212
|
+
selectedModel,
|
|
213
|
+
),
|
|
214
|
+
entries: [],
|
|
162
215
|
source: 'packaged',
|
|
163
216
|
error,
|
|
164
217
|
};
|
|
165
218
|
}
|
|
166
219
|
|
|
167
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Assemble the catalog views. `models` is everything the caller can invoke,
|
|
222
|
+
* deprecated models included. `defaultModels` drops models the gateway
|
|
223
|
+
* marked deprecated or retired, so new sessions pick a current model while
|
|
224
|
+
* existing configurations that name a deprecated model keep working.
|
|
225
|
+
*/
|
|
226
|
+
function liveCatalog(models, selectedModel, source, entries = []) {
|
|
227
|
+
const normalized = normalizeModelIds(models, selectedModel);
|
|
228
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
229
|
+
const defaults = normalized.filter((id) => {
|
|
230
|
+
const status = entryById.get(id)?.lifecycle?.status;
|
|
231
|
+
return status !== 'deprecated' && status !== 'retired';
|
|
232
|
+
});
|
|
168
233
|
return {
|
|
169
|
-
models:
|
|
234
|
+
models: normalized,
|
|
235
|
+
defaultModels: defaults.length ? defaults : normalized,
|
|
236
|
+
entries,
|
|
170
237
|
source,
|
|
171
238
|
error: null,
|
|
172
239
|
};
|
|
@@ -188,6 +255,8 @@ export async function resolveModelCatalog({
|
|
|
188
255
|
if (pinned?.length) {
|
|
189
256
|
return {
|
|
190
257
|
models: normalizeModelIds(pinned, selectedModel),
|
|
258
|
+
defaultModels: normalizeModelIds(pinned, selectedModel),
|
|
259
|
+
entries: [],
|
|
191
260
|
source: 'pinned',
|
|
192
261
|
error: null,
|
|
193
262
|
};
|
|
@@ -196,41 +265,60 @@ export async function resolveModelCatalog({
|
|
|
196
265
|
const key = apiKey?.trim() || '';
|
|
197
266
|
|
|
198
267
|
if (key) {
|
|
268
|
+
let catalogError;
|
|
199
269
|
try {
|
|
200
|
-
|
|
270
|
+
// The canonical catalog is key-scoped and access-filtered by the
|
|
271
|
+
// gateway, so no second request is needed when it answers.
|
|
272
|
+
const discovered = await fetchGatewayCatalog({
|
|
201
273
|
baseUrl,
|
|
202
274
|
apiKey: key,
|
|
203
|
-
path:
|
|
275
|
+
path: PUBLIC_MODELS_PATH,
|
|
204
276
|
fetchImpl,
|
|
205
277
|
timeoutMs,
|
|
206
278
|
signal,
|
|
207
279
|
});
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
280
|
+
// An older gateway answers /v1/models with a minimal public list;
|
|
281
|
+
// those ids still name the fleet, so use them rather than the
|
|
282
|
+
// packaged registry.
|
|
283
|
+
return discovered.canonical
|
|
284
|
+
? liveCatalog(
|
|
285
|
+
discovered.models,
|
|
286
|
+
selectedModel,
|
|
287
|
+
'catalog',
|
|
288
|
+
discovered.entries,
|
|
289
|
+
)
|
|
290
|
+
: liveCatalog(discovered.models, selectedModel, 'public');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (isAbortError(error) || signal?.aborted) throw error;
|
|
293
|
+
catalogError = error;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const discovered = await fetchGatewayCatalog({
|
|
298
|
+
baseUrl,
|
|
299
|
+
fetchImpl,
|
|
300
|
+
timeoutMs,
|
|
301
|
+
signal,
|
|
302
|
+
});
|
|
303
|
+
return liveCatalog(discovered.models, selectedModel, 'public');
|
|
304
|
+
} catch (publicError) {
|
|
305
|
+
if (isAbortError(publicError) || signal?.aborted) throw publicError;
|
|
306
|
+
return packagedCatalog(
|
|
307
|
+
selectedModel,
|
|
308
|
+
fallbackModels,
|
|
309
|
+
catalogError ?? publicError,
|
|
310
|
+
);
|
|
223
311
|
}
|
|
224
312
|
}
|
|
225
313
|
|
|
226
314
|
try {
|
|
227
|
-
const discovered = await
|
|
315
|
+
const discovered = await fetchGatewayCatalog({
|
|
228
316
|
baseUrl,
|
|
229
317
|
fetchImpl,
|
|
230
318
|
timeoutMs,
|
|
231
319
|
signal,
|
|
232
320
|
});
|
|
233
|
-
return liveCatalog(discovered, selectedModel, 'public');
|
|
321
|
+
return liveCatalog(discovered.models, selectedModel, 'public');
|
|
234
322
|
} catch (error) {
|
|
235
323
|
if (isAbortError(error) || signal?.aborted) throw error;
|
|
236
324
|
return packagedCatalog(selectedModel, fallbackModels, error);
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/bin/opencode-provider.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url';
|
|
2
2
|
import { modelIds, positiveInteger } from './launch-values.js';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
catalogEntriesFromMetadata,
|
|
5
|
+
modelSupportsVision,
|
|
6
|
+
} from './model-capabilities.js';
|
|
4
7
|
|
|
5
8
|
export const OPENCODE_PROVIDER_ID = 'subconscious';
|
|
6
9
|
const OPENCODE_IMAGE_WINDOW_PLUGIN = fileURLToPath(
|
|
@@ -40,15 +43,22 @@ export function opencodeModelDisplayName(modelId) {
|
|
|
40
43
|
return slug.split(/[-_]/).filter(Boolean).map(formatToken).join(' ');
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export function buildOpenCodeModels(
|
|
46
|
+
export function buildOpenCodeModels(
|
|
47
|
+
modelIds,
|
|
48
|
+
{ context, output, entries = [] },
|
|
49
|
+
) {
|
|
50
|
+
const entryById = new Map(entries.map((entry) => [entry.id, entry]));
|
|
44
51
|
return Object.fromEntries(
|
|
45
52
|
modelIds.map((id) => [
|
|
46
53
|
id,
|
|
47
54
|
{
|
|
48
55
|
name: opencodeModelDisplayName(id),
|
|
49
56
|
tools: true,
|
|
50
|
-
limit: {
|
|
51
|
-
|
|
57
|
+
limit: {
|
|
58
|
+
context: entryById.get(id)?.limits?.context_length ?? context,
|
|
59
|
+
output: entryById.get(id)?.limits?.max_output_tokens ?? output,
|
|
60
|
+
},
|
|
61
|
+
...(modelSupportsVision(id, entries)
|
|
52
62
|
? {
|
|
53
63
|
attachment: true,
|
|
54
64
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
@@ -65,6 +75,7 @@ export function buildOpenCodeConfig({
|
|
|
65
75
|
modelIds,
|
|
66
76
|
context,
|
|
67
77
|
output,
|
|
78
|
+
entries = [],
|
|
68
79
|
}) {
|
|
69
80
|
return {
|
|
70
81
|
$schema: 'https://opencode.ai/config.json',
|
|
@@ -82,7 +93,7 @@ export function buildOpenCodeConfig({
|
|
|
82
93
|
headers: { 'x-subconscious-client': 'opencode' },
|
|
83
94
|
modelsDiscovery: { enabled: false },
|
|
84
95
|
},
|
|
85
|
-
models: buildOpenCodeModels(modelIds, { context, output }),
|
|
96
|
+
models: buildOpenCodeModels(modelIds, { context, output, entries }),
|
|
86
97
|
},
|
|
87
98
|
},
|
|
88
99
|
model: `${OPENCODE_PROVIDER_ID}/${model}`,
|
|
@@ -100,5 +111,6 @@ export function openCodeConfigFromEnv(env) {
|
|
|
100
111
|
'OPENCODE_CONTEXT_LIMIT',
|
|
101
112
|
),
|
|
102
113
|
output: positiveInteger(env.OPENCODE_OUTPUT_LIMIT, 'OPENCODE_OUTPUT_LIMIT'),
|
|
114
|
+
entries: catalogEntriesFromMetadata(env.SUBC_MODEL_METADATA),
|
|
103
115
|
});
|
|
104
116
|
}
|
package/bin/profiles.js
CHANGED
|
@@ -566,11 +566,49 @@ export async function configCommand(
|
|
|
566
566
|
export function modelsCommand(models = SUPPORTED_MODELS, options = {}) {
|
|
567
567
|
const selectedModel =
|
|
568
568
|
options.selectedModel || models[0] || RUNBOOK_DEFAULTS.MODEL;
|
|
569
|
+
const entryById = new Map(
|
|
570
|
+
(options.entries ?? []).map((entry) => [entry.id, entry]),
|
|
571
|
+
);
|
|
572
|
+
const lifecycleNote = (model) => {
|
|
573
|
+
const lifecycle = entryById.get(model)?.lifecycle;
|
|
574
|
+
if (!lifecycle) return '';
|
|
575
|
+
const date = lifecycle.deprecation_date
|
|
576
|
+
? ` until ${lifecycle.deprecation_date}`
|
|
577
|
+
: '';
|
|
578
|
+
const replacement = lifecycle.replacement
|
|
579
|
+
? ` ${c.dim}-> ${lifecycle.replacement}${c.reset}`
|
|
580
|
+
: '';
|
|
581
|
+
if (lifecycle.status === 'deprecated') {
|
|
582
|
+
return ` ${c.yellow}[deprecated${date}]${c.reset}${replacement}`;
|
|
583
|
+
}
|
|
584
|
+
if (lifecycle.status === 'retired') {
|
|
585
|
+
return ` ${c.red}[retired${date}]${c.reset}${replacement}`;
|
|
586
|
+
}
|
|
587
|
+
return '';
|
|
588
|
+
};
|
|
569
589
|
console.log(`\n ${c.bold}Available models${c.reset}\n`);
|
|
570
590
|
for (const model of models) {
|
|
571
591
|
const suffix =
|
|
572
592
|
model === selectedModel ? ` ${c.dim}(default)${c.reset}` : '';
|
|
573
|
-
console.log(
|
|
593
|
+
console.log(
|
|
594
|
+
` ${c.cyan}${model}${c.reset}${suffix}${lifecycleNote(model)}`,
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const selectedLifecycle = entryById.get(selectedModel)?.lifecycle;
|
|
598
|
+
if (selectedLifecycle?.status === 'deprecated') {
|
|
599
|
+
const when = selectedLifecycle.deprecation_date
|
|
600
|
+
? ` on ${selectedLifecycle.deprecation_date}`
|
|
601
|
+
: '';
|
|
602
|
+
const to = selectedLifecycle.replacement
|
|
603
|
+
? ` Migrate to ${selectedLifecycle.replacement}.`
|
|
604
|
+
: '';
|
|
605
|
+
console.error(
|
|
606
|
+
`\n ${c.yellow}Selected model ${selectedModel} is deprecated${when}.${to}${c.reset}`,
|
|
607
|
+
);
|
|
608
|
+
} else if (selectedLifecycle?.status === 'retired') {
|
|
609
|
+
console.error(
|
|
610
|
+
`\n ${c.yellow}Selected model ${selectedModel} is retired and may stop serving without notice.${c.reset}`,
|
|
611
|
+
);
|
|
574
612
|
}
|
|
575
613
|
if (options.source === 'public' && options.hasApiKey) {
|
|
576
614
|
console.error(`\n ${c.dim}${PUBLIC_CATALOG_FALLBACK_MESSAGE}${c.reset}`);
|
package/bin/tui.js
CHANGED
|
@@ -330,7 +330,7 @@ export async function loadRemoteTuiUpdates(state, options = {}) {
|
|
|
330
330
|
});
|
|
331
331
|
if (signal?.aborted) return;
|
|
332
332
|
await enqueuePatch({
|
|
333
|
-
models: catalog.
|
|
333
|
+
models: catalog.defaultModels,
|
|
334
334
|
modelError: catalog.error?.message || '',
|
|
335
335
|
modelSource: catalog.source,
|
|
336
336
|
modelsLoading: false,
|
package/bin/usage.js
CHANGED
|
@@ -55,15 +55,6 @@ export function shortModelSlug(slug) {
|
|
|
55
55
|
: text;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
export function isLegacyUsagePayload(data) {
|
|
59
|
-
if (!data || typeof data !== 'object') return true;
|
|
60
|
-
if ('credits' in data && data.plan && typeof data.plan === 'object')
|
|
61
|
-
return false;
|
|
62
|
-
return (
|
|
63
|
-
'dailyTokenCeiling' in data || 'consumedToday' in data || !('plan' in data)
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
58
|
export function meterColor(percentage) {
|
|
68
59
|
if (percentage >= 100) return c.red;
|
|
69
60
|
if (percentage >= 80) return c.yellow;
|
|
@@ -91,27 +82,25 @@ export function formatAllowancePercent(percent) {
|
|
|
91
82
|
return `${whole}%`;
|
|
92
83
|
}
|
|
93
84
|
|
|
94
|
-
/**
|
|
85
|
+
/** Percent-only allowance payloads from the current platform. */
|
|
95
86
|
export function describeDailyAllowance(allowance) {
|
|
96
87
|
if (!allowance || typeof allowance !== 'object') return null;
|
|
97
88
|
if (allowance.basis === 'unavailable') return { kind: 'unavailable' };
|
|
98
89
|
if (allowance.basis === 'credit' || allowance.basis === 'tokens') {
|
|
99
90
|
return { kind: allowance.basis, percent: Number(allowance.percent) || 0 };
|
|
100
91
|
}
|
|
101
|
-
|
|
102
|
-
const consumed = Number(allowance.consumed) || 0;
|
|
103
|
-
const amount = Number(allowance.amount) || 0;
|
|
104
|
-
if (amount <= 0) return null;
|
|
105
|
-
return { kind: 'tokens', percent: (consumed / amount) * 100 };
|
|
92
|
+
return null;
|
|
106
93
|
}
|
|
107
94
|
|
|
108
95
|
export function formatUsageDisplay(data) {
|
|
109
96
|
const lines = [];
|
|
110
97
|
lines.push(`\n ${c.bold}Subconscious Usage${c.reset}\n`);
|
|
111
98
|
|
|
112
|
-
const
|
|
99
|
+
const billingTerms = data.billingTerms ?? null;
|
|
100
|
+
const invoiced = billingTerms === 'invoiced';
|
|
113
101
|
const plan = data.plan ?? {};
|
|
114
102
|
const credits = data.credits ?? {};
|
|
103
|
+
const invoice = invoiced ? (data.invoice ?? {}) : {};
|
|
115
104
|
const models = Array.isArray(data.models) ? data.models : [];
|
|
116
105
|
|
|
117
106
|
const planLabel = plan.label || 'No plan';
|
|
@@ -119,55 +108,81 @@ export function formatUsageDisplay(data) {
|
|
|
119
108
|
const planLine = planStatus ? `${planLabel} · ${planStatus}` : planLabel;
|
|
120
109
|
|
|
121
110
|
lines.push(` ${c.dim}Plan${c.reset} ${planLine}`);
|
|
122
|
-
if (
|
|
123
|
-
lines.push(` ${c.dim}Billing${c.reset}
|
|
111
|
+
if (invoiced) {
|
|
112
|
+
lines.push(` ${c.dim}Billing${c.reset} Invoiced monthly (net 30)`);
|
|
124
113
|
}
|
|
125
114
|
|
|
126
115
|
const allowance = plan.dailyAllowance;
|
|
127
|
-
const unlimitedComp = Boolean(plan.isUnlimitedComp);
|
|
128
116
|
|
|
129
|
-
|
|
117
|
+
// Invoiced orgs bill against the spend limit, not a daily grant, so the
|
|
118
|
+
// daily allowance does not pertain to them and is never shown.
|
|
119
|
+
const described = invoiced ? null : describeDailyAllowance(allowance);
|
|
120
|
+
if (described) {
|
|
130
121
|
lines.push('');
|
|
131
122
|
lines.push(` ${c.dim}Daily allowance${c.reset}`);
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
: 'of daily tokens used';
|
|
123
|
+
if (described.kind === 'unavailable') {
|
|
124
|
+
lines.push(` ${c.dim}Daily credit unavailable${c.reset}`);
|
|
125
|
+
lines.push(
|
|
126
|
+
` ${c.dim}Today's credit usage could not be read just now. Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
|
|
127
|
+
);
|
|
128
|
+
} else {
|
|
129
|
+
const label =
|
|
130
|
+
described.kind === 'credit'
|
|
131
|
+
? 'of daily credit used'
|
|
132
|
+
: 'of daily tokens used';
|
|
133
|
+
lines.push(
|
|
134
|
+
` ${renderProgressBar(described.percent)} ${formatAllowancePercent(described.percent)} ${label}`,
|
|
135
|
+
);
|
|
136
|
+
lines.push(` ${c.dim}Resets midnight UTC${c.reset}`);
|
|
137
|
+
if (described.kind === 'credit' && described.percent >= 100) {
|
|
148
138
|
lines.push(
|
|
149
|
-
` ${
|
|
139
|
+
` ${c.yellow}Past today's allowance - further usage bills from your balance.${c.reset}`,
|
|
150
140
|
);
|
|
151
|
-
lines.push(` ${c.dim}Resets midnight UTC${c.reset}`);
|
|
152
|
-
if (described.kind === 'credit' && described.percent >= 100) {
|
|
153
|
-
lines.push(
|
|
154
|
-
` ${c.yellow}Past today's allowance - further usage bills from your balance.${c.reset}`,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
157
141
|
}
|
|
158
142
|
}
|
|
159
143
|
}
|
|
160
144
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
145
|
+
if (invoiced) {
|
|
146
|
+
const accrued = invoice.accruedThisPeriodDollars;
|
|
147
|
+
const limit = invoice.limitDollars;
|
|
148
|
+
const periodEnd = invoice.periodEnd ? new Date(invoice.periodEnd) : null;
|
|
149
|
+
|
|
150
|
+
lines.push('');
|
|
151
|
+
lines.push(` ${c.dim}Spend this period${c.reset}`);
|
|
152
|
+
if (accrued == null) {
|
|
153
|
+
lines.push(` ${c.dim}Spend could not be read just now.${c.reset}`);
|
|
154
|
+
lines.push(
|
|
155
|
+
` ${c.dim}Your plan and access are unaffected, and the figure returns on its own.${c.reset}`,
|
|
156
|
+
);
|
|
157
|
+
} else {
|
|
158
|
+
const limitText =
|
|
159
|
+
limit != null ? ` of ${formatCurrency(limit)} limit` : '';
|
|
160
|
+
const spendColor = limit != null && accrued >= limit ? c.red : c.reset;
|
|
161
|
+
lines.push(
|
|
162
|
+
` ${spendColor}${formatCurrency(accrued)}${c.reset}${c.dim}${limitText}${c.reset}`,
|
|
163
|
+
);
|
|
164
|
+
if (periodEnd && !Number.isNaN(periodEnd.getTime())) {
|
|
165
|
+
const isoDay = periodEnd.toISOString().slice(0, 10);
|
|
166
|
+
lines.push(` ${c.dim}Period ends ${isoDay}${c.reset}`);
|
|
167
|
+
}
|
|
168
|
+
if (limit != null && accrued >= limit) {
|
|
169
|
+
lines.push(
|
|
170
|
+
` ${c.red}At the spend limit - further requests follow your limit policy.${c.reset}`,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} else if (credits.balanceDollars != null || data.credits) {
|
|
175
|
+
lines.push('');
|
|
168
176
|
lines.push(
|
|
169
|
-
` ${c.dim}
|
|
177
|
+
` ${c.dim}Credits${c.reset} ${formatCurrency(credits.balanceDollars ?? 0)}`,
|
|
170
178
|
);
|
|
179
|
+
const overage = Number(credits.overageThisPeriodDollars) || 0;
|
|
180
|
+
if (overage > 0 || allowance) {
|
|
181
|
+
const overageColor = overage > 0 ? c.yellow : c.reset;
|
|
182
|
+
lines.push(
|
|
183
|
+
` ${c.dim}Overage${c.reset} ${overageColor}${formatCurrency(overage)} this period${c.reset}`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
171
186
|
}
|
|
172
187
|
|
|
173
188
|
if (models.length > 0) {
|
|
@@ -251,14 +266,6 @@ export async function usageCommand(argv = [], options = {}) {
|
|
|
251
266
|
throw new Error(message);
|
|
252
267
|
}
|
|
253
268
|
|
|
254
|
-
if (isLegacyUsagePayload(data)) {
|
|
255
|
-
console.log(`\n ${c.yellow}Platform upgrade required.${c.reset}`);
|
|
256
|
-
console.log(
|
|
257
|
-
` ${c.dim}This platform host returns a legacy usage shape. Deploy the latest platform or point PLATFORM_URL at a current host.${c.reset}\n`,
|
|
258
|
-
);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
269
|
if (jsonOutput) {
|
|
263
270
|
console.log(JSON.stringify(data, null, 2));
|
|
264
271
|
return;
|
package/package.json
CHANGED