amicus 4.9.2 → 4.9.4
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/bin/amicus.js +6 -0
- package/docs/ROADMAP.md +5 -4
- package/docs/architecture-map.md +732 -0
- package/docs/configuration.md +175 -1
- package/docs/council.md +9 -0
- package/docs/doc-system.md +12 -9
- package/docs/testing.md +2 -1
- package/docs/troubleshooting.md +76 -0
- package/docs/usage.md +14 -6
- package/electron/main.js +25 -2
- package/electron/setup-ui-alias-groups.js +161 -0
- package/electron/setup-ui-alias-script.js +70 -4
- package/electron/setup-ui-aliases.js +25 -21
- package/electron/setup-ui.js +11 -1
- package/package.json +1 -1
- package/schemas/model-catalog.schema.json +2 -1
- package/schemas/run.schema.json +13 -0
- package/skills/sidecar/SKILL.md +1 -8
- package/src/cli-handlers-doctor.js +12 -16
- package/src/cli-handlers-fanout.js +10 -1
- package/src/cli-handlers-resume-continue.js +25 -0
- package/src/cli-handlers.js +17 -1
- 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/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/utils/api-key-validation.js +183 -94
- package/src/utils/config.js +65 -2
- package/src/utils/curated-models.js +8 -8
- package/src/utils/degrade.js +7 -0
- package/src/utils/doctor-credit-check.js +61 -0
- package/src/utils/doctor-key-auth-check.js +271 -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/live-probes.js +53 -0
- package/src/utils/model-catalog.js +36 -4
- package/src/utils/model-ceilings-modelsdev.js +230 -0
- package/src/utils/model-fetcher.js +14 -36
- package/src/utils/model-output-limit.js +132 -0
- package/src/utils/openrouter-credit.js +104 -0
- 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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup UI - Alias grouping rule (issue 213)
|
|
3
|
+
*
|
|
4
|
+
* The Step 3 alias editor used to bucket rows with a hardcoded list of alias
|
|
5
|
+
* NAMES, and `Other` was itself a fixed key list rather than a catch-all — so
|
|
6
|
+
* any alias whose name was not on the list (a local-provider route, a `free-*`
|
|
7
|
+
* council member, a case variant like `GLM`) rendered nowhere at all.
|
|
8
|
+
*
|
|
9
|
+
* Grouping is now derived from the alias's ROUTE VENDOR, which every alias has.
|
|
10
|
+
*
|
|
11
|
+
* REUSE NOTE: the vendor parse is `vendorOf` from src/sidecar/fallback-chains.js
|
|
12
|
+
* — the existing primitive, imported, not re-implemented. It PARSES a vendor
|
|
13
|
+
* segment (it never emits an id that gets called), which is the same
|
|
14
|
+
* ban-exempt category as the other allowlisted `vendorOf` callers in
|
|
15
|
+
* .eslintrc.js. `groupModelsByFamily` (src/utils/model-fetcher.js) is
|
|
16
|
+
* deliberately NOT reused: it keys on `id.split('/')[0]`, so every
|
|
17
|
+
* `openrouter/...` alias would collapse into a single "OpenRouter" bucket —
|
|
18
|
+
* exactly the grouping this file exists to avoid. Its DISPLAY half
|
|
19
|
+
* (PROVIDER_FAMILY_NAMES) is reused below.
|
|
20
|
+
*
|
|
21
|
+
* SHARED-WITH-THE-BROWSER NOTE — deliberately NOT shared. The wizard's inline
|
|
22
|
+
* script cannot `require`, so the browser could only get this rule as a copy:
|
|
23
|
+
* hand-written (silent divergence — a 3-segment direct id like `a/b/c` already
|
|
24
|
+
* splits differently under the two obvious spellings) or serialised from the
|
|
25
|
+
* source below (which would put `slice('openrouter/'.length)` back into the
|
|
26
|
+
* page). The page carrying its own gateway-prefix strip is the exact shape
|
|
27
|
+
* issue 214 removed and that tests/setup-ui.test.js still guards
|
|
28
|
+
* ("ships no routing policy to the page: ... no prefix derivation"), because
|
|
29
|
+
* that copy is how a direct id gets fabricated for a namespace that never
|
|
30
|
+
* served it.
|
|
31
|
+
*
|
|
32
|
+
* So there is ONE grouping rule and it lives here, server-side. The client
|
|
33
|
+
* (setup-ui-alias-script.js) never derives a vendor: a route added during the
|
|
34
|
+
* session goes into its own clearly-labelled "New routes" group, and vendor
|
|
35
|
+
* filing happens when the server next renders the editor.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const { vendorOf } = require('../src/sidecar/fallback-chains');
|
|
39
|
+
const { PROVIDER_FAMILY_NAMES, listDirectProviders } = require('../src/utils/provider-registry');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Display names for vendors seen in alias routes.
|
|
43
|
+
*
|
|
44
|
+
* DISPLAY ONLY — deliberately not folded into provider-registry's PROVIDERS,
|
|
45
|
+
* which is a *capability* registry (env var, direct-vs-gateway, live fetch).
|
|
46
|
+
* KNOWN_PROVIDERS / PROVIDER_ENV_MAP are derived from that list, so adding
|
|
47
|
+
* `z-ai` there would claim Amicus can hold a z-ai API key. The five real
|
|
48
|
+
* providers keep their single source of truth via PROVIDER_FAMILY_NAMES.
|
|
49
|
+
*/
|
|
50
|
+
const ALIAS_VENDOR_LABELS = {
|
|
51
|
+
...PROVIDER_FAMILY_NAMES,
|
|
52
|
+
// Vendors reachable through the gateway (curated + commonly pinned)
|
|
53
|
+
'qwen': 'Qwen',
|
|
54
|
+
'mistralai': 'Mistral AI',
|
|
55
|
+
'z-ai': 'Z.AI',
|
|
56
|
+
'minimax': 'MiniMax',
|
|
57
|
+
'x-ai': 'xAI',
|
|
58
|
+
'moonshotai': 'Moonshot AI',
|
|
59
|
+
'bytedance-seed': 'ByteDance Seed',
|
|
60
|
+
'thinkingmachines': 'Thinking Machines',
|
|
61
|
+
'cognitivecomputations': 'Cognitive Computations',
|
|
62
|
+
'inclusionai': 'InclusionAI',
|
|
63
|
+
'nvidia': 'NVIDIA',
|
|
64
|
+
'cohere': 'Cohere',
|
|
65
|
+
'meta-llama': 'Meta Llama',
|
|
66
|
+
'nousresearch': 'Nous Research',
|
|
67
|
+
'perplexity': 'Perplexity',
|
|
68
|
+
'microsoft': 'Microsoft',
|
|
69
|
+
'ai21': 'AI21',
|
|
70
|
+
'amazon': 'Amazon',
|
|
71
|
+
// Local providers (src/utils/local-providers.js PRESETS / VALID_FLAVORS)
|
|
72
|
+
'ollama': 'Ollama',
|
|
73
|
+
'lmstudio': 'LM Studio',
|
|
74
|
+
'vllm': 'vLLM',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** `some-new-vendor` -> `Some New Vendor`, so an unmapped vendor is not a raw slug. */
|
|
78
|
+
function titleCaseVendor(vendor) {
|
|
79
|
+
return String(vendor).split(/[-_]/).filter(Boolean)
|
|
80
|
+
.map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Vendor key for an alias route. Wraps the shared `vendorOf` with the two
|
|
85
|
+
* normalisations issue 213 flagged: case, and the leading `~` of a floating
|
|
86
|
+
* OpenRouter id (`openrouter/~z-ai/glm-latest` must not form a second group
|
|
87
|
+
* next to `z-ai`).
|
|
88
|
+
* @param {string} route @returns {string} '' when there is no usable route
|
|
89
|
+
*/
|
|
90
|
+
function aliasVendorOf(route) {
|
|
91
|
+
const v = vendorOf(route).toLowerCase();
|
|
92
|
+
return v.charAt(0) === '~' ? v.slice(1) : v;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Display label for a vendor key.
|
|
97
|
+
* hasOwnProperty, not a bare lookup: vendor is derived from a user-editable
|
|
98
|
+
* route, and `__proto__`/`constructor` would otherwise return prototype junk.
|
|
99
|
+
* @param {string} vendor @returns {string}
|
|
100
|
+
*/
|
|
101
|
+
function vendorLabel(vendor) {
|
|
102
|
+
if (!vendor) { return 'Other'; }
|
|
103
|
+
const hit = Object.prototype.hasOwnProperty.call(ALIAS_VENDOR_LABELS, vendor)
|
|
104
|
+
? ALIAS_VENDOR_LABELS[vendor] : null;
|
|
105
|
+
return hit || titleCaseVendor(vendor);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Direct-route vendors render first; everything else sorts by label. */
|
|
109
|
+
const PREFERRED_VENDOR_ORDER = listDirectProviders();
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Bucket an alias map by route vendor.
|
|
113
|
+
* INVARIANT: every own key of `aliases` lands in exactly one returned group —
|
|
114
|
+
* there is no whitelist to miss, and the empty vendor is a real catch-all.
|
|
115
|
+
* Order within a group follows the config's own key order.
|
|
116
|
+
* @param {Object<string,string>} aliases
|
|
117
|
+
* @returns {Array<{vendor: string, label: string, keys: string[]}>}
|
|
118
|
+
*/
|
|
119
|
+
function groupAliases(aliases) {
|
|
120
|
+
const byVendor = new Map();
|
|
121
|
+
for (const key of Object.keys(aliases || {})) {
|
|
122
|
+
const vendor = aliasVendorOf(aliases[key]);
|
|
123
|
+
if (!byVendor.has(vendor)) { byVendor.set(vendor, []); }
|
|
124
|
+
byVendor.get(vendor).push(key);
|
|
125
|
+
}
|
|
126
|
+
const rank = (vendor) => {
|
|
127
|
+
if (!vendor) { return Number.MAX_SAFE_INTEGER; } // catch-all group last
|
|
128
|
+
const i = PREFERRED_VENDOR_ORDER.indexOf(vendor);
|
|
129
|
+
return i === -1 ? PREFERRED_VENDOR_ORDER.length : i;
|
|
130
|
+
};
|
|
131
|
+
return Array.from(byVendor.entries())
|
|
132
|
+
.map(([vendor, keys]) => ({ vendor, label: vendorLabel(vendor), keys }))
|
|
133
|
+
.sort((a, b) => rank(a.vendor) - rank(b.vendor) ||
|
|
134
|
+
a.label.toLowerCase().localeCompare(b.label.toLowerCase()));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Heading for the client-side group that holds routes added during THIS
|
|
139
|
+
* wizard session. Exported so the inline script and the tests name the same
|
|
140
|
+
* string.
|
|
141
|
+
*
|
|
142
|
+
* Wording is deliberately non-committal about filing, but the reason is
|
|
143
|
+
* narrower than it once was. It used to be that Step 3 was built from
|
|
144
|
+
* getDefaultAliases(), so a custom alias had no row at all on reopen; that is
|
|
145
|
+
* fixed — electron/setup-ui.js now renders from the effective aliases, and a
|
|
146
|
+
* SAVED custom route is vendor-filed on the next open like any other.
|
|
147
|
+
*
|
|
148
|
+
* What the label still cannot promise is filing WITHIN this session: the page
|
|
149
|
+
* derives no vendors (issue 214 keeps routing policy server-side), so a route
|
|
150
|
+
* added here cannot move into its vendor group until the config round-trips.
|
|
151
|
+
* "this session" is exactly that scope.
|
|
152
|
+
*/
|
|
153
|
+
const NEW_ROUTES_GROUP_LABEL = 'New routes (this session)';
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
ALIAS_VENDOR_LABELS,
|
|
157
|
+
NEW_ROUTES_GROUP_LABEL,
|
|
158
|
+
aliasVendorOf,
|
|
159
|
+
vendorLabel,
|
|
160
|
+
groupAliases,
|
|
161
|
+
};
|
|
@@ -6,12 +6,21 @@
|
|
|
6
6
|
* Extracted from setup-ui.js to keep file sizes under 300 lines.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const { NEW_ROUTES_GROUP_LABEL } = require('./setup-ui-alias-groups');
|
|
10
|
+
|
|
9
11
|
/**
|
|
10
12
|
* Build the alias editor JS for inline inclusion in the wizard script
|
|
11
13
|
* @returns {string} JavaScript source (no <script> tags)
|
|
12
14
|
*/
|
|
13
15
|
function buildAliasScript() {
|
|
16
|
+
// This script runs in the wizard PAGE and cannot require(), so anything it
|
|
17
|
+
// shares with the Node builders is either serialised in as DATA (the group
|
|
18
|
+
// heading below) or not shared at all. The vendor grouping rule is the
|
|
19
|
+
// latter, on purpose: see the SHARED-WITH-THE-BROWSER note in
|
|
20
|
+
// setup-ui-alias-groups.js and the issue 214 guard in tests/setup-ui.test.js.
|
|
14
21
|
return `
|
|
22
|
+
var NEW_ROUTES_GROUP_LABEL = ${JSON.stringify(NEW_ROUTES_GROUP_LABEL)};
|
|
23
|
+
|
|
15
24
|
// Alias editor: search
|
|
16
25
|
var aliasSearchInput = $('alias-search');
|
|
17
26
|
if (aliasSearchInput) {
|
|
@@ -86,15 +95,68 @@ function buildAliasScript() {
|
|
|
86
95
|
}
|
|
87
96
|
});
|
|
88
97
|
}
|
|
89
|
-
//
|
|
98
|
+
// issue 211: the current value is echoed back only because NOTHING in the
|
|
99
|
+
// catalog matched it -- it is not an offer. Rendered bare it read as the
|
|
100
|
+
// one first-class option Amicus recommends (a delisted id outranking 13
|
|
101
|
+
// real ones). Same string, honest framing: its own labelled optgroup.
|
|
90
102
|
if (currentValue && !select.querySelector('option[value="' + CSS.escape(currentValue) + '"]')) {
|
|
103
|
+
var customGroup = document.createElement('optgroup');
|
|
104
|
+
customGroup.label = 'Current \\u2014 not found in catalog';
|
|
91
105
|
var custom = document.createElement('option');
|
|
92
106
|
custom.value = currentValue; custom.textContent = currentValue; custom.selected = true;
|
|
93
|
-
|
|
107
|
+
customGroup.appendChild(custom);
|
|
108
|
+
select.insertBefore(customGroup, select.firstChild);
|
|
94
109
|
}
|
|
95
110
|
return select;
|
|
96
111
|
}
|
|
97
112
|
|
|
113
|
+
// issue 213: a new custom route used to be appended as an ungrouped sibling
|
|
114
|
+
// of every <details>, so it rendered below the last group with no heading at
|
|
115
|
+
// all. It now goes into its own clearly-labelled group. Vendor filing is the
|
|
116
|
+
// SERVER's job (setup-ui-alias-groups.js) -- deriving a vendor here would
|
|
117
|
+
// mean shipping gateway-prefix stripping back into the page, which is what
|
|
118
|
+
// issue 214 removed.
|
|
119
|
+
function placeRowInNewRoutesGroup(row) {
|
|
120
|
+
var editor = document.querySelector('.alias-editor');
|
|
121
|
+
if (!editor) { return; }
|
|
122
|
+
var group = editor.querySelector('.alias-group[data-new-routes]');
|
|
123
|
+
if (!group) {
|
|
124
|
+
group = document.createElement('details');
|
|
125
|
+
group.className = 'alias-group';
|
|
126
|
+
group.setAttribute('data-new-routes', '1');
|
|
127
|
+
var summary = document.createElement('summary');
|
|
128
|
+
var labelEl = document.createElement('span');
|
|
129
|
+
labelEl.textContent = NEW_ROUTES_GROUP_LABEL + ' ';
|
|
130
|
+
var countEl = document.createElement('span');
|
|
131
|
+
countEl.className = 'alias-count';
|
|
132
|
+
summary.appendChild(labelEl); summary.appendChild(countEl);
|
|
133
|
+
group.appendChild(summary);
|
|
134
|
+
editor.insertBefore(group, $('alias-add-btn'));
|
|
135
|
+
}
|
|
136
|
+
group.appendChild(row);
|
|
137
|
+
group.open = true;
|
|
138
|
+
refreshAliasCounts();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Server-rendered counts are static; keep them true after add/remove/delete.
|
|
142
|
+
//
|
|
143
|
+
// Counts EXCLUDE .alias-deleted (council finding A3, PR 221). Deleting a
|
|
144
|
+
// server-rendered row marks it rather than removing it, so counting every
|
|
145
|
+
// .alias-row left the heading claiming rows the user had just struck out.
|
|
146
|
+
// groupAliases can never EMIT an empty group, but a server group can still be
|
|
147
|
+
// emptied here by deleting its last row -- it then honestly reads "(0)"
|
|
148
|
+
// rather than vanishing, because a struck-out row is still on screen and its
|
|
149
|
+
// deletion is not committed until Finish. Only the client-created new-routes
|
|
150
|
+
// group is dropped at zero: its rows are removed outright, so zero means gone.
|
|
151
|
+
function refreshAliasCounts() {
|
|
152
|
+
document.querySelectorAll('.alias-group').forEach(function(g) {
|
|
153
|
+
var rows = g.querySelectorAll('.alias-row:not(.alias-deleted)').length;
|
|
154
|
+
if (rows === 0 && g.hasAttribute('data-new-routes')) { g.remove(); return; }
|
|
155
|
+
var countEl = g.querySelector('.alias-count');
|
|
156
|
+
if (countEl) { countEl.textContent = '(' + rows + ')'; }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
98
160
|
// Alias editor: inline edit
|
|
99
161
|
document.addEventListener('click', function(e) {
|
|
100
162
|
var nameSpan = e.target.closest('.alias-name');
|
|
@@ -164,6 +226,10 @@ function buildAliasScript() {
|
|
|
164
226
|
} else {
|
|
165
227
|
delete aliasEdits[alias];
|
|
166
228
|
}
|
|
229
|
+
// A3: this handler owns SERVER-rendered rows, whose group heading carries a
|
|
230
|
+
// count baked in at render time. Without this the heading kept counting a
|
|
231
|
+
// row the user had just struck out.
|
|
232
|
+
refreshAliasCounts();
|
|
167
233
|
});
|
|
168
234
|
|
|
169
235
|
// Alias editor: add custom shortcut
|
|
@@ -186,8 +252,7 @@ function buildAliasScript() {
|
|
|
186
252
|
row.appendChild(arrow);
|
|
187
253
|
row.appendChild(modelSelect);
|
|
188
254
|
row.appendChild(delBtn);
|
|
189
|
-
|
|
190
|
-
if (editor) { editor.insertBefore(row, addBtn); }
|
|
255
|
+
placeRowInNewRoutesGroup(row);
|
|
191
256
|
nameInput.focus();
|
|
192
257
|
function commitNew() {
|
|
193
258
|
var n = nameInput.value.trim();
|
|
@@ -210,6 +275,7 @@ function buildAliasScript() {
|
|
|
210
275
|
var a = row.getAttribute('data-alias');
|
|
211
276
|
if (a) { delete aliasEdits[a]; }
|
|
212
277
|
row.remove();
|
|
278
|
+
refreshAliasCounts();
|
|
213
279
|
});
|
|
214
280
|
});
|
|
215
281
|
}`;
|
|
@@ -5,42 +5,46 @@
|
|
|
5
5
|
* delete, and add functionality for the setup wizard Step 3.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
{ name: 'Other', keys: ['glm', 'minimax', 'grok', 'kimi', 'seed', 'inkling'] },
|
|
17
|
-
];
|
|
8
|
+
const { groupAliases } = require('./setup-ui-alias-groups');
|
|
9
|
+
|
|
10
|
+
/** Attribute/text-safe rendering of user-controlled alias names and routes. */
|
|
11
|
+
function esc(value) {
|
|
12
|
+
return String(value === undefined || value === null ? '' : value)
|
|
13
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
14
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
15
|
+
}
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
18
|
* Build the HTML fragment for the alias editor section
|
|
19
|
+
*
|
|
20
|
+
* Issue 213: groups are derived from each alias's ROUTE VENDOR
|
|
21
|
+
* (setup-ui-alias-groups.js), not from a hardcoded list of alias names, so
|
|
22
|
+
* EVERY alias in `aliases` renders exactly once -- the old whitelist silently
|
|
23
|
+
* dropped any name it did not list (12 of 25 in a real config).
|
|
24
|
+
*
|
|
21
25
|
* @param {Object<string,string>} aliases - Map of alias name to model string
|
|
22
26
|
* @returns {string} HTML fragment with search, groups, rows, and add button
|
|
23
27
|
*/
|
|
24
28
|
function buildAliasEditorHTML(aliases) {
|
|
25
29
|
const searchInput = '<input type="text" id="alias-search" class="alias-search" placeholder="Search aliases..." autocomplete="off" spellcheck="false">';
|
|
26
30
|
|
|
27
|
-
const groups =
|
|
31
|
+
const groups = groupAliases(aliases).map(group => {
|
|
28
32
|
const rows = group.keys
|
|
29
|
-
.filter(key => aliases[key] !== undefined)
|
|
30
33
|
.map(key => {
|
|
31
34
|
const model = aliases[key];
|
|
32
|
-
return `<div class="alias-row" data-alias="${key}">` +
|
|
33
|
-
`<span class="alias-name">${key}</span>` +
|
|
35
|
+
return `<div class="alias-row" data-alias="${esc(key)}">` +
|
|
36
|
+
`<span class="alias-name">${esc(key)}</span>` +
|
|
34
37
|
'<span class="alias-arrow">\u2192</span>' +
|
|
35
|
-
`<span class="alias-model">${model}</span>` +
|
|
36
|
-
`<button class="alias-delete" data-alias="${key}">\u00d7</button>` +
|
|
38
|
+
`<span class="alias-model">${esc(model)}</span>` +
|
|
39
|
+
`<button class="alias-delete" data-alias="${esc(key)}">\u00d7</button>` +
|
|
37
40
|
'</div>';
|
|
38
41
|
}).join('\n ');
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
// data-vendor records WHICH vendor a group holds, for tests and for anyone
|
|
44
|
+
// inspecting the page. The client does not read it to place rows -- see the
|
|
45
|
+
// SHARED-WITH-THE-BROWSER note in setup-ui-alias-groups.js.
|
|
46
|
+
return `<details class="alias-group" data-vendor="${esc(group.vendor)}">
|
|
47
|
+
<summary>${esc(group.label)} <span class="alias-count">(${group.keys.length})</span></summary>
|
|
44
48
|
${rows}
|
|
45
49
|
</details>`;
|
|
46
50
|
}).join('\n ');
|
|
@@ -82,4 +86,4 @@ function buildAliasEditorHTML(aliases) {
|
|
|
82
86
|
</div>`;
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
module.exports = {
|
|
89
|
+
module.exports = { buildAliasEditorHTML };
|
package/electron/setup-ui.js
CHANGED
|
@@ -22,12 +22,22 @@ const { PROVIDER_FAMILY_NAMES } = require('../src/utils/model-fetcher');
|
|
|
22
22
|
* @param {Object<string,object>} [options.shortlists] - issue 138: per-alias vendor
|
|
23
23
|
* shortlist from buildModelShortlist(), passed through to buildModelStepHTML
|
|
24
24
|
* for the model-level <select>. Defaults to {} (no drill-down rendered).
|
|
25
|
+
* @param {Object<string,string>} [options.aliases] - issue 213: the alias map Step 3
|
|
26
|
+
* renders. Callers pass getEffectiveAliases() (defaults MERGED with the user's
|
|
27
|
+
* config); the default here stays getDefaultAliases() so an omitted option is
|
|
28
|
+
* the old behaviour exactly. This is the second half of issue 213: fixing
|
|
29
|
+
* buildAliasEditorHTML's grouping guarantees "every alias passed in renders
|
|
30
|
+
* exactly once", but the app was only ever passing the 21 built-in defaults,
|
|
31
|
+
* so a user's custom aliases had no row at all. The config that arrives later
|
|
32
|
+
* over IPC cannot repair that -- applyAliasEditsToUI only rewrites the model
|
|
33
|
+
* text of rows that ALREADY exist (`if (!row) { return; }`).
|
|
25
34
|
*/
|
|
26
35
|
function buildSetupHTML(options = {}) {
|
|
27
36
|
const {
|
|
28
37
|
client = 'code-local',
|
|
29
38
|
quickPicks = resolveQuickPicks([]), // pinned fallbacks when not provided
|
|
30
39
|
shortlists = {},
|
|
40
|
+
aliases = getDefaultAliases(), // issue 213
|
|
31
41
|
} = options;
|
|
32
42
|
// Council A1 (PR 215): a pick reaching the page WITHOUT canonicalRoutes makes
|
|
33
43
|
// pickRouteFor fall back to the raw openrouter/... route, which this codebase
|
|
@@ -42,7 +52,7 @@ function buildSetupHTML(options = {}) {
|
|
|
42
52
|
const brandName = getBrandName(client);
|
|
43
53
|
const keysHtml = buildKeysStepHTML(PROVIDERS);
|
|
44
54
|
const modelHtml = buildModelStepHTML(picks, undefined, undefined, shortlists);
|
|
45
|
-
const aliasHtml = buildAliasEditorHTML(
|
|
55
|
+
const aliasHtml = buildAliasEditorHTML(aliases);
|
|
46
56
|
const css = buildWizardCSS();
|
|
47
57
|
const providersJson = JSON.stringify(PROVIDERS);
|
|
48
58
|
const modelChoicesJson = JSON.stringify(picks);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amicus",
|
|
3
|
-
"version": "4.9.
|
|
3
|
+
"version": "4.9.4",
|
|
4
4
|
"mcpName": "io.github.BourbonDog/amicus",
|
|
5
5
|
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
6
6
|
"keywords": [
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"count": { "type": "number" },
|
|
15
15
|
"models": { "type": "array", "items": { "type": "object" } },
|
|
16
16
|
"lastRefreshAttempt": { "type": ["number", "null"] },
|
|
17
|
-
"lastRefreshError": { "type": ["string", "null"] }
|
|
17
|
+
"lastRefreshError": { "type": ["string", "null"] },
|
|
18
|
+
"ceilingEnrichment": { "type": ["object", "null"] }
|
|
18
19
|
}
|
|
19
20
|
}
|
package/schemas/run.schema.json
CHANGED
|
@@ -24,6 +24,19 @@
|
|
|
24
24
|
"usage": { "type": ["object", "null"] },
|
|
25
25
|
"pack": { "type": "object" },
|
|
26
26
|
"tag": { "type": "string" },
|
|
27
|
+
"finish": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"description": "#218 PR 3, optional. The engine's finish reason for the leg's last assistant message ('stop', 'length', 'tool-calls', …), copied verbatim from the assistant message's `finish`. EMIT-WHEN-SET like `ttftMs`: absent means the engine recorded none (the message never finalized, or an older engine). 'length' means the provider stopped at the max_tokens reservation — with answer text that is a cut review (announced as a Note on the `output-truncated` channel); without, the leg is status 'error' with an `OUTPUT_LENGTH:` reason."
|
|
30
|
+
},
|
|
31
|
+
"variant": {
|
|
32
|
+
"type": "string",
|
|
33
|
+
"description": "#218 PR 4, optional. The effort level amicus SENT as the engine's `variant` prompt field for this leg, after checking the engine's own declaration for the model (/config/providers). EMIT-WHEN-SENT: absent means no --thinking was requested, or the request was refused before anything was sent (then `status` is 'error' and `error` starts VARIANT_UNDECLARED or VARIANT_OVER_BUDGET — prefixed `Session setup failed: ` on an interactive `amicus start`). Not the engine's own `variant` echo, which it records even for a level it dropped (probe F3/M7). A leg whose no-output backstop fired before the prompt send resolved carries no `variant` even though one went out — the send result never arrived, so the record cannot claim it."
|
|
34
|
+
},
|
|
35
|
+
"variantUnverified": {
|
|
36
|
+
"type": "boolean",
|
|
37
|
+
"enum": [true],
|
|
38
|
+
"description": "#218 PR 4, optional, emit-when-true. The variant was sent to a model the engine's catalogue did not know within the wait (its row carries none of the cells a catalogue fills — no display name, family, release date, pricing or capabilities — only the descriptor amicus registered: a model newer than the engine's bundled catalogue before its startup refresh landed, or a custom/local model): the engine applied it only if it learned the model before building the request, and dropped it silently otherwise; amicus cannot tell which."
|
|
39
|
+
},
|
|
27
40
|
"ttftMs": {
|
|
28
41
|
"type": "integer",
|
|
29
42
|
"minimum": 0,
|
package/skills/sidecar/SKILL.md
CHANGED
|
@@ -236,14 +236,7 @@ amicus start \
|
|
|
236
236
|
- `--context-turns <N>`: Max conversation turns to include (default: 50)
|
|
237
237
|
- `--context-since <duration>`: Time filter for context (e.g., `2h`, `30m`, `1d`). Overrides `--context-turns`.
|
|
238
238
|
- `--context-max-tokens <N>`: Max context size (default: 80000)
|
|
239
|
-
- `--thinking <level>`:
|
|
240
|
-
- `none` - No extended thinking
|
|
241
|
-
- `minimal` - Minimal thinking (may be adjusted if unsupported by model)
|
|
242
|
-
- `low` - Low thinking effort
|
|
243
|
-
- `medium` - Medium thinking effort (default)
|
|
244
|
-
- `high` - High thinking effort
|
|
245
|
-
- `xhigh` - Extra high thinking effort
|
|
246
|
-
Note: If the model doesn't support the specified level, it will be automatically adjusted.
|
|
239
|
+
- `--thinking <level>`: Reasoning effort — `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max` (the levels the curated routes declare between them). Omit it for the provider's own default effort (nothing is sent then). A level the model does not declare is refused before anything is sent (`VARIANT_UNDECLARED`, naming the declared set); a declared level whose thinking budget would push the reservation over `outputBudget` on the direct Anthropic route is refused too (`VARIANT_OVER_BUDGET`); a model the engine's catalogue does not know in time is sent the level unverified (`variantUnverified: true` on the record). Nothing is adjusted.
|
|
247
240
|
- `--summary-length <length>`: Summary verbosity:
|
|
248
241
|
- `brief` - Concise summary
|
|
249
242
|
- `normal` - Standard summary (default)
|
|
@@ -14,9 +14,12 @@ const electronMcpCheck = require('./utils/doctor-electron-mcp-check');
|
|
|
14
14
|
const localProvidersCheck = require('./utils/doctor-local-providers-check');
|
|
15
15
|
// v4.6.2 PR1 (spec §4) — the 'anthropic-base-url' check body.
|
|
16
16
|
const baseUrlCheck = require('./utils/doctor-base-url-check');
|
|
17
|
+
const outputBudgetCheck = require('./utils/doctor-output-budget-check'); // #218 PR 2 — the 'output-budget' row.
|
|
17
18
|
// B3 (council review of PR 198, issue 195) — the 'aliases' check body,
|
|
18
19
|
// including its --fix repair of fabricated bare ids. Same split rationale.
|
|
19
20
|
const aliasCheck = require('./utils/doctor-alias-check');
|
|
21
|
+
const creditCheck = require('./utils/doctor-credit-check');
|
|
22
|
+
const keyAuthCheck = require('./utils/doctor-key-auth-check'); // #210 — 'keys' tests presence only; this re-validates.
|
|
20
23
|
|
|
21
24
|
const { DEFAULT_MAX_AGE_MS: MAX_CATALOG_AGE_MS } = require('./utils/model-catalog'); // 24h — single source
|
|
22
25
|
|
|
@@ -32,7 +35,8 @@ function realDeps() {
|
|
|
32
35
|
nodeVersion: process.version,
|
|
33
36
|
readApiKeys: () => require('./utils/api-key-store').readApiKeys(),
|
|
34
37
|
readApiKeyValues: () => require('./utils/api-key-store').readApiKeyValues(),
|
|
35
|
-
checkOpenRouterCredit: (key) =>
|
|
38
|
+
checkOpenRouterCredit: (key) => keyAuthCheck.probeOpenRouterCredit(key), // #210 — same gate as validateApiKey
|
|
39
|
+
validateApiKey: (p, k) => keyAuthCheck.probeApiKey(p, k), // #210
|
|
36
40
|
getCwd: () => process.cwd(),
|
|
37
41
|
readProjectMarkers: (dir) => {
|
|
38
42
|
const exists = (name) => { try { return fs.existsSync(path.join(dir, name)); } catch (_e) { return false; } };
|
|
@@ -42,6 +46,7 @@ function realDeps() {
|
|
|
42
46
|
resolveModel: () => require('./utils/config').resolveModel(),
|
|
43
47
|
readCache: () => require('./utils/model-catalog').readCache(),
|
|
44
48
|
collectAliasSources: () => require('./utils/alias-audit').collectAliasSources(),
|
|
49
|
+
readOutputBudgetRaw: () => (require('./utils/config').loadConfig() || {}).outputBudget, // #218 PR 2: as stored, so a malformed value is echoed
|
|
45
50
|
findStaleAliases: (s, c) => require('./utils/alias-audit').findStaleAliases(s, c),
|
|
46
51
|
findDriftedStoredAliases: (s, c) => require('./utils/alias-audit').findDriftedStoredAliases(s, c),
|
|
47
52
|
// B3: the narrow fabricated-bare-id repair class (pure detection) + the
|
|
@@ -141,6 +146,7 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
141
146
|
: { id: 'keys', name: 'API keys', status: 'error', message: 'no provider keys configured', hint: 'amicus key <provider> <key> (or run: amicus setup)' };
|
|
142
147
|
}));
|
|
143
148
|
|
|
149
|
+
checks.push(await guardAsync('key-auth', 'API key auth', () => keyAuthCheck.evaluateKeyAuth(d))); // #210
|
|
144
150
|
checks.push((() => {
|
|
145
151
|
try {
|
|
146
152
|
const model = d.resolveModel();
|
|
@@ -167,6 +173,7 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
167
173
|
// utils/doctor-alias-check.js for the check body and utils/alias-audit.js's
|
|
168
174
|
// findFabricatedAliasRepairs for the detection rule.
|
|
169
175
|
checks.push(guard('aliases', 'Model aliases', () => aliasCheck.evaluateAliasesCheck(d)));
|
|
176
|
+
checks.push(guard('output-budget', 'Output budget', () => outputBudgetCheck.evaluateOutputBudget(d))); // #218 PR 2
|
|
170
177
|
|
|
171
178
|
checks.push(guard('anthropic-base-url', 'ANTHROPIC_BASE_URL',
|
|
172
179
|
() => baseUrlCheck.evaluateAnthropicBaseUrl(d)));
|
|
@@ -211,21 +218,10 @@ async function runDoctorChecks(depsOverride = {}) {
|
|
|
211
218
|
|
|
212
219
|
checks.push(guard('session-metadata-tmp', 'Session metadata tmp files', () => metaSweep.evaluateSessionMetadataTmpSweep(d)));
|
|
213
220
|
|
|
214
|
-
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
if (!key) {
|
|
219
|
-
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'ok', message: 'no OpenRouter key — skipped', hint: null };
|
|
220
|
-
}
|
|
221
|
-
// Reuses the #38 non-blocking probe; resolves warning:null on any failure.
|
|
222
|
-
const res = (await d.checkOpenRouterCredit(key)) || {};
|
|
223
|
-
if (res.warning) {
|
|
224
|
-
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'warn', message: res.warning, hint: 'Add credit at openrouter.ai/credits, or build a free council (amicus setup → option 2).' };
|
|
225
|
-
}
|
|
226
|
-
const remaining = (typeof res.limitRemaining === 'number') ? ` ($${res.limitRemaining} remaining)` : '';
|
|
227
|
-
return { id: 'openrouter-credit', name: 'OpenRouter credit', status: 'ok', message: `credit ok${remaining}`, hint: null };
|
|
228
|
-
}));
|
|
221
|
+
// #43: OpenRouter credit/free-tier — warns (never errors); skipped when no
|
|
222
|
+
// key. Body in utils/doctor-credit-check.js (same split as the others).
|
|
223
|
+
checks.push(await guardAsync('openrouter-credit', 'OpenRouter credit',
|
|
224
|
+
() => creditCheck.evaluateOpenRouterCredit(d)));
|
|
229
225
|
|
|
230
226
|
// v4.2 §4.7 C8: configured local / OpenAI-compatible providers (Ollama, LM
|
|
231
227
|
// Studio, vLLM, generic) — reachability only; warn, never error (a napping
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
const { validateTaskId, validateTag } = require('./utils/validators');
|
|
11
|
+
const { validateTaskId, validateTag, validateThinkingLevel } = require('./utils/validators');
|
|
12
12
|
const { failJson, ERROR_CODES } = require('./utils/error-doc');
|
|
13
13
|
const { GATEWAY_MODES } = require('./utils/model-descriptor');
|
|
14
14
|
const { applyTemplateForArgs } = require('./cli-template-args');
|
|
@@ -54,6 +54,15 @@ async function handleFanout(args) {
|
|
|
54
54
|
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: `Error: --gateway must be one of: ${GATEWAY_MODES.join(', ')}` }));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
// #218 PR 4 whole-branch review (VCMD-2): the same vocabulary check `start` runs in
|
|
58
|
+
// validateStartArgs (cli.js) — fanout never did, so a typo (or a pack's) reached sendPrompt:
|
|
59
|
+
// sent unverified to an unknown model, a per-leg VARIANT_UNDECLARED after the spawn on a
|
|
60
|
+
// known one. Named mutant "FANOUTTHINKINGUNCHECKED" (tests/fanout-cli.test.js): drop this block.
|
|
61
|
+
const thinkingCheck = validateThinkingLevel(args.thinking);
|
|
62
|
+
if (!thinkingCheck.valid) {
|
|
63
|
+
process.exit(failJson(useJson, { code: ERROR_CODES.BAD_ARGS, message: thinkingCheck.error }));
|
|
64
|
+
}
|
|
65
|
+
|
|
57
66
|
const { resolvePromptSource } = require('./utils/prompt-source');
|
|
58
67
|
let promptRes;
|
|
59
68
|
if (args.prompt !== undefined || args['prompt-file'] !== undefined || args.template === undefined) {
|
|
@@ -32,6 +32,21 @@ async function handleResume(args) {
|
|
|
32
32
|
message: 'Error: --tag is not supported on resume — the tag is inherited from the parent session',
|
|
33
33
|
}));
|
|
34
34
|
}
|
|
35
|
+
// #218 PR 4, council #235 r1 (D6): the same silently-ignored-flag shape as --tag
|
|
36
|
+
// above. `--thinking` parses on every command because getKnownFlags()
|
|
37
|
+
// (utils/known-flags.js) scrapes the whole usage string and start's block declares
|
|
38
|
+
// it (cli.js), this handler never reads it, and validateStartArgs — PR 4's
|
|
39
|
+
// vocabulary check — runs only on `start` (cli-handlers-run.js), so even
|
|
40
|
+
// `--thinking bogus` used to exit 0 here having done nothing. Before PR 4 the level
|
|
41
|
+
// reached nothing on any path (probe F1) and the commands behaved alike by accident;
|
|
42
|
+
// it is the engine's `variant` on start/fanout now. Named mutant
|
|
43
|
+
// "RESUMETHINKINGSILENT" (tests/cli-handlers-resume-continue.test.js): drop this guard.
|
|
44
|
+
if (args.thinking !== undefined) {
|
|
45
|
+
process.exit(failJson(useJson, {
|
|
46
|
+
code: ERROR_CODES.BAD_ARGS,
|
|
47
|
+
message: 'Error: --thinking is not supported on resume — the level belongs to the run that started the session, and resume reopens that session',
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
35
50
|
const taskId = requireValidTaskId(args, useJson, 'resume', 'Usage: amicus resume <task_id>');
|
|
36
51
|
requireNoUiForJson(args, useJson);
|
|
37
52
|
|
|
@@ -71,6 +86,16 @@ async function handleContinue(args) {
|
|
|
71
86
|
message: 'Error: --tag is not supported on continue — the tag is inherited from the parent session',
|
|
72
87
|
}));
|
|
73
88
|
}
|
|
89
|
+
// #218 PR 4, council #235 r1 (D6): same rationale as handleResume above — the flag
|
|
90
|
+
// parses here, is read by nobody, and is not even vocabulary-checked. A continuation
|
|
91
|
+
// is a NEW session, so a level for it belongs on the `start` that opens one. Named
|
|
92
|
+
// mutant "CONTINUETHINKINGSILENT" (tests/cli-handlers-resume-continue.test.js).
|
|
93
|
+
if (args.thinking !== undefined) {
|
|
94
|
+
process.exit(failJson(useJson, {
|
|
95
|
+
code: ERROR_CODES.BAD_ARGS,
|
|
96
|
+
message: 'Error: --thinking is not supported on continue — the level belongs to the run that starts a session; use `amicus start --thinking <level>` to open one with a level',
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
74
99
|
const taskId = requireValidTaskId(args, useJson, 'continue', 'Usage: amicus continue <task_id> --prompt "..."');
|
|
75
100
|
|
|
76
101
|
// BL-1: accept --prompt-file (XOR --prompt) so the MCP handler can pass a long
|
package/src/cli-handlers.js
CHANGED
|
@@ -176,7 +176,23 @@ async function handleKey(args) {
|
|
|
176
176
|
|
|
177
177
|
console.log(`Validating ${provider} key...`);
|
|
178
178
|
const validation = await validateApiKey(provider, keyArg);
|
|
179
|
-
|
|
179
|
+
// 401 is the only status that means "this credential is not accepted".
|
|
180
|
+
// Everything else — 403 (disabled API, quota, region/bot block), 429, any
|
|
181
|
+
// 5xx, a 404 from a moved endpoint, a Cloudflare 52x during an origin
|
|
182
|
+
// outage, or no status at all because the machine is offline — says
|
|
183
|
+
// something about the REQUEST, not the key. Refusing to save on those is
|
|
184
|
+
// the false ALARM the doctor classifier stopped raising.
|
|
185
|
+
//
|
|
186
|
+
// ⚠️ An ALLOWLIST of what blocks, deliberately. This was a blocklist of
|
|
187
|
+
// what does NOT block, which left every unenumerated status falling through
|
|
188
|
+
// to process.exit(1) while the comment above it claimed only 401 blocked —
|
|
189
|
+
// the code and the narrative disagreed, and the narrative was the nicer of
|
|
190
|
+
// the two. An allowlist cannot rot as new status codes appear.
|
|
191
|
+
const BLOCKS_SAVE = new Set([401]);
|
|
192
|
+
if (!validation.valid && !BLOCKS_SAVE.has(validation.status)) {
|
|
193
|
+
console.warn(`Warning: ${validation.error}`);
|
|
194
|
+
console.warn('Saving the key anyway — run `amicus doctor` to re-check it later.');
|
|
195
|
+
} else if (!validation.valid) {
|
|
180
196
|
console.error(`Error: ${validation.error}`);
|
|
181
197
|
process.exit(1);
|
|
182
198
|
}
|
package/src/cli.js
CHANGED
|
@@ -326,16 +326,13 @@ function validateStartArgs(args) {
|
|
|
326
326
|
return { valid: false, error: `Error: --summary-length must be one of: ${validSummaryLengths.join(', ')}` };
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
-
// Validate thinking
|
|
330
|
-
|
|
329
|
+
// Validate the thinking level's VOCABULARY (#218 PR 4). Whether the model
|
|
330
|
+
// declares it is checked against the engine's own catalogue at send time
|
|
331
|
+
// (opencode-client.js :: sendPrompt); nothing is adjusted here any more.
|
|
332
|
+
const thinkingCheck = validateThinkingLevel(args.thinking);
|
|
331
333
|
if (!thinkingCheck.valid) {
|
|
332
334
|
return thinkingCheck;
|
|
333
335
|
}
|
|
334
|
-
// If model doesn't support the level, adjust it and warn
|
|
335
|
-
if (thinkingCheck.warning) {
|
|
336
|
-
logger.warn('Thinking level adjusted', { warning: thinkingCheck.warning, adjustedLevel: thinkingCheck.adjustedLevel });
|
|
337
|
-
args.thinking = thinkingCheck.adjustedLevel;
|
|
338
|
-
}
|
|
339
336
|
|
|
340
337
|
// Validate API key is present for the model's provider
|
|
341
338
|
const apiKeyCheck = validateApiKey(args.model);
|
|
@@ -444,7 +441,7 @@ Options for 'start':
|
|
|
444
441
|
--context-since <duration> Time filter (e.g., 2h). Overrides turns.
|
|
445
442
|
--context-max-tokens <N> Max context tokens (default: 80000)
|
|
446
443
|
--summary-length <length> Summary verbosity: brief, normal (default), verbose
|
|
447
|
-
--thinking <level> Reasoning effort: none, minimal, low, medium, high, xhigh
|
|
444
|
+
--thinking <level> Reasoning effort: none, minimal, low, medium, high, xhigh, max (omit for the provider's default)
|
|
448
445
|
--mcp <spec> Add MCP server. Formats:
|
|
449
446
|
- name=url (remote server)
|
|
450
447
|
- name=command (local server)
|