@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/README.md CHANGED
@@ -68,7 +68,7 @@ selecting the same provider preserves your current model.
68
68
  Type `/model ` to search providers and current-provider models by ID, name, or alias.
69
69
  Search is fuzzy, so `/model dskfl` finds DeepSeek Flash. Use Up/Down to select,
70
70
  Tab or Enter to complete a provider and continue to its models. Enter on a model
71
- suggestion fills its ID; press Enter again to apply it. Escape dismisses suggestions.
71
+ suggestion applies the selected model immediately; Tab fills its ID. Escape dismisses suggestions.
72
72
  Custom model IDs can still be entered directly.
73
73
  After a provider is entered, the picker names it and lists only its models. A successful
74
74
  live catalog replaces bundled suggestions, including when the catalog is empty;
@@ -577,6 +577,13 @@ When a saved session includes `zmux` state, `ztc resume` restores the pane layou
577
577
 
578
578
  Custom config profiles keep settings and sign-ins separate. Start one with `ztc --profile work`, or switch the running session with `/profile work`. `/profile create work` starts a new profile. Switching keeps the session ID and conversation while rebuilding the agent. A later `ztc resume` restores the session's saved profile unless you specify `--profile`. The default config stays at `~/.ztc/config.json`; custom named configs live at `~/.ztc/profiles/<name>/config.json`. Session history stays shared. Switching does not rewrite settings after the one-time migration described below.
579
579
 
580
+ The source `scripts/ztc-dev` launcher defaults to the `dev` config profile. It uses
581
+ the same `~/.ztc/profiles/dev/` settings and Router sign-in as `ztc --profile dev`.
582
+ Set its Router URL and sign in separately when creating that profile; the name
583
+ does not select a development endpoint by itself. An explicit `--profile default`
584
+ or `ZTC_PROFILE=default` overrides the launcher default. `ZTC_CONFIG_DIR` still
585
+ selects the shared config root. `ztc-dev resume` also selects `dev` unless overridden.
586
+
580
587
  On first use of this release, each existing config profile adopts the new default settings once. Its account registrations, authentication, API keys, and credential-bearing integration settings are retained. Later edits to that profile are kept. New profiles start with the same defaults. The migration writes only the active profile's config file, so switching to an unused profile applies its migration then.
581
588
 
582
589
  #### Bottom status line
@@ -14,6 +14,10 @@ or—when neither ships a license file—the crate's Cargo attribution plus the
14
14
  official SPDX license text. A choice such as `MIT OR Apache-2.0` means a recipient
15
15
  may use the component under either listed license.
16
16
 
17
+ The internal `@zerg/model-contracts` package is first-party Zerg code, version
18
+ 0.1.0, under the Zerg Source Available License 1.0. Its distribution includes
19
+ `LICENSE.md`; it adds no third-party dependencies.
20
+
17
21
  ## Direct npm dependencies
18
22
 
19
23
  | Package | Locked version | License | Upstream |
package/dist/App.js CHANGED
@@ -237,8 +237,8 @@ export const App = () => {
237
237
  // the provider-reported window (Codex) as the denominator. Only before the
238
238
  // first API response (no real count yet) do we fall back to the char/4 estimate.
239
239
  const contextStatus = useMemo(() => (agentState.contextTokens && agentState.contextTokens > 0)
240
- ? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow)
241
- : getContextStatus(messages, currentModel), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
240
+ ? contextStatusFromUsed(agentState.contextTokens, currentModel, agentState.contextWindow ?? configStore.getModelContextWindow(currentModel))
241
+ : getContextStatus(messages, currentModel, configStore.getModelContextWindow(currentModel)), [agentState.contextTokens, agentState.contextWindow, messages, currentModel]);
242
242
  const spinnerVerbs = configStore.get().spinnerVerbs || DEFAULT_SPINNER_VERBS;
243
243
  const spinnerVerbsKey = spinnerVerbs.join('|');
244
244
  const queuePreview = useMemo(() => getQueuePreview(queue), [queue]);
@@ -242,6 +242,10 @@ export class Agent {
242
242
  this.alwaysLoadedToolNames.add(name);
243
243
  }
244
244
  this.backend = this.config.backend;
245
+ if (this.backend.getModelCapabilities?.(this.config.model)?.tools === false) {
246
+ this.config.tools = [];
247
+ this.config.autoContinue = false;
248
+ }
245
249
  this.policy = this.config.policy;
246
250
  this.tracer = this.config.tracer;
247
251
  this._cwd = this.config.cwd;
@@ -257,7 +261,7 @@ export class Agent {
257
261
  this._hasCustomSystemPrompt = typeof config.systemPrompt === 'string' && config.systemPrompt.trim().length > 0;
258
262
  }
259
263
  setAutoContinue(enabled) {
260
- this._autoContinue = enabled;
264
+ this._autoContinue = enabled && this.backend.getModelCapabilities?.(this.config.model)?.tools !== false;
261
265
  }
262
266
  /** Override the agent's system prompt (e.g., for simulation persona injection). */
263
267
  setSystemPrompt(prompt) {
@@ -409,7 +413,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
409
413
  this._currentRequestToolIds = currentTurnToolIds;
410
414
  if (allToolIds.length > 0) {
411
415
  const activeModel = this.config.model || 'default';
412
- const budget = this.toolResponseStore.effectiveBudget(activeModel);
416
+ const budget = this.toolResponseStore.effectiveBudget(activeModel, this.backend.getModelCapabilities?.(activeModel) ? this.backend.getModelCapabilities(activeModel)?.context_window ?? null : undefined);
413
417
  this._selectorVerdict = this.toolResponseStore.selectInlineSet(allToolIds, currentTurnToolIds, budget);
414
418
  }
415
419
  else {
@@ -1192,6 +1196,27 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1192
1196
  // Skip the buffered pacing when the text was already streamed live.
1193
1197
  if (!this._lastResponseStreamed)
1194
1198
  await this.emitStreamText(iterationText, signal);
1199
+ // Deliver busy input before automatic continuation or ending this turn.
1200
+ // At the iteration cap leave the queue intact for the renderer's next turn.
1201
+ if (iterations < this.config.maxIterations) {
1202
+ const steering = takeSteering?.() ?? [];
1203
+ if (steering.length > 0) {
1204
+ if (iterationText.trim()) {
1205
+ const answer = {
1206
+ id: `assistant_${Date.now()}_${iterations}`, role: 'assistant', content: iterationText, timestamp: new Date(),
1207
+ ...(response.reasoning ? { metadata: { reasoningContent: response.reasoning } } : {}),
1208
+ };
1209
+ messages.push(answer);
1210
+ turnMessages.push(answer);
1211
+ }
1212
+ messages.push(...steering);
1213
+ turnMessages.push(...steering);
1214
+ this.emit({ type: 'history_checkpoint', messages: [...turnMessages] });
1215
+ finalContent = '';
1216
+ finalReasoning = undefined;
1217
+ continue;
1218
+ }
1219
+ }
1195
1220
  // Check if we should auto-continue. Bounded: every continuation is a
1196
1221
  // full-context request, so a model that keeps narrating an intention
1197
1222
  // instead of calling a tool must not be able to spend maxIterations
@@ -1811,7 +1836,8 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1811
1836
  let errorMessage = '';
1812
1837
  let admissionMessage = '';
1813
1838
  try {
1814
- const jsonMatch = message.match(/:\s*(\{[\s\S]*\})\s*$/);
1839
+ const bodyMessage = message.replace(/ \[(?:(?:fly-request-id|x-request-id|cf-ray|x-zerg-error-code)=[^\]\r\n ]+ ?)+\]$/, '');
1840
+ const jsonMatch = bodyMessage.match(/:\s*(\{[\s\S]*\})\s*$/);
1815
1841
  if (jsonMatch) {
1816
1842
  const parsed = JSON.parse(jsonMatch[1]);
1817
1843
  const errorPayload = parsed.error;
@@ -1848,6 +1874,11 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1848
1874
  catch {
1849
1875
  // Not valid JSON, use raw message
1850
1876
  }
1877
+ if (statusCode === 410 && message.startsWith('ZRouter API error (')
1878
+ && ['stream_producer_lost', 'stream_producer_failed', 'stream_resume_expired'].includes(errorCode)) {
1879
+ return { retryable: false, reason: 'The Router stream was interrupted and cannot be resumed. '
1880
+ + 'Send a message to continue from the existing conversation.' };
1881
+ }
1851
1882
  // Detected before the generic 400 bucket so the host can dump the request.
1852
1883
  if (errorCode === 'context_length_exceeded' || isContextOverflowMessage(`${errorMessage} ${message}`)) {
1853
1884
  return { retryable: false, reason: 'Context window exceeded', kind: 'context_overflow' };
@@ -1860,7 +1891,12 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1860
1891
  return { retryable: true, reason: 'Rate limit exceeded' };
1861
1892
  }
1862
1893
  if (statusCode === 503 || statusCode === 502 || statusCode === 500 || errorType === 'UNAVAILABLE' || errorType === 'INTERNAL') {
1863
- return { retryable: true, reason: 'Server temporarily unavailable' };
1894
+ // Only Router admission guidance is public. Provider diagnostics stay
1895
+ // private; a bounded response-header ID supplies support correlation.
1896
+ const requestId = /(?:fly-request-id|x-request-id)=([A-Za-z0-9._:-]{1,128})(?:[ \]]|$)/.exec(message)?.[1];
1897
+ const detail = [admissionMessage.trim().replace(/\s+/g, ' '), requestId && `request_id: ${requestId}`].filter(Boolean).join('; ');
1898
+ const cause = detail ? `: ${detail.length > 160 ? `${detail.slice(0, 159)}…` : detail}` : '';
1899
+ return { retryable: true, reason: `Upstream server error (${statusCode || 'no status'})${cause}` };
1864
1900
  }
1865
1901
  // Non-retryable errors with clean messages
1866
1902
  if (statusCode === 401 || errorType === 'authentication_error' || errorType === 'UNAUTHENTICATED') {
@@ -1876,7 +1912,7 @@ INVESTIGATION STRATEGY — before making tool calls to explore or debug, follow
1876
1912
  // unsubscribed user hits on the default route, so say what to do. Only the
1877
1913
  // Router's H3 envelope may supply its public wording; any other 402 body
1878
1914
  // stays private behind the canonical message.
1879
- if (statusCode === 402) {
1915
+ if (statusCode === 402 || errorCode === 'billing_required') {
1880
1916
  const guidance = 'Subscribe in Zerg Router (Dashboard → Billing), or choose another model with /model.';
1881
1917
  return {
1882
1918
  retryable: false,
@@ -1,5 +1,5 @@
1
+ import { modelContract } from '@zerg/model-contracts';
1
2
  import { parseSSEStream } from './sse.js';
2
- import { getSupportedClaudeReasoningEfforts } from '../../utils/models.js';
3
3
  // The TRUE context size for a request = uncached input + cache-read + cache-write.
4
4
  // Reading only input_tokens makes the context bar jump BACKWARDS on a cache hit
5
5
  // (input_tokens is then just the small uncached delta), which looks non-monotonic.
@@ -99,16 +99,11 @@ export class AnthropicBackend {
99
99
  tools[tools.length - 1] = { ...tools[tools.length - 1], cache_control: cc };
100
100
  }
101
101
  }
102
- // Reasoning effort (GA on Opus 4.5+/Sonnet 4.6): output_config.effort. The
103
- // value rides on request.codexEffort (the generic per-pane effort). 'minimal'
104
- // is codex-only; the Anthropic API accepts low|medium|high|xhigh|max. Gate on
105
- // the target model too: Haiku exposes NO effort parameter, so sending one
106
- // hard-400s every turn — getSupportedClaudeReasoningEfforts returns [] for it
107
- // (drop) and undefined for unrecognized ids (stay permissive).
102
+ // Exact reviewed families, including dated aliases, own the effort ladder.
103
+ // Unknown families and Haiku expose no unverified effort parameter.
108
104
  const effort = request.codexEffort;
109
- const supportedEfforts = getSupportedClaudeReasoningEfforts(request.model);
110
- const modelAllowsEffort = supportedEfforts === undefined ||
111
- (effort !== undefined && supportedEfforts.includes(effort));
105
+ const supportedEfforts = modelContract('anthropic', request.model)?.reasoningEfforts ?? [];
106
+ const modelAllowsEffort = effort !== undefined && supportedEfforts.includes(effort);
112
107
  const outputConfig = effort && ['low', 'medium', 'high', 'xhigh', 'max'].includes(effort) && modelAllowsEffort
113
108
  ? { output_config: { effort } }
114
109
  : {};
@@ -1,3 +1,4 @@
1
+ import { reasoningRequestParams } from '@zerg/model-contracts';
1
2
  import { BaseBackend } from './base.js';
2
3
  export class GeminiBackend extends BaseBackend {
3
4
  apiKey;
@@ -33,6 +34,7 @@ export class GeminiBackend extends BaseBackend {
33
34
  });
34
35
  };
35
36
  const body = {
37
+ ...reasoningRequestParams('gemini', request.model, request.codexEffort),
36
38
  systemInstruction: { parts: [{ text: request.system }] },
37
39
  contents: request.messages.map(m => ({
38
40
  role: m.role === 'assistant' ? 'model' : 'user',
@@ -59,6 +59,7 @@ export declare class TracedBackend implements AgentBackend {
59
59
  private readonly provider;
60
60
  private readonly backend;
61
61
  constructor(provider: string, backend: AgentBackend);
62
+ getModelCapabilities(model: string): import("../../utils/router_catalog.js").RouterModelCapabilities | undefined;
62
63
  steerUserMessage(message: Message): Promise<boolean>;
63
64
  /** Forward disposal so a process-backed inner (RotatingBackend, codex child)
64
65
  * is closed when the agent is. Without this, tracing (ZTC_BACKEND_TRACE=1)
@@ -38,6 +38,7 @@ export class TracedBackend {
38
38
  this.provider = provider;
39
39
  this.backend = backend;
40
40
  }
41
+ getModelCapabilities(model) { return this.backend.getModelCapabilities?.(model); }
41
42
  async steerUserMessage(message) {
42
43
  return this.backend.steerUserMessage?.(message) ?? false;
43
44
  }
@@ -148,10 +149,17 @@ export function createBackendForProvider(provider, opts) {
148
149
  return traced(provider, new KimiBackend({ apiKey: opts.apiKey }));
149
150
  case 'deepseek':
150
151
  return traced(provider, new OpenAICompatibleBackend({
152
+ provider: 'deepseek',
151
153
  apiKey: opts.apiKey,
152
154
  baseUrl: opts.baseUrl || 'https://api.deepseek.com/v1',
153
155
  requestBodyExtras: deepseekRequestParams,
154
156
  }));
157
+ case 'fireworks':
158
+ return traced(provider, new OpenAICompatibleBackend({
159
+ apiKey: opts.apiKey,
160
+ baseUrl: opts.baseUrl || 'https://api.fireworks.ai/inference/v1',
161
+ provider: 'fireworks',
162
+ }));
155
163
  case 'clarifai':
156
164
  return traced(provider, new ClarifaiBackend({ apiKey: opts.apiKey, headers: opts.extraHeaders }));
157
165
  case CODEX_APP_SERVER_PROVIDER:
@@ -206,7 +214,8 @@ export function createBackendForProvider(provider, opts) {
206
214
  }
207
215
  case 'anthropic':
208
216
  case 'claude':
209
- default:
210
217
  return traced(provider, new AnthropicBackend({ apiKey: opts.apiKey, apiEndpoint: opts.apiEndpoint }));
218
+ default:
219
+ throw new Error(`Unknown provider: ${provider}. Select a Router integration or a supported direct provider.`);
211
220
  }
212
221
  }
@@ -1,9 +1,3 @@
1
- /**
2
- * OpenAI backend — smart router between Chat Completions and Responses API.
3
- *
4
- * Codex models and GPT-6 Sol/Luna use Responses so reasoning and tool calls
5
- * can be combined. Everything else goes through Chat Completions.
6
- */
7
1
  import { AgentBackend, BackendRequest, BackendResponse, StreamEvent } from './types.js';
8
2
  interface OpenAIConfig {
9
3
  apiKey: string;
@@ -1,7 +1,8 @@
1
+ import { modelContract } from '@zerg/model-contracts';
1
2
  /**
2
3
  * OpenAI backend — smart router between Chat Completions and Responses API.
3
4
  *
4
- * Codex models and GPT-6 Sol/Luna use Responses so reasoning and tool calls
5
+ * Codex models and reviewed Responses tool models use Responses so reasoning and tool calls
5
6
  * can be combined. Everything else goes through Chat Completions.
6
7
  */
7
8
  import { OpenAICompatibleBackend } from './openai_compatible.js';
@@ -12,7 +13,8 @@ const RESPONSES_API_PATTERNS = [
12
13
  /^gpt-6-(?:sol|luna)$/i,
13
14
  ];
14
15
  function needsResponsesAPI(model) {
15
- return RESPONSES_API_PATTERNS.some(p => p.test(model));
16
+ return modelContract('openai', model)?.chatToolReasoningEfforts !== undefined
17
+ || RESPONSES_API_PATTERNS.some(p => p.test(model));
16
18
  }
17
19
  export class OpenAIBackend {
18
20
  chatBackend;
@@ -23,6 +25,7 @@ export class OpenAIBackend {
23
25
  apiKey: config.apiKey,
24
26
  baseUrl,
25
27
  apiPath: '/chat/completions',
28
+ provider: 'openai',
26
29
  });
27
30
  this.responsesBackend = new OpenAIResponsesBackend({
28
31
  apiKey: config.apiKey,
@@ -18,6 +18,7 @@ interface OpenAICompatibleConfig {
18
18
  * Cannot override `model`/`messages`/`tools`/the token limit — those are set
19
19
  * after this spread.
20
20
  */
21
+ provider?: string;
21
22
  requestBodyExtras?: (request: BackendRequest) => Record<string, unknown>;
22
23
  }
23
24
  export declare class OpenAICompatibleApiError extends Error {
@@ -31,6 +32,7 @@ export declare class OpenAICompatibleBackend implements AgentBackend {
31
32
  private apiPath;
32
33
  private headers;
33
34
  private extraBody;
35
+ private provider?;
34
36
  private requestBodyExtras?;
35
37
  private authTokenProvider?;
36
38
  constructor(config: OpenAICompatibleConfig);
@@ -1,3 +1,4 @@
1
+ import { fireworksRequestModel, modelContract, reasoningRequestParams } from '@zerg/model-contracts';
1
2
  import { measureResponse, measureStream } from './request_metrics.js';
2
3
  import { parseSSEStream } from './sse.js';
3
4
  import { parseTokenUsage } from './token_usage.js';
@@ -21,6 +22,7 @@ export class OpenAICompatibleBackend {
21
22
  apiPath;
22
23
  headers;
23
24
  extraBody;
25
+ provider;
24
26
  requestBodyExtras;
25
27
  authTokenProvider;
26
28
  constructor(config) {
@@ -29,6 +31,7 @@ export class OpenAICompatibleBackend {
29
31
  this.apiPath = config.apiPath || '/chat/completions';
30
32
  this.headers = config.headers || {};
31
33
  this.extraBody = config.extraBody || {};
34
+ this.provider = config.provider;
32
35
  this.requestBodyExtras = config.requestBodyExtras;
33
36
  this.authTokenProvider = config.authTokenProvider;
34
37
  }
@@ -39,29 +42,36 @@ export class OpenAICompatibleBackend {
39
42
  // O-series (o1, o3, o4-mini) and GPT-5/6 use max_completion_tokens instead of
40
43
  // max_tokens; Moonshot marks max_tokens "Deprecated, please refer to
41
44
  // max_completion_tokens" for the Kimi models too.
42
- const useCompletionTokens = /^o\d/.test(request.model)
45
+ const useCompletionTokens = this.provider === 'fireworks' ? false : /^o\d/.test(request.model)
43
46
  || /^gpt-[56]/.test(request.model)
44
47
  || isKimiModel(request.model);
45
48
  // Cap max_tokens for models with lower limits (gpt-4o: 16384)
46
49
  const isGpt4o = request.model.startsWith('gpt-4o');
47
- const limit = isGpt4o ? 16384 : isDeepSeekFlashModel(request.model) ? DEEPSEEK_FLASH_MAX_OUTPUT : Infinity;
50
+ const reviewedLimit = this.provider ? modelContract(this.provider, request.model)?.maxOutputTokens?.max : undefined;
51
+ const limit = reviewedLimit ?? (isGpt4o ? 16384 : isDeepSeekFlashModel(request.model) ? DEEPSEEK_FLASH_MAX_OUTPUT : Infinity);
48
52
  const effectiveMaxTokens = request.maxTokens === undefined ? undefined : Math.min(request.maxTokens, limit);
49
53
  const tokenParam = effectiveMaxTokens === undefined ? {} : useCompletionTokens
50
54
  ? { max_completion_tokens: effectiveMaxTokens }
51
55
  : { max_tokens: effectiveMaxTokens };
52
- const isGpt6Model = /^gpt-6-(?:sol|luna)$/i.test(request.model);
53
- const reasoningEffort = (request.model === 'gpt-5.6-sol' || isGpt6Model) && request.tools.length > 0
54
- ? 'none'
55
- : isGpt6Model ? request.codexEffort : undefined;
56
+ // Chat tool support differs from Responses. Never silently downgrade an
57
+ // explicit grade; default Chat requests use the reviewed tool-safe value.
58
+ const policyProvider = this.provider ?? 'openai';
59
+ const chatEfforts = modelContract(policyProvider, request.model)?.chatToolReasoningEfforts;
60
+ const toolRestricted = request.tools.length > 0 && chatEfforts !== undefined;
61
+ if (toolRestricted && request.codexEffort !== undefined && !chatEfforts.includes(request.codexEffort)) {
62
+ throw new Error('Use Responses for graded reasoning with tools on this model. Chat Completions supports effort none.');
63
+ }
64
+ const reasoningEffort = toolRestricted ? request.codexEffort ?? 'none' : request.codexEffort;
56
65
  const body = {
57
66
  ...this.extraBody,
67
+ ...reasoningRequestParams(policyProvider, request.model, reasoningEffort),
58
68
  ...(this.requestBodyExtras?.(request) ?? {}),
59
- model: request.model,
69
+ model: this.provider === 'fireworks' ? fireworksRequestModel(request.model) : request.model,
60
70
  ...tokenParam,
61
71
  // Chat Completions accepts GPT-6 Sol/Luna function tools only with
62
72
  // reasoning disabled. The direct OpenAI backend uses Responses instead;
63
73
  // keep this rule for explicitly configured compatible endpoints.
64
- ...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
74
+ ...(toolRestricted ? { reasoning_effort: reasoningEffort } : {}),
65
75
  messages: buildOpenAIMessages(request),
66
76
  ...(request.tools.length > 0 ? { tools: buildOpenAIToolDefs(request.tools) } : {})
67
77
  };
@@ -7,3 +7,4 @@ export declare function normalizeProviderId(provider: string): string;
7
7
  export declare function isCodexAppServerProvider(provider: string): boolean;
8
8
  export declare function isCodexLegacyProvider(provider: string): boolean;
9
9
  export declare function getProviderDisplayName(provider: string): string;
10
+ export declare function isLocalSubscriptionProvider(provider: string): boolean;
@@ -37,3 +37,7 @@ export function getProviderDisplayName(provider) {
37
37
  return provider;
38
38
  }
39
39
  }
40
+ export function isLocalSubscriptionProvider(provider) {
41
+ return ['codex_app_server', 'codex_legacy', 'claude_code', 'claude-code', 'claude_code_direct', 'claude-code-direct', 'kimi_code', 'kimi-code']
42
+ .includes(normalizeProviderId(provider));
43
+ }
@@ -259,6 +259,7 @@ export type StreamEvent = {
259
259
  stopReason: string;
260
260
  };
261
261
  export interface AgentBackend {
262
+ getModelCapabilities?(model: string): import('../../utils/router_catalog.js').RouterModelCapabilities | undefined;
262
263
  /** Deliver user input into a provider-owned active turn when supported. */
263
264
  steerUserMessage?(message: Message): Promise<boolean>;
264
265
  /**
@@ -1,5 +1,7 @@
1
+ import type { RouterCatalog, RouterModelCapabilities } from '../../utils/router_catalog.js';
1
2
  import { AgentBackend, BackendRequest, BackendResponse, StreamEvent } from './types.js';
2
3
  export interface ZRouterStreamConfig {
4
+ catalog?: RouterCatalog;
3
5
  apiKey: string;
4
6
  proxyUrl: string;
5
7
  provider: string;
@@ -26,6 +28,7 @@ export interface ZRouterStreamConfig {
26
28
  * uses the unified chat endpoint so routing can fall back across providers.
27
29
  */
28
30
  export declare class ZRouterStreamBackend implements AgentBackend {
31
+ private readonly catalog?;
29
32
  private apiKey;
30
33
  private proxyUrl;
31
34
  private provider;
@@ -38,8 +41,9 @@ export declare class ZRouterStreamBackend implements AgentBackend {
38
41
  isAuthenticationConfigured(): boolean;
39
42
  allowsBufferedFallbackAfterStreamFailure(): boolean;
40
43
  handlesTransportRetries(): boolean;
44
+ getModelCapabilities(model: string): RouterModelCapabilities | undefined;
41
45
  private buildOpenAIBody;
42
- private buildBody;
46
+ private serializeBody;
43
47
  private buildHeaders;
44
48
  /**
45
49
  * The conversation identity this turn declares to Router, which forwards it to