bareloop 0.40.0 → 0.41.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/CHANGELOG.md +33 -0
- package/bareloop.context.md +97 -124
- package/package.json +2 -2
- package/src/authorflow.js +9 -9
- package/src/authorjob.js +4 -4
- package/src/authorrun.js +25 -25
- package/src/authorscout.js +4 -4
- package/src/bundle.js +7 -4
- package/src/bundlerun.js +4 -1
- package/src/cli.js +1 -1
- package/src/config.js +129 -29
- package/src/draftspend.js +4 -4
- package/src/index.js +2 -4
- package/src/interviewrun.js +17 -25
- package/src/job.js +13 -7
- package/src/judged.js +11 -43
- package/src/keysfile.js +15 -7
- package/src/kinds.js +2 -2
- package/src/modellist.js +84 -0
- package/src/monthly.js +23 -43
- package/src/panel/authorroutes.js +19 -14
- package/src/panel/authorsession.js +62 -58
- package/src/panel/index.html +520 -145
- package/src/panel/runroutes.js +3 -0
- package/src/panel/server.js +35 -15
- package/src/panel/settingsroutes.js +232 -173
- package/src/planrun.js +2 -2
- package/src/providerrows.js +226 -141
- package/src/providers.js +58 -332
- package/src/providershapes.js +18 -0
- package/src/replay.js +1 -1
- package/src/thinking.js +130 -0
- package/src/userrun.js +50 -73
- package/types/authorflow.d.ts +5 -5
- package/types/authorjob.d.ts +2 -2
- package/types/authorscout.d.ts +2 -2
- package/types/config.d.ts +23 -6
- package/types/draftspend.d.ts +8 -8
- package/types/index.d.ts +1 -1
- package/types/job.d.ts +5 -5
- package/types/judged.d.ts +5 -36
- package/types/keysfile.d.ts +15 -4
- package/types/modellist.d.ts +50 -0
- package/types/monthly.d.ts +4 -3
- package/types/panel/authorsession.d.ts +2 -9
- package/types/panel/server.d.ts +14 -4
- package/types/providerrows.d.ts +192 -99
- package/types/providers.d.ts +34 -159
- package/types/providershapes.d.ts +10 -0
- package/types/thinking.d.ts +90 -0
- package/types/userrun.d.ts +6 -1
package/src/providers.js
CHANGED
|
@@ -21,62 +21,12 @@
|
|
|
21
21
|
// (`buildRunnerProviders`, below); it never prices a round (F113: rates
|
|
22
22
|
// passthrough is dead), and never decides a budget.
|
|
23
23
|
|
|
24
|
+
import { listModels } from './modellist.js';
|
|
25
|
+
import { SHAPE_IDS } from './providershapes.js';
|
|
24
26
|
import { createRequire } from 'node:module';
|
|
25
27
|
|
|
26
28
|
const require = createRequire(import.meta.url);
|
|
27
|
-
const { AnthropicProvider, OpenAIProvider, GeminiProvider
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Today's anthropic-api tier table — EXACTLY the `DEFAULT_TIER_MODELS` both
|
|
31
|
-
* runners hardcoded before this factory existed. Any change to these two
|
|
32
|
-
* values is a change to shipped anthropic-api behaviour, not a refactor.
|
|
33
|
-
*/
|
|
34
|
-
export const ANTHROPIC_TIER_MODELS = Object.freeze({
|
|
35
|
-
sonnet: 'claude-sonnet-5',
|
|
36
|
-
haiku: 'claude-haiku-4-5-20251001',
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* DeepSeek (F149/F150) is a SINGLE model, not a tiered family — hamr's
|
|
41
|
-
* ruling above: one secondary provider, not a menu. Both tiers map to the
|
|
42
|
-
* same model id ON PURPOSE: `resolveWorkerModel`/a runner's `providerFor
|
|
43
|
-
* (tier)` only ever LOOK UP a tier's model id (src/job.js:818-827,
|
|
44
|
-
* scripts/run-u.mjs's `providerFor`) — neither assumes the tiers name
|
|
45
|
-
* different models — so repeating the one id is the honest representation
|
|
46
|
-
* of a single-model provider, not a second model masquerading as `haiku`.
|
|
47
|
-
* If DeepSeek (or a future openai-api entrant) ever ships a genuinely
|
|
48
|
-
* cheaper tier model, this is where it would earn its own `haiku` id.
|
|
49
|
-
*
|
|
50
|
-
* MODEL SWAP (F171, item 34 L16, hamr 2026-09-13): `deepseek-chat` is no
|
|
51
|
-
* longer served — a live `/models` call returned only `deepseek-flash` and
|
|
52
|
-
* `deepseek-v4-pro`. Switched to `deepseek-flash` (DeepSeek V4.1). F149's
|
|
53
|
-
* `legacyMaxTokens` measurement (asked 64 output tokens, got 665/608) was
|
|
54
|
-
* taken on `deepseek-chat` and is UNVERIFIED on `deepseek-flash` — kept on
|
|
55
|
-
* below as the fail-safe (over-capping direction is safe; see F171).
|
|
56
|
-
*/
|
|
57
|
-
export const OPENAI_TIER_MODELS = Object.freeze({
|
|
58
|
-
sonnet: 'deepseek-flash',
|
|
59
|
-
haiku: 'deepseek-flash',
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Gemini (PRD item 31.3, hamr 2026-09-09: "anthropic, openai, gemini drop
|
|
64
|
-
* ollama for now"). Two real tiers, unlike DeepSeek's single model: `flash` is
|
|
65
|
-
* the cheap tier by design, so `haiku` maps to it honestly rather than
|
|
66
|
-
* repeating one id.
|
|
67
|
-
*
|
|
68
|
-
* ADMITTED-PENDING-PROBE. The probe rule stands (PRD item 28 ruling (d),
|
|
69
|
-
* 2026-09-09): no endpoint is in the menu without its own clean paid probe,
|
|
70
|
-
* and `gemini-api` has ZERO runs. `anthropic-api` is the original (~167
|
|
71
|
-
* archived runs) and `openai-api` was probed green through the shipped runner
|
|
72
|
-
* (run mtu12vks, $1.64/$4, F157); gemini owes that fire and has not paid it.
|
|
73
|
-
* `PROBE_STATUS` below is the machine-readable form of that debt — nothing in
|
|
74
|
-
* here silently pretends the provider is proven.
|
|
75
|
-
*/
|
|
76
|
-
export const GEMINI_TIER_MODELS = Object.freeze({
|
|
77
|
-
sonnet: 'gemini-2.5-pro',
|
|
78
|
-
haiku: 'gemini-2.5-flash',
|
|
79
|
-
});
|
|
29
|
+
const { AnthropicProvider, OpenAIProvider, GeminiProvider } = require('bare-agent/providers');
|
|
80
30
|
|
|
81
31
|
/**
|
|
82
32
|
* Which admitted providers have paid for their own end-to-end probe, and which
|
|
@@ -91,146 +41,10 @@ export const PROBE_STATUS = Object.freeze({
|
|
|
91
41
|
'gemini-api': Object.freeze({ probed: false, evidence: 'ZERO runs — admitted to the menu, owes its own paid probe (PRD item 31.3)' }),
|
|
92
42
|
});
|
|
93
43
|
|
|
94
|
-
/**
|
|
95
|
-
* Per-MODEL request-key gating (F149, live-measured 2026-09-09): DeepSeek
|
|
96
|
-
* silently IGNORES the modern `max_completion_tokens` key — asked for 64
|
|
97
|
-
* output tokens, it returned 665/608 — and only honours the legacy
|
|
98
|
-
* `max_tokens` key. `bare-agent`'s `OpenAIProvider` already exposes the
|
|
99
|
-
* switch (`legacyMaxTokens`, BA-24; no model-name sniffing inside
|
|
100
|
-
* bare-agent itself — "the caller declares the dialect"), so THIS table is
|
|
101
|
-
* bareloop's routing of it, keyed per model, never a global default. An
|
|
102
|
-
* output cap that does not bind is a money hazard: a reasoning model can
|
|
103
|
-
* run to its own ceiling on every round, silently, so getting this wrong
|
|
104
|
-
* for even one model defeats every budget that model's tier sets.
|
|
105
|
-
*
|
|
106
|
-
* A model with no entry here gets `{}` — bare-agent's own default
|
|
107
|
-
* (`max_completion_tokens`, GPT-5-safe) applies, unmodified.
|
|
108
|
-
*
|
|
109
|
-
* F171 (2026-09-13): `deepseek-chat` was retired and swapped for
|
|
110
|
-
* `deepseek-flash` (item 34 L16). This gating was measured on
|
|
111
|
-
* `deepseek-chat` only and carried forward onto `deepseek-flash`
|
|
112
|
-
* UNVERIFIED — re-verify before any paid DeepSeek run relies on it.
|
|
113
|
-
*/
|
|
114
|
-
const OPENAI_MODEL_OPTIONS = Object.freeze({
|
|
115
|
-
'deepseek-flash': Object.freeze({ legacyMaxTokens: true }),
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* THINKING (hamr 2026-10-11: no hardcoded per-model lists in bareloop — bare-agent >=0.51 owns the shapes). The drafter and the
|
|
120
|
-
* rubric judge run thinking OFF by default (deepseek-flash thinks by default and bills the reasoning as output: F192 run mv117wde
|
|
121
|
-
* truncated judge locates at the cap, and a drafter answer that runs to the output cap is a failed try). This module is the ONE
|
|
122
|
-
* owner of what a call sends: it asks bare-agent's `thinkingSupport(kind, model)` (pure lookup, no network) and sends the
|
|
123
|
-
* normalized per-call `thinkingMode: 'off'` ONLY when the model is measured switchable. A model measured NOT switchable
|
|
124
|
-
* (claude-opus-5-5 …) gets nothing — asking would throw before send — and a model bare-agent has not measured ('unverified') also
|
|
125
|
-
* gets nothing on this DEFAULT path, so a local/unknown model's drafting never breaks by default. PER-CALL, never constructor
|
|
126
|
-
* options (the drafter's provider is the worker's instance; the worker is deliberately left as it was). The raw `thinking`
|
|
127
|
-
* option is never sent (bare-agent throws if both are set).
|
|
128
|
-
*/
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The thinking-ladder kind bare-agent's `thinkingSupport` is keyed by, from a constructed provider; null for anything that is
|
|
132
|
-
* not one of the three admitted constructors (a test double, a clipipe) — such a provider is never sent the switch.
|
|
133
|
-
* OpenAI-shaped providers are DeepSeek by model id `deepseek-*` or a `deepseek.com` baseUrl (bare-agent's documented rule,
|
|
134
|
-
* provider-openai.js:80), else OpenAI.
|
|
135
|
-
* @param {any} provider
|
|
136
|
-
* @returns {'anthropic'|'openai'|'deepseek'|'gemini'|null}
|
|
137
|
-
*/
|
|
138
|
-
function thinkingKind(provider) {
|
|
139
|
-
if (provider instanceof AnthropicProvider) return 'anthropic';
|
|
140
|
-
if (provider instanceof GeminiProvider) return 'gemini';
|
|
141
|
-
if (provider instanceof OpenAIProvider) {
|
|
142
|
-
return /^deepseek-/i.test(String(provider.model)) || /deepseek\.com/i.test(String(provider.baseUrl)) ? 'deepseek' : 'openai';
|
|
143
|
-
}
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* What an 'off' request means for this provider instance, for the draft log and the panel row: `'off'` (measured switchable —
|
|
149
|
-
* the switch is sent), `'not-switchable'` (measured NOT switchable — nothing sent, the model thinks as it would),
|
|
150
|
-
* `'unverified'` (bare-agent has not measured this model — nothing sent on the default path).
|
|
151
|
-
* @param {any} provider a constructed bare-agent provider
|
|
152
|
-
* @returns {'off'|'not-switchable'|'unverified'}
|
|
153
|
-
*/
|
|
154
|
-
function offOutcome(provider) {
|
|
155
|
-
const kind = thinkingKind(provider);
|
|
156
|
-
if (kind === null) return 'not-switchable';
|
|
157
|
-
const support = thinkingSupport(kind, provider.model);
|
|
158
|
-
if (support.switchable === true) return 'off';
|
|
159
|
-
return support.switchable === 'unverified' ? 'unverified' : 'not-switchable';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* The per-call options the rubric JUDGE's model calls carry for this provider instance: `{ thinkingMode: 'off' }` when
|
|
164
|
-
* bare-agent measures the model switchable, else `{}` (F192: judge runs thinking off; a judge on a model that cannot turn it
|
|
165
|
-
* off must not throw).
|
|
166
|
-
* @param {any} provider a constructed bare-agent provider
|
|
167
|
-
* @returns {Record<string, any>}
|
|
168
|
-
*/
|
|
169
|
-
export function judgeCallOptions(provider) {
|
|
170
|
-
return offOutcome(provider) === 'off' ? { thinkingMode: 'off' } : {};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* The drafter's THINKING knob — an enumerated OPERATOR param, never model output and never read from env inside the library.
|
|
175
|
-
* `'on'` sends nothing (the model's own thinking, byte-identical to a no-knob call); `'off'` sends `thinkingMode: 'off'` where
|
|
176
|
-
* bare-agent measures the model switchable. The DEFAULT is `'off'` (hamr 2026-10-11, decided by the frozen ON/OFF contrast,
|
|
177
|
-
* docs/logs/2026-10-10-drafter-thinking-contrast-prereg.md); `'on'` stays selectable for a later Settings checkbox.
|
|
178
|
-
*/
|
|
179
|
-
export const DRAFT_THINKING_MODES = Object.freeze(['on', 'off']);
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Throw unless `mode` is one of {@link DRAFT_THINKING_MODES} — an operator-param guard, so a typo never silently means a mode.
|
|
183
|
-
* @param {unknown} mode
|
|
184
|
-
* @returns {'on'|'off'}
|
|
185
|
-
*/
|
|
186
|
-
export function assertDraftThinking(mode) {
|
|
187
|
-
if (mode === 'on' || mode === 'off') return mode;
|
|
188
|
-
throw new Error(`[providers] draftThinking must be one of ${DRAFT_THINKING_MODES.join(' | ')}, got ${JSON.stringify(mode)}`);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* What the drafting calls on this provider ACTUALLY carry for thinking, for the draft log and the panel row: `'on'` (mode on,
|
|
193
|
-
* nothing sent), `'off'` (mode off and the switch is sent), `'not-switchable'` (mode off, model measured not switchable —
|
|
194
|
-
* nothing sent), `'unverified'` (mode off, model not measured by bare-agent — nothing sent). Never re-derives a model list.
|
|
195
|
-
* @param {any} provider a constructed bare-agent provider
|
|
196
|
-
* @param {unknown} [mode] 'on' | 'off' (default 'off')
|
|
197
|
-
* @returns {'on'|'off'|'not-switchable'|'unverified'}
|
|
198
|
-
*/
|
|
199
|
-
export function draftThinkingSent(provider, mode = 'off') {
|
|
200
|
-
if (assertDraftThinking(mode) === 'on') return 'on';
|
|
201
|
-
return offOutcome(provider);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* {@link draftThinkingSent} for a model the caller has only NAMED (provider shape, model id, baseUrl) — no key, no network. For
|
|
206
|
-
* the panel's first drafting row, which is shown before the key is read. Constructing the provider only makes the class and
|
|
207
|
-
* the model id available to `thinkingSupport`; it sends nothing.
|
|
208
|
-
* @param {{providerName: string, model: string, baseUrl?: string|null}} o
|
|
209
|
-
* @param {unknown} [mode] 'on' | 'off' (default 'off')
|
|
210
|
-
* @returns {'on'|'off'|'not-switchable'|'unverified'}
|
|
211
|
-
*/
|
|
212
|
-
export function draftThinkingFor({ providerName, model, baseUrl }, mode = 'off') {
|
|
213
|
-
return draftThinkingSent(makeProvider(providerName, { apiKey: 'unset', model, ...(baseUrl ? { baseUrl } : {}) }), mode);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* The ONE owner of the per-call options every DRAFTING model call carries (scout, confirm, author, revise, structure retries).
|
|
218
|
-
* `{ thinkingMode: 'off' }` exactly when {@link draftThinkingSent} says `'off'`, else `{}`.
|
|
219
|
-
* @param {any} provider a constructed bare-agent provider
|
|
220
|
-
* @param {unknown} [mode] 'on' | 'off' (default 'off')
|
|
221
|
-
* @returns {Record<string, any>}
|
|
222
|
-
*/
|
|
223
|
-
export function draftCallOptions(provider, mode = 'off') {
|
|
224
|
-
return draftThinkingSent(provider, mode) === 'off' ? { thinkingMode: 'off' } : {};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
44
|
/**
|
|
228
45
|
* @typedef {object} ProviderTableEntry
|
|
229
46
|
* @property {new (options: any) => any} ctor the bare-agent provider class
|
|
230
|
-
* @property {string} envKey the environment variable this provider reads its key from
|
|
231
|
-
* @property {Readonly<Record<string, string>>} tiers tier name -> model id
|
|
232
47
|
* @property {string} endpointKey the CONSTRUCTOR OPTION NAME this provider reads its endpoint from — NOT assumed to be `baseUrl` (see below)
|
|
233
|
-
* @property {(model: string|undefined) => Record<string, any>} paramsFor per-model request-key/param gating, applied on TOP of {exposeErrorBody:true, apiKey, model, <endpointKey>?}
|
|
234
48
|
*/
|
|
235
49
|
|
|
236
50
|
// ── WHY `endpointKey` IS A FIELD AND NOT THE LITERAL `baseUrl` ──────────────
|
|
@@ -260,35 +74,16 @@ export function draftCallOptions(provider, mode = 'off') {
|
|
|
260
74
|
// the menu holds only http-shaped API providers, and the FIRST thing that
|
|
261
75
|
// changes when Ollama is admitted is the one line nobody would think to look at.
|
|
262
76
|
|
|
263
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* The three client classes bare-agent exports, chosen by shape id. NO key name, model id or per-model gating lives here: a row's
|
|
79
|
+
* key is its own env name (src/providerrows.js), a job carries its own model, and the old max_tokens spelling is the ROW's tick
|
|
80
|
+
* (`legacyMaxTokens`, `makeProvider`).
|
|
81
|
+
* @type {Readonly<Record<string, ProviderTableEntry>>}
|
|
82
|
+
*/
|
|
264
83
|
const PROVIDER_TABLE = Object.freeze({
|
|
265
|
-
'anthropic-api': Object.freeze({
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
tiers: ANTHROPIC_TIER_MODELS,
|
|
269
|
-
endpointKey: 'baseUrl',
|
|
270
|
-
// Anthropic has only ever had one request key for the output cap
|
|
271
|
-
// (`max_tokens`) — nothing to gate here, ever (unlike OpenAI-shaped
|
|
272
|
-
// backends, which fork on `max_tokens` vs `max_completion_tokens`).
|
|
273
|
-
paramsFor: () => ({}),
|
|
274
|
-
}),
|
|
275
|
-
'openai-api': Object.freeze({
|
|
276
|
-
ctor: OpenAIProvider,
|
|
277
|
-
envKey: 'OPENAI_API_KEY',
|
|
278
|
-
tiers: OPENAI_TIER_MODELS,
|
|
279
|
-
endpointKey: 'baseUrl',
|
|
280
|
-
paramsFor: (model) => OPENAI_MODEL_OPTIONS[/** @type {string} */ (model)] ?? {},
|
|
281
|
-
}),
|
|
282
|
-
'gemini-api': Object.freeze({
|
|
283
|
-
ctor: GeminiProvider,
|
|
284
|
-
envKey: 'GEMINI_API_KEY',
|
|
285
|
-
tiers: GEMINI_TIER_MODELS,
|
|
286
|
-
endpointKey: 'baseUrl',
|
|
287
|
-
// Gemini carries no `max_tokens`/`max_completion_tokens` fork of its own —
|
|
288
|
-
// bare-agent's GeminiProvider maps the cap internally. Nothing to gate, and
|
|
289
|
-
// an empty object is the honest statement of that, not an omission.
|
|
290
|
-
paramsFor: () => ({}),
|
|
291
|
-
}),
|
|
84
|
+
'anthropic-api': Object.freeze({ ctor: AnthropicProvider, endpointKey: 'baseUrl' }),
|
|
85
|
+
'openai-api': Object.freeze({ ctor: OpenAIProvider, endpointKey: 'baseUrl' }),
|
|
86
|
+
'gemini-api': Object.freeze({ ctor: GeminiProvider, endpointKey: 'baseUrl' }),
|
|
292
87
|
});
|
|
293
88
|
|
|
294
89
|
/**
|
|
@@ -348,15 +143,15 @@ export function apiKeyProblem(value) {
|
|
|
348
143
|
}
|
|
349
144
|
|
|
350
145
|
/**
|
|
351
|
-
* Construct ONE provider instance for `providerName`/`model`, applying
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
146
|
+
* Construct ONE provider instance for `providerName`/`model`, applying `exposeErrorBody: true` (F153 — every constructed
|
|
147
|
+
* provider, unconditionally). `baseUrl` (PRD item 28, ruling (d), 2026-09-09: admitted in v1) is forwarded only when given.
|
|
148
|
+
* `legacyMaxTokens` is the Settings ROW's "old max_tokens" tick: a server that ignores `max_completion_tokens` (F149) does not
|
|
149
|
+
* bind the output cap until it is on. It is forwarded only for `openai-api` and only when `true` — nothing here sniffs a host
|
|
150
|
+
* or a model id to decide it.
|
|
356
151
|
* @param {string} providerName
|
|
357
|
-
* @param {{apiKey?: string, model?: string, baseUrl?: string}} [options]
|
|
152
|
+
* @param {{apiKey?: string, model?: string, baseUrl?: string, legacyMaxTokens?: boolean}} [options]
|
|
358
153
|
*/
|
|
359
|
-
export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
|
|
154
|
+
export function makeProvider(providerName, { apiKey, model, baseUrl, legacyMaxTokens } = {}) {
|
|
360
155
|
const entry = resolveProvider(providerName);
|
|
361
156
|
return new entry.ctor({
|
|
362
157
|
exposeErrorBody: true,
|
|
@@ -369,105 +164,59 @@ export function makeProvider(providerName, { apiKey, model, baseUrl } = {}) {
|
|
|
369
164
|
// is spelled differently would otherwise IGNORE the endpoint silently and
|
|
370
165
|
// run against its own default; see the note above the table.
|
|
371
166
|
...(baseUrl !== undefined ? { [entry.endpointKey]: baseUrl } : {}),
|
|
372
|
-
...
|
|
167
|
+
...(providerName === 'openai-api' && legacyMaxTokens === true ? { legacyMaxTokens: true } : {}),
|
|
373
168
|
});
|
|
374
169
|
}
|
|
375
170
|
|
|
376
171
|
/**
|
|
377
|
-
* Build the `{provider, providerFor, judgeProvider}` triple both runners
|
|
378
|
-
*
|
|
379
|
-
*
|
|
380
|
-
* (the caller runs `resolveWorkerModel` itself, exactly as both runners did
|
|
381
|
-
* before this factory existed — that decision stays where each runner's own
|
|
382
|
-
* `--model` flag/spec precedence lives, not duplicated in here).
|
|
383
|
-
*
|
|
384
|
-
* THE JUDGE IS NO LONGER PINNED TO ANTHROPIC (PRD item 32.2, hamr 2026-09-10:
|
|
385
|
-
* *"what i care about is that judge becomes llm agnostic and not set to one
|
|
386
|
-
* model or provider"*). It is constructed through this same factory, from a
|
|
387
|
-
* `judgeProviderName`/`judgeModel` pair the CALLER resolved (`resolveJudge`,
|
|
388
|
-
* src/judged.js — the spec's signed `judge` override, else the job's own worker
|
|
389
|
-
* provider and model). Item 28's pin, and item 28 part (2)'s deferral of a
|
|
390
|
-
* signed `judge:{provider,model}` field, both close here.
|
|
172
|
+
* Build the `{provider, providerFor, judgeProvider}` triple both authoring runners need (`src/authorrun.js` and
|
|
173
|
+
* `src/panel/authorsession.js` call it) — ONE seam instead of two copies of the same construction. `model` is the
|
|
174
|
+
* already-RESOLVED worker model id (the caller picked the Settings line).
|
|
391
175
|
*
|
|
392
|
-
*
|
|
393
|
-
* may
|
|
394
|
-
*
|
|
395
|
-
* have exactly one. The caller reads the resolved judge provider's own `envKey`
|
|
396
|
-
* and hands the key in — the same contract the worker key already has here.
|
|
176
|
+
* THE JUDGE IS NOT PINNED (PRD item 32.2): it is constructed through this same factory from a `judgeProviderName`/`judgeModel`
|
|
177
|
+
* pair the CALLER resolved. `judgeApiKey` stays a SEPARATE argument: the judge may be a different provider from the worker,
|
|
178
|
+
* and each key is read by the caller from its own row.
|
|
397
179
|
* @param {object} args
|
|
398
|
-
* @param {string} args.providerName
|
|
399
|
-
* @param {string|undefined} args.apiKey the WORKER's key, read from
|
|
180
|
+
* @param {string} args.providerName the worker's API shape id
|
|
181
|
+
* @param {string|undefined} args.apiKey the WORKER's key, read from its row's env name
|
|
400
182
|
* @param {string} args.model the already-resolved worker model id
|
|
401
|
-
* @param {
|
|
402
|
-
* @param {
|
|
403
|
-
* @param {string|undefined} args.judgeApiKey the judge's own key
|
|
183
|
+
* @param {string|undefined} [args.baseUrl] the worker row's Base URL
|
|
184
|
+
* @param {boolean} [args.legacyMaxTokens] the worker row's old max_tokens tick
|
|
185
|
+
* @param {string|undefined} args.judgeApiKey the judge's own key
|
|
404
186
|
* @param {string} args.judgeModel the resolved judge model id
|
|
405
|
-
* @param {string} [args.judgeProviderName] the resolved judge
|
|
406
|
-
* @param {string|undefined} [args.judgeBaseUrl] the judge
|
|
187
|
+
* @param {string} [args.judgeProviderName] the resolved judge shape; defaults to `providerName`
|
|
188
|
+
* @param {string|undefined} [args.judgeBaseUrl] the judge row's endpoint
|
|
189
|
+
* @param {boolean} [args.judgeLegacyMaxTokens] the judge row's old max_tokens tick (defaults to the worker's when the judge shares its provider)
|
|
407
190
|
*/
|
|
408
191
|
export function buildRunnerProviders({
|
|
409
|
-
providerName, apiKey, model,
|
|
410
|
-
judgeProviderName = providerName, judgeBaseUrl = undefined,
|
|
192
|
+
providerName, apiKey, model, baseUrl, legacyMaxTokens = false, judgeApiKey, judgeModel,
|
|
193
|
+
judgeProviderName = providerName, judgeBaseUrl = undefined, judgeLegacyMaxTokens = undefined,
|
|
411
194
|
}) {
|
|
412
|
-
const provider = makeProvider(providerName, { apiKey, model, baseUrl });
|
|
413
|
-
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
));
|
|
420
|
-
// the judge goes through the SAME table as the worker — one place names a
|
|
421
|
-
// provider identity, which is the entire reason this factory exists. Reusing
|
|
422
|
-
// the worker instance when the identity matches exactly is not an optimisation
|
|
423
|
-
// for its own sake: a second instance against the same endpoint/model is a
|
|
424
|
-
// second prompt-cache prefix, and cache writes are ~41% of the bill.
|
|
195
|
+
const provider = makeProvider(providerName, { apiKey, model, baseUrl, legacyMaxTokens });
|
|
196
|
+
// a plan step's `model` is the one schema token `sonnet` (STEP_MODELS, src/plan.js): it means THE JOB'S worker model
|
|
197
|
+
const providerFor = (/** @type {string} */ _tier) => provider;
|
|
198
|
+
// the judge goes through the SAME table as the worker. Reusing the worker instance when the identity matches exactly is not an
|
|
199
|
+
// optimisation for its own sake: a second instance against the same endpoint/model is a second prompt-cache prefix, and cache
|
|
200
|
+
// writes are ~41% of the bill.
|
|
201
|
+
const judgeLegacy = judgeLegacyMaxTokens ?? (judgeProviderName === providerName ? legacyMaxTokens : false);
|
|
425
202
|
const judgeProvider = (judgeProviderName === providerName && judgeModel === model && judgeApiKey === apiKey
|
|
426
|
-
&& judgeBaseUrl === baseUrl)
|
|
203
|
+
&& judgeBaseUrl === baseUrl && judgeLegacy === legacyMaxTokens)
|
|
427
204
|
? provider
|
|
428
|
-
: makeProvider(judgeProviderName, { apiKey: judgeApiKey, model: judgeModel, baseUrl: judgeBaseUrl });
|
|
205
|
+
: makeProvider(judgeProviderName, { apiKey: judgeApiKey, model: judgeModel, baseUrl: judgeBaseUrl, legacyMaxTokens: judgeLegacy });
|
|
429
206
|
return { provider, providerFor, judgeProvider };
|
|
430
207
|
}
|
|
431
208
|
|
|
432
209
|
/**
|
|
433
|
-
*
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
438
|
-
* header — never a completion/chat endpoint, so this never spends a token.
|
|
439
|
-
* @param {string} providerName
|
|
440
|
-
* @param {string} [baseUrl]
|
|
441
|
-
* @returns {{url: string, headers: Record<string,string>}|null} `null` when
|
|
442
|
-
* this provider's models-list shape is not known (never guessed at)
|
|
443
|
-
*/
|
|
444
|
-
function modelsListRequest(providerName, baseUrl) {
|
|
445
|
-
if (providerName === 'anthropic-api') {
|
|
446
|
-
return {
|
|
447
|
-
url: `${baseUrl || 'https://api.anthropic.com/v1'}/models`,
|
|
448
|
-
headers: { 'x-api-key': '', 'anthropic-version': '2023-06-01' },
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
if (providerName === 'openai-api') {
|
|
452
|
-
return { url: `${baseUrl || 'https://api.openai.com/v1'}/models`, headers: {} };
|
|
453
|
-
}
|
|
454
|
-
return null;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/**
|
|
458
|
-
* $0 reachability check: a GET against `providerName`'s own models-list
|
|
459
|
-
* endpoint, server-side, with the key in a header — NEVER a completion
|
|
460
|
-
* request, so it never spends a token (PANEL-BUILD.md P3 build item 7: "the
|
|
461
|
-
* person must get feedback BEFORE drafting if the chosen model can't
|
|
462
|
-
* work"). Reused, unmodified, by the panel's model-readiness route today
|
|
463
|
-
* (`src/panel/authorroutes.js`) and by the future Settings providers
|
|
464
|
-
* table's own "test" column (PANEL-BUILD.md P4) — ONE function, not two
|
|
465
|
-
* hand-typed copies of the same probe.
|
|
210
|
+
* $0 reachability check: ONE GET of the provider's models list (`src/modellist.js`, the one owner of
|
|
211
|
+
* that call), server-side, with the key in a header — NEVER a completion request, so it never spends
|
|
212
|
+
* a token (PANEL-BUILD.md P3 build item 7: "the person must get feedback BEFORE drafting if the
|
|
213
|
+
* chosen model can't work"). Used by the panel's model-readiness route (`src/panel/authorroutes.js`);
|
|
214
|
+
* Settings > Providers' Test and Refresh call `listModels` directly. A blank `baseUrl` makes no call.
|
|
466
215
|
*
|
|
467
|
-
* NEVER returns the key, never logs it — the key only ever appears in a
|
|
468
|
-
*
|
|
216
|
+
* NEVER returns the key, never logs it — the key only ever appears in a request header sent
|
|
217
|
+
* straight to the provider's own host.
|
|
469
218
|
* @param {object} o
|
|
470
|
-
* @param {string} o.providerName
|
|
219
|
+
* @param {string} o.providerName an API shape id
|
|
471
220
|
* @param {string} o.apiKey
|
|
472
221
|
* @param {string} [o.model] when given, the response is also checked for this model id
|
|
473
222
|
* @param {string} [o.baseUrl]
|
|
@@ -478,39 +227,16 @@ function modelsListRequest(providerName, baseUrl) {
|
|
|
478
227
|
export async function checkProviderReachable({
|
|
479
228
|
providerName, apiKey, model, baseUrl, fetchImpl = fetch, timeoutMs = 4000,
|
|
480
229
|
}) {
|
|
481
|
-
|
|
482
|
-
if (!req) {
|
|
230
|
+
if (!SHAPE_IDS.includes(providerName)) {
|
|
483
231
|
return {
|
|
484
232
|
reachable: false, modelListed: null, status: 'unsupported-provider',
|
|
485
233
|
note: `no models-list check known for "${providerName}"`,
|
|
486
234
|
};
|
|
487
235
|
}
|
|
488
|
-
const
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
const controller = new AbortController();
|
|
493
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
494
|
-
try {
|
|
495
|
-
// GET ONLY — no body, no completion/chat call, no tokens spent.
|
|
496
|
-
const res = await fetchImpl(req.url, { method: 'GET', headers, signal: controller.signal });
|
|
497
|
-
if (!res.ok) {
|
|
498
|
-
return { reachable: false, modelListed: null, status: `HTTP ${res.status}`, note: `models list returned HTTP ${res.status}` };
|
|
499
|
-
}
|
|
500
|
-
/** @type {any} */
|
|
501
|
-
let body = null;
|
|
502
|
-
try { body = await res.json(); } catch { body = null; }
|
|
503
|
-
const ids = Array.isArray(body?.data) ? body.data.map((/** @type {any} */ m) => m?.id).filter(Boolean) : [];
|
|
504
|
-
const modelListed = model ? ids.includes(model) : null;
|
|
505
|
-
return { reachable: true, modelListed, status: 'ok', note: null };
|
|
506
|
-
} catch (/** @type {any} */ e) {
|
|
507
|
-
const cls = e?.name === 'AbortError' ? 'timeout' : (e?.code || e?.name || 'network-error');
|
|
508
|
-
// connection refused surfaces as fetch's bare TypeError ("fetch failed") — say it plainly
|
|
509
|
-
const refused = e?.name === 'TypeError' || e?.code === 'ECONNREFUSED' || e?.cause?.code === 'ECONNREFUSED';
|
|
510
|
-
return { reachable: false, modelListed: null, status: String(cls), note: refused ? `nothing answering at ${req.url}` : `request failed: ${cls}` };
|
|
511
|
-
} finally {
|
|
512
|
-
clearTimeout(timer);
|
|
513
|
-
}
|
|
236
|
+
const r = await listModels({ shape: providerName, baseUrl: baseUrl ?? '', key: apiKey, fetchImpl, timeoutMs });
|
|
237
|
+
if (r.ok) return { reachable: true, modelListed: model ? r.models.includes(model) : null, status: 'ok', note: null };
|
|
238
|
+
const note = r.status.startsWith('HTTP ') ? `models list returned ${r.status}` : (r.detail ?? r.why);
|
|
239
|
+
return { reachable: false, modelListed: null, status: r.status, note };
|
|
514
240
|
}
|
|
515
241
|
|
|
516
242
|
/**
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// rewritten-from: fwdloop src/providershapes.js@f9449e8
|
|
2
|
+
// PANEL-BUILD.md P8 — the API shapes the Settings > Providers dropdown offers, ONE owner.
|
|
3
|
+
// A shape is a wire protocol, never a place: no web address lives here (each row's Base URL
|
|
4
|
+
// is typed in Settings). Only a shape `makeProvider` (src/providers.js) can really build is listed.
|
|
5
|
+
// Kept in its own file so config.js can validate a shape without importing providers.js.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `id` is the value saved in config.json (and the signed spec's `provider`).
|
|
9
|
+
* @type {readonly {id: string, label: string}[]}
|
|
10
|
+
*/
|
|
11
|
+
export const SHAPES = Object.freeze([
|
|
12
|
+
Object.freeze({ id: 'anthropic-api', label: 'Anthropic' }),
|
|
13
|
+
Object.freeze({ id: 'openai-api', label: 'OpenAI-compatible' }),
|
|
14
|
+
Object.freeze({ id: 'gemini-api', label: 'Gemini' }),
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/** @type {readonly string[]} */
|
|
18
|
+
export const SHAPE_IDS = Object.freeze(SHAPES.map((s) => s.id));
|
package/src/replay.js
CHANGED
|
@@ -1520,7 +1520,7 @@ export function replayRun(spineEvents, auditEvents = [], { runId = null, auditAv
|
|
|
1520
1520
|
// never hidden. `null` when there is nothing to compare (a resumed run,
|
|
1521
1521
|
// where a gap is expected and already explained; or `thisFileSpend`
|
|
1522
1522
|
// itself unknown) or when the two already agree within $0.0001.
|
|
1523
|
-
spendMismatch: (!resumed && spentUsd !== null && thisFileSpend.value !== null && Math.abs(spentUsd - thisFileSpend.value) >
|
|
1523
|
+
spendMismatch: (!resumed && spentUsd !== null && thisFileSpend.value !== null && Math.abs(spentUsd - thisFileSpend.value) > 1e-4)
|
|
1524
1524
|
? { spentUsd, thisFileUsd: thisFileSpend.value, diffUsd: spentUsd - thisFileSpend.value }
|
|
1525
1525
|
: null,
|
|
1526
1526
|
timelineKind,
|
package/src/thinking.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// rewritten-from: fwdloop src/thinking.js@f9449e8
|
|
2
|
+
// PANEL-BUILD.md P8 batch 5 — THE one owner of what a drafting or judging model call sends for thinking. bareloop keeps NO per-model
|
|
3
|
+
// thinking list and NO vendor regex: bare-agent (>=0.52) owns which request shape switches thinking for which model, and
|
|
4
|
+
// `thinkingSupport(kind, model, {baseUrl})` is its own pure lookup (no network): `true` (measured switchable), `false` (measured
|
|
5
|
+
// NOT switchable), `'unverified'` (not measured — the shape ladder is tried at runtime; if every shape fails with a client
|
|
6
|
+
// validation error ONE plain request decides, and the call still runs, reported `thinkingStatus: 'unsupported'`).
|
|
7
|
+
//
|
|
8
|
+
// The tick is the person's, per job ("Thinking on", the signed optional `thinking` field; absent = off). It covers DRAFTING and the
|
|
9
|
+
// rubric JUDGE; the WORKER is never handed a thinking option. PER-CALL options, never constructor options (the drafter's provider is
|
|
10
|
+
// the worker's instance, deliberately left as it was). The raw `thinking` option is never sent (bare-agent throws if both are set).
|
|
11
|
+
import { createRequire } from 'node:module';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const { thinkingSupport: baseSupport, AnthropicProvider, OpenAIProvider, GeminiProvider } = require('bare-agent/providers');
|
|
15
|
+
|
|
16
|
+
/** The words under a greyed "Thinking on" box (the server's `thinking.canChange` decides; the page decides nothing). */
|
|
17
|
+
export const THINKING_FIXED_SAY = "can't change this for this model";
|
|
18
|
+
|
|
19
|
+
/** API shape id -> bare-agent's thinking-ladder kind. A missing shape (clipipe, a test double) has no kind. */
|
|
20
|
+
const KIND_OF_SHAPE = Object.freeze({ 'anthropic-api': 'anthropic', 'openai-api': 'openai', 'gemini-api': 'gemini' });
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* What bare-agent knows about switching this model's thinking. A missing kind, a non-function `impl`, or a throw all read
|
|
24
|
+
* `'unverified'` — never a guess in either direction.
|
|
25
|
+
* @param {string} shape an API shape id (src/providershapes.js)
|
|
26
|
+
* @param {string} model
|
|
27
|
+
* @param {{ baseUrl?: string|null, impl?: Function }} [o] `impl` is a test seam; production uses bare-agent's own
|
|
28
|
+
* @returns {true|false|'unverified'}
|
|
29
|
+
*/
|
|
30
|
+
export function thinkingSupport(shape, model, { baseUrl, impl = baseSupport } = {}) {
|
|
31
|
+
const kind = /** @type {Record<string, string>} */ (KIND_OF_SHAPE)[shape];
|
|
32
|
+
if (kind === undefined || typeof impl !== 'function') return 'unverified';
|
|
33
|
+
try {
|
|
34
|
+
const r = impl(kind, model, baseUrl ? { baseUrl } : undefined);
|
|
35
|
+
return r?.switchable === true ? true : (r?.switchable === false ? false : 'unverified');
|
|
36
|
+
} catch { return 'unverified'; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The per-call options for one support answer and one tick (`true` = "Thinking on"). The table (PANEL-BUILD.md P8, ruling Q1):
|
|
41
|
+
* tick on : true -> on | false -> {} (the box is greyed, cannot be on) | 'unverified' -> on
|
|
42
|
+
* tick off: true -> off | false -> {} (the model thinks as it would) | 'unverified' -> off (bare-agent decides; plain request on failure)
|
|
43
|
+
* @param {true|false|'unverified'} support
|
|
44
|
+
* @param {boolean} tick
|
|
45
|
+
* @returns {{ thinkingMode: 'on'|'off' } | {}}
|
|
46
|
+
*/
|
|
47
|
+
export function thinkingOptions(support, tick) {
|
|
48
|
+
if (support === false) return {};
|
|
49
|
+
return { thinkingMode: tick ? 'on' : 'off' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The enumerated operator knob a drafting call is built with: 'on' | 'off' (the tick, spelled as a word). */
|
|
53
|
+
export const THINKING_MODES = Object.freeze(['on', 'off']);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Throw unless `mode` is one of {@link THINKING_MODES} — an operator-param guard, so a typo never silently means a mode.
|
|
57
|
+
* @param {unknown} mode
|
|
58
|
+
* @returns {'on'|'off'}
|
|
59
|
+
*/
|
|
60
|
+
export function assertThinkingMode(mode) {
|
|
61
|
+
if (mode === 'on' || mode === 'off') return mode;
|
|
62
|
+
throw new Error(`[thinking] draftThinking must be one of ${THINKING_MODES.join(' | ')}, got ${JSON.stringify(mode)}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The API shape of a constructed bare-agent provider (the three client classes only); `null` for a test double or a clipipe.
|
|
67
|
+
* @param {any} provider
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
export function shapeOfProvider(provider) {
|
|
71
|
+
if (provider instanceof AnthropicProvider) return 'anthropic-api';
|
|
72
|
+
if (provider instanceof OpenAIProvider) return 'openai-api';
|
|
73
|
+
if (provider instanceof GeminiProvider) return 'gemini-api';
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The per-call options every DRAFTING and JUDGING model call on this provider instance carries for this mode. A provider that is
|
|
79
|
+
* not one of the three client classes (a test double, a clipipe) is never sent the switch.
|
|
80
|
+
* @param {any} provider a constructed bare-agent provider
|
|
81
|
+
* @param {unknown} [mode] 'on' | 'off' (default 'off')
|
|
82
|
+
* @returns {{ thinkingMode: 'on'|'off' } | {}}
|
|
83
|
+
*/
|
|
84
|
+
export function thinkingCallOptions(provider, mode = 'off') {
|
|
85
|
+
const m = assertThinkingMode(mode);
|
|
86
|
+
const shape = shapeOfProvider(provider);
|
|
87
|
+
if (shape === null) return {};
|
|
88
|
+
return thinkingOptions(thinkingSupport(shape, String(provider.model), { baseUrl: provider.baseUrl }), m === 'on');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* What the drafting calls will carry for a model the caller has only NAMED (shape, model id, Base URL) — no key, no network. The
|
|
93
|
+
* first drafting row is shown from this, before the key is read.
|
|
94
|
+
* @param {{providerName: string, model: string, baseUrl?: string|null}} o
|
|
95
|
+
* @param {unknown} [mode] 'on' | 'off'
|
|
96
|
+
* @returns {{ support: true|false|'unverified', mode: 'on'|'off', options: {thinkingMode?: 'on'|'off'} }}
|
|
97
|
+
*/
|
|
98
|
+
export function thinkingPlanFor({ providerName, model, baseUrl }, mode = 'off') {
|
|
99
|
+
const m = assertThinkingMode(mode);
|
|
100
|
+
const support = thinkingSupport(providerName, model, { baseUrl });
|
|
101
|
+
return { support, mode: m, options: thinkingOptions(support, m === 'on') };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The server's answer for one Model-menu line: whether the "Thinking on" box can change and the words under it. `false` greys and
|
|
106
|
+
* unticks the box (the note reads {@link THINKING_FIXED_SAY}); `'unverified'` leaves it enabled with no note.
|
|
107
|
+
* @param {string} shape @param {string} model @param {string|null|undefined} baseUrl
|
|
108
|
+
* @returns {{ support: true|false|'unverified', canChange: boolean, say: string }}
|
|
109
|
+
*/
|
|
110
|
+
export function thinkingForLine(shape, model, baseUrl) {
|
|
111
|
+
const support = thinkingSupport(shape, model, { baseUrl });
|
|
112
|
+
return { support, canChange: support !== false, say: support === false ? THINKING_FIXED_SAY : '' };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The first drafting row's words — code-owned, ALWAYS starting "thinking on" or "thinking off". `status` is bare-agent's own
|
|
117
|
+
* `thinkingStatus` for a call that has run ('applied' | 'unsupported' | 'ignored' | 'unknown'); undefined = before any call, read from
|
|
118
|
+
* the plan (`support`, `mode`).
|
|
119
|
+
* @param {{ mode: 'on'|'off', support?: true|false|'unverified', status?: string }} s
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
export function thinkingRowText({ mode, support, status }) {
|
|
123
|
+
if (status === 'applied') return `thinking ${mode}`;
|
|
124
|
+
if (status === 'unsupported') return "thinking on (this model can't change it)";
|
|
125
|
+
if (status === 'ignored') return 'thinking on (the model thought anyway)';
|
|
126
|
+
if (status === 'unknown') return `thinking ${mode} (not confirmed)`;
|
|
127
|
+
if (support === false) return "thinking on (this model can't change it)";
|
|
128
|
+
if (support === 'unverified') return `thinking ${mode} (not verified for this model)`;
|
|
129
|
+
return `thinking ${mode}`;
|
|
130
|
+
}
|