@bitkyc08/opencodex 2.49.0 → 2.51.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/AGENTS_INSTALL.md +9 -1
- package/README.md +3 -0
- package/bin/ocx.mjs +222 -71
- package/gui/dist/assets/index-D7BdZpZm.js +115 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/qoder/adapter.ts +69 -1
- package/src/adapters/qoder/scaffold-guard.ts +233 -0
- package/src/claude/agents-inject.ts +29 -5
- package/src/claude/desktop-3p.ts +31 -3
- package/src/claude/gateway-cache.ts +12 -21
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +43 -1
- package/src/cli/claude-agent-startup-sync.ts +26 -1
- package/src/cli/claude.ts +138 -20
- package/src/cli/config-command.ts +67 -1
- package/src/cli/connect.ts +181 -14
- package/src/cli/dispatch.ts +53 -9
- package/src/cli/doctor.ts +9 -2
- package/src/cli/ensure-desired-integrations.ts +10 -0
- package/src/cli/gui-pair-client.ts +1 -12
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +367 -0
- package/src/cli/index.ts +99 -31
- package/src/cli/launcher-context.ts +1 -1
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/registry.ts +43 -3
- package/src/cli/status.ts +325 -5
- package/src/cli/version-skew.ts +4 -1
- package/src/cli.ts +2 -2
- package/src/client/catalog-compatibility.ts +192 -0
- package/src/client/connect.ts +31 -0
- package/src/client/hub-client.ts +52 -0
- package/src/client/hub-state.ts +214 -0
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +50 -13
- package/src/codex/auth-api.ts +156 -28
- package/src/codex/auth-context.ts +21 -0
- package/src/codex/catalog/effort.ts +67 -8
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +71 -2
- package/src/codex/catalog/sync.ts +99 -0
- package/src/codex/codex-write-lock.ts +11 -2
- package/src/codex/desired-state.ts +47 -1
- package/src/codex/inject-coordination.ts +10 -5
- package/src/codex/inject.ts +29 -12
- package/src/codex/loopback-target.ts +45 -0
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +54 -8
- package/src/codex/routing.ts +48 -1
- package/src/codex/runtime.ts +37 -3
- package/src/codex/sync.ts +29 -9
- package/src/codex/warmup.ts +21 -4
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config/pending-teardown.ts +1 -1
- package/src/config.ts +184 -12
- package/src/generated/compatibility-version.json +188 -116
- package/src/grok/status.ts +9 -1
- package/src/integrations/config-io.ts +54 -1
- package/src/lib/bun-runtime.ts +1 -1
- package/src/lib/errors.ts +8 -0
- package/src/lib/gui-pair-capability.ts +27 -0
- package/src/lib/local-destinations.ts +162 -0
- package/src/lib/package-tree-integrity.ts +1 -1
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +130 -20
- package/src/lib/service-secrets.ts +28 -0
- package/src/lib/test-home-guard.ts +49 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/opencode-go-transport.ts +9 -1
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/quota.ts +20 -1
- package/src/providers/registry.ts +35 -6
- package/src/remote/hub-state.ts +182 -0
- package/src/server/auth-cors.ts +11 -0
- package/src/server/chat-completions.ts +10 -7
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +12 -6
- package/src/server/hub-state.ts +98 -0
- package/src/server/images.ts +2 -2
- package/src/server/index.ts +149 -8
- package/src/server/management/api-access.ts +14 -3
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/cursor-integration-routes.ts +13 -4
- package/src/server/management/logs-usage-routes.ts +4 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +9 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +1 -1
- package/src/server/proxy-liveness.ts +7 -1
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log-conversation.ts +41 -1
- package/src/server/request-log.ts +10 -0
- package/src/server/responses/codex-auth-error.ts +18 -1
- package/src/server/responses/codex-ws-exchange.ts +36 -4
- package/src/server/responses/codex-ws-wire.ts +76 -5
- package/src/server/responses/compact.ts +28 -11
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +201 -48
- package/src/server/responses/policy-fallback.ts +13 -3
- package/src/server/search.ts +2 -2
- package/src/server/system-env-shell.ts +14 -2
- package/src/server/system-env.ts +106 -14
- package/src/service.ts +965 -68
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +93 -4
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/update/badge.ts +3 -2
- package/src/update/index.ts +317 -64
- package/src/update/install-detection.d.mts +6 -0
- package/src/update/install-detection.mjs +73 -0
- package/src/update/job.ts +101 -49
- package/src/update/pnpm-global-install.d.mts +144 -0
- package/src/update/pnpm-global-install.mjs +591 -0
- package/src/update/pnpm-invocation.d.mts +43 -0
- package/src/update/pnpm-invocation.mjs +141 -0
- package/src/update/registry-integrity.d.mts +16 -0
- package/src/update/registry-integrity.mjs +37 -0
- package/src/update/transactional-install.d.mts +1 -1
- package/src/update/transactional-install.mjs +101 -7
- package/src/update/tray-update-plan.mjs +1 -1
- package/src/vision/plan.ts +13 -3
- package/src/vision/routed-describe.ts +51 -20
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/gui/dist/assets/index-BtyONQrZ.js +0 -115
|
@@ -61,6 +61,16 @@ function canonicalize(path: string): string {
|
|
|
61
61
|
const REAL_HOME = process.env[REAL_HOME_ENV]?.trim() || homedir();
|
|
62
62
|
const PROTECTED_HOME = canonicalize(join(REAL_HOME, ".opencodex"));
|
|
63
63
|
const PROTECTED_CODEX_HOME = canonicalize(join(REAL_HOME, ".codex"));
|
|
64
|
+
/**
|
|
65
|
+
* `~/Library/LaunchAgents` needs its own entry because HOME isolation does not reach it:
|
|
66
|
+
* `os.homedir()` reads the password database, not `$HOME`, so a macOS test that rewrites
|
|
67
|
+
* HOME still resolves `plistPath()` to the developer's real LaunchAgents directory. The
|
|
68
|
+
* launchd install tests were doing exactly that — replacing the live
|
|
69
|
+
* `com.opencodex.proxy.plist` with one whose token file, log path and Bun paths all point
|
|
70
|
+
* into a temp sandbox, for as long as the case ran. launchd holds its own parsed copy, so
|
|
71
|
+
* nothing broke until the job next restarted.
|
|
72
|
+
*/
|
|
73
|
+
const PROTECTED_LAUNCH_AGENTS = canonicalize(join(REAL_HOME, "Library", "LaunchAgents"));
|
|
64
74
|
|
|
65
75
|
/** The production home this process protects. Exported for the guard's own tests. */
|
|
66
76
|
export function protectedHomeForTests(): string {
|
|
@@ -76,6 +86,23 @@ export function isTestHomeGuardArmed(): boolean {
|
|
|
76
86
|
return process.env[GUARD_ENV] === "1";
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Whether `dir` IS the protected production home, decided with the SAME canonicalization as
|
|
91
|
+
* {@link assertNotRealHomeUnderTest}.
|
|
92
|
+
*
|
|
93
|
+
* For the caller that must FILTER the real home out of a candidate list instead of refusing
|
|
94
|
+
* one write: `serviceStatePaths()` in `src/service.ts` keeps a legacy
|
|
95
|
+
* `~/.opencodex/service-state.json` entry so an install made before OPENCODEX_HOME existed
|
|
96
|
+
* can still be found, and under an armed test process that entry is the developer's live
|
|
97
|
+
* record. Exported so that filter cannot drift onto a weaker comparison — `resolve()` alone
|
|
98
|
+
* calls `/var/folders/...` and `/private/var/folders/...` different paths, which is exactly
|
|
99
|
+
* how a macOS sandbox path slips past a string compare.
|
|
100
|
+
*/
|
|
101
|
+
export function isProtectedHomeUnderTest(dir: string): boolean {
|
|
102
|
+
if (!isTestHomeGuardArmed()) return false;
|
|
103
|
+
return canonicalize(dir) === PROTECTED_HOME;
|
|
104
|
+
}
|
|
105
|
+
|
|
79
106
|
/**
|
|
80
107
|
* Throw when an armed test process is about to write the real OpenCodex home.
|
|
81
108
|
*
|
|
@@ -94,6 +121,28 @@ export function assertNotRealHomeUnderTest(dir: string): void {
|
|
|
94
121
|
);
|
|
95
122
|
}
|
|
96
123
|
|
|
124
|
+
/** The production LaunchAgents directory this process protects. Exported for its tests. */
|
|
125
|
+
export function protectedLaunchAgentsDirForTests(): string {
|
|
126
|
+
return PROTECTED_LAUNCH_AGENTS;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Throw when an armed test process is about to write the real `~/Library/LaunchAgents`.
|
|
131
|
+
*
|
|
132
|
+
* Same contract as {@link assertNotRealHomeUnderTest}: call before any mkdir/write, and
|
|
133
|
+
* pass a DIRECTORY. A launchd test gives `installLaunchd` an explicit plist path inside its
|
|
134
|
+
* own fixture directory instead.
|
|
135
|
+
*/
|
|
136
|
+
export function assertNotRealLaunchAgentsUnderTest(dir: string): void {
|
|
137
|
+
if (!isTestHomeGuardArmed()) return;
|
|
138
|
+
if (canonicalize(dir) !== PROTECTED_LAUNCH_AGENTS) return;
|
|
139
|
+
throw new Error(
|
|
140
|
+
`refusing to write the real LaunchAgents directory (${PROTECTED_LAUNCH_AGENTS}) from a test `
|
|
141
|
+
+ "process: os.homedir() ignores HOME, so rewriting HOME does not move this path. Pass an "
|
|
142
|
+
+ "explicit plist path inside the test's own fixture directory instead.",
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
97
146
|
/** Throw when an armed test process is about to write the real native Codex home. */
|
|
98
147
|
export function assertNotRealCodexHomeUnderTest(dir: string): void {
|
|
99
148
|
if (!isTestHomeGuardArmed()) return;
|
package/src/oauth/health.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getCodexAccountHealthSnapshot, type CodexCooldownSource } from "../codex/routing";
|
|
2
2
|
import { getAnthropicAccountHealthSnapshot } from "./anthropic-routing";
|
|
3
3
|
import { isAccountNeedsReauth } from "../codex/account-runtime-state";
|
|
4
|
-
import { getCodexAccountCredential, listCodexAccountIds } from "../codex/account-store";
|
|
4
|
+
import { getCodexAccountCredential, listCodexAccountIds, readCodexAccountRecord } from "../codex/account-store";
|
|
5
5
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
6
6
|
import { readRuntimePort } from "../config/process-state";
|
|
7
7
|
import { LOCAL_MANAGEMENT_READ_PATHS } from "../lib/local-management-capability";
|
|
@@ -15,7 +15,7 @@ export type OAuthAccountHealth =
|
|
|
15
15
|
| { status: "healthy" }
|
|
16
16
|
| { status: "cooldown"; until: string; reason: "rate_limit" | "quota" }
|
|
17
17
|
| { status: "reauth_required"; reason: "unauthorized" | "forbidden" | "refresh_failed" }
|
|
18
|
-
| { status: "warning"; reason: "refresh_conflict" | "metadata_mismatch" | "stale_credentials" };
|
|
18
|
+
| { status: "warning"; reason: "refresh_conflict" | "metadata_mismatch" | "stale_credentials" | "validation_pending" };
|
|
19
19
|
|
|
20
20
|
export type OAuthHealthLabel =
|
|
21
21
|
| "Healthy"
|
|
@@ -24,7 +24,8 @@ export type OAuthHealthLabel =
|
|
|
24
24
|
| "Reauthentication required"
|
|
25
25
|
| "Refresh failed"
|
|
26
26
|
| "Metadata mismatch"
|
|
27
|
-
| "Credential conflict"
|
|
27
|
+
| "Credential conflict"
|
|
28
|
+
| "Validation pending";
|
|
28
29
|
|
|
29
30
|
/** Shared masked-id fallback when `maskAccountId` returns nullish. */
|
|
30
31
|
export const MASKED_ACCOUNT_FALLBACK = "account-…????";
|
|
@@ -88,6 +89,9 @@ export function projectOAuthAccountHealth(input: {
|
|
|
88
89
|
export const CODEX_REAUTH_ACTION = "reauthenticate via the dashboard Codex account pool";
|
|
89
90
|
|
|
90
91
|
function actionFor(provider: string, health: OAuthAccountHealth): string | undefined {
|
|
92
|
+
if (health.status === "warning" && health.reason === "validation_pending") {
|
|
93
|
+
return "wait for quota recovery, then click Refresh quotas in the dashboard Codex account pool to finish validation";
|
|
94
|
+
}
|
|
91
95
|
if (health.status === "reauth_required") {
|
|
92
96
|
if (provider === "codex") return CODEX_REAUTH_ACTION;
|
|
93
97
|
return `run \`ocx login ${provider}\``;
|
|
@@ -112,6 +116,8 @@ export function oauthHealthLabel(health: OAuthAccountHealth): OAuthHealthLabel {
|
|
|
112
116
|
return health.reason === "refresh_failed" ? "Refresh failed" : "Reauthentication required";
|
|
113
117
|
case "warning":
|
|
114
118
|
switch (health.reason) {
|
|
119
|
+
case "validation_pending":
|
|
120
|
+
return "Validation pending";
|
|
115
121
|
case "refresh_conflict":
|
|
116
122
|
return "Credential conflict";
|
|
117
123
|
case "metadata_mismatch":
|
|
@@ -198,11 +204,42 @@ export function projectCodexAccountHealth(input: {
|
|
|
198
204
|
needsReauth: boolean;
|
|
199
205
|
now?: number;
|
|
200
206
|
}): OAuthAccountHealth {
|
|
207
|
+
// One read serves every verdict below. Each lookup re-reads and re-hardens the whole store
|
|
208
|
+
// file, and the main account lives in the native Codex auth file rather than the pool store,
|
|
209
|
+
// so a lookup for it could only ever miss.
|
|
210
|
+
const stored = input.accountId !== MAIN_CODEX_ACCOUNT_ID ? readCodexAccountRecord(input.accountId) : null;
|
|
211
|
+
const record = stored?.deletedAt == null ? stored : null;
|
|
212
|
+
|
|
213
|
+
// A successful quota read is not evidence that model authorization recovered.
|
|
214
|
+
// Preserve this guidance until validation succeeds or reauthentication replaces it.
|
|
215
|
+
const validationAuthFailed = record !== null
|
|
216
|
+
&& record.codexValidationPending === true
|
|
217
|
+
&& record.lastCodexValidationStatus === "failed"
|
|
218
|
+
&& (record.lastCodexValidationError === "http_status:401" || record.lastCodexValidationError === "http_status:403");
|
|
219
|
+
|
|
220
|
+
// A persisted terminal verdict outranks the in-memory reauth flag rather than duplicating it:
|
|
221
|
+
// the flag lives in this process and a revoked grant does not. Without it, an account whose
|
|
222
|
+
// grant was revoked upstream keeps its login-time `lastCodexValidationStatus: "ok"` and every
|
|
223
|
+
// surface reports it healthy until someone tries to use it (#4120). Only a re-login clears the
|
|
224
|
+
// marker, so `reauth_required` is the accurate projection — and it is deliberately checked
|
|
225
|
+
// ahead of any cooldown, because telling an operator to wait out a rate limit on a credential
|
|
226
|
+
// that will never work again is a false promise.
|
|
227
|
+
const terminalGrantFailure = record !== null
|
|
228
|
+
&& record.lastCodexValidationTerminal === true
|
|
229
|
+
&& record.lastCodexValidationStatus === "failed";
|
|
230
|
+
|
|
231
|
+
const needsReauth = input.needsReauth || validationAuthFailed || terminalGrantFailure;
|
|
232
|
+
|
|
233
|
+
// Deferred validation is only worth reporting while the credential itself is still viable. A
|
|
234
|
+
// revoked grant needs a re-login, not a "Refresh quotas" click, so reauth is resolved first.
|
|
235
|
+
if (!needsReauth && record?.codexValidationPending) {
|
|
236
|
+
return { status: "warning", reason: "validation_pending" };
|
|
237
|
+
}
|
|
201
238
|
const now = input.now ?? Date.now();
|
|
202
239
|
const snap = getCodexAccountHealthSnapshot(input.accountId, now);
|
|
203
240
|
return projectOAuthAccountHealth({
|
|
204
|
-
needsReauth
|
|
205
|
-
reauthReason:
|
|
241
|
+
needsReauth,
|
|
242
|
+
reauthReason: needsReauth ? "refresh_failed" : undefined,
|
|
206
243
|
cooldownUntilMs: snap?.cooldownUntil,
|
|
207
244
|
cooldownReason: cooldownReasonFromSource(snap?.cooldownSource),
|
|
208
245
|
now,
|
|
@@ -272,13 +309,11 @@ function collectLocalCodexEntries(now: number): OAuthHealthEntry[] {
|
|
|
272
309
|
const hasPoolCredential = accountId !== MAIN_CODEX_ACCOUNT_ID && getCodexAccountCredential(accountId) !== null;
|
|
273
310
|
if (!hasPoolCredential && !needsReauth && !snap) continue;
|
|
274
311
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
now,
|
|
281
|
-
});
|
|
312
|
+
// Call the projector rather than inlining a second copy of it. This collector serves the CLI
|
|
313
|
+
// (`ocx status`, `ocx doctor`) while the dashboard DTO goes through projectCodexAccountHealth,
|
|
314
|
+
// and the duplicated body is exactly how the CLI would have kept reporting a revoked account
|
|
315
|
+
// as healthy after the dashboard stopped.
|
|
316
|
+
const health = projectCodexAccountHealth({ accountId, needsReauth, now });
|
|
282
317
|
pushEntry(entries, "codex", accountId, health);
|
|
283
318
|
}
|
|
284
319
|
return entries;
|
package/src/oauth/index.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { parseCallbackInput } from "./callback-server";
|
|
|
4
4
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
5
5
|
import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
|
|
6
6
|
import { resolveProviderApiKey } from "../providers/key-store";
|
|
7
|
-
import {
|
|
7
|
+
import { projectEmail } from "../lib/privacy";
|
|
8
8
|
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
9
9
|
import {
|
|
10
10
|
OAuthMutationBusyError,
|
|
@@ -1781,19 +1781,54 @@ export function submitManualLoginCode(provider: string, input: string): { ok: tr
|
|
|
1781
1781
|
return { ok: true };
|
|
1782
1782
|
}
|
|
1783
1783
|
|
|
1784
|
-
export interface OAuthAccountSummary {
|
|
1784
|
+
export interface OAuthAccountSummary {
|
|
1785
|
+
id: string;
|
|
1786
|
+
alias?: string;
|
|
1787
|
+
email?: string;
|
|
1788
|
+
active: boolean;
|
|
1789
|
+
needsReauth?: boolean;
|
|
1790
|
+
expiresAt?: number;
|
|
1791
|
+
/**
|
|
1792
|
+
* Subscription tier, mirroring the field the OpenAI/Codex provider reports, so a consumer
|
|
1793
|
+
* weighting a multi-account pool by seat size needs no per-provider branching (#3777).
|
|
1794
|
+
*
|
|
1795
|
+
* Always present and explicitly `null` when the tier is unknown. The distinction matters:
|
|
1796
|
+
* an ABSENT key means the proxy is too old to report a tier at all, while `null` means this
|
|
1797
|
+
* version looked and upstream did not say. Omitting it would make those indistinguishable and
|
|
1798
|
+
* invite a consumer to assume a tier.
|
|
1799
|
+
*
|
|
1800
|
+
* Every OAuth provider reports `null` today. Anthropic's `/api/oauth/usage` returns quota
|
|
1801
|
+
* buckets only — `five_hour`, `seven_day`, the model-scoped weekly windows and `limits[]` —
|
|
1802
|
+
* and carries no subscription/tier field, and its token response carries none either. See
|
|
1803
|
+
* `fetchAnthropicUsageQuota` in `src/providers/quota.ts`.
|
|
1804
|
+
*/
|
|
1805
|
+
plan: string | null;
|
|
1806
|
+
}
|
|
1785
1807
|
|
|
1786
|
-
|
|
1808
|
+
/**
|
|
1809
|
+
* Token-safe login state for one provider.
|
|
1810
|
+
*
|
|
1811
|
+
* `maskEmails` is an explicit boolean rather than a config read (#3859). This module must not
|
|
1812
|
+
* acquire a dependency on config I/O to answer a redaction question: the caller already holds
|
|
1813
|
+
* the config at its request boundary and resolves the policy there with `emailMaskingEnabled`.
|
|
1814
|
+
* The default masks, so every existing caller keeps today's behaviour.
|
|
1815
|
+
*/
|
|
1816
|
+
export function getLoginStatus(provider: string, maskEmails = true): { loggedIn: boolean; email?: string; source?: OAuthCredentials["source"]; error?: string; done: boolean; activeAccountId?: string; accounts?: OAuthAccountSummary[] } {
|
|
1787
1817
|
const cred = getCredential(provider);
|
|
1788
1818
|
const st = loginState.get(provider);
|
|
1789
1819
|
const set = getAccountSet(provider);
|
|
1790
1820
|
const accounts: OAuthAccountSummary[] | undefined = set?.accounts.map(a => ({
|
|
1791
1821
|
id: a.id,
|
|
1792
1822
|
...(a.alias ? { alias: a.alias } : {}),
|
|
1793
|
-
email:
|
|
1823
|
+
email: projectEmail(a.credential.email, maskEmails) ?? undefined,
|
|
1794
1824
|
active: a.id === set.activeAccountId,
|
|
1795
1825
|
...(a.needsReauth ? { needsReauth: true } : {}),
|
|
1796
1826
|
expiresAt: a.credential.expires,
|
|
1827
|
+
// Explicitly null rather than omitted — see OAuthAccountSummary.plan. No OAuth provider
|
|
1828
|
+
// exposes a subscription tier today, so there is nothing truthful to put here; deriving one
|
|
1829
|
+
// from quota percentages is not possible, because they are normalized per account and a
|
|
1830
|
+
// half-consumed small seat is indistinguishable from a half-consumed large one.
|
|
1831
|
+
plan: null,
|
|
1797
1832
|
}));
|
|
1798
1833
|
|
|
1799
1834
|
// A stored credential counts as "logged in" when it exists and is not marked for
|
|
@@ -1805,7 +1840,7 @@ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: s
|
|
|
1805
1840
|
.find(a => a.id === set.activeAccountId)?.needsReauth === true;
|
|
1806
1841
|
return {
|
|
1807
1842
|
loggedIn: !!cred && !activeNeedsReauth,
|
|
1808
|
-
email:
|
|
1843
|
+
email: projectEmail(cred?.email, maskEmails) ?? undefined,
|
|
1809
1844
|
source: cred?.source,
|
|
1810
1845
|
error: st?.error,
|
|
1811
1846
|
done: st?.done ?? false,
|
|
@@ -1813,10 +1848,13 @@ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: s
|
|
|
1813
1848
|
};
|
|
1814
1849
|
}
|
|
1815
1850
|
|
|
1816
|
-
/**
|
|
1817
|
-
|
|
1851
|
+
/**
|
|
1852
|
+
* Token-safe per-provider login state for the CLI `ocx status` logins section. Never tokens; the
|
|
1853
|
+
* email follows the operator's `privacy.maskEmails` policy, masked by default (#3859).
|
|
1854
|
+
*/
|
|
1855
|
+
export function oauthLoginSummary(maskEmails = true): Array<{ provider: string; loggedIn: boolean; email?: string }> {
|
|
1818
1856
|
return listOAuthProviders().map(provider => {
|
|
1819
|
-
const status = getLoginStatus(provider);
|
|
1857
|
+
const status = getLoginStatus(provider, maskEmails);
|
|
1820
1858
|
return { provider, loggedIn: status.loggedIn, ...(status.email ? { email: status.email } : {}) };
|
|
1821
1859
|
});
|
|
1822
1860
|
}
|
|
@@ -211,21 +211,30 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
211
211
|
if (!cred) continue;
|
|
212
212
|
const needsRefresh = cred.expiresAt <= nowMs + horizonMs;
|
|
213
213
|
const needsWarmup = opts.codexWarmupEnabled
|
|
214
|
+
&& !record.codexValidationPending
|
|
214
215
|
&& (record.lastCodexValidatedAt === undefined || nowMs - record.lastCodexValidatedAt > opts.codexWarmupMaxAgeSeconds * 1000);
|
|
215
216
|
if (!needsRefresh && !needsWarmup) continue;
|
|
216
217
|
const key = `codex:${id}`;
|
|
217
218
|
if (inBackoff(key, nowMs)) { result.skippedBackoff.push(key); continue; }
|
|
219
|
+
// The generation this sweep is acting on. A successful refresh commits a new one, and a
|
|
220
|
+
// failure that follows belongs to THAT credential, so the fence has to move with it.
|
|
221
|
+
let observedGeneration = record.generation;
|
|
218
222
|
tasks.push(async () => {
|
|
223
|
+
let warmupGeneration: number | undefined;
|
|
219
224
|
try {
|
|
220
225
|
const token = await getValidCodexToken(id);
|
|
226
|
+
observedGeneration = token.generation;
|
|
221
227
|
if (needsRefresh) result.refreshed.push(key);
|
|
222
|
-
|
|
228
|
+
const current = readCodexAccountRecord(id);
|
|
229
|
+
if (needsWarmup && current?.credential && current.deletedAt == null
|
|
230
|
+
&& !current.codexValidationPending && current.generation === token.generation) {
|
|
231
|
+
warmupGeneration = token.generation;
|
|
223
232
|
await warmCodexAccount({
|
|
224
233
|
accessToken: token.accessToken,
|
|
225
234
|
chatgptAccountId: token.chatgptAccountId,
|
|
226
235
|
model: opts.codexWarmupModel,
|
|
227
236
|
});
|
|
228
|
-
markCodexAccountValidated(id, Date.now());
|
|
237
|
+
markCodexAccountValidated(id, Date.now(), token.generation);
|
|
229
238
|
result.warmed.push(key);
|
|
230
239
|
}
|
|
231
240
|
backoff.delete(key);
|
|
@@ -235,11 +244,28 @@ export async function guardianSweep(nowMs: number = Date.now()): Promise<Guardia
|
|
|
235
244
|
result.skippedBackoff.push(key);
|
|
236
245
|
return;
|
|
237
246
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
const terminal = err instanceof TokenRefreshError && (err.reason === "revoked" || err.reason === "expired")
|
|
248
|
+
? err
|
|
249
|
+
: undefined;
|
|
250
|
+
if (terminal) {
|
|
251
|
+
// A revoked or expired refresh grant is the strongest terminal evidence there is, and
|
|
252
|
+
// it used to be the one class that never reached the record: the persisted-verdict
|
|
253
|
+
// branch below requires `needsWarmup`, which is false in the default configuration,
|
|
254
|
+
// and additionally excluded every TokenRefreshError. The verdict landed only in the
|
|
255
|
+
// in-memory backoff map, which no health surface reads and no restart survives, so the
|
|
256
|
+
// account kept its login-time "ok" while every request with it 401'd (#4120).
|
|
257
|
+
markCodexAccountValidationFailed(id, `refresh_${terminal.reason}`, {
|
|
258
|
+
expectedGeneration: observedGeneration,
|
|
259
|
+
terminal: true,
|
|
260
|
+
});
|
|
261
|
+
} else if (warmupGeneration !== undefined && !(err instanceof TokenRefreshError)) {
|
|
262
|
+
// warmupGeneration is set only once the warmup actually started against a record
|
|
263
|
+
// still at the token's generation, so it is a tighter fence than the pre-sweep read.
|
|
264
|
+
markCodexAccountValidationFailed(id, codexWarmupFailureReason(err), {
|
|
265
|
+
expectedGeneration: warmupGeneration,
|
|
266
|
+
});
|
|
241
267
|
}
|
|
242
|
-
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds,
|
|
268
|
+
recordFailure(key, nowMs, opts.backoffBaseSeconds, opts.backoffMaxSeconds, terminal !== undefined, writerGeneration);
|
|
243
269
|
result.failed.push(key);
|
|
244
270
|
}
|
|
245
271
|
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
extractModelEnvelopeRows,
|
|
3
|
+
isValidModelDiscoveryModelId,
|
|
4
|
+
type ProviderModelItemsResult,
|
|
5
|
+
type ProviderModelsApiItem,
|
|
6
|
+
} from "./model-discovery";
|
|
7
|
+
|
|
8
|
+
const GOOGLE_MODEL_PREFIX = "models/";
|
|
9
|
+
const MAX_GENERATION_METHODS = 32;
|
|
10
|
+
const MAX_GENERATION_METHOD_LENGTH = 64;
|
|
11
|
+
|
|
12
|
+
/** Returns the value if it is a positive safe integer; otherwise undefined. */
|
|
13
|
+
function positiveSafeInteger(value: unknown): number | undefined {
|
|
14
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
15
|
+
? value
|
|
16
|
+
: undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracts and normalizes supported model items from a Google AI Studio
|
|
21
|
+
* /v1beta/models response payload.
|
|
22
|
+
*
|
|
23
|
+
* Validates the native models[] envelope, strips the 'models/' prefix, filters
|
|
24
|
+
* to rows supporting 'generateContent', maps input/output token limits, and
|
|
25
|
+
* resiliently skips toxic or malformed individual rows.
|
|
26
|
+
*/
|
|
27
|
+
export function extractGoogleAiStudioModelItems(
|
|
28
|
+
value: unknown,
|
|
29
|
+
maxModels: number,
|
|
30
|
+
): ProviderModelItemsResult {
|
|
31
|
+
const envelope = extractModelEnvelopeRows(value, maxModels, ["models"]);
|
|
32
|
+
if (!envelope.ok) return envelope;
|
|
33
|
+
|
|
34
|
+
const items: ProviderModelsApiItem[] = [];
|
|
35
|
+
const seen = new Set<string>();
|
|
36
|
+
for (const raw of envelope.rows) {
|
|
37
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const name = Reflect.get(raw, "name");
|
|
41
|
+
const generationMethods = Reflect.get(raw, "supportedGenerationMethods");
|
|
42
|
+
if (!isValidModelDiscoveryModelId(name)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (generationMethods === undefined) continue;
|
|
46
|
+
if (
|
|
47
|
+
!Array.isArray(generationMethods)
|
|
48
|
+
|| generationMethods.length > MAX_GENERATION_METHODS
|
|
49
|
+
|| generationMethods.some(method => typeof method !== "string" || method.length > MAX_GENERATION_METHOD_LENGTH)
|
|
50
|
+
) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (!generationMethods.includes("generateContent")) continue;
|
|
54
|
+
|
|
55
|
+
const id = name.startsWith(GOOGLE_MODEL_PREFIX)
|
|
56
|
+
? name.slice(GOOGLE_MODEL_PREFIX.length)
|
|
57
|
+
: name;
|
|
58
|
+
if (!isValidModelDiscoveryModelId(id) || seen.has(id)) continue;
|
|
59
|
+
seen.add(id);
|
|
60
|
+
|
|
61
|
+
const inputTokenLimit = positiveSafeInteger(Reflect.get(raw, "inputTokenLimit"));
|
|
62
|
+
const outputTokenLimit = positiveSafeInteger(Reflect.get(raw, "outputTokenLimit"));
|
|
63
|
+
items.push({
|
|
64
|
+
id,
|
|
65
|
+
owned_by: "google",
|
|
66
|
+
...(inputTokenLimit !== undefined
|
|
67
|
+
? { context_length: inputTokenLimit, max_input_tokens: inputTokenLimit }
|
|
68
|
+
: {}),
|
|
69
|
+
...(outputTokenLimit !== undefined ? { max_output_tokens: outputTokenLimit } : {}),
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return { ok: true, items, rawCount: envelope.rows.length };
|
|
73
|
+
}
|
|
74
|
+
|
|
@@ -22,7 +22,15 @@ export function deriveOpenCodeGoSessionId(sessionLane: string): string {
|
|
|
22
22
|
return `ocx_${digest}`;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Add Go affinity only to the canonical fixed-key destination.
|
|
27
|
+
*
|
|
28
|
+
* Callers on the request path resolve the lane with `getOrAllocateRequestSessionLane`, which returns
|
|
29
|
+
* real conversation identity when the client supplied it and a per-request value otherwise, so a
|
|
30
|
+
* request reaching this helper from the proxy always carries a lane. The `!sessionLane` guard stays
|
|
31
|
+
* for direct callers that have no request context; it is not a per-request identity of its own, and
|
|
32
|
+
* minting one here would hand each retry a different value.
|
|
33
|
+
*/
|
|
26
34
|
export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
|
|
27
35
|
provider: T,
|
|
28
36
|
sessionLane: string | undefined,
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
* bodies and may omit `Retry-After` / `X-RateLimit-*`; when those headers are
|
|
8
8
|
* present they still take precedence. Distinct from the keyless desktop
|
|
9
9
|
* ~200 requests / 5h quota documented on `opencode-free`.
|
|
10
|
+
*
|
|
11
|
+
* The same module also owns the keyless free-tier admission explanation (#4121):
|
|
12
|
+
* Zen rejects a request that carries no `x-opencode-session` header with
|
|
13
|
+
* `MissingSessionID` / "OpenCode's free tier can only be used in OpenCode".
|
|
14
|
+
* opencodex does not synthesize that header — see {@link enrichOpenCodeZenFreeTierMessage}.
|
|
10
15
|
*/
|
|
11
16
|
import { validateClientRetryAfterHeader } from "../lib/retry-after";
|
|
12
17
|
import { registryEntryForProviderDestination } from "./registry";
|
|
@@ -100,3 +105,73 @@ export function enrichOpenCodeZenRateLimitMessage(
|
|
|
100
105
|
+ paceHint
|
|
101
106
|
);
|
|
102
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Zen's keyless free tier admits only OpenCode's own client. A request without an
|
|
111
|
+
* `x-opencode-session` header is refused with error type `MissingSessionID` and the
|
|
112
|
+
* message "OpenCode's free tier can only be used in OpenCode" (#4121).
|
|
113
|
+
*
|
|
114
|
+
* Presence of the header is the whole gate — any value clears it — so opencodex could
|
|
115
|
+
* pass by minting one. It does not. Fabricating a session identifier and a versioned
|
|
116
|
+
* `opencode/<version>` User-Agent is a claim to *be* the OpenCode client, and no upstream
|
|
117
|
+
* contract authorizes a third-party agent to make it; an HTTP 200 obtained that way is a
|
|
118
|
+
* bypassed admission check, not permission. Until OpenCode publishes a third-party
|
|
119
|
+
* integration path for this exact keyless tier, the supported route is the keyed
|
|
120
|
+
* `opencode-zen` provider.
|
|
121
|
+
*
|
|
122
|
+
* Two markers are matched because the two request surfaces expose different parts of the
|
|
123
|
+
* upstream envelope: the Responses path forwards the bounded raw body (which carries the
|
|
124
|
+
* `MissingSessionID` type), while the native Chat path forwards only the parsed message.
|
|
125
|
+
*/
|
|
126
|
+
const OPENCODE_ZEN_FREE_TIER_LOCK_IN = /MissingSessionID|free tier can only be used in OpenCode/i;
|
|
127
|
+
|
|
128
|
+
/** Idempotence marker — the appended guidance must not stack across enrichment layers. */
|
|
129
|
+
const FREE_TIER_ENRICHMENT_MARKER = "does not send a fabricated OpenCode session header";
|
|
130
|
+
|
|
131
|
+
/** True when an upstream error body is Zen's keyless free-tier admission refusal. */
|
|
132
|
+
export function isOpenCodeZenFreeTierLockIn(message: string, upstreamErrorType?: string | null): boolean {
|
|
133
|
+
if (upstreamErrorType && OPENCODE_ZEN_FREE_TIER_LOCK_IN.test(upstreamErrorType)) return true;
|
|
134
|
+
return OPENCODE_ZEN_FREE_TIER_LOCK_IN.test(message);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Replace a raw `MissingSessionID` passthrough with an explanation of the upstream
|
|
139
|
+
* restriction and the supported alternative. No-op for every other provider and every
|
|
140
|
+
* other error, and idempotent so layered enrichment cannot append it twice.
|
|
141
|
+
*/
|
|
142
|
+
export function enrichOpenCodeZenFreeTierMessage(
|
|
143
|
+
message: string,
|
|
144
|
+
opts: {
|
|
145
|
+
providerName?: string;
|
|
146
|
+
baseUrl?: string;
|
|
147
|
+
adapter?: string;
|
|
148
|
+
/** Upstream `error.type`, when the caller parsed one out of the envelope. */
|
|
149
|
+
upstreamErrorType?: string | null;
|
|
150
|
+
},
|
|
151
|
+
): string {
|
|
152
|
+
if (message.includes(FREE_TIER_ENRICHMENT_MARKER)) return message;
|
|
153
|
+
if (!isOpenCodeZenFreeTierLockIn(message, opts.upstreamErrorType)) return message;
|
|
154
|
+
if (!isOpenCodeZenRateLimitProvider(opts)) return message;
|
|
155
|
+
return (
|
|
156
|
+
`${message}`
|
|
157
|
+
+ " OpenCode Zen's keyless free tier admits only OpenCode's own client: it refuses any"
|
|
158
|
+
+ " request that arrives without an x-opencode-session header."
|
|
159
|
+
+ ` opencodex ${FREE_TIER_ENRICHMENT_MARKER}, because presenting itself as the OpenCode`
|
|
160
|
+
+ " client is a claim no upstream contract supports."
|
|
161
|
+
+ " Use the keyed opencode-zen provider with an OpenCode Zen API key"
|
|
162
|
+
+ " (https://opencode.ai/auth), or route this model through another provider."
|
|
163
|
+
+ " Upstream terms: https://opencode.ai/docs/zen/."
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Single entry point for Zen upstream-error guidance on the Responses wire: short-window
|
|
169
|
+
* rate limits first, then the keyless free-tier admission refusal. Each layer is a no-op
|
|
170
|
+
* outside its own case, so the composition is safe for every other upstream failure.
|
|
171
|
+
*/
|
|
172
|
+
export function enrichOpenCodeZenUpstreamMessage(
|
|
173
|
+
message: string,
|
|
174
|
+
opts: Parameters<typeof enrichOpenCodeZenRateLimitMessage>[1] & { upstreamErrorType?: string | null },
|
|
175
|
+
): string {
|
|
176
|
+
return enrichOpenCodeZenFreeTierMessage(enrichOpenCodeZenRateLimitMessage(message, opts), opts);
|
|
177
|
+
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -1398,6 +1398,21 @@ function parseClaudeLimit(value: unknown): { label: string; percent: number; res
|
|
|
1398
1398
|
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
1399
1399
|
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
1400
1400
|
|
|
1401
|
+
/**
|
|
1402
|
+
* Anthropic per-credential usage.
|
|
1403
|
+
*
|
|
1404
|
+
* This endpoint reports quota only. Its body carries `five_hour`, `seven_day`, the
|
|
1405
|
+
* model-scoped weekly buckets (`seven_day_fable`/`_opus`/`_sonnet`) and a `limits` array,
|
|
1406
|
+
* and **no subscription or tier field** — nor does the OAuth token response, which yields only
|
|
1407
|
+
* `account.uuid` and `account.email_address` (`src/oauth/anthropic.ts`). That is why
|
|
1408
|
+
* `OAuthAccountSummary.plan` is `null` for Anthropic rather than populated here (#3777); it is
|
|
1409
|
+
* a missing upstream field, not an unfinished mapping.
|
|
1410
|
+
*
|
|
1411
|
+
* A tier must not be inferred from what is here. Percentages are normalized per account, so a
|
|
1412
|
+
* Max x5 seat at 50% is byte-identical to a Max x20 seat at 50%, and the presence of a
|
|
1413
|
+
* model-scoped window tracks entitlement rather than seat size. Populate `plan` only when
|
|
1414
|
+
* upstream returns the tier itself.
|
|
1415
|
+
*/
|
|
1401
1416
|
async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
|
|
1402
1417
|
const joinable = anthropicUsageInflight.get(accessToken);
|
|
1403
1418
|
if (joinable) return joinable;
|
|
@@ -2891,7 +2906,11 @@ function keyQuotaReaderForProvider(name: string, provider: OcxProviderConfig): K
|
|
|
2891
2906
|
if (name === "deepseek" && isCanonicalDeepSeekBaseUrl(provider.baseUrl)) return fetchDeepSeekQuota;
|
|
2892
2907
|
if (name === "cline-pass" && isCanonicalClineBaseUrl(provider.baseUrl)) return fetchClineQuota;
|
|
2893
2908
|
if (isCanonicalOllamaCloudBaseUrl(provider.baseUrl ?? getProviderRegistryEntry(name)?.baseUrl)) return fetchOllamaCloudQuota;
|
|
2894
|
-
|
|
2909
|
+
// #4201: the Responses preset is the same domestic GLM Coding Plan subscription on the OpenAI
|
|
2910
|
+
// Responses wire, so it reads the same monitor endpoint. Eligibility stays a name list AND the
|
|
2911
|
+
// canonical-URL guard: the guard is what keeps BigModel's bare-key Authorization from reaching a
|
|
2912
|
+
// lookalike host, so a same-named custom destination still dispatches nothing.
|
|
2913
|
+
if (["zai", "glm", "glm-cn", "zhipu-bigmodel-coding", "zhipu-bigmodel-responses"].includes(name) && isCanonicalZaiBaseUrl(provider.baseUrl)) return fetchZaiQuota;
|
|
2895
2914
|
if (["minimax", "minimax-cn"].includes(name) && isCanonicalMinimaxBaseUrl(provider.baseUrl)) return fetchMinimaxQuota;
|
|
2896
2915
|
if (name === "moonshot" && isCanonicalMoonshotBaseUrl(provider.baseUrl)) return fetchMoonshotQuota;
|
|
2897
2916
|
if (name === "venice" && isCanonicalVeniceBaseUrl(provider.baseUrl)) return fetchVeniceQuota;
|
|
@@ -2640,6 +2640,23 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2640
2640
|
// Narrowed carry of #3641: the official Codex example declares a local static catalog,
|
|
2641
2641
|
// not an HTTP /models contract. Keep Responses separate from the Chat endpoint above.
|
|
2642
2642
|
// Source: https://docs.bigmodel.cn/cn/coding-plan/tool/codex.md (checked 2026-09-07).
|
|
2643
|
+
//
|
|
2644
|
+
// #4201 completes the roster. The `models.json` example on that Codex page is a *starter
|
|
2645
|
+
// catalog*, not the set of models the endpoint serves, and reading it as the latter is what
|
|
2646
|
+
// left Flash off a subscription that sells it. Three upstream pages say so directly, all
|
|
2647
|
+
// checked 2026-09-11:
|
|
2648
|
+
// - coding-plan/latest-model.md pins Codex to THIS baseUrl
|
|
2649
|
+
// (`Codex:https://open.bigmodel.cn/api/v1`) and opens with GLM Coding Plan supporting
|
|
2650
|
+
// GLM-5.3 and GLM-5.3-Flash for every tier (Max & Pro & Lite), then treats
|
|
2651
|
+
// `glm-5.3-flash` as an already-callable id in that same tool.
|
|
2652
|
+
// - coding-plan/overview.md: every plan supports GLM-5.3 and GLM-5.3-Flash, and calls to
|
|
2653
|
+
// GLM-5-Turbo are auto-switched to GLM-5.3-Flash. Turbo below is therefore an alias of
|
|
2654
|
+
// the very model this row omitted, which is the clearest statement that the endpoint
|
|
2655
|
+
// serves Flash: it was already serving it under another name.
|
|
2656
|
+
// - guide/models/vlm/glm-5.3-flash.md: native multimodal input, 1M context, and text
|
|
2657
|
+
// parameters explicitly "consistent with GLM-5.3".
|
|
2658
|
+
// No authenticated /models probe is implied by any of this, so `liveModels` and
|
|
2659
|
+
// `apiKeyValidation` below are deliberately unchanged.
|
|
2643
2660
|
{
|
|
2644
2661
|
id: "zhipu-bigmodel-responses",
|
|
2645
2662
|
label: "Zhipu AI — BigModel Coding Plan (Responses)",
|
|
@@ -2648,22 +2665,34 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
2648
2665
|
authKind: "key",
|
|
2649
2666
|
dashboardUrl: "https://bigmodel.cn/console/usercenter/apikeys",
|
|
2650
2667
|
defaultModel: "glm-5.3",
|
|
2651
|
-
models: ["glm-5.3", "glm-5-turbo"],
|
|
2668
|
+
models: ["glm-5.3", "glm-5.3-flash", "glm-5-turbo"],
|
|
2652
2669
|
liveModels: false,
|
|
2653
2670
|
// The local Codex catalog does not establish an authenticated HTTP /models contract.
|
|
2654
2671
|
apiKeyValidation: "unknown",
|
|
2655
2672
|
jawcodeBundle: "zai",
|
|
2656
2673
|
// A pre-existing same-named custom provider must retain its destination and key boundary.
|
|
2657
2674
|
preserveCustomDestination: true,
|
|
2658
|
-
|
|
2659
|
-
|
|
2675
|
+
// Flash tracks its 5.3 sibling on this row rather than the Chat row's 1_000_000. Both
|
|
2676
|
+
// models are documented as "1M", and this preset expresses that family's 1M the way
|
|
2677
|
+
// BigModel's own Codex declaration does. Splitting the two would leave one preset
|
|
2678
|
+
// claiming two different sizes for one documented window.
|
|
2679
|
+
modelContextWindows: { "glm-5.3": 1_048_576, "glm-5.3-flash": 1_048_576, "glm-5-turbo": 204_800 },
|
|
2680
|
+
// Flash is the only row here that can actually see an image. Its siblings are declared
|
|
2681
|
+
// text-only and get `image` back from the vision sidecar at catalog-build time; declaring
|
|
2682
|
+
// Flash text-only would route a native VLM's pictures through a describe-it-first detour
|
|
2683
|
+
// and hand the model prose about an image it could have read (same defect
|
|
2684
|
+
// ZAI_GLM_5X_SIDECAR_VISION_MODELS exists to prevent on the Chat rows).
|
|
2685
|
+
modelInputModalities: { "glm-5.3": ["text"], "glm-5.3-flash": ["text", "image"], "glm-5-turbo": ["text"] },
|
|
2660
2686
|
modelReasoningEfforts: {
|
|
2661
2687
|
"glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
|
|
2688
|
+
// Same three effective tiers: upstream documents Flash's text parameters as identical
|
|
2689
|
+
// to GLM-5.3, and the Codex effort table folds every inbound value into low/high/max.
|
|
2690
|
+
"glm-5.3-flash": ZAI_GLM_53_REASONING_EFFORTS,
|
|
2662
2691
|
// Explicitly empty: Turbo must not inherit the generic selectable effort ladder.
|
|
2663
2692
|
"glm-5-turbo": [],
|
|
2664
2693
|
},
|
|
2665
|
-
modelDefaultReasoningEfforts: { "glm-5.3": "max", "glm-5-turbo": "max" },
|
|
2666
|
-
modelSupportsReasoningSummaries: { "glm-5.3": true, "glm-5-turbo": true },
|
|
2694
|
+
modelDefaultReasoningEfforts: { "glm-5.3": "max", "glm-5.3-flash": "max", "glm-5-turbo": "max" },
|
|
2695
|
+
modelSupportsReasoningSummaries: { "glm-5.3": true, "glm-5.3-flash": true, "glm-5-turbo": true },
|
|
2667
2696
|
// Responses replay uses this provider-level flag, not the Chat-path model list.
|
|
2668
2697
|
preserveResponsesReasoningContent: true,
|
|
2669
2698
|
note: "Domestic BigModel Coding Plan Responses endpoint; static model roster",
|
|
@@ -3018,7 +3047,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
|
|
|
3018
3047
|
keyOptional: true,
|
|
3019
3048
|
featured: true,
|
|
3020
3049
|
liveModels: true,
|
|
3021
|
-
note: "No key needed
|
|
3050
|
+
note: "No key needed, but OpenCode now gates this tier to its own client: Zen refuses any request that arrives without an x-opencode-session header (error type MissingSessionID, \"OpenCode's free tier can only be used in OpenCode\"). opencodex does not mint that header or claim an OpenCode client identity, because no upstream contract authorizes a third-party agent to present itself as OpenCode. Until OpenCode publishes a third-party integration path for the keyless tier, use the keyed opencode-zen provider instead (https://opencode.ai/auth). Quota figures for when the tier admitted a request: OpenCode advertises about 200 Big Pickle/free-model requests per 5 hours, and the same Zen gateway can short-window rate-limit free models at roughly 15-20 requests/minute, and may return generic 429s without Retry-After (opencodex synthesizes backoff only when that header is omitted). Free models are discovered live from Zen. Data use: per OpenCode's Zen docs (https://opencode.ai/docs/zen/), prompts sent to free models may be retained and used for training/improvement — do not send confidential material through this provider.",
|
|
3022
3051
|
dashboardUrl: "https://opencode.ai",
|
|
3023
3052
|
staticHeaders: {
|
|
3024
3053
|
// Zen answers a bare runtime User-Agent (Bun/x.y.z) more aggressively than a client
|