@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,3 +1,5 @@
|
|
|
1
|
+
import type { ReasoningWireSurface } from '@zerg/model-contracts';
|
|
2
|
+
import type { ReasoningContract } from '../../utils/reasoning.js';
|
|
1
3
|
import { AgentState, Message, FormFieldDef, type TodoStatus } from '../../types.js';
|
|
2
4
|
import { ZTCConfig, ZTCAuthMode, ZTCConfigScope, OpenAICompatibleSettings, ZTCPermissionProfile, IntentLayerSettings, WorkflowSettings, CodexReasoningEffort } from '../../config/types.js';
|
|
3
5
|
import type { AuthContext, ResolvedLLMConfig } from '../../config/resolved.js';
|
|
@@ -9,6 +11,11 @@ import type { BenchController } from '../../bench/controller.js';
|
|
|
9
11
|
import type { SlashUsage } from './usage_spec.js';
|
|
10
12
|
import type { KeyEvent } from '../../ui/widgets/types.js';
|
|
11
13
|
export interface ConfigController {
|
|
14
|
+
getRouterCatalogConnection?: () => import('../../utils/router_catalog.js').RouterCatalogConnection | undefined;
|
|
15
|
+
getRoutedModelCatalog?: () => import('../../utils/router_catalog.js').RouterCatalog | undefined;
|
|
16
|
+
adoptRouterCatalog?(catalog: import('../../utils/router_catalog.js').RouterCatalog): Promise<void>;
|
|
17
|
+
getRouterCatalog?: () => import('../../utils/router_catalog.js').RouterCatalog | undefined;
|
|
18
|
+
getModelContextWindow?: (model?: string, provider?: string) => number | null | undefined;
|
|
12
19
|
locationLabel?: string;
|
|
13
20
|
get: () => ZTCConfig;
|
|
14
21
|
set: <K extends keyof ZTCConfig>(key: K, value: ZTCConfig[K]) => void;
|
|
@@ -49,6 +56,8 @@ export interface ConfigController {
|
|
|
49
56
|
getModel?: () => string;
|
|
50
57
|
setModelEffort: (model: string, level: CodexReasoningEffort | null) => void;
|
|
51
58
|
/** Effort chosen by a startup `--effort` for this process only. */
|
|
59
|
+
getReasoningWireSurface?: (provider?: string, model?: string) => ReasoningWireSurface;
|
|
60
|
+
getReasoningContract?: (provider?: string, model?: string) => ReasoningContract;
|
|
52
61
|
getCodexEffortOverride?: () => CodexReasoningEffort | undefined;
|
|
53
62
|
/** Drop the process-only effort override after an explicit in-session choice. */
|
|
54
63
|
clearCodexEffortOverride?: () => void;
|
|
@@ -93,9 +102,13 @@ export interface ClipboardController {
|
|
|
93
102
|
writeText: (text: string) => Promise<void>;
|
|
94
103
|
}
|
|
95
104
|
export interface ModelsController {
|
|
105
|
+
catalog?: (signal?: AbortSignal, options?: {
|
|
106
|
+
refresh?: boolean;
|
|
107
|
+
}) => Promise<import('../../utils/router_catalog.js').RouterCatalog>;
|
|
96
108
|
list: (provider?: string, options?: {
|
|
97
109
|
signal?: AbortSignal;
|
|
98
110
|
authMode?: 'direct' | 'zrouter';
|
|
111
|
+
refresh?: boolean;
|
|
99
112
|
}) => Promise<string[]>;
|
|
100
113
|
}
|
|
101
114
|
export interface SkillsController {
|
|
@@ -293,6 +306,12 @@ export interface CommandContext {
|
|
|
293
306
|
}) => void;
|
|
294
307
|
/** Reconfigure this running session from another config profile. */
|
|
295
308
|
switchConfigProfile?: (name: string) => Promise<void>;
|
|
309
|
+
/**
|
|
310
|
+
* Copy a profile's settings into a new one and switch to it, returning the
|
|
311
|
+
* new profile's name. Optional exactly as `switchConfigProfile` is: a
|
|
312
|
+
* renderer that cannot switch in place cannot duplicate either.
|
|
313
|
+
*/
|
|
314
|
+
duplicateProfile?: (name: string, source: string) => Promise<string>;
|
|
296
315
|
/**
|
|
297
316
|
* Apply a provider/model selection to the currently running process. This
|
|
298
317
|
* updates any session-local runtime override before recreating the agent, so
|
package/dist/agent/factory.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { readRouterCatalog } from '../utils/router_catalog.js';
|
|
1
2
|
import { Agent } from './agent.js';
|
|
2
|
-
import {
|
|
3
|
+
import { ZergAPIBackend, ZRouterStreamBackend, createBackendForProvider } from './backends/index.js';
|
|
3
4
|
import { CODEX_AUTH_SENTINEL } from './backends/codex_auth.js';
|
|
4
5
|
import { CLAUDE_CODE_AUTH_SENTINEL } from './backends/claude_cli.js';
|
|
5
6
|
import { KIMI_CODE_AUTH_SENTINEL } from './backends/kimi_code_auth.js';
|
|
@@ -232,7 +233,17 @@ export function resolveAgentBackendFromConfig(options) {
|
|
|
232
233
|
runtimeDiag('error', '[ztc] Modal requires Zerg Router authentication. Run ztc auth login and use authMode zrouter.');
|
|
233
234
|
return null;
|
|
234
235
|
}
|
|
235
|
-
if (
|
|
236
|
+
if (useProxy) {
|
|
237
|
+
// Route selection is resolved before any provider-specific backend.
|
|
238
|
+
apiKey = proxyToken;
|
|
239
|
+
const connection = configStore.getRouterCatalogConnection(proxyUrl);
|
|
240
|
+
const workspaceId = connection?.token === apiKey ? connection.workspaceId : undefined;
|
|
241
|
+
runtimeDiag('info', `[ztc] routing LLM via ZergRouter proxy ${JSON.stringify({ proxy_url: proxyUrl, provider, has_token: Boolean(proxyToken) })}`);
|
|
242
|
+
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider, conversationId: options.sessionId,
|
|
243
|
+
workspaceId,
|
|
244
|
+
catalog: readRouterCatalog({ origin: proxyUrl, token: apiKey, workspaceId }) });
|
|
245
|
+
}
|
|
246
|
+
else if (isCodexAppServerProvider(provider)) {
|
|
236
247
|
try {
|
|
237
248
|
backend = createBackendForProvider(provider, {
|
|
238
249
|
apiKey: '',
|
|
@@ -297,71 +308,6 @@ export function resolveAgentBackendFromConfig(options) {
|
|
|
297
308
|
}
|
|
298
309
|
apiKey = KIMI_CODE_AUTH_SENTINEL;
|
|
299
310
|
}
|
|
300
|
-
else if (useProxy) {
|
|
301
|
-
// Proxy mode: stream through the platform ZRouter proxy.
|
|
302
|
-
//
|
|
303
|
-
// ZRouter emits a UNIFIED custom SSE ({type:delta|usage|done}) for ALL
|
|
304
|
-
// providers — on /v1/messages (anthropic) AND /v1/chat/completions (openai
|
|
305
|
-
// et al.), NOT the provider-native SSE. So the SDK-shaped backends
|
|
306
|
-
// (AnthropicBackend / OpenAICompatibleBackend) can't parse the proxy stream
|
|
307
|
-
// (they expect native SSE) and would render empty. ZRouterStreamBackend
|
|
308
|
-
// speaks the unified format for every provider, so route all proxy arms
|
|
309
|
-
// through it. The one exception is a user-supplied direct Gemini key, which
|
|
310
|
-
// hits Google natively and skips the proxy.
|
|
311
|
-
apiKey = proxyToken;
|
|
312
|
-
// Proof-chain log: confirms the agent chose the router (visible in the
|
|
313
|
-
// container's logs via ZergCloud /container-logs). Never logs the token.
|
|
314
|
-
runtimeDiag('info', `[ztc] routing LLM via ZergRouter proxy ${JSON.stringify({
|
|
315
|
-
proxy_url: proxyUrl,
|
|
316
|
-
provider,
|
|
317
|
-
has_token: Boolean(proxyToken),
|
|
318
|
-
})}`);
|
|
319
|
-
// Router forwards this to Modal as a conversation pin, so the turns of one
|
|
320
|
-
// conversation keep reusing one container's cached prompt prefix. It is the
|
|
321
|
-
// ZTC session id, so it survives `--resume`; the per-process client session
|
|
322
|
-
// Router already receives does not. Undefined when the caller has no session
|
|
323
|
-
// (the backend then omits the header).
|
|
324
|
-
const conversationId = options.sessionId;
|
|
325
|
-
switch (provider) {
|
|
326
|
-
case 'openai':
|
|
327
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider: 'openai', conversationId });
|
|
328
|
-
break;
|
|
329
|
-
case 'gemini': {
|
|
330
|
-
// Auto mode prefers the user's direct Gemini key; forced Router mode
|
|
331
|
-
// keeps its authenticated proxy route. Otherwise
|
|
332
|
-
// stream through ZRouter's gemini path. Use the resolver's already-computed
|
|
333
|
-
// directApiKey — re-reading the env-first singleton would bypass the
|
|
334
|
-
// options.config snapshot / apiKey override the resolver honors.
|
|
335
|
-
const geminiDirectKey = directApiKey;
|
|
336
|
-
if (geminiDirectKey && authMode !== 'zrouter') {
|
|
337
|
-
backend = new GeminiBackend({ apiKey: geminiDirectKey });
|
|
338
|
-
apiKey = geminiDirectKey;
|
|
339
|
-
}
|
|
340
|
-
else {
|
|
341
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider: 'gemini', conversationId });
|
|
342
|
-
}
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
case 'inception':
|
|
346
|
-
// ZRouter has no inception provider path; keep the OpenAI-compatible shim.
|
|
347
|
-
backend = new OpenAICompatibleBackend({ apiKey, baseUrl: `${proxyUrl}/v1`, extraBody: { provider: 'inception' } });
|
|
348
|
-
break;
|
|
349
|
-
case 'openai_compatible':
|
|
350
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider: 'openai_compatible', conversationId });
|
|
351
|
-
break;
|
|
352
|
-
case 'kimi':
|
|
353
|
-
case 'moonshot':
|
|
354
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider: 'kimi', conversationId });
|
|
355
|
-
break;
|
|
356
|
-
case 'modal':
|
|
357
|
-
case 'deepseek':
|
|
358
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider, conversationId });
|
|
359
|
-
break;
|
|
360
|
-
case 'anthropic':
|
|
361
|
-
default:
|
|
362
|
-
backend = new ZRouterStreamBackend({ apiKey, proxyUrl: proxyUrl, provider: 'anthropic', conversationId });
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
311
|
else if (useDirectProvider) {
|
|
366
312
|
// Use direct provider with user's API key — delegate to shared factory.
|
|
367
313
|
// For openai_compatible we pass full settings (headers / apiPath) so
|
|
@@ -734,7 +680,7 @@ export function createAgentFromConfig(options) {
|
|
|
734
680
|
tracer: options.tracer,
|
|
735
681
|
hooks,
|
|
736
682
|
approvalRequester: options.hookApprovalRequester,
|
|
737
|
-
codexEffort: resolveCodexEffort(options.config, provider, effectiveModel),
|
|
683
|
+
codexEffort: resolveCodexEffort(options.config, provider, effectiveModel, backend.getModelCapabilities?.(effectiveModel)),
|
|
738
684
|
codexFastMode: options.config.codexFastMode,
|
|
739
685
|
// Codex panes never defer: dynamicTools are registered once at thread/start,
|
|
740
686
|
// so a schema hidden behind tool_search would be unreachable for the whole
|
package/dist/agent/subagent.d.ts
CHANGED
|
@@ -1,13 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SubAgent - Isolated agent for context-heavy operations
|
|
3
|
-
*
|
|
4
|
-
* Spawns a separate agent with its own context to handle tasks like:
|
|
5
|
-
* - Exploring codebases
|
|
6
|
-
* - Searching for patterns
|
|
7
|
-
* - Reading multiple files
|
|
8
|
-
*
|
|
9
|
-
* Only the final summary is returned to the main agent, keeping the main context clean.
|
|
10
|
-
*/
|
|
11
1
|
import { AgentBackend } from './backends/index.js';
|
|
12
2
|
import type { Tool } from './tools/types.js';
|
|
13
3
|
import type { Policy } from './runtime/policy.js';
|
package/dist/agent/subagent.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ZRouterStreamBackend } from './backends/zrouter_stream.js';
|
|
2
|
+
import { readRouterCatalog } from '../utils/router_catalog.js';
|
|
1
3
|
/**
|
|
2
4
|
* SubAgent - Isolated agent for context-heavy operations
|
|
3
5
|
*
|
|
@@ -44,6 +46,18 @@ function isToolUseBlock(block) {
|
|
|
44
46
|
function getSubAgentModel(provider, preferredModel) {
|
|
45
47
|
if (preferredModel)
|
|
46
48
|
return preferredModel;
|
|
49
|
+
if (configStore.resolveLLMBackendConfig({ provider }).useProxy) {
|
|
50
|
+
if (configStore.getProvider() === provider)
|
|
51
|
+
return configStore.getModel();
|
|
52
|
+
const catalog = configStore.getRouterCatalog();
|
|
53
|
+
const rows = catalog?.models.filter(row => row.owned_by === provider && (catalog.version === 0 || row.capabilities?.chat));
|
|
54
|
+
const selected = rows?.find(row => row.is_default) ?? rows?.[0];
|
|
55
|
+
if (selected)
|
|
56
|
+
return selected.id;
|
|
57
|
+
if (!configStore.getRouterCatalog() || configStore.getRouterCatalog()?.version === 0)
|
|
58
|
+
return getDefaultModel(provider, 'cheap');
|
|
59
|
+
throw new Error('Select a Router model for this subagent.');
|
|
60
|
+
}
|
|
47
61
|
return getDefaultModel(provider, 'cheap');
|
|
48
62
|
}
|
|
49
63
|
/**
|
|
@@ -57,6 +71,12 @@ function getSubAgentModel(provider, preferredModel) {
|
|
|
57
71
|
*/
|
|
58
72
|
function createBackend(provider, apiKey) {
|
|
59
73
|
const config = configStore.get();
|
|
74
|
+
const resolved = configStore.resolveLLMBackendConfig({ provider, apiKeyOverride: apiKey });
|
|
75
|
+
if (resolved.useProxy) {
|
|
76
|
+
return new ZRouterStreamBackend({ apiKey: resolved.authToken, proxyUrl: resolved.proxyUrl, provider,
|
|
77
|
+
workspaceId: configStore.getRouterCatalogConnection()?.workspaceId,
|
|
78
|
+
catalog: readRouterCatalog({ origin: resolved.proxyUrl, token: resolved.authToken, workspaceId: configStore.getRouterCatalogConnection()?.workspaceId }) });
|
|
79
|
+
}
|
|
60
80
|
const accountOpts = accountBackendOptions(config, provider);
|
|
61
81
|
const rotationOpts = subagentShouldRotate(resolveRotation(config))
|
|
62
82
|
? buildAccountRotation(config) ?? {}
|
|
@@ -109,6 +129,8 @@ export class SubAgent {
|
|
|
109
129
|
* @param opts Optional overrides (abort signal, custom system prompt, tools)
|
|
110
130
|
*/
|
|
111
131
|
async run(task, opts) {
|
|
132
|
+
if (this.backend.getModelCapabilities?.(this.model)?.tools === false)
|
|
133
|
+
throw new Error('Chat-only models cannot run tool-dependent subagents.');
|
|
112
134
|
const signal = opts?.signal;
|
|
113
135
|
const startTime = Date.now();
|
|
114
136
|
const timeoutMs = this.config.timeoutMs || 120000; // 2 minute default
|
|
@@ -72,7 +72,7 @@ export declare class ToolResponseStore {
|
|
|
72
72
|
* fraction of the active model's context window so a small-context model
|
|
73
73
|
* (gpt-4 8k → 2k effective) doesn't get crushed by a Claude-tuned default.
|
|
74
74
|
*/
|
|
75
|
-
effectiveBudget(model: string): number;
|
|
75
|
+
effectiveBudget(model: string, windowOverride?: number | null): number;
|
|
76
76
|
private cacheBudgetTokens;
|
|
77
77
|
/**
|
|
78
78
|
* Swap to a new session directory. Preloads only `meta` from each record
|
|
@@ -173,9 +173,11 @@ export class ToolResponseStore {
|
|
|
173
173
|
* fraction of the active model's context window so a small-context model
|
|
174
174
|
* (gpt-4 8k → 2k effective) doesn't get crushed by a Claude-tuned default.
|
|
175
175
|
*/
|
|
176
|
-
effectiveBudget(model) {
|
|
176
|
+
effectiveBudget(model, windowOverride) {
|
|
177
177
|
const configured = this.config.configuredInlineBudgetTokens;
|
|
178
|
-
const window = getContextWindowSize(model);
|
|
178
|
+
const window = getContextWindowSize(model, windowOverride);
|
|
179
|
+
if (window === 0)
|
|
180
|
+
return configured;
|
|
179
181
|
return Math.min(configured, Math.floor(window * PER_MODEL_BUDGET_FRACTION));
|
|
180
182
|
}
|
|
181
183
|
cacheBudgetTokens() {
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Terminal pane management tools for zmux mode.
|
|
3
|
-
*
|
|
4
|
-
* These tools allow the agent to create, read, write to, close,
|
|
5
|
-
* and list terminal panes when running inside zmux.
|
|
6
|
-
* They communicate with the zmux Rust binary via the IPC socket.
|
|
7
|
-
*/
|
|
8
1
|
import { Tool } from './types.js';
|
|
9
2
|
import type { PaneCommandTransport } from '../../ui/ratatui/pane_relay.js';
|
|
10
3
|
import type { RustEvent } from '../../ui/ratatui/protocol.js';
|
package/dist/agent/tools/pane.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { configStore } from '../../config.js';
|
|
1
2
|
/**
|
|
2
3
|
* Terminal pane management tools for zmux mode.
|
|
3
4
|
*
|
|
@@ -440,8 +441,8 @@ export function buildChildZtcPaneLaunch(extraArgs = [], childSessionId = generat
|
|
|
440
441
|
i++;
|
|
441
442
|
}
|
|
442
443
|
}
|
|
443
|
-
const resolved = model ? resolveModelAndProvider(model, provider) : { model: undefined, provider };
|
|
444
|
-
const knownProvider = isPaneProvider(resolved.provider) ? resolved.provider : undefined;
|
|
444
|
+
const resolved = model ? resolveModelAndProvider(model, provider, configStore.getResolvedAuthMode()) : { model: undefined, provider };
|
|
445
|
+
const knownProvider = isPaneProvider(resolved.provider, configStore.getResolvedAuthMode()) ? resolved.provider : undefined;
|
|
445
446
|
const fallbackArgs = [];
|
|
446
447
|
if (provider && !knownProvider)
|
|
447
448
|
fallbackArgs.push('--provider', provider);
|
|
@@ -681,7 +682,7 @@ export const createZtcPaneTool = {
|
|
|
681
682
|
let provider = args.provider ? String(args.provider) : undefined;
|
|
682
683
|
let model = args.model ? String(args.model) : undefined;
|
|
683
684
|
if (model) {
|
|
684
|
-
const resolved = resolveModelAndProvider(model, provider);
|
|
685
|
+
const resolved = resolveModelAndProvider(model, provider, configStore.getResolvedAuthMode());
|
|
685
686
|
model = resolved.model;
|
|
686
687
|
provider = resolved.provider;
|
|
687
688
|
}
|
|
@@ -725,7 +726,7 @@ export const createZtcPaneTool = {
|
|
|
725
726
|
sessionMode: 'new',
|
|
726
727
|
cwd,
|
|
727
728
|
title,
|
|
728
|
-
providerOverride: isPaneProvider(provider) ? provider : undefined,
|
|
729
|
+
providerOverride: isPaneProvider(provider, configStore.getResolvedAuthMode()) ? provider : undefined,
|
|
729
730
|
modelOverride: model,
|
|
730
731
|
extraArgs: filterChildZtcArgs(allExtraArgs),
|
|
731
732
|
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
*
|
|
4
|
-
* Shared helpers for building CommandContext objects used by command handlers.
|
|
5
|
-
* Eliminates duplication between TUI (cli_ratatui.ts) and headless (headless.ts) renderers.
|
|
6
|
-
*/
|
|
7
|
-
import { listModels } from '../utils/models.js';
|
|
1
|
+
import { discoverRouterCatalog, readRouterCatalog, routerModelIds, routerCatalogScope } from '../utils/router_catalog.js';
|
|
2
|
+
import { loadModelCatalog } from './model_catalog_cache.js';
|
|
8
3
|
import { getSkills } from '../skills/registry.js';
|
|
9
4
|
import { runShellCommand } from '../utils/shell.js';
|
|
10
5
|
import { configStore } from '../config.js';
|
|
@@ -44,18 +39,46 @@ export function routeUserInput(text) {
|
|
|
44
39
|
*/
|
|
45
40
|
export function buildModelsController(config) {
|
|
46
41
|
return {
|
|
42
|
+
catalog: async (signal, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const connection = config.getRouterCatalogConnection?.();
|
|
45
|
+
if (!connection)
|
|
46
|
+
throw new Error('Router discovery requires a Router login. Run ztc auth login.');
|
|
47
|
+
const scope = routerCatalogScope(connection);
|
|
48
|
+
signal?.throwIfAborted();
|
|
49
|
+
const previous = readRouterCatalog(connection);
|
|
50
|
+
const age = previous === undefined ? Infinity : Date.now() - previous.fetchedAt;
|
|
51
|
+
const catalog = !options?.refresh && previous && !previous.authRequired && age >= 0 && age < 30_000
|
|
52
|
+
? previous : await discoverRouterCatalog(connection, signal);
|
|
53
|
+
signal?.throwIfAborted();
|
|
54
|
+
const active = config.getRouterCatalogConnection?.();
|
|
55
|
+
if (!active || routerCatalogScope(active) !== scope)
|
|
56
|
+
throw new Error('Router discovery context changed. Retry model discovery.');
|
|
57
|
+
await config.adoptRouterCatalog?.(catalog);
|
|
58
|
+
return catalog;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const cached = config.getRouterCatalog?.();
|
|
62
|
+
if (!signal?.aborted && cached)
|
|
63
|
+
return { ...cached, ...(error instanceof Error && /authentication|expired|login|origin/i.test(error.message) ? { authRequired: true } : {}) };
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
47
67
|
list: async (provider, options) => {
|
|
68
|
+
if ((options?.authMode ?? config.getAuthMode?.()) === 'zrouter') {
|
|
69
|
+
const controller = buildModelsController(config);
|
|
70
|
+
return routerModelIds(await controller.catalog(options?.signal, options), provider ?? config.getProvider());
|
|
71
|
+
}
|
|
48
72
|
const llm = config.resolveLLMBackendConfig({ provider,
|
|
49
73
|
config: options?.authMode ? { ...config.get(), authMode: options.authMode } : undefined });
|
|
50
|
-
|
|
51
|
-
// catalog from a successful provider response with zero supported models.
|
|
52
|
-
return listModels({
|
|
74
|
+
return loadModelCatalog({
|
|
53
75
|
signal: options?.signal,
|
|
54
76
|
provider: llm.provider,
|
|
55
77
|
apiKey: llm.apiKey,
|
|
56
78
|
baseUrl: llm.provider === 'openai_compatible' ? llm.compatBaseUrl : undefined,
|
|
57
79
|
proxyUrl: llm.useProxy ? llm.proxyUrl : undefined,
|
|
58
80
|
authToken: llm.useProxy ? llm.authToken : undefined,
|
|
81
|
+
workspaceId: llm.useProxy ? config.getRouterCatalogConnection?.()?.workspaceId : undefined,
|
|
59
82
|
codexAppServerCommand: llm.codexAppServerCommand,
|
|
60
83
|
codexAppServerArgs: llm.codexAppServerArgs,
|
|
61
84
|
codexHome: llm.codexHome,
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { listModels } from '../utils/models.js';
|
|
2
|
+
type ListModelsOptions = Parameters<typeof listModels>[0];
|
|
3
|
+
/** Shared discovery for all renderers. Disk rows are previews, not entitlements. */
|
|
4
|
+
export declare function loadModelCatalog(options: ListModelsOptions, onVerified?: (models: string[]) => void): Promise<string[]>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { profileConfigDir } from '../config/profiles.js';
|
|
3
|
+
import { listModels } from '../utils/models.js';
|
|
4
|
+
import { clearCachedModels, readCachedModels, readCachedModelsFetchedAt, writeCachedModels } from '../utils/model_cache.js';
|
|
5
|
+
const FRESH_MS = 30_000;
|
|
6
|
+
const OFFLINE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
7
|
+
const MAX_SCOPES = 64;
|
|
8
|
+
const MAX_CACHED_MODELS = 512;
|
|
9
|
+
const catalogs = new Map();
|
|
10
|
+
let writes = Promise.resolve();
|
|
11
|
+
function scopeFor(options) {
|
|
12
|
+
// Never persist raw credentials. Profiles, endpoints and credential identities
|
|
13
|
+
// are separate catalogs; switching any of them must bypass the old entry.
|
|
14
|
+
const identity = [profileConfigDir(), options.provider, options.proxyUrl?.replace(/\/$/, ''),
|
|
15
|
+
options.authToken, options.workspaceId, options.baseUrl?.replace(/\/$/, ''), options.apiKey,
|
|
16
|
+
options.codexAppServerCommand, options.codexAppServerArgs, options.codexHome,
|
|
17
|
+
options.codexAuthFilePath, options.kimiCredsFilePath];
|
|
18
|
+
return `${options.provider}:${createHash('sha256').update(JSON.stringify(identity)).digest('hex')}`;
|
|
19
|
+
}
|
|
20
|
+
function persist(profile, action) {
|
|
21
|
+
writes = writes.then(async () => {
|
|
22
|
+
// A response can arrive after a profile switch. Do not write that response
|
|
23
|
+
// into the new profile's cache, even under an unreachable hashed scope.
|
|
24
|
+
if (profileConfigDir() === profile)
|
|
25
|
+
await action();
|
|
26
|
+
}).catch(() => { });
|
|
27
|
+
}
|
|
28
|
+
function joinRequest(entry, pending, signal) {
|
|
29
|
+
signal?.throwIfAborted();
|
|
30
|
+
entry.waiters++;
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
let finished = false;
|
|
33
|
+
const finish = () => {
|
|
34
|
+
if (finished)
|
|
35
|
+
return false;
|
|
36
|
+
finished = true;
|
|
37
|
+
signal?.removeEventListener('abort', abort);
|
|
38
|
+
entry.waiters--;
|
|
39
|
+
if (!entry.waiters && !entry.background)
|
|
40
|
+
entry.controller?.abort();
|
|
41
|
+
return true;
|
|
42
|
+
};
|
|
43
|
+
const abort = () => { if (finish())
|
|
44
|
+
reject(signal.reason); };
|
|
45
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
46
|
+
pending.then(models => { if (finish())
|
|
47
|
+
resolve([...models]); }, error => { if (finish())
|
|
48
|
+
reject(error); });
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function freshModels(entry) {
|
|
52
|
+
if (entry.models === undefined || entry.checkedAt === null)
|
|
53
|
+
return undefined;
|
|
54
|
+
const age = Date.now() - entry.checkedAt;
|
|
55
|
+
return age >= 0 && age < FRESH_MS ? [...entry.models] : undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Shared discovery for all renderers. Disk rows are previews, not entitlements. */
|
|
58
|
+
export async function loadModelCatalog(options, onVerified) {
|
|
59
|
+
options.signal?.throwIfAborted();
|
|
60
|
+
const profile = profileConfigDir();
|
|
61
|
+
const scope = scopeFor(options);
|
|
62
|
+
let entry = catalogs.get(scope);
|
|
63
|
+
if (entry?.controller?.signal.aborted) {
|
|
64
|
+
catalogs.delete(scope);
|
|
65
|
+
entry = undefined;
|
|
66
|
+
}
|
|
67
|
+
if (!entry) {
|
|
68
|
+
if (catalogs.size >= MAX_SCOPES) {
|
|
69
|
+
const oldest = catalogs.keys().next().value;
|
|
70
|
+
catalogs.get(oldest).controller?.abort();
|
|
71
|
+
catalogs.delete(oldest);
|
|
72
|
+
}
|
|
73
|
+
entry = { checkedAt: null, waiters: 0, background: false, diskChecked: false };
|
|
74
|
+
catalogs.set(scope, entry);
|
|
75
|
+
}
|
|
76
|
+
// Keep the map in access order for bounded eviction.
|
|
77
|
+
catalogs.delete(scope);
|
|
78
|
+
catalogs.set(scope, entry);
|
|
79
|
+
const fresh = freshModels(entry);
|
|
80
|
+
if (fresh !== undefined) {
|
|
81
|
+
onVerified?.([...fresh]);
|
|
82
|
+
return fresh;
|
|
83
|
+
}
|
|
84
|
+
if (entry.models === undefined && !entry.pending && !entry.diskChecked) {
|
|
85
|
+
entry.diskChecked = true;
|
|
86
|
+
const [cached, fetchedAt] = await Promise.all([readCachedModels(scope), readCachedModelsFetchedAt(scope)]);
|
|
87
|
+
options.signal?.throwIfAborted();
|
|
88
|
+
const age = fetchedAt === undefined ? Infinity : Date.now() - fetchedAt;
|
|
89
|
+
if (entry.models === undefined && cached.length && age >= 0 && age < OFFLINE_MAX_AGE_MS)
|
|
90
|
+
entry.models = cached;
|
|
91
|
+
}
|
|
92
|
+
if (catalogs.get(scope) !== entry)
|
|
93
|
+
throw new DOMException('Model discovery was superseded', 'AbortError');
|
|
94
|
+
const afterRead = freshModels(entry);
|
|
95
|
+
if (afterRead !== undefined) {
|
|
96
|
+
onVerified?.([...afterRead]);
|
|
97
|
+
return afterRead;
|
|
98
|
+
}
|
|
99
|
+
const current = entry;
|
|
100
|
+
const refresh = () => {
|
|
101
|
+
if (current.pending)
|
|
102
|
+
return current.pending;
|
|
103
|
+
const controller = new AbortController();
|
|
104
|
+
current.controller = controller;
|
|
105
|
+
const pending = listModels({ ...options, signal: controller.signal }).then(models => {
|
|
106
|
+
controller.signal.throwIfAborted();
|
|
107
|
+
const cacheable = models.length <= MAX_CACHED_MODELS
|
|
108
|
+
&& models.every(id => id.length <= 256 && !/[\r\n\0]/.test(id));
|
|
109
|
+
current.models = cacheable ? [...models] : undefined;
|
|
110
|
+
current.checkedAt = Date.now();
|
|
111
|
+
persist(profile, () => models.length && cacheable ? writeCachedModels(scope, models) : clearCachedModels(scope));
|
|
112
|
+
return models;
|
|
113
|
+
}).catch(error => {
|
|
114
|
+
// A denial invalidates the preview. Temporary transport errors retain it.
|
|
115
|
+
if (error instanceof Error && /\((401|403)\)/.test(error.message)) {
|
|
116
|
+
current.models = undefined;
|
|
117
|
+
persist(profile, () => clearCachedModels(scope));
|
|
118
|
+
}
|
|
119
|
+
throw error;
|
|
120
|
+
}).finally(() => {
|
|
121
|
+
if (current.pending === pending) {
|
|
122
|
+
current.pending = undefined;
|
|
123
|
+
current.controller = undefined;
|
|
124
|
+
current.background = false;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
current.pending = pending;
|
|
128
|
+
return pending;
|
|
129
|
+
};
|
|
130
|
+
if (current.models !== undefined) {
|
|
131
|
+
current.background = true;
|
|
132
|
+
void refresh().then(models => onVerified?.([...models])).catch(() => { });
|
|
133
|
+
return [...current.models];
|
|
134
|
+
}
|
|
135
|
+
const models = await joinRequest(current, refresh(), options.signal);
|
|
136
|
+
onVerified?.([...models]);
|
|
137
|
+
return models;
|
|
138
|
+
}
|
package/dist/app/restart.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export declare function reduceDesktopUpdateOwnerHandshake(phase: DesktopUpdateOw
|
|
|
69
69
|
*/
|
|
70
70
|
export declare function prepareDesktopUpdateAuthorization(persistSession: (force: true) => Promise<void>, send: (command: DesktopUpdateRestartCommand) => boolean): Promise<DesktopUpdatePreparation>;
|
|
71
71
|
export declare function buildHotRestartArgs(argv: readonly string[], sessionId: string): string[];
|
|
72
|
-
export declare function buildHotRestartLaunch(argv: readonly string[], sessionId: string, execPath: string): HotRestartLaunch;
|
|
72
|
+
export declare function buildHotRestartLaunch(argv: readonly string[], sessionId: string, execPath: string, cwd?: string): HotRestartLaunch;
|
|
73
73
|
export declare function buildDesktopUpdateRelaunchContext(argv: readonly string[], sessionId: string, execPath: string, cwd: string, ownerPid: number): string;
|
|
74
74
|
export declare function requestColonyCellRestart(env?: Record<string, string | undefined>, fetchImpl?: typeof fetch): Promise<void>;
|
|
75
75
|
export {};
|
package/dist/app/restart.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
1
3
|
const RESTART_SESSION_FLAGS = new Set(['--resume', '--continue', '-c', '--session-id']);
|
|
2
4
|
/**
|
|
3
5
|
* Reduce native updater protocol events and local persistence outcomes into
|
|
@@ -113,14 +115,23 @@ export function buildHotRestartArgs(argv, sessionId) {
|
|
|
113
115
|
}
|
|
114
116
|
return [...next, '--resume', sessionId];
|
|
115
117
|
}
|
|
116
|
-
export function buildHotRestartLaunch(argv, sessionId, execPath) {
|
|
118
|
+
export function buildHotRestartLaunch(argv, sessionId, execPath, cwd) {
|
|
117
119
|
const relaunchArgs = buildHotRestartArgs(argv, sessionId);
|
|
120
|
+
// The picker can switch workspaces. Override a launcher's original --cwd;
|
|
121
|
+
// cli_main deliberately uses the last occurrence of that flag.
|
|
122
|
+
if (cwd)
|
|
123
|
+
relaunchArgs.push('--cwd', cwd);
|
|
118
124
|
const entryScript = relaunchArgs[0] || '';
|
|
119
|
-
const isTsEntry = entryScript
|
|
125
|
+
const isTsEntry = /\.[cm]?tsx?$/.test(entryScript);
|
|
126
|
+
// Resolve from this installation, not the selected session's working dir.
|
|
127
|
+
// Direct argv also preserves spaces/metacharacters without a shell or npx.
|
|
128
|
+
const loaderArgs = isTsEntry
|
|
129
|
+
? ['--import', pathToFileURL(createRequire(import.meta.url).resolve('tsx')).href]
|
|
130
|
+
: [];
|
|
120
131
|
return {
|
|
121
|
-
command:
|
|
122
|
-
args:
|
|
123
|
-
shell:
|
|
132
|
+
command: execPath,
|
|
133
|
+
args: [...loaderArgs, ...relaunchArgs],
|
|
134
|
+
shell: false,
|
|
124
135
|
};
|
|
125
136
|
}
|
|
126
137
|
export function buildDesktopUpdateRelaunchContext(argv, sessionId, execPath, cwd, ownerPid) {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { type RouterCatalog } from '../utils/router_catalog.js';
|
|
2
|
+
import { type ReasoningContract } from '../utils/reasoning.js';
|
|
3
|
+
import type { CodexReasoningEffort } from '../config/types.js';
|
|
1
4
|
import { type SlashCommandUsage } from '../agent/commands/usage_spec.js';
|
|
2
5
|
export interface SelectionCandidate {
|
|
3
6
|
value: string;
|
|
@@ -8,7 +11,9 @@ export interface SelectionCandidate {
|
|
|
8
11
|
needsArgument?: boolean;
|
|
9
12
|
}
|
|
10
13
|
export interface SelectionCompletionView {
|
|
11
|
-
kind?: 'command' | 'provider' | 'model' | 'usage';
|
|
14
|
+
kind?: 'command' | 'provider' | 'model' | 'usage' | 'reasoning';
|
|
15
|
+
command?: 'effort' | 'thinking';
|
|
16
|
+
model?: string;
|
|
12
17
|
query?: string;
|
|
13
18
|
usage?: string;
|
|
14
19
|
provider?: string;
|
|
@@ -22,6 +27,13 @@ export interface SelectionCompletionView {
|
|
|
22
27
|
unavailable?: boolean;
|
|
23
28
|
visibleRows?: number;
|
|
24
29
|
}
|
|
30
|
+
export interface ReasoningCompletionContext {
|
|
31
|
+
catalog?: RouterCatalog;
|
|
32
|
+
current?: CodexReasoningEffort;
|
|
33
|
+
contract?: ReasoningContract;
|
|
34
|
+
/** Opaque account/profile identity; invalidate definitions when it changes. */
|
|
35
|
+
scope?: string;
|
|
36
|
+
}
|
|
25
37
|
export declare function fuzzySelections(candidates: SelectionCandidate[], query: string): SelectionCandidate[];
|
|
26
38
|
export declare function modelSelections(provider: string, discovered?: string[]): SelectionCandidate[];
|
|
27
39
|
/** Static preview for renderers without a completion controller. Explicit null hides it. */
|
|
@@ -49,8 +61,10 @@ export declare class SelectionCompletion {
|
|
|
49
61
|
private requestController;
|
|
50
62
|
private commands;
|
|
51
63
|
private currentModel?;
|
|
64
|
+
private routerCatalog?;
|
|
65
|
+
private reasoning;
|
|
52
66
|
constructor(changed?: () => void);
|
|
53
|
-
update(text: string, provider: string, loadModels?: (signal: AbortSignal, provider: string, authMode?: 'direct' | 'zrouter') => Promise<string[]>, commands?: SlashCommandUsage[], currentModel?: string): void;
|
|
67
|
+
update(text: string, provider: string, loadModels?: (signal: AbortSignal, provider: string, authMode?: 'direct' | 'zrouter') => Promise<string[]>, commands?: SlashCommandUsage[], currentModel?: string, loadCatalog?: (signal: AbortSignal) => Promise<RouterCatalog>, activeRouted?: boolean, reasoning?: ReasoningCompletionContext): void;
|
|
54
68
|
private filter;
|
|
55
69
|
private revealSelection;
|
|
56
70
|
setVisibleRows(rows: number): void;
|
|
@@ -73,6 +87,7 @@ export declare class SelectionCompletion {
|
|
|
73
87
|
}): {
|
|
74
88
|
handled: boolean;
|
|
75
89
|
text?: string;
|
|
90
|
+
submit?: boolean;
|
|
76
91
|
};
|
|
77
92
|
}
|
|
78
93
|
export declare const SELECTION_VISIBLE_ROWS = 4;
|