@yansigit/opencodex 2.33.1 → 2.35.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-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -3
- 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 +9 -9
- 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-shell.ts +103 -6
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +74 -39
- package/src/adapters/cursor/request-builder.ts +32 -10
- package/src/adapters/cursor/tool-definitions.ts +13 -10
- package/src/adapters/cursor/tool-result-normalize.ts +35 -3
- package/src/adapters/cursor/types.ts +16 -4
- package/src/adapters/cursor.ts +97 -70
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +156 -65
- 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/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- 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 +27 -7
- package/src/cli/account-extended.ts +10 -3
- 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/index.ts +5 -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/telemetry-commands.ts +25 -0
- 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 +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- 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 -37
- 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 +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- 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/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +229 -153
- 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/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +54 -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/login-cli.ts +19 -20
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- 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 +73 -14
- 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 +23 -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/effort-policy.ts +16 -0
- package/src/server/index.ts +152 -80
- 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/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/provider-routes.ts +137 -3
- 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 +46 -20
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +571 -137
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- 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/ws-upstream.ts +35 -5
- 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 +15 -4
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +77 -0
- package/src/types/request.ts +16 -10
- package/src/types/tools.ts +20 -25
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +17 -7
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-CIDo4y4k.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
- package/src/server/aistudio-ws-hub.ts +0 -295
|
@@ -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. */
|
|
@@ -181,6 +182,6 @@ export function dropProviderCustomModels(config: OcxConfig, provider: string): n
|
|
|
181
182
|
// `[]`, so the `customModels` field is absent either way. Only that field —
|
|
182
183
|
// the `customModelCatalogMigration` marker is deliberately left in place.
|
|
183
184
|
if (kept.length > 0) config.customModels = kept;
|
|
184
|
-
else
|
|
185
|
+
else deleteConfigTopLevelKey(config, "customModels");
|
|
185
186
|
return existing.length - kept.length;
|
|
186
187
|
}
|
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";
|
|
@@ -16,7 +18,6 @@ import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "./xai-transport
|
|
|
16
18
|
import { getProviderRegistryEntry, providerCodexAccountMode, registryEntryForProviderDestination } from "./registry";
|
|
17
19
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
18
20
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
|
|
19
|
-
import { globalAiStudioRelayHub } from "../server/aistudio-ws-hub";
|
|
20
21
|
import {
|
|
21
22
|
captureConfigGeneration,
|
|
22
23
|
sweepExpiredOnWrite,
|
|
@@ -174,6 +175,27 @@ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
|
|
|
174
175
|
};
|
|
175
176
|
}
|
|
176
177
|
|
|
178
|
+
function providerQuotaFromCodexQuota(
|
|
179
|
+
quota: StoredAccountQuota | Omit<StoredAccountQuota, "updatedAt"> | null | undefined,
|
|
180
|
+
): CodexCapacityQuota | null {
|
|
181
|
+
if (!quota) return null;
|
|
182
|
+
// Every Codex-sourced provider report funnels through here — the pooled path via
|
|
183
|
+
// listCodexAuthAccountsSnapshot and the `direct` path via fetchMainAccountInfoSnapshot, which
|
|
184
|
+
// never touches the Codex Auth DTO. Applying the Spark preference at this one point is what
|
|
185
|
+
// stops the row surviving on /api/provider-quotas after the operator switched it off.
|
|
186
|
+
quota = withSparkVisibility(quota ?? null) ?? quota;
|
|
187
|
+
return {
|
|
188
|
+
...(quota.shortPercent !== undefined ? { fiveHourPercent: quota.shortPercent } : {}),
|
|
189
|
+
...(quota.shortResetAt !== undefined ? { fiveHourResetAt: quota.shortResetAt } : {}),
|
|
190
|
+
...(quota.weeklyPercent !== undefined ? { weeklyPercent: quota.weeklyPercent } : {}),
|
|
191
|
+
...(quota.weeklyResetAt !== undefined ? { weeklyResetAt: quota.weeklyResetAt } : {}),
|
|
192
|
+
...(quota.monthlyPercent !== undefined ? { monthlyPercent: quota.monthlyPercent } : {}),
|
|
193
|
+
...(quota.monthlyResetAt !== undefined ? { monthlyResetAt: quota.monthlyResetAt } : {}),
|
|
194
|
+
...(quota.customWindows !== undefined ? { customWindows: quota.customWindows } : {}),
|
|
195
|
+
updatedAt: "updatedAt" in quota ? quota.updatedAt : Date.now(),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
177
199
|
/** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
|
|
178
200
|
function cacheKeyWithAggregationState(
|
|
179
201
|
config: OcxConfig,
|
|
@@ -191,7 +213,7 @@ function cacheKeyWithAggregationState(
|
|
|
191
213
|
plan: codexPlanKey(account.plan) ?? null,
|
|
192
214
|
paused: account.paused,
|
|
193
215
|
needsReauth: account.needsReauth === true,
|
|
194
|
-
quota: quotaSignatureValue(account.quota
|
|
216
|
+
quota: quotaSignatureValue(providerQuotaFromCodexQuota(account.quota)),
|
|
195
217
|
}));
|
|
196
218
|
const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
|
|
197
219
|
const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
|
|
@@ -1111,9 +1133,8 @@ async function fetchChatGptForwardQuota(
|
|
|
1111
1133
|
): Promise<ProviderQuotaReport | null> {
|
|
1112
1134
|
if (providerCodexAccountMode(provider, providerConfig) === "direct") {
|
|
1113
1135
|
const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
|
|
1114
|
-
const quota = snapshot.info.quota
|
|
1115
|
-
|
|
1116
|
-
: null;
|
|
1136
|
+
const quota = providerQuotaFromCodexQuota(snapshot.info.quota);
|
|
1137
|
+
if (quota) quota.updatedAt = Date.now();
|
|
1117
1138
|
return quota
|
|
1118
1139
|
? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
|
|
1119
1140
|
: null;
|
|
@@ -1121,10 +1142,14 @@ async function fetchChatGptForwardQuota(
|
|
|
1121
1142
|
const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
|
|
1122
1143
|
const accounts = snapshot.accounts;
|
|
1123
1144
|
const activeId = effectiveCodexAuthAccountId(config);
|
|
1124
|
-
const capacityAccounts = accounts.map(account => ({
|
|
1145
|
+
const capacityAccounts = accounts.map(account => ({
|
|
1146
|
+
...account,
|
|
1147
|
+
active: account.id === activeId,
|
|
1148
|
+
quota: providerQuotaFromCodexQuota(account.quota),
|
|
1149
|
+
}));
|
|
1125
1150
|
const active = capacityAccounts.find(account => account.active)
|
|
1126
|
-
??
|
|
1127
|
-
??
|
|
1151
|
+
?? capacityAccounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
|
|
1152
|
+
?? capacityAccounts[0];
|
|
1128
1153
|
const now = Date.now();
|
|
1129
1154
|
const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
|
|
1130
1155
|
if (capacity.aggregation && capacity.quota) {
|
|
@@ -1140,7 +1165,12 @@ async function fetchChatGptForwardQuota(
|
|
|
1140
1165
|
}
|
|
1141
1166
|
const activeUsable = !!active && !active.paused && active.needsReauth !== true;
|
|
1142
1167
|
const quota = activeUsable && active?.quota
|
|
1143
|
-
? {
|
|
1168
|
+
? {
|
|
1169
|
+
...active.quota,
|
|
1170
|
+
fiveHourPercent: active.quota.fiveHourPercent ?? active.quota.shortPercent,
|
|
1171
|
+
fiveHourResetAt: active.quota.fiveHourResetAt ?? active.quota.shortResetAt,
|
|
1172
|
+
updatedAt: active.quota.updatedAt ?? Date.now(),
|
|
1173
|
+
} as CodexCapacityQuota
|
|
1144
1174
|
: null;
|
|
1145
1175
|
const quotaFresh = !!quota
|
|
1146
1176
|
&& Number.isFinite(quota.updatedAt)
|
|
@@ -1286,6 +1316,24 @@ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number
|
|
|
1286
1316
|
return { percent, resetAt };
|
|
1287
1317
|
}
|
|
1288
1318
|
|
|
1319
|
+
function parseClaudeLimit(value: unknown): { label: string; percent: number; resetAt?: number } | null {
|
|
1320
|
+
const rec = asRecord(value);
|
|
1321
|
+
if (!rec) return null;
|
|
1322
|
+
const percent = normalizePercent(rec.percent);
|
|
1323
|
+
if (percent === undefined) return null;
|
|
1324
|
+
const scope = asRecord(rec.scope);
|
|
1325
|
+
const model = asRecord(scope?.model);
|
|
1326
|
+
const rawLabel = String(model?.display_name ?? "").trim();
|
|
1327
|
+
if (!rawLabel) return null;
|
|
1328
|
+
const lowerLabel = rawLabel.toLowerCase();
|
|
1329
|
+
const label = lowerLabel.includes("fable") ? "Fable"
|
|
1330
|
+
: lowerLabel.includes("opus") ? "Opus"
|
|
1331
|
+
: lowerLabel.includes("sonnet") ? "Sonnet"
|
|
1332
|
+
: rawLabel;
|
|
1333
|
+
const resetAt = normalizeResetAt(rec.resets_at);
|
|
1334
|
+
return { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1289
1337
|
/** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
|
|
1290
1338
|
const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
|
|
1291
1339
|
|
|
@@ -1309,11 +1357,25 @@ async function fetchAnthropicUsageQuota(accessToken: string): Promise<ProviderQu
|
|
|
1309
1357
|
if (!body) return null;
|
|
1310
1358
|
const fiveHour = parseClaudeBucket(body.five_hour);
|
|
1311
1359
|
const sevenDay = parseClaudeBucket(body.seven_day);
|
|
1360
|
+
const fable = parseClaudeBucket(body.seven_day_fable);
|
|
1312
1361
|
const opus = parseClaudeBucket(body.seven_day_opus);
|
|
1313
1362
|
const sonnet = parseClaudeBucket(body.seven_day_sonnet);
|
|
1314
1363
|
const customWindows: ProviderQuotaWindow[] = [];
|
|
1364
|
+
if (fable?.percent !== undefined) customWindows.push({ label: "Fable", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
|
|
1315
1365
|
if (opus?.percent !== undefined) customWindows.push({ label: "Opus", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
|
|
1316
1366
|
if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
|
|
1367
|
+
const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
|
|
1368
|
+
const limits = Array.isArray(body.limits) ? body.limits : [];
|
|
1369
|
+
for (const rawLimit of limits) {
|
|
1370
|
+
const limitRecord = asRecord(rawLimit);
|
|
1371
|
+
// `session` and `weekly_all` mirror the canonical five-hour and weekly
|
|
1372
|
+
// buckets above; only model-scoped weekly limits add a third window.
|
|
1373
|
+
if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
|
|
1374
|
+
const limit = parseClaudeLimit(rawLimit);
|
|
1375
|
+
if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
|
|
1376
|
+
knownLabels.add(limit.label.toLowerCase());
|
|
1377
|
+
customWindows.push(limit);
|
|
1378
|
+
}
|
|
1317
1379
|
const quota: ProviderQuota = {
|
|
1318
1380
|
// Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
|
|
1319
1381
|
// rows: report it in the canonical fields so the dashboard renders it with the standard
|
|
@@ -2252,15 +2314,12 @@ async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig
|
|
|
2252
2314
|
});
|
|
2253
2315
|
}
|
|
2254
2316
|
|
|
2255
|
-
async function fetchAiStudioQuota(name: string,
|
|
2256
|
-
const active = globalAiStudioRelayHub.hasActiveSessions();
|
|
2257
|
-
const sessionCount = globalAiStudioRelayHub.getActiveSessionCount();
|
|
2317
|
+
async function fetchAiStudioQuota(name: string, _provider: OcxProviderConfig): Promise<ProviderQuotaReport | null> {
|
|
2258
2318
|
const now = Date.now();
|
|
2259
|
-
|
|
2260
2319
|
return {
|
|
2261
2320
|
provider: name,
|
|
2262
2321
|
label: "Google AI Studio (Web)",
|
|
2263
|
-
source:
|
|
2322
|
+
source: "Direct Session",
|
|
2264
2323
|
updatedAt: now,
|
|
2265
2324
|
quota: {
|
|
2266
2325
|
updatedAt: now,
|