@bitkyc08/opencodex 2.33.0 → 2.34.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 +3 -3
- package/gui/dist/assets/index-C4TMRloX.js +112 -0
- package/gui/dist/assets/index-DQ-Ie18T.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +40 -3
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +29 -6
- package/src/adapters/cursor/tool-definitions.ts +11 -2
- package/src/adapters/cursor/tool-result-normalize.ts +32 -1
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +189 -37
- package/src/adapters/google-antigravity-replay.ts +102 -23
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +115 -16
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +334 -24
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -24
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +32 -4
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +46 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +107 -9
- package/src/generated/compatibility-version.json +191 -139
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/types.ts +15 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +160 -68
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +77 -11
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +6 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +21 -0
- package/src/server/request-log.ts +41 -15
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +494 -114
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/types/config.ts +53 -0
- package/src/types/provider.ts +64 -0
- package/src/types/request.ts +11 -3
- package/src/types/tools.ts +7 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +3 -0
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-23-Lf7jR.js +0 -102
- package/gui/dist/assets/index-DxJMDyOr.css +0 -1
package/src/oauth/types.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
|
|
2
2
|
export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* How the account authenticated. Mirrors `KiroAuthType` in `./kiro-credentials`, restated here so
|
|
6
|
+
* the credential-store types do not depend on the SQLite import module.
|
|
7
|
+
*
|
|
8
|
+
* `aws_sso_oidc` covers AWS Builder ID, which never issues an account-scoped CodeWhisperer
|
|
9
|
+
* profile ARN; the adapter needs that distinction to tell a Builder ID account apart from a
|
|
10
|
+
* `kiro_desktop` account whose profile import merely failed.
|
|
11
|
+
*/
|
|
12
|
+
export type KiroCredentialAuthType = "kiro_desktop" | "aws_sso_oidc";
|
|
13
|
+
|
|
4
14
|
/** Account-scoped Kiro data required for refresh and request routing. */
|
|
5
15
|
export interface KiroOAuthMetadata {
|
|
6
16
|
profileArn?: string;
|
|
@@ -8,6 +18,11 @@ export interface KiroOAuthMetadata {
|
|
|
8
18
|
apiRegion?: string;
|
|
9
19
|
clientId?: string;
|
|
10
20
|
clientSecret?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Non-secret routing signal. Derived from the presence of a device-registration client pair, so
|
|
23
|
+
* it stays accurate even though `clientId`/`clientSecret` never leave the credential store.
|
|
24
|
+
*/
|
|
25
|
+
authType?: KiroCredentialAuthType;
|
|
11
26
|
}
|
|
12
27
|
|
|
13
28
|
export type OAuthCredentials = {
|
|
@@ -9,12 +9,44 @@ const COMMAND_CODE_MODEL_EFFORTS = {
|
|
|
9
9
|
efforts: ["high", "max"],
|
|
10
10
|
profileUrl: "https://commandcode.ai/models/deepseek-v4-flash",
|
|
11
11
|
},
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/*
|
|
13
|
+
* Three live routes that reached the catalog without an effort ladder (#2647).
|
|
14
|
+
* Without a row here the model advertises no efforts at all, so a client that
|
|
15
|
+
* sends one gets it stripped or rejected rather than honored.
|
|
16
|
+
*
|
|
17
|
+
* PROVENANCE, stated plainly: these three ladders are the reporter's
|
|
18
|
+
* (darwintree, #2647), recorded as reported and NOT independently verified.
|
|
19
|
+
* All three profileUrls return HTTP 200, but commandcode.ai renders these
|
|
20
|
+
* pages client-side and ships the ladder inside a serialized React payload
|
|
21
|
+
* whose `reasoningEfforts` array is EMPTY in the delivered HTML. There is no
|
|
22
|
+
* fetchable statement of these ladders to check them against.
|
|
23
|
+
*
|
|
24
|
+
* Do not assume the refresh path launders this. It does not:
|
|
25
|
+
* `parsedProfileEfforts` below matches prose of the form
|
|
26
|
+
* "Reasoning efforts ... are supported;", and `grep -c -i 'reasoning efforts'`
|
|
27
|
+
* against the live pages returns 0 — for these three AND for the older rows
|
|
28
|
+
* (deepseek-v4-pro, GLM-5.3, muse-spark-1.2 all measured 0 on 2026-08-27).
|
|
29
|
+
* So `refreshCommandCodeReasoningEfforts` returns undefined and the caller
|
|
30
|
+
* keeps whatever is written here, indefinitely. The self-correction mechanism
|
|
31
|
+
* is currently dead for EVERY row in this table, which is a pre-existing
|
|
32
|
+
* defect worth its own fix (teach the parser to read the embedded payload),
|
|
33
|
+
* not something these three rows introduced.
|
|
34
|
+
*
|
|
35
|
+
* The practical consequence: a wrong ladder here stays wrong until a human
|
|
36
|
+
* changes it. It degrades safely — an effort the upstream rejects surfaces as
|
|
37
|
+
* an error rather than silent corruption — but it does not self-heal.
|
|
38
|
+
*/
|
|
39
|
+
"deepseek/deepseek-v4-flash-vision-exp": {
|
|
40
|
+
efforts: ["high", "max"],
|
|
41
|
+
profileUrl: "https://commandcode.ai/models/deepseek-v4-flash-vision-exp",
|
|
42
|
+
},
|
|
43
|
+
"gpt-5.6-luna": {
|
|
44
|
+
efforts: ["low", "medium", "high", "xhigh", "max"],
|
|
45
|
+
profileUrl: "https://commandcode.ai/models/gpt-5-6-luna",
|
|
46
|
+
},
|
|
47
|
+
"google/gemini-3.7-flash": {
|
|
48
|
+
efforts: ["low", "medium", "high"],
|
|
49
|
+
profileUrl: "https://commandcode.ai/models/gemini-3-7-flash",
|
|
18
50
|
},
|
|
19
51
|
// Keys must match the EXACT upstream /provider/v1/models ids (GLM ships as
|
|
20
52
|
// `zai-org/GLM-5.3`, not `zai-org/glm-5.3`). The table doubles as the router's
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
2
3
|
|
|
3
4
|
export const DEFAULT_PROVIDER_CONTEXT_CAP = 350_000;
|
|
4
5
|
|
|
@@ -50,7 +51,7 @@ export function setProviderContextCap(config: OcxConfig, provider: string, enabl
|
|
|
50
51
|
delete next[provider];
|
|
51
52
|
}
|
|
52
53
|
if (Object.keys(next).length > 0) config.providerContextCaps = next;
|
|
53
|
-
else
|
|
54
|
+
else deleteConfigTopLevelKey(config, "providerContextCaps");
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
/**
|
|
@@ -71,12 +72,12 @@ export function setGlobalContextCapValue(config: OcxConfig, value: number, apply
|
|
|
71
72
|
/** Enable the cap for every named provider at the current value, or clear all caps. */
|
|
72
73
|
export function setAllProviderContextCaps(config: OcxConfig, providerNames: string[], enabled: boolean): void {
|
|
73
74
|
if (!enabled) {
|
|
74
|
-
|
|
75
|
+
deleteConfigTopLevelKey(config, "providerContextCaps");
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
const value = globalContextCapValue(config);
|
|
78
79
|
const next: Record<string, number> = {};
|
|
79
80
|
for (const name of providerNames) next[name] = value;
|
|
80
81
|
if (Object.keys(next).length > 0) config.providerContextCaps = next;
|
|
81
|
-
else
|
|
82
|
+
else deleteConfigTopLevelKey(config, "providerContextCaps");
|
|
82
83
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
2
|
+
|
|
3
|
+
export const MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
4
|
+
|
|
5
|
+
/** Ordered, most-specific-first built-in aliases. Updated with the provider registry. */
|
|
6
|
+
export const DEFAULT_MODEL_ALIASES: ReadonlyArray<{ match: RegExp; alias: string }> = [
|
|
7
|
+
{ match: /^claude-opus-5/, alias: "opus" },
|
|
8
|
+
{ match: /^claude-sonnet-5/, alias: "sonnet" },
|
|
9
|
+
{ match: /^claude-haiku/, alias: "haiku" },
|
|
10
|
+
{ match: /^gemini-3(?:\.\d+)?-pro/, alias: "g3p" },
|
|
11
|
+
{ match: /^gemini-3(?:\.\d+)?-flash/, alias: "g3f" },
|
|
12
|
+
{ match: /^deepseek-v4/, alias: "ds4" },
|
|
13
|
+
{ match: /^grok-4/, alias: "grok" },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export interface EffectiveModelAlias { alias: string; source: "user" | "builtin" }
|
|
17
|
+
|
|
18
|
+
function builtinRule(id: string): { match: RegExp; alias: string } | undefined {
|
|
19
|
+
const tail = id.slice(id.lastIndexOf("/") + 1);
|
|
20
|
+
return DEFAULT_MODEL_ALIASES.find(rule => rule.match.test(id) || rule.match.test(tail));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function defaultAliasesEnabled(config: Pick<OcxConfig, "defaultModelAliases">, provider: OcxProviderConfig): boolean {
|
|
24
|
+
return provider.defaultAliases ?? config.defaultModelAliases ?? false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function effectiveModelAliases(
|
|
28
|
+
config: Pick<OcxConfig, "defaultModelAliases">,
|
|
29
|
+
provider: OcxProviderConfig,
|
|
30
|
+
knownIds: Iterable<string>,
|
|
31
|
+
): Map<string, EffectiveModelAlias> {
|
|
32
|
+
const result = new Map<string, EffectiveModelAlias>();
|
|
33
|
+
for (const [id, alias] of Object.entries(provider.modelAliases ?? {})) {
|
|
34
|
+
if (typeof alias === "string" && MODEL_ALIAS_PATTERN.test(alias)) result.set(id, { alias, source: "user" });
|
|
35
|
+
}
|
|
36
|
+
if (!defaultAliasesEnabled(config, provider)) return result;
|
|
37
|
+
const claims = new Map<string, string[]>();
|
|
38
|
+
for (const id of knownIds) {
|
|
39
|
+
if (result.has(id)) continue;
|
|
40
|
+
const rule = builtinRule(id);
|
|
41
|
+
if (!rule) continue;
|
|
42
|
+
const key = rule.alias.toLowerCase();
|
|
43
|
+
claims.set(key, [...(claims.get(key) ?? []), id]);
|
|
44
|
+
}
|
|
45
|
+
for (const [alias, ids] of claims) {
|
|
46
|
+
if (ids.length !== 1) continue;
|
|
47
|
+
const id = ids[0]!;
|
|
48
|
+
// Catalog drift must skip, never shadow, a native id or explicit user alias.
|
|
49
|
+
if ([...knownIds].some(candidate => candidate.toLowerCase() === alias)) continue;
|
|
50
|
+
if ([...result.values()].some(value => value.alias.toLowerCase() === alias)) continue;
|
|
51
|
+
result.set(id, { alias: builtinRule(id)!.alias, source: "builtin" });
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resolveModelAlias(
|
|
57
|
+
config: Pick<OcxConfig, "defaultModelAliases">,
|
|
58
|
+
provider: OcxProviderConfig,
|
|
59
|
+
knownIds: Iterable<string>,
|
|
60
|
+
requested: string,
|
|
61
|
+
): string | undefined {
|
|
62
|
+
const needle = requested.toLowerCase();
|
|
63
|
+
return [...effectiveModelAliases(config, provider, knownIds)]
|
|
64
|
+
.find(([, value]) => value.alias.toLowerCase() === needle)?.[0];
|
|
65
|
+
}
|
package/src/providers/derive.ts
CHANGED
|
@@ -390,6 +390,32 @@ function serviceTierModelDefaultsFor(
|
|
|
390
390
|
: undefined;
|
|
391
391
|
}
|
|
392
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Materialize the registry's verbosity opt-out into the provider config at seed/enrich time.
|
|
395
|
+
*
|
|
396
|
+
* The catalog hint pass must not read PROVIDER_REGISTRY: a gather flight captures its registry
|
|
397
|
+
* authority up front and forbids any later read, so consulting the registry per model turned
|
|
398
|
+
* every hint pass into a post-lookup read and dropped a custom-destination flight's own
|
|
399
|
+
* discovery result (tests/codex-gather-authority.test.ts).
|
|
400
|
+
*
|
|
401
|
+
* Registry values go in first so an explicit user entry still wins, matching
|
|
402
|
+
* `applyReasoningSummaryDefaults`. `supportsVerbosity` is the provider-wide default, expanded
|
|
403
|
+
* across the seeded model list so an id discovered later still inherits it through the same
|
|
404
|
+
* Record the hint pass already reads.
|
|
405
|
+
*/
|
|
406
|
+
function applyVerbosityDefaults(prov: OcxProviderConfig, entry: ProviderRegistryEntry | undefined): void {
|
|
407
|
+
if (!entry) return;
|
|
408
|
+
const perModel = entry.modelSupportsVerbosity;
|
|
409
|
+
if (!perModel && entry.supportsVerbosity === undefined) return;
|
|
410
|
+
prov.modelSupportsVerbosity = {
|
|
411
|
+
...(perModel ?? {}),
|
|
412
|
+
...(prov.modelSupportsVerbosity ?? {}),
|
|
413
|
+
};
|
|
414
|
+
if (entry.supportsVerbosity !== undefined) {
|
|
415
|
+
prov.supportsVerbosity ??= entry.supportsVerbosity;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
393
419
|
/**
|
|
394
420
|
* Last-resort enrichment for a provider whose NAME matches no registry id.
|
|
395
421
|
*
|
|
@@ -426,6 +452,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
426
452
|
// destinations, so a templated or overridable base URL cannot be claimed by it.
|
|
427
453
|
enrichReasoningSummariesByDestination(prov);
|
|
428
454
|
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(registryEntryForProviderDestination(prov), prov));
|
|
455
|
+
applyVerbosityDefaults(prov, registryEntryForProviderDestination(prov));
|
|
429
456
|
return;
|
|
430
457
|
}
|
|
431
458
|
const explicitDirectReasoning: DirectReasoningEffortOverrides = {
|
|
@@ -489,6 +516,7 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
489
516
|
if (prov.preserveResponsesReasoningContent === undefined && entry.preserveResponsesReasoningContent !== undefined) prov.preserveResponsesReasoningContent = entry.preserveResponsesReasoningContent;
|
|
490
517
|
applyReasoningSummaryDefaults(prov, entry.modelSupportsReasoningSummaries);
|
|
491
518
|
applyServiceTierModelDefaults(prov, serviceTierModelDefaultsFor(entry, prov));
|
|
519
|
+
applyVerbosityDefaults(prov, entry);
|
|
492
520
|
// Registry-only repair policy (#938): fill only when the runtime provider has
|
|
493
521
|
// no explicit policy, and deep-clone so saved/user values never alias the
|
|
494
522
|
// registry constant.
|
|
@@ -246,6 +246,7 @@ export function tierObservationContext(
|
|
|
246
246
|
policy: ResolvedFastPolicy,
|
|
247
247
|
fastMode: boolean | undefined,
|
|
248
248
|
callerTier: string | undefined,
|
|
249
|
+
responseTierAuthoritative?: boolean,
|
|
249
250
|
): TierObservationContext {
|
|
250
251
|
return {
|
|
251
252
|
capability: policy.capability,
|
|
@@ -253,6 +254,7 @@ export function tierObservationContext(
|
|
|
253
254
|
fastWire: policy.fastWire,
|
|
254
255
|
demandDecision: fastMode === true ? "force-fast" : fastMode === false ? "force-default" : "inherit",
|
|
255
256
|
...(callerTier !== undefined ? { callerTier } : {}),
|
|
257
|
+
...(responseTierAuthoritative !== undefined ? { responseTierAuthoritative } : {}),
|
|
256
258
|
};
|
|
257
259
|
}
|
|
258
260
|
|
|
@@ -344,7 +346,11 @@ export function createAdapterTierMetadata(
|
|
|
344
346
|
|
|
345
347
|
const responseCanConfirmFast = effectiveFastRequested
|
|
346
348
|
&& context.eligibility === "eligible"
|
|
347
|
-
&& wireValue !== null
|
|
349
|
+
&& wireValue !== null
|
|
350
|
+
// A destination whose echo is not authoritative can neither confirm nor deny Fast. The
|
|
351
|
+
// ChatGPT-internal Codex backend echoes "default" on priority-scheduled turns, so believing
|
|
352
|
+
// it reported every Fast request as `response-declined` (#2558).
|
|
353
|
+
&& context.responseTierAuthoritative !== false;
|
|
348
354
|
return {
|
|
349
355
|
outcome,
|
|
350
356
|
observeResponseServiceTier(value: unknown) {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shipped per-provider "latest/core" model presets (#2465).
|
|
3
|
+
*
|
|
4
|
+
* Adding a high-volume provider exposes its entire catalog to the Codex picker on day one:
|
|
5
|
+
* openrouter ships 400+ rows, an Anthropic provider ships every historical snapshot. The useful
|
|
6
|
+
* set is a handful of current flagships, so a newly added provider seeds from a curated preset
|
|
7
|
+
* and "everything" stays one click away.
|
|
8
|
+
*
|
|
9
|
+
* Rules are ID PATTERNS rather than literal id lists so a vendor snapshot suffix does not stale
|
|
10
|
+
* the preset between releases: `claude-opus-5-20260814` and `claude-opus-5` both match one rule.
|
|
11
|
+
*
|
|
12
|
+
* The preset is a SEED, not a lock. Materialization evaluates these rules against the provider's
|
|
13
|
+
* current catalog and stores CONCRETE ids in `selectedModels`, so the visibility hot path
|
|
14
|
+
* (`filterCatalogVisibleModels`) never learns about patterns and an older binary still sees a
|
|
15
|
+
* plain allowlist. Design: devlog/_plan/260824_model_ux_aliases_and_defaults/030_default_preset.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { OcxProviderConfig } from "../types";
|
|
19
|
+
|
|
20
|
+
export interface ModelPresetRule {
|
|
21
|
+
readonly pattern: RegExp;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ModelPreset {
|
|
25
|
+
/** Bumped whenever this provider's rules change; drives upgrade reconciliation. */
|
|
26
|
+
readonly version: number;
|
|
27
|
+
readonly rules: readonly ModelPresetRule[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Curated on the same release train as the provider registry. A provider WITHOUT an entry has no
|
|
32
|
+
* preset and behaves exactly as today (mode "all").
|
|
33
|
+
*
|
|
34
|
+
* Deliberately limited to high-volume catalogs. A provider that already ships a short curated
|
|
35
|
+
* list gains nothing from a preset and would only add a marker to reconcile.
|
|
36
|
+
*/
|
|
37
|
+
export const MODEL_PRESETS: Readonly<Record<string, ModelPreset>> = Object.freeze({
|
|
38
|
+
openrouter: {
|
|
39
|
+
version: 1,
|
|
40
|
+
rules: [
|
|
41
|
+
{ pattern: /^anthropic\/claude-(opus|sonnet|haiku)-[45]/ },
|
|
42
|
+
{ pattern: /^google\/gemini-3(\.\d+)?-(pro|flash)/ },
|
|
43
|
+
{ pattern: /^openai\/gpt-5(\.\d+)?/ },
|
|
44
|
+
{ pattern: /^deepseek\/deepseek-(v4|r2)/ },
|
|
45
|
+
{ pattern: /^x-ai\/grok-4(\.\d+)?/ },
|
|
46
|
+
{ pattern: /^moonshotai\/kimi-k[23]/ },
|
|
47
|
+
{ pattern: /^z-ai\/glm-[45]/ },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
anthropic: {
|
|
51
|
+
version: 1,
|
|
52
|
+
rules: [
|
|
53
|
+
{ pattern: /^claude-opus-[45]/ },
|
|
54
|
+
{ pattern: /^claude-sonnet-[45]/ },
|
|
55
|
+
{ pattern: /^claude-haiku-[45]/ },
|
|
56
|
+
{ pattern: /^claude-fable-5/ },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
"anthropic-apikey": {
|
|
60
|
+
version: 1,
|
|
61
|
+
rules: [
|
|
62
|
+
{ pattern: /^claude-opus-[45]/ },
|
|
63
|
+
{ pattern: /^claude-sonnet-[45]/ },
|
|
64
|
+
{ pattern: /^claude-haiku-[45]/ },
|
|
65
|
+
{ pattern: /^claude-fable-5/ },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
/** The preset for a provider, or undefined when none is shipped. */
|
|
71
|
+
export function modelPresetFor(providerName: string): ModelPreset | undefined {
|
|
72
|
+
return MODEL_PRESETS[providerName];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** True when a provider has a shipped preset at all. */
|
|
76
|
+
export function hasModelPreset(providerName: string): boolean {
|
|
77
|
+
return modelPresetFor(providerName) !== undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Evaluate a provider's rules against concrete catalog ids.
|
|
82
|
+
*
|
|
83
|
+
* Input order is preserved so a caller can show the preset in the same order the picker does.
|
|
84
|
+
* Duplicates are collapsed; a model matching several rules is selected once.
|
|
85
|
+
*/
|
|
86
|
+
export function materializeModelPreset(
|
|
87
|
+
providerName: string,
|
|
88
|
+
catalogModelIds: Iterable<string>,
|
|
89
|
+
): string[] {
|
|
90
|
+
const preset = modelPresetFor(providerName);
|
|
91
|
+
if (!preset) return [];
|
|
92
|
+
const seen = new Set<string>();
|
|
93
|
+
const out: string[] = [];
|
|
94
|
+
for (const id of catalogModelIds) {
|
|
95
|
+
if (seen.has(id)) continue;
|
|
96
|
+
// `RegExp.test` on a shared literal is safe here because no rule uses the `g` flag; a
|
|
97
|
+
// sticky/global pattern would carry lastIndex between calls and skip rows.
|
|
98
|
+
if (preset.rules.some(rule => rule.pattern.test(id))) {
|
|
99
|
+
seen.add(id);
|
|
100
|
+
out.push(id);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Flip a provider out of preset mode when the user edits its selection (#2465).
|
|
109
|
+
*
|
|
110
|
+
* No-op unless the provider is currently in preset mode: "all" has no marker to keep, and
|
|
111
|
+
* "custom" is already terminal. The applied version is retained so the GUI can still offer
|
|
112
|
+
* "a newer preset is available" without losing what the user chose.
|
|
113
|
+
*/
|
|
114
|
+
export function markModelPresetDiverged(provider: OcxProviderConfig): void {
|
|
115
|
+
const marker = provider.modelPreset;
|
|
116
|
+
if (marker?.mode !== "preset") return;
|
|
117
|
+
provider.modelPreset = { ...marker, mode: "custom" };
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { routedSlug } from "./slug-codec";
|
|
3
|
+
|
|
4
|
+
export const MODEL_REMOVAL_GRACE_FETCHES = 3;
|
|
5
|
+
export const MAX_KNOWN_MODELS_PER_PROVIDER = 2_000;
|
|
6
|
+
export const MAX_RECENT_ARRIVALS_PER_PROVIDER = 50;
|
|
7
|
+
|
|
8
|
+
export type KnownModelBaseline = NonNullable<NonNullable<OcxConfig["modelDiscovery"]>["knownModels"]>[string];
|
|
9
|
+
export type NewModelPolicy = "on" | "off";
|
|
10
|
+
|
|
11
|
+
export interface NewModelPolicyResult {
|
|
12
|
+
newIds: string[];
|
|
13
|
+
nextBaseline: KnownModelBaseline;
|
|
14
|
+
slugsToDisable: string[];
|
|
15
|
+
arrivals: Array<{ id: string; at: string }>;
|
|
16
|
+
overflow: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Whether a transition actually changes persisted state.
|
|
21
|
+
*
|
|
22
|
+
* `updatedAt` moves on every successful fetch, so comparing whole baselines would report a
|
|
23
|
+
* change every time and rewrite config.json on every catalog convergence — a write amplification
|
|
24
|
+
* that also churns the config generation other writers revalidate against. Only the fields that
|
|
25
|
+
* carry meaning are compared.
|
|
26
|
+
*/
|
|
27
|
+
function baselineDiffers(prior: KnownModelBaseline | undefined, next: KnownModelBaseline): boolean {
|
|
28
|
+
if (!prior) return true;
|
|
29
|
+
const sameList = (a: readonly string[], b: readonly string[]) =>
|
|
30
|
+
a.length === b.length && a.every((value, index) => value === b[index]);
|
|
31
|
+
if (!sameList(prior.ids, next.ids) || !sameList(prior.removed, next.removed)) return true;
|
|
32
|
+
const priorMissing = prior.missing ?? {};
|
|
33
|
+
const nextMissing = next.missing ?? {};
|
|
34
|
+
const keys = new Set([...Object.keys(priorMissing), ...Object.keys(nextMissing)]);
|
|
35
|
+
for (const key of keys) if (priorMissing[key] !== nextMissing[key]) return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Pure successful-discovery transition. An absent baseline bootstraps without hiding anything. */
|
|
40
|
+
export function applyNewModelPolicy(options: {
|
|
41
|
+
provider: string;
|
|
42
|
+
discoveredIds: Iterable<string>;
|
|
43
|
+
baseline?: KnownModelBaseline;
|
|
44
|
+
policy: NewModelPolicy;
|
|
45
|
+
hasSelectedModels?: boolean;
|
|
46
|
+
now: string;
|
|
47
|
+
}): NewModelPolicyResult {
|
|
48
|
+
const discovered = [...new Set(options.discoveredIds)].sort();
|
|
49
|
+
const prior = options.baseline;
|
|
50
|
+
if (!prior) {
|
|
51
|
+
return {
|
|
52
|
+
newIds: [],
|
|
53
|
+
nextBaseline: { ids: discovered, removed: [], updatedAt: options.now },
|
|
54
|
+
slugsToDisable: [], arrivals: [],
|
|
55
|
+
overflow: discovered.length > MAX_KNOWN_MODELS_PER_PROVIDER,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const active = new Set(prior.ids);
|
|
59
|
+
const removed = new Set(prior.removed);
|
|
60
|
+
const seen = new Set(discovered);
|
|
61
|
+
const newIds = discovered.filter(id => !active.has(id) && !removed.has(id));
|
|
62
|
+
const missing: Record<string, number> = {};
|
|
63
|
+
for (const id of active) {
|
|
64
|
+
if (seen.has(id)) continue;
|
|
65
|
+
const count = (prior.missing?.[id] ?? 0) + 1;
|
|
66
|
+
if (count >= MODEL_REMOVAL_GRACE_FETCHES) {
|
|
67
|
+
active.delete(id);
|
|
68
|
+
removed.add(id);
|
|
69
|
+
} else missing[id] = count;
|
|
70
|
+
}
|
|
71
|
+
for (const id of discovered) active.add(id);
|
|
72
|
+
const unionSize = active.size + removed.size;
|
|
73
|
+
const overflow = unionSize > MAX_KNOWN_MODELS_PER_PROVIDER;
|
|
74
|
+
const arrivals = newIds.map(id => ({ id, at: options.now }));
|
|
75
|
+
return {
|
|
76
|
+
newIds,
|
|
77
|
+
nextBaseline: {
|
|
78
|
+
ids: [...active].sort(), removed: [...removed].sort(), updatedAt: options.now,
|
|
79
|
+
...(Object.keys(missing).length ? { missing } : {}),
|
|
80
|
+
},
|
|
81
|
+
// A non-empty preset/custom allowlist already excludes arrivals. This explicit no-op is
|
|
82
|
+
// deliberate: preset mode owns which matching flagships arrive on.
|
|
83
|
+
slugsToDisable: !overflow && options.policy === "off" && !options.hasSelectedModels
|
|
84
|
+
? newIds.map(id => routedSlug(options.provider, id)) : [],
|
|
85
|
+
arrivals,
|
|
86
|
+
overflow,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function effectiveNewModelPolicy(config: OcxConfig, provider: string): NewModelPolicy {
|
|
91
|
+
const local = config.providers[provider]?.newModelPolicy;
|
|
92
|
+
if (local === "on" || local === "off") return local;
|
|
93
|
+
return config.modelDiscovery?.newModelPolicy ?? "on";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Apply authoritative provider rows to a mutable convergence copy; degraded providers are omitted. */
|
|
97
|
+
export function reconcileSuccessfulModelDiscoveries(options: {
|
|
98
|
+
config: OcxConfig;
|
|
99
|
+
models: Iterable<{ provider: string; id: string; custom?: boolean }>;
|
|
100
|
+
authoritativeProviders: Iterable<string>;
|
|
101
|
+
now: string;
|
|
102
|
+
}): boolean {
|
|
103
|
+
const byProvider = new Map<string, string[]>();
|
|
104
|
+
for (const model of options.models) {
|
|
105
|
+
if (model.custom) continue;
|
|
106
|
+
const ids = byProvider.get(model.provider) ?? [];
|
|
107
|
+
ids.push(model.id); byProvider.set(model.provider, ids);
|
|
108
|
+
}
|
|
109
|
+
let changed = false;
|
|
110
|
+
for (const provider of options.authoritativeProviders) {
|
|
111
|
+
const configured = options.config.providers[provider];
|
|
112
|
+
if (!configured || configured.liveModels === false) continue;
|
|
113
|
+
const discoveredIds = byProvider.get(provider) ?? [];
|
|
114
|
+
const discovery = options.config.modelDiscovery ??= {};
|
|
115
|
+
const known = discovery.knownModels ??= {};
|
|
116
|
+
const result = applyNewModelPolicy({
|
|
117
|
+
provider, discoveredIds, baseline: known[provider],
|
|
118
|
+
policy: effectiveNewModelPolicy(options.config, provider),
|
|
119
|
+
hasSelectedModels: (configured.selectedModels?.length ?? 0) > 0,
|
|
120
|
+
now: options.now,
|
|
121
|
+
});
|
|
122
|
+
if (result.overflow) continue;
|
|
123
|
+
const priorBaseline = known[provider];
|
|
124
|
+
const baselineChanged = baselineDiffers(priorBaseline, result.nextBaseline);
|
|
125
|
+
known[provider] = result.nextBaseline;
|
|
126
|
+
// Keep the previous timestamp when nothing else moved, so a steady-state roster does not
|
|
127
|
+
// make the baseline look dirty on the next comparison either.
|
|
128
|
+
if (!baselineChanged && priorBaseline) known[provider] = priorBaseline;
|
|
129
|
+
if (result.slugsToDisable.length) {
|
|
130
|
+
const disabled = options.config.disabledModels ??= [];
|
|
131
|
+
for (const slug of result.slugsToDisable) {
|
|
132
|
+
if (disabled.includes(slug)) continue;
|
|
133
|
+
disabled.push(slug);
|
|
134
|
+
changed = true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (result.arrivals.length) {
|
|
138
|
+
const recent = discovery.recentArrivals ??= {};
|
|
139
|
+
recent[provider] = [...(recent[provider] ?? []), ...result.arrivals]
|
|
140
|
+
.slice(-MAX_RECENT_ARRIVALS_PER_PROVIDER);
|
|
141
|
+
changed = true;
|
|
142
|
+
}
|
|
143
|
+
if (baselineChanged) changed = true;
|
|
144
|
+
}
|
|
145
|
+
return changed;
|
|
146
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
2
3
|
|
|
3
4
|
export interface ProviderRewriteResult {
|
|
4
5
|
/** Number of references re-pointed. */
|
|
@@ -174,6 +175,6 @@ export function dropProviderCustomModels(config: OcxConfig, provider: string): n
|
|
|
174
175
|
// `[]`, so the `customModels` field is absent either way. Only that field —
|
|
175
176
|
// the `customModelCatalogMigration` marker is deliberately left in place.
|
|
176
177
|
if (kept.length > 0) config.customModels = kept;
|
|
177
|
-
else
|
|
178
|
+
else deleteConfigTopLevelKey(config, "customModels");
|
|
178
179
|
return existing.length - kept.length;
|
|
179
180
|
}
|
package/src/providers/quota.ts
CHANGED
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
effectiveCodexAuthAccountId,
|
|
4
4
|
fetchMainAccountInfoSnapshot,
|
|
5
5
|
listCodexAuthAccountsSnapshot,
|
|
6
|
+
withSparkVisibility,
|
|
6
7
|
} from "../codex/auth-api";
|
|
8
|
+
import type { StoredAccountQuota } from "../codex/quota";
|
|
7
9
|
import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
|
|
8
10
|
import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
|
|
9
11
|
import { codexPlanKey } from "../codex/plan";
|
|
@@ -166,6 +168,27 @@ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
|
|
|
166
168
|
};
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
function providerQuotaFromCodexQuota(
|
|
172
|
+
quota: StoredAccountQuota | Omit<StoredAccountQuota, "updatedAt"> | null | undefined,
|
|
173
|
+
): CodexCapacityQuota | null {
|
|
174
|
+
if (!quota) return null;
|
|
175
|
+
// Every Codex-sourced provider report funnels through here — the pooled path via
|
|
176
|
+
// listCodexAuthAccountsSnapshot and the `direct` path via fetchMainAccountInfoSnapshot, which
|
|
177
|
+
// never touches the Codex Auth DTO. Applying the Spark preference at this one point is what
|
|
178
|
+
// stops the row surviving on /api/provider-quotas after the operator switched it off.
|
|
179
|
+
quota = withSparkVisibility(quota ?? null) ?? quota;
|
|
180
|
+
return {
|
|
181
|
+
...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}),
|
|
182
|
+
...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}),
|
|
183
|
+
...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}),
|
|
184
|
+
...(quota.weeklyResetAt !== undefined ? { weeklyResetAt: quota.weeklyResetAt } : {}),
|
|
185
|
+
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
|
|
186
|
+
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
|
|
187
|
+
...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}),
|
|
188
|
+
updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
169
192
|
/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
|
|
170
193
|
function cacheKeyWithAggregationState(
|
|
171
194
|
config: OcxConfig,
|
|
@@ -183,7 +206,7 @@ function cacheKeyWithAggregationState(
|
|
|
183
206
|
plan: codexPlanKey(account.plan) ?? null,
|
|
184
207
|
paused: account.paused,
|
|
185
208
|
needsReauth: account.needsReauth === true,
|
|
186
|
-
quota: quotaSignatureValue(account.quota
|
|
209
|
+
quota: quotaSignatureValue(providerQuotaFromCodexQuota(account.quota)),
|
|
187
210
|
}));
|
|
188
211
|
const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
|
|
189
212
|
const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
|
|
@@ -1103,9 +1126,8 @@ async function fetchChatGptForwardQuota(
|
|
|
1103
1126
|
): Promise<ProviderQuotaReport | null> {
|
|
1104
1127
|
if (providerCodexAccountMode(provider, providerConfig) === "direct") {
|
|
1105
1128
|
const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
|
|
1106
|
-
const quota = snapshot.info.quota
|
|
1107
|
-
|
|
1108
|
-
: null;
|
|
1129
|
+
const quota = providerQuotaFromCodexQuota(snapshot.info.quota);
|
|
1130
|
+
if (quota) quota.updatedAt = Date.now();
|
|
1109
1131
|
return quota
|
|
1110
1132
|
? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
|
|
1111
1133
|
: null;
|
|
@@ -1113,10 +1135,14 @@ async function fetchChatGptForwardQuota(
|
|
|
1113
1135
|
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
|
|
1114
1136
|
const accounts = snapshot.accounts;
|
|
1115
1137
|
const activeId = effectiveCodexAuthAccountId(config);
|
|
1116
|
-
const capacityAccounts = accounts.map(account => ({
|
|
1138
|
+
const capacityAccounts = accounts.map(account => ({
|
|
1139
|
+
...account,
|
|
1140
|
+
active: account.id === activeId,
|
|
1141
|
+
quota: providerQuotaFromCodexQuota(account.quota),
|
|
1142
|
+
}));
|
|
1117
1143
|
const active = capacityAccounts.find(account => account.active)
|
|
1118
|
-
??
|
|
1119
|
-
??
|
|
1144
|
+
?? capacityAccounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
|
|
1145
|
+
?? capacityAccounts[0];
|
|
1120
1146
|
const now = Date.now();
|
|
1121
1147
|
const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
|
|
1122
1148
|
if (capacity.aggregation && capacity.quota) {
|
|
@@ -1278,6 +1304,24 @@ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number
|
|
|
1278
1304
|
return { percent, resetAt };
|
|
1279
1305
|
}
|
|
1280
1306
|
|
|
1307
|
+
function parseClaudeLimit(value: unknown): { label: string; percent: number; resetAt?: number } | null {
|
|
1308
|
+
const rec = asRecord(value);
|
|
1309
|
+
if (!rec) return null;
|
|
1310
|
+
const percent = normalizePercent(rec.percent);
|
|
1311
|
+
if (percent === undefined) return null;
|
|
1312
|
+
const scope = asRecord(rec.scope);
|
|
1313
|
+
const model = asRecord(scope?.model);
|
|
1314
|
+
const rawLabel = String(model?.display_name ?? "").trim();
|
|
1315
|
+
if (!rawLabel) return null;
|
|
1316
|
+
const lowerLabel = rawLabel.toLowerCase();
|
|
1317
|
+
const label = lowerLabel.includes("fable") ? "Fable"
|
|
1318
|
+
: lowerLabel.includes("opus") ? "Opus"
|
|
1319
|
+
: lowerLabel.includes("sonnet") ? "Sonnet"
|
|
1320
|
+
: rawLabel;
|
|
1321
|
+
const resetAt = normalizeResetAt(rec.resets_at);
|
|
1322
|
+
return { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1281
1325
|
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
1282
1326
|
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
1283
1327
|
|
|
@@ -1301,11 +1345,25 @@ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQu
|
|
|
1301
1345
|
if (!body) return null;
|
|
1302
1346
|
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
1303
1347
|
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
1348
|
+
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
1304
1349
|
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
1305
1350
|
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
1306
1351
|
const customWindows: ProviderQuotaWindow[] = [];
|
|
1352
|
+
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
1307
1353
|
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
1308
1354
|
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
1355
|
+
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
1356
|
+
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
1357
|
+
for (const rawLimit of limits) {
|
|
1358
|
+
const limitRecord = asRecord(rawLimit);
|
|
1359
|
+
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
1360
|
+
// buckets above; only model-scoped weekly limits add a third window.
|
|
1361
|
+
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
1362
|
+
const limit = parseClaudeLimit(rawLimit);
|
|
1363
|
+
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
1364
|
+
knownLabels.add(limit.label.toLowerCase());
|
|
1365
|
+
customWindows.push(limit);
|
|
1366
|
+
}
|
|
1309
1367
|
const quota: ProviderQuota = {
|
|
1310
1368
|
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
1311
1369
|
// rows: report it in the canonical fields so the dashboard renders it with the standard
|