@bitkyc08/opencodex 2.26.0 → 2.28.0
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/gui/dist/assets/{index-RL6b1bTV.js → index-D2sP-biU.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +60 -1
- package/src/adapters/base.ts +16 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google-antigravity-replay.ts +16 -8
- package/src/adapters/google.ts +22 -5
- package/src/adapters/openai-chat.ts +189 -60
- package/src/adapters/openai-responses.ts +37 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +19 -6
- package/src/cli/models.ts +13 -6
- package/src/codex/account-usability.ts +3 -0
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-api.ts +22 -5
- package/src/codex/auth-context.ts +108 -3
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/metadata.ts +17 -3
- package/src/codex/catalog/native-models.ts +22 -14
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +63 -15
- package/src/codex/convergence.ts +61 -13
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/model-entitlements.ts +353 -0
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/quota.ts +28 -3
- package/src/codex/routing.ts +14 -8
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +110 -70
- package/src/images/loop.ts +11 -4
- package/src/lib/destination-policy.ts +47 -0
- package/src/lib/shadow-call.ts +15 -0
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/oauth/index.ts +33 -5
- package/src/oauth/store.ts +11 -5
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/fastwire.ts +39 -8
- package/src/providers/quota.ts +9 -2
- package/src/providers/registry.ts +120 -6
- package/src/providers/service-tier.ts +50 -15
- package/src/responses/parser.ts +59 -11
- package/src/responses/state.ts +162 -5
- package/src/responses/tool-search-compat.ts +301 -0
- package/src/router.ts +17 -3
- package/src/routing/capability.ts +26 -9
- package/src/routing/compatibility/behavior.ts +44 -6
- package/src/routing/profile.ts +1 -1
- package/src/server/chat-native.ts +11 -2
- package/src/server/index.ts +59 -8
- package/src/server/management/agent-settings-routes.ts +16 -2
- package/src/server/management/shared.ts +3 -1
- package/src/server/request-log.ts +31 -0
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/compact.ts +54 -7
- package/src/server/responses/core.ts +259 -43
- package/src/server/responses/input-admission.ts +7 -2
- package/src/server/responses/responses-field-backfill.ts +88 -6
- package/src/server/responses/terminal-guard.ts +10 -0
- package/src/server/responses-tool-search-repair.ts +217 -0
- package/src/server/system-env.ts +74 -5
- package/src/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/usage/log.ts +4 -0
- package/src/web-search/loop.ts +11 -4
|
@@ -130,6 +130,50 @@ function registryAllowsPrivateNetwork(name: string): boolean {
|
|
|
130
130
|
return getProviderRegistryEntry(name)?.allowPrivateNetworkByDefault === true;
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* OAuth registry entries that opt into `allowBaseUrlOverride` send bearer credentials to a
|
|
135
|
+
* user-configured endpoint (review findings, PR #2109 / PR #2110): a cleartext `http:`
|
|
136
|
+
* override would expose the OAuth token on the wire. `https:` is therefore required for
|
|
137
|
+
* every non-local destination. Loopback/localhost/private relays keep working over
|
|
138
|
+
* `http:` because they already sit behind the explicit `allowPrivateNetwork` opt-in
|
|
139
|
+
* enforced by {@link providerDestinationConfigError}. Keyed/local providers (Ollama,
|
|
140
|
+
* vLLM, LM Studio, LiteLLM, Moonshot, Qwen, Alibaba) are untouched: they are not
|
|
141
|
+
* `authKind: "oauth"`, so this check never fires for them.
|
|
142
|
+
*/
|
|
143
|
+
function registrySendsOAuthToOverriddenBaseUrl(name: string): boolean {
|
|
144
|
+
const entry = getProviderRegistryEntry(name);
|
|
145
|
+
return entry?.authKind === "oauth" && entry.allowBaseUrlOverride === true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function providerSecureTransportConfigError(
|
|
149
|
+
name: string,
|
|
150
|
+
provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">,
|
|
151
|
+
): string | null {
|
|
152
|
+
if (!registrySendsOAuthToOverriddenBaseUrl(name)) return null;
|
|
153
|
+
let parsed: URL;
|
|
154
|
+
try {
|
|
155
|
+
parsed = new URL(provider.baseUrl.trim());
|
|
156
|
+
} catch {
|
|
157
|
+
return null; // invalid URLs are providerBaseUrlConfigError's concern
|
|
158
|
+
}
|
|
159
|
+
if (parsed.protocol !== "http:") return null;
|
|
160
|
+
const assessment = assessDestination(provider.baseUrl);
|
|
161
|
+
// Classify FIRST, then consult the opt-in. `allowPrivateNetwork` says "this destination is
|
|
162
|
+
// intentionally local", which is a statement about the address, not a waiver of transport
|
|
163
|
+
// security — reading it before classification let `http://attacker.example` with the opt-in
|
|
164
|
+
// set carry an OAuth bearer in cleartext to a public host.
|
|
165
|
+
if (!assessment) return null;
|
|
166
|
+
const local = assessment.kind === "localhost"
|
|
167
|
+
|| assessment.kind === "loopback"
|
|
168
|
+
|| assessment.kind === "private";
|
|
169
|
+
if (local && providerAllowsPrivateNetwork(name, provider)) {
|
|
170
|
+
// A genuinely local relay over http stays reachable through the explicit opt-in; the
|
|
171
|
+
// private-network gate still governs whether it may be reached at all.
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
return "baseUrl must use https: this provider sends OAuth credentials to its endpoint, and http is allowed only for loopback/private relays";
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
/**
|
|
134
178
|
* Whether a provider may reach loopback/private addresses.
|
|
135
179
|
*
|
|
@@ -150,6 +194,8 @@ export function providerAllowsPrivateNetwork(
|
|
|
150
194
|
}
|
|
151
195
|
|
|
152
196
|
export function providerDestinationConfigError(name: string, provider: Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork">): string | null {
|
|
197
|
+
const secureTransportError = providerSecureTransportConfigError(name, provider);
|
|
198
|
+
if (secureTransportError) return secureTransportError;
|
|
153
199
|
const assessment = assessDestination(provider.baseUrl);
|
|
154
200
|
if (!assessment) return null;
|
|
155
201
|
if (assessment.kind === "public" || assessment.kind === "hostname") return null;
|
|
@@ -331,3 +377,4 @@ export async function resolvePublicAddresses(
|
|
|
331
377
|
export async function assertUrlResolvesPublic(url: string): Promise<void> {
|
|
332
378
|
await resolvePublicAddresses(url);
|
|
333
379
|
}
|
|
380
|
+
|
package/src/lib/shadow-call.ts
CHANGED
|
@@ -29,6 +29,21 @@ export function isShadowSourceModel(modelId: string, configured?: unknown): bool
|
|
|
29
29
|
return shadowSourceModels(configured).some(prefix => modelId.startsWith(prefix));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* The configured source prefix this model matched, or undefined.
|
|
34
|
+
*
|
|
35
|
+
* Callers that RECORD the intercepted model must record this rather than the caller's raw
|
|
36
|
+
* `modelId`. Matching is by prefix, so `gpt-5.6-luna` plus arbitrary trailing text still
|
|
37
|
+
* intercepts — and the raw string is caller-controlled, reaches `usage.jsonl` and `/api/logs`,
|
|
38
|
+
* and only passes a pattern-based redactor on the way. A credential family that redactor does
|
|
39
|
+
* not recognize survives verbatim. Returning the operator-configured prefix keeps the log
|
|
40
|
+
* field inside a set the operator chose, so no caller string is ever persisted.
|
|
41
|
+
*/
|
|
42
|
+
export function shadowSourceModelPrefix(modelId: string, configured?: unknown): string | undefined {
|
|
43
|
+
if (modelId.includes("/")) return undefined;
|
|
44
|
+
return shadowSourceModels(configured).find(prefix => modelId.startsWith(prefix));
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Decide whether a matching source model should use the opt-in intercept.
|
|
34
49
|
*
|
|
@@ -35,7 +35,7 @@ import { listLiveOAuthAccountKeys, reconcileOAuthReauthState } from "../oauth/st
|
|
|
35
35
|
import { reconcileGuardianBackoff } from "../oauth/token-guardian";
|
|
36
36
|
import { sweepExpiredApiKeyCooldowns } from "../providers/key-failover";
|
|
37
37
|
import { reconcileProviderRequestPacing } from "../providers/request-pacing";
|
|
38
|
-
import { sweepExpiredResponseStates } from "../responses/state";
|
|
38
|
+
import { sweepAbandonedResponseStateTemps, sweepExpiredResponseStates } from "../responses/state";
|
|
39
39
|
import { sweepExpiredAntigravityReplay } from "../adapters/google-antigravity-replay";
|
|
40
40
|
import { reconcileProviderAccountQuotaRows } from "../providers/quota";
|
|
41
41
|
import { reconcileRouterWarningMemos } from "../router";
|
|
@@ -84,7 +84,13 @@ export const STATE_STORE_REGISTRATIONS = [
|
|
|
84
84
|
},
|
|
85
85
|
{ name: "anthropic-routing-health", sweepExpired: sweepExpiredAnthropicRoutingHealth },
|
|
86
86
|
{ name: "xai-refresh-verdicts", sweepExpired: sweepExpiredXaiPermanentFailureVerdicts },
|
|
87
|
-
{
|
|
87
|
+
{
|
|
88
|
+
name: "responses-continuation",
|
|
89
|
+
sweepExpired: sweepExpiredResponseStates,
|
|
90
|
+
// Disk reclaim rides the liveness tick, not the TTL tick: sweepExpiredOnWrite puts
|
|
91
|
+
// sweepExpired on hot write paths, where a directory scan does not belong.
|
|
92
|
+
sweepLiveness: sweepAbandonedResponseStateTemps,
|
|
93
|
+
},
|
|
88
94
|
{ name: "antigravity-replay", sweepExpired: sweepExpiredAntigravityReplay },
|
|
89
95
|
{ name: "config-warning-memos", reconcileGeneration: (context: GenerationContext) => reconcileConfigWarningMemos(context.generation) },
|
|
90
96
|
{ name: "catalog-warning-memos", reconcileGeneration: (context: GenerationContext) => reconcileCatalogWarningMemos(context.generation) },
|
package/src/oauth/index.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { loginCommandCode, refreshCommandCodeToken } from "./command-code";
|
|
|
17
17
|
import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
|
|
18
18
|
import { deriveOAuthDefaultModel, deriveOAuthProviderConfig } from "../providers/derive";
|
|
19
19
|
import { apiKeyPoolEntryId, sanitizeApiKeyValue } from "../providers/api-keys";
|
|
20
|
-
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../providers/registry";
|
|
20
|
+
import { effectiveGoogleMode, getProviderRegistryEntry, mergeRegistryStaticHeaders, providerMatchesRegistryTransport } from "../providers/registry";
|
|
21
21
|
import { resolveProviderModelDiscoveryUrl } from "../providers/model-discovery";
|
|
22
22
|
import { resolveProviderTransport } from "../providers/xai-transport";
|
|
23
23
|
import { detectClaudeCodeToken, detectGrokCliToken, hasComparableGrokIdentity, isSameGrokIdentity, shouldAdoptGrokGeneration } from "./local-token-detect";
|
|
@@ -322,6 +322,13 @@ export class OAuthReauthIdentityUnverifiedError extends Error {
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
class OAuthLoginSupersededError extends Error {
|
|
326
|
+
constructor() {
|
|
327
|
+
super("OAuth login was superseded before credential persistence");
|
|
328
|
+
this.name = "OAuthLoginSupersededError";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
325
332
|
/** Project arbitrary OAuth failures onto the small, stable public error vocabulary. */
|
|
326
333
|
export function publicOAuthAuthenticationErrorMessage(error: unknown): string {
|
|
327
334
|
if (error instanceof OAuthMutationBusyError) {
|
|
@@ -828,7 +835,16 @@ export function buildModelsRequest(
|
|
|
828
835
|
undefined,
|
|
829
836
|
copilotApiBaseUrl,
|
|
830
837
|
);
|
|
831
|
-
|
|
838
|
+
// Model discovery is an upstream request like any other, so it carries the same registry
|
|
839
|
+
// static headers the inference path does. Without this a provider is identified correctly
|
|
840
|
+
// when it answers a completion but anonymously when it lists its own models, which is the
|
|
841
|
+
// kind of split fingerprint an upstream rate limiter reads as two different clients.
|
|
842
|
+
const registryStaticHeaders = providerMatchesRegistryTransport(providerName, effectiveProvider)
|
|
843
|
+
? getProviderRegistryEntry(providerName)?.staticHeaders
|
|
844
|
+
: undefined;
|
|
845
|
+
const headers: Record<string, string> = {
|
|
846
|
+
...(mergeRegistryStaticHeaders(registryStaticHeaders, effectiveProvider.headers) ?? {}),
|
|
847
|
+
};
|
|
832
848
|
const discoveryUrl = (defaultUrl: string): string => resolveProviderModelDiscoveryUrl(
|
|
833
849
|
providerName,
|
|
834
850
|
prov,
|
|
@@ -1096,6 +1112,7 @@ interface RunLoginDeps {
|
|
|
1096
1112
|
settleKiroLoginTransaction?: typeof settleKiroLoginTransaction;
|
|
1097
1113
|
removeAccount?: typeof removeAccount;
|
|
1098
1114
|
setActiveAccount?: typeof setActiveAccount;
|
|
1115
|
+
assertCurrentOwner?: () => void;
|
|
1099
1116
|
}
|
|
1100
1117
|
|
|
1101
1118
|
/** Roll back only accounts created by this forced login, preserving concurrent refreshes of others. */
|
|
@@ -1145,6 +1162,7 @@ export async function runLogin(
|
|
|
1145
1162
|
const cred: OAuthCredentials = rawCred.source ? rawCred : { ...rawCred, source: "oauth" };
|
|
1146
1163
|
const settleKiroTransaction = deps.settleKiroLoginTransaction ?? settleKiroLoginTransaction;
|
|
1147
1164
|
try {
|
|
1165
|
+
deps.assertCurrentOwner?.();
|
|
1148
1166
|
// Validate the provider row before credential persistence. A namespace claimed during the
|
|
1149
1167
|
// credential write is handled again below before the latest row is re-upserted.
|
|
1150
1168
|
if (provider !== "chatgpt") {
|
|
@@ -1165,10 +1183,13 @@ export async function runLogin(
|
|
|
1165
1183
|
if (!identityMatches) {
|
|
1166
1184
|
throw new OAuthReauthIdentityMismatchError();
|
|
1167
1185
|
}
|
|
1168
|
-
await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred
|
|
1186
|
+
await (deps.saveAccountCredential ?? saveAccountCredential)(provider, opts.reauthAccountId, cred, {
|
|
1187
|
+
assertBeforePersist: deps.assertCurrentOwner,
|
|
1188
|
+
});
|
|
1169
1189
|
} else {
|
|
1170
1190
|
await (deps.saveCredential ?? saveCredential)(provider, cred, {
|
|
1171
1191
|
preserveIdentityless: opts?.forceLogin === true,
|
|
1192
|
+
assertBeforePersist: deps.assertCurrentOwner,
|
|
1172
1193
|
});
|
|
1173
1194
|
}
|
|
1174
1195
|
if (provider !== "chatgpt") {
|
|
@@ -1235,6 +1256,7 @@ export async function runLogin(
|
|
|
1235
1256
|
*/
|
|
1236
1257
|
const loginState = new Map<string, { error?: string; done: boolean }>();
|
|
1237
1258
|
const loginAbort = new Map<string, AbortController>();
|
|
1259
|
+
const kiroLoginSettling = new Set<string>();
|
|
1238
1260
|
|
|
1239
1261
|
/** Pending paste for a login in progress: either a waiter or a stashed early submission. */
|
|
1240
1262
|
interface ManualCodeSlot {
|
|
@@ -1403,13 +1425,14 @@ export async function startLoginFlow(
|
|
|
1403
1425
|
const def = OAUTH_PROVIDERS[provider];
|
|
1404
1426
|
if (!def) throw new UnsupportedOAuthProviderError(provider);
|
|
1405
1427
|
const existing = loginState.get(provider);
|
|
1406
|
-
if (existing && !existing.done) {
|
|
1428
|
+
if ((existing && !existing.done) || (provider === "kiro" && kiroLoginSettling.has(provider))) {
|
|
1407
1429
|
throw new Error(`A login for ${provider} is already in progress`);
|
|
1408
1430
|
}
|
|
1409
1431
|
clearManualCodeSlot(provider);
|
|
1410
1432
|
loginState.set(provider, { done: false });
|
|
1411
1433
|
const abort = new AbortController();
|
|
1412
1434
|
loginAbort.set(provider, abort);
|
|
1435
|
+
if (provider === "kiro") kiroLoginSettling.add(provider);
|
|
1413
1436
|
return new Promise((resolve, reject) => {
|
|
1414
1437
|
let urlResolved = false;
|
|
1415
1438
|
const ctrl: OAuthController = {
|
|
@@ -1460,7 +1483,10 @@ export async function startLoginFlow(
|
|
|
1460
1483
|
};
|
|
1461
1484
|
// Background: runLogin persists the credential + provider entry to disk. The lifecycle hook
|
|
1462
1485
|
// lets a long-lived server config adopt that settled state before clients observe done=true.
|
|
1463
|
-
|
|
1486
|
+
const assertCurrentOwner = (): void => {
|
|
1487
|
+
if (loginAbort.get(provider) !== abort) throw new OAuthLoginSupersededError();
|
|
1488
|
+
};
|
|
1489
|
+
void runLogin(provider, ctrl, opts, { assertCurrentOwner }).then(
|
|
1464
1490
|
() => settle(),
|
|
1465
1491
|
(e: unknown) => settle(e),
|
|
1466
1492
|
).catch((e: unknown) => {
|
|
@@ -1471,6 +1497,8 @@ export async function startLoginFlow(
|
|
|
1471
1497
|
const msg = publicOAuthAuthenticationErrorMessage(e);
|
|
1472
1498
|
loginState.set(provider, { done: true, error: msg });
|
|
1473
1499
|
if (!urlResolved) reject(e);
|
|
1500
|
+
}).finally(() => {
|
|
1501
|
+
if (provider === "kiro") kiroLoginSettling.delete(provider);
|
|
1474
1502
|
});
|
|
1475
1503
|
});
|
|
1476
1504
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -465,10 +465,11 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
|
|
|
465
465
|
drainOAuthMutations();
|
|
466
466
|
return result;
|
|
467
467
|
}
|
|
468
|
-
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
468
|
+
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number; assertBeforePersist?: () => void }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
469
469
|
const { store, hadLegacy } = loadAuthStoreInternal();
|
|
470
470
|
if (hadLegacy) backupLegacyOnce();
|
|
471
471
|
const result = await fn(store);
|
|
472
|
+
options?.assertBeforePersist?.();
|
|
472
473
|
persist(store);
|
|
473
474
|
return result;
|
|
474
475
|
}finally{guard.release();}}, retainedValues, options?.waitMs);
|
|
@@ -491,7 +492,7 @@ export function getCredential(provider: string): OAuthCredentials | null {
|
|
|
491
492
|
export async function saveCredential(
|
|
492
493
|
provider: string,
|
|
493
494
|
cred: OAuthCredentials,
|
|
494
|
-
opts: { preserveIdentityless?: boolean } = {},
|
|
495
|
+
opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
|
|
495
496
|
): Promise<void> {
|
|
496
497
|
const safe = normalizeCredential(cred);
|
|
497
498
|
if (!safe) return;
|
|
@@ -542,7 +543,7 @@ export async function saveCredential(
|
|
|
542
543
|
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
543
544
|
set.activeAccountId = id;
|
|
544
545
|
}
|
|
545
|
-
}, [provider, safe]);
|
|
546
|
+
}, [provider, safe], { assertBeforePersist: opts.assertBeforePersist });
|
|
546
547
|
}
|
|
547
548
|
|
|
548
549
|
/**
|
|
@@ -632,7 +633,12 @@ export function getAccountCredential(provider: string, accountId: string): OAuth
|
|
|
632
633
|
}
|
|
633
634
|
|
|
634
635
|
/** Persist a refreshed credential for a SPECIFIC account without touching activeAccountId. */
|
|
635
|
-
export async function saveAccountCredential(
|
|
636
|
+
export async function saveAccountCredential(
|
|
637
|
+
provider: string,
|
|
638
|
+
accountId: string,
|
|
639
|
+
cred: OAuthCredentials,
|
|
640
|
+
opts: { assertBeforePersist?: () => void } = {},
|
|
641
|
+
): Promise<void> {
|
|
636
642
|
const safe = normalizeCredential(cred);
|
|
637
643
|
if (!safe) return;
|
|
638
644
|
await mutateStore(store => {
|
|
@@ -640,7 +646,7 @@ export async function saveAccountCredential(provider: string, accountId: string,
|
|
|
640
646
|
if (!account) return;
|
|
641
647
|
account.credential = safe;
|
|
642
648
|
delete account.needsReauth;
|
|
643
|
-
}, [provider, accountId, safe]);
|
|
649
|
+
}, [provider, accountId, safe], { assertBeforePersist: opts.assertBeforePersist });
|
|
644
650
|
}
|
|
645
651
|
|
|
646
652
|
export async function setActiveAccount(provider: string, accountId: string): Promise<boolean> {
|
|
@@ -72,6 +72,12 @@ const ANTIGRAVITY_WIRE_IDS_BY_PICKER_MODEL: Record<string, string[]> = Object.en
|
|
|
72
72
|
}, {});
|
|
73
73
|
|
|
74
74
|
const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;
|
|
75
|
+
type AntigravityDiscoveryEffort = typeof ANTIGRAVITY_DISCOVERY_EFFORTS[number];
|
|
76
|
+
type AntigravityEffortWireModelIds = Partial<Record<AntigravityDiscoveryEffort, string>>;
|
|
77
|
+
|
|
78
|
+
function isAntigravityDiscoveryEffort(value: string): value is AntigravityDiscoveryEffort {
|
|
79
|
+
return (ANTIGRAVITY_DISCOVERY_EFFORTS as readonly string[]).includes(value);
|
|
80
|
+
}
|
|
75
81
|
|
|
76
82
|
function pickerModelIdForDiscoveredWireId(
|
|
77
83
|
wireId: string,
|
|
@@ -151,6 +157,25 @@ const ANTIGRAVITY_EFFORT_WIRE_MAP: Record<string, Record<string, string>> = {
|
|
|
151
157
|
},
|
|
152
158
|
};
|
|
153
159
|
|
|
160
|
+
function completeDiscoveredEffortWireModelIds(
|
|
161
|
+
pickerId: string,
|
|
162
|
+
available: ReadonlyMap<string, Record<string, unknown>>,
|
|
163
|
+
): AntigravityEffortWireModelIds | undefined {
|
|
164
|
+
const explicitEffortMap = ANTIGRAVITY_EFFORT_WIRE_MAP[pickerId];
|
|
165
|
+
if (explicitEffortMap && Object.values(explicitEffortMap).every(wireId => available.has(wireId))) {
|
|
166
|
+
return { ...explicitEffortMap };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!isKnownAntigravityPickerModelId(pickerId)) return undefined;
|
|
170
|
+
const suffixEffortMap: AntigravityEffortWireModelIds = {};
|
|
171
|
+
for (const effort of ANTIGRAVITY_DISCOVERY_EFFORTS) {
|
|
172
|
+
const wireId = `${pickerId}-${effort}`;
|
|
173
|
+
if (!available.has(wireId)) return undefined;
|
|
174
|
+
suffixEffortMap[effort] = wireId;
|
|
175
|
+
}
|
|
176
|
+
return suffixEffortMap;
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
// ── Default effort per Gemini base model ──
|
|
155
180
|
const ANTIGRAVITY_DEFAULT_EFFORT: Record<string, string> = {
|
|
156
181
|
"gemini-3.1-pro": "high",
|
|
@@ -270,6 +295,8 @@ export interface AntigravityAvailableModel {
|
|
|
270
295
|
id: string;
|
|
271
296
|
/** CCA model id used by the agent envelope when `id` comes from display metadata. */
|
|
272
297
|
wireModelId: string;
|
|
298
|
+
/** Complete effort-to-wire mapping retained for collapsed discovered tier sets. */
|
|
299
|
+
effortWireModelIds?: AntigravityEffortWireModelIds;
|
|
273
300
|
contextWindow?: number;
|
|
274
301
|
inputModalities?: string[];
|
|
275
302
|
}
|
|
@@ -286,6 +313,7 @@ function antigravityPositiveInteger(value: unknown): number | undefined {
|
|
|
286
313
|
|
|
287
314
|
interface DiscoveredWireModelMapping {
|
|
288
315
|
readonly models: ReadonlyMap<string, string>;
|
|
316
|
+
readonly effortModels: ReadonlyMap<string, AntigravityEffortWireModelIds>;
|
|
289
317
|
readonly generation?: { provider: string; cacheGeneration: string };
|
|
290
318
|
}
|
|
291
319
|
|
|
@@ -327,17 +355,21 @@ export function registerAntigravityDiscoveredWireModels(
|
|
|
327
355
|
const key = antigravityBaseUrlKey(baseUrl);
|
|
328
356
|
if (!key) return;
|
|
329
357
|
const wireModels = new Map<string, string>();
|
|
330
|
-
|
|
358
|
+
const effortModels = new Map<string, AntigravityEffortWireModelIds>();
|
|
359
|
+
for (const model of models) {
|
|
360
|
+
wireModels.set(model.id, model.wireModelId);
|
|
361
|
+
if (model.effortWireModelIds) effortModels.set(model.id, { ...model.effortWireModelIds });
|
|
362
|
+
}
|
|
331
363
|
discoveredWireModelsByBaseUrl.set(key, {
|
|
332
364
|
models: wireModels,
|
|
365
|
+
effortModels,
|
|
333
366
|
...(generation ? { generation } : {}),
|
|
334
367
|
});
|
|
335
368
|
}
|
|
336
369
|
|
|
337
|
-
function
|
|
338
|
-
modelId: string,
|
|
370
|
+
function discoveredAntigravityMapping(
|
|
339
371
|
baseUrl: string | undefined,
|
|
340
|
-
):
|
|
372
|
+
): DiscoveredWireModelMapping | undefined {
|
|
341
373
|
const key = antigravityBaseUrlKey(baseUrl);
|
|
342
374
|
if (!key) return undefined;
|
|
343
375
|
const mapping = discoveredWireModelsByBaseUrl.get(key);
|
|
@@ -347,7 +379,35 @@ function discoveredAntigravityWireModelId(
|
|
|
347
379
|
discoveredWireModelsByBaseUrl.delete(key);
|
|
348
380
|
return undefined;
|
|
349
381
|
}
|
|
350
|
-
return mapping
|
|
382
|
+
return mapping;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function discoveredAntigravityWireModelId(
|
|
386
|
+
modelId: string,
|
|
387
|
+
baseUrl: string | undefined,
|
|
388
|
+
): string | undefined {
|
|
389
|
+
return discoveredAntigravityMapping(baseUrl)?.models.get(modelId);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function discoveredAntigravityEffortWireModelId(
|
|
393
|
+
modelId: string,
|
|
394
|
+
effort: string | undefined,
|
|
395
|
+
baseUrl: string | undefined,
|
|
396
|
+
): string | undefined {
|
|
397
|
+
const effortMap = discoveredAntigravityMapping(baseUrl)?.effortModels.get(modelId);
|
|
398
|
+
if (!effortMap) return undefined;
|
|
399
|
+
|
|
400
|
+
const requestedEffort = effort ? resolveAntigravityThinkingLevel(effort) : undefined;
|
|
401
|
+
if (requestedEffort && isAntigravityDiscoveryEffort(requestedEffort) && effortMap[requestedEffort]) {
|
|
402
|
+
return effortMap[requestedEffort];
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const defaultEffort = ANTIGRAVITY_DEFAULT_EFFORT[modelId]
|
|
406
|
+
?? ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
|
|
407
|
+
if (defaultEffort && isAntigravityDiscoveryEffort(defaultEffort) && effortMap[defaultEffort]) {
|
|
408
|
+
return effortMap[defaultEffort];
|
|
409
|
+
}
|
|
410
|
+
return Object.values(effortMap)[0];
|
|
351
411
|
}
|
|
352
412
|
|
|
353
413
|
/**
|
|
@@ -465,9 +525,11 @@ export function parseAntigravityAvailableModels(
|
|
|
465
525
|
const id = pickerModelIdForDiscoveredWireId(wireId, info, available);
|
|
466
526
|
if (seen.has(id)) continue;
|
|
467
527
|
seen.add(id);
|
|
528
|
+
const effortWireModelIds = completeDiscoveredEffortWireModelIds(id, available);
|
|
468
529
|
out.push({
|
|
469
530
|
id,
|
|
470
531
|
wireModelId: wireId,
|
|
532
|
+
...(effortWireModelIds ? { effortWireModelIds } : {}),
|
|
471
533
|
...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
|
|
472
534
|
// Tri-state, deliberately not a ternary: `true` asserts image support,
|
|
473
535
|
// `false` asserts against it, and ABSENT is unknown. Collapsing absent into
|
|
@@ -522,6 +584,9 @@ export function resolveAntigravityEffortWireModel(
|
|
|
522
584
|
effort?: string,
|
|
523
585
|
baseUrl?: string,
|
|
524
586
|
): { wireModelId: string; thinkingLevel?: string } {
|
|
587
|
+
const discoveredEffortWireModelId = discoveredAntigravityEffortWireModelId(modelId, effort, baseUrl);
|
|
588
|
+
if (discoveredEffortWireModelId) return { wireModelId: discoveredEffortWireModelId };
|
|
589
|
+
|
|
525
590
|
// A collapsed picker row reports ONE representative wire id (whichever tier CCA
|
|
526
591
|
// listed first), so live discovery cannot describe a ladder — it can only name a
|
|
527
592
|
// single rung. Letting it answer for a base model we already have a ladder for
|
package/src/providers/derive.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { cloneFastWire } from "./fastwire";
|
|
|
3
3
|
import {
|
|
4
4
|
PROVIDER_REGISTRY,
|
|
5
5
|
registryEntryForProviderDestination,
|
|
6
|
+
registryModelServiceTierCapabilityApplies,
|
|
6
7
|
type ProviderRegistryEntry,
|
|
7
8
|
} from "./registry";
|
|
8
9
|
import {
|
|
@@ -377,6 +378,15 @@ function applyServiceTierModelDefaults(
|
|
|
377
378
|
};
|
|
378
379
|
}
|
|
379
380
|
|
|
381
|
+
function serviceTierModelDefaultsFor(
|
|
382
|
+
entry: ProviderRegistryEntry | undefined,
|
|
383
|
+
prov: OcxProviderConfig,
|
|
384
|
+
): Readonly<Record<string, boolean>> | undefined {
|
|
385
|
+
return entry && registryModelServiceTierCapabilityApplies(entry, prov)
|
|
386
|
+
? entry.modelSupportsServiceTier
|
|
387
|
+
: undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
380
390
|
/**
|
|
381
391
|
* Last-resort enrichment for a provider whose NAME matches no registry id.
|
|
382
392
|
*
|
|
@@ -412,7 +422,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
412
422
|
// which vendor endpoint is this row talking to — and is already restricted to fixed key
|
|
413
423
|
// destinations, so a templated or overridable base URL cannot be claimed by it.
|
|
414
424
|
enrichReasoningSummariesByDestination(prov);
|
|
415
|
-
applyServiceTierModelDefaults(prov, registryEntryForProviderDestination(prov)
|
|
425
|
+
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(registryEntryForProviderDestination(prov), prov));
|
|
416
426
|
return;
|
|
417
427
|
}
|
|
418
428
|
const explicitDirectReasoning: DirectReasoningEffortOverrides = {
|
|
@@ -466,7 +476,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
466
476
|
if (prov.supportsServiceTier === undefined && entry.supportsServiceTier !== undefined) prov.supportsServiceTier = entry.supportsServiceTier;
|
|
467
477
|
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
468
478
|
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
|
|
469
|
-
applyServiceTierModelDefaults(prov, entry
|
|
479
|
+
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
|
|
470
480
|
// Registry-only repair policy (#938): fill only when the runtime provider has
|
|
471
481
|
// no explicit policy, and deep-clone so saved/user values never alias the
|
|
472
482
|
// registry constant.
|
|
@@ -7,7 +7,7 @@ import type {
|
|
|
7
7
|
} from "../types";
|
|
8
8
|
import { MODEL_ADAPTER_OVERRIDE_ALLOWED } from "../types";
|
|
9
9
|
import { sanitizeLogMetadataString } from "../lib/redact";
|
|
10
|
-
import type { InboundWire, ModelWireDefault } from "./registry";
|
|
10
|
+
import type { InboundWire, ModelWireDefault, ProviderAuthKind } from "./registry";
|
|
11
11
|
|
|
12
12
|
const SERVICE_TIER_ADAPTERS = new Set(["openai-chat", "openai-responses"]);
|
|
13
13
|
const FAST_WIRE_ADAPTERS: Readonly<Record<FastWire["kind"], ReadonlySet<string>>> = {
|
|
@@ -31,6 +31,7 @@ export type FastPolicyAuthTransport =
|
|
|
31
31
|
|
|
32
32
|
export interface FastPolicyAuthority {
|
|
33
33
|
readonly providerAdapter: string;
|
|
34
|
+
readonly providerAuthMode?: ProviderAuthKind;
|
|
34
35
|
readonly fastWireDeclaration: FastWire | null | undefined;
|
|
35
36
|
readonly modelWireOverrideAllowed: boolean;
|
|
36
37
|
readonly authTransport: FastPolicyAuthTransport;
|
|
@@ -114,21 +115,33 @@ function registryDefaultForModel(
|
|
|
114
115
|
defaults: Readonly<Record<string, ModelWireDefault>>,
|
|
115
116
|
modelId: string,
|
|
116
117
|
inbound: InboundWire,
|
|
117
|
-
|
|
118
|
+
authMode: ProviderAuthKind | undefined,
|
|
119
|
+
): { adapter: string; forwardCallerServiceTier?: boolean } | undefined {
|
|
118
120
|
const normalizedModelId = modelId.trim().toLowerCase();
|
|
119
121
|
if (!Object.hasOwn(defaults, normalizedModelId)) return undefined;
|
|
120
122
|
const declared = defaults[normalizedModelId];
|
|
121
123
|
if (declared === undefined) return undefined;
|
|
122
|
-
if (typeof declared !== "string"
|
|
124
|
+
if (typeof declared !== "string") {
|
|
125
|
+
if (!declared.inbound.includes(inbound)) return undefined;
|
|
126
|
+
if (declared.authModes && (authMode === undefined || !declared.authModes.includes(authMode))) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
123
130
|
const wire = typeof declared === "string" ? declared : declared.wire;
|
|
124
|
-
|
|
131
|
+
if (!MODEL_ADAPTER_OVERRIDE_ALLOWED.has(wire)) return undefined;
|
|
132
|
+
return {
|
|
133
|
+
adapter: wire,
|
|
134
|
+
...(typeof declared !== "string" && declared.forwardCallerServiceTier !== undefined
|
|
135
|
+
? { forwardCallerServiceTier: declared.forwardCallerServiceTier }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
125
138
|
}
|
|
126
139
|
|
|
127
140
|
function resolvePolicyAdapter(
|
|
128
141
|
authority: FastPolicyAuthority,
|
|
129
142
|
modelId: string,
|
|
130
143
|
inbound: InboundWire,
|
|
131
|
-
): { adapter: string; hardPinned: boolean } {
|
|
144
|
+
): { adapter: string; hardPinned: boolean; forwardCallerServiceTier?: boolean } {
|
|
132
145
|
// Hard pins and configured overrides deliberately use the same exact-key semantics as
|
|
133
146
|
// resolveWireProtocolOverride(). Registry defaults alone normalize ids at their boundary.
|
|
134
147
|
const hardPin = Object.hasOwn(authority.hardPins, modelId)
|
|
@@ -143,8 +156,21 @@ function resolvePolicyAdapter(
|
|
|
143
156
|
return { adapter: configured, hardPinned: false };
|
|
144
157
|
}
|
|
145
158
|
if (MODEL_ADAPTER_OVERRIDE_ALLOWED.has(authority.providerAdapter)) {
|
|
146
|
-
const registryDefault = registryDefaultForModel(
|
|
147
|
-
|
|
159
|
+
const registryDefault = registryDefaultForModel(
|
|
160
|
+
authority.registryWireDefaults,
|
|
161
|
+
modelId,
|
|
162
|
+
inbound,
|
|
163
|
+
authority.providerAuthMode,
|
|
164
|
+
);
|
|
165
|
+
if (registryDefault !== undefined) {
|
|
166
|
+
return {
|
|
167
|
+
adapter: registryDefault.adapter,
|
|
168
|
+
hardPinned: false,
|
|
169
|
+
...(registryDefault.forwardCallerServiceTier !== undefined
|
|
170
|
+
? { forwardCallerServiceTier: registryDefault.forwardCallerServiceTier }
|
|
171
|
+
: {}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
148
174
|
}
|
|
149
175
|
}
|
|
150
176
|
return { adapter: authority.providerAdapter, hardPinned: false };
|
|
@@ -155,7 +181,11 @@ export function resolveFastPolicy(
|
|
|
155
181
|
modelId: string,
|
|
156
182
|
inbound: InboundWire = "responses",
|
|
157
183
|
): ResolvedFastPolicy {
|
|
158
|
-
const { adapter, hardPinned } = resolvePolicyAdapter(
|
|
184
|
+
const { adapter, hardPinned, forwardCallerServiceTier } = resolvePolicyAdapter(
|
|
185
|
+
authority,
|
|
186
|
+
modelId,
|
|
187
|
+
inbound,
|
|
188
|
+
);
|
|
159
189
|
const exactCapability = exactModelValue(authority.capability.models, modelId);
|
|
160
190
|
const capability = authority.capability.provider === false
|
|
161
191
|
? false
|
|
@@ -173,6 +203,7 @@ export function resolveFastPolicy(
|
|
|
173
203
|
// tier still needs the final wire's forwarding permission.
|
|
174
204
|
const forwardCallerTier = capability !== false
|
|
175
205
|
&& callerWireAvailable
|
|
206
|
+
&& forwardCallerServiceTier !== false
|
|
176
207
|
&& (adapter !== "openai-chat" || authority.capability.chatServiceTier === true);
|
|
177
208
|
|
|
178
209
|
let eligibility: ResolvedFastPolicy["eligibility"];
|
package/src/providers/quota.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { getAccountCredential, getAccountSet, getCredential } from "../oauth/sto
|
|
|
13
13
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
14
14
|
import { apiKeyPoolEntryId } from "./api-keys";
|
|
15
15
|
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
|
|
16
|
-
import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
|
|
16
|
+
import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
|
|
17
17
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
18
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
|
|
19
19
|
import {
|
|
@@ -2084,7 +2084,14 @@ async function maybeFetchProviderQuota(
|
|
|
2084
2084
|
&& isCanonicalCommandCodeBaseUrl(provider.baseUrl)) {
|
|
2085
2085
|
return fetchCommandCodeQuota(name, provider);
|
|
2086
2086
|
}
|
|
2087
|
-
|
|
2087
|
+
// Identify OpenCode Go by where it routes, not by what the row is called. Multi-account
|
|
2088
|
+
// setups keep the same destination under names like `opencode-go-2` (#1924), and those rows
|
|
2089
|
+
// silently had no quota panel and no `ocx provider quota --json` report while the literal
|
|
2090
|
+
// name was the gate. `registryEntryForProviderDestination` is the existing predicate for
|
|
2091
|
+
// exactly this question: normalized endpoint + adapter + key auth, so a canonical URL behind
|
|
2092
|
+
// a different adapter is still not OpenCode Go. The defensive URL check inside
|
|
2093
|
+
// `fetchOpenCodeGoQuota` stays — sending a key anywhere must not depend on this gate.
|
|
2094
|
+
if ((provider.authMode ?? "key") === "key" && registryEntryForProviderDestination(provider)?.id === "opencode-go") {
|
|
2088
2095
|
return fetchOpenCodeGoQuota(name, provider);
|
|
2089
2096
|
}
|
|
2090
2097
|
if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
|