@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
package/dist/config.d.ts CHANGED
@@ -1,3 +1,6 @@
1
+ import { type ReasoningWireSurface } from '@zerg/model-contracts';
2
+ import { type RouterCatalog, type RouterCatalogConnection } from './utils/router_catalog.js';
3
+ import type { ReasoningContract } from './utils/reasoning.js';
1
4
  import { ZTCConfig, ZTCAuthCredentials, ApiEnvironment, ZTCAuthMode, ZTCConfigScope, CompactionTriggerMode, OpenAICompatibleSettings, ZTCPermissionProfile, IntentLayerSettings, ContextProgressBarSettings, CodexReasoningEffort, WorkflowSettings, Account, AccountRotation } from './config/types.js';
2
5
  import type { AuthContext, ResolvedLLMConfig } from './config/resolved.js';
3
6
  /** Validate and normalize an OpenAI-compatible inference destination. */
@@ -14,6 +17,12 @@ export declare class ConfigStore {
14
17
  private config;
15
18
  private profileEmulation;
16
19
  private legacyApiKeyFromEnvironment;
20
+ /**
21
+ * Whether the routing in `this.config` came from a saved file or an explicit
22
+ * choice, rather than the built-in default. A config-less process that never
23
+ * picked a route must not hard-fail on the preset Router route it inherited.
24
+ */
25
+ private routingIsConfigured;
17
26
  private routerCredentials;
18
27
  private routerCredentialsError;
19
28
  private routerFallbackWarning;
@@ -21,6 +30,7 @@ export declare class ConfigStore {
21
30
  private configScope;
22
31
  /** Session-local overrides. Never persisted to config.json. */
23
32
  private sessionOverrides;
33
+ private restoredSessionSelection;
24
34
  /** CLI-only overrides. Never persisted to config.json. */
25
35
  private cliOverrides;
26
36
  /**
@@ -35,6 +45,10 @@ export declare class ConfigStore {
35
45
  private applyEnvOverrides;
36
46
  load(force?: boolean): Promise<ZTCConfig>;
37
47
  refresh(): Promise<void>;
48
+ /** Copy saved settings and Router credentials, then activate them transactionally.
49
+ * Account registrations retain their credential paths; account homes are not cloned.
50
+ */
51
+ duplicateProfile(requestedName: string, source?: string, activate?: () => void | Promise<void>): Promise<string>;
38
52
  /** Load a profile in this process; restore the old view if runtime activation fails. */
39
53
  switchProfile(name: string, activate: () => void | Promise<void>): Promise<void>;
40
54
  save(): Promise<void>;
@@ -54,6 +68,7 @@ export declare class ConfigStore {
54
68
  setConfigScope(scope: ZTCConfigScope): void;
55
69
  clearSessionOverrides(): void;
56
70
  withConfigScope<T>(scope: ZTCConfigScope, fn: () => T | Promise<T>): Promise<T>;
71
+ private releaseRestoredSelection;
57
72
  set<K extends keyof ZTCConfig>(key: K, value: ZTCConfig[K]): void;
58
73
  setGlobal<K extends keyof ZTCConfig>(key: K, value: ZTCConfig[K]): void;
59
74
  setAll(next: Partial<ZTCConfig>): void;
@@ -68,7 +83,7 @@ export declare class ConfigStore {
68
83
  * `undefined` removes the override instead of shadowing the saved value with
69
84
  * an explicit undefined, so the configured setting applies again.
70
85
  */
71
- setCliOverride<K extends keyof Pick<ZTCConfig, 'provider' | 'model' | 'codexEffort'>>(key: K, value: Pick<ZTCConfig, 'provider' | 'model' | 'codexEffort'>[K]): void;
86
+ setCliOverride<K extends keyof Pick<ZTCConfig, 'provider' | 'model' | 'codexEffort' | 'authMode' | 'zergProxyUrl'>>(key: K, value: Pick<ZTCConfig, 'provider' | 'model' | 'codexEffort' | 'authMode' | 'zergProxyUrl'>[K]): void;
72
87
  getProvider(): string;
73
88
  setProvider(provider: string): void;
74
89
  /** All registered accounts (empty when no pool is configured). */
@@ -110,7 +125,10 @@ export declare class ConfigStore {
110
125
  * Effective Codex effort for the active (or given) provider/model. Priority:
111
126
  * per-model > per-provider > global. Defaults to the current provider/model.
112
127
  */
128
+ getReasoningWireSurface(provider?: string, model?: string): ReasoningWireSurface;
113
129
  getCodexEffort(provider?: string, model?: string): CodexReasoningEffort | undefined;
130
+ /** The active transport owns the model's choices, aliases and advertised default. */
131
+ getReasoningContract(provider?: string, model?: string): ReasoningContract;
114
132
  /** Saved effort the active (or given) model rejects; undefined when it applies. */
115
133
  getUnsupportedCodexEffort(provider?: string, model?: string): CodexReasoningEffort | undefined;
116
134
  /** Effort chosen by a startup `--effort` flag for this process (= none when unset). */
@@ -165,6 +183,21 @@ export declare class ConfigStore {
165
183
  * getters stay — they are still used widely.
166
184
  */
167
185
  getAuthContext(): AuthContext;
186
+ restoreSessionSelection(metadata: {
187
+ provider: string;
188
+ model: string;
189
+ authMode?: ZTCAuthMode;
190
+ routerOrigin?: string;
191
+ configProfile?: string;
192
+ }): void;
193
+ getRouterCatalogConnection(destination?: string | undefined): RouterCatalogConnection | undefined;
194
+ /** New children use configured routing, not a parent's restored inference route. */
195
+ getChildAuthMode(): ZTCAuthMode;
196
+ adoptRouterCatalog(catalog: RouterCatalog): Promise<void>;
197
+ getRouterCatalog(): RouterCatalog | undefined;
198
+ getResolvedAuthMode(): ZTCAuthMode;
199
+ getRoutedModelCatalog(): RouterCatalog | undefined;
200
+ getModelContextWindow(model?: string, provider?: string): number | null | undefined;
168
201
  /**
169
202
  * Resolve everything a backend factory needs into one struct. Folds in the
170
203
  * provider/model override precedence, the direct-key precedence
@@ -179,8 +212,7 @@ export declare class ConfigStore {
179
212
  *
180
213
  * Reads through `get()` and the env-aware getters (never raw `this.config`)
181
214
  * so cli/session overrides stay honored. `apiKey` is the DIRECT provider key;
182
- * `authToken`/`useProxy` describe the proxy route separately, so a caller can
183
- * still prefer a direct key (e.g. Gemini) over the proxy.
215
+ * `authToken`/`useProxy` describe the selected proxy route separately.
184
216
  */
185
217
  resolveLLMBackendConfig(opts?: {
186
218
  provider?: string;
package/dist/config.js CHANGED
@@ -1,7 +1,10 @@
1
- import { join } from 'path';
2
- import { readFile, writeFile, mkdir, chmod, unlink, rename, realpath } from 'fs/promises';
1
+ import { modelContract } from '@zerg/model-contracts';
2
+ import { isLocalSubscriptionProvider } from './agent/backends/provider_ids.js';
3
+ import { resolveRouterModel, readRouterCatalog } from './utils/router_catalog.js';
4
+ import { dirname, join } from 'path';
5
+ import { stat, readFile, writeFile, mkdir, chmod, rm, unlink, rename, realpath } from 'fs/promises';
3
6
  import { randomUUID } from 'crypto';
4
- import { activeProfile, builtinProfileEmulation, profileConfigDir, validateProfileName } from './config/profiles.js';
7
+ import { activeProfile, builtinProfileEmulation, listConfigProfiles, profileConfigDir, validateProfileName } from './config/profiles.js';
5
8
  import { accountSourceToBackendOptions, canonicalAccountProvider, probeAccountIsolated, resolveActiveAccount, resolveRotation, } from './agent/backends/account_pool.js';
6
9
  import { notifyAccountsChanged } from './agent/backends/account_events.js';
7
10
  import { inferDefaultProvider, getDefaultApiKeyForProvider, getDefaultModelForProvider, resolveCodexEffort, unsupportedCodexEffort } from './config/defaults.js';
@@ -290,6 +293,20 @@ const ROUTING_SETTING_KEYS = [
290
293
  function normalizedOrigin(value) {
291
294
  return value?.trim().replace(/\/+$/, '') || undefined;
292
295
  }
296
+ /** Explicit deployment routing takes precedence over a saved session origin. */
297
+ function routerOriginFromEnvironment() {
298
+ if (process.env.ZERG_PROXY_URL)
299
+ return process.env.ZERG_PROXY_URL;
300
+ if (process.env.ZERG_ROUTER_BASE_URL)
301
+ return normalizedOrigin(process.env.ZERG_ROUTER_BASE_URL);
302
+ const tiers = {
303
+ local: 'http://host.docker.internal:8000/api/zrouter',
304
+ dev: 'https://api-dev.zergai.com/api/zrouter', development: 'https://api-dev.zergai.com/api/zrouter',
305
+ prod: 'https://api.zergai.com/api/zrouter', production: 'https://api.zergai.com/api/zrouter', main: 'https://api.zergai.com/api/zrouter',
306
+ };
307
+ const tier = process.env.ZERG_DEPLOY_TIER || '';
308
+ return Object.hasOwn(tiers, tier) ? tiers[tier] : undefined;
309
+ }
293
310
  /**
294
311
  * True when the saved provider route can already reach a model: a direct or
295
312
  * platform credential, a subscription or account login, a configured
@@ -402,6 +419,12 @@ export class ConfigStore {
402
419
  // The legacy slot also carries runtime defaults. Only a configured key may
403
420
  // be migrated into apiKeys or written to disk.
404
421
  legacyApiKeyFromEnvironment = true;
422
+ /**
423
+ * Whether the routing in `this.config` came from a saved file or an explicit
424
+ * choice, rather than the built-in default. A config-less process that never
425
+ * picked a route must not hard-fail on the preset Router route it inherited.
426
+ */
427
+ routingIsConfigured = false;
405
428
  routerCredentials = null;
406
429
  routerCredentialsError = null;
407
430
  routerFallbackWarning = null;
@@ -409,6 +432,7 @@ export class ConfigStore {
409
432
  configScope = 'global';
410
433
  /** Session-local overrides. Never persisted to config.json. */
411
434
  sessionOverrides = {};
435
+ restoredSessionSelection = false;
412
436
  /** CLI-only overrides. Never persisted to config.json. */
413
437
  cliOverrides = {};
414
438
  /**
@@ -475,23 +499,10 @@ export class ConfigStore {
475
499
  else if (tier === 'prod' || tier === 'production' || tier === 'main')
476
500
  this.config.apiEnvironment = 'production';
477
501
  }
478
- // LLM proxy URL (routes through platform ZRouter instead of direct API)
479
- if (process.env.ZERG_PROXY_URL) {
480
- this.config.zergProxyUrl = process.env.ZERG_PROXY_URL;
481
- }
482
- else if (process.env.ZERG_ROUTER_BASE_URL) {
483
- // Standalone ZergRouter (https://zergrouter.com) serves /v1/* at
484
- // its root, no /api/zrouter suffix. Lower precedence than ZERG_PROXY_URL.
485
- this.config.zergProxyUrl = process.env.ZERG_ROUTER_BASE_URL.replace(/\/+$/, '');
486
- }
487
- else if (process.env.ZERG_DEPLOY_TIER) {
488
- const tier = process.env.ZERG_DEPLOY_TIER;
489
- if (tier === 'local')
490
- this.config.zergProxyUrl = 'http://host.docker.internal:8000/api/zrouter';
491
- else if (tier === 'dev' || tier === 'development')
492
- this.config.zergProxyUrl = 'https://api-dev.zergai.com/api/zrouter';
493
- else if (tier === 'prod' || tier === 'production' || tier === 'main')
494
- this.config.zergProxyUrl = 'https://api.zergai.com/api/zrouter';
502
+ // Resolve all explicit origin environment variables through one policy.
503
+ const originOverride = routerOriginFromEnvironment();
504
+ if (originOverride) {
505
+ this.config.zergProxyUrl = originOverride;
495
506
  }
496
507
  else if (!this.config.zergProxyUrl) {
497
508
  // Local default: infer ZRouter from the selected API environment.
@@ -599,6 +610,59 @@ export class ConfigStore {
599
610
  this.sessionOverrides = sessionOverrides;
600
611
  this.configScope = scope;
601
612
  }
613
+ /** Copy saved settings and Router credentials, then activate them transactionally.
614
+ * Account registrations retain their credential paths; account homes are not cloned.
615
+ */
616
+ async duplicateProfile(requestedName, source = activeProfile(), activate = () => { }) {
617
+ const name = validateProfileName(requestedName.trim());
618
+ const from = validateProfileName(source);
619
+ if (name === 'default' || builtinProfileEmulation(name)) {
620
+ throw new Error(`Cannot duplicate onto "${name}": it is a built-in profile sharing the default settings. Pick another name.`);
621
+ }
622
+ if (name === from || (await listConfigProfiles()).includes(name)) {
623
+ throw new Error(`Config profile "${name}" already exists. Pick another name.`);
624
+ }
625
+ const sourceDir = profileConfigDir(from);
626
+ const emulation = builtinProfileEmulation(from);
627
+ if (from !== 'default' && !emulation && !(await stat(sourceDir)).isDirectory()) {
628
+ throw new Error(`Config profile "${from}" does not exist.`);
629
+ }
630
+ let saved = {};
631
+ try {
632
+ const parsed = JSON.parse(await readFile(join(sourceDir, 'config.json'), 'utf8'));
633
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
634
+ throw new Error(`Invalid config profile "${from}": expected an object.`);
635
+ }
636
+ saved = parsed;
637
+ }
638
+ catch (error) {
639
+ if (error.code !== 'ENOENT')
640
+ throw error;
641
+ }
642
+ if (emulation)
643
+ saved.emulationId = emulation;
644
+ const destination = profileConfigDir(name);
645
+ await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
646
+ // An existing destination, including one created concurrently, is never ours to modify.
647
+ await mkdir(destination, { mode: 0o700 });
648
+ try {
649
+ await writeFile(join(destination, 'config.json'), JSON.stringify(saved, null, 2), { flag: 'wx', mode: 0o600 });
650
+ try {
651
+ const auth = await readFile(join(sourceDir, 'router-auth.json'));
652
+ await writeFile(join(destination, 'router-auth.json'), auth, { flag: 'wx', mode: 0o600 });
653
+ }
654
+ catch (error) {
655
+ if (error.code !== 'ENOENT')
656
+ throw error;
657
+ }
658
+ await this.switchProfile(name, activate);
659
+ }
660
+ catch (error) {
661
+ await rm(destination, { recursive: true, force: true });
662
+ throw error;
663
+ }
664
+ return name;
665
+ }
602
666
  /** Load a profile in this process; restore the old view if runtime activation fails. */
603
667
  async switchProfile(name, activate) {
604
668
  const next = validateProfileName(name);
@@ -627,6 +691,7 @@ export class ConfigStore {
627
691
  routerCredentialsError: this.routerCredentialsError,
628
692
  routerFallbackWarning: this.routerFallbackWarning,
629
693
  sessionOverrides: this.sessionOverrides,
694
+ restoredSessionSelection: this.restoredSessionSelection,
630
695
  cliOverrides: this.cliOverrides,
631
696
  runtimeActivePerProvider: this.runtimeActivePerProvider,
632
697
  configScope: this.configScope,
@@ -640,6 +705,7 @@ export class ConfigStore {
640
705
  }
641
706
  else {
642
707
  this.sessionOverrides = {};
708
+ this.restoredSessionSelection = false;
643
709
  this.cliOverrides = {};
644
710
  this.runtimeActivePerProvider = {};
645
711
  this.configScope = 'global';
@@ -732,6 +798,8 @@ export class ConfigStore {
732
798
  return {
733
799
  ...(this.cliOverrides.provider !== undefined ? { provider: this.cliOverrides.provider } : {}),
734
800
  ...(this.cliOverrides.model !== undefined ? { model: this.cliOverrides.model } : {}),
801
+ ...(this.cliOverrides.authMode !== undefined ? { authMode: this.cliOverrides.authMode } : {}),
802
+ ...(this.cliOverrides.zergProxyUrl !== undefined ? { zergProxyUrl: this.cliOverrides.zergProxyUrl } : {}),
735
803
  };
736
804
  }
737
805
  getMaxTokensSource() {
@@ -747,6 +815,7 @@ export class ConfigStore {
747
815
  }
748
816
  clearSessionOverrides() {
749
817
  this.sessionOverrides = {};
818
+ this.restoredSessionSelection = false;
750
819
  }
751
820
  async withConfigScope(scope, fn) {
752
821
  const previous = this.configScope;
@@ -758,7 +827,15 @@ export class ConfigStore {
758
827
  this.configScope = previous;
759
828
  }
760
829
  }
830
+ releaseRestoredSelection(key) {
831
+ if (!this.restoredSessionSelection || !['provider', 'model', 'authMode', 'zergProxyUrl'].includes(key))
832
+ return;
833
+ for (const route of ['provider', 'model', 'authMode', 'zergProxyUrl'])
834
+ delete this.sessionOverrides[route];
835
+ this.restoredSessionSelection = false;
836
+ }
761
837
  set(key, value) {
838
+ this.releaseRestoredSelection(key);
762
839
  if (this.configScope === 'session' || (key === 'emulationId' && this.profileEmulation)) {
763
840
  this.sessionOverrides[key] = value;
764
841
  return;
@@ -768,11 +845,14 @@ export class ConfigStore {
768
845
  this.legacyApiKeyFromEnvironment = false;
769
846
  }
770
847
  setGlobal(key, value) {
848
+ this.releaseRestoredSelection(key);
771
849
  this.config[key] = value;
772
850
  if (key === 'apiKey')
773
851
  this.legacyApiKeyFromEnvironment = false;
774
852
  }
775
853
  setAll(next) {
854
+ for (const key of Object.keys(next))
855
+ this.releaseRestoredSelection(key);
776
856
  this.config = { ...build_default_config(), ...this.config, ...next };
777
857
  if (Object.hasOwn(next, 'apiKey'))
778
858
  this.legacyApiKeyFromEnvironment = false;
@@ -1021,8 +1101,34 @@ export class ConfigStore {
1021
1101
  * Effective Codex effort for the active (or given) provider/model. Priority:
1022
1102
  * per-model > per-provider > global. Defaults to the current provider/model.
1023
1103
  */
1104
+ getReasoningWireSurface(provider = this.getProvider(), model = this.getModel()) {
1105
+ if (provider === 'openai_compatible')
1106
+ return 'chat_tools';
1107
+ if (modelContract(provider, model)?.chatToolReasoningEfforts === undefined)
1108
+ return 'native_api';
1109
+ try {
1110
+ return this.resolveLLMBackendConfig({ provider, model }).useProxy ? 'chat_tools' : 'native_api';
1111
+ }
1112
+ catch (error) {
1113
+ if (error instanceof RouterConfigurationError)
1114
+ return 'chat_tools';
1115
+ throw error;
1116
+ }
1117
+ }
1024
1118
  getCodexEffort(provider, model) {
1025
- return resolveCodexEffort(this.get(), provider ?? this.getProvider(), model ?? this.getModel());
1119
+ const selectedProvider = provider ?? this.getProvider();
1120
+ const selectedModel = model ?? this.getModel();
1121
+ const catalog = this.getRoutedModelCatalog();
1122
+ const capabilities = catalog ? resolveRouterModel(catalog, selectedProvider, selectedModel)?.capabilities : undefined;
1123
+ return resolveCodexEffort(this.get(), selectedProvider, selectedModel, capabilities
1124
+ ?? this.getReasoningWireSurface(selectedProvider, selectedModel));
1125
+ }
1126
+ /** The active transport owns the model's choices, aliases and advertised default. */
1127
+ getReasoningContract(provider = this.getProvider(), model = this.getModel()) {
1128
+ const catalog = this.getRoutedModelCatalog();
1129
+ if (catalog?.version === 1)
1130
+ return resolveRouterModel(catalog, provider, model)?.capabilities?.reasoning ?? [];
1131
+ return this.getReasoningWireSurface(provider, model);
1026
1132
  }
1027
1133
  /** Saved effort the active (or given) model rejects; undefined when it applies. */
1028
1134
  getUnsupportedCodexEffort(provider, model) {
@@ -1201,6 +1307,84 @@ export class ConfigStore {
1201
1307
  authMode: this.getAuthMode(),
1202
1308
  };
1203
1309
  }
1310
+ restoreSessionSelection(metadata) {
1311
+ // Legacy sessions did not persist routes. Explicit launch overrides retain priority.
1312
+ if (!metadata.authMode || this.cliOverrides.provider || this.cliOverrides.model
1313
+ || (metadata.configProfile && metadata.configProfile !== activeProfile()))
1314
+ return;
1315
+ this.restoredSessionSelection = true;
1316
+ this.sessionOverrides.provider = metadata.provider;
1317
+ this.sessionOverrides.model = metadata.model;
1318
+ this.sessionOverrides.authMode = metadata.authMode;
1319
+ if (metadata.authMode !== 'direct' && metadata.routerOrigin && !routerOriginFromEnvironment())
1320
+ this.sessionOverrides.zergProxyUrl = metadata.routerOrigin;
1321
+ }
1322
+ getRouterCatalogConnection(destination = this.get().zergProxyUrl) {
1323
+ const origin = normalizedOrigin(destination);
1324
+ if (!origin)
1325
+ return undefined;
1326
+ const credentials = this.routerCredentials;
1327
+ if (credentials && !this.routerCredentialsError && credentials.origin === origin) {
1328
+ return { origin, token: credentials.token, workspaceId: credentials.workspaceId };
1329
+ }
1330
+ // Platform sessions are scoped to the platform's proxy, never another Router.
1331
+ const token = origin === this.deriveZRouterUrl(this.getApiBaseUrl()) ? this.getAuthToken() : undefined;
1332
+ return token ? { origin, token, workspaceId: this.getActiveWorkspace()?.id.toString() } : undefined;
1333
+ }
1334
+ /** New children use configured routing, not a parent's restored inference route. */
1335
+ getChildAuthMode() {
1336
+ if (!this.restoredSessionSelection)
1337
+ return this.getAuthMode();
1338
+ const { authMode: _restored, ...session } = this.sessionOverrides;
1339
+ return resolveAuthMode({ ...this.config, ...session, ...this.cliOverrides });
1340
+ }
1341
+ async adoptRouterCatalog(catalog) {
1342
+ if (catalog.version !== 1 || catalog.authRequired || this.getResolvedAuthMode() !== 'zrouter')
1343
+ return;
1344
+ const provider = this.getProvider(), model = this.getModel();
1345
+ const row = resolveRouterModel(catalog, provider, model);
1346
+ if (!row || (row.id === model && row.owned_by === provider))
1347
+ return;
1348
+ if (this.cliOverrides.provider || this.cliOverrides.model) {
1349
+ this.cliOverrides.provider = row.owned_by;
1350
+ this.cliOverrides.model = row.id;
1351
+ }
1352
+ else if (this.sessionOverrides.provider || this.sessionOverrides.model) {
1353
+ this.sessionOverrides.provider = row.owned_by;
1354
+ this.sessionOverrides.model = row.id;
1355
+ }
1356
+ else {
1357
+ this.config.provider = row.owned_by;
1358
+ this.config.model = row.id;
1359
+ await this.save();
1360
+ }
1361
+ }
1362
+ getRouterCatalog() {
1363
+ const connection = this.getRouterCatalogConnection();
1364
+ return connection ? readRouterCatalog(connection) : undefined;
1365
+ }
1366
+ getResolvedAuthMode() {
1367
+ const mode = this.getAuthMode();
1368
+ if (mode !== 'auto')
1369
+ return mode;
1370
+ try {
1371
+ return this.resolveLLMBackendConfig().useProxy ? 'zrouter' : 'direct';
1372
+ }
1373
+ catch {
1374
+ return mode;
1375
+ }
1376
+ }
1377
+ getRoutedModelCatalog() {
1378
+ if (isLocalSubscriptionProvider(this.getProvider()))
1379
+ return undefined;
1380
+ return this.getResolvedAuthMode() === 'zrouter' ? this.getRouterCatalog() : undefined;
1381
+ }
1382
+ getModelContextWindow(model = this.getModel(), provider = this.getProvider()) {
1383
+ const catalog = this.getRoutedModelCatalog();
1384
+ if (catalog?.version !== 1)
1385
+ return undefined;
1386
+ return resolveRouterModel(catalog, provider, model)?.capabilities?.context_window ?? null;
1387
+ }
1204
1388
  /**
1205
1389
  * Resolve everything a backend factory needs into one struct. Folds in the
1206
1390
  * provider/model override precedence, the direct-key precedence
@@ -1215,8 +1399,7 @@ export class ConfigStore {
1215
1399
  *
1216
1400
  * Reads through `get()` and the env-aware getters (never raw `this.config`)
1217
1401
  * so cli/session overrides stay honored. `apiKey` is the DIRECT provider key;
1218
- * `authToken`/`useProxy` describe the proxy route separately, so a caller can
1219
- * still prefer a direct key (e.g. Gemini) over the proxy.
1402
+ * `authToken`/`useProxy` describe the selected proxy route separately.
1220
1403
  */
1221
1404
  resolveLLMBackendConfig(opts = {}) {
1222
1405
  // `opts.config` resolves the config reads below (provider, model, authMode,
@@ -1233,15 +1416,15 @@ export class ConfigStore {
1233
1416
  const repoOverride = !opts.provider && !opts.model && !this.hasExplicitModelOverride() && opts.cwd
1234
1417
  ? resolveRepoModelOverride(opts.cwd)
1235
1418
  : null;
1236
- const provider = repoOverride?.provider
1419
+ let provider = repoOverride?.provider
1237
1420
  ?? normalizeProviderId(opts.provider || cfg.provider || this.getProvider());
1238
1421
  const configProvider = cfg.provider ? normalizeProviderId(cfg.provider) : undefined;
1239
- const model = repoOverride?.model
1422
+ let model = repoOverride?.model
1240
1423
  ?? opts.model
1241
1424
  ?? (configProvider === provider ? cfg.model : undefined)
1242
1425
  ?? getDefaultModelForProvider(provider);
1243
- const authMode = resolveAuthMode(cfg);
1244
- const proxyUrl = cfg.zergProxyUrl;
1426
+ const authMode = isLocalSubscriptionProvider(provider) ? 'direct' : resolveAuthMode(cfg);
1427
+ const proxyUrl = normalizedOrigin(cfg.zergProxyUrl);
1245
1428
  // Legacy platform tokens belong only to its /api/zrouter endpoint. A
1246
1429
  // standalone Router requires its own device login, including after logout.
1247
1430
  // Modal shared endpoints are unavailable through the legacy platform proxy.
@@ -1293,7 +1476,19 @@ export class ConfigStore {
1293
1476
  || this.getOpenAICompatibleSettings(cfg).baseUrl
1294
1477
  || cfg.openaiCompatibleBaseUrl;
1295
1478
  const proxyAvailable = Boolean(proxyUrl && authToken);
1296
- const useProxy = authMode === 'direct' ? false : proxyAvailable;
1479
+ // Resolve the route here so discovery and backend construction agree. Auto
1480
+ // retains the existing local-subscription and direct-Gemini preferences.
1481
+ const localSubscription = isLocalSubscriptionProvider(provider);
1482
+ const useProxy = authMode === 'direct' ? false : proxyAvailable
1483
+ && !localSubscription && !(authMode === 'auto' && provider === 'gemini' && Boolean(apiKey));
1484
+ if (useProxy) {
1485
+ const catalog = readRouterCatalog({ origin: proxyUrl, token: authToken, workspaceId: this.getRouterCatalogConnection(proxyUrl)?.workspaceId });
1486
+ const descriptor = catalog?.version === 1 ? resolveRouterModel(catalog, provider, model) : undefined;
1487
+ if (descriptor) {
1488
+ provider = descriptor.owned_by;
1489
+ model = descriptor.id;
1490
+ }
1491
+ }
1297
1492
  const compatBaseUrl = provider === 'openai_compatible' && rawCompatBaseUrl
1298
1493
  ? compatEndpoint(rawCompatBaseUrl, { apiKey })
1299
1494
  : rawCompatBaseUrl;
@@ -1314,7 +1509,9 @@ export class ConfigStore {
1314
1509
  codexHome: accountOptions.codexHome,
1315
1510
  codexAuthFilePath: accountOptions.codexAuthFilePath,
1316
1511
  kimiCredsFilePath: accountOptions.kimiCredsFilePath,
1317
- codexEffort: this.getCodexEffort(provider, model),
1512
+ codexEffort: resolveCodexEffort(cfg, provider, model, (useProxy
1513
+ ? this.getRouterCatalog()?.models.find(row => row.id === model && row.owned_by === provider)?.capabilities : undefined)
1514
+ ?? (useProxy || provider === 'openai_compatible' ? 'chat_tools' : 'native_api')),
1318
1515
  };
1319
1516
  }
1320
1517
  getEnvKey(provider) {
@@ -1335,6 +1532,7 @@ export class ConfigStore {
1335
1532
  : undefined;
1336
1533
  case 'openai': return process.env.OPENAI_API_KEY;
1337
1534
  case 'deepseek': return process.env.DEEPSEEK_API_KEY;
1535
+ case 'fireworks': return process.env.FIREWORKS_API_KEY;
1338
1536
  case 'gemini': return process.env.GOOGLE_API_KEY || process.env.GEMINI_API_KEY;
1339
1537
  case 'inception': return process.env.INCEPTION_API_KEY;
1340
1538
  case 'openai_compatible': return process.env.OPENAI_COMPAT_API_KEY;
package/dist/headless.js CHANGED
@@ -2736,6 +2736,8 @@ export class HeadlessApp {
2736
2736
  * auto-starts; the user runs /goal resume. Shared by both load branches.
2737
2737
  */
2738
2738
  applyResumedSession(session, restoreRuntimeMode = true) {
2739
+ if (!session.hosted_settings)
2740
+ configStore.restoreSessionSelection(session.metadata);
2739
2741
  this.sessionId = session.metadata.id;
2740
2742
  this.sessionManager.setSessionId(this.sessionId);
2741
2743
  this.sessionName = session.metadata.name;
@@ -125,8 +125,12 @@ function formatProjectLine(options) {
125
125
  return parts.join(' · ');
126
126
  }
127
127
  function formatRuntimeLine(options) {
128
+ const providerId = singleLine(options.provider);
129
+ const modelId = singleLine(options.model);
130
+ const displayModel = providerId && modelId.startsWith(`${providerId}/`)
131
+ ? modelId.slice(providerId.length + 1) : modelId;
128
132
  const provider = compact(options.provider, 12) || 'not configured';
129
- const model = compact(options.model, 24) || 'default';
133
+ const model = compact(displayModel, 24) || 'default';
130
134
  const version = compact(options.version, 12);
131
135
  return `Runtime: ${provider}/${model}${version ? ` · ZTC v${version}` : ''}`;
132
136
  }
@@ -1,5 +1,5 @@
1
1
  export declare const PANE_PROVIDERS: readonly ["anthropic", "claude_code", "openai", "codex_app_server", "codex_legacy", "gemini", "deepseek", "fireworks", "modal", "kimi", "kimi_code"];
2
- export type PaneProvider = typeof PANE_PROVIDERS[number];
2
+ export type PaneProvider = string;
3
3
  export interface CatalogModel {
4
4
  id: string;
5
5
  name: string;
@@ -14,13 +14,13 @@ export interface CatalogProviderGroup {
14
14
  models: CatalogModel[];
15
15
  }
16
16
  export declare const MODEL_CATALOG: CatalogProviderGroup[];
17
- export declare function isPaneProvider(value: string | undefined): value is PaneProvider;
17
+ export declare function isPaneProvider(value: string | undefined, route?: 'auto' | 'direct' | 'zrouter'): value is PaneProvider;
18
18
  export declare function getModelDisplayName(modelId: string): string;
19
19
  export declare function getProviderForModel(modelId: string): PaneProvider | undefined;
20
20
  /** Provider-aware membership for duplicate model IDs such as the Codex catalogs. */
21
21
  export declare function isModelAvailableForProvider(provider: string, modelId: string): boolean;
22
22
  export declare function isCatalogModelForProvider(provider: PaneProvider, modelId: string): boolean;
23
- export declare function resolveModelAndProvider(rawModel: string, rawProvider?: string): {
23
+ export declare function resolveModelAndProvider(rawModel: string, rawProvider?: string, route?: 'auto' | 'direct' | 'zrouter'): {
24
24
  model: string;
25
25
  provider?: PaneProvider | string;
26
26
  };
@@ -1,3 +1,4 @@
1
+ import { configStore } from '../config.js';
1
2
  import { GPT_6_API_EFFORTS, getKimiModelInfo, getSupportedCodexReasoningEfforts, KIMI_CODE_MODEL_IDS, KIMI_MODEL_IDS, KIMI_MODEL_INFO, OFFICIAL_CODEX_MODEL_IDS, OFFICIAL_CODEX_MODEL_INFO, } from '../utils/models.js';
2
3
  import { DEEPSEEK_FLASH_EFFORTS, DEEPSEEK_FLASH_MODEL, MODAL_DEEPSEEK_FLASH_EFFORTS, MODAL_DEEPSEEK_FLASH_MODEL } from '../utils/deepseek.js';
3
4
  import { FIREWORKS_CHAT_MODELS, FIREWORKS_FUNDED_MODEL } from '../utils/fireworks.js';
@@ -183,8 +184,8 @@ function getCodexModelAliases(id) {
183
184
  return [];
184
185
  }
185
186
  }
186
- export function isPaneProvider(value) {
187
- return !!value && PROVIDER_SET.has(value);
187
+ export function isPaneProvider(value, route) {
188
+ return !!value && (PROVIDER_SET.has(value) || ((route === 'zrouter' || route === 'auto') && /^[a-z][a-z0-9_-]{0,127}$/.test(value)));
188
189
  }
189
190
  export function getModelDisplayName(modelId) {
190
191
  return MODEL_BY_ID.get(modelId)?.name
@@ -203,8 +204,17 @@ export function isModelAvailableForProvider(provider, modelId) {
203
204
  export function isCatalogModelForProvider(provider, modelId) {
204
205
  return MODEL_CATALOG.some((group) => group.provider === provider && group.models.some((model) => model.id === modelId));
205
206
  }
206
- export function resolveModelAndProvider(rawModel, rawProvider) {
207
+ export function resolveModelAndProvider(rawModel, rawProvider, route) {
207
208
  const trimmed = rawModel.trim();
209
+ if (route === 'zrouter' && rawProvider)
210
+ return { model: trimmed, provider: rawProvider };
211
+ if (route === 'zrouter') {
212
+ const matches = configStore.getRouterCatalog()?.models.filter(row => row.id === trimmed && row.capabilities?.chat);
213
+ if (matches?.length === 1)
214
+ return { model: trimmed, provider: matches[0].owned_by };
215
+ if (matches && matches.length > 1)
216
+ throw new Error('Ambiguous Router model. Specify --provider.');
217
+ }
208
218
  const normalized = normalizeLookupValue(trimmed);
209
219
  const providerNormalized = (rawProvider || '').toLowerCase();
210
220
  // A custom OpenAI-compatible endpoint names its own models, so the id is a
@@ -289,5 +299,19 @@ function getCodexLegacyModelAliases(id) {
289
299
  }
290
300
  }
291
301
  export function getModelCatalogPayload() {
292
- return { providers: MODEL_CATALOG };
302
+ const catalog = configStore.getRoutedModelCatalog();
303
+ if (catalog?.version !== 1)
304
+ return { providers: MODEL_CATALOG };
305
+ const groups = new Map();
306
+ for (const row of catalog.models) {
307
+ if (!row.capabilities?.chat)
308
+ continue;
309
+ const group = groups.get(row.owned_by) ?? { provider: row.owned_by, label: row.provider_display_name || row.owned_by, models: [] };
310
+ group.models.push({ id: row.id, name: row.display_name || row.id,
311
+ description: [row.capabilities.tools === false ? 'Chat only' : '', row.available === false ? 'Access unavailable' : ''].filter(Boolean).join(' · '),
312
+ supportedReasoningEfforts: row.capabilities.reasoning?.choices,
313
+ defaultReasoningEffort: row.capabilities.reasoning?.default });
314
+ groups.set(row.owned_by, group);
315
+ }
316
+ return { providers: [...groups.values()] };
293
317
  }
@@ -26,15 +26,19 @@ function inferZtcLaunchSpec(pane) {
26
26
  }
27
27
  if (!sessionId)
28
28
  return undefined;
29
+ const route = parseFlagValue(args, '--auth-mode');
30
+ const authMode = route === 'zrouter' || route === 'direct' || route === 'auto' ? route : undefined;
29
31
  const explicitProvider = parseFlagValue(args, '--provider');
30
32
  const explicitModel = parseFlagValue(args, '--model');
31
- const resolved = explicitModel ? resolveModelAndProvider(explicitModel, explicitProvider) : { model: undefined, provider: explicitProvider };
33
+ const resolved = explicitModel ? resolveModelAndProvider(explicitModel, explicitProvider, authMode) : { model: undefined, provider: explicitProvider };
32
34
  return {
33
35
  kind: 'ztc',
36
+ authMode,
37
+ routerOrigin: parseFlagValue(args, '--router-url'),
34
38
  session: { mode: sessionMode, id: sessionId },
35
39
  cwd: pane.cwd,
36
40
  title: pane.title,
37
- providerOverride: isPaneProvider(resolved.provider) ? resolved.provider : undefined,
41
+ providerOverride: isPaneProvider(resolved.provider, authMode) ? resolved.provider : undefined,
38
42
  modelOverride: resolved.model,
39
43
  };
40
44
  }
@@ -13,6 +13,8 @@ export type PaneSessionSelector = {
13
13
  id: string;
14
14
  };
15
15
  export interface ZtcPaneLaunchSpec {
16
+ authMode?: 'auto' | 'direct' | 'zrouter';
17
+ routerOrigin?: string;
16
18
  kind: 'ztc';
17
19
  session: PaneSessionSelector;
18
20
  cwd?: string;