@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.
Files changed (92) hide show
  1. package/README.md +8 -1
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/dist/App.js +2 -2
  4. package/dist/agent/agent.js +41 -5
  5. package/dist/agent/backends/anthropic.js +5 -10
  6. package/dist/agent/backends/gemini.js +2 -0
  7. package/dist/agent/backends/index.d.ts +1 -0
  8. package/dist/agent/backends/index.js +10 -1
  9. package/dist/agent/backends/openai.d.ts +0 -6
  10. package/dist/agent/backends/openai.js +5 -2
  11. package/dist/agent/backends/openai_compatible.d.ts +2 -0
  12. package/dist/agent/backends/openai_compatible.js +18 -8
  13. package/dist/agent/backends/provider_ids.d.ts +1 -0
  14. package/dist/agent/backends/provider_ids.js +4 -0
  15. package/dist/agent/backends/types.d.ts +1 -0
  16. package/dist/agent/backends/zrouter_stream.d.ts +5 -1
  17. package/dist/agent/backends/zrouter_stream.js +141 -37
  18. package/dist/agent/commands/compact.js +6 -5
  19. package/dist/agent/commands/config.js +4 -1
  20. package/dist/agent/commands/context.js +6 -1
  21. package/dist/agent/commands/effort.js +14 -6
  22. package/dist/agent/commands/model.js +12 -2
  23. package/dist/agent/commands/profile.js +21 -2
  24. package/dist/agent/commands/provider_selection.js +14 -1
  25. package/dist/agent/commands/settings.js +45 -7
  26. package/dist/agent/commands/types.d.ts +19 -0
  27. package/dist/agent/factory.js +14 -68
  28. package/dist/agent/subagent.d.ts +0 -10
  29. package/dist/agent/subagent.js +22 -0
  30. package/dist/agent/tool_response_store.d.ts +1 -1
  31. package/dist/agent/tool_response_store.js +4 -2
  32. package/dist/agent/tools/pane.d.ts +0 -7
  33. package/dist/agent/tools/pane.js +5 -4
  34. package/dist/app/command_context.js +33 -10
  35. package/dist/app/model_catalog_cache.d.ts +5 -0
  36. package/dist/app/model_catalog_cache.js +138 -0
  37. package/dist/app/restart.d.ts +1 -1
  38. package/dist/app/restart.js +16 -5
  39. package/dist/app/selection_completion.d.ts +17 -2
  40. package/dist/app/selection_completion.js +74 -6
  41. package/dist/app/session_manager.js +13 -8
  42. package/dist/cli_args.js +2 -16
  43. package/dist/cli_main.js +25 -6
  44. package/dist/cli_ratatui.js +70 -33
  45. package/dist/config/defaults.d.ts +2 -1
  46. package/dist/config/defaults.js +9 -2
  47. package/dist/config/resolved.d.ts +2 -2
  48. package/dist/config.d.ts +35 -3
  49. package/dist/config.js +227 -29
  50. package/dist/headless.js +2 -0
  51. package/dist/onboarding/first_run.js +5 -1
  52. package/dist/panes/model_catalog.d.ts +3 -3
  53. package/dist/panes/model_catalog.js +28 -4
  54. package/dist/panes/pane_registry.js +6 -2
  55. package/dist/panes/types.d.ts +2 -0
  56. package/dist/panes/ztc_launch.js +12 -0
  57. package/dist/session.d.ts +6 -2
  58. package/dist/session.js +65 -10
  59. package/dist/ui/views/completion_menu.js +10 -5
  60. package/dist/ui/views/status_bar.js +9 -5
  61. package/dist/ui/widgets/resume_panel.d.ts +2 -0
  62. package/dist/ui/widgets/resume_panel.js +17 -15
  63. package/dist/utils/compaction.d.ts +1 -1
  64. package/dist/utils/compaction.js +2 -2
  65. package/dist/utils/context.d.ts +4 -4
  66. package/dist/utils/context.js +14 -12
  67. package/dist/utils/deepseek.d.ts +2 -2
  68. package/dist/utils/deepseek.js +13 -5
  69. package/dist/utils/model_cache.d.ts +15 -0
  70. package/dist/utils/model_cache.js +143 -0
  71. package/dist/utils/models.d.ts +4 -2
  72. package/dist/utils/models.js +12 -16
  73. package/dist/utils/reasoning.d.ts +8 -1
  74. package/dist/utils/reasoning.js +31 -11
  75. package/dist/utils/router_catalog.d.ts +47 -0
  76. package/dist/utils/router_catalog.js +222 -0
  77. package/dist/zlm-embed.mjs +1180 -514
  78. package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
  79. package/node_modules/@zerg/model-contracts/README.md +78 -0
  80. package/node_modules/@zerg/model-contracts/index.d.ts +18 -0
  81. package/node_modules/@zerg/model-contracts/index.js +220 -0
  82. package/node_modules/@zerg/model-contracts/package.json +8 -0
  83. package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
  84. package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
  85. package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
  86. package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
  87. package/package.json +9 -6
  88. package/packages/model-contracts/LICENSE.md +375 -0
  89. package/packages/model-contracts/README.md +78 -0
  90. package/packages/model-contracts/index.d.ts +18 -0
  91. package/packages/model-contracts/index.js +220 -0
  92. package/packages/model-contracts/package.json +8 -0
@@ -1,3 +1,5 @@
1
+ import { routerModelIds, routerModelsForProvider, resolveRouterModel } from '../utils/router_catalog.js';
2
+ import { reasoningCommandChoices, reasoningEnabled } from '../utils/reasoning.js';
1
3
  import { MODEL_CATALOG } from '../panes/model_catalog.js';
2
4
  import { canRouteThroughRouter, canonicalProvider, PROVIDER_CHOICES } from '../agent/commands/provider_choices.js';
3
5
  import { resolveSlashUsage } from '../agent/commands/usage_spec.js';
@@ -42,6 +44,11 @@ function commandUsage(text, commands) {
42
44
  function parseSelectionInput(text, commands) {
43
45
  if (commands.length && /^\/[\w-]*$/.test(text))
44
46
  return { kind: 'command', prefix: '/', query: text.slice(1) };
47
+ const reasoning = text.match(/^\/(effort|thinking)\s+([^\s]*)\s*$/i);
48
+ if (reasoning && commands.some(command => command.name === reasoning[1].toLowerCase())) {
49
+ const command = reasoning[1].toLowerCase();
50
+ return { kind: 'reasoning', command, prefix: `/${command} `, query: reasoning[2] };
51
+ }
45
52
  const model = text.match(/^\/(model|models)\s+([^\n]*)$/i);
46
53
  if (!model)
47
54
  return commandUsage(text, commands);
@@ -98,6 +105,9 @@ function commandSelections(commands) {
98
105
  /** Static preview for renderers without a completion controller. Explicit null hides it. */
99
106
  export function commandCompletionView(text, commands) {
100
107
  const parsed = parseSelectionInput(text, commands);
108
+ if (parsed?.kind === 'reasoning')
109
+ return { kind: 'usage', candidates: [], selected: 0, loading: false,
110
+ usage: `/${parsed.command} [${parsed.command === 'effort' ? 'level|default' : 'setting'}]` };
101
111
  if (parsed?.kind !== 'command' && parsed?.kind !== 'usage')
102
112
  return null;
103
113
  return { kind: parsed.kind, query: parsed.query, usage: parsed.usage,
@@ -126,23 +136,31 @@ export class SelectionCompletion {
126
136
  requestController = null;
127
137
  commands = [];
128
138
  currentModel;
139
+ routerCatalog;
140
+ reasoning = {};
129
141
  constructor(changed = () => { }) {
130
142
  this.changed = changed;
131
143
  }
132
- update(text, provider, loadModels, commands = [], currentModel) {
144
+ update(text, provider, loadModels, commands = [], currentModel, loadCatalog, activeRouted = false, reasoning = {}) {
133
145
  this.commands = commands;
134
- this.currentModel = currentModel;
135
146
  const parsed = parseSelectionInput(text, commands);
136
147
  const catalogProvider = parsed?.modelProvider ?? provider;
148
+ const routed = parsed?.routed ?? ((parsed?.includeModels || parsed?.kind === 'reasoning') && activeRouted ? true : undefined);
137
149
  if (provider !== this.provider || catalogProvider !== this.catalogProvider
138
- || parsed?.routed !== this.catalogRoute || parsed?.kind !== this.parsed?.kind) {
150
+ || routed !== this.catalogRoute || parsed?.kind !== this.parsed?.kind
151
+ || reasoning.scope !== this.reasoning.scope || (parsed?.kind === 'reasoning' && currentModel !== this.currentModel)) {
139
152
  this.requestController?.abort();
140
153
  this.generation++;
141
154
  this.requested = false;
142
155
  this.loading = false;
143
156
  this.unavailable = false;
144
157
  this.discovered = undefined;
158
+ this.routerCatalog = undefined;
145
159
  }
160
+ this.currentModel = currentModel;
161
+ this.reasoning = reasoning;
162
+ if (reasoning.catalog)
163
+ this.routerCatalog = reasoning.catalog;
146
164
  if (text !== this.text || provider !== this.provider) {
147
165
  this.selected = 0;
148
166
  this.firstVisible = 0;
@@ -153,9 +171,28 @@ export class SelectionCompletion {
153
171
  this.text = text;
154
172
  this.provider = provider;
155
173
  this.catalogProvider = catalogProvider;
156
- this.catalogRoute = parsed?.routed;
174
+ this.catalogRoute = routed;
157
175
  this.parsed = parsed;
158
176
  this.filter();
177
+ if ((parsed?.routerOnly || routed) && !this.requested && loadCatalog) {
178
+ this.requested = true;
179
+ this.loading = true;
180
+ this.requestController = new AbortController();
181
+ const generation = this.generation;
182
+ void loadCatalog(this.requestController.signal).then(catalog => {
183
+ if (generation !== this.generation)
184
+ return;
185
+ this.routerCatalog = catalog;
186
+ this.discovered = routerModelIds(catalog, catalogProvider);
187
+ }).catch(() => { if (generation === this.generation)
188
+ this.unavailable = true; }).finally(() => {
189
+ if (generation !== this.generation)
190
+ return;
191
+ this.loading = false;
192
+ this.filter();
193
+ this.changed();
194
+ });
195
+ }
159
196
  if ((parsed?.kind === 'model' || parsed?.includeModels) && !this.requested && loadModels) {
160
197
  this.requested = true;
161
198
  this.loading = true;
@@ -181,9 +218,35 @@ export class SelectionCompletion {
181
218
  }
182
219
  filter() {
183
220
  const selectedValue = this.candidates[this.selected]?.value;
184
- const models = modelSelections(this.catalogProvider, this.discovered)
221
+ if (this.parsed?.kind === 'reasoning') {
222
+ const catalog = this.routerCatalog;
223
+ const contract = this.catalogRoute
224
+ ? catalog?.version === 1 && !catalog.authRequired
225
+ ? resolveRouterModel(catalog, this.provider, this.currentModel ?? '')?.capabilities?.reasoning ?? []
226
+ : catalog?.version === 0 && !catalog.authRequired ? this.reasoning.contract : []
227
+ : this.reasoning.contract;
228
+ const command = this.parsed.command;
229
+ const current = command === 'effort' ? this.reasoning.current
230
+ : this.reasoning.current === undefined ? undefined : reasoningEnabled(this.reasoning.current) ? 'on' : 'off';
231
+ this.candidates = fuzzySelections(reasoningCommandChoices(command, this.provider, this.currentModel ?? '', contract)
232
+ .map(value => ({ value, label: value, current: value === current,
233
+ description: value === 'default' ? 'Inherit model/provider defaults' : undefined })), this.parsed.query);
234
+ const previousIndex = this.candidates.findIndex(candidate => candidate.value === selectedValue);
235
+ this.selected = previousIndex >= 0 ? previousIndex : 0;
236
+ this.revealSelection();
237
+ return;
238
+ }
239
+ const routed = this.catalogRoute && this.routerCatalog?.version === 1;
240
+ const models = (routed ? routerModelsForProvider(this.routerCatalog, this.catalogProvider).map(row => ({
241
+ value: row.id, label: row.display_name || row.id,
242
+ description: [row.capabilities?.tools === false ? 'Chat only' : '', row.available === false ? 'Access unavailable' : '', this.routerCatalog?.cached ? 'Cached catalog' : ''].filter(Boolean).join(' · '),
243
+ })) : modelSelections(this.catalogProvider, this.discovered))
185
244
  .map(model => ({ ...model, current: this.catalogProvider === this.provider && model.value === this.currentModel }));
186
- const providers = PROVIDER_CHOICES.filter(p => !this.parsed?.routerOnly || canRouteThroughRouter(p.id))
245
+ const routerProviders = this.parsed?.routerOnly && this.routerCatalog
246
+ ? [...new Map(this.routerCatalog.models.filter(row => this.routerCatalog?.version === 0 || row.capabilities?.chat).map(row => [row.owned_by, {
247
+ id: row.owned_by, label: row.provider_display_name || row.owned_by, description: this.routerCatalog?.cached ? 'Cached catalog' : 'Via Zerg Router', aliases: [],
248
+ }])).values()] : PROVIDER_CHOICES;
249
+ const providers = routerProviders.filter(p => !this.parsed?.routerOnly || canRouteThroughRouter(p.id))
187
250
  .map(p => ({ value: p.id, label: p.label, description: p.description, aliases: p.aliases,
188
251
  current: p.id === this.provider, needsArgument: true }));
189
252
  const source = this.parsed?.kind === 'command' ? commandSelections(this.commands)
@@ -218,6 +281,7 @@ export class SelectionCompletion {
218
281
  return null;
219
282
  return { candidates: this.candidates, selected: this.selected, firstVisible: this.firstVisible, loading: this.loading, unavailable: this.unavailable,
220
283
  kind: this.parsed.kind, query: this.parsed.query, usage: this.parsed.usage,
284
+ command: this.parsed.command, model: this.parsed.kind === 'reasoning' ? this.currentModel : undefined,
221
285
  provider: this.parsed.kind === 'model' ? this.catalogProvider : undefined,
222
286
  routed: this.parsed.kind === 'model' ? this.catalogRoute : undefined,
223
287
  mixed: this.parsed.includeModels };
@@ -255,6 +319,10 @@ export class SelectionCompletion {
255
319
  if (this.parsed.kind === 'provider' && candidate.needsArgument) {
256
320
  return { handled: true, text: `${completed} ` };
257
321
  }
322
+ if (key.return && (this.parsed.kind === 'model' || this.parsed.kind === 'reasoning' || (this.parsed.includeModels && !candidate.needsArgument))) {
323
+ this.dismissedInput = this.text;
324
+ return { handled: true, text: completed, submit: true };
325
+ }
258
326
  if (key.return && completed === this.text)
259
327
  return { handled: false };
260
328
  this.dismissedInput = completed;
@@ -7,7 +7,7 @@
7
7
  import { saveSession, saveSessionToFile, saveMuxStateOnly, cleanupOldSessions } from '../session.js';
8
8
  import { pruneZtcTempDirs } from '../utils/ztc_cleanup.js';
9
9
  import { configStore } from '../config.js';
10
- import { estimateConversationTokens, formatTokenCount, getContextStatus, contextStatusFromUsed, resolveAutoCompact, } from '../utils/context.js';
10
+ import { estimateConversationTokens, formatTokenCount, getContextStatus, getContextWindowSize, contextStatusFromUsed, resolveAutoCompact, } from '../utils/context.js';
11
11
  import { compactConversation, describeCompaction, compactMessages, smartSummarizer, localSummarizer } from '../utils/compaction.js';
12
12
  import { stripEphemeralMessages } from '../utils/ephemeral.js';
13
13
  /**
@@ -47,7 +47,8 @@ export class SessionManager {
47
47
  async writeCurrentSession(options) {
48
48
  const messages = this.deps.getMessages();
49
49
  const muxState = this.deps.getMuxState ? await this.deps.getMuxState() : undefined;
50
- const runtimeState = this.deps.getRuntimeState ? this.deps.getRuntimeState() : undefined;
50
+ const runtimeState = { ...this.deps.getRuntimeState?.(), authMode: configStore.getResolvedAuthMode(),
51
+ routerOrigin: configStore.getResolvedAuthMode() === 'direct' ? undefined : configStore.get().zergProxyUrl };
51
52
  const hostedSettings = this.deps.getHostedSettings ? this.deps.getHostedSettings() : undefined;
52
53
  const hasRuntimeState = runtimeState !== undefined && (runtimeState.mode !== undefined
53
54
  || runtimeState.planFile !== undefined
@@ -153,8 +154,8 @@ export class SessionManager {
153
154
  // /context); fall back to the char/4 estimate only before the first API call.
154
155
  const realUsed = this.deps.getContextTokens?.() ?? 0;
155
156
  const status = realUsed > 0
156
- ? contextStatusFromUsed(realUsed, model, this.deps.getContextWindow?.())
157
- : getContextStatus(messages, model);
157
+ ? contextStatusFromUsed(realUsed, model, (configStore.getModelContextWindow(model) !== undefined ? configStore.getModelContextWindow(model) : this.deps.getContextWindow?.()))
158
+ : getContextStatus(messages, model, configStore.getModelContextWindow(model));
158
159
  const threshold = opts?.threshold ?? configStore.getAutoCompactThreshold();
159
160
  const targetPercent = configStore.getCompactionTargetPercent();
160
161
  const triggerMode = configStore.getCompactionTriggerMode();
@@ -164,7 +165,7 @@ export class SessionManager {
164
165
  const decision = resolveAutoCompact({
165
166
  used: realUsed > 0 ? realUsed : estimateConversationTokens(messages),
166
167
  model,
167
- windowOverride: this.deps.getContextWindow?.(),
168
+ windowOverride: (configStore.getModelContextWindow(model) !== undefined ? configStore.getModelContextWindow(model) : this.deps.getContextWindow?.()),
168
169
  thresholdPercent: threshold,
169
170
  triggerMode,
170
171
  tokenBudget: configStore.getContextTokenBudget(),
@@ -199,7 +200,7 @@ export class SessionManager {
199
200
  return false;
200
201
  }
201
202
  this.deps.setMessages(result.messages);
202
- const newStatus = getContextStatus(result.messages, model);
203
+ const newStatus = getContextStatus(result.messages, model, configStore.getModelContextWindow(model));
203
204
  const verb = describeCompaction(result);
204
205
  this.deps.addMessage({
205
206
  role: 'system',
@@ -228,13 +229,17 @@ export class SessionManager {
228
229
  const summarizer = this.buildSummarizer();
229
230
  this.deps.debugLog('[SessionManager.forceCompact] Forcing aggressive compaction');
230
231
  try {
231
- const result = await compactMessages(messages, model, summarizer, 30, 5);
232
+ // A provider can reject request structure before its token window fills.
233
+ // Forced recovery must shrink even when the history already fits 30% of
234
+ // that window; otherwise the next request repeats the same rejection.
235
+ const targetTokens = Math.max(1, Math.min(Math.floor((getContextWindowSize(model, configStore.getModelContextWindow(model)) || estimateConversationTokens(messages)) * 0.3), Math.floor(estimateConversationTokens(messages) / 2)));
236
+ const result = await compactMessages(messages, model, summarizer, 30, 5, targetTokens);
232
237
  if (result.removed === 0) {
233
238
  this.deps.debugLog('[SessionManager.forceCompact] Nothing to compact — all messages are recent or system');
234
239
  return false;
235
240
  }
236
241
  this.deps.setMessages(result.messages);
237
- const newStatus = getContextStatus(result.messages, model);
242
+ const newStatus = getContextStatus(result.messages, model, configStore.getModelContextWindow(model));
238
243
  this.deps.debugLog(`[SessionManager.forceCompact] Compacted ${result.removed} msgs: ${formatTokenCount(result.tokensBefore)} → ${formatTokenCount(result.tokensAfter)} (${Math.round(newStatus.percentage)}%)`);
239
244
  return true;
240
245
  }
package/dist/cli_args.js CHANGED
@@ -1,11 +1,9 @@
1
- import { CODEX_REASONING_EFFORTS } from './config/types.js';
2
- import { getSupportedCodexReasoningEfforts, unsupportedEffortMessage } from './utils/models.js';
3
1
  import { validateProfileName } from './config/profiles.js';
4
2
  const BOOLEAN_FLAGS = new Set([
5
3
  '--no-pin-task-header', '--help', '-h', '--version', '-v', '--list-sessions', '--plan', '--desktop', '--mux', '--no-mux', '--new-window', '--fullscreen', '--pane', '--human-sim-pane', '--zlicky', '--computer-use', '--ml', '--sfx-list', '--voyager-loop', '--mirror', '--open-mirror', '--mirror-open', '--no-browser-open', '--navigator-hidden', '--headless-mirror', '--headless-mirror-open', '--headless-interactive', '--headless', '--json', '--json-stream', '--quiet', '-q', '--timestamps', '--verbose', '--colony',
6
4
  ]);
7
5
  const REQUIRED_VALUE_FLAGS = new Set([
8
- '--cwd', '--name', '--session-id', '--session-file', '--mode', '--agent-domain', '--domain-paths', '--embedded', '--embedded-rom', '--embedded-workflow', '--embedded-runtime-tick-ms', '--embedded-runtime-frame-budget', '--bench', '--bench-workflow', '--num-zergs', '--provider', '--model', '--profile', '--effort', '--thinking', '--emulation', '--prompt', '--max-iterations', '--timeout', '--output', '--voyager-task', '--voyager-max-rounds', '--mirror-port', '--navigator-group', '--connect-zerg',
6
+ '--cwd', '--name', '--session-id', '--session-file', '--mode', '--agent-domain', '--domain-paths', '--embedded', '--embedded-rom', '--embedded-workflow', '--embedded-runtime-tick-ms', '--embedded-runtime-frame-budget', '--bench', '--bench-workflow', '--num-zergs', '--provider', '--model', '--auth-mode', '--router-url', '--profile', '--effort', '--thinking', '--emulation', '--prompt', '--max-iterations', '--timeout', '--output', '--voyager-task', '--voyager-max-rounds', '--mirror-port', '--navigator-group', '--connect-zerg',
9
7
  ]);
10
8
  const OPTIONAL_VALUE_FLAGS = new Set(['--resume', '--continue', '-c', '--sfx']);
11
9
  /** Separate prompt text from options before the legacy flag consumers see it. */
@@ -96,19 +94,7 @@ export function validateCliArgs(args) {
96
94
  return { ok: false, message: error.message, error: 'invalid_profile', flag: token, value: next };
97
95
  }
98
96
  }
99
- if (token === '--effort') {
100
- if (!CODEX_REASONING_EFFORTS.includes(next)) {
101
- return { ok: false, message: `--effort must be one of: ${CODEX_REASONING_EFFORTS.join(', ')}.`, error: 'invalid_effort', flag: token, value: next };
102
- }
103
- const modelIndex = args.indexOf('--model');
104
- const providerIndex = args.indexOf('--provider');
105
- const model = modelIndex >= 0 ? args[modelIndex + 1] : undefined;
106
- const provider = providerIndex >= 0 ? args[providerIndex + 1] : undefined;
107
- const supported = getSupportedCodexReasoningEfforts(model, provider);
108
- if (supported && !supported.includes(next)) {
109
- return { ok: false, message: unsupportedEffortMessage(model, next, supported), error: 'unsupported_effort', flag: token, value: next };
110
- }
111
- }
97
+ // Model-specific effort validation runs after configuration and Router definitions load.
112
98
  i += 1;
113
99
  continue;
114
100
  }
package/dist/cli_main.js CHANGED
@@ -298,6 +298,8 @@ Options:
298
298
  --bench-workflow <id>
299
299
  Run a bench workflow after opening the workspace
300
300
  --provider <name> LLM provider override (anthropic, claude_code, openai, codex_app_server, codex_legacy, gemini, inception, kimi, kimi_code, etc.; codex is a legacy alias)
301
+ --auth-mode <mode> Override routing for this process (auto, direct, zrouter)
302
+ --router-url <url> Router origin for this process; matching login is required
301
303
  --profile <name> Use named settings in ~/.ztc/profiles/<name>/config.json
302
304
  (default: ~/.ztc/config.json). Sessions stay shared.
303
305
  --model <name> LLM model override (e.g., claude-sonnet-4-20250514, gpt-4o)
@@ -1043,9 +1045,20 @@ async function main() {
1043
1045
  const { initMcpServers } = await import('./mcp/client.js');
1044
1046
  await initMcpServers(mcpServers);
1045
1047
  }
1048
+ const authModeIndex = args.indexOf('--auth-mode');
1049
+ const routerUrlIndex = args.indexOf('--router-url');
1050
+ if (authModeIndex >= 0 || routerUrlIndex >= 0) {
1051
+ const authMode = authModeIndex >= 0 ? args[authModeIndex + 1] : configStore.getAuthMode();
1052
+ if (!['auto', 'direct', 'zrouter'].includes(authMode))
1053
+ throw new Error('Invalid --auth-mode');
1054
+ configStore.setCliOverride('authMode', authMode);
1055
+ if (routerUrlIndex >= 0)
1056
+ configStore.setCliOverride('zergProxyUrl', args[routerUrlIndex + 1]);
1057
+ }
1046
1058
  // Apply --provider / --model CLI overrides (never persisted to config.json)
1059
+ const cliRoute = configStore.getResolvedAuthMode();
1047
1060
  const resolvedCliOverride = cliModel
1048
- ? resolveModelAndProvider(cliModel, cliProvider || undefined)
1061
+ ? resolveModelAndProvider(cliModel, cliProvider || undefined, cliRoute)
1049
1062
  : undefined;
1050
1063
  const effectiveCliProvider = (resolvedCliOverride?.provider || cliProvider);
1051
1064
  const effectiveCliModel = resolvedCliOverride?.model || cliModel;
@@ -1069,11 +1082,17 @@ async function main() {
1069
1082
  const thinkingIndex = args.indexOf('--thinking');
1070
1083
  if (effortIndex >= 0 || thinkingIndex >= 0) {
1071
1084
  const { parseReasoningSelection } = await import('./utils/reasoning.js');
1072
- const level = parseReasoningSelection(configStore.getProvider(), configStore.getModel(), configStore.getCodexEffort(), {
1073
- effort: effortIndex >= 0 ? args[effortIndex + 1] : undefined,
1074
- thinking: thinkingIndex >= 0 ? args[thinkingIndex + 1] : undefined,
1075
- });
1076
- configStore.setCliOverride('codexEffort', level ?? undefined);
1085
+ try {
1086
+ const level = parseReasoningSelection(configStore.getProvider(), configStore.getModel(), configStore.getCodexEffort(), {
1087
+ effort: effortIndex >= 0 ? args[effortIndex + 1] : undefined,
1088
+ thinking: thinkingIndex >= 0 ? args[thinkingIndex + 1] : undefined,
1089
+ }, configStore.getReasoningContract());
1090
+ configStore.setCliOverride('codexEffort', level ?? undefined);
1091
+ }
1092
+ catch (error) {
1093
+ console.error(`[ztc] ${error instanceof Error ? error.message : String(error)}`);
1094
+ process.exit(2);
1095
+ }
1077
1096
  }
1078
1097
  // Background-refresh superuser status so pre-existing logins get the field backfilled.
1079
1098
  // Skip for colony children (they inherit the parent status) and explicitly offline
@@ -13,7 +13,7 @@ import { randomUUID } from 'crypto';
13
13
  import { appendFileSync, existsSync, writeFileSync } from 'fs';
14
14
  import { tmpdir } from 'os';
15
15
  import { join } from 'path';
16
- import { execFile, spawn as spawnChild } from 'child_process';
16
+ import { execFile, spawnSync } from 'child_process';
17
17
  import { promisify } from 'util';
18
18
  import { configStore } from './config.js';
19
19
  import { withActiveProfileArg } from './config/profiles.js';
@@ -79,6 +79,7 @@ import { createAgentWithConfig, AgentRunLifecycle } from './app/agent_runner.js'
79
79
  import { clearWorkerExecutor } from './worker/index.js';
80
80
  import { isPhantomCompletion } from './app/phantom_completion.js';
81
81
  import { SelectionCompletion } from './app/selection_completion.js';
82
+ import { routerCatalogScope } from './utils/router_catalog.js';
82
83
  import { shouldAttachParentMirrorClient, shouldMarkMirrorServerAsColonyChild } from './app/mirror_startup.js';
83
84
  import { buildDesktopUpdateRelaunchContext, buildHotRestartLaunch, prepareDesktopUpdateAuthorization, reduceDesktopUpdateOwnerHandshake, requestColonyCellRestart, } from './app/restart.js';
84
85
  export { buildDesktopUpdateRelaunchContext, buildHotRestartArgs, requestColonyCellRestart, } from './app/restart.js';
@@ -1031,6 +1032,8 @@ export class RatatuiApp {
1031
1032
  if (options?.resumeSession) {
1032
1033
  this.sessionId = options.resumeSession.metadata.id;
1033
1034
  this.resumedSession = options.resumeSession;
1035
+ if (!options.resumeSession.hosted_settings)
1036
+ configStore.restoreSessionSelection(options.resumeSession.metadata);
1034
1037
  this.resumedPaneSpecs = mergeSessionPaneSpecs(options.resumeSession.mux_state, options.resumeSession.pane_specs);
1035
1038
  this.sessionName = options.resumeSession.metadata.name;
1036
1039
  this.hostedSessionSettings = options.resumeSession.hosted_settings
@@ -1084,7 +1087,7 @@ export class RatatuiApp {
1084
1087
  });
1085
1088
  // Initialize context status to show meter from the start
1086
1089
  const model = configStore.get().model;
1087
- const total = getContextWindowSize(model);
1090
+ const total = getContextWindowSize(model, configStore.getModelContextWindow(model));
1088
1091
  this.controller.dispatch({
1089
1092
  type: 'SET_CONTEXT',
1090
1093
  length: 0,
@@ -1580,7 +1583,7 @@ export class RatatuiApp {
1580
1583
  this.mirrorServer?.setSuperuser(configStore.isSuperuser());
1581
1584
  }
1582
1585
  /** Change the active config while retaining this renderer's session and transcript. */
1583
- async switchConfigProfileInPlace(name) {
1586
+ async switchConfigProfileInPlace(name, duplicateSource) {
1584
1587
  if (this.isHostedRuntime()) {
1585
1588
  throw new Error('Hosted thread settings cannot be replaced by a local config profile.');
1586
1589
  }
@@ -1620,7 +1623,7 @@ export class RatatuiApp {
1620
1623
  await preloadSkillsManifest();
1621
1624
  };
1622
1625
  try {
1623
- await configStore.switchProfile(name, async () => {
1626
+ const activate = async () => {
1624
1627
  touchedRuntime = true;
1625
1628
  applyColonyOptIn();
1626
1629
  mcpChanged = previousMcp !== JSON.stringify(configStore.get().mcpServers ?? {})
@@ -1636,7 +1639,11 @@ export class RatatuiApp {
1636
1639
  this.hookLifecycle.ensureWatcher();
1637
1640
  if (this.shouldPersistSession())
1638
1641
  await this.saveCurrentSession(true);
1639
- });
1642
+ };
1643
+ if (duplicateSource === undefined)
1644
+ await configStore.switchProfile(name, activate);
1645
+ else
1646
+ await configStore.duplicateProfile(name, duplicateSource, activate);
1640
1647
  }
1641
1648
  catch (error) {
1642
1649
  this.overseerModelOverride = previousOverseerOverride;
@@ -1722,6 +1729,10 @@ export class RatatuiApp {
1722
1729
  setAgentState: (state) => this.controller.setAgentState(state),
1723
1730
  reloadAgent: (options) => this.reloadAgentInPlace(options?.defer),
1724
1731
  switchConfigProfile: (name) => this.switchConfigProfileInPlace(name),
1732
+ duplicateProfile: async (name, source) => {
1733
+ await this.switchConfigProfileInPlace(name, source);
1734
+ return name;
1735
+ },
1725
1736
  setRuntimeModelOverride: (provider, model) => {
1726
1737
  configStore.setCliOverride('provider', provider);
1727
1738
  configStore.setCliOverride('model', model);
@@ -3370,7 +3381,7 @@ export class RatatuiApp {
3370
3381
  sessionName: this.resumedSession.metadata.name,
3371
3382
  messageCount: this.resumedSession.messages.length,
3372
3383
  updatedAt: this.resumedSession.metadata.updatedAt,
3373
- contextPercentage: getContextStatus(this.resumedSession.messages, model).percentage,
3384
+ contextPercentage: getContextStatus(this.resumedSession.messages, model, configStore.getModelContextWindow(model)).percentage,
3374
3385
  mode: launchMode,
3375
3386
  credentialsReady,
3376
3387
  });
@@ -3862,14 +3873,14 @@ export class RatatuiApp {
3862
3873
  }
3863
3874
  createZtcPaneSpec(options) {
3864
3875
  const resolved = options?.model
3865
- ? resolveModelAndProvider(options.model, options.provider)
3876
+ ? resolveModelAndProvider(options.model, options.provider, configStore.getResolvedAuthMode())
3866
3877
  : { model: undefined, provider: options?.provider };
3867
3878
  return createZtcLaunchSpec({
3868
3879
  sessionId: options?.sessionId,
3869
3880
  sessionMode: options?.sessionMode,
3870
3881
  cwd: options?.cwd ?? (this.shellCwd || process.cwd()),
3871
3882
  title: options?.title || 'ZTC',
3872
- providerOverride: isPaneProvider(resolved.provider) ? resolved.provider : undefined,
3883
+ providerOverride: isPaneProvider(resolved.provider, configStore.getResolvedAuthMode()) ? resolved.provider : undefined,
3873
3884
  modelOverride: resolved.model,
3874
3885
  extraArgs: [],
3875
3886
  });
@@ -3882,7 +3893,7 @@ export class RatatuiApp {
3882
3893
  const configProvider = configStore.getProvider();
3883
3894
  const providerOverride = launchSpec?.kind === 'ztc' ? launchSpec.providerOverride : undefined;
3884
3895
  const modelOverride = launchSpec?.kind === 'ztc' ? launchSpec.modelOverride : undefined;
3885
- const configuredPaneProvider = isPaneProvider(configProvider) ? configProvider : undefined;
3896
+ const configuredPaneProvider = isPaneProvider(configProvider, configStore.getResolvedAuthMode()) ? configProvider : undefined;
3886
3897
  const effectiveProvider = isZtc
3887
3898
  ? providerOverride
3888
3899
  || (!modelOverride ? configuredPaneProvider : undefined)
@@ -4284,13 +4295,18 @@ export class RatatuiApp {
4284
4295
  // --- Shared validation ---
4285
4296
  let nextOverride = null;
4286
4297
  if (command.override) {
4287
- const resolved = resolveModelAndProvider(command.override.model, command.override.provider);
4288
- if (!isPaneProvider(resolved.provider) || !resolved.model || !isCatalogModelForProvider(resolved.provider, resolved.model)) {
4298
+ const route = configStore.getResolvedAuthMode();
4299
+ const resolved = resolveModelAndProvider(command.override.model, command.override.provider, route);
4300
+ const catalog = configStore.getRoutedModelCatalog();
4301
+ const knownModel = catalog?.version === 1
4302
+ ? catalog.models.some(row => row.owned_by === resolved.provider && row.id === resolved.model && row.capabilities?.chat)
4303
+ : isCatalogModelForProvider(resolved.provider || '', resolved.model);
4304
+ if (!isPaneProvider(resolved.provider, route) || !resolved.model || !knownModel) {
4289
4305
  const error = 'Unsupported pane model selection.';
4290
4306
  this.publishPaneSystemMessage(command.pane_id, error);
4291
4307
  return { ok: false, error };
4292
4308
  }
4293
- if (!configStore.getApiKey(resolved.provider) && !configStore.isAuthenticated()) {
4309
+ if (route !== 'zrouter' && !configStore.getApiKey(resolved.provider) && !configStore.isAuthenticated()) {
4294
4310
  const error = `no API key configured for provider ${resolved.provider}`;
4295
4311
  this.publishPaneSystemMessage(command.pane_id, error);
4296
4312
  return { ok: false, error };
@@ -4352,7 +4368,7 @@ export class RatatuiApp {
4352
4368
  const effectiveProvider = nextOverride?.provider || configStore.getProvider();
4353
4369
  this.childPaneModels.set(command.pane_id, effectiveModel);
4354
4370
  pane.effectiveModel = effectiveModel;
4355
- pane.effectiveProvider = isPaneProvider(effectiveProvider) ? effectiveProvider : undefined;
4371
+ pane.effectiveProvider = isPaneProvider(effectiveProvider, configStore.getResolvedAuthMode()) ? effectiveProvider : undefined;
4356
4372
  this.mirrorServer?.setPaneModel(command.pane_id, effectiveModel, effectiveProvider);
4357
4373
  if (pane.launchSpec?.kind === 'ztc') {
4358
4374
  pane.launchSpec.providerOverride = nextOverride?.provider;
@@ -4377,8 +4393,8 @@ export class RatatuiApp {
4377
4393
  return { ok: false, error };
4378
4394
  }
4379
4395
  const level = (effort ?? null);
4380
- if (level !== null && !isCodexReasoningEffortSupported(command.provider, command.model, level)) {
4381
- const supported = getSupportedCodexReasoningEfforts(command.model, command.provider) ?? [];
4396
+ if (level !== null && !isCodexReasoningEffortSupported(command.provider, command.model, level, configStore.getReasoningWireSurface(command.provider, command.model))) {
4397
+ const supported = getSupportedCodexReasoningEfforts(command.model, command.provider, configStore.getReasoningWireSurface(command.provider, command.model)) ?? [];
4382
4398
  const error = unsupportedEffortMessage(command.model, level, supported);
4383
4399
  this.publishPaneSystemMessage(command.pane_id, error);
4384
4400
  return { ok: false, error };
@@ -4897,7 +4913,8 @@ export class RatatuiApp {
4897
4913
  // the static table alone would revert the denominator (e.g. 400K -> 200K for
4898
4914
  // would revert the denominator to a stale static fallback until the next
4899
4915
  // token_usage event corrects it.
4900
- const windowOverride = this.controller.getState().contextStatus?.total;
4916
+ const advertisedWindow = configStore.getModelContextWindow(model);
4917
+ const windowOverride = advertisedWindow !== undefined ? advertisedWindow : this.controller.getState().contextStatus?.total;
4901
4918
  const status = contextStatusFromUsed(used, model, windowOverride);
4902
4919
  this.controller.dispatch({ type: 'SET_CONTEXT', length: used, estimated, status });
4903
4920
  const agentState = this.controller.getState().agentState;
@@ -5616,7 +5633,16 @@ export class RatatuiApp {
5616
5633
  ? this.inputState.segments[0].text : '';
5617
5634
  if (textCursorIndex(this.inputState.segments, this.inputState.cursor) !== text.length)
5618
5635
  text = '';
5619
- this.selectionCompletion.update(text, configStore.getProvider(), (signal, provider, authMode) => buildModelsController(configStore).list(provider, { signal, authMode }), commands, configStore.getModel());
5636
+ let routed = configStore.getAuthMode() === 'zrouter';
5637
+ try {
5638
+ routed = configStore.resolveLLMBackendConfig().useProxy;
5639
+ }
5640
+ catch { /* Discovery can still show its isolated cache. */ }
5641
+ const reasoningCommand = /^\/(?:effort|thinking)\s/i.test(text);
5642
+ const connection = reasoningCommand && routed ? configStore.getRouterCatalogConnection() : undefined;
5643
+ this.selectionCompletion.update(text, configStore.getProvider(), (signal, provider, authMode) => buildModelsController(configStore).list(provider, { signal, authMode }), commands, configStore.getModel(), buildModelsController(configStore).catalog, routed, reasoningCommand ? { catalog: routed ? configStore.getRoutedModelCatalog() : undefined,
5644
+ current: configStore.getCodexEffort(), contract: configStore.getReasoningContract(),
5645
+ scope: connection ? routerCatalogScope(connection) : undefined } : {});
5620
5646
  return this.selectionCompletion.view();
5621
5647
  }
5622
5648
  handleInput(input, key) {
@@ -5708,8 +5734,15 @@ export class RatatuiApp {
5708
5734
  this.updateSelectionCompletion();
5709
5735
  const selection = this.selectionCompletion.handleKey({ ...key, return: key.return || input === '\r' || input === '\n' });
5710
5736
  if (selection.handled) {
5711
- if (selection.text !== undefined)
5712
- this.setInputToText(selection.text, selection.text.length);
5737
+ if (selection.text !== undefined) {
5738
+ if (selection.submit && !this.queueEdit) {
5739
+ this.clearComposerAfterSubmit(selection.text);
5740
+ void this.handleSubmit(selection.text);
5741
+ }
5742
+ else {
5743
+ this.setInputToText(selection.text, selection.text.length);
5744
+ }
5745
+ }
5713
5746
  else
5714
5747
  this.render();
5715
5748
  return;
@@ -7420,7 +7453,7 @@ export class RatatuiApp {
7420
7453
  // exact same math /context and auto-compact use, so all surfaces agree.
7421
7454
  // event.contextWindow (when the provider reports one, e.g. Codex's
7422
7455
  // modelContextWindow) overrides the static window guess.
7423
- const status = contextStatusFromUsed(used, model, event.contextWindow);
7456
+ const status = contextStatusFromUsed(used, model, event.contextWindow ?? configStore.getModelContextWindow(model));
7424
7457
  const total = status.total;
7425
7458
  const percentage = status.percentage;
7426
7459
  const previousContextTokens = this.controller.getState().agentState.contextTokens;
@@ -10389,33 +10422,37 @@ export class RatatuiApp {
10389
10422
  try {
10390
10423
  debugLog(`[${options.label}] Saving session before relaunch...`);
10391
10424
  await this.saveCurrentSession();
10392
- // Clean up
10393
- this.sessionManager?.stopAutoSave();
10394
- if (this.dictationCleanup) {
10395
- this.dictationCleanup();
10396
- this.dictationCleanup = null;
10397
- }
10398
- void shutdownLSPManager();
10399
10425
  if (this.mirrorServer) {
10400
10426
  this.mirrorServer.publishSessionEnd({ reloadOnReconnect: true });
10401
10427
  await stopMirrorStreamServer();
10402
10428
  this.mirrorServer = null;
10403
10429
  }
10404
- // Tear down TUI (exits raw mode, leaves alternate screen)
10405
- await this.tui.stop();
10430
+ // execve skips exit handlers. Release the old session's registry entry,
10431
+ // timers, backends and renderer before replacing the process. Persistence
10432
+ // already happened above, while the renderer could still snapshot panes.
10433
+ await this.exit(false, { announce: false, persistSession: false, terminateProcess: false });
10406
10434
  // Build relaunch args: preserve runtime flags but force the target session
10407
10435
  // as the only resume target.
10408
- const { command, args, shell } = buildHotRestartLaunch(withActiveProfileArg(process.argv.slice(1)), options.sessionId, process.execPath);
10409
10436
  const spawnCwd = existsSync(options.cwd) ? options.cwd : process.cwd();
10437
+ const { command, args, shell } = buildHotRestartLaunch(withActiveProfileArg(process.argv.slice(1)), options.sessionId, process.execPath, spawnCwd);
10410
10438
  debugLog(`[${options.label}] Spawning new process: ${command} ${args.join(' ')} (cwd=${spawnCwd})`);
10411
- const child = spawnChild(command, args, {
10439
+ // Keep the foreground job alive. Spawning and exiting lets the shell
10440
+ // reclaim the terminal, so the orphaned renderer fails with raw-mode EIO.
10441
+ // Node 20 lacks execve; its fallback must wait until the replacement ends.
10442
+ const runtime = process;
10443
+ if (runtime.execve) {
10444
+ process.chdir(spawnCwd);
10445
+ runtime.execve(command, [command, ...args], process.env);
10446
+ }
10447
+ const result = spawnSync(command, args, {
10412
10448
  stdio: 'inherit',
10413
10449
  cwd: spawnCwd,
10414
10450
  env: process.env,
10415
10451
  shell,
10416
10452
  });
10417
- child.unref();
10418
- process.exit(0);
10453
+ if (result.error)
10454
+ throw result.error;
10455
+ process.exit(result.status ?? 1);
10419
10456
  }
10420
10457
  catch (err) {
10421
10458
  debugLog(`[${options.label}] Failed: ${err.message}`);
@@ -1,3 +1,4 @@
1
+ import type { ReasoningWireSurface } from '@zerg/model-contracts';
1
2
  import type { CodexReasoningEffort, ZTCConfig } from './types.js';
2
3
  export declare const DEFAULT_OPENAI_MODEL = "gpt-5.4-nano";
3
4
  export declare const DEFAULT_CODEX_MODEL = "gpt-5.6-sol";
@@ -14,7 +15,7 @@ export declare const DEFAULT_CLARIFAI_MODEL = "https://clarifai.com/openai/chat-
14
15
  * undefined (use the provider default). Used both when building an agent
15
16
  * (factory) and to display the active effort in the model picker.
16
17
  */
17
- export declare function resolveCodexEffort(config: ZTCConfig, provider?: string, model?: string): CodexReasoningEffort | undefined;
18
+ export declare function resolveCodexEffort(config: ZTCConfig, provider?: string, model?: string, contract?: ReasoningWireSurface | import('../utils/router_catalog.js').RouterModelCapabilities): CodexReasoningEffort | undefined;
18
19
  /**
19
20
  * The highest-priority saved effort when the active model rejects it, so the
20
21
  * UI can say the choice is not applied instead of silently falling back.
@@ -20,14 +20,19 @@ export const DEFAULT_CLARIFAI_MODEL = 'https://clarifai.com/openai/chat-completi
20
20
  * undefined (use the provider default). Used both when building an agent
21
21
  * (factory) and to display the active effort in the model picker.
22
22
  */
23
- export function resolveCodexEffort(config, provider, model) {
23
+ export function resolveCodexEffort(config, provider, model, contract = config.authMode === 'zrouter' || provider === 'openai_compatible' ? 'chat_tools' : 'native_api') {
24
24
  const candidates = [
25
25
  model ? config.codexEffortByModel?.[model] : undefined,
26
26
  provider ? config.codexEffortByProvider?.[provider] : undefined,
27
27
  config.codexEffort,
28
28
  ];
29
29
  for (const candidate of candidates) {
30
- if (candidate && isCodexReasoningEffortSupported(provider, model, candidate))
30
+ if (!candidate)
31
+ continue;
32
+ const supported = typeof contract === 'string'
33
+ ? isCodexReasoningEffortSupported(provider, model, candidate, contract)
34
+ : contract.reasoning?.choices.includes(candidate);
35
+ if (candidate && supported)
31
36
  return candidate;
32
37
  }
33
38
  return undefined;
@@ -143,6 +148,8 @@ export function getDefaultApiKeyForProvider(provider) {
143
148
  return process.env.DEEPSEEK_API_KEY;
144
149
  case 'modal':
145
150
  return undefined; // Modal credentials belong to the Router platform.
151
+ case 'fireworks':
152
+ return process.env.FIREWORKS_API_KEY;
146
153
  default:
147
154
  return (process.env.ANTHROPIC_API_KEY
148
155
  || process.env.OPENAI_API_KEY
@@ -35,8 +35,8 @@ export interface AuthContext {
35
35
  * `apiKey` is the DIRECT provider key (what a direct backend would use).
36
36
  * `authToken` is the platform/proxy bearer. When `useProxy` is true the active
37
37
  * backend streams through `proxyUrl` with `authToken`; the direct `apiKey` is
38
- * still surfaced so callers that special-case a provider (e.g. a direct Gemini
39
- * key) can opt out of the proxy.
38
+ * still surfaced for direct-provider construction. Auto-mode preferences are
39
+ * resolved before `useProxy` is returned.
40
40
  */
41
41
  export interface ResolvedLLMConfig {
42
42
  provider: string;