@zergai/ztc 0.4.0-beta.1 → 0.4.1-dev.36919234045
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/README.md +8 -1
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/dist/App.js +2 -2
- package/dist/agent/agent.js +41 -5
- package/dist/agent/backends/anthropic.js +5 -10
- package/dist/agent/backends/gemini.js +2 -0
- package/dist/agent/backends/index.d.ts +1 -0
- package/dist/agent/backends/index.js +10 -1
- package/dist/agent/backends/openai.d.ts +0 -6
- package/dist/agent/backends/openai.js +5 -2
- package/dist/agent/backends/openai_compatible.d.ts +2 -0
- package/dist/agent/backends/openai_compatible.js +18 -8
- package/dist/agent/backends/provider_ids.d.ts +1 -0
- package/dist/agent/backends/provider_ids.js +4 -0
- package/dist/agent/backends/types.d.ts +1 -0
- package/dist/agent/backends/zrouter_stream.d.ts +5 -1
- package/dist/agent/backends/zrouter_stream.js +141 -37
- package/dist/agent/commands/compact.js +6 -5
- package/dist/agent/commands/config.js +4 -1
- package/dist/agent/commands/context.js +6 -1
- package/dist/agent/commands/effort.js +14 -6
- package/dist/agent/commands/model.js +12 -2
- package/dist/agent/commands/profile.js +21 -2
- package/dist/agent/commands/provider_selection.js +14 -1
- package/dist/agent/commands/settings.js +45 -7
- package/dist/agent/commands/types.d.ts +19 -0
- package/dist/agent/factory.js +14 -68
- package/dist/agent/subagent.d.ts +0 -10
- package/dist/agent/subagent.js +22 -0
- package/dist/agent/tool_response_store.d.ts +1 -1
- package/dist/agent/tool_response_store.js +4 -2
- package/dist/agent/tools/pane.d.ts +0 -7
- package/dist/agent/tools/pane.js +5 -4
- package/dist/app/command_context.js +33 -10
- package/dist/app/model_catalog_cache.d.ts +5 -0
- package/dist/app/model_catalog_cache.js +138 -0
- package/dist/app/restart.d.ts +1 -1
- package/dist/app/restart.js +16 -5
- package/dist/app/selection_completion.d.ts +17 -2
- package/dist/app/selection_completion.js +74 -6
- package/dist/app/session_manager.js +13 -8
- package/dist/cli_args.js +2 -16
- package/dist/cli_main.js +25 -6
- package/dist/cli_ratatui.js +70 -33
- package/dist/config/defaults.d.ts +2 -1
- package/dist/config/defaults.js +9 -2
- package/dist/config/resolved.d.ts +2 -2
- package/dist/config.d.ts +35 -3
- package/dist/config.js +227 -29
- package/dist/headless.js +2 -0
- package/dist/onboarding/first_run.js +5 -1
- package/dist/panes/model_catalog.d.ts +3 -3
- package/dist/panes/model_catalog.js +28 -4
- package/dist/panes/pane_registry.js +6 -2
- package/dist/panes/types.d.ts +2 -0
- package/dist/panes/ztc_launch.js +12 -0
- package/dist/session.d.ts +6 -2
- package/dist/session.js +65 -10
- package/dist/ui/views/completion_menu.js +10 -5
- package/dist/ui/views/status_bar.js +9 -5
- package/dist/ui/widgets/resume_panel.d.ts +2 -0
- package/dist/ui/widgets/resume_panel.js +17 -15
- package/dist/utils/compaction.d.ts +1 -1
- package/dist/utils/compaction.js +2 -2
- package/dist/utils/context.d.ts +4 -4
- package/dist/utils/context.js +14 -12
- package/dist/utils/deepseek.d.ts +2 -2
- package/dist/utils/deepseek.js +13 -5
- package/dist/utils/model_cache.d.ts +15 -0
- package/dist/utils/model_cache.js +143 -0
- package/dist/utils/models.d.ts +4 -2
- package/dist/utils/models.js +12 -16
- package/dist/utils/reasoning.d.ts +8 -1
- package/dist/utils/reasoning.js +31 -11
- package/dist/utils/router_catalog.d.ts +47 -0
- package/dist/utils/router_catalog.js +222 -0
- package/dist/zlm-embed.mjs +1180 -514
- package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
- package/node_modules/@zerg/model-contracts/README.md +78 -0
- package/node_modules/@zerg/model-contracts/index.d.ts +18 -0
- package/node_modules/@zerg/model-contracts/index.js +220 -0
- package/node_modules/@zerg/model-contracts/package.json +8 -0
- package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +9 -6
- package/packages/model-contracts/LICENSE.md +375 -0
- package/packages/model-contracts/README.md +78 -0
- package/packages/model-contracts/index.d.ts +18 -0
- package/packages/model-contracts/index.js +220 -0
- package/packages/model-contracts/package.json +8 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { modelContract, normalizeContractModel } from '@zerg/model-contracts';
|
|
2
|
+
import { resolveRouterModel } from '../../utils/router_catalog.js';
|
|
1
3
|
import { randomUUID } from 'node:crypto';
|
|
2
4
|
import { measureResponse, measureStream } from './request_metrics.js';
|
|
3
5
|
import { parseSSEEvents } from './sse.js';
|
|
4
6
|
import { buildOpenAIMessages, buildOpenAIToolDefs } from './openai_format.js';
|
|
5
|
-
import {
|
|
7
|
+
import { isKimiModel } from './kimi_params.js';
|
|
6
8
|
import { runtimeDiag } from '../../utils/debug_log.js';
|
|
7
9
|
import { createZrouterEventMapper } from './zrouter_events.js';
|
|
8
10
|
import { parseTokenUsage } from './token_usage.js';
|
|
9
|
-
import {
|
|
11
|
+
import { DEEPSEEK_FLASH_MAX_OUTPUT, isModalDeepSeekFlashModel, MODAL_DEEPSEEK_FLASH_MAX_OUTPUT, isDeepSeekFlashModel } from '../../utils/deepseek.js';
|
|
10
12
|
import { getVersion } from '../../utils/version.js';
|
|
11
13
|
import { isTransportError } from './transport_errors.js';
|
|
12
14
|
const ZROUTER_HEADERS_TIMEOUT_MS = 90_000;
|
|
@@ -23,19 +25,13 @@ const PINNABLE_DECORATION = /^[A-Za-z0-9._:-]{1,64}$/;
|
|
|
23
25
|
function isPinnableDecoration(value) {
|
|
24
26
|
return value !== undefined && PINNABLE_DECORATION.test(value);
|
|
25
27
|
}
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
// the reconnect resumes instead of restarting. Every knob is env-overridable
|
|
34
|
-
// because a self-hosted Router may restart more slowly.
|
|
35
|
-
const DEFAULT_RECONNECT_ATTEMPTS = 6;
|
|
36
|
-
const DEFAULT_RECONNECT_BASE_MS = 500;
|
|
37
|
-
const DEFAULT_RECONNECT_CAP_MS = 10_000;
|
|
38
|
-
const MAX_RECONNECT_ATTEMPTS = 20;
|
|
28
|
+
// Allow Router restarts to recover through bounded, cancellable retries. Each
|
|
29
|
+
// reconnect reports progress and resumes the same turn/cursor. Hosts can tune
|
|
30
|
+
// the attempt count and backoff through the environment.
|
|
31
|
+
const DEFAULT_RECONNECT_ATTEMPTS = 120;
|
|
32
|
+
const DEFAULT_RECONNECT_BASE_MS = 1_000;
|
|
33
|
+
const DEFAULT_RECONNECT_CAP_MS = 30_000;
|
|
34
|
+
const MAX_RECONNECT_ATTEMPTS = 120;
|
|
39
35
|
const MAX_RECONNECT_MS = 300_000;
|
|
40
36
|
function envBoundedInt(name, fallback, min, max) {
|
|
41
37
|
const parsed = Number(process.env[name]);
|
|
@@ -59,6 +55,7 @@ function resolveReconnectPolicy() {
|
|
|
59
55
|
* uses the unified chat endpoint so routing can fall back across providers.
|
|
60
56
|
*/
|
|
61
57
|
export class ZRouterStreamBackend {
|
|
58
|
+
catalog;
|
|
62
59
|
apiKey;
|
|
63
60
|
proxyUrl;
|
|
64
61
|
provider;
|
|
@@ -67,6 +64,7 @@ export class ZRouterStreamBackend {
|
|
|
67
64
|
conversationId;
|
|
68
65
|
clientName;
|
|
69
66
|
constructor(config) {
|
|
67
|
+
this.catalog = config.catalog ? structuredClone(config.catalog) : undefined;
|
|
70
68
|
this.apiKey = config.apiKey;
|
|
71
69
|
this.proxyUrl = config.proxyUrl.replace(/\/+$/, '');
|
|
72
70
|
this.provider = config.provider;
|
|
@@ -87,12 +85,45 @@ export class ZRouterStreamBackend {
|
|
|
87
85
|
handlesTransportRetries() {
|
|
88
86
|
return true;
|
|
89
87
|
}
|
|
88
|
+
getModelCapabilities(model) {
|
|
89
|
+
return this.catalog?.version === 1
|
|
90
|
+
? resolveRouterModel(this.catalog, this.provider, model)?.capabilities : undefined;
|
|
91
|
+
}
|
|
90
92
|
buildOpenAIBody(request) {
|
|
91
|
-
|
|
93
|
+
if (this.catalog?.version === 1) {
|
|
94
|
+
const descriptor = resolveRouterModel(this.catalog, this.provider, request.model);
|
|
95
|
+
if (!descriptor?.capabilities?.chat)
|
|
96
|
+
throw new Error('This model is no longer in the Router chat catalog. Refresh /model.');
|
|
97
|
+
const caps = descriptor.capabilities;
|
|
98
|
+
if (request.codexEffort && caps.reasoning && !caps.reasoning.choices.includes(request.codexEffort)) {
|
|
99
|
+
throw new Error(`Unsupported reasoning effort: ${request.codexEffort}`);
|
|
100
|
+
}
|
|
101
|
+
if (request.maxTokens !== undefined && caps.max_output_tokens !== undefined && request.maxTokens > caps.max_output_tokens) {
|
|
102
|
+
throw new Error(`Max output tokens exceeds the advertised limit (${caps.max_output_tokens}).`);
|
|
103
|
+
}
|
|
104
|
+
if (caps.images === false && request.messages.some(message => Array.isArray(message.content)
|
|
105
|
+
&& message.content.some(block => block.type === 'image' || block.type === 'video')))
|
|
106
|
+
throw new Error('This model does not support images.');
|
|
107
|
+
const prepared = caps.tools === false ? chatOnlyRequest(request) : request;
|
|
108
|
+
return { provider: descriptor.owned_by, model: descriptor.id,
|
|
109
|
+
...(request.routePlan ? { route: request.routePlan } : {}),
|
|
110
|
+
...(request.maxTokens === undefined ? {} : { max_output_tokens: request.maxTokens }),
|
|
111
|
+
...(caps.reasoning && request.codexEffort ? { reasoning_effort: request.codexEffort } : {}),
|
|
112
|
+
messages: buildOpenAIMessages(prepared),
|
|
113
|
+
...(prepared.tools.length ? { tools: buildOpenAIToolDefs(prepared.tools) } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Legacy contract for older Routers only. ZTC stores the model separately from its provider. Router's global model
|
|
92
117
|
// namespace is a wire concern; keep existing namespaced IDs compatible.
|
|
93
118
|
if (this.provider === 'modal' && !/^modal\//i.test(request.model)) {
|
|
94
119
|
request = { ...request, model: `modal/${request.model}` };
|
|
95
120
|
}
|
|
121
|
+
// Router resolves the Fireworks upstream id from the NAMESPACED form:
|
|
122
|
+
// fireworksUpstreamModelId returns null without the prefix and provider-client
|
|
123
|
+
// then answers 400 before dispatch. ZTC stores the bare slug, so add it here.
|
|
124
|
+
if (this.provider === 'fireworks') {
|
|
125
|
+
request = { ...request, model: `fireworks/${normalizeContractModel('fireworks', request.model)}` };
|
|
126
|
+
}
|
|
96
127
|
// Kimi deprecates max_tokens in favour of max_completion_tokens, same as the
|
|
97
128
|
// o-series and GPT-5+; keep this in step with the direct backend so the token
|
|
98
129
|
// field does not depend on how the user authenticated.
|
|
@@ -115,20 +146,42 @@ export class ZRouterStreamBackend {
|
|
|
115
146
|
model: request.model,
|
|
116
147
|
...(request.routePlan ? { route: request.routePlan } : {}),
|
|
117
148
|
...tokenParam,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
// No-op for every non-Kimi model.
|
|
121
|
-
// Modal uses Kimi's effort ladder but keeps its own max_tokens contract.
|
|
122
|
-
...buildKimiRequestParams(this.provider === 'modal'
|
|
123
|
-
? { ...request, model: request.model.slice('modal/'.length) }
|
|
124
|
-
: request),
|
|
125
|
-
...deepseekRequestParams(request),
|
|
149
|
+
...(request.codexEffort && modelContract(this.provider, request.model, 'chat_tools')?.reasoningEfforts.includes(request.codexEffort)
|
|
150
|
+
? { reasoning_effort: request.codexEffort } : {}),
|
|
126
151
|
messages: buildOpenAIMessages(request),
|
|
127
152
|
...(request.tools.length > 0 ? { tools: buildOpenAIToolDefs(request.tools) } : {})
|
|
128
153
|
};
|
|
129
154
|
}
|
|
130
|
-
|
|
131
|
-
|
|
155
|
+
serializeBody(request, stream = false) {
|
|
156
|
+
const body = {
|
|
157
|
+
...this.buildOpenAIBody(request),
|
|
158
|
+
...(stream ? { stream: true } : {}),
|
|
159
|
+
};
|
|
160
|
+
const serialized = JSON.stringify(body);
|
|
161
|
+
// Funded DeepSeek admission scans at most 10,000 JSON values, independently
|
|
162
|
+
// of the model's token window. A longer text history is otherwise rejected
|
|
163
|
+
// as non-text trial input, and resumable streaming masks that as a 5xx.
|
|
164
|
+
// Signal context overflow locally so hosts compact and retry, including
|
|
165
|
+
// when an agent's tool loop crosses the limit before returning to idle.
|
|
166
|
+
if (body.model === 'modal/deepseek-v4.1-flash'
|
|
167
|
+
|| body.model === 'fireworks/deepseek-v4p1-flash') {
|
|
168
|
+
// Count the serialized wire shape: undefined properties disappear, and
|
|
169
|
+
// tool calls/results expand into separate messages during encoding.
|
|
170
|
+
const pending = [JSON.parse(serialized)];
|
|
171
|
+
let values = 1;
|
|
172
|
+
while (pending.length > 0) {
|
|
173
|
+
const value = pending.pop();
|
|
174
|
+
if (value === null || typeof value !== 'object')
|
|
175
|
+
continue;
|
|
176
|
+
for (const child of Object.values(value)) {
|
|
177
|
+
if (++values > 10_000) {
|
|
178
|
+
throw new Error('Prompt is too long for Router\'s context structure limit (10,000 JSON values). Compact the conversation before retrying.');
|
|
179
|
+
}
|
|
180
|
+
pending.push(child);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return serialized;
|
|
132
185
|
}
|
|
133
186
|
async buildHeaders(request) {
|
|
134
187
|
const token = this.authTokenProvider
|
|
@@ -141,6 +194,7 @@ export class ZRouterStreamBackend {
|
|
|
141
194
|
const conversationPin = this.conversationPin(request);
|
|
142
195
|
return {
|
|
143
196
|
'Content-Type': 'application/json',
|
|
197
|
+
...(this.catalog?.version === 1 ? { 'X-Zerg-Model-Contract': '1' } : {}),
|
|
144
198
|
'Authorization': `Bearer ${normalizedToken}`,
|
|
145
199
|
'X-Zerg-Client': this.clientName,
|
|
146
200
|
'X-Zerg-Client-Version': getVersion(),
|
|
@@ -183,7 +237,7 @@ export class ZRouterStreamBackend {
|
|
|
183
237
|
return measureResponse(routedRequest, fetcher => this.generateResponse(routedRequest, fetcher));
|
|
184
238
|
}
|
|
185
239
|
async generateResponse(request, fetcher) {
|
|
186
|
-
const body = this.
|
|
240
|
+
const body = this.serializeBody(request);
|
|
187
241
|
const headers = await this.buildHeaders(request);
|
|
188
242
|
// Proof-chain log: the exact router endpoint + model this turn dispatches to.
|
|
189
243
|
runtimeDiag('info', `[ztc] zrouter dispatch ${JSON.stringify({
|
|
@@ -195,15 +249,19 @@ export class ZRouterStreamBackend {
|
|
|
195
249
|
const response = await fetcher(this.endpointFor(), {
|
|
196
250
|
method: 'POST',
|
|
197
251
|
headers,
|
|
198
|
-
body
|
|
252
|
+
body,
|
|
199
253
|
signal: request.signal,
|
|
200
254
|
});
|
|
201
255
|
if (!response.ok) {
|
|
202
256
|
const errorText = await response.text();
|
|
203
|
-
throw new Error(`ZRouter API error (${response.status}): ${errorText}`);
|
|
257
|
+
throw new Error(`ZRouter API error (${response.status}): ${errorText}${failureHeaderSuffix(response)}`);
|
|
204
258
|
}
|
|
205
259
|
const data = await response.json();
|
|
206
|
-
|
|
260
|
+
const parsed = this.parseOpenAIResponse(data);
|
|
261
|
+
if (this.getModelCapabilities(request.model)?.tools === false && parsed.content.some(block => block.type === 'tool_use')) {
|
|
262
|
+
throw new Error('Chat-only model returned tools; execution is disabled.');
|
|
263
|
+
}
|
|
264
|
+
return parsed;
|
|
207
265
|
}
|
|
208
266
|
parseOpenAIResponse(data) {
|
|
209
267
|
// ZRouter wraps OpenAI responses in OpenAI-compatible format
|
|
@@ -248,12 +306,15 @@ export class ZRouterStreamBackend {
|
|
|
248
306
|
return measureStream(routedRequest, fetcher => this.streamResponse(routedRequest, fetcher));
|
|
249
307
|
}
|
|
250
308
|
async *streamResponse(request, fetcher) {
|
|
251
|
-
const body =
|
|
309
|
+
const body = this.serializeBody(request, true);
|
|
252
310
|
const mapEvent = createZrouterEventMapper();
|
|
253
311
|
let resumeAfter;
|
|
254
312
|
let emittedMappedEvent = false;
|
|
255
313
|
let resumeSupported = false;
|
|
256
314
|
const policy = resolveReconnectPolicy();
|
|
315
|
+
let reconnectStarted;
|
|
316
|
+
const budget = () => resumeAfter ? 300_000 : 30_000;
|
|
317
|
+
const remaining = () => budget() - (reconnectStarted === undefined ? 0 : Date.now() - reconnectStarted);
|
|
257
318
|
for (let attempt = 1; attempt <= policy.attempts; attempt++) {
|
|
258
319
|
try {
|
|
259
320
|
const headers = await this.buildHeaders(request);
|
|
@@ -262,12 +323,12 @@ export class ZRouterStreamBackend {
|
|
|
262
323
|
const response = await fetchWithHeadersTimeout(fetcher, this.endpointFor(), {
|
|
263
324
|
method: 'POST',
|
|
264
325
|
headers,
|
|
265
|
-
body
|
|
326
|
+
body,
|
|
266
327
|
signal: request.signal,
|
|
267
|
-
}, ZROUTER_HEADERS_TIMEOUT_MS);
|
|
328
|
+
}, Math.min(ZROUTER_HEADERS_TIMEOUT_MS, Math.max(1, remaining())));
|
|
268
329
|
if (!response.ok) {
|
|
269
330
|
const errorText = await response.text();
|
|
270
|
-
throw new Error(`ZRouter API error (${response.status}): ${errorText}`);
|
|
331
|
+
throw new Error(`ZRouter API error (${response.status}): ${errorText}${failureHeaderSuffix(response)}`);
|
|
271
332
|
}
|
|
272
333
|
if (!response.body) {
|
|
273
334
|
throw new Error('ZRouter API error: no response body for streaming request');
|
|
@@ -298,6 +359,9 @@ export class ZRouterStreamBackend {
|
|
|
298
359
|
throw new Error(`ZRouter API error (${status}): ${JSON.stringify(ev.body ?? {})}`);
|
|
299
360
|
}
|
|
300
361
|
const mapped = mapEvent(ev);
|
|
362
|
+
if (this.getModelCapabilities(request.model)?.tools === false && mapped.some(event => event.type.startsWith('tool_use'))) {
|
|
363
|
+
throw new Error('Chat-only model returned tools; execution is disabled.');
|
|
364
|
+
}
|
|
301
365
|
if (mapped.length > 0) {
|
|
302
366
|
// Heartbeats are liveness, not model output. A drop while only
|
|
303
367
|
// heartbeats have arrived cannot duplicate visible text, so it
|
|
@@ -328,15 +392,22 @@ export class ZRouterStreamBackend {
|
|
|
328
392
|
// second copy of the turn, so fail the attempt instead.
|
|
329
393
|
throw error;
|
|
330
394
|
}
|
|
331
|
-
|
|
395
|
+
reconnectStarted ??= Date.now();
|
|
396
|
+
const attempts = resumeAfter ? policy.attempts : Math.min(6, policy.attempts);
|
|
397
|
+
if (attempt >= attempts || remaining() <= 0)
|
|
332
398
|
throw error;
|
|
333
399
|
// The Router keeps producing under this turn id, so waiting costs the
|
|
334
400
|
// turn nothing: the next attempt resumes from the cursor, not from zero.
|
|
335
|
-
const delayMs = reconnectDelay(policy, attempt);
|
|
401
|
+
const delayMs = Math.min(reconnectDelay(policy, attempt), remaining());
|
|
336
402
|
const reason = error instanceof Error ? error.message : String(error);
|
|
337
403
|
runtimeDiag('warn', `[ztc] zrouter stream dropped (${reason}); reconnecting in ${delayMs}ms (attempt ${attempt + 1}/${policy.attempts})`);
|
|
338
|
-
|
|
404
|
+
const publicReason = isRetryableResumeStatus(error)
|
|
405
|
+
? `Router temporarily unavailable ${reason.match(/\(\d{3}\)/)?.[0] ?? ''}. Resuming the response.`
|
|
406
|
+
: reason;
|
|
407
|
+
request.onTransportRetry?.({ attempt, maxAttempts: attempts, delayMs, reason: publicReason });
|
|
339
408
|
await waitForReconnect(delayMs, request.signal);
|
|
409
|
+
if (remaining() <= 0)
|
|
410
|
+
throw error;
|
|
340
411
|
}
|
|
341
412
|
}
|
|
342
413
|
}
|
|
@@ -387,6 +458,27 @@ async function waitForReconnect(delayMs, signal) {
|
|
|
387
458
|
signal?.addEventListener('abort', abort, { once: true });
|
|
388
459
|
});
|
|
389
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Diagnostics that survive a body-less failure.
|
|
463
|
+
*
|
|
464
|
+
* A gateway that throws before it can classify anything often answers with a
|
|
465
|
+
* bare `Server Error` and puts the identifying information in the headers
|
|
466
|
+
* instead. Fly (Router's host) stamps a request id on every response; carrying
|
|
467
|
+
* it into the error is the only way to correlate a client-visible 500 with the
|
|
468
|
+
* server-side exception, since the body has nothing to correlate on.
|
|
469
|
+
*
|
|
470
|
+
* Bounded and header-only: never reads the body (already consumed), and caps
|
|
471
|
+
* each value so a hostile/proxied header cannot bloat an error message.
|
|
472
|
+
*/
|
|
473
|
+
function failureHeaderSuffix(response) {
|
|
474
|
+
const parts = [];
|
|
475
|
+
for (const name of ['fly-request-id', 'x-request-id', 'cf-ray', 'x-zerg-error-code']) {
|
|
476
|
+
const value = response.headers.get(name)?.trim();
|
|
477
|
+
if (value && value.length <= 128 && !/[\r\n\0]/.test(value))
|
|
478
|
+
parts.push(`${name}=${value}`);
|
|
479
|
+
}
|
|
480
|
+
return parts.length > 0 ? ` [${parts.join(' ')}]` : '';
|
|
481
|
+
}
|
|
390
482
|
/**
|
|
391
483
|
* True for a `ZRouter API error (NNN)` a resume is worth retrying: a gateway or
|
|
392
484
|
* rate-limit failure that is expected to clear. Kept separate from
|
|
@@ -415,3 +507,15 @@ function normalizeHeaderValue(value, label, maxLength) {
|
|
|
415
507
|
}
|
|
416
508
|
return normalized;
|
|
417
509
|
}
|
|
510
|
+
/** A wire-only projection: saved tool history remains byte-for-byte intact. */
|
|
511
|
+
function chatOnlyRequest(request) {
|
|
512
|
+
return { ...request, tools: [], messages: request.messages.map(message => ({ ...message,
|
|
513
|
+
content: typeof message.content === 'string' ? message.content : message.content.map(block => {
|
|
514
|
+
if (block.type === 'tool_use')
|
|
515
|
+
return { type: 'text', text: `Previous tool call ${block.name} (${block.id}): ${JSON.stringify(block.input)}` };
|
|
516
|
+
if (block.type === 'tool_result')
|
|
517
|
+
return { type: 'text', text: `Previous tool result (${block.tool_use_id}): ${typeof block.content === 'string' ? block.content : block.content.map(part => part.type === 'text' ? part.text : '[image]').join('\n')}` };
|
|
518
|
+
return block;
|
|
519
|
+
}),
|
|
520
|
+
})) };
|
|
521
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { compactConversation, describeCompaction, smartSummarizer, localSummarizer } from '../../utils/compaction.js';
|
|
2
|
-
import { getContextStatus, contextStatusFromUsed, formatTokenCount } from '../../utils/context.js';
|
|
2
|
+
import { estimateConversationTokens, getContextStatus, contextStatusFromUsed, formatTokenCount } from '../../utils/context.js';
|
|
3
3
|
import { MAX_COMPACTION_TARGET_PERCENT, MIN_COMPACTION_TARGET_PERCENT, resolveCompactionSettings, resolveCompactionTargetPercent, resolveContextTokenBudget, tokenTriggerActive, } from '../../config/compaction.js';
|
|
4
4
|
export const compactCommand = {
|
|
5
5
|
name: 'compact',
|
|
@@ -18,9 +18,10 @@ export const compactCommand = {
|
|
|
18
18
|
// The "before" percentage should match the context bar (the real provider
|
|
19
19
|
// token count); fall back to the char/4 estimate only before the first call.
|
|
20
20
|
const realUsed = ctx.getContextTokens?.() ?? 0;
|
|
21
|
+
const catalogWindow = ctx.config.getModelContextWindow?.(model);
|
|
21
22
|
const status = realUsed > 0
|
|
22
|
-
? contextStatusFromUsed(realUsed, model, ctx.getContextWindow?.())
|
|
23
|
-
: getContextStatus(messages, model);
|
|
23
|
+
? contextStatusFromUsed(realUsed, model, catalogWindow !== undefined ? catalogWindow : ctx.getContextWindow?.())
|
|
24
|
+
: getContextStatus(messages, model, catalogWindow);
|
|
24
25
|
// An explicit argument wins; otherwise the configured target applies, so
|
|
25
26
|
// /compact and auto-compaction aim at the same place.
|
|
26
27
|
const targetPercent = args[0] ? parseInt(args[0], 10) : resolveCompactionTargetPercent(ctx.config.get());
|
|
@@ -55,7 +56,7 @@ export const compactCommand = {
|
|
|
55
56
|
? resolveContextTokenBudget(ctx.config.get())
|
|
56
57
|
: undefined;
|
|
57
58
|
const targetTokens = budget === undefined
|
|
58
|
-
? undefined
|
|
59
|
+
? (catalogWindow === undefined ? undefined : Math.max(1, Math.floor((catalogWindow ?? estimateConversationTokens(messages)) * targetPercent / 100)))
|
|
59
60
|
: Math.floor(budget * (targetPercent / 100));
|
|
60
61
|
const result = await compactConversation(messages, model, {
|
|
61
62
|
method: compaction.method,
|
|
@@ -74,7 +75,7 @@ export const compactCommand = {
|
|
|
74
75
|
...(msg.metadata ? { metadata: msg.metadata } : {}),
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
|
-
const newStatus = getContextStatus(result.messages, model);
|
|
78
|
+
const newStatus = getContextStatus(result.messages, model, catalogWindow);
|
|
78
79
|
// Reactively shrink the context bar to the compacted size — the backend
|
|
79
80
|
// won't report this until the next turn, and the bar would otherwise stay
|
|
80
81
|
// stuck at the pre-compaction (larger) value.
|
|
@@ -894,7 +894,10 @@ Usage: /config context-progress-bar <enabled|accent|warning|critical|track|reset
|
|
|
894
894
|
return;
|
|
895
895
|
}
|
|
896
896
|
case 'effort': {
|
|
897
|
-
const
|
|
897
|
+
const catalog = ctx.config.getRoutedModelCatalog?.();
|
|
898
|
+
const EFFORT_LEVELS = catalog?.version === 1
|
|
899
|
+
? catalog.models.find(row => row.owned_by === ctx.config.getProvider() && row.id === getEffectiveModel(ctx))?.capabilities?.reasoning?.choices ?? []
|
|
900
|
+
: CODEX_REASONING_EFFORTS;
|
|
898
901
|
const CLEAR_WORDS = ['clear', 'default', 'unset', 'reset'];
|
|
899
902
|
const config = ctx.config.get();
|
|
900
903
|
const activeEffort = () => `Active (${config.provider} · ${config.model}): ${formatReasoningEffort(resolveCodexEffort(ctx.config.get(), config.provider, config.model))}`;
|
|
@@ -55,7 +55,12 @@ export const contextCommand = {
|
|
|
55
55
|
const estimated = realTokens <= 0;
|
|
56
56
|
const usedTokens = estimated ? estimateConversationTokens(messages) : realTokens;
|
|
57
57
|
// Use the provider-reported window the bar used (Codex) so /context agrees.
|
|
58
|
-
const
|
|
58
|
+
const catalogWindow = ctx.config.getModelContextWindow?.(model);
|
|
59
|
+
const status = contextStatusFromUsed(usedTokens, model, catalogWindow !== undefined ? catalogWindow : estimated ? undefined : ctx.getContextWindow?.());
|
|
60
|
+
if (status.total === 0) {
|
|
61
|
+
ctx.addMessage({ role: 'system', content: `Context: ${formatTokens(usedTokens)} tokens${estimated ? ' (estimated)' : ''}. Context limit is unknown for this Router model.` });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
59
64
|
const total = status.total;
|
|
60
65
|
const usedPct = status.percentage;
|
|
61
66
|
const freeTokens = Math.max(0, total - usedTokens);
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import { CODEX_REASONING_EFFORTS } from '../../config/types.js';
|
|
2
2
|
import { resolveCodexEffort } from '../../config/defaults.js';
|
|
3
3
|
import { getSupportedCodexReasoningEfforts } from '../../utils/models.js';
|
|
4
|
-
import { formatReasoningEffort, parseReasoningSelection } from '../../utils/reasoning.js';
|
|
4
|
+
import { formatReasoningEffort, parseReasoningSelection, reasoningCommandChoices } from '../../utils/reasoning.js';
|
|
5
|
+
import { resolveRouterModel } from '../../utils/router_catalog.js';
|
|
5
6
|
async function configureReasoning(kind, args, ctx) {
|
|
6
7
|
const config = ctx.config.get();
|
|
7
8
|
const provider = ctx.config.getProvider?.() || config.provider || 'anthropic';
|
|
8
9
|
const model = ctx.config.getModel?.() || config.model;
|
|
9
|
-
const
|
|
10
|
-
|
|
10
|
+
const surface = ctx.config.getReasoningWireSurface?.(provider, model)
|
|
11
|
+
?? (config.authMode === 'zrouter' || provider === 'openai_compatible' ? 'chat_tools' : 'native_api');
|
|
12
|
+
const catalog = ctx.config.getRoutedModelCatalog?.();
|
|
13
|
+
const capabilities = catalog?.version === 1 ? resolveRouterModel(catalog, provider, model)?.capabilities : undefined;
|
|
14
|
+
const current = resolveCodexEffort(config, provider, model, capabilities ?? surface);
|
|
15
|
+
const supported = catalog?.version === 1 ? capabilities?.reasoning?.choices ?? [] : getSupportedCodexReasoningEfforts(model, provider, surface);
|
|
16
|
+
const contract = ctx.config.getReasoningContract?.(provider, model)
|
|
17
|
+
?? (catalog?.version === 1 ? capabilities?.reasoning ?? [] : surface);
|
|
18
|
+
const thinkingChoices = reasoningCommandChoices('thinking', provider, model, contract);
|
|
11
19
|
const status = () => {
|
|
12
|
-
const effective = resolveCodexEffort(ctx.config.get(), provider, model);
|
|
20
|
+
const effective = resolveCodexEffort(ctx.config.get(), provider, model, capabilities ?? surface);
|
|
13
21
|
// A startup flag is this process's value, never the saved default, so say
|
|
14
22
|
// where an effort that came from one is coming from.
|
|
15
23
|
const processEffort = ctx.config.getCodexEffortOverride?.();
|
|
@@ -20,7 +28,7 @@ async function configureReasoning(kind, args, ctx) {
|
|
|
20
28
|
ctx.addMessage({ role: 'system', content: status() +
|
|
21
29
|
(supported?.length === 0 ? '\nThis model does not support a reasoning-effort setting.' :
|
|
22
30
|
`\nSupported: ${(supported ?? CODEX_REASONING_EFFORTS).join(', ')}. /effort default inherits provider/global defaults.`) +
|
|
23
|
-
(
|
|
31
|
+
(thinkingChoices.length ? `\nThinking settings: ${thinkingChoices.join(', ')}.` : '') });
|
|
24
32
|
return;
|
|
25
33
|
}
|
|
26
34
|
if (args.length !== 1) {
|
|
@@ -29,7 +37,7 @@ async function configureReasoning(kind, args, ctx) {
|
|
|
29
37
|
}
|
|
30
38
|
let level;
|
|
31
39
|
try {
|
|
32
|
-
level = parseReasoningSelection(provider, model, current, { [kind]: args[0] });
|
|
40
|
+
level = parseReasoningSelection(provider, model, current, { [kind]: args[0] }, contract);
|
|
33
41
|
}
|
|
34
42
|
catch (error) {
|
|
35
43
|
ctx.addMessage({ role: 'system', content: error.message });
|
|
@@ -6,10 +6,20 @@ const MODEL_SUBCOMMANDS = ['list', 'latest', 'set'];
|
|
|
6
6
|
export const modelCommand = {
|
|
7
7
|
name: 'model',
|
|
8
8
|
description: 'Select provider and model, or set a model on the current provider',
|
|
9
|
-
usage: '<provider> [model] | zergrouter <provider> [model] | <model-id> | list [provider] | latest [provider] | set <model-id>',
|
|
9
|
+
usage: '<provider> [model] | zergrouter <provider> [model] | <model-id> | list [provider] | latest [provider] | set <model-id> | refresh',
|
|
10
10
|
agentRunnable: true,
|
|
11
11
|
category: 'config',
|
|
12
|
-
handler: (args, ctx) => {
|
|
12
|
+
handler: async (args, ctx) => {
|
|
13
|
+
if (args[0] === 'refresh' && ctx.models.catalog) {
|
|
14
|
+
try {
|
|
15
|
+
const catalog = await ctx.models.catalog(undefined, { refresh: true });
|
|
16
|
+
ctx.addMessage({ role: 'system', content: `${catalog.authRequired ? 'Router authentication rejected. Run ztc auth login again. Showing cached catalog' : catalog.cached ? 'Router unavailable. Showing cached catalog' : 'Router catalog refreshed'}: ${catalog.models.length} models. Active responses continue with their current selection.` });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
ctx.addMessage({ role: 'system', content: `Could not refresh Router models: ${error.message}` });
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
13
23
|
if (args.length > 0 && MODEL_SUBCOMMANDS.includes(args[0].toLowerCase())) {
|
|
14
24
|
return modelsCommand.handler(args, ctx);
|
|
15
25
|
}
|
|
@@ -6,12 +6,13 @@ const USAGE = [
|
|
|
6
6
|
' /profile <name> switch this session to an existing profile',
|
|
7
7
|
' /profile use <name> same as above',
|
|
8
8
|
' /profile create <name> create and switch to a custom profile',
|
|
9
|
+
' /profile duplicate [name] copy the active profile and switch to the copy',
|
|
9
10
|
].join('\n');
|
|
10
11
|
const OLD_BENCHMARK_COMMANDS = new Set(['show', 'gates', 'command', 'run', 'optimize', 'sweep']);
|
|
11
12
|
export const profileCommand = {
|
|
12
13
|
name: 'profile',
|
|
13
|
-
description: 'List and
|
|
14
|
-
usage: '[list|<name>|use <name>|create <name
|
|
14
|
+
description: 'List, switch, create, and duplicate configuration profiles',
|
|
15
|
+
usage: '[list|<name>|use <name>|create <name>|duplicate [name]]',
|
|
15
16
|
handler: async (args, ctx) => {
|
|
16
17
|
const [action, ...rest] = args;
|
|
17
18
|
if (!action || action === 'list' || action === 'configs' || action === 'help') {
|
|
@@ -31,6 +32,24 @@ export const profileCommand = {
|
|
|
31
32
|
ctx.addMessage({ role: 'system', content: `Config profiles (* active):\n${lines.join('\n')}${!action ? `\n\n${USAGE}` : ''}` });
|
|
32
33
|
return;
|
|
33
34
|
}
|
|
35
|
+
if (action === 'duplicate' || action === 'copy' || action === 'clone') {
|
|
36
|
+
if (rest.length > 1) {
|
|
37
|
+
ctx.addMessage({ role: 'system', content: USAGE });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const source = activeProfile();
|
|
41
|
+
const target = rest[0] ?? `${source}-copy`;
|
|
42
|
+
try {
|
|
43
|
+
if (!ctx.duplicateProfile)
|
|
44
|
+
throw new Error('This renderer cannot switch config profiles in place.');
|
|
45
|
+
const name = await ctx.duplicateProfile(target, source);
|
|
46
|
+
ctx.addMessage({ role: 'system', content: `Duplicated config profile ${source} to ${name} and switched to it.` });
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
ctx.addMessage({ role: 'system', content: `Profile duplicate failed: ${error.message}` });
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
34
53
|
if (OLD_BENCHMARK_COMMANDS.has(action)) {
|
|
35
54
|
ctx.addMessage({ role: 'system', content: 'Use /emulation to inspect prompt modes. Profile benchmark gates remain available through npm run profiles:baseline.' });
|
|
36
55
|
return;
|
|
@@ -4,6 +4,7 @@ import { applyRuntimeModelSelection, getEffectiveModel } from './runtime_model.j
|
|
|
4
4
|
import { canRouteThroughRouter, canonicalProvider } from './provider_choices.js';
|
|
5
5
|
import { resolveModelAndProvider } from '../../panes/model_catalog.js';
|
|
6
6
|
import { getDefaultModel } from '../../utils/models.js';
|
|
7
|
+
import { resolveRouterModel } from '../../utils/router_catalog.js';
|
|
7
8
|
/** Apply a provider and optional model in one validated config transition. */
|
|
8
9
|
export async function selectProviderModel(args, ctx) {
|
|
9
10
|
const values = args.filter(Boolean);
|
|
@@ -27,11 +28,23 @@ export async function selectProviderModel(args, ctx) {
|
|
|
27
28
|
const authMode = routed ? 'zrouter' : 'direct';
|
|
28
29
|
const defaultModel = provider === 'inception' ? getDefaultModel(provider) : getDefaultModelForProvider(provider);
|
|
29
30
|
const explicitModel = values[choice === 'zergrouter' ? 2 : 1];
|
|
30
|
-
|
|
31
|
+
let model = explicitModel || (provider === current ? getEffectiveModel(ctx)
|
|
31
32
|
: provider === 'openai_compatible'
|
|
32
33
|
? (ctx.config.getOpenAICompatibleSettings?.().defaultModel || getDefaultModelForProvider(provider))
|
|
33
34
|
: resolveModelAndProvider(defaultModel, provider).model);
|
|
34
35
|
try {
|
|
36
|
+
if (routed && ctx.models.catalog) {
|
|
37
|
+
const catalog = await ctx.models.catalog().catch(() => undefined);
|
|
38
|
+
if (catalog?.version === 1) {
|
|
39
|
+
const models = catalog.models.filter(row => row.owned_by === provider && row.capabilities?.chat);
|
|
40
|
+
if (!explicitModel)
|
|
41
|
+
model = models.find(row => row.is_default)?.id ?? models[0]?.id ?? '';
|
|
42
|
+
const selected = resolveRouterModel(catalog, provider, model);
|
|
43
|
+
if (!selected?.capabilities?.chat)
|
|
44
|
+
throw new Error('Model is not in the Router catalog. Refresh /model.');
|
|
45
|
+
model = selected.id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
35
48
|
const llm = ctx.config.resolveLLMBackendConfig({ provider, model,
|
|
36
49
|
config: { ...ctx.config.get(), provider, model, authMode } });
|
|
37
50
|
if (llm.authMode !== authMode) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parseReasoningSelection } from '../../utils/reasoning.js';
|
|
2
|
+
import { resolveRouterModel } from '../../utils/router_catalog.js';
|
|
3
|
+
import { getSupportedCodexReasoningEfforts } from '../../utils/models.js';
|
|
1
4
|
import { resolveExecutionSettings } from '../../config/execution.js';
|
|
2
5
|
import { accountsCommand } from './accounts.js';
|
|
3
6
|
import { authCommand } from './auth.js';
|
|
@@ -132,6 +135,8 @@ export async function buildSettingsCategories(ctx) {
|
|
|
132
135
|
const routerSignedIn = routerCredentials !== null && Date.parse(routerCredentials.expiresAt) > Date.now();
|
|
133
136
|
const provider = ctx.config.getProvider();
|
|
134
137
|
const model = ctx.config.getModel?.() || config.model;
|
|
138
|
+
const reasoningEfforts = getSupportedCodexReasoningEfforts(model, provider, ctx.config.getReasoningWireSurface?.(provider, model)
|
|
139
|
+
?? (config.authMode === 'zrouter' || provider === 'openai_compatible' ? 'chat_tools' : 'native_api'));
|
|
135
140
|
const claudeCliInstalled = isClaudeCliAvailable();
|
|
136
141
|
const claudeCodeTokenPresent = Boolean(process.env.CLAUDE_CODE_OAUTH_TOKEN?.trim());
|
|
137
142
|
const claudeCodeReady = hasUsableClaudeCodeCliAuth();
|
|
@@ -168,6 +173,15 @@ export async function buildSettingsCategories(ctx) {
|
|
|
168
173
|
modelOptions = models.slice(0, 20).map(m => ({ value: m, label: m }));
|
|
169
174
|
}
|
|
170
175
|
catch { /* fallback: no pre-fetched options, user can type manually */ }
|
|
176
|
+
const routerCatalog = ctx.config.getRoutedModelCatalog?.();
|
|
177
|
+
const routerDescriptor = routerCatalog?.version === 1
|
|
178
|
+
? resolveRouterModel(routerCatalog, provider, model) : undefined;
|
|
179
|
+
if (routerCatalog?.version === 1)
|
|
180
|
+
modelOptions = routerCatalog.models.filter(row => row.owned_by === provider && row.capabilities?.chat)
|
|
181
|
+
.map(row => ({ value: row.id, label: `${row.display_name || row.id}${row.capabilities?.tools === false ? ' (chat only)' : ''}${row.available === false ? ' (access unavailable)' : ''}` }));
|
|
182
|
+
const reasoningChoices = routerCatalog?.version === 1 ? routerDescriptor?.capabilities?.reasoning?.choices ?? []
|
|
183
|
+
: reasoningEfforts ?? CODEX_REASONING_EFFORTS;
|
|
184
|
+
const effortSupported = config.codexEffort === undefined || reasoningChoices.includes(config.codexEffort);
|
|
171
185
|
const sections = [
|
|
172
186
|
{
|
|
173
187
|
name: 'Config Scope',
|
|
@@ -242,7 +256,8 @@ export async function buildSettingsCategories(ctx) {
|
|
|
242
256
|
// option matches a canonical PROVIDERS value instead of nothing.
|
|
243
257
|
value: normalizeProviderId(provider),
|
|
244
258
|
type: 'select',
|
|
245
|
-
options:
|
|
259
|
+
options: routerCatalog?.version === 1 ? [...new Map(routerCatalog.models.filter(row => row.capabilities?.chat)
|
|
260
|
+
.map(row => [row.owned_by, { value: row.owned_by, label: row.provider_display_name || row.owned_by }])).values()] : PROVIDERS,
|
|
246
261
|
},
|
|
247
262
|
{
|
|
248
263
|
name: 'model',
|
|
@@ -258,13 +273,15 @@ export async function buildSettingsCategories(ctx) {
|
|
|
258
273
|
value: config.maxTokens === undefined ? 'provider default' : String(config.maxTokens),
|
|
259
274
|
type: 'text',
|
|
260
275
|
placeholder: 'provider default',
|
|
261
|
-
description:
|
|
276
|
+
description: `Maximum output tokens, including reasoning tokens. ${routerDescriptor?.capabilities?.max_output_tokens ? `Router limit: ${routerDescriptor.capabilities.max_output_tokens.toLocaleString()}. ` : ''}Enter a positive whole number, or "provider default" to clear the limit.`,
|
|
262
277
|
},
|
|
263
278
|
{
|
|
264
279
|
name: 'codexEffort', label: 'Default reasoning effort',
|
|
265
|
-
value: config.codexEffort ?? 'provider default', type: 'select',
|
|
266
|
-
options: [{ value: 'provider default', label: 'provider default' }, ...
|
|
267
|
-
description:
|
|
280
|
+
value: effortSupported ? config.codexEffort ?? 'provider default' : 'provider default', type: 'select',
|
|
281
|
+
options: [{ value: 'provider default', label: 'provider default' }, ...reasoningChoices.map(level => ({ value: level, label: level }))],
|
|
282
|
+
description: reasoningChoices.length === 0
|
|
283
|
+
? `This model takes no reasoning-effort setting (${provider} · ${model}).`
|
|
284
|
+
: `Levels supported by ${provider} · ${model}. Model and provider overrides take precedence.`,
|
|
268
285
|
},
|
|
269
286
|
{
|
|
270
287
|
name: 'codexFastMode', label: 'Fast mode', value: String(config.codexFastMode === true), type: 'select',
|
|
@@ -1058,6 +1075,18 @@ function applySettingChange(ctx, setting, value) {
|
|
|
1058
1075
|
}
|
|
1059
1076
|
switch (setting) {
|
|
1060
1077
|
case 'provider':
|
|
1078
|
+
{
|
|
1079
|
+
const catalog = ctx.config.getRoutedModelCatalog?.();
|
|
1080
|
+
if (catalog?.version === 1) {
|
|
1081
|
+
const rows = catalog.models.filter(row => row.owned_by === value && row.capabilities?.chat);
|
|
1082
|
+
const selected = rows.find(row => row.is_default) ?? rows[0];
|
|
1083
|
+
if (!selected)
|
|
1084
|
+
throw new Error('Provider is not in the Router chat catalog.');
|
|
1085
|
+
ctx.config.setProvider(value);
|
|
1086
|
+
ctx.config.set('model', selected.id);
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1061
1090
|
ctx.config.setProvider(value);
|
|
1062
1091
|
{
|
|
1063
1092
|
const resetModel = resolveModelForProviderChange(value, getEffectiveModel(ctx));
|
|
@@ -1082,10 +1111,19 @@ function applySettingChange(ctx, setting, value) {
|
|
|
1082
1111
|
break;
|
|
1083
1112
|
}
|
|
1084
1113
|
case 'codexEffort':
|
|
1085
|
-
|
|
1114
|
+
const catalog = ctx.config.getRoutedModelCatalog?.();
|
|
1115
|
+
const choices = catalog?.version === 1 ? resolveRouterModel(catalog, ctx.config.getProvider(), getEffectiveModel(ctx))?.capabilities?.reasoning?.choices ?? [] : CODEX_REASONING_EFFORTS;
|
|
1116
|
+
if (value !== 'provider default' && !choices.includes(value)) {
|
|
1086
1117
|
throw new Error(`Unknown reasoning effort: ${value}`);
|
|
1087
1118
|
}
|
|
1088
|
-
ctx.config.
|
|
1119
|
+
const activeConfig = ctx.config.get();
|
|
1120
|
+
const activeProvider = ctx.config.getProvider();
|
|
1121
|
+
const activeModel = ctx.config.getModel?.() || activeConfig.model;
|
|
1122
|
+
const selectedEffort = parseReasoningSelection(activeProvider, activeModel, undefined, { effort: value === 'provider default' ? 'default' : value }, catalog?.version === 1
|
|
1123
|
+
? resolveRouterModel(catalog, activeProvider, activeModel)?.capabilities?.reasoning?.choices ?? []
|
|
1124
|
+
: ctx.config.getReasoningWireSurface?.(activeProvider, activeModel)
|
|
1125
|
+
?? (activeConfig.authMode === 'zrouter' || activeProvider === 'openai_compatible' ? 'chat_tools' : 'native_api'));
|
|
1126
|
+
ctx.config.set('codexEffort', selectedEffort ?? undefined);
|
|
1089
1127
|
break;
|
|
1090
1128
|
case 'codexFastMode':
|
|
1091
1129
|
ctx.config.set('codexFastMode', value === 'true');
|