@bitkyc08/opencodex 2.7.30 → 2.7.33-preview.20260722
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.ja.md +438 -0
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.ru.md +480 -0
- package/README.zh-CN.md +1 -1
- package/bin/ocx.mjs +18 -1
- package/gui/dist/assets/index-B79f-04T.js +52 -0
- package/gui/dist/assets/index-D6Fcl4yM.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +17 -2
- package/src/adapters/cursor/discovery.ts +2 -2
- package/src/adapters/google-tool-schema.ts +4 -0
- package/src/adapters/google.ts +17 -2
- package/src/adapters/openai-chat.ts +36 -1
- package/src/adapters/openai-responses.ts +2 -1
- package/src/bridge.ts +12 -4
- package/src/cli/account-api.ts +4 -2
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude.ts +6 -1
- package/src/cli/help.ts +25 -4
- package/src/cli/init.ts +38 -3
- package/src/cli/models.ts +206 -7
- package/src/codex/auth-api.ts +45 -3
- package/src/codex/catalog.ts +105 -12
- package/src/codex/routing.ts +85 -4
- package/src/combos/index.ts +1 -1
- package/src/combos/request.ts +26 -4
- package/src/combos/types.ts +4 -4
- package/src/config.ts +60 -3
- package/src/lib/upstream-retry.ts +21 -0
- package/src/lib/winsw.ts +7 -1
- package/src/oauth/anthropic.ts +23 -1
- package/src/oauth/github-copilot.ts +1 -0
- package/src/oauth/index.ts +88 -8
- package/src/oauth/kiro.ts +12 -1
- package/src/oauth/local-token-detect.ts +3 -0
- package/src/oauth/store.ts +43 -0
- package/src/oauth/types.ts +3 -1
- package/src/providers/antigravity-models.ts +123 -14
- package/src/providers/api-keys.ts +12 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/registry.ts +41 -17
- package/src/router.ts +2 -1
- package/src/server/auth-cors.ts +5 -0
- package/src/server/index.ts +3 -3
- package/src/server/management-api.ts +143 -6
- package/src/server/relay.ts +31 -5
- package/src/server/request-log.ts +12 -0
- package/src/server/responses.ts +127 -31
- package/src/service.ts +12 -3
- package/src/types.ts +44 -3
- package/src/update/index.ts +19 -2
- package/src/update/job.ts +13 -3
- package/src/usage/expected-prices.ts +22 -9
- package/src/usage/summary.ts +43 -0
- package/gui/dist/assets/index-B-UauL1p.css +0 -1
- package/gui/dist/assets/index-avcinRsG.js +0 -40
package/src/oauth/store.ts
CHANGED
|
@@ -36,6 +36,38 @@ export function getAuthRefreshIntentLockPath(provider: string, accountId: string
|
|
|
36
36
|
const accountHash = createHash("sha256").update(accountId).digest("hex").slice(0, 24);
|
|
37
37
|
return join(getConfigDir(), `auth.refresh.${safeProvider}.${accountHash}.lock`);
|
|
38
38
|
}
|
|
39
|
+
export function getAuthRefreshIntentPath(provider: string, accountId: string): string {
|
|
40
|
+
return `${getAuthRefreshIntentLockPath(provider, accountId)}.json`;
|
|
41
|
+
}
|
|
42
|
+
export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; uncertain?: true }
|
|
43
|
+
export function readOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent | undefined {
|
|
44
|
+
const path = getAuthRefreshIntentPath(provider, accountId);
|
|
45
|
+
try {
|
|
46
|
+
hardenConfigDir();
|
|
47
|
+
hardenExistingSecret(path);
|
|
48
|
+
const value = JSON.parse(readFileSync(path, "utf8")) as Partial<OAuthRefreshIntent>;
|
|
49
|
+
if (value.version !== 1 || value.provider !== provider || value.accountId !== accountId || typeof value.generation !== "string" || typeof value.createdAt !== "number") {
|
|
50
|
+
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
51
|
+
}
|
|
52
|
+
return value as OAuthRefreshIntent;
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
55
|
+
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now()): void {
|
|
59
|
+
const dir = getConfigDir();
|
|
60
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
|
+
hardenConfigDir();
|
|
62
|
+
const intent: OAuthRefreshIntent = { version: 1, provider, accountId, generation, createdAt };
|
|
63
|
+
atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify(intent)}\n`);
|
|
64
|
+
}
|
|
65
|
+
export function clearOAuthRefreshIntent(provider: string, accountId: string, generation: string): boolean {
|
|
66
|
+
const current = readOAuthRefreshIntent(provider, accountId);
|
|
67
|
+
if (!current || current.generation !== generation) return false;
|
|
68
|
+
try { unlinkSync(getAuthRefreshIntentPath(provider, accountId)); return true; }
|
|
69
|
+
catch (error) { if (errorCode(error) === "ENOENT") return false; throw error; }
|
|
70
|
+
}
|
|
39
71
|
export function credentialGeneration(cred: OAuthCredentials): string {
|
|
40
72
|
return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
|
|
41
73
|
}
|
|
@@ -147,6 +179,7 @@ function normalizeAccount(value: unknown): ProviderAccount | null {
|
|
|
147
179
|
const credential = normalizeCredential(candidate.credential);
|
|
148
180
|
if (!credential) return null;
|
|
149
181
|
const account: ProviderAccount = { id: candidate.id, credential };
|
|
182
|
+
if (typeof candidate.alias === "string" && candidate.alias.trim()) account.alias = candidate.alias.trim();
|
|
150
183
|
if (candidate.needsReauth === true) account.needsReauth = true;
|
|
151
184
|
if (typeof candidate.addedAt === "number") account.addedAt = candidate.addedAt;
|
|
152
185
|
return account;
|
|
@@ -309,6 +342,16 @@ export async function setActiveAccount(provider: string, accountId: string): Pro
|
|
|
309
342
|
});
|
|
310
343
|
}
|
|
311
344
|
|
|
345
|
+
export async function setAccountAlias(provider: string, accountId: string, alias: string | undefined): Promise<boolean> {
|
|
346
|
+
return await mutateStore(store => {
|
|
347
|
+
const account = store[provider]?.accounts.find(a => a.id === accountId);
|
|
348
|
+
if (!account) return false;
|
|
349
|
+
if (alias) account.alias = alias;
|
|
350
|
+
else delete account.alias;
|
|
351
|
+
return true;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
312
355
|
/** Remove one account by id; active removal promotes the first remaining account. */
|
|
313
356
|
export async function removeAccount(provider: string, accountId: string): Promise<boolean> {
|
|
314
357
|
return await mutateStore(store => {
|
package/src/oauth/types.ts
CHANGED
|
@@ -21,6 +21,8 @@ export type OAuthCredentials = {
|
|
|
21
21
|
export interface ProviderAccount {
|
|
22
22
|
/** Stable short id, generated once at append time; never re-derived after rotation. */
|
|
23
23
|
id: string;
|
|
24
|
+
/** User-owned display label; never participates in auth identity or routing. */
|
|
25
|
+
alias?: string;
|
|
24
26
|
credential: OAuthCredentials;
|
|
25
27
|
/** Terminal refresh failure (invalid_grant / reused / revoked) — re-login required. */
|
|
26
28
|
needsReauth?: boolean;
|
|
@@ -34,7 +36,7 @@ export interface ProviderAccountSet {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
export interface OAuthController {
|
|
37
|
-
onAuth?(info: { url: string; instructions?: string }): void;
|
|
39
|
+
onAuth?(info: { url: string; instructions?: string; deviceCode?: string }): void;
|
|
38
40
|
onProgress?(message: string): void;
|
|
39
41
|
onManualCodeInput?(expectedState?: string): Promise<string>;
|
|
40
42
|
signal?: AbortSignal;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Google Antigravity (Cloud Code Assist) bundled model list.
|
|
2
2
|
//
|
|
3
3
|
// Single source of truth: the Antigravity `:fetchAvailableModels` backend, the same one the `agy`
|
|
4
|
-
// CLI resolves labels against. The ids below
|
|
5
|
-
//
|
|
6
|
-
// "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
4
|
+
// CLI resolves labels against. The ids below separate CCA wire ids, collapsed picker entries,
|
|
5
|
+
// and hidden compatibility aliases for saved selections. The CCA envelope's `model` field must
|
|
6
|
+
// receive the wire id (for example "Gemini 3.1 Pro (High)" => gemini-pro-agent), while the
|
|
7
|
+
// picker exposes collapsed base models with reasoning-effort routing.
|
|
8
|
+
|
|
9
|
+
// ── Wire IDs (what CCA :fetchAvailableModels returns) ──
|
|
7
10
|
const ANTIGRAVITY_WIRE_MODELS = [
|
|
8
|
-
"gemini-3.
|
|
9
|
-
"gemini-3-flash-
|
|
10
|
-
"gemini-3.
|
|
11
|
+
"gemini-3.6-flash-low",
|
|
12
|
+
"gemini-3.6-flash-medium",
|
|
13
|
+
"gemini-3.6-flash-high",
|
|
11
14
|
"gemini-3.1-pro-low",
|
|
12
15
|
"gemini-pro-agent",
|
|
13
16
|
"claude-sonnet-4-6",
|
|
@@ -15,23 +18,76 @@ const ANTIGRAVITY_WIRE_MODELS = [
|
|
|
15
18
|
"gpt-oss-120b-medium",
|
|
16
19
|
];
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
// ── Effort ladders per collapsed base model ──
|
|
22
|
+
// Gemini models: effort → wire model suffix (official agy UI pattern).
|
|
23
|
+
// Claude Opus: effort → thinkingConfig.thinkingLevel (CLIProxyAPI proven pattern).
|
|
24
|
+
export const ANTIGRAVITY_MODEL_EFFORTS: Record<string, string[]> = {
|
|
25
|
+
"gemini-3.6-flash": ["low", "medium", "high"],
|
|
26
|
+
"gemini-3.1-pro": ["low", "high"],
|
|
27
|
+
"claude-sonnet-4-6": ["low", "medium", "high", "max"],
|
|
28
|
+
"claude-opus-4-6-thinking": ["low", "medium", "high", "max"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ── Effort → wire model map for Gemini base models ──
|
|
32
|
+
const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
33
|
+
"gemini-3.6-flash": {
|
|
34
|
+
low: "gemini-3.6-flash-low",
|
|
35
|
+
medium: "gemini-3.6-flash-medium",
|
|
36
|
+
high: "gemini-3.6-flash-high",
|
|
37
|
+
},
|
|
38
|
+
"gemini-3.1-pro": {
|
|
39
|
+
low: "gemini-3.1-pro-low",
|
|
40
|
+
high: "gemini-pro-agent",
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ── Default effort per Gemini base model ──
|
|
45
|
+
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
46
|
+
"gemini-3.6-flash": "medium",
|
|
47
|
+
"gemini-3.1-pro": "high",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ── Visible client aliases (kept for saved-config compat, not picker-visible) ──
|
|
51
|
+
const ANTIGRAVITY_VISIBLE_MODEL_ALIASES: Record<string, string> = {
|
|
21
52
|
"gemini-3.1-pro-high": "gemini-pro-agent",
|
|
22
53
|
"gemini-3.1-pro-preview": "gemini-pro-agent",
|
|
23
54
|
};
|
|
24
55
|
|
|
56
|
+
// ── Hidden compatibility aliases for saved selections ──
|
|
57
|
+
// Wire suffix IDs are identity aliases — they resolve to themselves so saved configs
|
|
58
|
+
// with explicit suffixes (e.g. gemini-3.6-flash-low) continue to work.
|
|
59
|
+
const ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES: Record<string, string> = {
|
|
60
|
+
"gemini-3.6-flash-low": "gemini-3.6-flash-low",
|
|
61
|
+
"gemini-3.6-flash-medium": "gemini-3.6-flash-medium",
|
|
62
|
+
"gemini-3.6-flash-high": "gemini-3.6-flash-high",
|
|
63
|
+
"gemini-3.1-pro-low": "gemini-3.1-pro-low",
|
|
64
|
+
"gemini-pro-agent": "gemini-pro-agent",
|
|
65
|
+
"gemini-3.5-flash-extra-low": "gemini-3.6-flash-low",
|
|
66
|
+
"gemini-3.5-flash-low": "gemini-3.6-flash-medium",
|
|
67
|
+
"gemini-3.5-flash-mid": "gemini-3.6-flash-medium",
|
|
68
|
+
"gemini-3.5-flash-high": "gemini-3.6-flash-high",
|
|
69
|
+
"gemini-3-flash-agent": "gemini-3.6-flash-high",
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const ANTIGRAVITY_MODEL_ALIASES: Record<string, string> = {
|
|
73
|
+
...ANTIGRAVITY_VISIBLE_MODEL_ALIASES,
|
|
74
|
+
...ANTIGRAVITY_COMPATIBILITY_MODEL_ALIASES,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Picker-visible: collapsed base models only.
|
|
25
78
|
export const ANTIGRAVITY_MODELS = [
|
|
26
|
-
|
|
27
|
-
|
|
79
|
+
"gemini-3.6-flash",
|
|
80
|
+
"gemini-3.1-pro",
|
|
81
|
+
"claude-sonnet-4-6",
|
|
82
|
+
"claude-opus-4-6-thinking",
|
|
83
|
+
"gpt-oss-120b-medium",
|
|
28
84
|
];
|
|
29
85
|
|
|
30
86
|
// Context windows from the upstream `:fetchAvailableModels` maxTokens per model.
|
|
31
87
|
const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
32
|
-
"gemini-3.
|
|
33
|
-
"gemini-3-flash-
|
|
34
|
-
"gemini-3.
|
|
88
|
+
"gemini-3.6-flash-low": 1_048_576,
|
|
89
|
+
"gemini-3.6-flash-medium": 1_048_576,
|
|
90
|
+
"gemini-3.6-flash-high": 1_048_576,
|
|
35
91
|
"gemini-3.1-pro-low": 1_048_576,
|
|
36
92
|
"gemini-pro-agent": 1_048_576,
|
|
37
93
|
"claude-sonnet-4-6": 200_000,
|
|
@@ -40,6 +96,10 @@ const ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
40
96
|
};
|
|
41
97
|
|
|
42
98
|
export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
99
|
+
// Collapsed base IDs — explicit entries for the picker.
|
|
100
|
+
"gemini-3.6-flash": 1_048_576,
|
|
101
|
+
"gemini-3.1-pro": 1_048_576,
|
|
102
|
+
// Wire IDs and aliases via derivation.
|
|
43
103
|
...ANTIGRAVITY_WIRE_MODEL_CONTEXT_WINDOWS,
|
|
44
104
|
...Object.fromEntries(
|
|
45
105
|
Object.entries(ANTIGRAVITY_MODEL_ALIASES).map(([alias, wire]) => [
|
|
@@ -52,3 +112,52 @@ export const ANTIGRAVITY_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
|
|
|
52
112
|
export function resolveAntigravityWireModelId(modelId: string): string {
|
|
53
113
|
return ANTIGRAVITY_MODEL_ALIASES[modelId] ?? modelId;
|
|
54
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the given model ID is a suffix wire ID or compat alias that already encodes
|
|
118
|
+
* an effort level. For these IDs, the caller must NOT send thinkingConfig — the suffix
|
|
119
|
+
* IS the effort, and sending both creates a contradictory request.
|
|
120
|
+
*/
|
|
121
|
+
export function isAntigravitySuffixModelId(modelId: string): boolean {
|
|
122
|
+
return !(ANTIGRAVITY_MODELS as string[]).includes(modelId);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve a picker-visible base model + optional reasoning effort to the CCA wire model ID.
|
|
127
|
+
*
|
|
128
|
+
* Precedence (evaluated in order):
|
|
129
|
+
* 1. Suffix wire ID or compat alias → resolve via `resolveAntigravityWireModelId`, no thinkingConfig.
|
|
130
|
+
* 2. Mapped Gemini base with effort → return mapped wire ID + thinkingLevel.
|
|
131
|
+
* 3. Mapped Gemini base without effort → return default-effort wire ID, no thinkingConfig.
|
|
132
|
+
* 4. Claude Opus with effort → return identity + thinkingLevel (no suffix variants exist).
|
|
133
|
+
* 5. All other IDs → return `resolveAntigravityWireModelId(modelId)`, no thinkingConfig.
|
|
134
|
+
*/
|
|
135
|
+
export function resolveAntigravityEffortWireModel(
|
|
136
|
+
modelId: string,
|
|
137
|
+
effort?: string,
|
|
138
|
+
): { wireModelId: string; thinkingLevel?: string } {
|
|
139
|
+
// Rule 1: suffix/compat alias — suffix IS the effort.
|
|
140
|
+
if (isAntigravitySuffixModelId(modelId)) {
|
|
141
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Rule 2/3: mapped Gemini base model.
|
|
145
|
+
const effortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[modelId];
|
|
146
|
+
if (effortMap) {
|
|
147
|
+
if (effort && effort in effortMap) {
|
|
148
|
+
return { wireModelId: effortMap[effort]!, thinkingLevel: effort };
|
|
149
|
+
}
|
|
150
|
+
const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]!;
|
|
151
|
+
return { wireModelId: effortMap[defaultEffort]! };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Rule 4: Claude models — effort via thinkingConfig only (no suffix variants).
|
|
155
|
+
// Anthropic adaptive thinking supports low/medium/high/max for both Sonnet 4.6 and Opus 4.6.
|
|
156
|
+
// CLIProxyAPI proves CCA accepts thinkingConfig on base IDs (validation confirmed).
|
|
157
|
+
if (/^claude-/.test(modelId) && effort) {
|
|
158
|
+
return { wireModelId: modelId, thinkingLevel: effort };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Rule 5: everything else.
|
|
162
|
+
return { wireModelId: resolveAntigravityWireModelId(modelId) };
|
|
163
|
+
}
|
|
@@ -102,6 +102,18 @@ export function setActiveProviderApiKey(config: OcxConfig, name: string, id: str
|
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** Rename a key slot without changing its id, secret, or active routing state. */
|
|
106
|
+
export function setProviderApiKeyLabel(config: OcxConfig, name: string, id: string, label: string | undefined): boolean {
|
|
107
|
+
const provider = config.providers[name];
|
|
108
|
+
if (!provider || !isKeyAuthProvider(provider)) return false;
|
|
109
|
+
const entry = ensurePool(provider).find(e => e.id === id);
|
|
110
|
+
if (!entry) return false;
|
|
111
|
+
if (label) entry.label = label;
|
|
112
|
+
else delete entry.label;
|
|
113
|
+
saveConfig(config);
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
105
117
|
/** Remove one key; removing the active one promotes the first remaining. Persists config. */
|
|
106
118
|
export function removeProviderApiKey(config: OcxConfig, name: string, id: string): boolean {
|
|
107
119
|
const provider = config.providers[name];
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { OcxProviderConfig, OpenRouterProviderRouting } from "../types";
|
|
2
|
+
|
|
3
|
+
const ROUTING_KEYS = new Set(["order", "only", "allowFallbacks"]);
|
|
4
|
+
const MAX_PROVIDER_SLUGS = 64;
|
|
5
|
+
|
|
6
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
8
|
+
const prototype = Object.getPrototypeOf(value);
|
|
9
|
+
return prototype === Object.prototype || prototype === null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isCanonicalOpenRouterTarget(baseUrl: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(baseUrl);
|
|
15
|
+
return url.origin === "https://openrouter.ai"
|
|
16
|
+
&& !url.username
|
|
17
|
+
&& !url.password
|
|
18
|
+
&& !url.search
|
|
19
|
+
&& !url.hash
|
|
20
|
+
&& url.pathname.replace(/\/+$/, "") === "/api/v1";
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function routingPreferenceError(value: unknown, field: string): string | null {
|
|
27
|
+
if (!isPlainRecord(value)) return `${field} must be a plain object`;
|
|
28
|
+
const unknown = Object.keys(value).find(key => !ROUTING_KEYS.has(key));
|
|
29
|
+
if (unknown) return `${field} contains unknown field "${unknown}"`;
|
|
30
|
+
|
|
31
|
+
for (const listField of ["order", "only"] as const) {
|
|
32
|
+
const list = value[listField];
|
|
33
|
+
if (list === undefined) continue;
|
|
34
|
+
if (!Array.isArray(list) || list.length === 0 || list.length > MAX_PROVIDER_SLUGS) {
|
|
35
|
+
return `${field}.${listField} must contain 1-${MAX_PROVIDER_SLUGS} provider slugs`;
|
|
36
|
+
}
|
|
37
|
+
const seen = new Set<string>();
|
|
38
|
+
for (const slug of list) {
|
|
39
|
+
if (typeof slug !== "string" || !slug.trim() || slug !== slug.trim() || slug.length > 128) {
|
|
40
|
+
return `${field}.${listField} must contain nonblank trimmed provider slugs up to 128 characters`;
|
|
41
|
+
}
|
|
42
|
+
if (seen.has(slug)) return `${field}.${listField} must not contain duplicate provider slugs`;
|
|
43
|
+
seen.add(slug);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (value.allowFallbacks !== undefined && typeof value.allowFallbacks !== "boolean") {
|
|
47
|
+
return `${field}.allowFallbacks must be a boolean`;
|
|
48
|
+
}
|
|
49
|
+
if (value.order === undefined && value.only === undefined && value.allowFallbacks === undefined) {
|
|
50
|
+
return `${field} must define order, only, or allowFallbacks`;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function openRouterRoutingConfigError(provider: OcxProviderConfig): string | null {
|
|
56
|
+
const hasDefault = provider.openRouterRouting !== undefined;
|
|
57
|
+
const hasModels = provider.modelOpenRouterRouting !== undefined;
|
|
58
|
+
if (!hasDefault && !hasModels) return null;
|
|
59
|
+
if (provider.adapter !== "openai-chat") {
|
|
60
|
+
return "OpenRouter routing preferences require the openai-chat adapter";
|
|
61
|
+
}
|
|
62
|
+
if (!isCanonicalOpenRouterTarget(provider.baseUrl)) {
|
|
63
|
+
return "OpenRouter routing preferences require the canonical https://openrouter.ai/api/v1 baseUrl";
|
|
64
|
+
}
|
|
65
|
+
if (hasDefault) {
|
|
66
|
+
const error = routingPreferenceError(provider.openRouterRouting, "openRouterRouting");
|
|
67
|
+
if (error) return error;
|
|
68
|
+
}
|
|
69
|
+
if (hasModels) {
|
|
70
|
+
const routes = provider.modelOpenRouterRouting;
|
|
71
|
+
if (!isPlainRecord(routes)) return "modelOpenRouterRouting must be a plain object";
|
|
72
|
+
for (const [modelId, preference] of Object.entries(routes)) {
|
|
73
|
+
if (!modelId.trim() || modelId !== modelId.trim()) {
|
|
74
|
+
return "modelOpenRouterRouting keys must be nonblank trimmed model ids";
|
|
75
|
+
}
|
|
76
|
+
const error = routingPreferenceError(preference, `modelOpenRouterRouting.${modelId}`);
|
|
77
|
+
if (error) return error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resolveOpenRouterRouting(
|
|
84
|
+
provider: OcxProviderConfig,
|
|
85
|
+
modelId: string,
|
|
86
|
+
): OpenRouterProviderRouting | undefined {
|
|
87
|
+
if (!isCanonicalOpenRouterTarget(provider.baseUrl)) return undefined;
|
|
88
|
+
const modelRoutes = provider.modelOpenRouterRouting;
|
|
89
|
+
return modelRoutes && Object.hasOwn(modelRoutes, modelId)
|
|
90
|
+
? modelRoutes[modelId]
|
|
91
|
+
: provider.openRouterRouting;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function openRouterProviderPayload(
|
|
95
|
+
preference: OpenRouterProviderRouting,
|
|
96
|
+
): Record<string, unknown> {
|
|
97
|
+
return {
|
|
98
|
+
...(preference.order ? { order: [...preference.order] } : {}),
|
|
99
|
+
...(preference.only ? { only: [...preference.only] } : {}),
|
|
100
|
+
...(preference.allowFallbacks !== undefined ? { allow_fallbacks: preference.allowFallbacks } : {}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CodexAccountMode, OcxProviderConfig } from "../types";
|
|
2
2
|
import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
|
|
3
|
-
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS } from "./antigravity-models";
|
|
3
|
+
import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS } from "./antigravity-models";
|
|
4
4
|
import type { ProviderBaseUrlChoice } from "./base-url-choices";
|
|
5
5
|
import {
|
|
6
6
|
QWEN_CLOUD_BASE_URL_CHOICES, QWEN_CLOUD_TOKEN_PLAN_BASE_URL,
|
|
@@ -65,6 +65,8 @@ export interface ProviderRegistryEntry {
|
|
|
65
65
|
noPenaltyModels?: string[];
|
|
66
66
|
/** Opt this provider into parallel tool calls (see OcxProviderConfig.parallelToolCalls). */
|
|
67
67
|
parallelToolCalls?: boolean;
|
|
68
|
+
/** Opt this provider into forwarding prompt_cache_key (OpenAI-specific; strict backends reject it). */
|
|
69
|
+
promptCacheKey?: boolean;
|
|
68
70
|
autoToolChoiceOnlyModels?: string[];
|
|
69
71
|
preserveReasoningContentModels?: string[];
|
|
70
72
|
thinkingToggleModels?: string[];
|
|
@@ -196,36 +198,42 @@ const ALIBABA_TOKEN_PLAN_QWEN_MODELS = [
|
|
|
196
198
|
];
|
|
197
199
|
const ALIBABA_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
198
200
|
"qwen3.8-max-preview": ["text", "image"],
|
|
199
|
-
"qwen3.7-max": ["text"],
|
|
201
|
+
"qwen3.7-max": ["text", "image"],
|
|
200
202
|
"qwen3.7-plus": ["text", "image"],
|
|
201
203
|
"qwen3.6-flash": ["text", "image"],
|
|
202
204
|
"glm-5.2": ["text"],
|
|
203
205
|
"deepseek-v4-pro": ["text"],
|
|
204
206
|
};
|
|
205
207
|
|
|
206
|
-
// 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore).
|
|
208
|
+
// 260721 Alibaba Token Plan International (ap-southeast-1 / Singapore, hardened 260721).
|
|
207
209
|
// Multi-vendor lineup distinct from Beijing — includes DeepSeek V4 flash, Kimi K2.7, MiniMax.
|
|
208
210
|
// Evidence: https://www.alibabacloud.com/help/en/model-studio/token-plan-overview
|
|
211
|
+
// https://qwencloud.com/pricing/token-plan (qwen3.8 metadata)
|
|
209
212
|
const ALIBABA_INTL_TOKEN_PLAN_MODELS = [
|
|
210
|
-
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
213
|
+
"qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
211
214
|
"deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2",
|
|
212
|
-
"kimi-k2.7-code",
|
|
213
|
-
"glm-5.2",
|
|
215
|
+
"kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5",
|
|
216
|
+
"glm-5.2", "glm-5.1", "glm-5",
|
|
214
217
|
"MiniMax-M2.5",
|
|
215
218
|
];
|
|
216
219
|
const ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS = [
|
|
217
|
-
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
220
|
+
"qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash",
|
|
218
221
|
];
|
|
219
222
|
const ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES: Record<string, string[]> = {
|
|
220
|
-
"qwen3.
|
|
223
|
+
"qwen3.8-max-preview": ["text", "image"],
|
|
224
|
+
"qwen3.7-max": ["text", "image"],
|
|
221
225
|
"qwen3.7-plus": ["text", "image"],
|
|
222
226
|
"qwen3.6-plus": ["text", "image"],
|
|
223
227
|
"qwen3.6-flash": ["text", "image"],
|
|
224
228
|
"deepseek-v4-pro": ["text"],
|
|
225
229
|
"deepseek-v4-flash": ["text"],
|
|
226
230
|
"deepseek-v3.2": ["text"],
|
|
227
|
-
"kimi-k2.7-code": ["text"],
|
|
231
|
+
"kimi-k2.7-code": ["text", "image"],
|
|
232
|
+
"kimi-k2.6": ["text", "image"],
|
|
233
|
+
"kimi-k2.5": ["text", "image"],
|
|
228
234
|
"glm-5.2": ["text"],
|
|
235
|
+
"glm-5.1": ["text"],
|
|
236
|
+
"glm-5": ["text"],
|
|
229
237
|
"MiniMax-M2.5": ["text"],
|
|
230
238
|
};
|
|
231
239
|
|
|
@@ -632,9 +640,11 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
632
640
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
633
641
|
{
|
|
634
642
|
id: "google", label: "Google Gemini", adapter: "google", baseUrl: "https://generativelanguage.googleapis.com", authKind: "key", featured: true,
|
|
635
|
-
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.5-flash", "gemini-3.1-pro-preview"],
|
|
636
|
-
modelContextWindows: { "gemini-3.5-flash": 1_000_000 },
|
|
643
|
+
dashboardUrl: "https://aistudio.google.com/apikey", defaultModel: "gemini-3.5-flash", models: ["gemini-3.6-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite", "gemini-3.1-pro-preview"],
|
|
644
|
+
modelContextWindows: { "gemini-3.6-flash": 1_048_576, "gemini-3.5-flash": 1_000_000, "gemini-3.5-flash-lite": 1_048_576 },
|
|
645
|
+
modelInputModalities: { "gemini-3.6-flash": ["text", "image"], "gemini-3.5-flash-lite": ["text", "image"] },
|
|
637
646
|
modelReasoningEfforts: {
|
|
647
|
+
"gemini-3.6-flash": ["minimal", "low", "medium", "high"],
|
|
638
648
|
"gemini-3.5-flash": ["minimal", "low", "medium", "high"],
|
|
639
649
|
"gemini-3.1-pro-preview": ["low", "medium", "high"],
|
|
640
650
|
},
|
|
@@ -643,7 +653,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
643
653
|
// 2026-07-10: defaultModel is frozen pending Vertex-specific Tier-2 evidence; Gemini API
|
|
644
654
|
// evidence from ai.google.dev does not establish Vertex publisher availability.
|
|
645
655
|
{ id: "google-vertex", label: "Google Vertex AI", adapter: "google", baseUrl: "https://aiplatform.googleapis.com", authKind: "key", dashboardUrl: "https://console.cloud.google.com/vertex-ai", defaultModel: "gemini-3-pro", googleMode: "vertex", jawcodeBundle: "google", extraMetadataAliases: ["gemini-vertex"] },
|
|
646
|
-
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.
|
|
656
|
+
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] },
|
|
647
657
|
{ id: "azure-openai", label: "Azure OpenAI", adapter: "azure-openai", baseUrl: "https://{resource}.openai.azure.com/openai", authKind: "key", featured: true, dashboardUrl: "https://portal.azure.com" },
|
|
648
658
|
{ id: "ollama", label: "Ollama (local)", adapter: "openai-chat", baseUrl: "http://localhost:11434/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
649
659
|
{ id: "vllm", label: "vLLM (local)", adapter: "openai-chat", baseUrl: "http://localhost:8000/v1", authKind: "local", allowPrivateNetworkByDefault: true, allowBaseUrlOverride: true, featured: true, note: "Local — key usually blank" },
|
|
@@ -762,6 +772,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
762
772
|
liveModels: false,
|
|
763
773
|
note: "Token Plan Personal Edition · China (Beijing)",
|
|
764
774
|
modelInputModalities: ALIBABA_TOKEN_PLAN_INPUT_MODALITIES,
|
|
775
|
+
modelContextWindows: {
|
|
776
|
+
"qwen3.8-max-preview": 983_616, "qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000,
|
|
777
|
+
"qwen3.6-flash": 1_000_000, "glm-5.2": 1_000_000, "deepseek-v4-pro": 1_000_000,
|
|
778
|
+
},
|
|
765
779
|
modelReasoningEfforts: {
|
|
766
780
|
...Object.fromEntries(ALIBABA_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
767
781
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
@@ -769,7 +783,8 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
769
783
|
},
|
|
770
784
|
modelReasoningEffortMap: { "deepseek-v4-pro": DEEPSEEK_THINKING_REASONING_MAP },
|
|
771
785
|
thinkingBudgetModels: ALIBABA_TOKEN_PLAN_QWEN_MODELS,
|
|
772
|
-
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max-preview"],
|
|
786
|
+
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
|
|
787
|
+
noVisionModels: ["glm-5.2", "deepseek-v4-pro"],
|
|
773
788
|
},
|
|
774
789
|
{
|
|
775
790
|
id: "alibaba-token-plan-intl",
|
|
@@ -786,9 +801,17 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
786
801
|
note: "Token Plan Team Edition · Singapore (ap-southeast-1)",
|
|
787
802
|
metadataModelIdNormalize: "case-insensitive",
|
|
788
803
|
modelInputModalities: ALIBABA_INTL_TOKEN_PLAN_INPUT_MODALITIES,
|
|
789
|
-
modelContextWindows: {
|
|
804
|
+
modelContextWindows: {
|
|
805
|
+
"qwen3.8-max-preview": 983_616,
|
|
806
|
+
"qwen3.7-max": 1_000_000, "qwen3.7-plus": 1_000_000, "qwen3.6-plus": 1_000_000, "qwen3.6-flash": 1_000_000,
|
|
807
|
+
"deepseek-v4-pro": 1_000_000, "deepseek-v4-flash": 1_000_000, "deepseek-v3.2": 131_072,
|
|
808
|
+
"kimi-k2.7-code": 262_144, "kimi-k2.6": 262_144, "kimi-k2.5": 262_144,
|
|
809
|
+
"glm-5.2": 1_000_000, "glm-5.1": 1_000_000, "glm-5": 1_000_000,
|
|
810
|
+
"MiniMax-M2.5": 204_800,
|
|
811
|
+
},
|
|
790
812
|
modelReasoningEfforts: {
|
|
791
813
|
...Object.fromEntries(ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS.map(id => [id, THINKING_BUDGET_EFFORTS])),
|
|
814
|
+
"qwen3.8-max-preview": ["low", "high", "xhigh"],
|
|
792
815
|
"glm-5.2": ZAI_GLM_52_REASONING_EFFORTS,
|
|
793
816
|
"deepseek-v4-pro": DEEPSEEK_THINKING_EFFORTS,
|
|
794
817
|
"deepseek-v4-flash": DEEPSEEK_THINKING_EFFORTS,
|
|
@@ -798,9 +821,10 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
798
821
|
"deepseek-v4-flash": DEEPSEEK_THINKING_REASONING_MAP,
|
|
799
822
|
},
|
|
800
823
|
thinkingBudgetModels: ALIBABA_INTL_TOKEN_PLAN_QWEN_MODELS,
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
noReasoningModels: ["kimi-k2.7-code", "deepseek-v3.2", "MiniMax-M2.5"],
|
|
824
|
+
preserveReasoningContentModels: ["glm-5.2", "deepseek-v4-pro", "deepseek-v4-flash", "qwen3.8-max-preview", "qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus", "qwen3.6-flash"],
|
|
825
|
+
noVisionModels: ["deepseek-v4-pro", "deepseek-v4-flash", "deepseek-v3.2", "glm-5.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
|
|
826
|
+
noReasoningModels: ["kimi-k2.7-code", "kimi-k2.6", "kimi-k2.5", "deepseek-v3.2", "glm-5.1", "glm-5", "MiniMax-M2.5"],
|
|
827
|
+
modelDefaultReasoningEfforts: { "qwen3.8-max-preview": "xhigh" },
|
|
804
828
|
},
|
|
805
829
|
// NEEDS_HUMAN 2026-07-10: kept for config compatibility, but this is a dashboard URL,
|
|
806
830
|
// no /models endpoint is documented, and tools are silently ignored upstream per docs.parallel.ai.
|
package/src/router.ts
CHANGED
|
@@ -182,6 +182,7 @@ function routedProviderConfig(providerName: string, provider: OcxProviderConfig)
|
|
|
182
182
|
// Scalar backfill: a persisted config created before the flag shipped inherits the registry
|
|
183
183
|
// opt-in, while an explicit user `false` keeps overriding registry `true`.
|
|
184
184
|
...(provider.parallelToolCalls === undefined && registryEntry.parallelToolCalls !== undefined ? { parallelToolCalls: registryEntry.parallelToolCalls } : {}),
|
|
185
|
+
...(provider.promptCacheKey === undefined && registryEntry.promptCacheKey !== undefined ? { promptCacheKey: registryEntry.promptCacheKey } : {}),
|
|
185
186
|
...(modelContextWindows ? { modelContextWindows } : {}),
|
|
186
187
|
...(modelInputModalities ? { modelInputModalities } : {}),
|
|
187
188
|
...(modelMaxInputTokens ? { modelMaxInputTokens } : {}),
|
|
@@ -208,7 +209,7 @@ function activeProviderEntries(config: OcxConfig): [string, OcxProviderConfig][]
|
|
|
208
209
|
|
|
209
210
|
export class NoEnabledOpenAiProviderError extends Error {
|
|
210
211
|
constructor(modelId: string) {
|
|
211
|
-
super(`No enabled
|
|
212
|
+
super(`No enabled OpenAI provider for model: ${modelId}. Run 'ocx init' to configure a provider, or check that your config has an enabled 'openai' provider.`);
|
|
212
213
|
this.name = "NoEnabledOpenAiProviderError";
|
|
213
214
|
}
|
|
214
215
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
|
10
10
|
import { getProviderRegistryEntry, providerCodexAccountMode } from "../providers/registry";
|
|
11
11
|
import { providerConfigSeed } from "../providers/derive";
|
|
12
12
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
13
|
+
import { openRouterRoutingConfigError } from "../providers/openrouter-routing";
|
|
13
14
|
|
|
14
15
|
let _corsOrigin = "http://localhost:10100";
|
|
15
16
|
export function setCorsOrigin(port: number): void { _corsOrigin = `http://localhost:${port}`; }
|
|
@@ -226,6 +227,8 @@ export function providerManagementConfigError(name: unknown, provider: unknown):
|
|
|
226
227
|
if (headersError) return `provider ${name} ${headersError}`;
|
|
227
228
|
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
228
229
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
230
|
+
const openRouterError = openRouterRoutingConfigError(typed);
|
|
231
|
+
if (openRouterError) return `provider ${name} ${openRouterError}`;
|
|
229
232
|
if (typed.authMode === "local") {
|
|
230
233
|
// "local" bypasses key-requirement enforcement (api-keys/key-failover treat non-oauth/
|
|
231
234
|
// forward as key auth; openai-chat skips credential checks for local). Only providers
|
|
@@ -290,6 +293,8 @@ export function safeConfigDTO(config: OcxConfig): unknown {
|
|
|
290
293
|
"models",
|
|
291
294
|
"contextWindow",
|
|
292
295
|
"modelContextWindows",
|
|
296
|
+
"openRouterRouting",
|
|
297
|
+
"modelOpenRouterRouting",
|
|
293
298
|
"reasoningEfforts",
|
|
294
299
|
"modelReasoningEfforts",
|
|
295
300
|
"noVisionModels",
|
package/src/server/index.ts
CHANGED
|
@@ -537,7 +537,7 @@ export function startServer(port?: number) {
|
|
|
537
537
|
void (async () => {
|
|
538
538
|
const start = Date.now();
|
|
539
539
|
const requestId = nextRequestLogId(start);
|
|
540
|
-
const logCtx = { model: "unknown", provider: "unknown" };
|
|
540
|
+
const logCtx: RequestLogContext = { model: "unknown", provider: "unknown" };
|
|
541
541
|
let logged = false;
|
|
542
542
|
const finalizeLog = (
|
|
543
543
|
status: number,
|
|
@@ -556,7 +556,7 @@ export function startServer(port?: number) {
|
|
|
556
556
|
body: JSON.stringify({ ...payload, stream: true }),
|
|
557
557
|
});
|
|
558
558
|
try {
|
|
559
|
-
let terminalRecorder: ((status: ResponsesTerminalStatus) => void) | undefined;
|
|
559
|
+
let terminalRecorder: ((status: ResponsesTerminalStatus, httpStatusOverride?: number) => void) | undefined;
|
|
560
560
|
const response = await handleResponses(req, config, logCtx, {
|
|
561
561
|
forceEmptyResponseId: true,
|
|
562
562
|
abortSignal: turnAbort.signal,
|
|
@@ -570,7 +570,7 @@ export function startServer(port?: number) {
|
|
|
570
570
|
await sendResponseToWebSocket(ws, response, isCurrent, {
|
|
571
571
|
onSsePayload: payload => inspectResponseLogSsePayload(logCtx, payload),
|
|
572
572
|
onTerminal: status => {
|
|
573
|
-
terminalRecorder?.(status);
|
|
573
|
+
terminalRecorder?.(status, logCtx.terminalHttpStatus);
|
|
574
574
|
finalizeLog(httpStatusForRequestLogTerminal(status, logCtx), {
|
|
575
575
|
terminalStatus: status,
|
|
576
576
|
closeReason: "terminal",
|