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.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +324 -0
  3. package/README.md +1 -1
  4. package/docs/ROADMAP.md +8 -5
  5. package/docs/architecture-map.md +736 -0
  6. package/docs/configuration.md +165 -26
  7. package/docs/council.md +9 -0
  8. package/docs/doc-system.md +12 -9
  9. package/docs/testing.md +2 -1
  10. package/docs/troubleshooting.md +113 -0
  11. package/docs/usage.md +11 -6
  12. package/package.json +1 -1
  13. package/schemas/model-catalog.schema.json +2 -1
  14. package/schemas/run.schema.json +13 -0
  15. package/scripts/postinstall.js +4 -0
  16. package/skills/sidecar/SKILL.md +1 -8
  17. package/src/cli-handlers-doctor.js +3 -0
  18. package/src/cli-handlers-fanout.js +10 -1
  19. package/src/cli-handlers-resume-continue.js +25 -0
  20. package/src/cli.js +5 -8
  21. package/src/council/briefings-chair.js +4 -2
  22. package/src/council/run-assemble.js +7 -2
  23. package/src/council/run-retry-notes.js +21 -1
  24. package/src/council/run-stages.js +8 -1
  25. package/src/headless.js +125 -7
  26. package/src/mcp-server.js +26 -0
  27. package/src/mcp-tools.js +4 -4
  28. package/src/opencode-client.js +84 -8
  29. package/src/pack/pack-validate.js +3 -0
  30. package/src/session-manager.js +2 -2
  31. package/src/sidecar/continue.js +6 -1
  32. package/src/sidecar/conversation-mirror.js +35 -11
  33. package/src/sidecar/electron-install.js +81 -81
  34. package/src/sidecar/electron-provision.js +179 -0
  35. package/src/sidecar/electron-trust.js +299 -0
  36. package/src/sidecar/fanout-leg-fallback.js +1 -0
  37. package/src/sidecar/fanout-leg.js +10 -2
  38. package/src/sidecar/fanout.js +2 -2
  39. package/src/sidecar/interactive.js +31 -4
  40. package/src/sidecar/models-ceiling-line.js +72 -0
  41. package/src/sidecar/models.js +4 -2
  42. package/src/sidecar/reopen-notices.js +97 -0
  43. package/src/sidecar/reopen-spend.js +3 -2
  44. package/src/sidecar/resume.js +15 -2
  45. package/src/sidecar/session-finalize.js +4 -1
  46. package/src/sidecar/session-utils.js +5 -1
  47. package/src/sidecar/start-metadata.js +1 -1
  48. package/src/sidecar/start.js +10 -5
  49. package/src/sidecar/unzip.js +40 -0
  50. package/src/utils/config.js +33 -12
  51. package/src/utils/curated-models.js +8 -8
  52. package/src/utils/degrade.js +7 -0
  53. package/src/utils/doctor-output-budget-check.js +198 -0
  54. package/src/utils/engine-output-flag.js +105 -0
  55. package/src/utils/engine-variants.js +298 -0
  56. package/src/utils/http-get.js +284 -0
  57. package/src/utils/model-catalog.js +36 -4
  58. package/src/utils/model-ceilings-modelsdev.js +230 -0
  59. package/src/utils/model-fetcher.js +12 -36
  60. package/src/utils/model-output-limit.js +21 -13
  61. package/src/utils/output-length.js +90 -0
  62. package/src/utils/result-schema.js +7 -2
  63. package/src/utils/spend-ledger.js +5 -1
  64. package/src/utils/thinking-validators.js +27 -80
  65. 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 is ARITHMETICALLY INERT. kimi-k3's
21
- * true ceiling is 943,718 and Math.min(943718, 32000) is still 32000. Only
22
- * a value BELOW 32000 changes the outbound request. The issue's headline
23
- * framing ("a 32,000 reservation against a 943,718 ceiling is arbitrary")
24
- * reads as though feeding the real ceiling would help. It would not.
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 — that is what this module enables. Mode 2 (a leg spending its whole
34
- * allowance on reasoning and emitting 0-2 output tokens) would need it RAISED,
35
- * which the descriptor cannot do at all because of the Math.min; its real cause
36
- * is reasoning effort, a knob amicus already owns (`sidecar/fanout.js`
37
- * `body.reasoning`). Lowering the budget makes those legs fail faster and
38
- * cheaper. It does not make them produce output. No claim is made that it does.
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 pre-#218 behaviour.
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}} opts
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
- * Model-specific thinking level validation.
5
- * Extracted from validators.js to keep modules under 300 lines.
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
- * Validate thinking level for a specific model (synchronous)
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, model) {
62
- if (!thinking) {
63
- return { valid: true };
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 };
@@ -214,7 +214,7 @@ function validateHeadlessAgent(agent) {
214
214
  }
215
215
 
216
216
  const { validateMcpSpec, validateMcpConfigFile } = require('./mcp-validators');
217
- const { MODEL_THINKING_SUPPORT, getSupportedThinkingLevels, validateThinkingLevel } = require('./thinking-validators');
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
- MODEL_THINKING_SUPPORT,
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,