@bitkyc08/opencodex 2.42.0 → 2.43.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 +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import {
|
|
2
|
+
accountBoundNativeOpenAiSlugsBySelector,
|
|
3
|
+
shouldIncludeAccountBoundNativeOpenAi,
|
|
4
|
+
shouldIncludeNativeOpenAi,
|
|
5
|
+
UPSTREAM_NATIVE_ENTRIES,
|
|
6
|
+
} from "../codex/catalog/metadata";
|
|
7
|
+
import { comboModelId, comboPublicModelId } from "../combos/types";
|
|
8
|
+
import { policyModelId, policyPublicModelId } from "../routing/profile";
|
|
9
|
+
import type { InboundWire } from "../providers/registry";
|
|
10
|
+
import { fastPolicyForModel } from "../providers/service-tier";
|
|
11
|
+
import type { OcxConfig } from "../types";
|
|
12
|
+
import {
|
|
13
|
+
isKnownId,
|
|
14
|
+
knownEffortRowIds,
|
|
15
|
+
loadDetectedCursorEffortTable,
|
|
16
|
+
parseEffortRowId,
|
|
17
|
+
parseRequestEffortRowId,
|
|
18
|
+
type EffortRowKnownIds,
|
|
19
|
+
type ParsedEffortRowId,
|
|
20
|
+
} from "./effort-row";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Synthetic Fast selectors: `<base-id>--fast` published on external model listings for
|
|
24
|
+
* models whose resolved Fast policy is eligible, so a client that can only pick a model by
|
|
25
|
+
* id reaches the priority service tier. The Codex app has a picker toggle for this; nothing
|
|
26
|
+
* else did (devlog/_plan/260904_external_fast_wire).
|
|
27
|
+
*
|
|
28
|
+
* The marker is `--fast`, not `-fast`. Terminal `-fast` is a REAL id across this catalog —
|
|
29
|
+
* grok-4-fast, glm-5.3-fast, gpt-5-fast, and every Cursor fast variant — so a single hyphen
|
|
30
|
+
* cannot tell a product apart from a tier. `--` is the same terminal separator the effort-row
|
|
31
|
+
* grammar relies on for the same reason.
|
|
32
|
+
*/
|
|
33
|
+
const FAST_ROW_SUFFIX = "--fast";
|
|
34
|
+
|
|
35
|
+
export interface ParsedFastRowId {
|
|
36
|
+
baseId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface ParsedSyntheticRow {
|
|
40
|
+
fastRow: ParsedFastRowId | null;
|
|
41
|
+
effortRow: ParsedEffortRowId | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function fastRowId(baseId: string): string {
|
|
45
|
+
return `${baseId}${FAST_ROW_SUFFIX}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a provider/model pair's resolved Fast policy may be published as a row.
|
|
50
|
+
*
|
|
51
|
+
* `eligible` alone. `unclassified` means capability is undefined, and `decideTier` makes
|
|
52
|
+
* `fastMode` inert there, so publishing it would advertise a tier the runtime then refuses
|
|
53
|
+
* to send.
|
|
54
|
+
*/
|
|
55
|
+
export function fastRowEligible(
|
|
56
|
+
provider: Parameters<typeof fastPolicyForModel>[0],
|
|
57
|
+
modelId: string,
|
|
58
|
+
providerName?: string,
|
|
59
|
+
inbound: InboundWire = "responses",
|
|
60
|
+
): boolean {
|
|
61
|
+
return fastPolicyForModel(provider, modelId, providerName, inbound).eligibility === "eligible";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Shared discovery/export policy; native rows additionally need upstream tier evidence. */
|
|
65
|
+
export function catalogFastRowEligible(
|
|
66
|
+
config: OcxConfig,
|
|
67
|
+
model: { provider: string; id: string; native?: boolean; supportsServiceTier?: boolean },
|
|
68
|
+
): boolean {
|
|
69
|
+
if (config.fastRows === false) return false;
|
|
70
|
+
// Configured suffix-shaped IDs are real bases; live-only ones are deliberately
|
|
71
|
+
// refused by ingress. Discovery must not advertise a selector ingress cannot strip.
|
|
72
|
+
if (model.id.endsWith(FAST_ROW_SUFFIX)
|
|
73
|
+
&& !fastRowBases(config)(model.native ? model.id : `${model.provider}/${model.id}`)) return false;
|
|
74
|
+
if (model.native) {
|
|
75
|
+
const id = model.id.slice(model.id.lastIndexOf("/") + 1);
|
|
76
|
+
const tiers = UPSTREAM_NATIVE_ENTRIES.get(id)?.additional_speed_tiers;
|
|
77
|
+
const provider = config.providers.openai;
|
|
78
|
+
return Array.isArray(tiers) && tiers.includes("fast") && provider !== undefined
|
|
79
|
+
&& fastRowEligible(provider, id, "openai");
|
|
80
|
+
}
|
|
81
|
+
if (model.supportsServiceTier !== undefined) return model.supportsServiceTier === true;
|
|
82
|
+
const provider = config.providers[model.provider];
|
|
83
|
+
return provider !== undefined && fastRowEligible(provider, model.id, model.provider);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Bases that may carry a fast row.
|
|
88
|
+
*
|
|
89
|
+
* Deliberately a SUPERSET of what the listings publish, and deliberately not
|
|
90
|
+
* `knownEffortRowIds`. That set answers "which exact ids defeat the synthetic grammar"; it
|
|
91
|
+
* does not answer "which bases are routable". Bare natives prove the gap: the `openai`
|
|
92
|
+
* registry entry declares no `models` list and the default provider config declares none
|
|
93
|
+
* either, because they route through a family-pattern rule instead. `gpt-5.6-sol` is
|
|
94
|
+
* therefore absent from it, and requiring membership would publish `gpt-5.6-sol--fast` and
|
|
95
|
+
* then refuse to parse it.
|
|
96
|
+
*
|
|
97
|
+
* Being too permissive costs nothing here: routing still rejects a base it cannot serve, and
|
|
98
|
+
* the exact-id guard in `parseFastRowId` still protects real models. Being too strict breaks
|
|
99
|
+
* the feature.
|
|
100
|
+
*
|
|
101
|
+
* Membership must not depend on the live-model cache. An earlier version seeded this set
|
|
102
|
+
* from `knownEffortRowIds` alone, which reads `getStaleCached` (router.ts:125), so a
|
|
103
|
+
* live-only model leaving the cache silently stopped its `--fast` selector from parsing.
|
|
104
|
+
* The argument that its base row leaves the listing at the same time is true but
|
|
105
|
+
* irrelevant: `/v1/models` is discovery, not a routing allowlist, and `routeModel` still
|
|
106
|
+
* serves the bare base through the default provider (router.ts:791) and the qualified base
|
|
107
|
+
* through its configured provider (router.ts:680). So the base kept working while only the
|
|
108
|
+
* fast selector broke — the asymmetry this set exists to prevent.
|
|
109
|
+
*
|
|
110
|
+
* A pure Set cannot express this. Listings also publish LIVE-discovered and retained models
|
|
111
|
+
* that appear in no config (`provider-fetch.ts` publishes `goModels` and `retainModels`), and
|
|
112
|
+
* enumerating them means reading the very cache whose churn caused the original defect. So
|
|
113
|
+
* this returns a PREDICATE: an id is a routable base when it is a known static/config id, or
|
|
114
|
+
* when it is namespaced under an enabled configured provider. The second clause is
|
|
115
|
+
* structural, so it holds for a live-discovered model without consulting the cache, and it
|
|
116
|
+
* is exactly the shape `routeModel` uses to accept a qualified id (router.ts:680).
|
|
117
|
+
*
|
|
118
|
+
* A base is RECOGNIZED here and then judged by routing, which is the component that actually
|
|
119
|
+
* knows whether it can serve it.
|
|
120
|
+
*/
|
|
121
|
+
export function fastRowBases(config: OcxConfig): (id: string) => boolean {
|
|
122
|
+
const bases = new Set<string>();
|
|
123
|
+
// Configured providers: any model the router would accept for this provider, plus the
|
|
124
|
+
// namespaced and alias-namespaced spellings a listing can publish. Deliberately NOT
|
|
125
|
+
// knownEffortRowIds, whose live-cache half makes membership time-dependent.
|
|
126
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
127
|
+
if (providerConfig.disabled === true) continue;
|
|
128
|
+
const namespaces = [providerName, providerConfig.alias].filter(
|
|
129
|
+
(value): value is string => typeof value === "string" && value.length > 0,
|
|
130
|
+
);
|
|
131
|
+
const declared = [
|
|
132
|
+
...(providerConfig.models ?? []),
|
|
133
|
+
...(providerConfig.defaultModel ? [providerConfig.defaultModel] : []),
|
|
134
|
+
...Object.values(providerConfig.modelAliases ?? {}),
|
|
135
|
+
...(config.customModels ?? [])
|
|
136
|
+
.filter(model => model.provider === providerName && model.modelId)
|
|
137
|
+
.map(model => model.modelId),
|
|
138
|
+
];
|
|
139
|
+
for (const id of declared) {
|
|
140
|
+
bases.add(id);
|
|
141
|
+
for (const namespace of namespaces) bases.add(`${namespace}/${id}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// The STATIC upstream table, not `visibleNativeSlugs()`: that one filters by
|
|
145
|
+
// disabled/shadowed state and reaches the catalog cache on disk, so it would both read a
|
|
146
|
+
// file per parsed selector and SHRINK as runtime state changes. A base disappearing
|
|
147
|
+
// mid-session would strand a client still holding the id it was published.
|
|
148
|
+
if (shouldIncludeNativeOpenAi(config)) {
|
|
149
|
+
for (const slug of UPSTREAM_NATIVE_ENTRIES.keys()) bases.add(slug);
|
|
150
|
+
}
|
|
151
|
+
if (shouldIncludeAccountBoundNativeOpenAi(config)) {
|
|
152
|
+
// An EMPTY observed-entry list on purpose: the default argument reads the Codex models
|
|
153
|
+
// cache and catalog from disk. The empty form still seeds every selector with the native
|
|
154
|
+
// model set, and anything publishable is in UPSTREAM_NATIVE_ENTRIES anyway.
|
|
155
|
+
for (const [selector, slugs] of accountBoundNativeOpenAiSlugsBySelector(config, [])) {
|
|
156
|
+
for (const slug of slugs) bases.add(`${selector}/${slug}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// Namespaces whose qualified ids route, whatever the cache currently holds.
|
|
160
|
+
const namespaces = new Set<string>();
|
|
161
|
+
// Virtual rows. A combo or routing profile is published under its canonical
|
|
162
|
+
// `<namespace>/<id>` AND under an operator alias, which may be an arbitrary bare string
|
|
163
|
+
// with no namespace to vouch for it, so the structural clause below cannot reach it. A
|
|
164
|
+
// combo also cannot be covered by config.providers: declaring a provider named `combo`
|
|
165
|
+
// is rejected outright (combos/types.ts:191).
|
|
166
|
+
for (const [id, combo] of Object.entries(config.combos ?? {})) {
|
|
167
|
+
bases.add(comboModelId(id));
|
|
168
|
+
bases.add(comboPublicModelId(id, combo));
|
|
169
|
+
}
|
|
170
|
+
for (const [id, profile] of Object.entries(config.routingProfiles ?? {})) {
|
|
171
|
+
bases.add(policyModelId(id));
|
|
172
|
+
bases.add(policyPublicModelId(id, profile));
|
|
173
|
+
}
|
|
174
|
+
for (const [providerName, providerConfig] of Object.entries(config.providers)) {
|
|
175
|
+
if (providerConfig.disabled === true) continue;
|
|
176
|
+
namespaces.add(providerName.toLowerCase());
|
|
177
|
+
if (typeof providerConfig.alias === "string" && providerConfig.alias.length > 0) {
|
|
178
|
+
namespaces.add(providerConfig.alias.toLowerCase());
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return (id: string): boolean => {
|
|
182
|
+
if (bases.has(id)) return true;
|
|
183
|
+
const slash = id.indexOf("/");
|
|
184
|
+
if (slash <= 0 || slash === id.length - 1) return false;
|
|
185
|
+
// A live-discovered or retained model is published as `<provider>/<model>` and appears in
|
|
186
|
+
// no config, so structural recognition is the only cache-free way to accept it.
|
|
187
|
+
//
|
|
188
|
+
// But NOT when the remainder itself ends in the marker. A real live model may legitimately
|
|
189
|
+
// be named `foo--fast`; its exact id is protected by the known-id guard only while the
|
|
190
|
+
// discovery cache still holds it, and after eviction that guard goes quiet while this
|
|
191
|
+
// clause would still accept `provider/foo` structurally - silently routing a DIFFERENT
|
|
192
|
+
// model than the client selected. Refusing the strip is the safe side: the caller then
|
|
193
|
+
// sends the id verbatim and routing resolves the real model, or fails honestly.
|
|
194
|
+
if (id.slice(slash + 1).endsWith(FAST_ROW_SUFFIX)) return false;
|
|
195
|
+
return namespaces.has(id.slice(0, slash).toLowerCase());
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseFastRowId(
|
|
200
|
+
id: string,
|
|
201
|
+
config: Pick<OcxConfig, "fastRows">,
|
|
202
|
+
knownIds?: EffortRowKnownIds,
|
|
203
|
+
routableBases?: EffortRowKnownIds,
|
|
204
|
+
): ParsedFastRowId | null {
|
|
205
|
+
if (config.fastRows === false) return null;
|
|
206
|
+
if (!id.endsWith(FAST_ROW_SUFFIX)) return null;
|
|
207
|
+
// An exact configured/public id always beats the synthetic grammar, the same precedence
|
|
208
|
+
// effort rows use. An operator who really named a model `x--fast` keeps it.
|
|
209
|
+
if (isKnownId(knownIds, id)) return null;
|
|
210
|
+
const baseId = id.slice(0, -FAST_ROW_SUFFIX.length);
|
|
211
|
+
if (baseId.length === 0) return null;
|
|
212
|
+
return isKnownId(routableBases, baseId) ? { baseId } : null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* True when an effort-row base still carries a fast marker, i.e. the selector nested the two
|
|
217
|
+
* grammars. Composition is not supported, so such an id resolves to neither rather than
|
|
218
|
+
* silently to whichever parser ran first.
|
|
219
|
+
*
|
|
220
|
+
* Known-id guarded, so a real model named `foo--fast` keeps its legitimate `foo--fast--high`
|
|
221
|
+
* effort row.
|
|
222
|
+
*/
|
|
223
|
+
export function effortBaseCarriesFastMarker(
|
|
224
|
+
baseId: string,
|
|
225
|
+
knownIds: EffortRowKnownIds | undefined,
|
|
226
|
+
): boolean {
|
|
227
|
+
return baseId.endsWith(FAST_ROW_SUFFIX) && !isKnownId(knownIds, baseId);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Resolve one ingress selector against both synthetic grammars, returning at most one.
|
|
232
|
+
* Callers pass the id the client sent and never a value another parser mutated.
|
|
233
|
+
*/
|
|
234
|
+
export function parseSyntheticRowId(
|
|
235
|
+
id: string,
|
|
236
|
+
config: OcxConfig,
|
|
237
|
+
// Claude surfaces decode the alias before the marker is unambiguous, so they pass the
|
|
238
|
+
// decoded form for Fast while effort parsing keeps seeing the id the client sent. A THUNK,
|
|
239
|
+
// not a string: arguments are evaluated before the call, so an eager decode would run its
|
|
240
|
+
// alias lookups even on the fastRows-off path this function exists to leave untouched.
|
|
241
|
+
fastSelector?: () => string,
|
|
242
|
+
): ParsedSyntheticRow {
|
|
243
|
+
// Explicit opt-out preserves the effort-only parser and avoids Fast inventory work.
|
|
244
|
+
if (config.fastRows === false) {
|
|
245
|
+
return { fastRow: null, effortRow: parseRequestEffortRowId(id, config) };
|
|
246
|
+
}
|
|
247
|
+
const selector = fastSelector?.() ?? id;
|
|
248
|
+
const wantsFast = selector.endsWith(FAST_ROW_SUFFIX);
|
|
249
|
+
// Bail before building any inventory when neither grammar can match. A readable Claude
|
|
250
|
+
// alias is `claude-ocx-<provider>--<model>`, so it ALWAYS contains `--`: testing only for
|
|
251
|
+
// the separator would rebuild the whole model inventory on every Claude turn for a
|
|
252
|
+
// selector that cannot be a fast row. With effort parsing off, the terminal suffix is the
|
|
253
|
+
// only thing that can match.
|
|
254
|
+
const wantsEffort = config.cursorEffortRows === true && id.lastIndexOf("--") > 0;
|
|
255
|
+
if (!wantsFast && !wantsEffort) return { fastRow: null, effortRow: null };
|
|
256
|
+
const knownIds = knownEffortRowIds(config);
|
|
257
|
+
const fastRow = wantsFast
|
|
258
|
+
? parseFastRowId(selector, config, knownIds, fastRowBases(config))
|
|
259
|
+
: null;
|
|
260
|
+
if (fastRow) return { fastRow, effortRow: null };
|
|
261
|
+
// Cursor install detection stays behind its own flag, exactly as parseRequestEffortRowId
|
|
262
|
+
// gates it today.
|
|
263
|
+
const effortRow = wantsEffort
|
|
264
|
+
? parseEffortRowId(id, config, { knownIds, table: loadDetectedCursorEffortTable() })
|
|
265
|
+
: null;
|
|
266
|
+
return effortRow && effortBaseCarriesFastMarker(effortRow.baseId, knownIds)
|
|
267
|
+
? { fastRow: null, effortRow: null }
|
|
268
|
+
: { fastRow: null, effortRow };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Fast-only resolution for surfaces that never parsed an effort row. */
|
|
272
|
+
export function parseFastOnlyRowId(
|
|
273
|
+
config: OcxConfig,
|
|
274
|
+
selector: () => string,
|
|
275
|
+
): ParsedFastRowId | null {
|
|
276
|
+
if (config.fastRows === false) return null;
|
|
277
|
+
return parseSyntheticRowId("", config, selector).fastRow;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Add a fast sibling beside an eligible row. The base row is always kept: a fast row is an
|
|
282
|
+
* addition, never a replacement. That is the deliberate difference from `fastMode`, which
|
|
283
|
+
* replaces the listed Cursor id — replacement suits a global switch, but a per-request
|
|
284
|
+
* selector has to leave the default reachable.
|
|
285
|
+
*/
|
|
286
|
+
export function expandFastRow<T extends { id: string }>(
|
|
287
|
+
row: T,
|
|
288
|
+
eligible: boolean,
|
|
289
|
+
config: Pick<OcxConfig, "fastRows">,
|
|
290
|
+
knownIds?: EffortRowKnownIds,
|
|
291
|
+
): T[] {
|
|
292
|
+
if (config.fastRows === false || !eligible) return [row];
|
|
293
|
+
const id = fastRowId(row.id);
|
|
294
|
+
return isKnownId(knownIds, id) ? [row] : [row, { ...row, id }];
|
|
295
|
+
}
|
package/src/server/gui-static.ts
CHANGED
|
@@ -90,6 +90,22 @@ function runtimeRoleMeta(runtimeRole: string): string {
|
|
|
90
90
|
return `<meta name="opencodex-runtime-role" content="${escapeHtmlAttribute(runtimeRole)}">`;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Does this bind require a typed management credential?
|
|
95
|
+
*
|
|
96
|
+
* Emitted for the same reason as the role: so the dashboard can answer a question on first
|
|
97
|
+
* paint without asking a remote-hub endpoint. It is NOT the same question as the role.
|
|
98
|
+
* `standalone` + `hostname: "0.0.0.0"` is an operator who deliberately exposed the dashboard
|
|
99
|
+
* and must type the admin token, while a `hub` on loopback still mints its own session — so
|
|
100
|
+
* the role cannot stand in for this, and using it that way locked out exactly the operator
|
|
101
|
+
* who is supposed to see the prompt.
|
|
102
|
+
*
|
|
103
|
+
* Non-secret: it restates the bind the operator chose, which `/healthz` and the dashboard
|
|
104
|
+
* already reflect.
|
|
105
|
+
*/
|
|
106
|
+
function managementAuthRequiredMeta(required: boolean): string {
|
|
107
|
+
return `<meta name="opencodex-management-auth-required" content="${required ? "1" : "0"}">`;
|
|
108
|
+
}
|
|
93
109
|
function htmlDocumentResponse(html: string): Response {
|
|
94
110
|
return new Response(html, {
|
|
95
111
|
headers: {
|
|
@@ -101,9 +117,18 @@ function htmlDocumentResponse(html: string): Response {
|
|
|
101
117
|
});
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
function htmlResponse(
|
|
120
|
+
function htmlResponse(
|
|
121
|
+
path: string,
|
|
122
|
+
session?: GuiSessionBootstrap,
|
|
123
|
+
runtimeRole?: string,
|
|
124
|
+
managementAuthRequired?: boolean,
|
|
125
|
+
): Response {
|
|
105
126
|
let html = readFileSync(path, "utf8");
|
|
106
|
-
const bootstrap =
|
|
127
|
+
const bootstrap = [
|
|
128
|
+
runtimeRole ? runtimeRoleMeta(runtimeRole) : "",
|
|
129
|
+
managementAuthRequired === undefined ? "" : managementAuthRequiredMeta(managementAuthRequired),
|
|
130
|
+
session ? sessionBootstrapMeta(session) : "",
|
|
131
|
+
].join("");
|
|
107
132
|
if (bootstrap) {
|
|
108
133
|
html = html.includes("</head>") ? html.replace("</head>", `${bootstrap}</head>`) : `${bootstrap}${html}`;
|
|
109
134
|
}
|
|
@@ -126,6 +151,7 @@ export function serveGuiFile(
|
|
|
126
151
|
guiDist = findGuiDist(),
|
|
127
152
|
session?: GuiSessionBootstrap,
|
|
128
153
|
runtimeRole?: string,
|
|
154
|
+
managementAuthRequired?: boolean,
|
|
129
155
|
): Response | null {
|
|
130
156
|
if (!guiDist) return null;
|
|
131
157
|
const filePath = resolveGuiFilePath(guiDist, pathname);
|
|
@@ -135,7 +161,7 @@ export function serveGuiFile(
|
|
|
135
161
|
if (!extname(pathname)) {
|
|
136
162
|
const indexPath = join(guiDist, "index.html");
|
|
137
163
|
if (isFile(indexPath)) {
|
|
138
|
-
return htmlResponse(indexPath, session, runtimeRole);
|
|
164
|
+
return htmlResponse(indexPath, session, runtimeRole, managementAuthRequired);
|
|
139
165
|
}
|
|
140
166
|
}
|
|
141
167
|
return null;
|
|
@@ -143,7 +169,7 @@ export function serveGuiFile(
|
|
|
143
169
|
|
|
144
170
|
const ext = extname(filePath);
|
|
145
171
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
146
|
-
if (ext === ".html") return htmlResponse(filePath, session, runtimeRole);
|
|
172
|
+
if (ext === ".html") return htmlResponse(filePath, session, runtimeRole, managementAuthRequired);
|
|
147
173
|
// Snapshot bytes before returning the response. Bun.file is lazy: if gui/dist is replaced
|
|
148
174
|
// after Bun frames the response but before the stream finishes, its Content-Length can
|
|
149
175
|
// describe the old file while the body comes from the new one (#2792).
|
package/src/server/index.ts
CHANGED
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
} from "./ws-bridge";
|
|
13
13
|
import type { Server, ServerWebSocket } from "bun";
|
|
14
14
|
import {
|
|
15
|
-
DEFAULT_SUBAGENT_MODELS,
|
|
16
15
|
applyProxyEnv,
|
|
17
16
|
armClaudeCodeBaseline,
|
|
18
17
|
loadConfig,
|
|
@@ -22,6 +21,8 @@ import {
|
|
|
22
21
|
} from "../config";
|
|
23
22
|
import { grokDefaultReasoningEffort } from "../grok/effort";
|
|
24
23
|
import { flushConfigDirHardening } from "../config/paths";
|
|
24
|
+
import { migrateStartupSubagentModels } from "./subagent-models-startup";
|
|
25
|
+
import { migrateStartupXaiResponses } from "./xai-responses-startup";
|
|
25
26
|
import { reconcileOAuthProviders } from "../oauth";
|
|
26
27
|
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
27
28
|
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
@@ -33,8 +34,12 @@ import {
|
|
|
33
34
|
type NativeCodexOwnership,
|
|
34
35
|
type OwnershipInspection,
|
|
35
36
|
} from "../integrations/native/ownership-preflight";
|
|
36
|
-
import {
|
|
37
|
+
import {
|
|
38
|
+
createResetCreditWhamClient,
|
|
39
|
+
registerCodexCooldownRecoveryProbeWorker,
|
|
40
|
+
} from "../codex/auth-api";
|
|
37
41
|
import { activateResetCreditAutoRedeem } from "../codex/reset-credit-auto-redeem";
|
|
42
|
+
import { registerCodexQuotaAutoRefreshWorker } from "../codex/quota-auto-refresh";
|
|
38
43
|
import {
|
|
39
44
|
reconcileLiveStateStores,
|
|
40
45
|
setLiveStateStoreConfig,
|
|
@@ -230,6 +235,7 @@ import { recordCursorSeen } from "../integrations/cursor-seen";
|
|
|
230
235
|
import { detectCursorInstalls } from "../integrations/cursor-detect";
|
|
231
236
|
import { loadCursorEffortTable } from "../integrations/cursor-effort-table";
|
|
232
237
|
import { expandCursorEffortRow, knownEffortRowIds } from "./effort-row";
|
|
238
|
+
import { catalogFastRowEligible, expandFastRow } from "./fast-row";
|
|
233
239
|
|
|
234
240
|
export const MAX_WS_FRAME_BYTES = 50 * 1024 * 1024;
|
|
235
241
|
const WEBSOCKET_IDLE_TIMEOUT_SECONDS = 0;
|
|
@@ -509,7 +515,7 @@ function attachLiveSidebandUpstream(
|
|
|
509
515
|
|
|
510
516
|
// Adapter resolution + wire-protocol override extracted to ./server/adapter-resolve.
|
|
511
517
|
|
|
512
|
-
// Source invariant for tests/passthrough-abort.test.ts after the pure module split:
|
|
518
|
+
// Source invariant for tests/responses/passthrough-abort.test.ts after the pure module split:
|
|
513
519
|
// if (isEventStream && upstreamResponse.body) {
|
|
514
520
|
// const repairConfig = route.provider.responsesItemIdRepair;
|
|
515
521
|
// const needsClientRewrite = imageGenCallAliases.size > 0
|
|
@@ -583,6 +589,8 @@ export interface StartServerDeps {
|
|
|
583
589
|
readinessGate?: ReadinessGate;
|
|
584
590
|
/** Test-only package-tree observation; production captures package.json identity at boot. */
|
|
585
591
|
packageTreeIntegrity?: PackageTreeIntegrityGuard;
|
|
592
|
+
/** Test-only seam for observing quota-worker registration ownership. */
|
|
593
|
+
registerCodexQuotaAutoRefreshWorker?: typeof registerCodexQuotaAutoRefreshWorker;
|
|
586
594
|
}
|
|
587
595
|
|
|
588
596
|
function inspectStartupOwnership(
|
|
@@ -645,7 +653,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
645
653
|
// Captured before loadConfig() starts the optional ACL flight so stop() drains the same dir
|
|
646
654
|
// even if OPENCODEX_HOME changes underneath a long-lived process.
|
|
647
655
|
const startupConfigDir = getConfigDir();
|
|
648
|
-
const
|
|
656
|
+
const startupConfig = migrateStartupSubagentModels(
|
|
657
|
+
runModelRenameStartupMigration(runAlibabaRegionStartupMigration(runOpenAiTierStartupMigration(loadConfig()))),
|
|
658
|
+
);
|
|
659
|
+
// Reconcile disk-backed presets first: it replaces provider rows and must not undo
|
|
660
|
+
// an in-memory wire upgrade when that upgrade's persistence is temporarily unavailable.
|
|
661
|
+
reconcileOAuthProviders(startupConfig);
|
|
662
|
+
const config = migrateStartupXaiResponses(startupConfig);
|
|
649
663
|
warnAgentTaskRecoveryStartup(config);
|
|
650
664
|
setLiveStateStoreConfig(config);
|
|
651
665
|
applyProxyEnv(config);
|
|
@@ -655,16 +669,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
655
669
|
let userCostOverlayReconciler: { stop(): void } | null = null;
|
|
656
670
|
// Arm synchronously before listen. A pending journal therefore makes __main__ unusable
|
|
657
671
|
// before any request can resolve its physical credential, while health/management/Pool stay live.
|
|
658
|
-
// Refresh OAuth provider presets (models/noReasoningModels) from the registry so a proxy update
|
|
659
|
-
// adding/dropping models reaches existing configs on start — not just fresh installs.
|
|
660
|
-
reconcileOAuthProviders(config);
|
|
661
672
|
reconcileLiveStateStores();
|
|
662
|
-
// Seed default featured subagent models on first run only (UNSET → defaults). A user-set list,
|
|
663
|
-
// even [], is left alone so GUI removals persist.
|
|
664
|
-
if (config.subagentModels === undefined) {
|
|
665
|
-
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
666
|
-
saveConfig(config);
|
|
667
|
-
}
|
|
668
673
|
// authMode migration (devlog 260726_claude_auth_auto/015): before "auto" existed,
|
|
669
674
|
// choosing Subscription DELETED the key, so a pre-upgrade block with no authMode is
|
|
670
675
|
// indistinguishable from "never chose". Pin those to subscription once so an upgrade
|
|
@@ -798,6 +803,10 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
798
803
|
* on the direct-spawn host into a 404 (#3192). The handler still runs its own admission, so a
|
|
799
804
|
* loopback caller without a ChatGPT credential is refused inside it rather than by this gate.
|
|
800
805
|
*
|
|
806
|
+
* The standalone Images client uses the same base URL for its two POST routes. Their handler
|
|
807
|
+
* keeps the paid upstream behind its own admission and forward-credential checks, so admit only
|
|
808
|
+
* the exact methods and paths it serves (#3428).
|
|
809
|
+
*
|
|
801
810
|
* `GET /v1/models` is on the list for a reason that is easy to miss. When catalog
|
|
802
811
|
* materialization fails or finds no source, `syncCodex` warns and injects with
|
|
803
812
|
* `catalogPath: null`; Codex then builds an ONLINE model manager and `model/list` refreshes
|
|
@@ -811,6 +820,9 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
811
820
|
}
|
|
812
821
|
if (path === "/v1/responses/compact") return req.method === "POST";
|
|
813
822
|
if (path === "/v1/alpha/search") return req.method === "POST";
|
|
823
|
+
if (path === "/v1/images/generations" || path === "/v1/images/edits") {
|
|
824
|
+
return req.method === "POST";
|
|
825
|
+
}
|
|
814
826
|
if (path === "/v1/models") return req.method === "GET";
|
|
815
827
|
// Realtime voice — a directly-spawned `codex app-server` needs these for desktop voice
|
|
816
828
|
// the same way it needs /v1/responses. Two shapes, same trust model as /v1/responses:
|
|
@@ -1017,8 +1029,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1017
1029
|
return "public";
|
|
1018
1030
|
}
|
|
1019
1031
|
let backgroundLifecycle: ReturnType<typeof acquireServerBackgroundLifecycle> | null = null;
|
|
1032
|
+
let unregisterQuotaAutoRefresh: (() => void) | null = null;
|
|
1020
1033
|
try {
|
|
1021
1034
|
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
1035
|
+
unregisterQuotaAutoRefresh = (deps.registerCodexQuotaAutoRefreshWorker
|
|
1036
|
+
?? registerCodexQuotaAutoRefreshWorker)(config);
|
|
1022
1037
|
// External `ocx config set` / direct config.json edits run in other
|
|
1023
1038
|
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
1024
1039
|
// Started inside the guarded startup transaction so the catch below can
|
|
@@ -1270,7 +1285,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1270
1285
|
// Built directly rather than through formatErrorResponse: that helper derives
|
|
1271
1286
|
// `code` from the status and message via classifyError, and these two need stable,
|
|
1272
1287
|
// specific codes. `catalog_not_found` in particular is what lets a caller — and
|
|
1273
|
-
// tests/api-key-attribution.test.ts — tell "this route exists and has no catalog"
|
|
1288
|
+
// tests/server/api-key-attribution.test.ts — tell "this route exists and has no catalog"
|
|
1274
1289
|
// apart from "this route is gone", which is the difference between admission proof
|
|
1275
1290
|
// and a vacuous pass.
|
|
1276
1291
|
return withCors(
|
|
@@ -1430,6 +1445,37 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1430
1445
|
// Codex catalog (client_version) and the OpenAI list shape below stay byte-identical.
|
|
1431
1446
|
const wantsAnthropicList = req.headers.get("anthropic-version") !== null
|
|
1432
1447
|
|| url.searchParams.get("flavor") === "anthropic";
|
|
1448
|
+
/**
|
|
1449
|
+
* Whether a NATIVE slug may carry a Fast sibling.
|
|
1450
|
+
*
|
|
1451
|
+
* Both halves are required. Upstream asserts the tier per model — the same
|
|
1452
|
+
* `additional_speed_tiers` the Codex picker's own toggle is built from — but an
|
|
1453
|
+
* operator capability override or the final wire resolution can still make the
|
|
1454
|
+
* route ineligible, and `decideTier` would then drop the tier the row advertised.
|
|
1455
|
+
*
|
|
1456
|
+
* Declared here, above the Claude discovery call, because that call reads it while
|
|
1457
|
+
* the raw OpenAI mapper further down does too; defining it there would leave this
|
|
1458
|
+
* use in its temporal dead zone.
|
|
1459
|
+
*/
|
|
1460
|
+
const nativeFastEligible = (metadataId: string): boolean =>
|
|
1461
|
+
catalogFastRowEligible(config, { provider: OPENAI_CODEX_PROVIDER_ID, id: metadataId, native: true });
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* Whether a routed catalog row may carry a Fast sibling.
|
|
1465
|
+
*
|
|
1466
|
+
* A combo is its own namespace with no `config.providers` entry — declaring a
|
|
1467
|
+
* provider named `combo` is rejected (combos/types.ts:191) — so provider lookup
|
|
1468
|
+
* cannot classify it. Its aggregated `supportsServiceTier` is already true only
|
|
1469
|
+
* when EVERY member supports the tier (aggregation.ts:201), which is the right
|
|
1470
|
+
* rule for a row that fans out to all of them.
|
|
1471
|
+
*
|
|
1472
|
+
* Declared beside nativeFastEligible, above the Claude discovery call that reads
|
|
1473
|
+
* both; defining it near the raw OpenAI mapper below would leave that use in its
|
|
1474
|
+
* temporal dead zone.
|
|
1475
|
+
*/
|
|
1476
|
+
const catalogRowFastEligible = (m: { provider: string; id: string; supportsServiceTier?: boolean }): boolean =>
|
|
1477
|
+
catalogFastRowEligible(config, m);
|
|
1478
|
+
|
|
1433
1479
|
if (wantsAnthropicList && !url.searchParams.has("client_version")) {
|
|
1434
1480
|
if (config.claudeCode?.enabled === false) return jsonResponse({ data: [] }, 200, req, policy);
|
|
1435
1481
|
// Build Desktop 3P registry so inbound alias resolution works for subsequent requests.
|
|
@@ -1451,7 +1497,22 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1451
1497
|
: idsParam === "desktop"
|
|
1452
1498
|
? "desktop3p" as const
|
|
1453
1499
|
: (/^claude-code\//i.test(req.headers.get("user-agent") ?? "") ? "readable" as const : "desktop3p" as const);
|
|
1454
|
-
const data = buildAnthropicModelInfos(
|
|
1500
|
+
const data = buildAnthropicModelInfos(
|
|
1501
|
+
desktopNativeSlugs,
|
|
1502
|
+
goOrdered,
|
|
1503
|
+
resolveAutoContext(config.claudeCode),
|
|
1504
|
+
idStyle,
|
|
1505
|
+
activeDesktop3pAlias,
|
|
1506
|
+
nativeContextLimits(config),
|
|
1507
|
+
config.fastMode,
|
|
1508
|
+
// Explicit opt-out omits the Fast predicate.
|
|
1509
|
+
config.fastRows !== false
|
|
1510
|
+
? (model: { provider: string; id: string; supportsServiceTier?: boolean }) =>
|
|
1511
|
+
model.provider === "native"
|
|
1512
|
+
? nativeFastEligible(model.id)
|
|
1513
|
+
: catalogRowFastEligible(model)
|
|
1514
|
+
: undefined,
|
|
1515
|
+
);
|
|
1455
1516
|
return jsonResponse({ data }, 200, req, policy);
|
|
1456
1517
|
}
|
|
1457
1518
|
if (url.searchParams.has("client_version")) {
|
|
@@ -1580,7 +1641,13 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1580
1641
|
// The projection is opt-in. Keep the default path free of Cursor install detection,
|
|
1581
1642
|
// and resolve the bundle table once for the whole list rather than once per row.
|
|
1582
1643
|
const effortRowsEnabled = config.cursorEffortRows === true;
|
|
1583
|
-
|
|
1644
|
+
// Explicit opt-out skips policy resolution and additional rows.
|
|
1645
|
+
const fastRowsEnabled = config.fastRows !== false;
|
|
1646
|
+
// One inventory serves both grammars; building it twice would double the work on a
|
|
1647
|
+
// hot path for no benefit.
|
|
1648
|
+
const effortRowKnownIds = effortRowsEnabled || fastRowsEnabled
|
|
1649
|
+
? knownEffortRowIds(config)
|
|
1650
|
+
: undefined;
|
|
1584
1651
|
const privateInference = effortRowsEnabled
|
|
1585
1652
|
? detectCursorInstalls().find(install => install.build === "private-inference")
|
|
1586
1653
|
: undefined;
|
|
@@ -1593,7 +1660,15 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1593
1660
|
knownIds: effortRowKnownIds,
|
|
1594
1661
|
table: cursorEffortTable,
|
|
1595
1662
|
supportsReasoning: reasoningEfforts.length > 0,
|
|
1596
|
-
})
|
|
1663
|
+
}).flatMap(row => expandFastRow(
|
|
1664
|
+
row,
|
|
1665
|
+
// Only the BASE row earns a fast sibling. An effort row already spent the
|
|
1666
|
+
// grammar, and the parser requires the stripped base to be routable, so
|
|
1667
|
+
// `<base>--<effort>--fast` would publish a row no ingress can resolve.
|
|
1668
|
+
row.id === id && nativeFastEligible(metadataId),
|
|
1669
|
+
config,
|
|
1670
|
+
effortRowKnownIds,
|
|
1671
|
+
));
|
|
1597
1672
|
};
|
|
1598
1673
|
const routedRows = await Promise.all(uniqueCatalogModelsForRawPublicList(goOrdered).map(async m => {
|
|
1599
1674
|
// Same rule as the anthropic branch: with the global fast switch on, a client
|
|
@@ -1634,7 +1709,12 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1634
1709
|
knownIds: effortRowKnownIds,
|
|
1635
1710
|
table: cursorEffortTable,
|
|
1636
1711
|
supportsReasoning: (m.reasoningEfforts ?? []).length > 0,
|
|
1637
|
-
})
|
|
1712
|
+
}).flatMap(expanded => expandFastRow(
|
|
1713
|
+
expanded,
|
|
1714
|
+
expanded.id === row.id && catalogRowFastEligible(m),
|
|
1715
|
+
config,
|
|
1716
|
+
effortRowKnownIds,
|
|
1717
|
+
));
|
|
1638
1718
|
}));
|
|
1639
1719
|
const data = [
|
|
1640
1720
|
...visibleNatives.flatMap(id => expandedNativeModelRow(id)),
|
|
@@ -1753,7 +1833,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1753
1833
|
...admissionFields(admission),
|
|
1754
1834
|
};
|
|
1755
1835
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => {
|
|
1756
|
-
const response = await handleSearch(req, config, logCtx, turnAdmissionLease);
|
|
1836
|
+
const response = await handleSearch(req, config, logCtx, turnAdmissionLease, admission);
|
|
1757
1837
|
addFinalRequestLog(requestId, start, logCtx, response.status,
|
|
1758
1838
|
response.status === 499 ? { closeReason: "client_cancel" } : undefined);
|
|
1759
1839
|
return withCors(response, req, policy);
|
|
@@ -1855,7 +1935,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
1855
1935
|
// pre-translation stream + native passthrough callbacks) — do not re-wrap the
|
|
1856
1936
|
// translated Anthropic stream here.
|
|
1857
1937
|
return runAdmittedHttpTurn(req, policy, async turnAdmissionLease => withCors(
|
|
1858
|
-
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease }, policy),
|
|
1938
|
+
await handleClaudeMessages(req, config, logCtx, { requestId, start, turnAdmissionLease, admission }, policy),
|
|
1859
1939
|
req,
|
|
1860
1940
|
policy,
|
|
1861
1941
|
));
|
|
@@ -2056,6 +2136,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
2056
2136
|
undefined,
|
|
2057
2137
|
guiSessionCandidate ?? undefined,
|
|
2058
2138
|
config.runtimeRole ?? "standalone",
|
|
2139
|
+
isApiAuthRequired(config),
|
|
2059
2140
|
);
|
|
2060
2141
|
if (guiFile) return guiFile;
|
|
2061
2142
|
if (url.pathname === "/" && req.method === "GET") {
|
|
@@ -2322,6 +2403,7 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
2322
2403
|
}
|
|
2323
2404
|
}
|
|
2324
2405
|
} catch (error) {
|
|
2406
|
+
unregisterQuotaAutoRefresh?.();
|
|
2325
2407
|
userCostOverlayReconciler?.stop();
|
|
2326
2408
|
backgroundLifecycle?.releaseAfterFailedStart();
|
|
2327
2409
|
void nativeMainLifecycle.release();
|
|
@@ -2347,7 +2429,11 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
2347
2429
|
? [() => managementIngressRef.stop(closeActiveConnections)]
|
|
2348
2430
|
: []),
|
|
2349
2431
|
async () => {
|
|
2350
|
-
|
|
2432
|
+
try {
|
|
2433
|
+
userCostOverlayReconciler?.stop();
|
|
2434
|
+
} finally {
|
|
2435
|
+
unregisterQuotaAutoRefresh?.();
|
|
2436
|
+
}
|
|
2351
2437
|
},
|
|
2352
2438
|
],
|
|
2353
2439
|
async () => {
|
|
@@ -70,7 +70,7 @@ import type { PersistedUsageAttempt } from "../../usage/log";
|
|
|
70
70
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
71
71
|
import { applySystemEnvToggle } from "../system-env";
|
|
72
72
|
|
|
73
|
-
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
|
|
73
|
+
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchInitializedModels as fetchAllModels, fetchGrokCandidateModels, buildClaudeDesktopState } from "./shared";
|
|
74
74
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
75
75
|
import { readManagementJsonBody, readOptionalManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
76
76
|
|
|
@@ -377,7 +377,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
377
377
|
// sequence after the transition. A failure here is a persistence failure (the
|
|
378
378
|
// writers' ok:false result or a throw from the underlying atomic write helper),
|
|
379
379
|
// reported as 502 naming the failed key plus the writes that already landed.
|
|
380
|
-
// NOTE: do not name that helper literally here — tests/grok-writer-boundary.test.ts
|
|
380
|
+
// NOTE: do not name that helper literally here — tests/providers/xai/grok-writer-boundary.test.ts
|
|
381
381
|
// asserts this route file contains no direct write primitive, and matches on the
|
|
382
382
|
// symbol name even inside a comment.
|
|
383
383
|
const scalarWrites: Array<{ field: string; run: () => { ok: true; changed: boolean } | { ok: false; error: string } }> = [];
|