@bitkyc08/opencodex 2.39.0 → 2.40.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/README.md +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { resolveEnvValue, saveConfigPreservingClaudeCode } from "../config";
|
|
3
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Opt-in OS keychain storage for provider API keys (#1221).
|
|
7
|
+
*
|
|
8
|
+
* `config.json` keeps only a reference (`keychain:<provider>` for the active key,
|
|
9
|
+
* `keychain:<provider>/<pool id>` for pool entries); the secret lives in the OS credential store
|
|
10
|
+
* under one service name. Reads are synchronous on purpose: `routedProviderConfig` and the
|
|
11
|
+
* quota/compaction/catalog callers are all sync, and `@napi-rs/keyring` ships a sync `Entry`.
|
|
12
|
+
*
|
|
13
|
+
* Policy: a reference that cannot be resolved fails closed (no key) and is warned once per
|
|
14
|
+
* account; nothing ever rewrites plaintext into config or its backups. Opting in verifies the
|
|
15
|
+
* keychain by writing and reading back before the config is touched, so an unavailable store
|
|
16
|
+
* (headless service, locked session) refuses rather than half-migrating.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
|
|
20
|
+
export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
|
|
21
|
+
|
|
22
|
+
export interface ProviderKeychainEntry {
|
|
23
|
+
getPassword(): string | null;
|
|
24
|
+
setPassword(password: string): void;
|
|
25
|
+
deletePassword(): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type ProviderKeychainEntryFactory = (service: string, account: string) => ProviderKeychainEntry;
|
|
29
|
+
|
|
30
|
+
const nodeRequire = createRequire(import.meta.url);
|
|
31
|
+
|
|
32
|
+
function defaultEntryFactory(service: string, account: string): ProviderKeychainEntry {
|
|
33
|
+
const { Entry } = nodeRequire("@napi-rs/keyring") as { Entry: new (s: string, a: string) => ProviderKeychainEntry };
|
|
34
|
+
return new Entry(service, account);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let entryFactory: ProviderKeychainEntryFactory = defaultEntryFactory;
|
|
38
|
+
const resolvedCache = new Map<string, string>();
|
|
39
|
+
const warnedAccounts = new Set<string>();
|
|
40
|
+
|
|
41
|
+
/** Test seam: swap the OS entry for an in-memory one and drop caches. */
|
|
42
|
+
export function setProviderKeychainEntryFactoryForTests(factory: ProviderKeychainEntryFactory | null): void {
|
|
43
|
+
entryFactory = factory ?? defaultEntryFactory;
|
|
44
|
+
resolvedCache.clear();
|
|
45
|
+
warnedAccounts.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isKeychainReference(value: string | undefined): value is string {
|
|
49
|
+
return typeof value === "string" && value.startsWith(KEYCHAIN_REFERENCE_PREFIX) && value.length > KEYCHAIN_REFERENCE_PREFIX.length;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function keychainAccount(reference: string): string {
|
|
53
|
+
return reference.slice(KEYCHAIN_REFERENCE_PREFIX.length);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function readKeychain(account: string): string | undefined {
|
|
57
|
+
const cached = resolvedCache.get(account);
|
|
58
|
+
if (cached !== undefined) return cached;
|
|
59
|
+
try {
|
|
60
|
+
const value = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).getPassword();
|
|
61
|
+
if (typeof value === "string" && value.trim()) {
|
|
62
|
+
resolvedCache.set(account, value);
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// fall through to the single warning below
|
|
67
|
+
}
|
|
68
|
+
if (!warnedAccounts.has(account)) {
|
|
69
|
+
warnedAccounts.add(account);
|
|
70
|
+
console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Single resolver for provider key material: env references, keychain references, or the
|
|
77
|
+
* literal value. Every request-time read of `apiKey` goes through here.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveProviderApiKey(value: string | undefined): string | undefined {
|
|
80
|
+
if (!value) return undefined;
|
|
81
|
+
if (isKeychainReference(value)) return readKeychain(keychainAccount(value));
|
|
82
|
+
return resolveEnvValue(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
|
|
86
|
+
|
|
87
|
+
export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
|
|
88
|
+
const key = provider?.apiKey;
|
|
89
|
+
if (!key) return "none";
|
|
90
|
+
if (isKeychainReference(key)) return "keychain";
|
|
91
|
+
if (/^\$\{?\w+\}?$/.test(key)) return "env";
|
|
92
|
+
return "file";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Probe the OS keychain with a throwaway account: write, read back, delete. */
|
|
96
|
+
export function probeProviderKeychain(): { available: true } | { available: false; reason: string } {
|
|
97
|
+
const account = `probe-${process.pid}-${Date.now()}`;
|
|
98
|
+
try {
|
|
99
|
+
const entry = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
|
|
100
|
+
entry.setPassword("ok");
|
|
101
|
+
const back = entry.getPassword();
|
|
102
|
+
try { entry.deletePassword(); } catch { /* best effort */ }
|
|
103
|
+
if (back !== "ok") return { available: false, reason: "keychain read-back did not match" };
|
|
104
|
+
return { available: true };
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return { available: false, reason: error instanceof Error ? error.message : "keychain unavailable" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function writeVerified(account: string, secret: string): void {
|
|
111
|
+
const entry = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account);
|
|
112
|
+
entry.setPassword(secret);
|
|
113
|
+
if (entry.getPassword() !== secret) throw new Error(`keychain read-back mismatch for ${account}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Move a provider's active key and every plaintext pool entry into the OS keychain and rewrite
|
|
118
|
+
* config with references. All keychain writes are verified before config changes; on any
|
|
119
|
+
* failure the entries written so far are deleted and config is left untouched.
|
|
120
|
+
*/
|
|
121
|
+
export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { ok: true; moved: number } | { ok: false; error: string; status: number } {
|
|
122
|
+
const provider = config.providers[name];
|
|
123
|
+
if (!provider) return { ok: false, error: "unknown provider", status: 404 };
|
|
124
|
+
if (provider.authMode === "oauth" || provider.authMode === "forward") {
|
|
125
|
+
return { ok: false, error: "provider does not use API-key auth", status: 400 };
|
|
126
|
+
}
|
|
127
|
+
const probe = probeProviderKeychain();
|
|
128
|
+
if (!probe.available) return { ok: false, error: `OS keychain unavailable: ${probe.reason}`, status: 503 };
|
|
129
|
+
|
|
130
|
+
const written: string[] = [];
|
|
131
|
+
const planned: Array<() => void> = [];
|
|
132
|
+
const pool = provider.apiKeyPool ?? [];
|
|
133
|
+
try {
|
|
134
|
+
for (const entry of pool) {
|
|
135
|
+
if (isKeychainReference(entry.key)) continue;
|
|
136
|
+
const secret = resolveEnvValue(entry.key);
|
|
137
|
+
if (!secret) continue; // unresolved env reference stays as-is
|
|
138
|
+
const account = `${name}/${entry.id}`;
|
|
139
|
+
writeVerified(account, secret);
|
|
140
|
+
written.push(account);
|
|
141
|
+
planned.push(() => { entry.key = `${KEYCHAIN_REFERENCE_PREFIX}${account}`; });
|
|
142
|
+
}
|
|
143
|
+
if (provider.apiKey && !isKeychainReference(provider.apiKey)) {
|
|
144
|
+
const active = pool.find(e => e.key === provider.apiKey || (isKeychainReference(e.key) && false));
|
|
145
|
+
const secret = resolveEnvValue(provider.apiKey);
|
|
146
|
+
if (secret) {
|
|
147
|
+
if (active) {
|
|
148
|
+
// Mirror the pool reference so failover keeps comparing equal strings.
|
|
149
|
+
planned.push(() => { provider.apiKey = `${KEYCHAIN_REFERENCE_PREFIX}${name}/${active.id}`; });
|
|
150
|
+
} else {
|
|
151
|
+
writeVerified(name, secret);
|
|
152
|
+
written.push(name);
|
|
153
|
+
planned.push(() => { provider.apiKey = `${KEYCHAIN_REFERENCE_PREFIX}${name}`; });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
for (const account of written) {
|
|
159
|
+
try { entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).deletePassword(); } catch { /* best effort */ }
|
|
160
|
+
}
|
|
161
|
+
return { ok: false, error: `OS keychain write failed: ${error instanceof Error ? error.message : "unknown"}`, status: 503 };
|
|
162
|
+
}
|
|
163
|
+
for (const apply of planned) apply();
|
|
164
|
+
resolvedCache.clear();
|
|
165
|
+
warnedAccounts.clear();
|
|
166
|
+
saveConfigPreservingClaudeCode(config);
|
|
167
|
+
return { ok: true, moved: written.length };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Reverse of `storeProviderKeyInKeychain`: read every reference back, write plaintext, delete items. */
|
|
171
|
+
export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string): { ok: true; restored: number } | { ok: false; error: string; status: number } {
|
|
172
|
+
const provider = config.providers[name];
|
|
173
|
+
if (!provider) return { ok: false, error: "unknown provider", status: 404 };
|
|
174
|
+
const pool = provider.apiKeyPool ?? [];
|
|
175
|
+
const resolved = new Map<string, string>();
|
|
176
|
+
const refs = [provider.apiKey, ...pool.map(e => e.key)].filter(isKeychainReference);
|
|
177
|
+
for (const ref of refs) {
|
|
178
|
+
const account = keychainAccount(ref);
|
|
179
|
+
if (resolved.has(account)) continue;
|
|
180
|
+
let value: string | null = null;
|
|
181
|
+
try { value = entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).getPassword(); } catch { value = null; }
|
|
182
|
+
if (!value) return { ok: false, error: `OS keychain has no readable secret for ${ref}; config left unchanged`, status: 503 };
|
|
183
|
+
resolved.set(account, value);
|
|
184
|
+
}
|
|
185
|
+
for (const entry of pool) {
|
|
186
|
+
if (isKeychainReference(entry.key)) entry.key = resolved.get(keychainAccount(entry.key))!;
|
|
187
|
+
}
|
|
188
|
+
if (isKeychainReference(provider.apiKey)) provider.apiKey = resolved.get(keychainAccount(provider.apiKey))!;
|
|
189
|
+
for (const account of resolved.keys()) {
|
|
190
|
+
try { entryFactory(PROVIDER_KEYCHAIN_SERVICE, account).deletePassword(); } catch { /* best effort */ }
|
|
191
|
+
}
|
|
192
|
+
resolvedCache.clear();
|
|
193
|
+
warnedAccounts.clear();
|
|
194
|
+
saveConfigPreservingClaudeCode(config);
|
|
195
|
+
return { ok: true, restored: resolved.size };
|
|
196
|
+
}
|
|
197
|
+
|
|
@@ -99,6 +99,9 @@ const MODEL_ID_LISTS = [
|
|
|
99
99
|
// their catalog instead of being renamed. OAuth reconciliation does not cover this
|
|
100
100
|
// field, so the rename has to.
|
|
101
101
|
"selectedModels",
|
|
102
|
+
// Same reasoning as `selectedModels`: a retired id pinned here would be resurrected as a
|
|
103
|
+
// ghost row on every discovery instead of following the rename (#1690).
|
|
104
|
+
"retainModels",
|
|
102
105
|
"noVisionModels",
|
|
103
106
|
"noReasoningModels",
|
|
104
107
|
"noTemperatureModels",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveProviderApiKey } from "./key-store";
|
|
2
2
|
import {
|
|
3
3
|
CodexPoolAuthenticationError,
|
|
4
4
|
headersForCodexAuthContext,
|
|
@@ -198,7 +198,7 @@ export function selectOpenAiImagesProvider(config: OcxConfig): OpenAiImagesProvi
|
|
|
198
198
|
&& provider.authMode !== "forward"
|
|
199
199
|
&& provider.baseUrl.replace(/\/+$/, "") === "https://api.openai.com/v1"
|
|
200
200
|
) {
|
|
201
|
-
const apiKey =
|
|
201
|
+
const apiKey = resolveProviderApiKey(provider.apiKey)?.trim();
|
|
202
202
|
if (apiKey) selection.keyed = { providerName: OPENAI_API_PROVIDER_ID, provider, apiKey };
|
|
203
203
|
}
|
|
204
204
|
return selection;
|
|
@@ -236,7 +236,7 @@ export function selectImagesProvider(config: OcxConfig): OpenAiImagesProviderSel
|
|
|
236
236
|
};
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
-
const apiKey =
|
|
239
|
+
const apiKey = resolveProviderApiKey(provider.apiKey)?.trim();
|
|
240
240
|
if (!apiKey) {
|
|
241
241
|
return { forwardCandidates: [], error: `images.provider "${providerName}" has no usable API key` };
|
|
242
242
|
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -9,10 +9,11 @@ import type { StoredAccountQuota } from "../codex/quota";
|
|
|
9
9
|
import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
|
|
10
10
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
11
11
|
import { codexPlanKey } from "../codex/plan";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveProviderApiKey } from "./key-store";
|
|
13
13
|
import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
|
|
14
14
|
import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
|
|
15
15
|
import { antigravityUserAgent } from "../adapters/client-fingerprint";
|
|
16
|
+
import { providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../lib/provider-outbound";
|
|
16
17
|
import { apiKeyPoolEntryId } from "./api-keys";
|
|
17
18
|
import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport";
|
|
18
19
|
import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
|
|
@@ -153,7 +154,7 @@ function cacheKey(config: OcxConfig): string {
|
|
|
153
154
|
const providers = Object.entries(config.providers)
|
|
154
155
|
.map(([name, provider]) => {
|
|
155
156
|
const resolvedKey = typeof provider.apiKey === "string"
|
|
156
|
-
?
|
|
157
|
+
? resolveProviderApiKey(provider.apiKey)?.trim()
|
|
157
158
|
: undefined;
|
|
158
159
|
const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
|
|
159
160
|
return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
|
|
@@ -370,7 +371,7 @@ function firstFinite(record: Record<string, unknown> | null, names: string[]): n
|
|
|
370
371
|
async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
371
372
|
// Never send a configured API key to a lookalike host or through a redirect.
|
|
372
373
|
if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
|
|
373
|
-
const apiKey =
|
|
374
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
374
375
|
if (!apiKey) return null;
|
|
375
376
|
const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
|
|
376
377
|
const [subscriptionResponse, tokenResponse] = await Promise.all([
|
|
@@ -464,7 +465,7 @@ function parseOpenCodeGoUsageWindow(value: unknown): { percent: number; resetAt?
|
|
|
464
465
|
async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
465
466
|
// Never send a configured API key when the provider destination is not the built-in Go endpoint.
|
|
466
467
|
if (!isCanonicalOpenCodeGoBaseUrl(config.baseUrl)) return null;
|
|
467
|
-
const apiKey =
|
|
468
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
468
469
|
if (!apiKey) return null;
|
|
469
470
|
const response = await fetch(OPENCODE_GO_USAGE_URL, {
|
|
470
471
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -510,7 +511,7 @@ async function fetchOpenCodeGoQuota(provider: string, config: OcxProviderConfig)
|
|
|
510
511
|
async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
511
512
|
// Never send a configured API key to a lookalike host or through a redirect.
|
|
512
513
|
if (!isCanonicalOpenRouterBaseUrl(config.baseUrl)) return null;
|
|
513
|
-
const apiKey =
|
|
514
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
514
515
|
if (!apiKey) return null;
|
|
515
516
|
const response = await fetch(`${OPENROUTER_BASE_URL}/key`, {
|
|
516
517
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -557,7 +558,7 @@ async function fetchOpenRouterQuota(provider: string, config: OcxProviderConfig)
|
|
|
557
558
|
*/
|
|
558
559
|
async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
559
560
|
if (!isCanonicalDeepSeekBaseUrl(config.baseUrl)) return null;
|
|
560
|
-
const apiKey =
|
|
561
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
561
562
|
if (!apiKey) return null;
|
|
562
563
|
const response = await fetch(`${DEEPSEEK_BASE_URL}/user/balance`, {
|
|
563
564
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -602,7 +603,7 @@ async function fetchDeepSeekQuota(provider: string, config: OcxProviderConfig):
|
|
|
602
603
|
*/
|
|
603
604
|
async function fetchClineQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
604
605
|
if (!isCanonicalClineBaseUrl(config.baseUrl)) return null;
|
|
605
|
-
const apiKey =
|
|
606
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
606
607
|
if (!apiKey) return null;
|
|
607
608
|
const response = await fetch(`${CLINE_BASE_URL}/api/v1/users/me/plan/usage-limits`, {
|
|
608
609
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -750,7 +751,7 @@ function parseZaiQuotaLegacyFields(data: Record<string, unknown> | null): Provid
|
|
|
750
751
|
*/
|
|
751
752
|
async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
752
753
|
if (!isCanonicalZaiBaseUrl(config.baseUrl)) return null;
|
|
753
|
-
const apiKey =
|
|
754
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
754
755
|
if (!apiKey) return null;
|
|
755
756
|
const normalized = normalizedBaseUrl(config.baseUrl);
|
|
756
757
|
const monitorHost = normalized === ZAI_BASE_URL || normalized === `${ZAI_BASE_URL}/api/coding/paas/v4`
|
|
@@ -793,7 +794,7 @@ async function fetchZaiQuota(provider: string, config: OcxProviderConfig): Promi
|
|
|
793
794
|
*/
|
|
794
795
|
async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
795
796
|
if (!isCanonicalMinimaxBaseUrl(config.baseUrl)) return null;
|
|
796
|
-
const apiKey =
|
|
797
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
797
798
|
if (!apiKey) return null;
|
|
798
799
|
const cnHost = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.minimaxi.com");
|
|
799
800
|
const remainsUrl = cnHost ? "https://api.minimaxi.com/v1/token_plan/remains" : MINIMAX_REMAINS_URL;
|
|
@@ -837,7 +838,7 @@ async function fetchMinimaxQuota(provider: string, config: OcxProviderConfig): P
|
|
|
837
838
|
*/
|
|
838
839
|
async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
839
840
|
if (!isCanonicalMoonshotBaseUrl(config.baseUrl)) return null;
|
|
840
|
-
const apiKey =
|
|
841
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
841
842
|
if (!apiKey) return null;
|
|
842
843
|
const host = normalizedBaseUrl(config.baseUrl)?.startsWith("https://api.moonshot.cn") ? "https://api.moonshot.cn/v1" : MOONSHOT_BASE_URL;
|
|
843
844
|
const response = await fetch(`${host}/users/me/balance`, {
|
|
@@ -881,7 +882,7 @@ async function fetchMoonshotQuota(provider: string, config: OcxProviderConfig):
|
|
|
881
882
|
*/
|
|
882
883
|
async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
883
884
|
if (!isCanonicalVeniceBaseUrl(config.baseUrl)) return null;
|
|
884
|
-
const apiKey =
|
|
885
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
885
886
|
if (!apiKey) return null;
|
|
886
887
|
const response = await fetch(`${VENICE_BASE_URL}/billing/balance`, {
|
|
887
888
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -924,7 +925,7 @@ async function fetchVeniceQuota(provider: string, config: OcxProviderConfig): Pr
|
|
|
924
925
|
*/
|
|
925
926
|
async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
926
927
|
if (!isCanonicalSyntheticBaseUrl(config.baseUrl)) return null;
|
|
927
|
-
const apiKey =
|
|
928
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
928
929
|
if (!apiKey) return null;
|
|
929
930
|
const response = await fetch(`${SYNTHETIC_BASE_URL}/quotas`, {
|
|
930
931
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -972,7 +973,7 @@ async function fetchSyntheticQuota(provider: string, config: OcxProviderConfig):
|
|
|
972
973
|
*/
|
|
973
974
|
async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
974
975
|
if (!isCanonicalDeepInfraBaseUrl(config.baseUrl)) return null;
|
|
975
|
-
const apiKey =
|
|
976
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
976
977
|
if (!apiKey) return null;
|
|
977
978
|
const response = await fetch(`${DEEPINFRA_BASE_URL}/payment/checklist?compute_owed=true`, {
|
|
978
979
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -1014,7 +1015,7 @@ async function fetchDeepInfraQuota(provider: string, config: OcxProviderConfig):
|
|
|
1014
1015
|
*/
|
|
1015
1016
|
async function fetchNeuralwattQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
|
|
1016
1017
|
if (!isCanonicalNeuralwattBaseUrl(config.baseUrl)) return null;
|
|
1017
|
-
const apiKey =
|
|
1018
|
+
const apiKey = resolveProviderApiKey(config.apiKey)?.trim();
|
|
1018
1019
|
if (!apiKey) return null;
|
|
1019
1020
|
const response = await fetch(`${NEURALWATT_BASE_URL}/quota`, {
|
|
1020
1021
|
headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
|
|
@@ -1474,7 +1475,7 @@ export interface ProviderAccountQuota {
|
|
|
1474
1475
|
|
|
1475
1476
|
/** Providers whose per-account quota can be probed. Extend as other OAuth APIs are covered. */
|
|
1476
1477
|
export function supportsPerAccountQuota(provider: string): boolean {
|
|
1477
|
-
return provider === "anthropic" || provider === "kiro";
|
|
1478
|
+
return provider === "anthropic" || provider === "kiro" || provider === "google-antigravity";
|
|
1478
1479
|
}
|
|
1479
1480
|
|
|
1480
1481
|
function accountCacheKey(provider: string, accountId: string): string {
|
|
@@ -1617,7 +1618,16 @@ async function fetchAccountQuota(
|
|
|
1617
1618
|
quota = kiroSnapshot?.quota ?? null;
|
|
1618
1619
|
} else {
|
|
1619
1620
|
const token = await getTokenForAccountQuotaProbe(provider, accountId);
|
|
1620
|
-
|
|
1621
|
+
if (provider === "google-antigravity") {
|
|
1622
|
+
// Per-account Gem/Cla windows (#1082). The project id is part of the stored
|
|
1623
|
+
// credential; without it the probe cannot be made, and that is "unavailable",
|
|
1624
|
+
// never 0%.
|
|
1625
|
+
const projectId = getAccountCredential(provider, accountId)?.projectId;
|
|
1626
|
+
if (!projectId) throw new Error("antigravity account has no project id");
|
|
1627
|
+
quota = await fetchAntigravityUsageQuota(token, projectId);
|
|
1628
|
+
} else {
|
|
1629
|
+
quota = await fetchAnthropicUsageQuota(token);
|
|
1630
|
+
}
|
|
1621
1631
|
}
|
|
1622
1632
|
if (!quota) {
|
|
1623
1633
|
// Preserve last-good bars and mark unavailable; advance TTL so failures
|
|
@@ -1810,7 +1820,7 @@ async function resolveKimiQuotaBearer(config: OcxProviderConfig): Promise<string
|
|
|
1810
1820
|
// ACTIVE key only: silently walking apiKeyPool when the primary env reference is
|
|
1811
1821
|
// unresolved would render a quota bar for a DIFFERENT account than the one routing
|
|
1812
1822
|
// requests — a wrong meter is worse than no meter.
|
|
1813
|
-
const primary =
|
|
1823
|
+
const primary = resolveProviderApiKey(config.apiKey)?.trim();
|
|
1814
1824
|
return primary || null;
|
|
1815
1825
|
}
|
|
1816
1826
|
|
|
@@ -1917,7 +1927,7 @@ async function resolveCommandCodeQuotaBearer(config: OcxProviderConfig): Promise
|
|
|
1917
1927
|
}
|
|
1918
1928
|
// ACTIVE key only: a quota bar for a different account than the one routing
|
|
1919
1929
|
// requests is a wrong meter, not a helpful one.
|
|
1920
|
-
return
|
|
1930
|
+
return resolveProviderApiKey(config.apiKey)?.trim() || null;
|
|
1921
1931
|
}
|
|
1922
1932
|
|
|
1923
1933
|
/**
|
|
@@ -2179,31 +2189,10 @@ function antigravityUsedPercent(quotaInfo: Record<string, unknown>): number | un
|
|
|
2179
2189
|
return normalizePercent(100 - remaining);
|
|
2180
2190
|
}
|
|
2181
2191
|
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
if (!credential?.projectId) return null;
|
|
2185
|
-
let accessToken: string;
|
|
2186
|
-
try {
|
|
2187
|
-
accessToken = await getValidAccessToken("google-antigravity");
|
|
2188
|
-
} catch {
|
|
2189
|
-
return null;
|
|
2190
|
-
}
|
|
2191
|
-
const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, "");
|
|
2192
|
-
const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
|
|
2193
|
-
method: "POST",
|
|
2194
|
-
headers: {
|
|
2195
|
-
Accept: "application/json",
|
|
2196
|
-
"Content-Type": "application/json",
|
|
2197
|
-
"User-Agent": antigravityUserAgent(),
|
|
2198
|
-
Authorization: `Bearer ${accessToken}`,
|
|
2199
|
-
},
|
|
2200
|
-
body: JSON.stringify({ project: credential.projectId }),
|
|
2201
|
-
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2202
|
-
});
|
|
2203
|
-
if (!response.ok) return null;
|
|
2204
|
-
const body = asRecord(await readQuotaJson(response));
|
|
2192
|
+
/** Gem/Cla windows from a `fetchAvailableModels` body; shared by the provider and account probes. */
|
|
2193
|
+
function antigravityWindowsFromModels(body: Record<string, unknown> | null): ProviderQuotaWindow[] {
|
|
2205
2194
|
const models = asRecord(body?.models);
|
|
2206
|
-
if (!models) return
|
|
2195
|
+
if (!models) return [];
|
|
2207
2196
|
|
|
2208
2197
|
const windows = new Map<string, ProviderQuotaWindow>();
|
|
2209
2198
|
for (const [modelId, rawModelInfo] of Object.entries(models)) {
|
|
@@ -2226,6 +2215,66 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
2226
2215
|
const window = windows.get(label);
|
|
2227
2216
|
return window ? [window] : [];
|
|
2228
2217
|
});
|
|
2218
|
+
return customWindows;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const ANTIGRAVITY_ACCOUNT_QUOTA_BASE = "https://daily-cloudcode-pa.googleapis.com";
|
|
2222
|
+
let antigravityOutboundDependencies: ProviderOutboundDependencies = {};
|
|
2223
|
+
|
|
2224
|
+
/** Test seam: inject resolver/pinned transport for the per-account Antigravity probe. */
|
|
2225
|
+
export function setAntigravityAccountQuotaTransportForTests(dependencies: ProviderOutboundDependencies | null): void {
|
|
2226
|
+
antigravityOutboundDependencies = dependencies ?? {};
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
/**
|
|
2230
|
+
* Per-account Antigravity quota (#1082). Always probes Google's own Cloud Code Assist host
|
|
2231
|
+
* through the pinned provider-outbound transport: a configured `baseUrl` is a routing choice
|
|
2232
|
+
* for requests, not a second source of Google's accounting for a stored credential, and fixing
|
|
2233
|
+
* the destination keeps the `provider\0accountId` cache identity exact across config changes.
|
|
2234
|
+
* A redirect or non-2xx yields null (unavailable), never a partial row.
|
|
2235
|
+
*/
|
|
2236
|
+
export async function fetchAntigravityUsageQuota(accessToken: string, projectId: string): Promise<ProviderQuota | null> {
|
|
2237
|
+
const url = `${ANTIGRAVITY_ACCOUNT_QUOTA_BASE}/v1internal:fetchAvailableModels`;
|
|
2238
|
+
const response = await providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
|
|
2239
|
+
headers: {
|
|
2240
|
+
Accept: "application/json",
|
|
2241
|
+
"Content-Type": "application/json",
|
|
2242
|
+
"User-Agent": antigravityUserAgent(),
|
|
2243
|
+
Authorization: `Bearer ${accessToken}`,
|
|
2244
|
+
},
|
|
2245
|
+
body: JSON.stringify({ project: projectId }),
|
|
2246
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2247
|
+
}, antigravityOutboundDependencies);
|
|
2248
|
+
if (await providerRedirectError(response, url)) return null;
|
|
2249
|
+
if (!response.ok) return null;
|
|
2250
|
+
const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
|
|
2251
|
+
if (customWindows.length === 0) return null;
|
|
2252
|
+
return { customWindows, updatedAt: Date.now() };
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
|
|
2256
|
+
const credential = getCredential("google-antigravity");
|
|
2257
|
+
if (!credential?.projectId) return null;
|
|
2258
|
+
let accessToken: string;
|
|
2259
|
+
try {
|
|
2260
|
+
accessToken = await getValidAccessToken("google-antigravity");
|
|
2261
|
+
} catch {
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
const baseUrl = (config.baseUrl || ANTIGRAVITY_ACCOUNT_QUOTA_BASE).replace(/\/+$/, "");
|
|
2265
|
+
const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, {
|
|
2266
|
+
method: "POST",
|
|
2267
|
+
headers: {
|
|
2268
|
+
Accept: "application/json",
|
|
2269
|
+
"Content-Type": "application/json",
|
|
2270
|
+
"User-Agent": antigravityUserAgent(),
|
|
2271
|
+
Authorization: `Bearer ${accessToken}`,
|
|
2272
|
+
},
|
|
2273
|
+
body: JSON.stringify({ project: credential.projectId }),
|
|
2274
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
2275
|
+
});
|
|
2276
|
+
if (!response.ok) return null;
|
|
2277
|
+
const customWindows = antigravityWindowsFromModels(asRecord(await readQuotaJson(response)));
|
|
2229
2278
|
if (customWindows.length === 0) return null;
|
|
2230
2279
|
return report(provider, "google-antigravity:fetchAvailableModels", {
|
|
2231
2280
|
customWindows,
|
|
@@ -13,10 +13,12 @@ import {
|
|
|
13
13
|
CURSOR_NO_VISION_MODELS,
|
|
14
14
|
CURSOR_STATIC_MODELS,
|
|
15
15
|
cursorModelContextWindows,
|
|
16
|
+
cursorModelDisplayNames,
|
|
16
17
|
cursorModelIds,
|
|
17
18
|
cursorModelInputModalities,
|
|
18
19
|
cursorModelReasoningEfforts,
|
|
19
20
|
} from "../adapters/cursor/discovery";
|
|
21
|
+
import { cursorFastCapableBases } from "../adapters/cursor/catalog";
|
|
20
22
|
import { COMMAND_CODE_MODEL_REASONING_EFFORTS } from "./command-code-efforts";
|
|
21
23
|
import { isCanonicalOpenRouterTarget } from "./openrouter-routing";
|
|
22
24
|
|
|
@@ -270,6 +272,12 @@ export interface ProviderRegistryEntry {
|
|
|
270
272
|
modelDiscovery?: ProviderModelDiscoverySpec;
|
|
271
273
|
contextWindow?: number;
|
|
272
274
|
modelContextWindows?: Record<string, number>;
|
|
275
|
+
/**
|
|
276
|
+
* Registry-supplied picker labels. Without these a routed row shows its raw slug,
|
|
277
|
+
* because `routedDisplayName` (codex/catalog/sync.ts) passes the slug through for every
|
|
278
|
+
* provider. An operator's `modelDisplayNames` still wins: derive only fills when absent.
|
|
279
|
+
*/
|
|
280
|
+
modelDisplayNames?: Record<string, string>;
|
|
273
281
|
modelInputModalities?: Record<string, string[]>;
|
|
274
282
|
defaultMaxOutputTokens?: number;
|
|
275
283
|
modelMaxOutputTokens?: Record<string, number>;
|
|
@@ -306,6 +314,7 @@ export interface ProviderRegistryEntry {
|
|
|
306
314
|
preserveReasoningContentModels?: string[];
|
|
307
315
|
requiresReasoningPlaceholderModels?: string[];
|
|
308
316
|
reasoningSplitModels?: string[];
|
|
317
|
+
reasoningDetailsModels?: string[];
|
|
309
318
|
thinkingToggleModels?: string[];
|
|
310
319
|
thinkingBudgetModels?: string[];
|
|
311
320
|
escapeBuiltinToolNames?: boolean;
|
|
@@ -324,10 +333,11 @@ export type ProviderConfigSeed = Pick<
|
|
|
324
333
|
OcxProviderConfig,
|
|
325
334
|
"adapter" | "baseUrl" | "apiKeyTransport" | "responsesPath" | "authMode" | "keyOptional" | "freeTier" | "modelSuffixBracketStrip" | "defaultModel" | "models"
|
|
326
335
|
| "liveModels" | "contextWindow" | "modelContextWindows" | "modelInputModalities"
|
|
336
|
+
| "modelDisplayNames"
|
|
327
337
|
| "modelMaxInputTokens" | "defaultMaxOutputTokens" | "modelMaxOutputTokens"
|
|
328
338
|
| "reasoningEfforts" | "modelReasoningEfforts" | "modelDefaultReasoningEfforts" | "reasoningEffortMap" | "modelReasoningEffortMap" | "reasoningWireFormat"
|
|
329
339
|
| "noVisionModels" | "noReasoningModels" | "noTemperatureModels" | "noTopPModels" | "noPenaltyModels"
|
|
330
|
-
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance"
|
|
340
|
+
| "autoToolChoiceOnlyModels" | "preserveReasoningContentModels" | "requiresReasoningPlaceholderModels" | "reasoningSplitModels" | "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "escapeBuiltinToolNames" | "openaiChatEofTolerance"
|
|
331
341
|
| "googleMode" | "project" | "location" | "headers"
|
|
332
342
|
>;
|
|
333
343
|
|
|
@@ -335,8 +345,10 @@ export type ProviderConfigSeed = Pick<
|
|
|
335
345
|
// same static model seed.
|
|
336
346
|
// 260710 context refresh: Tier-2 evidence in
|
|
337
347
|
// devlog/_plan/260710_provider_hardening/001_research_frontier.md.
|
|
338
|
-
|
|
339
|
-
|
|
348
|
+
// 260902 Claude Fable 5.1 (`claude-fable-5-1`): 1M context / 128K output / adaptive thinking
|
|
349
|
+
// always on, per the official models overview and pricing page (platform.claude.com).
|
|
350
|
+
const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
|
|
351
|
+
const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
|
|
340
352
|
|
|
341
353
|
// 260814 GLM-5.3 is registered pre-emptively alongside 5.2 everywhere 5.2 appears. Z.AI's
|
|
342
354
|
// devpack "How to Switch Models" page (docs.z.ai/devpack/latest-model) lists glm-5.3 and
|
|
@@ -1108,6 +1120,16 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
1108
1120
|
liveModels: true,
|
|
1109
1121
|
defaultModel: "auto",
|
|
1110
1122
|
modelContextWindows: cursorModelContextWindows(CURSOR_STATIC_MODELS),
|
|
1123
|
+
modelDisplayNames: cursorModelDisplayNames(),
|
|
1124
|
+
// Cursor's Fast product is a model VARIANT, not a service_tier field, so the wire kind
|
|
1125
|
+
// is cursor-variant and the request builder consumes the decision.
|
|
1126
|
+
fastWire: { kind: "cursor-variant", canonicalToWire: { priority: "fast" }, foreignCallerTiers: "drop" },
|
|
1127
|
+
// Deliberately NO provider-level supportsServiceTier: resolveFastPolicy short-circuits on
|
|
1128
|
+
// `capability.provider === false` BEFORE consulting the per-model map, which would make
|
|
1129
|
+
// these entries dead config. Absent leaves unlisted bases "unclassified", and a
|
|
1130
|
+
// non-service-tier adapter cannot forward a caller tier, so they still publish no toggle.
|
|
1131
|
+
modelSupportsServiceTier: Object.fromEntries(cursorFastCapableBases().map(id => [id, true])),
|
|
1132
|
+
fastTierDescription: "Cursor Fast variant",
|
|
1111
1133
|
modelInputModalities: cursorModelInputModalities(CURSOR_STATIC_MODELS),
|
|
1112
1134
|
modelReasoningEfforts: cursorModelReasoningEfforts(CURSOR_STATIC_MODELS),
|
|
1113
1135
|
// Kimi K3 documents `max` as its API default, and its Cursor ladder has no `medium`
|
|
@@ -2642,6 +2664,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2642
2664
|
// never a fabricated placeholder (chatgpt-codex-connector P2 on #1205).
|
|
2643
2665
|
requiresReasoningPlaceholderModels: [],
|
|
2644
2666
|
reasoningSplitModels: MINIMAX_MODELS,
|
|
2667
|
+
// With reasoning_split the upstream returns thinking as a structured
|
|
2668
|
+
// reasoning_details array (cumulative text snapshots per stream chunk) and
|
|
2669
|
+
// requires that array back verbatim on the next turn — a reasoning_content
|
|
2670
|
+
// string replay is the native-format pass-back the docs say is unsupported.
|
|
2671
|
+
// Evidence: platform.minimax.io/docs/guides/text-m3-function-call and
|
|
2672
|
+
// /docs/api-reference/text-openai-api (verified 2026-09-01).
|
|
2673
|
+
reasoningDetailsModels: MINIMAX_MODELS,
|
|
2645
2674
|
thinkingToggleModels: ["MiniMax-M3"],
|
|
2646
2675
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "Subscription Key or API Key",
|
|
2647
2676
|
},
|
|
@@ -2655,6 +2684,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2655
2684
|
preserveReasoningContentModels: MINIMAX_MODELS,
|
|
2656
2685
|
requiresReasoningPlaceholderModels: [],
|
|
2657
2686
|
reasoningSplitModels: MINIMAX_MODELS,
|
|
2687
|
+
reasoningDetailsModels: MINIMAX_MODELS,
|
|
2658
2688
|
thinkingToggleModels: ["MiniMax-M3"],
|
|
2659
2689
|
jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "中国区 Subscription Key",
|
|
2660
2690
|
},
|
package/src/reasoning-effort.ts
CHANGED
|
@@ -80,6 +80,38 @@ export function codexEffortRank(effort: string): number {
|
|
|
80
80
|
return CODEX_REASONING_ORDER.indexOf(effort);
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a requested effort against the rungs a target actually supports, never
|
|
85
|
+
* raising above the request.
|
|
86
|
+
*
|
|
87
|
+
* Returns the request itself when supported, otherwise the highest supported rung
|
|
88
|
+
* at or below it, otherwise the lowest supported rung, and `undefined` when the
|
|
89
|
+
* supported set contains no rankable rung at all (including the empty ladder, which
|
|
90
|
+
* is how a no-reasoning model is expressed).
|
|
91
|
+
*
|
|
92
|
+
* This lives here rather than beside its first caller because two very different
|
|
93
|
+
* planes need the same answer: the catalog advertises a combo's default effort, and
|
|
94
|
+
* the request path injects one. When they disagreed, the catalog promised `max` and
|
|
95
|
+
* the runtime silently sent nothing, so the provider default applied instead (#3108).
|
|
96
|
+
* `reasoning-effort.ts` is a leaf — its only import is `./types` — so the request
|
|
97
|
+
* path can share this without pulling the catalog plane along.
|
|
98
|
+
*/
|
|
99
|
+
export function resolveEffortAtOrBelow(
|
|
100
|
+
requested: string | null | undefined,
|
|
101
|
+
supported: readonly string[],
|
|
102
|
+
): string | undefined {
|
|
103
|
+
if (!requested) return undefined;
|
|
104
|
+
if (supported.includes(requested)) return requested;
|
|
105
|
+
const requestedRank = codexEffortRank(requested);
|
|
106
|
+
const ranked = supported
|
|
107
|
+
.map(effort => ({ effort, rank: codexEffortRank(effort) }))
|
|
108
|
+
.filter(item => item.rank >= 0)
|
|
109
|
+
.sort((a, b) => a.rank - b.rank);
|
|
110
|
+
if (ranked.length === 0) return undefined;
|
|
111
|
+
const atOrBelow = ranked.filter(item => item.rank <= requestedRank);
|
|
112
|
+
return atOrBelow.at(-1)?.effort ?? ranked[0]!.effort;
|
|
113
|
+
}
|
|
114
|
+
|
|
83
115
|
export function modelRecordValue<T>(record: Record<string, T> | undefined, modelId: string): T | undefined {
|
|
84
116
|
if (!record) return undefined;
|
|
85
117
|
if (Object.prototype.hasOwnProperty.call(record, modelId)) return record[modelId];
|