@zergai/ztc 0.3.1-dev.36465749722 → 0.4.1-dev.36919234045
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/THIRD_PARTY_NOTICES.md +4 -0
- package/dist/App.js +2 -2
- package/dist/agent/agent.js +41 -5
- package/dist/agent/backends/anthropic.js +5 -10
- package/dist/agent/backends/gemini.js +2 -0
- package/dist/agent/backends/index.d.ts +1 -0
- package/dist/agent/backends/index.js +10 -1
- package/dist/agent/backends/openai.d.ts +0 -6
- package/dist/agent/backends/openai.js +5 -2
- package/dist/agent/backends/openai_compatible.d.ts +2 -0
- package/dist/agent/backends/openai_compatible.js +18 -8
- package/dist/agent/backends/provider_ids.d.ts +1 -0
- package/dist/agent/backends/provider_ids.js +4 -0
- package/dist/agent/backends/types.d.ts +1 -0
- package/dist/agent/backends/zrouter_stream.d.ts +5 -1
- package/dist/agent/backends/zrouter_stream.js +141 -37
- package/dist/agent/commands/compact.js +6 -5
- package/dist/agent/commands/config.js +4 -1
- package/dist/agent/commands/context.js +6 -1
- package/dist/agent/commands/effort.js +14 -6
- package/dist/agent/commands/model.js +12 -2
- package/dist/agent/commands/profile.js +21 -2
- package/dist/agent/commands/provider_selection.js +14 -1
- package/dist/agent/commands/settings.js +45 -7
- package/dist/agent/commands/types.d.ts +19 -0
- package/dist/agent/factory.js +14 -68
- package/dist/agent/subagent.d.ts +0 -10
- package/dist/agent/subagent.js +22 -0
- package/dist/agent/tool_response_store.d.ts +1 -1
- package/dist/agent/tool_response_store.js +4 -2
- package/dist/agent/tools/pane.d.ts +0 -7
- package/dist/agent/tools/pane.js +5 -4
- package/dist/app/command_context.js +33 -10
- package/dist/app/model_catalog_cache.d.ts +5 -0
- package/dist/app/model_catalog_cache.js +138 -0
- package/dist/app/restart.d.ts +1 -1
- package/dist/app/restart.js +16 -5
- package/dist/app/selection_completion.d.ts +17 -2
- package/dist/app/selection_completion.js +74 -6
- package/dist/app/session_manager.js +13 -8
- package/dist/cli_args.js +2 -16
- package/dist/cli_main.js +25 -6
- package/dist/cli_ratatui.js +70 -33
- package/dist/config/defaults.d.ts +2 -1
- package/dist/config/defaults.js +9 -2
- package/dist/config/resolved.d.ts +2 -2
- package/dist/config.d.ts +35 -3
- package/dist/config.js +227 -29
- package/dist/headless.js +2 -0
- package/dist/onboarding/first_run.js +5 -1
- package/dist/panes/model_catalog.d.ts +3 -3
- package/dist/panes/model_catalog.js +28 -4
- package/dist/panes/pane_registry.js +6 -2
- package/dist/panes/types.d.ts +2 -0
- package/dist/panes/ztc_launch.js +12 -0
- package/dist/session.d.ts +6 -2
- package/dist/session.js +65 -10
- package/dist/ui/views/completion_menu.js +10 -5
- package/dist/ui/views/status_bar.js +9 -5
- package/dist/ui/widgets/resume_panel.d.ts +2 -0
- package/dist/ui/widgets/resume_panel.js +17 -15
- package/dist/utils/compaction.d.ts +1 -1
- package/dist/utils/compaction.js +2 -2
- package/dist/utils/context.d.ts +4 -4
- package/dist/utils/context.js +14 -12
- package/dist/utils/deepseek.d.ts +2 -2
- package/dist/utils/deepseek.js +13 -5
- package/dist/utils/model_cache.d.ts +15 -0
- package/dist/utils/model_cache.js +143 -0
- package/dist/utils/models.d.ts +4 -2
- package/dist/utils/models.js +12 -16
- package/dist/utils/reasoning.d.ts +8 -1
- package/dist/utils/reasoning.js +31 -11
- package/dist/utils/router_catalog.d.ts +47 -0
- package/dist/utils/router_catalog.js +222 -0
- package/dist/zlm-embed.mjs +1180 -514
- package/node_modules/@zerg/model-contracts/LICENSE.md +375 -0
- package/node_modules/@zerg/model-contracts/README.md +78 -0
- package/node_modules/@zerg/model-contracts/index.d.ts +18 -0
- package/node_modules/@zerg/model-contracts/index.js +220 -0
- package/node_modules/@zerg/model-contracts/package.json +8 -0
- package/node_modules/glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/glob/node_modules/brace-expansion/package.json +1 -1
- package/node_modules/readdir-glob/node_modules/brace-expansion/index.js +78 -22
- package/node_modules/readdir-glob/node_modules/brace-expansion/package.json +1 -1
- package/package.json +6 -3
- package/packages/model-contracts/LICENSE.md +375 -0
- package/packages/model-contracts/README.md +78 -0
- package/packages/model-contracts/index.d.ts +18 -0
- package/packages/model-contracts/index.js +220 -0
- package/packages/model-contracts/package.json +8 -0
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
|
|
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
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -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]);
|
package/dist/agent/agent.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
//
|
|
103
|
-
//
|
|
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 =
|
|
110
|
-
const modelAllowsEffort =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
...(
|
|
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
|
|
46
|
+
private serializeBody;
|
|
43
47
|
private buildHeaders;
|
|
44
48
|
/**
|
|
45
49
|
* The conversation identity this turn declares to Router, which forwards it to
|