amicus 4.9.3 → 4.9.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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +324 -0
- package/README.md +1 -1
- package/docs/ROADMAP.md +8 -5
- package/docs/architecture-map.md +736 -0
- package/docs/configuration.md +165 -26
- package/docs/council.md +9 -0
- package/docs/doc-system.md +12 -9
- package/docs/testing.md +2 -1
- package/docs/troubleshooting.md +113 -0
- package/docs/usage.md +11 -6
- package/package.json +1 -1
- package/schemas/model-catalog.schema.json +2 -1
- package/schemas/run.schema.json +13 -0
- package/scripts/postinstall.js +4 -0
- package/skills/sidecar/SKILL.md +1 -8
- package/src/cli-handlers-doctor.js +3 -0
- package/src/cli-handlers-fanout.js +10 -1
- package/src/cli-handlers-resume-continue.js +25 -0
- package/src/cli.js +5 -8
- package/src/council/briefings-chair.js +4 -2
- package/src/council/run-assemble.js +7 -2
- package/src/council/run-retry-notes.js +21 -1
- package/src/council/run-stages.js +8 -1
- package/src/headless.js +125 -7
- package/src/mcp-server.js +26 -0
- package/src/mcp-tools.js +4 -4
- package/src/opencode-client.js +84 -8
- package/src/pack/pack-validate.js +3 -0
- package/src/session-manager.js +2 -2
- package/src/sidecar/continue.js +6 -1
- package/src/sidecar/conversation-mirror.js +35 -11
- package/src/sidecar/electron-install.js +81 -81
- package/src/sidecar/electron-provision.js +179 -0
- package/src/sidecar/electron-trust.js +299 -0
- package/src/sidecar/fanout-leg-fallback.js +1 -0
- package/src/sidecar/fanout-leg.js +10 -2
- package/src/sidecar/fanout.js +2 -2
- package/src/sidecar/interactive.js +31 -4
- package/src/sidecar/models-ceiling-line.js +72 -0
- package/src/sidecar/models.js +4 -2
- package/src/sidecar/reopen-notices.js +97 -0
- package/src/sidecar/reopen-spend.js +3 -2
- package/src/sidecar/resume.js +15 -2
- package/src/sidecar/session-finalize.js +4 -1
- package/src/sidecar/session-utils.js +5 -1
- package/src/sidecar/start-metadata.js +1 -1
- package/src/sidecar/start.js +10 -5
- package/src/sidecar/unzip.js +40 -0
- package/src/utils/config.js +33 -12
- package/src/utils/curated-models.js +8 -8
- package/src/utils/degrade.js +7 -0
- package/src/utils/doctor-output-budget-check.js +198 -0
- package/src/utils/engine-output-flag.js +105 -0
- package/src/utils/engine-variants.js +298 -0
- package/src/utils/http-get.js +284 -0
- package/src/utils/model-catalog.js +36 -4
- package/src/utils/model-ceilings-modelsdev.js +230 -0
- package/src/utils/model-fetcher.js +12 -36
- package/src/utils/model-output-limit.js +21 -13
- package/src/utils/output-length.js +90 -0
- package/src/utils/result-schema.js +7 -2
- package/src/utils/spend-ledger.js +5 -1
- package/src/utils/thinking-validators.js +27 -80
- package/src/utils/validators.js +2 -3
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
* i.e. `ProviderTransform.maxOutputTokens(model) = Math.min(model.limit.output,
|
|
18
18
|
* OUTPUT_TOKEN_MAX)`. Three consequences, each a trap:
|
|
19
19
|
*
|
|
20
|
-
* 1. Supplying the model's REAL ceiling
|
|
21
|
-
* true ceiling is 943,718 and
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* 1. Supplying the model's REAL ceiling through THIS DESCRIPTOR ALONE is
|
|
21
|
+
* ARITHMETICALLY INERT. kimi-k3's true ceiling is 943,718 and
|
|
22
|
+
* Math.min(943718, 32000) is still 32000. Through the descriptor only a
|
|
23
|
+
* value BELOW OUTPUT_TOKEN_MAX changes the outbound request. Raising
|
|
24
|
+
* OUTPUT_TOKEN_MAX itself is engine-output-flag.js's job (PR 2): with the
|
|
25
|
+
* flag set to the same budget the two levers agree on min(budget, ceiling)
|
|
26
|
+
* — measured, probe rows C2 and K6.
|
|
25
27
|
* 2. `limit.context` is MANDATORY whenever `limit` is present. A `limit` with
|
|
26
28
|
* only `output` is a hard ConfigInvalidError — and it poisons the ENTIRE
|
|
27
29
|
* config for the server's lifetime, not just that model. Measured against
|
|
@@ -30,15 +32,21 @@
|
|
|
30
32
|
*
|
|
31
33
|
* WHAT THIS DOES NOT FIX. #218 conflates two modes that pull in OPPOSITE
|
|
32
34
|
* directions on this one knob. Mode 1 (credit rejection) needs the reservation
|
|
33
|
-
* LOWERED —
|
|
34
|
-
* allowance on reasoning and emitting 0-2 output tokens)
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
35
|
+
* LOWERED — this descriptor does that. Mode 2 (a leg spending its whole
|
|
36
|
+
* allowance on reasoning and emitting 0-2 output tokens) needs it RAISED, which
|
|
37
|
+
* this descriptor cannot do (the Math.min) and the engine flag can
|
|
38
|
+
* (engine-output-flag.js) — but its real cause is reasoning effort, which #218
|
|
39
|
+
* PR 4 now delivers: `--thinking` reaches the engine as its `variant` field, not
|
|
40
|
+
* the `reasoning` object the prompt API never read (F1), and is checked against
|
|
41
|
+
* the model's own declaration first — so on the one route where a variant moves
|
|
42
|
+
* the reservation the leg is refused, not silently overshot (M2: 24000 + 16000 =
|
|
43
|
+
* 40000; the fit that lands the sum on the budget is M17). Lowering the budget
|
|
44
|
+
* makes such a leg fail faster and cheaper; raising it gives the reasoning more
|
|
45
|
+
* room. Neither makes it produce output, and no claim is made that either does.
|
|
39
46
|
*
|
|
40
47
|
* POLICY: opt-in, no default change. With no configured budget every model is
|
|
41
|
-
* still registered as `{}` — byte-identical to
|
|
48
|
+
* still registered as `{}` and no engine flag is set — byte-identical to
|
|
49
|
+
* pre-#218 behaviour.
|
|
42
50
|
*/
|
|
43
51
|
|
|
44
52
|
'use strict';
|
|
@@ -121,4 +129,4 @@ function computeModelLimit(row, budget) {
|
|
|
121
129
|
return { context, output: Math.max(1, Math.min(ceiling, want)) };
|
|
122
130
|
}
|
|
123
131
|
|
|
124
|
-
module.exports = { normalizeOutputBudget, buildLimitLookup, computeModelLimit };
|
|
132
|
+
module.exports = { normalizeOutputBudget, buildLimitLookup, computeModelLimit, positiveCount };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module utils/output-length
|
|
3
|
+
* #218 PR 3: name the "Mode 2" death.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM. A council leg whose provider stopped for length before any
|
|
6
|
+
* answer text -- the whole max_tokens reservation went to reasoning (the #218
|
|
7
|
+
* ledger rows: 32000 reasoning, 0-2 output, $0.63 billed for nothing) -- came
|
|
8
|
+
* back `complete` with an empty summary and was announced as "the leg ended
|
|
9
|
+
* 'complete' with no usable output"; with VISIBLE reasoning it came back
|
|
10
|
+
* `complete` with its thinking promoted to the review and was adjudicated as one.
|
|
11
|
+
*
|
|
12
|
+
* WHAT THE ENGINE RECORDS (scripts/probe-max-tokens.js rows A/H1/L1-L4, engine
|
|
13
|
+
* 1.18.15): `finish: 'length'` on the assistant message on both provider
|
|
14
|
+
* families; a reasoning/output token split on OpenAI-compatible routes
|
|
15
|
+
* (L3: output = completion - reasoning) but NOT on the direct Anthropic route
|
|
16
|
+
* (L4: everything is `output`, reasoning 0); and, with visible reasoning, a
|
|
17
|
+
* `reasoning` part and no `text` part (L2/L4), which
|
|
18
|
+
* sidecar/conversation-mirror.js :: mirrorMessages promotes to `output` -- so
|
|
19
|
+
* `output` cannot be the test; the mirror records the last message's own facts
|
|
20
|
+
* and this module reads only those. No row carries an engine error for the stop.
|
|
21
|
+
*
|
|
22
|
+
* So the death is keyed on `finish` plus "no answer text arrived", never on a
|
|
23
|
+
* token count; the counts are reported, not decided on. Pure: no I/O, no clock.
|
|
24
|
+
* headless.js :: runHeadless calls both functions once, post-loop.
|
|
25
|
+
*/
|
|
26
|
+
'use strict';
|
|
27
|
+
|
|
28
|
+
const {
|
|
29
|
+
outputTokenFlagValue, ENGINE_DEFAULT_OUTPUT_TOKENS, OUTPUT_TOKEN_FLAG, PLAIN_OUTPUT_TOKEN_FLAG,
|
|
30
|
+
} = require('./engine-output-flag');
|
|
31
|
+
|
|
32
|
+
/** The prefix a consumer can classify on, like `NO_OUTPUT_BACKSTOP:`. */
|
|
33
|
+
const OUTPUT_LENGTH_PREFIX = 'OUTPUT_LENGTH:';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Is this leg the Mode 2 death? The provider stopped for length AND the LAST
|
|
37
|
+
* assistant message carries no answer text: nothing at all (L1), or only
|
|
38
|
+
* reasoning (L2/L4). Decided per message, never on the session's accumulated
|
|
39
|
+
* output (council #232 r1 B2/D1). Named mutants (tests/utils/output-length.test.js):
|
|
40
|
+
* "NOTLENGTH" drops the finish check, "TEXTIGNORED" drops the text check.
|
|
41
|
+
* @param {{finish?: string|null, hasText?: boolean}} last the last assistant message's facts
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function isOutputLengthDeath({ finish, hasText }) {
|
|
45
|
+
return finish === 'length' && hasText !== true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The reason string. Every clause is an observation: `finish` and the two
|
|
50
|
+
* counts are the engine's own record of the message; the budget clause is what
|
|
51
|
+
* the engine serving the leg was spawned with: the budget (`null` = unset,
|
|
52
|
+
* `undefined` = unknown — no handle value and config unreadable) or, when no
|
|
53
|
+
* budget was set, the ambient flag. The remedy names the one
|
|
54
|
+
* lever that exists today; PR 4 adds the effort lever. Named mutant
|
|
55
|
+
* "BUDGETUNSET": always print the unset clause.
|
|
56
|
+
* @param {{tokens?: {reasoning?: number, output?: number}|null,
|
|
57
|
+
* budget?: number|null, reasoningOnly?: boolean,
|
|
58
|
+
* ambientFlag?: string|null}} args `reasoningOnly` = the
|
|
59
|
+
* message carried reasoning parts and no text -- L2/L4; `ambientFlag` = the
|
|
60
|
+
* ambient `OPENCODE_EXPERIMENTAL_OUTPUT_TOKEN_MAX` the engine was started with
|
|
61
|
+
* when no budget was set (`null` when none, or when a budget overrode it);
|
|
62
|
+
* named mutant "AMBIENTIGNORED".
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
function formatOutputLengthReason({ tokens, budget, reasoningOnly, ambientFlag }) {
|
|
66
|
+
const t = tokens || {};
|
|
67
|
+
const count = (n) => (Number.isFinite(n) ? n : 0);
|
|
68
|
+
const streamed = reasoningOnly
|
|
69
|
+
? 'only reasoning was streamed, no answer text'
|
|
70
|
+
: 'no answer text arrived';
|
|
71
|
+
// PLAIN_OUTPUT_TOKEN_FLAG is the one form measured to be honoured (C1, K5,
|
|
72
|
+
// K12); 64000abc and 0 fell back to 32000 (D1/D2); every other form is
|
|
73
|
+
// unmeasured, and the clause below says so -- shared with the doctor row
|
|
74
|
+
// (doctor-output-budget-check.js :: evaluateOutputBudget) so the gates agree.
|
|
75
|
+
const ambient = typeof ambientFlag === 'string' ? ambientFlag : null;
|
|
76
|
+
const knob = budget === undefined
|
|
77
|
+
? 'outputBudget could not be read'
|
|
78
|
+
: budget !== null
|
|
79
|
+
? `outputBudget is ${outputTokenFlagValue(budget)}`
|
|
80
|
+
: ambient === null
|
|
81
|
+
? `outputBudget is unset — the engine's ${ENGINE_DEFAULT_OUTPUT_TOKENS} default reservation governs`
|
|
82
|
+
: PLAIN_OUTPUT_TOKEN_FLAG.test(ambient)
|
|
83
|
+
? `outputBudget is unset — the ambient ${OUTPUT_TOKEN_FLAG}=${ambient} the engine was started with governs (each leg reserves min(${ambient}, the ceiling the engine's catalog knows for it))`
|
|
84
|
+
: `outputBudget is unset and the ambient ${OUTPUT_TOKEN_FLAG}=${ambient} the engine was started with is not a plain positive integer — the only form measured to be honoured (probe D1/D2: 64000abc and 0 fell back to ${ENGINE_DEFAULT_OUTPUT_TOKENS} silently); any other form is unmeasured`;
|
|
85
|
+
return `${OUTPUT_LENGTH_PREFIX} the provider stopped at the max_tokens reservation (finish 'length') and ${streamed} — `
|
|
86
|
+
+ `${count(t.reasoning)} reasoning / ${count(t.output)} output tokens; ${knob} — `
|
|
87
|
+
+ 'raise outputBudget in config.json (docs/configuration.md, Output budget)';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { OUTPUT_LENGTH_PREFIX, isOutputLengthDeath, formatOutputLengthReason };
|
|
@@ -50,6 +50,7 @@ function durationBetween(createdAt, completedAt) {
|
|
|
50
50
|
* metadata.pack was recorded (solo session launched via --pack), sourced straight off
|
|
51
51
|
* `metadata` like `usage`/`opencodeSessionId` already are (no new function parameter needed).
|
|
52
52
|
* `tag` (v4.7 F8/D13) is additive the same way — present only when metadata.tag was recorded.
|
|
53
|
+
* `finish` (#218 PR 3) likewise — the engine's finish reason for the leg's last assistant message. `variant` / `variantUnverified` (#218 PR 4) likewise — the effort level SENT, and whether the engine's catalogue knew the model when it was sent.
|
|
53
54
|
*/
|
|
54
55
|
function buildRunResult({ taskId, metadata = {}, result = null, summary = null, modelInput = null, sessionDir = null, waveId = null, usage = null }) {
|
|
55
56
|
const status = result ? statusFromResult(result) : (metadata.status || 'unknown');
|
|
@@ -80,6 +81,8 @@ function buildRunResult({ taskId, metadata = {}, result = null, summary = null,
|
|
|
80
81
|
// (B3): emit-when-VALID via the shared predicate — `metadata` is read off
|
|
81
82
|
// disk, so NaN/±Infinity/negatives/fractions all reach here. See ./ttft.js.
|
|
82
83
|
...(isMeasuredTtft(metadata.ttftMs) ? { ttftMs: metadata.ttftMs } : {}),
|
|
84
|
+
...(typeof metadata.finish === 'string' ? { finish: metadata.finish } : {}), // #218 PR 3: emit-when-set (named mutant FINISHCOERCED)
|
|
85
|
+
...(typeof metadata.variant === 'string' ? { variant: metadata.variant } : {}), ...(metadata.variantUnverified === true ? { variantUnverified: true } : {}), // #218 PR 4: emit-when-sent (named mutants VARIANTCOERCED / UNVERIFIEDCOERCED)
|
|
83
86
|
usage: usage !== null ? usage : (metadata.usage || null),
|
|
84
87
|
...(metadata.pack ? { pack: metadata.pack } : {}),
|
|
85
88
|
...(metadata.tag ? { tag: metadata.tag } : {}),
|
|
@@ -184,10 +187,11 @@ const { buildRunResultFromSession, buildWaveResultFromSession } = require('./res
|
|
|
184
187
|
* #13: lastRefreshAttempt/lastRefreshError are additive — null/null when the
|
|
185
188
|
* last refresh attempt on record succeeded (or none has happened yet).
|
|
186
189
|
* @param {{models: Array, fetchedAt: number|null, refreshed?: boolean, search?: string|null,
|
|
187
|
-
* lastRefreshAttempt?: number|null, lastRefreshError?: string|null
|
|
190
|
+
* lastRefreshAttempt?: number|null, lastRefreshError?: string|null,
|
|
191
|
+
* ceilingEnrichment?: object|null}} opts
|
|
188
192
|
*/
|
|
189
193
|
function buildCatalogDoc({ models, fetchedAt, refreshed = false, search = null,
|
|
190
|
-
lastRefreshAttempt = null, lastRefreshError = null }) {
|
|
194
|
+
lastRefreshAttempt = null, lastRefreshError = null, ceilingEnrichment = null }) {
|
|
191
195
|
return {
|
|
192
196
|
schemaVersion: SCHEMA_VERSION,
|
|
193
197
|
type: 'model-catalog',
|
|
@@ -198,6 +202,7 @@ function buildCatalogDoc({ models, fetchedAt, refreshed = false, search = null,
|
|
|
198
202
|
models,
|
|
199
203
|
lastRefreshAttempt: lastRefreshAttempt || null,
|
|
200
204
|
lastRefreshError: lastRefreshError || null,
|
|
205
|
+
ceilingEnrichment: ceilingEnrichment || null, // #218 P3: additive within SCHEMA_VERSION
|
|
201
206
|
};
|
|
202
207
|
}
|
|
203
208
|
|
|
@@ -62,11 +62,13 @@ const SPEND_LEDGER_FILE = 'spend-ledger.jsonl';
|
|
|
62
62
|
* @param {number} [opts.attempt] fallback attempt count (omitted if absent)
|
|
63
63
|
* @param {string} [opts.substitutedFor] substituted model (omitted if absent)
|
|
64
64
|
* @param {string} [opts.retryOfWaveId] wave id being retried (omitted if absent)
|
|
65
|
+
* @param {string} [opts.finish] the leg's finish reason (omitted if absent) — #218 PR 3: 'length' on a row is the Mode 2 receipt
|
|
66
|
+
* @param {string} [opts.variant] the effort level sent (omitted if absent) — #218 PR 4
|
|
65
67
|
* @param {{dir?:string}} [ctx] test seam — dir overrides getConfigDir()
|
|
66
68
|
*/
|
|
67
69
|
function appendSpend({ taskId, waveId, model, mode, usage,
|
|
68
70
|
op, status, councilRunId, councilName, project, gateway, tag,
|
|
69
|
-
attempt, substitutedFor, retryOfWaveId }, ctx = {}) {
|
|
71
|
+
attempt, substitutedFor, retryOfWaveId, finish, variant }, ctx = {}) {
|
|
70
72
|
if (!usage) { return; }
|
|
71
73
|
try {
|
|
72
74
|
const dir = ctx.dir || getConfigDir();
|
|
@@ -96,6 +98,8 @@ function appendSpend({ taskId, waveId, model, mode, usage,
|
|
|
96
98
|
if (attempt !== undefined) { row.attempt = attempt; }
|
|
97
99
|
if (substitutedFor !== undefined) { row.substitutedFor = substitutedFor; }
|
|
98
100
|
if (retryOfWaveId !== undefined) { row.retryOfWaveId = retryOfWaveId; }
|
|
101
|
+
if (typeof finish === 'string') { row.finish = finish; }
|
|
102
|
+
if (typeof variant === 'string') { row.variant = variant; } // #218 PR 4: emit-when-sent (named mutant "VARIANTNULLED")
|
|
99
103
|
// v4.4.1 CA-2: a leg whose OWN cost is known but which spawned a child
|
|
100
104
|
// session the walk could not price writes a PRICED row — so `unpricedRows`
|
|
101
105
|
// never catches it and `amicus spend` reads as a complete measurement while
|
|
@@ -1,92 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Thinking Level Validators
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* #218 PR 4: the CLI's `--thinking` check is a VOCABULARY check only — the seven
|
|
5
|
+
* levels the curated routes declare between them (engine-variants.js ::
|
|
6
|
+
* VARIANT_LEVELS, measured on the engine's /config/providers dump, probe M0).
|
|
7
|
+
* Whether a given MODEL declares the level is the engine's call, read from that
|
|
8
|
+
* same dump at send time (opencode-client.js :: sendPrompt), where a level the
|
|
9
|
+
* model does not declare is refused before anything is sent. The per-model table
|
|
10
|
+
* this file carried until PR 4 (gpt-5 "without minimal", gemini "with everything")
|
|
11
|
+
* was a static guess the dump CONFIRMS on one row and contradicts on the other
|
|
12
|
+
* (M0: both exclude `minimal` for gpt-5 — gpt-5.6-terra declares none, low, medium,
|
|
13
|
+
* high, xhigh, max — while the table gave gemini `none` and `xhigh` and
|
|
14
|
+
* gemini-3.6-flash declares neither, only minimal, low, medium, high), and its "use
|
|
15
|
+
* medium instead" adjustment sent a level the user never asked for. Both are gone;
|
|
16
|
+
* nothing is adjusted here — guessing right on one row does not make a guess a
|
|
17
|
+
* declaration (council #235 r1 wave 2: this docblock had cited the CONFIRMING row
|
|
18
|
+
* as the contradiction, the same inversion the CHANGELOG carried).
|
|
6
19
|
*/
|
|
7
20
|
|
|
8
|
-
|
|
9
|
-
* Model-specific thinking level support (static fallback)
|
|
10
|
-
* Maps model patterns to their supported thinking levels.
|
|
11
|
-
*
|
|
12
|
-
* NOTE: For dynamic, up-to-date capabilities, use model-capabilities.js
|
|
13
|
-
* which fetches from OpenRouter API and caches the results.
|
|
14
|
-
* This static map is used as a fast fallback for CLI validation.
|
|
15
|
-
*/
|
|
16
|
-
const MODEL_THINKING_SUPPORT = {
|
|
17
|
-
// OpenAI GPT-5.x does NOT support 'minimal'
|
|
18
|
-
'gpt-5': ['none', 'low', 'medium', 'high', 'xhigh'],
|
|
19
|
-
// o3/o3-mini supports all levels
|
|
20
|
-
'o3': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
21
|
-
// Gemini supports all levels
|
|
22
|
-
'gemini': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
23
|
-
// Default: all levels supported
|
|
24
|
-
'default': ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Get supported thinking levels for a model (synchronous, static fallback)
|
|
29
|
-
*
|
|
30
|
-
* For dynamic lookup from OpenRouter API cache, use:
|
|
31
|
-
* const { getSupportedThinkingLevels } = require('./model-capabilities');
|
|
32
|
-
*
|
|
33
|
-
* @param {string} model - Model identifier
|
|
34
|
-
* @returns {string[]} Array of supported thinking levels
|
|
35
|
-
*/
|
|
36
|
-
function getSupportedThinkingLevels(model) {
|
|
37
|
-
if (!model) {return MODEL_THINKING_SUPPORT.default;}
|
|
38
|
-
|
|
39
|
-
const modelLower = model.toLowerCase();
|
|
40
|
-
|
|
41
|
-
// Check each known model pattern
|
|
42
|
-
for (const [pattern, levels] of Object.entries(MODEL_THINKING_SUPPORT)) {
|
|
43
|
-
if (pattern !== 'default' && modelLower.includes(pattern)) {
|
|
44
|
-
return levels;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return MODEL_THINKING_SUPPORT.default;
|
|
49
|
-
}
|
|
21
|
+
const { VARIANT_LEVELS } = require('./engine-variants');
|
|
50
22
|
|
|
51
23
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* For async validation with dynamic API cache, use:
|
|
55
|
-
* const { validateThinkingForModel } = require('./model-capabilities');
|
|
56
|
-
*
|
|
57
|
-
* @param {string} thinking - Thinking level ('minimal', 'low', 'medium', 'high', 'xhigh', 'none')
|
|
58
|
-
* @param {string} model - Model identifier
|
|
59
|
-
* @returns {{valid: boolean, error?: string, warning?: string, adjustedLevel?: string}}
|
|
24
|
+
* @param {string} [thinking] the requested level; omitted (undefined/null) is valid (nothing is sent then)
|
|
25
|
+
* @returns {{valid: boolean, error?: string}}
|
|
60
26
|
*/
|
|
61
|
-
function validateThinkingLevel(thinking
|
|
62
|
-
|
|
63
|
-
|
|
27
|
+
function validateThinkingLevel(thinking) {
|
|
28
|
+
// council #235 r2 (A2): presence, not truthiness. `--thinking=` parses to '' (cli.js's
|
|
29
|
+
// inline-value branch) and a truthiness test accepted it as "flag omitted", so the level
|
|
30
|
+
// was silently dropped. An omitted flag is undefined/null; anything else the user typed
|
|
31
|
+
// must face the vocabulary check. Named mutant "FALSYLEVELACCEPTED": restore `if (!thinking)`.
|
|
32
|
+
if (thinking === undefined || thinking === null) { return { valid: true }; }
|
|
33
|
+
if (!VARIANT_LEVELS.includes(thinking)) {
|
|
34
|
+
return { valid: false, error: `Error: --thinking must be one of: ${VARIANT_LEVELS.join(', ')}` };
|
|
64
35
|
}
|
|
65
|
-
|
|
66
|
-
const allLevels = MODEL_THINKING_SUPPORT.default;
|
|
67
|
-
if (!allLevels.includes(thinking)) {
|
|
68
|
-
return {
|
|
69
|
-
valid: false,
|
|
70
|
-
error: `Error: --thinking must be one of: ${allLevels.join(', ')}`
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const supportedLevels = getSupportedThinkingLevels(model);
|
|
75
|
-
if (!supportedLevels.includes(thinking)) {
|
|
76
|
-
// Map to nearest supported level
|
|
77
|
-
const fallback = thinking === 'minimal' ? 'low' : 'medium';
|
|
78
|
-
return {
|
|
79
|
-
valid: true,
|
|
80
|
-
warning: `Warning: Model '${model}' does not support thinking level '${thinking}'. Using '${fallback}' instead.`,
|
|
81
|
-
adjustedLevel: fallback
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
|
|
85
36
|
return { valid: true };
|
|
86
37
|
}
|
|
87
38
|
|
|
88
|
-
module.exports = {
|
|
89
|
-
MODEL_THINKING_SUPPORT,
|
|
90
|
-
getSupportedThinkingLevels,
|
|
91
|
-
validateThinkingLevel
|
|
92
|
-
};
|
|
39
|
+
module.exports = { VARIANT_LEVELS, validateThinkingLevel };
|
package/src/utils/validators.js
CHANGED
|
@@ -214,7 +214,7 @@ function validateHeadlessAgent(agent) {
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const { validateMcpSpec, validateMcpConfigFile } = require('./mcp-validators');
|
|
217
|
-
const {
|
|
217
|
+
const { VARIANT_LEVELS, validateThinkingLevel } = require('./thinking-validators');
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* Validate API key is present for the given model's provider.
|
|
@@ -260,7 +260,7 @@ function validateApiKey(model) {
|
|
|
260
260
|
module.exports = {
|
|
261
261
|
VALID_AGENT_MODES,
|
|
262
262
|
PROVIDER_KEY_MAP,
|
|
263
|
-
|
|
263
|
+
VARIANT_LEVELS,
|
|
264
264
|
TASK_ID_PATTERN,
|
|
265
265
|
validateTaskId,
|
|
266
266
|
TAG_PATTERN,
|
|
@@ -278,7 +278,6 @@ module.exports = {
|
|
|
278
278
|
validateMcpConfigFile,
|
|
279
279
|
validateApiKey,
|
|
280
280
|
validateThinkingLevel,
|
|
281
|
-
getSupportedThinkingLevels,
|
|
282
281
|
findSessionInProjectDirs,
|
|
283
282
|
// Re-exported from input-validators.js
|
|
284
283
|
validateStartInputs: require('./input-validators').validateStartInputs,
|