@bitkyc08/opencodex 2.7.40 → 2.7.41
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 +63 -12
- package/assets/claude-code-models.gif +0 -0
- package/gui/dist/assets/index-B2J4t3te.css +1 -0
- package/gui/dist/assets/index-BmvM6wRb.js +65 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -2
- package/src/adapters/google.ts +48 -9
- package/src/adapters/kiro-events.ts +15 -3
- package/src/adapters/kiro.ts +292 -28
- package/src/adapters/openai-chat.ts +63 -15
- package/src/adapters/openai-responses.ts +18 -0
- package/src/bridge.ts +76 -21
- package/src/chat/outbound.ts +66 -34
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p.ts +121 -21
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/inbound-debug.ts +4 -0
- package/src/claude/model-info.ts +9 -3
- package/src/cli/account-extended.ts +34 -0
- package/src/cli/account.ts +3 -1
- package/src/cli/claude-desktop.ts +152 -0
- package/src/cli/claude.ts +37 -3
- package/src/cli/doctor.ts +187 -6
- package/src/cli/help.ts +13 -1
- package/src/cli/index.ts +151 -57
- package/src/cli/status-oauth.ts +68 -0
- package/src/cli/status.ts +4 -0
- package/src/codex/account-lifecycle.ts +31 -0
- package/src/codex/auth-api.ts +133 -26
- package/src/codex/auth-collision.ts +55 -11
- package/src/codex/auth-context.ts +50 -6
- package/src/codex/catalog/provider-fetch.ts +6 -2
- package/src/codex/home.ts +61 -1
- package/src/codex/inject.ts +28 -66
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +39 -3
- package/src/codex/main-account-cache.ts +25 -0
- package/src/codex/model-cache.ts +20 -1
- package/src/codex/paths.ts +5 -0
- package/src/codex/routing.ts +138 -11
- package/src/codex/subagent-model-fallback.ts +455 -0
- package/src/codex/sync.ts +17 -0
- package/src/combos/failover.ts +10 -2
- package/src/combos/index.ts +1 -0
- package/src/combos/types.ts +9 -0
- package/src/config.ts +117 -0
- package/src/grok/inject.ts +339 -0
- package/src/grok/status.ts +88 -0
- package/src/grok/sync.ts +66 -0
- package/src/lib/destination-policy.ts +13 -0
- package/src/lib/errors.ts +59 -0
- package/src/lib/privacy.ts +9 -0
- package/src/lib/process-control.ts +57 -4
- package/src/oauth/health.ts +375 -0
- package/src/oauth/index.ts +69 -22
- package/src/oauth/kiro.ts +5 -3
- package/src/oauth/log.ts +48 -0
- package/src/oauth/store.ts +55 -6
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +143 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/api-keys.ts +5 -5
- package/src/providers/derive.ts +22 -1
- package/src/providers/free-directory.ts +181 -0
- package/src/providers/key-failover.ts +2 -2
- package/src/providers/kiro-models.ts +3 -2
- package/src/providers/openai-tiers.ts +1 -1
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/registry.ts +49 -2
- package/src/responses/parser.ts +49 -20
- package/src/responses/state.ts +156 -2
- package/src/router.ts +73 -5
- package/src/server/chat-completions.ts +78 -22
- package/src/server/claude-messages.ts +8 -0
- package/src/server/images.ts +2 -1
- package/src/server/index.ts +50 -30
- package/src/server/live.ts +2 -1
- package/src/server/management/agent-settings-routes.ts +337 -18
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/combo-routes.ts +3 -3
- package/src/server/management/config-routes.ts +4 -4
- package/src/server/management/logs-usage-routes.ts +64 -4
- package/src/server/management/model-routes.ts +114 -8
- package/src/server/management/oauth-account-routes.ts +31 -5
- package/src/server/management/provider-routes.ts +42 -11
- package/src/server/management/shared.ts +81 -2
- package/src/server/management/system-routes.ts +6 -1
- package/src/server/management-api.ts +24 -7
- package/src/server/port-reclaim.ts +261 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/collaboration.ts +11 -6
- package/src/server/responses/compact.ts +2 -1
- package/src/server/responses/core.ts +358 -155
- package/src/server/responses/passthrough-error.ts +53 -0
- package/src/server/search.ts +2 -1
- package/src/server/system-env.ts +23 -6
- package/src/server/windows-tcp-drop.ts +174 -0
- package/src/service.ts +43 -3
- package/src/types.ts +74 -6
- package/src/update/index.ts +30 -19
- package/src/update/job.ts +30 -15
- package/src/usage/log.ts +158 -2
- package/src/usage/summary.ts +8 -4
- package/src/web-search/loop.ts +4 -1
- package/gui/dist/assets/index-CMip1DzF.css +0 -1
- package/gui/dist/assets/index-cydcmbzC.js +0 -52
package/src/oauth/index.ts
CHANGED
|
@@ -16,6 +16,27 @@ import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers
|
|
|
16
16
|
import { effectiveGoogleMode } from "../providers/registry";
|
|
17
17
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
18
18
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
19
|
+
import { logOAuthEvent } from "./log";
|
|
20
|
+
export {
|
|
21
|
+
CODEX_HEALTH_UNAVAILABLE_NOTE,
|
|
22
|
+
MASKED_ACCOUNT_FALLBACK,
|
|
23
|
+
collectOAuthHealthEntries,
|
|
24
|
+
collectOAuthHealthEntriesForCli,
|
|
25
|
+
detectOAuthWarning,
|
|
26
|
+
oauthAccountHealthFields,
|
|
27
|
+
oauthHealthLabel,
|
|
28
|
+
oauthHealthSummary,
|
|
29
|
+
projectCodexAccountHealth,
|
|
30
|
+
projectOAuthAccountHealth,
|
|
31
|
+
projectStoredOAuthAccountHealth,
|
|
32
|
+
type CodexHealthSource,
|
|
33
|
+
type OAuthAccountHealth,
|
|
34
|
+
type OAuthAccountHealthFields,
|
|
35
|
+
type OAuthCliHealthReport,
|
|
36
|
+
type OAuthHealthEntry,
|
|
37
|
+
type OAuthHealthLabel,
|
|
38
|
+
} from "./health";
|
|
39
|
+
export { OAUTH_REFRESH_LOCK_WAIT_MS, peekAuthStore, peekOAuthRefreshIntent } from "./store";
|
|
19
40
|
|
|
20
41
|
const REFRESH_SKEW_MS = 60_000;
|
|
21
42
|
export interface OAuthAccessSnapshot {
|
|
@@ -30,6 +51,7 @@ const XAI_PERMANENT_FAILURE_TTL_MS=30_000;
|
|
|
30
51
|
const permanentRefreshFailures=new Map<string,number>();
|
|
31
52
|
interface XaiRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void> }
|
|
32
53
|
interface AnthropicRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; now?:()=>number; afterPrePersistRead?:()=>void|Promise<void> }
|
|
54
|
+
interface GenericRefreshDeps { intentLock?:ReturnType<typeof createOAuthRefreshIntentLock>; afterPrePersistRead?:()=>void|Promise<void> }
|
|
33
55
|
function verdictKey(p:string,a:string,c:OAuthCredentials){return `${p}\0${a}\0${credentialGeneration(c)}`;}
|
|
34
56
|
function cached(p:string,a:string,c:OAuthCredentials,now:()=>number){const k=verdictKey(p,a,c),u=permanentRefreshFailures.get(k);if(u===undefined)return false;if(u<=now()){permanentRefreshFailures.delete(k);return false;}return true;}
|
|
35
57
|
|
|
@@ -195,7 +217,10 @@ async function resolveAccessSnapshotForAccount(
|
|
|
195
217
|
|
|
196
218
|
const key = `${provider}\u0000${accountId}`;
|
|
197
219
|
const existing = tokenRefreshes.get(key);
|
|
198
|
-
if (existing)
|
|
220
|
+
if (existing) {
|
|
221
|
+
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
|
|
222
|
+
return existing;
|
|
223
|
+
}
|
|
199
224
|
|
|
200
225
|
const refresh = (async (): Promise<OAuthAccessSnapshot> => {
|
|
201
226
|
const accessToken = await refreshAndPersistAccessToken(provider, accountId, def, cred);
|
|
@@ -349,6 +374,48 @@ export async function refreshAnthropicAccountWithLock(
|
|
|
349
374
|
}
|
|
350
375
|
}
|
|
351
376
|
|
|
377
|
+
export async function refreshGenericAccountWithLock(
|
|
378
|
+
provider: string,
|
|
379
|
+
accountId: string,
|
|
380
|
+
def: OAuthProviderDef,
|
|
381
|
+
callerCredential: OAuthCredentials,
|
|
382
|
+
deps: GenericRefreshDeps = {},
|
|
383
|
+
): Promise<string> {
|
|
384
|
+
logOAuthEvent("OAuth refresh started", { provider, accountId });
|
|
385
|
+
const guard = await (deps.intentLock ?? createOAuthRefreshIntentLock(provider, accountId)).acquire();
|
|
386
|
+
try {
|
|
387
|
+
const stored = getAccountCredential(provider, accountId);
|
|
388
|
+
if (!stored) throw new OAuthLoginRequiredError(provider);
|
|
389
|
+
if (
|
|
390
|
+
credentialGeneration(stored) !== credentialGeneration(callerCredential)
|
|
391
|
+
&& stored.expires > Date.now() + REFRESH_SKEW_MS
|
|
392
|
+
) {
|
|
393
|
+
logOAuthEvent("OAuth refresh joined existing operation", { provider, accountId });
|
|
394
|
+
return stored.access;
|
|
395
|
+
}
|
|
396
|
+
const generation = credentialGeneration(stored);
|
|
397
|
+
try {
|
|
398
|
+
const fresh = merged(await def.refresh(stored.refresh), stored);
|
|
399
|
+
const outcome = await mergeAccountCredential(provider, accountId, fresh, {
|
|
400
|
+
expectedGeneration: generation,
|
|
401
|
+
afterPrePersistRead: deps.afterPrePersistRead,
|
|
402
|
+
});
|
|
403
|
+
if (outcome.superseded) {
|
|
404
|
+
if (outcome.stored.expires > Date.now() + REFRESH_SKEW_MS) return outcome.stored.access;
|
|
405
|
+
throw new OAuthLoginRequiredError(provider);
|
|
406
|
+
}
|
|
407
|
+
logOAuthEvent("OAuth credentials rotated and persisted", { provider, accountId });
|
|
408
|
+
return fresh.access;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (!isTerminalRefreshError(error)) throw error;
|
|
411
|
+
await markAccountNeedsReauthIfGeneration(provider, accountId, generation);
|
|
412
|
+
throw new OAuthLoginRequiredError(provider);
|
|
413
|
+
}
|
|
414
|
+
} finally {
|
|
415
|
+
guard.release();
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
352
419
|
async function refreshAndPersistAccessToken(
|
|
353
420
|
provider: string,
|
|
354
421
|
accountId: string,
|
|
@@ -368,23 +435,7 @@ async function refreshAndPersistAccessToken(
|
|
|
368
435
|
if (provider === "xai") return refreshXaiAccountWithLock(provider, accountId, def, cred);
|
|
369
436
|
if (provider === "anthropic") return refreshAnthropicAccountWithLock(provider, accountId, def, cred);
|
|
370
437
|
try {
|
|
371
|
-
|
|
372
|
-
const detachedLocalCli = provider === "xai" && cred.source === "local-cli";
|
|
373
|
-
if (detachedLocalCli) console.warn(XAI_LOCAL_CLI_DETACH_WARNING);
|
|
374
|
-
// Persist to THIS account (rotation-safe: new refresh token hits disk before use) without
|
|
375
|
-
// touching activeAccountId.
|
|
376
|
-
await saveAccountCredential(provider, accountId, {
|
|
377
|
-
...fresh,
|
|
378
|
-
source: detachedLocalCli ? "oauth" : fresh.source ?? cred.source ?? "oauth",
|
|
379
|
-
// Preserve a previously-discovered project id when a refresh-time re-discovery comes back empty
|
|
380
|
-
// (e.g. a transient network blip), so Antigravity does not lose its CCA project across refresh.
|
|
381
|
-
...(fresh.projectId === undefined && cred.projectId ? { projectId: cred.projectId } : {}),
|
|
382
|
-
...(fresh.apiBaseUrl === undefined && cred.apiBaseUrl ? { apiBaseUrl: cred.apiBaseUrl } : {}),
|
|
383
|
-
// Preserve identity fields the refresh response may omit, so identity matching stays stable.
|
|
384
|
-
...(fresh.email === undefined && cred.email ? { email: cred.email } : {}),
|
|
385
|
-
...(fresh.accountId === undefined && cred.accountId ? { accountId: cred.accountId } : {}),
|
|
386
|
-
});
|
|
387
|
-
return fresh.access;
|
|
438
|
+
return await refreshGenericAccountWithLock(provider, accountId, def, cred);
|
|
388
439
|
} catch (err) {
|
|
389
440
|
if (provider === "kiro" && isActive) {
|
|
390
441
|
const imported = readFreshKiroCliCredential();
|
|
@@ -393,10 +444,6 @@ async function refreshAndPersistAccessToken(
|
|
|
393
444
|
return imported.access;
|
|
394
445
|
}
|
|
395
446
|
}
|
|
396
|
-
if (isTerminalRefreshError(err)) {
|
|
397
|
-
await markAccountNeedsReauth(provider, accountId, true);
|
|
398
|
-
throw new OAuthLoginRequiredError(provider);
|
|
399
|
-
}
|
|
400
447
|
throw err;
|
|
401
448
|
}
|
|
402
449
|
}
|
package/src/oauth/kiro.ts
CHANGED
|
@@ -80,15 +80,17 @@ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials
|
|
|
80
80
|
url: "",
|
|
81
81
|
instructions:
|
|
82
82
|
"No kiro-cli token found. Paste a Kiro access token below (starts with 'aoa'). " +
|
|
83
|
-
"
|
|
83
|
+
"Otherwise install the Kiro CLI (`curl -fsSL https://cli.kiro.dev/install | bash`), " +
|
|
84
|
+
"run `kiro-cli login`, and retry — or set KIRO_ACCESS_TOKEN.",
|
|
84
85
|
});
|
|
85
|
-
ctrl.onProgress?.("No kiro-cli token found. Paste a Kiro access token (starts with 'aoa').");
|
|
86
|
+
ctrl.onProgress?.("No kiro-cli token found. Paste a Kiro access token (starts with 'aoa'), or install the Kiro CLI and run `kiro-cli login` first.");
|
|
86
87
|
const raw = (await ctrl.onManualCodeInput()).trim();
|
|
87
88
|
if (raw) return { access: raw, refresh: "", expires: Date.now() + 3600_000, source: "manual" };
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
throw new Error(
|
|
91
|
-
"Kiro: no token found.
|
|
92
|
+
"Kiro: no token found. Install the Kiro CLI (`curl -fsSL https://cli.kiro.dev/install | bash`) " +
|
|
93
|
+
"and run `kiro-cli login` to import its session, or set KIRO_ACCESS_TOKEN. " +
|
|
92
94
|
"Browser login is not supported for Kiro.",
|
|
93
95
|
);
|
|
94
96
|
}
|
package/src/oauth/log.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// src/oauth/log.ts
|
|
2
|
+
import { maskAccountId } from "../lib/privacy";
|
|
3
|
+
|
|
4
|
+
/** Normalize camelCase / snake_case / kebab-case field names before secret checks. */
|
|
5
|
+
function normalizeFieldKey(key: string): string {
|
|
6
|
+
return key
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
8
|
+
.replace(/-/g, "_")
|
|
9
|
+
.toLowerCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const FORBIDDEN_NORMALIZED = new Set([
|
|
13
|
+
"access",
|
|
14
|
+
"refresh",
|
|
15
|
+
"authorization",
|
|
16
|
+
"code",
|
|
17
|
+
"token",
|
|
18
|
+
"access_token",
|
|
19
|
+
"refresh_token",
|
|
20
|
+
"id_token",
|
|
21
|
+
"client_secret",
|
|
22
|
+
"oauth_code",
|
|
23
|
+
"clientsecret",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function isForbiddenFieldKey(key: string): boolean {
|
|
27
|
+
const normalized = normalizeFieldKey(key);
|
|
28
|
+
if (FORBIDDEN_NORMALIZED.has(normalized)) return true;
|
|
29
|
+
if (normalized.endsWith("_token") || normalized.endsWith("_secret") || normalized.endsWith("_code")) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function logOAuthEvent(
|
|
36
|
+
event: string,
|
|
37
|
+
fields: { provider: string; accountId?: string; [key: string]: unknown },
|
|
38
|
+
): void {
|
|
39
|
+
const parts = [`[opencodex] ${event}`, `provider=${fields.provider}`];
|
|
40
|
+
if (fields.accountId) parts.push(`account=${maskAccountId(fields.accountId)}`);
|
|
41
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
42
|
+
if (key === "provider" || key === "accountId") continue;
|
|
43
|
+
if (isForbiddenFieldKey(key)) continue;
|
|
44
|
+
if (value === undefined) continue;
|
|
45
|
+
parts.push(`${key}=${String(value)}`);
|
|
46
|
+
}
|
|
47
|
+
console.info(parts.join(" "));
|
|
48
|
+
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -40,16 +40,41 @@ export function getAuthRefreshIntentPath(provider: string, accountId: string): s
|
|
|
40
40
|
return `${getAuthRefreshIntentLockPath(provider, accountId)}.json`;
|
|
41
41
|
}
|
|
42
42
|
export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; uncertain?: true }
|
|
43
|
+
function parseOAuthRefreshIntent(
|
|
44
|
+
provider: string,
|
|
45
|
+
accountId: string,
|
|
46
|
+
raw: string,
|
|
47
|
+
): OAuthRefreshIntent {
|
|
48
|
+
const value = JSON.parse(raw) as Partial<OAuthRefreshIntent>;
|
|
49
|
+
if (
|
|
50
|
+
value.version !== 1
|
|
51
|
+
|| value.provider !== provider
|
|
52
|
+
|| value.accountId !== accountId
|
|
53
|
+
|| typeof value.generation !== "string"
|
|
54
|
+
|| typeof value.createdAt !== "number"
|
|
55
|
+
) {
|
|
56
|
+
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
57
|
+
}
|
|
58
|
+
return value as OAuthRefreshIntent;
|
|
59
|
+
}
|
|
60
|
+
|
|
43
61
|
export function readOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent | undefined {
|
|
44
62
|
const path = getAuthRefreshIntentPath(provider, accountId);
|
|
45
63
|
try {
|
|
46
64
|
hardenConfigDir();
|
|
47
65
|
hardenExistingSecret(path);
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
66
|
+
return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
69
|
+
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Observe-only intent read for diagnostics: no chmod/ACL hardening. */
|
|
74
|
+
export function peekOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent | undefined {
|
|
75
|
+
const path = getAuthRefreshIntentPath(provider, accountId);
|
|
76
|
+
try {
|
|
77
|
+
return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
|
|
53
78
|
} catch (error) {
|
|
54
79
|
if (errorCode(error) === "ENOENT") return undefined;
|
|
55
80
|
return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
|
|
@@ -89,6 +114,20 @@ export function loadAuthStore(): AuthStore {
|
|
|
89
114
|
return loadAuthStoreInternal().store;
|
|
90
115
|
}
|
|
91
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Observe-only auth store read for diagnostics (`ocx doctor` / status).
|
|
119
|
+
* Does not chmod paths or backup invalid JSON — corrupt files are treated as empty.
|
|
120
|
+
*/
|
|
121
|
+
export function peekAuthStore(): AuthStore {
|
|
122
|
+
const path = getAuthStorePath();
|
|
123
|
+
if (!existsSync(path)) return {};
|
|
124
|
+
try {
|
|
125
|
+
return normalizeAuthStore(JSON.parse(readFileSync(path, "utf-8"))).store;
|
|
126
|
+
} catch {
|
|
127
|
+
return {};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
92
131
|
function persist(store: AuthStore): void {
|
|
93
132
|
const dir = getConfigDir();
|
|
94
133
|
if (!existsSync(dir)) {
|
|
@@ -114,7 +153,17 @@ export function createOAuthFileLock(options: OAuthFileLockOptions): { acquire():
|
|
|
114
153
|
return { async acquire() { hardenConfigDir(); if(!existsSync(getConfigDir())) mkdirSync(getConfigDir(),{recursive:true,mode:0o700}); const ownerId=randomUUID(),started=now(); for(;;){ let fd:number|undefined; try { fd=openSync(options.path,"wx",0o600); const bytes=`${JSON.stringify({version:1,ownerId,pid:process.pid,createdAt:now()})}\n`; write(fd,bytes); const fs=fstatSync(fd); closeSync(fd); fd=undefined; const owned=snapshot(options.path); if(owned.bytes!==bytes||!sameFd(owned,fs)) throw new OAuthFileLockError("OAuth lock changed during creation"); let released=false; return {ownerId,release(){if(released)return;released=true;try{const a=snapshot(options.path);if(!sameSnapshot(owned,a))return;options.beforeReleaseUnlink?.();const b=snapshot(options.path);if(sameSnapshot(owned,b))unlinkSync(options.path);}catch(e){if(errorCode(e)!=="ENOENT")console.warn(`[oauth] lock release failed: ${e instanceof Error?e.message:String(e)}`);}}}; } catch(e) { if(fd!==undefined){let fs;try{fs=fstatSync(fd);}catch{}try{closeSync(fd);}catch{}if(fs)try{const a=snapshot(options.path);if(sameFd(a,fs)){options.beforeFailedCreateUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b)&&sameFd(b,fs))unlinkSync(options.path);}}catch{}} if(errorCode(e)!=="EEXIST")throw e instanceof OAuthFileLockError?e:new OAuthFileLockError("Could not create OAuth file lock",{cause:e}); }
|
|
115
154
|
try{const a=snapshot(options.path);let created=a.mtimeMs;try{const p=JSON.parse(a.bytes);if(typeof p.createdAt==="number")created=Math.max(created,p.createdAt);}catch{}if(now()-created>stale){options.beforeStaleUnlink?.();const b=snapshot(options.path);if(sameSnapshot(a,b))unlinkSync(options.path);continue;}}catch(e){if(errorCode(e)==="ENOENT")continue;throw new OAuthFileLockError("Could not inspect OAuth file lock",{cause:e});} const elapsed=now()-started;if(elapsed>=wait)throw new OAuthFileLockError(`Timed out after ${wait}ms waiting for OAuth file lock`);await sleep(Math.min(wait-elapsed,min+Math.floor(random()*(max-min+1)))); } } };
|
|
116
155
|
}
|
|
117
|
-
|
|
156
|
+
/** Wait long enough for slow IdP refreshes (e.g. Cursor 15s × 3 attempts) before timing out. */
|
|
157
|
+
export const OAUTH_REFRESH_LOCK_WAIT_MS = 60_000;
|
|
158
|
+
|
|
159
|
+
export function createOAuthRefreshIntentLock(provider:string,accountId:string,overrides:Partial<OAuthFileLockOptions>={}) {
|
|
160
|
+
return createOAuthFileLock({
|
|
161
|
+
path: getAuthRefreshIntentLockPath(provider, accountId),
|
|
162
|
+
staleAfterMs: 120000,
|
|
163
|
+
waitTimeoutMs: OAUTH_REFRESH_LOCK_WAIT_MS,
|
|
164
|
+
...overrides,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
118
167
|
|
|
119
168
|
/**
|
|
120
169
|
* One-time downgrade safety net: the first time we persist the NEW shape over a file that
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, linkSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { getConfigPath } from "../config";
|
|
3
|
+
|
|
4
|
+
export interface AlibabaBackupIO {
|
|
5
|
+
exists: (path: string) => boolean;
|
|
6
|
+
read: (path: string) => Buffer;
|
|
7
|
+
copy: (source: string, destination: string) => void;
|
|
8
|
+
/** Publish with no-replace semantics: fails with EEXIST if the destination exists. */
|
|
9
|
+
publishNoReplace: (temp: string, destination: string) => void;
|
|
10
|
+
remove: (path: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_IO: AlibabaBackupIO = {
|
|
14
|
+
exists: existsSync,
|
|
15
|
+
read: path => readFileSync(path),
|
|
16
|
+
copy: (source, destination) => copyFileSync(source, destination),
|
|
17
|
+
publishNoReplace: linkSync,
|
|
18
|
+
remove: path => rmSync(path, { force: true }),
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class AlibabaBackupIntegrityError extends Error {}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Immutable pre-migration snapshot of `config.json`.
|
|
25
|
+
*
|
|
26
|
+
* Written to a temp file first and published by `link`, which fails with EEXIST
|
|
27
|
+
* rather than replacing. Exclusive *creation* alone is not enough: a crash
|
|
28
|
+
* mid-copy can leave a truncated destination that the next run would accept as a
|
|
29
|
+
* valid rollback point. Copy-verify-link means a published snapshot is always a
|
|
30
|
+
* complete one.
|
|
31
|
+
*
|
|
32
|
+
* An existing snapshot is NOT required to equal the current config. Demanding
|
|
33
|
+
* that has a false positive that would brick a working install: a run that
|
|
34
|
+
* created the snapshot and then aborted for an unrelated reason leaves a
|
|
35
|
+
* perfectly valid snapshot, the user then edits `config.json` legitimately, and
|
|
36
|
+
* every subsequent start would throw out of `startServer` and refuse to boot. A
|
|
37
|
+
* safety net must never become the thing that stops the product.
|
|
38
|
+
*
|
|
39
|
+
* So an existing snapshot is reused as-is — it is the earliest one, which is
|
|
40
|
+
* exactly what a rollback point should be. Integrity is carried by publication.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately not built on `backupConfigBeforeOpenAiTierMigration`: that
|
|
43
|
+
* function's stale/rollback classification exists for a repeatable migration and
|
|
44
|
+
* would be wrong here.
|
|
45
|
+
*/
|
|
46
|
+
export function backupConfigBeforeAlibabaRegionMigration(
|
|
47
|
+
configPath = getConfigPath(),
|
|
48
|
+
io: AlibabaBackupIO = DEFAULT_IO,
|
|
49
|
+
): "absent" | "created" | "reused" {
|
|
50
|
+
if (!io.exists(configPath)) return "absent";
|
|
51
|
+
const backup = `${configPath}.pre-alibaba-region-v1.bak`;
|
|
52
|
+
// The earliest snapshot is the most valuable one: it predates every migration
|
|
53
|
+
// this config has been through.
|
|
54
|
+
if (io.exists(backup)) return "reused";
|
|
55
|
+
|
|
56
|
+
const source = io.read(configPath);
|
|
57
|
+
const temp = `${backup}.${process.pid}.tmp`;
|
|
58
|
+
try {
|
|
59
|
+
io.copy(configPath, temp);
|
|
60
|
+
// Verify before publishing: a short copy must never become the snapshot.
|
|
61
|
+
if (!io.read(temp).equals(source)) {
|
|
62
|
+
throw new AlibabaBackupIntegrityError(`failed to write a complete backup to ${temp}`);
|
|
63
|
+
}
|
|
64
|
+
io.publishNoReplace(temp, backup);
|
|
65
|
+
return "created";
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Another process published between our `exists` check and the link. It went
|
|
68
|
+
// through the same copy-verify-link sequence, so it is complete by
|
|
69
|
+
// construction — nothing else writes this filename.
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") return "reused";
|
|
71
|
+
throw error;
|
|
72
|
+
} finally {
|
|
73
|
+
io.remove(temp);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { ALIBABA_INTL_BASE_URL_CHOICES } from "./base-url-choices";
|
|
2
|
+
import { providerConfigSeed } from "./derive";
|
|
3
|
+
import { rewriteProviderReferences } from "./provider-id-rewrite";
|
|
4
|
+
import { PROVIDER_REGISTRY } from "./registry";
|
|
5
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
6
|
+
|
|
7
|
+
const BEIJING_ID = "alibaba-token-plan";
|
|
8
|
+
const INTL_ID = "alibaba-token-plan-intl";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Credentials and switches the user owns directly. Everything else on the moved
|
|
12
|
+
* row is registry-derived Beijing metadata (`ocx provider add` and the GUI persist
|
|
13
|
+
* it, and registry enrichment only fills absent fields) and must NOT travel to the
|
|
14
|
+
* international id — carrying it would leave the intl provider serving Singapore
|
|
15
|
+
* while advertising the six-model Beijing Personal Edition catalog.
|
|
16
|
+
*
|
|
17
|
+
* `baseUrl` is carried deliberately: the migration only fires for a URL that
|
|
18
|
+
* `ALIBABA_INTL_BASE_URL_CHOICES` recognises, so it is nonblank and
|
|
19
|
+
* placeholder-free and satisfies the override guard in the router.
|
|
20
|
+
* `liveModels` is carried because it is directly user-editable, so resetting it
|
|
21
|
+
* to the seed's `false` would silently undo a deliberate choice.
|
|
22
|
+
* `authMode` is NOT carried — both entries are key-auth and the seed's value is
|
|
23
|
+
* the one matching the destination's registry contract.
|
|
24
|
+
* `defaultModel` and `note` are user-editable too and are handled below.
|
|
25
|
+
*/
|
|
26
|
+
const USER_OWNED_FIELDS = ["apiKey", "apiKeyPool", "disabled", "baseUrl", "allowPrivateNetwork", "liveModels"] as const;
|
|
27
|
+
|
|
28
|
+
export interface AlibabaRegionMigrationProjection {
|
|
29
|
+
config: OcxConfig;
|
|
30
|
+
changed: boolean;
|
|
31
|
+
warnings: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const normalize = (url: string): string => url.trim().replace(/\/+$/, "").toLowerCase();
|
|
35
|
+
|
|
36
|
+
/** True when the saved URL is one the international entry actually serves. */
|
|
37
|
+
function isInternationalEndpoint(baseUrl: string): boolean {
|
|
38
|
+
return ALIBABA_INTL_BASE_URL_CHOICES.some(
|
|
39
|
+
choice => choice.baseUrl && normalize(choice.baseUrl) === normalize(baseUrl),
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Seed the destination from the international registry entry, then overlay only
|
|
45
|
+
* what the user owns. `providerConfigSeed` is the same function `ocx provider add`
|
|
46
|
+
* uses, so the migrated row is indistinguishable from one the user had created
|
|
47
|
+
* against the international provider directly.
|
|
48
|
+
*/
|
|
49
|
+
function buildIntlRow(source: OcxProviderConfig): OcxProviderConfig {
|
|
50
|
+
const entry = PROVIDER_REGISTRY.find(e => e.id === INTL_ID);
|
|
51
|
+
// Fail fast rather than fabricate a row: a missing registry entry means the
|
|
52
|
+
// destination this migration targets no longer exists.
|
|
53
|
+
if (!entry) throw new Error(`registry entry "${INTL_ID}" not found; cannot migrate "${BEIJING_ID}"`);
|
|
54
|
+
const seeded = providerConfigSeed(entry);
|
|
55
|
+
const writable = seeded as unknown as Record<string, unknown>;
|
|
56
|
+
const readable = source as unknown as Record<string, unknown>;
|
|
57
|
+
for (const field of USER_OWNED_FIELDS) {
|
|
58
|
+
if (readable[field] !== undefined) writable[field] = readable[field];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const servable = new Set(entry.models ?? []);
|
|
62
|
+
// A user-chosen default survives only when the destination can actually serve
|
|
63
|
+
// it; otherwise the international default already on the seed stands.
|
|
64
|
+
if (source.defaultModel && servable.has(source.defaultModel)) seeded.defaultModel = source.defaultModel;
|
|
65
|
+
// Same rule for the allowlist, handled here where the destination catalog is
|
|
66
|
+
// known — never by the generic reference rewriter.
|
|
67
|
+
const selected = source.selectedModels?.filter(id => servable.has(id));
|
|
68
|
+
if (selected && selected.length > 0) seeded.selectedModels = selected;
|
|
69
|
+
else delete seeded.selectedModels;
|
|
70
|
+
|
|
71
|
+
// `providerConfigSeed` does not carry `note`, so seed the destination's registry
|
|
72
|
+
// note explicitly, then let a genuinely user-authored note win. Comparing
|
|
73
|
+
// against the SOURCE registry note is what distinguishes "the user typed this"
|
|
74
|
+
// from "the Beijing entry's stock note came along for the ride".
|
|
75
|
+
const beijingNote = PROVIDER_REGISTRY.find(e => e.id === BEIJING_ID)?.note;
|
|
76
|
+
if (entry.note) seeded.note = entry.note;
|
|
77
|
+
if (source.note && source.note !== beijingNote) seeded.note = source.note;
|
|
78
|
+
|
|
79
|
+
return seeded;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Repair a config whose `alibaba-token-plan` entry points at an international
|
|
84
|
+
* endpoint (issue #457). The Beijing entry is pinned to cn-beijing and does not
|
|
85
|
+
* take a baseUrl override by design, so such a config routes an international key
|
|
86
|
+
* to Beijing and fails with 401 on every request.
|
|
87
|
+
*
|
|
88
|
+
* The entry is MOVED to `alibaba-token-plan-intl` rather than the Beijing entry
|
|
89
|
+
* being taught to serve Singapore: the two ids carry different model lists,
|
|
90
|
+
* context windows, modality maps and dashboards, and a URL override cannot carry
|
|
91
|
+
* those contracts across. That override fix shipped once, was backed out twice,
|
|
92
|
+
* and was refused again when PR #459 closed.
|
|
93
|
+
*
|
|
94
|
+
* Refuses to act when the destination already holds a value — a provider row or a
|
|
95
|
+
* key the reference rewrite would land on — because merging two settings is a
|
|
96
|
+
* user decision, not a migration's.
|
|
97
|
+
*/
|
|
98
|
+
export function projectAlibabaRegionMigration(config: OcxConfig): AlibabaRegionMigrationProjection {
|
|
99
|
+
const beijing = config.providers[BEIJING_ID];
|
|
100
|
+
const savedBaseUrl = typeof beijing?.baseUrl === "string" ? beijing.baseUrl : "";
|
|
101
|
+
if (!beijing || !savedBaseUrl || !isInternationalEndpoint(savedBaseUrl)) {
|
|
102
|
+
return { config, changed: false, warnings: [] };
|
|
103
|
+
}
|
|
104
|
+
if (config.providers[INTL_ID]) {
|
|
105
|
+
return {
|
|
106
|
+
config,
|
|
107
|
+
changed: false,
|
|
108
|
+
warnings: [
|
|
109
|
+
`provider "${BEIJING_ID}" is configured with an international endpoint, but "${INTL_ID}" `
|
|
110
|
+
+ `already exists. Both were left untouched: merging two credential sets is not a decision `
|
|
111
|
+
+ `this migration can make. Move the key to "${INTL_ID}" and delete the unused entry.`,
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const projected = structuredClone(config);
|
|
117
|
+
delete projected.providers[BEIJING_ID];
|
|
118
|
+
projected.providers[INTL_ID] = buildIntlRow(beijing);
|
|
119
|
+
const { changed: rewritten, collisions } = rewriteProviderReferences(projected, BEIJING_ID, INTL_ID);
|
|
120
|
+
// The rewriter is not transactional, so the clone is already partly rewritten
|
|
121
|
+
// here. Returning the ORIGINAL config discards it.
|
|
122
|
+
if (collisions.length > 0) {
|
|
123
|
+
return {
|
|
124
|
+
config,
|
|
125
|
+
changed: false,
|
|
126
|
+
warnings: [
|
|
127
|
+
`provider "${BEIJING_ID}" needs to move to "${INTL_ID}", but ${collisions.join(", ")} `
|
|
128
|
+
+ `already hold values for the destination. Nothing was changed: choosing which value `
|
|
129
|
+
+ `survives is not a decision this migration can make. Resolve those entries and restart.`,
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
config: projected,
|
|
136
|
+
changed: true,
|
|
137
|
+
warnings: [
|
|
138
|
+
`moved provider "${BEIJING_ID}" to "${INTL_ID}": its saved endpoint is the international one, `
|
|
139
|
+
+ `which the Beijing entry cannot serve. ${rewritten} reference(s) were re-pointed; the `
|
|
140
|
+
+ `international model catalog now applies.`,
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { saveConfig } from "../config";
|
|
2
|
+
import { backupConfigBeforeAlibabaRegionMigration } from "./alibaba-region-backup";
|
|
3
|
+
import { projectAlibabaRegionMigration } from "./alibaba-region-migration";
|
|
4
|
+
import type { OcxConfig } from "../types";
|
|
5
|
+
|
|
6
|
+
export interface AlibabaRegionStartupDeps {
|
|
7
|
+
project: typeof projectAlibabaRegionMigration;
|
|
8
|
+
backup: () => void;
|
|
9
|
+
save: (config: OcxConfig) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run the #457 recovery migration at startup.
|
|
14
|
+
*
|
|
15
|
+
* Fail-closed: a backup failure throws, and nothing between here and
|
|
16
|
+
* `startServer` catches it, so the proxy does not start rather than rewriting
|
|
17
|
+
* credentials without a rollback point. That is the same posture the OpenAI tier
|
|
18
|
+
* migration takes.
|
|
19
|
+
*/
|
|
20
|
+
export function runAlibabaRegionStartupMigration(
|
|
21
|
+
config: OcxConfig,
|
|
22
|
+
deps: AlibabaRegionStartupDeps = {
|
|
23
|
+
project: projectAlibabaRegionMigration,
|
|
24
|
+
backup: () => { backupConfigBeforeAlibabaRegionMigration(); },
|
|
25
|
+
save: saveConfig,
|
|
26
|
+
},
|
|
27
|
+
): OcxConfig {
|
|
28
|
+
const projection = deps.project(config);
|
|
29
|
+
// Warnings are emitted even on a no-op: the collision case IS the warning.
|
|
30
|
+
for (const warning of projection.warnings) console.warn(`[alibaba-region-migration] ${warning}`);
|
|
31
|
+
if (!projection.changed) return projection.config;
|
|
32
|
+
// Strictly before the save: the snapshot must describe the config as it was.
|
|
33
|
+
deps.backup();
|
|
34
|
+
deps.save(projection.config);
|
|
35
|
+
return projection.config;
|
|
36
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* A provider with a legacy bare `apiKey` is seeded into a one-entry pool on first touch.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
-
import {
|
|
10
|
+
import { saveConfigPreservingClaudeCode } from "../config";
|
|
11
11
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
12
12
|
|
|
13
13
|
export interface ProviderApiKeyInfo {
|
|
@@ -87,7 +87,7 @@ export function addProviderApiKey(config: OcxConfig, name: string, key: string,
|
|
|
87
87
|
pool.push({ id, key: trimmed, ...(label?.trim() ? { label: label.trim() } : {}), addedAt: Date.now() });
|
|
88
88
|
}
|
|
89
89
|
provider.apiKey = trimmed;
|
|
90
|
-
|
|
90
|
+
saveConfigPreservingClaudeCode(config);
|
|
91
91
|
return { id };
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -98,7 +98,7 @@ export function setActiveProviderApiKey(config: OcxConfig, name: string, id: str
|
|
|
98
98
|
const entry = ensurePool(provider).find(e => e.id === id);
|
|
99
99
|
if (!entry) return false;
|
|
100
100
|
provider.apiKey = entry.key;
|
|
101
|
-
|
|
101
|
+
saveConfigPreservingClaudeCode(config);
|
|
102
102
|
return true;
|
|
103
103
|
}
|
|
104
104
|
|
|
@@ -110,7 +110,7 @@ export function setProviderApiKeyLabel(config: OcxConfig, name: string, id: stri
|
|
|
110
110
|
if (!entry) return false;
|
|
111
111
|
if (label) entry.label = label;
|
|
112
112
|
else delete entry.label;
|
|
113
|
-
|
|
113
|
+
saveConfigPreservingClaudeCode(config);
|
|
114
114
|
return true;
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -128,6 +128,6 @@ export function removeProviderApiKey(config: OcxConfig, name: string, id: string
|
|
|
128
128
|
else delete provider.apiKey;
|
|
129
129
|
}
|
|
130
130
|
if (provider.apiKeyPool.length === 0) delete provider.apiKeyPool;
|
|
131
|
-
|
|
131
|
+
saveConfigPreservingClaudeCode(config);
|
|
132
132
|
return true;
|
|
133
133
|
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface DerivedKeyLoginProvider {
|
|
|
27
27
|
noPenaltyModels?: string[];
|
|
28
28
|
autoToolChoiceOnlyModels?: string[];
|
|
29
29
|
preserveReasoningContentModels?: string[];
|
|
30
|
+
reasoningSplitModels?: string[];
|
|
30
31
|
thinkingToggleModels?: string[];
|
|
31
32
|
thinkingBudgetModels?: string[];
|
|
32
33
|
escapeBuiltinToolNames?: boolean;
|
|
@@ -77,6 +78,23 @@ function cloneRecordOfArrays(input: Record<string, string[]>): Record<string, st
|
|
|
77
78
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, [...value]]));
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Fill registry defaults BENEATH the user's per-model entries.
|
|
83
|
+
*
|
|
84
|
+
* Capability maps are keyed per model, so an all-or-nothing fill lets a single
|
|
85
|
+
* customized model hide the registry's knowledge about every other model. That
|
|
86
|
+
* is how a partially customized `modelInputModalities` could leave a
|
|
87
|
+
* vision-capable model advertising no image support, which in turn collapses any
|
|
88
|
+
* combo containing it to text-only. Routing already merges these maps per key
|
|
89
|
+
* (`mergeRecordFill` in src/router.ts); catalog enrichment now matches.
|
|
90
|
+
*/
|
|
91
|
+
function fillRecordOfArrays(
|
|
92
|
+
seed: Record<string, string[]>,
|
|
93
|
+
user: Record<string, string[]> | undefined,
|
|
94
|
+
): Record<string, string[]> {
|
|
95
|
+
return { ...cloneRecordOfArrays(seed), ...(user ? cloneRecordOfArrays(user) : {}) };
|
|
96
|
+
}
|
|
97
|
+
|
|
80
98
|
function cloneNestedRecord(input: Record<string, Record<string, string>>): Record<string, Record<string, string>> {
|
|
81
99
|
return Object.fromEntries(Object.entries(input).map(([key, value]) => [key, { ...value }]));
|
|
82
100
|
}
|
|
@@ -113,6 +131,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
113
131
|
...(entry.parallelToolCalls !== undefined ? { parallelToolCalls: entry.parallelToolCalls } : {}),
|
|
114
132
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
115
133
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
134
|
+
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
|
|
116
135
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
117
136
|
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
118
137
|
...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
|
|
@@ -153,6 +172,7 @@ export function deriveKeyLoginMap(): Record<string, DerivedKeyLoginProvider> {
|
|
|
153
172
|
...(entry.noPenaltyModels ? { noPenaltyModels: [...entry.noPenaltyModels] } : {}),
|
|
154
173
|
...(entry.autoToolChoiceOnlyModels ? { autoToolChoiceOnlyModels: [...entry.autoToolChoiceOnlyModels] } : {}),
|
|
155
174
|
...(entry.preserveReasoningContentModels ? { preserveReasoningContentModels: [...entry.preserveReasoningContentModels] } : {}),
|
|
175
|
+
...(entry.reasoningSplitModels ? { reasoningSplitModels: [...entry.reasoningSplitModels] } : {}),
|
|
156
176
|
...(entry.thinkingToggleModels ? { thinkingToggleModels: [...entry.thinkingToggleModels] } : {}),
|
|
157
177
|
...(entry.thinkingBudgetModels ? { thinkingBudgetModels: [...entry.thinkingBudgetModels] } : {}),
|
|
158
178
|
...(entry.escapeBuiltinToolNames !== undefined ? { escapeBuiltinToolNames: entry.escapeBuiltinToolNames } : {}),
|
|
@@ -208,7 +228,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
208
228
|
if (prov.liveModels === undefined && seed.liveModels !== undefined) prov.liveModels = seed.liveModels;
|
|
209
229
|
if (prov.contextWindow === undefined && seed.contextWindow !== undefined) prov.contextWindow = seed.contextWindow;
|
|
210
230
|
if (!prov.modelContextWindows && seed.modelContextWindows) prov.modelContextWindows = { ...seed.modelContextWindows };
|
|
211
|
-
if (
|
|
231
|
+
if (seed.modelInputModalities) prov.modelInputModalities = fillRecordOfArrays(seed.modelInputModalities, prov.modelInputModalities);
|
|
212
232
|
if (prov.defaultMaxOutputTokens === undefined && seed.defaultMaxOutputTokens !== undefined) prov.defaultMaxOutputTokens = seed.defaultMaxOutputTokens;
|
|
213
233
|
if (!prov.modelMaxOutputTokens && seed.modelMaxOutputTokens) prov.modelMaxOutputTokens = { ...seed.modelMaxOutputTokens };
|
|
214
234
|
if (!prov.reasoningEfforts && seed.reasoningEfforts) prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
@@ -224,6 +244,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
224
244
|
if (prov.parallelToolCalls === undefined && seed.parallelToolCalls !== undefined) prov.parallelToolCalls = seed.parallelToolCalls;
|
|
225
245
|
if (!prov.autoToolChoiceOnlyModels && seed.autoToolChoiceOnlyModels) prov.autoToolChoiceOnlyModels = [...seed.autoToolChoiceOnlyModels];
|
|
226
246
|
if (!prov.preserveReasoningContentModels && seed.preserveReasoningContentModels) prov.preserveReasoningContentModels = [...seed.preserveReasoningContentModels];
|
|
247
|
+
if (!prov.reasoningSplitModels && seed.reasoningSplitModels) prov.reasoningSplitModels = [...seed.reasoningSplitModels];
|
|
227
248
|
if (!prov.thinkingToggleModels && seed.thinkingToggleModels) prov.thinkingToggleModels = [...seed.thinkingToggleModels];
|
|
228
249
|
if (!prov.thinkingBudgetModels && seed.thinkingBudgetModels) prov.thinkingBudgetModels = [...seed.thinkingBudgetModels];
|
|
229
250
|
if (prov.escapeBuiltinToolNames === undefined && seed.escapeBuiltinToolNames !== undefined) prov.escapeBuiltinToolNames = seed.escapeBuiltinToolNames;
|