@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
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
setDebugSettings,
|
|
53
53
|
type DebugFlag,
|
|
54
54
|
} from "../../lib/debug-settings";
|
|
55
|
-
import type { OcxClaudeCodeConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
55
|
+
import type { OcxClaudeCodeConfig, OcxComboConfig, OcxConfig, OcxCustomModel, OcxProviderConfig } from "../../types";
|
|
56
56
|
import { drainAndShutdown } from "../lifecycle";
|
|
57
57
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
58
58
|
import { filterRequestLogs, getRequestLogEntries, type RequestLogEntry } from "../request-log";
|
|
@@ -66,6 +66,7 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
66
66
|
import type { ManagementContext } from "./context";
|
|
67
67
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
68
68
|
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
69
|
+
import { COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS } from "../../combos";
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
@@ -75,15 +76,29 @@ import { shadowCallTargetError } from "./shadow-call-validation";
|
|
|
75
76
|
* materialized in every user's config.json.
|
|
76
77
|
*/
|
|
77
78
|
function sparseComboConfig<T extends {
|
|
79
|
+
cooldownMs?: number;
|
|
80
|
+
waitForCooldownMs?: number;
|
|
78
81
|
imageInput?: "auto" | "disabled";
|
|
79
82
|
reasoningEffortMode?: "strict" | "adaptive";
|
|
80
|
-
}>(combo: T): Omit<T, "imageInput" | "reasoningEffortMode"> & {
|
|
83
|
+
}>(combo: T): Omit<T, "cooldownMs" | "waitForCooldownMs" | "imageInput" | "reasoningEffortMode"> & {
|
|
84
|
+
cooldownMs?: number;
|
|
85
|
+
waitForCooldownMs?: number;
|
|
81
86
|
imageInput?: "disabled";
|
|
82
87
|
reasoningEffortMode?: "adaptive";
|
|
83
88
|
} {
|
|
84
|
-
const {
|
|
89
|
+
const {
|
|
90
|
+
cooldownMs,
|
|
91
|
+
waitForCooldownMs,
|
|
92
|
+
imageInput,
|
|
93
|
+
reasoningEffortMode,
|
|
94
|
+
...rest
|
|
95
|
+
} = combo;
|
|
85
96
|
return {
|
|
86
97
|
...rest,
|
|
98
|
+
...(cooldownMs !== undefined ? { cooldownMs } : {}),
|
|
99
|
+
...(waitForCooldownMs !== undefined && waitForCooldownMs !== COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS
|
|
100
|
+
? { waitForCooldownMs }
|
|
101
|
+
: {}),
|
|
87
102
|
...(imageInput === "disabled" ? { imageInput: "disabled" as const } : {}),
|
|
88
103
|
...(reasoningEffortMode === "adaptive" ? { reasoningEffortMode: "adaptive" as const } : {}),
|
|
89
104
|
};
|
|
@@ -141,13 +156,28 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
141
156
|
comboPublicModelId,
|
|
142
157
|
normalizeComboConfig,
|
|
143
158
|
} = await import("../../combos");
|
|
144
|
-
const
|
|
159
|
+
const sourceId = renameFrom ?? id;
|
|
160
|
+
const previous = config.combos?.[sourceId];
|
|
161
|
+
if (!isPlainRecord(body.combo)) {
|
|
162
|
+
return jsonResponse({ error: "combo must be an object" }, 400);
|
|
163
|
+
}
|
|
164
|
+
const requestedCombo: Record<string, unknown> = body.combo;
|
|
165
|
+
const effectiveCombo = {
|
|
166
|
+
...requestedCombo,
|
|
167
|
+
...(!Object.hasOwn(requestedCombo, "cooldownMs") && previous?.cooldownMs !== undefined
|
|
168
|
+
? { cooldownMs: previous.cooldownMs }
|
|
169
|
+
: {}),
|
|
170
|
+
...(!Object.hasOwn(requestedCombo, "waitForCooldownMs") && previous?.waitForCooldownMs !== undefined
|
|
171
|
+
? { waitForCooldownMs: previous.waitForCooldownMs }
|
|
172
|
+
: {}),
|
|
173
|
+
};
|
|
174
|
+
const error = comboConfigError(id, effectiveCombo, config.providers, {
|
|
145
175
|
requireEnabledTarget: true,
|
|
146
176
|
combos: config.combos,
|
|
147
|
-
excludeComboId:
|
|
177
|
+
excludeComboId: sourceId,
|
|
148
178
|
});
|
|
149
179
|
if (error) return jsonResponse({ error }, 400);
|
|
150
|
-
const normalized = normalizeComboConfig(
|
|
180
|
+
const normalized = normalizeComboConfig(effectiveCombo as unknown as OcxComboConfig);
|
|
151
181
|
// Persist only non-default identity/capability fields so config stays sparse.
|
|
152
182
|
// Capability defaults (`imageInput`, `reasoningEffortMode`) go through the same
|
|
153
183
|
// helper the GET/PUT responses use, so the wire shape and the stored shape cannot drift.
|
|
@@ -157,14 +187,12 @@ export async function handleComboRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
157
187
|
displayName: normalizedDisplayName,
|
|
158
188
|
...normalizedBase
|
|
159
189
|
} = sparseComboConfig(normalized);
|
|
160
|
-
const stored:
|
|
190
|
+
const stored: OcxComboConfig = {
|
|
161
191
|
...normalizedBase,
|
|
162
192
|
...(normalizedAlias ? { alias: normalizedAlias } : {}),
|
|
163
193
|
...(normalizedNativeAlias ? { nativeAlias: true } : {}),
|
|
164
194
|
...(normalizedDisplayName ? { displayName: normalizedDisplayName } : {}),
|
|
165
195
|
};
|
|
166
|
-
const sourceId = renameFrom ?? id;
|
|
167
|
-
const previous = config.combos?.[sourceId];
|
|
168
196
|
const oldPublicModel = previous ? comboPublicModelId(sourceId, previous) : null;
|
|
169
197
|
const newPublicModel = comboPublicModelId(id, normalized);
|
|
170
198
|
const disabledIdentityChanged = previous !== undefined && (
|
|
@@ -41,6 +41,15 @@ import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../provid
|
|
|
41
41
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
42
42
|
import { clearThreadAccountMap } from "../../codex/routing";
|
|
43
43
|
import { primeCodexPoolQuotas } from "../../codex/auth-api";
|
|
44
|
+
import { isSelectableCodexPoolAccount } from "../../codex/account-id";
|
|
45
|
+
import { isCodexAccountPriorityKey } from "../../codex/account-priority";
|
|
46
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../../codex/main-account";
|
|
47
|
+
import { getAccountQuota } from "../../codex/quota";
|
|
48
|
+
import {
|
|
49
|
+
codexQuotaAutoRefreshStatus,
|
|
50
|
+
runCodexQuotaAutoRefresh,
|
|
51
|
+
} from "../../codex/quota-auto-refresh";
|
|
52
|
+
import { getMainAccountHardLockStatus } from "../../codex/main-account-hard-lock";
|
|
44
53
|
import {
|
|
45
54
|
codexAccountPickerEnabled,
|
|
46
55
|
initializeDefaultCodexAccountNamespaces,
|
|
@@ -106,6 +115,13 @@ import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, C
|
|
|
106
115
|
import type { ManagementContext } from "./context";
|
|
107
116
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
108
117
|
|
|
118
|
+
function quotaAutoRefreshSettings(config: OcxConfig) {
|
|
119
|
+
return Object.fromEntries(Object.entries(config.codexQuotaAutoRefresh ?? {}).map(([id, setting]) => [
|
|
120
|
+
id,
|
|
121
|
+
{ fiveHour: setting.fiveHour === true, weekly: setting.weekly === true },
|
|
122
|
+
]));
|
|
123
|
+
}
|
|
124
|
+
|
|
109
125
|
async function sidecarVisionResponseSettings(config: OcxConfig): Promise<{
|
|
110
126
|
model: string;
|
|
111
127
|
reasoning: string;
|
|
@@ -300,9 +316,15 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
300
316
|
streamMode: config.streamMode ?? "auto",
|
|
301
317
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
302
318
|
codexAccountPickerEnabled: codexAccountPickerEnabled(config),
|
|
319
|
+
codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
|
|
303
320
|
// Absent means hidden, so the GUI renders the switch without having to know that
|
|
304
321
|
// `undefined` and `false` mean the same thing.
|
|
305
322
|
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
323
|
+
// Absent means off, same convention: the GUI renders a plain switch without
|
|
324
|
+
// needing to know that `undefined` and `false` mean the same thing here.
|
|
325
|
+
ultraFastTier: config.ultraFastTier === true,
|
|
326
|
+
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
327
|
+
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
306
328
|
// Absent means the historical auto-open, so the GUI can render the toggle
|
|
307
329
|
// without having to know that `undefined` and `true` mean the same thing.
|
|
308
330
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
@@ -392,18 +414,24 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
392
414
|
streamMode?: unknown;
|
|
393
415
|
appOwnedMemoryBudgetMb?: unknown;
|
|
394
416
|
codexAccountPickerEnabled?: unknown;
|
|
417
|
+
codexQuotaAutoRefresh?: unknown;
|
|
395
418
|
oauthOpenBrowser?: unknown;
|
|
396
419
|
showCodexSparkQuota?: unknown;
|
|
420
|
+
ultraFastTier?: unknown;
|
|
421
|
+
codexMainAccountHardLock?: unknown;
|
|
397
422
|
codexDesktopAuthless?: unknown;
|
|
398
423
|
};
|
|
399
424
|
if (body.codexAutoStart === undefined
|
|
400
425
|
&& body.streamMode === undefined
|
|
401
426
|
&& body.appOwnedMemoryBudgetMb === undefined
|
|
402
427
|
&& body.codexAccountPickerEnabled === undefined
|
|
428
|
+
&& body.codexQuotaAutoRefresh === undefined
|
|
403
429
|
&& body.oauthOpenBrowser === undefined
|
|
404
430
|
&& body.showCodexSparkQuota === undefined
|
|
431
|
+
&& body.ultraFastTier === undefined
|
|
432
|
+
&& body.codexMainAccountHardLock === undefined
|
|
405
433
|
&& body.codexDesktopAuthless === undefined) {
|
|
406
|
-
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, oauthOpenBrowser, showCodexSparkQuota, or codexDesktopAuthless" }, 400);
|
|
434
|
+
return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexSparkQuota, ultraFastTier, codexMainAccountHardLock, or codexDesktopAuthless" }, 400);
|
|
407
435
|
}
|
|
408
436
|
if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
|
|
409
437
|
return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
|
|
@@ -421,9 +449,36 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
421
449
|
if (body.showCodexSparkQuota !== undefined && typeof body.showCodexSparkQuota !== "boolean") {
|
|
422
450
|
return jsonResponse({ error: "showCodexSparkQuota boolean is required" }, 400);
|
|
423
451
|
}
|
|
452
|
+
if (body.ultraFastTier !== undefined && typeof body.ultraFastTier !== "boolean") {
|
|
453
|
+
return jsonResponse({ error: "ultraFastTier boolean is required" }, 400);
|
|
454
|
+
}
|
|
455
|
+
if (body.codexMainAccountHardLock !== undefined && typeof body.codexMainAccountHardLock !== "boolean") {
|
|
456
|
+
return jsonResponse({ error: "codexMainAccountHardLock boolean is required" }, 400);
|
|
457
|
+
}
|
|
424
458
|
if (body.codexDesktopAuthless !== undefined && typeof body.codexDesktopAuthless !== "boolean") {
|
|
425
459
|
return jsonResponse({ error: "codexDesktopAuthless boolean is required" }, 400);
|
|
426
460
|
}
|
|
461
|
+
let quotaAutoRefreshChange: { id: string; window: "fiveHour" | "weekly"; enabled: boolean } | undefined;
|
|
462
|
+
if (body.codexQuotaAutoRefresh !== undefined) {
|
|
463
|
+
if (!isPlainRecord(body.codexQuotaAutoRefresh)) {
|
|
464
|
+
return jsonResponse({ error: "codexQuotaAutoRefresh must be an object" }, 400);
|
|
465
|
+
}
|
|
466
|
+
const change = body.codexQuotaAutoRefresh;
|
|
467
|
+
const id = typeof change.id === "string" ? change.id.trim() : "";
|
|
468
|
+
if (!isCodexAccountPriorityKey(id)) return jsonResponse({ error: "Invalid account id format" }, 400);
|
|
469
|
+
if (change.window !== "fiveHour" && change.window !== "weekly") {
|
|
470
|
+
return jsonResponse({ error: "window must be fiveHour or weekly" }, 400);
|
|
471
|
+
}
|
|
472
|
+
if (typeof change.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
473
|
+
const exists = id === MAIN_CODEX_ACCOUNT_ID
|
|
474
|
+
|| (config.codexAccounts ?? []).some(account => isSelectableCodexPoolAccount(account) && account.id === id);
|
|
475
|
+
if (!exists) return jsonResponse({ error: "Account not found" }, 404);
|
|
476
|
+
const status = codexQuotaAutoRefreshStatus(config, id, getAccountQuota(id));
|
|
477
|
+
if (change.enabled && !status[change.window === "fiveHour" ? "fiveHourAvailable" : "weeklyAvailable"]) {
|
|
478
|
+
return jsonResponse({ error: "Quota window is not available for this account" }, 409);
|
|
479
|
+
}
|
|
480
|
+
quotaAutoRefreshChange = { id, window: change.window, enabled: change.enabled };
|
|
481
|
+
}
|
|
427
482
|
if (body.appOwnedMemoryBudgetMb !== undefined && (
|
|
428
483
|
typeof body.appOwnedMemoryBudgetMb !== "number"
|
|
429
484
|
|| !Number.isInteger(body.appOwnedMemoryBudgetMb)
|
|
@@ -443,10 +498,16 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
443
498
|
hasCodexAccountNamespaces: Object.hasOwn(config, "codexAccountNamespaces"),
|
|
444
499
|
codexAccountPickerEnabled: config.codexAccountPickerEnabled,
|
|
445
500
|
hasCodexAccountPickerEnabled: Object.hasOwn(config, "codexAccountPickerEnabled"),
|
|
501
|
+
codexQuotaAutoRefresh: config.codexQuotaAutoRefresh,
|
|
502
|
+
hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"),
|
|
446
503
|
oauthOpenBrowser: config.oauthOpenBrowser,
|
|
447
504
|
hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
|
|
448
505
|
showCodexSparkQuota: config.showCodexSparkQuota,
|
|
449
506
|
hasShowCodexSparkQuota: Object.hasOwn(config, "showCodexSparkQuota"),
|
|
507
|
+
ultraFastTier: config.ultraFastTier,
|
|
508
|
+
hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
|
|
509
|
+
codexMainAccountHardLock: config.codexMainAccountHardLock,
|
|
510
|
+
hasCodexMainAccountHardLock: Object.hasOwn(config, "codexMainAccountHardLock"),
|
|
450
511
|
codexDesktopAuthless: config.codexDesktopAuthless,
|
|
451
512
|
hasCodexDesktopAuthless: Object.hasOwn(config, "codexDesktopAuthless"),
|
|
452
513
|
};
|
|
@@ -479,8 +540,25 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
479
540
|
if (typeof body.showCodexSparkQuota === "boolean") {
|
|
480
541
|
config.showCodexSparkQuota = body.showCodexSparkQuota;
|
|
481
542
|
}
|
|
543
|
+
// Off deletes the key rather than persisting `false`: absent is the documented
|
|
544
|
+
// default, and a written `false` would survive as a decision nobody made.
|
|
545
|
+
if (body.ultraFastTier === true) config.ultraFastTier = true;
|
|
546
|
+
else if (body.ultraFastTier === false) deleteConfigTopLevelKey(config, "ultraFastTier");
|
|
547
|
+
if (body.codexMainAccountHardLock === true) config.codexMainAccountHardLock = true;
|
|
548
|
+
else if (body.codexMainAccountHardLock === false) deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
482
549
|
if (body.codexDesktopAuthless === true) config.codexDesktopAuthless = true;
|
|
483
550
|
else if (body.codexDesktopAuthless === false) deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
551
|
+
if (quotaAutoRefreshChange) {
|
|
552
|
+
const { id, window, enabled } = quotaAutoRefreshChange;
|
|
553
|
+
const setting = { ...(config.codexQuotaAutoRefresh?.[id] ?? {}) };
|
|
554
|
+
if (enabled) setting[window] = true;
|
|
555
|
+
else delete setting[window];
|
|
556
|
+
const all = { ...(config.codexQuotaAutoRefresh ?? {}) };
|
|
557
|
+
if (Object.keys(setting).length > 0) all[id] = setting;
|
|
558
|
+
else delete all[id];
|
|
559
|
+
if (Object.keys(all).length > 0) config.codexQuotaAutoRefresh = all;
|
|
560
|
+
else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh");
|
|
561
|
+
}
|
|
484
562
|
pickerIsEnabled = codexAccountPickerEnabled(config);
|
|
485
563
|
(deps.saveConfigPreservingClaudeCode ?? saveConfigPreservingClaudeCode)(config);
|
|
486
564
|
} catch (error) {
|
|
@@ -497,12 +575,21 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
497
575
|
if (previousSettings.hasCodexAccountPickerEnabled) {
|
|
498
576
|
config.codexAccountPickerEnabled = previousSettings.codexAccountPickerEnabled;
|
|
499
577
|
} else deleteConfigTopLevelKey(config, "codexAccountPickerEnabled");
|
|
578
|
+
if (previousSettings.hasCodexQuotaAutoRefresh) {
|
|
579
|
+
config.codexQuotaAutoRefresh = previousSettings.codexQuotaAutoRefresh;
|
|
580
|
+
} else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh");
|
|
500
581
|
if (previousSettings.hasOauthOpenBrowser) {
|
|
501
582
|
config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
|
|
502
583
|
} else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
|
|
503
584
|
if (previousSettings.hasShowCodexSparkQuota) {
|
|
504
585
|
config.showCodexSparkQuota = previousSettings.showCodexSparkQuota;
|
|
505
586
|
} else deleteConfigTopLevelKey(config, "showCodexSparkQuota");
|
|
587
|
+
if (previousSettings.hasUltraFastTier) {
|
|
588
|
+
config.ultraFastTier = previousSettings.ultraFastTier;
|
|
589
|
+
} else deleteConfigTopLevelKey(config, "ultraFastTier");
|
|
590
|
+
if (previousSettings.hasCodexMainAccountHardLock) {
|
|
591
|
+
config.codexMainAccountHardLock = previousSettings.codexMainAccountHardLock;
|
|
592
|
+
} else deleteConfigTopLevelKey(config, "codexMainAccountHardLock");
|
|
506
593
|
if (previousSettings.hasCodexDesktopAuthless) {
|
|
507
594
|
config.codexDesktopAuthless = previousSettings.codexDesktopAuthless;
|
|
508
595
|
} else deleteConfigTopLevelKey(config, "codexDesktopAuthless");
|
|
@@ -522,16 +609,20 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
522
609
|
? catalogRefreshIsPending(catalogRefresh)
|
|
523
610
|
: false;
|
|
524
611
|
invalidateStartupHealthCache();
|
|
612
|
+
if (quotaAutoRefreshChange) void runCodexQuotaAutoRefresh(config);
|
|
525
613
|
return jsonResponse({
|
|
526
614
|
ok: true,
|
|
527
615
|
codexAutoStart: codexAutoStartEnabled(config),
|
|
528
616
|
streamMode: config.streamMode ?? "auto",
|
|
529
617
|
appOwnedMemoryBudgetMb: config.appOwnedMemoryBudgetMb ?? 256,
|
|
530
618
|
codexAccountPickerEnabled: pickerIsEnabled,
|
|
619
|
+
codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
|
|
531
620
|
oauthOpenBrowser: config.oauthOpenBrowser !== false,
|
|
532
621
|
catalogRefreshPending,
|
|
533
622
|
showCodexSparkQuota: config.showCodexSparkQuota === true,
|
|
534
623
|
codexDesktopAuthless: authlessIsEnabled,
|
|
624
|
+
codexMainAccountHardLock: config.codexMainAccountHardLock === true,
|
|
625
|
+
mainAccountHardLock: getMainAccountHardLockStatus(config),
|
|
535
626
|
startupHealth: await readStartupHealth(config),
|
|
536
627
|
});
|
|
537
628
|
}
|
|
@@ -679,7 +770,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
679
770
|
}
|
|
680
771
|
// Reject ONLY a model we can prove is blind. An id nothing knows about stays
|
|
681
772
|
// allowed: the operator may be ahead of our catalog, and the runtime never
|
|
682
|
-
// required catalog membership (`tests/vision-reasoning-contract.test.ts`
|
|
773
|
+
// required catalog membership (`tests/vision/vision-reasoning-contract.test.ts`
|
|
683
774
|
// pins `custom-vision` → 200). The catalog is read ONCE and reused for the
|
|
684
775
|
// rejection body, so a 400 cannot cost two provider fetches.
|
|
685
776
|
if (body.vision && typeof body.vision.model === "string" && body.vision.model !== "") {
|
|
@@ -76,6 +76,15 @@ export interface IntegrationJournalRow {
|
|
|
76
76
|
configPath: string;
|
|
77
77
|
snapshot: "none" | "stored" | "expired";
|
|
78
78
|
undoable: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* May the operator retire this row?
|
|
81
|
+
*
|
|
82
|
+
* Computed HERE, not in the GUI, because the DELETE route enforces the same
|
|
83
|
+
* rule and two copies of it would drift. False for a client newest row: it
|
|
84
|
+
* is the undo entry point (`undoable` above keys off exactly this), and it
|
|
85
|
+
* is what a user reaches for right after the mistake.
|
|
86
|
+
*/
|
|
87
|
+
deletable: boolean;
|
|
79
88
|
}
|
|
80
89
|
|
|
81
90
|
export interface IntegrationToggleBody {
|
|
@@ -293,6 +302,94 @@ function writerFailureResponse(
|
|
|
293
302
|
}, 500, ctx.req, ctx.config);
|
|
294
303
|
}
|
|
295
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Who asked for this deletion, as an audit value that is safe to persist.
|
|
307
|
+
*
|
|
308
|
+
* The tombstone lives in an append-only log the user can read, so this must be
|
|
309
|
+
* a principal NAME and nothing else -- never the admin token, a session id, or
|
|
310
|
+
* a filesystem path. `principal` is undefined only in direct-dispatch tests,
|
|
311
|
+
* which the auth gate documents as the untrusted admin-token case.
|
|
312
|
+
*/
|
|
313
|
+
function journalDeletePrincipal(ctx: ManagementContext): string {
|
|
314
|
+
return ctx.principal ?? "admin-token";
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Retire one journal row at the operator request.
|
|
319
|
+
*
|
|
320
|
+
* The opId travels in the QUERY STRING, matching DELETE
|
|
321
|
+
* /api/codex-auth/accounts?id= -- the repository other DELETE-by-identifier. A
|
|
322
|
+
* body on DELETE is legal but unevenly handled by intermediaries, and there is
|
|
323
|
+
* nothing here a query cannot carry.
|
|
324
|
+
*/
|
|
325
|
+
async function handleJournalDelete(ctx: ManagementContext): Promise<Response> {
|
|
326
|
+
const { req, url } = ctx;
|
|
327
|
+
const opId = url.searchParams.get("opId")?.trim();
|
|
328
|
+
if (!opId) {
|
|
329
|
+
return jsonResponse({
|
|
330
|
+
error: "opId must be a non-empty string",
|
|
331
|
+
code: "invalid_op_id",
|
|
332
|
+
}, 400, req, ctx.config);
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const store = integrationStore();
|
|
336
|
+
const operation = store.findOperation(opId);
|
|
337
|
+
if (!operation) {
|
|
338
|
+
// Already retired, or never existed. Both are 404: the tombstone hides
|
|
339
|
+
// the row from findOperation, so a double-click is idempotent here
|
|
340
|
+
// rather than a second deletion of something.
|
|
341
|
+
return jsonResponse({
|
|
342
|
+
error: "integration operation not found",
|
|
343
|
+
code: "integration_operation_not_found",
|
|
344
|
+
opId,
|
|
345
|
+
}, 404, req, ctx.config);
|
|
346
|
+
}
|
|
347
|
+
/*
|
|
348
|
+
* The newest row per client is refused, and refused by the SERVER even
|
|
349
|
+
* though the GUI already hides its button. The button is a courtesy; this
|
|
350
|
+
* is the rule. An admin-token caller has no GUI at all.
|
|
351
|
+
*
|
|
352
|
+
* Re-read immediately before the write: a restore that landed while the
|
|
353
|
+
* dialog was open appends a new row and changes which opId is newest.
|
|
354
|
+
*/
|
|
355
|
+
const newest = store.listOperations(operation.clientId, 1)[0];
|
|
356
|
+
if (newest?.opId === opId) {
|
|
357
|
+
return jsonResponse({
|
|
358
|
+
error: "the newest operation for a client cannot be deleted",
|
|
359
|
+
code: "integration_journal_newest_protected",
|
|
360
|
+
clientId: operation.clientId,
|
|
361
|
+
opId,
|
|
362
|
+
}, 409, req, ctx.config);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
store.retireOperation({
|
|
366
|
+
tombstone: opId,
|
|
367
|
+
at: new Date().toISOString(),
|
|
368
|
+
by: journalDeletePrincipal(ctx),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
/*
|
|
372
|
+
* Snapshot bytes go too, and go AFTER the tombstone -- the same post-commit
|
|
373
|
+
* ordering appendOperation uses (journal.ts rule 1). If this fails, the row
|
|
374
|
+
* is still retired and retentionDegraded discloses the leftover file; the
|
|
375
|
+
* reverse order would delete a user backup for a deletion that then failed
|
|
376
|
+
* to record.
|
|
377
|
+
*/
|
|
378
|
+
const pruned = store.pruneSnapshots(operation.clientId);
|
|
379
|
+
if (pruned.ok) store.clearPruneFailure(operation.clientId);
|
|
380
|
+
else store.markPruneFailure(operation.clientId, pruned.error);
|
|
381
|
+
|
|
382
|
+
return jsonResponse({
|
|
383
|
+
ok: true,
|
|
384
|
+
opId,
|
|
385
|
+
clientId: operation.clientId,
|
|
386
|
+
snapshotRemoved: pruned.ok,
|
|
387
|
+
}, 200, req, ctx.config);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
return internalErrorResponse(error, ctx);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
296
393
|
export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
297
394
|
const { req, url } = ctx;
|
|
298
395
|
|
|
@@ -319,6 +416,10 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
319
416
|
}
|
|
320
417
|
}
|
|
321
418
|
|
|
419
|
+
if (url.pathname === "/api/client-integrations/journal" && req.method === "DELETE") {
|
|
420
|
+
return handleJournalDelete(ctx);
|
|
421
|
+
}
|
|
422
|
+
|
|
322
423
|
if (url.pathname === "/api/client-integrations/journal") {
|
|
323
424
|
if (req.method !== "GET") return null;
|
|
324
425
|
const requestedClient = url.searchParams.get("client");
|
|
@@ -371,6 +472,13 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise<R
|
|
|
371
472
|
const current = currentConfigText(operation.configPath);
|
|
372
473
|
return current === undefined ? false : matchesOperationResult(operation, current);
|
|
373
474
|
})(),
|
|
475
|
+
/*
|
|
476
|
+
* Deliberately NOT an `undoable` derivative. The two axes are
|
|
477
|
+
* independent: an expired row is undoable-false and deletable-true,
|
|
478
|
+
* which is the pairing this route exists to produce -- a row whose
|
|
479
|
+
* bytes are gone previously carried no action at all.
|
|
480
|
+
*/
|
|
481
|
+
deletable: newestByClient.get(operation.clientId) !== operation.opId,
|
|
374
482
|
};
|
|
375
483
|
});
|
|
376
484
|
return jsonResponse({ operations } satisfies IntegrationJournalEnvelope, 200, req, ctx.config);
|
|
@@ -154,6 +154,7 @@ import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostR
|
|
|
154
154
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
155
155
|
import type { ManagementContext } from "./context";
|
|
156
156
|
import { listManagementModelRows, loadExportModels } from "./model-rows";
|
|
157
|
+
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
157
158
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
158
159
|
import {
|
|
159
160
|
hasModelPreset,
|
|
@@ -536,6 +537,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
536
537
|
}
|
|
537
538
|
|
|
538
539
|
const providerConfig = hasOwnProvider(config.providers, provider) ? config.providers[provider] : undefined;
|
|
540
|
+
if (initialModelSelectionPending(providerConfig)) {
|
|
541
|
+
return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
|
|
542
|
+
}
|
|
539
543
|
const isVirtualComboNamespace = provider === COMBO_NAMESPACE && !preservesPhysicalComboProvider(config);
|
|
540
544
|
if (!providerConfig && provider !== "openai" && !isVirtualComboNamespace) {
|
|
541
545
|
return jsonResponse({ error: "unknown model visibility provider" }, 400);
|
|
@@ -836,7 +840,13 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
836
840
|
if (mode !== "preset" && mode !== "all" && mode !== "custom") {
|
|
837
841
|
return jsonResponse({ error: "mode must be preset, all, or custom" }, 400);
|
|
838
842
|
}
|
|
843
|
+
if (mode === "preset" && !hasModelPreset(provider)) {
|
|
844
|
+
return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
|
|
845
|
+
}
|
|
839
846
|
const target = config.providers[provider];
|
|
847
|
+
if (initialModelSelectionPending(target)) {
|
|
848
|
+
return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
|
|
849
|
+
}
|
|
840
850
|
if (mode === "all") {
|
|
841
851
|
// Same effect as today's empty-list PUT: no allowlist, no marker to reconcile.
|
|
842
852
|
delete target.selectedModels;
|
|
@@ -851,9 +861,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
851
861
|
persistConfig(config);
|
|
852
862
|
return jsonResponse({ ok: true, provider, mode, selected: [...(target.selectedModels ?? [])] });
|
|
853
863
|
}
|
|
854
|
-
if (!hasModelPreset(provider)) {
|
|
855
|
-
return jsonResponse({ error: `no model preset is shipped for provider '${provider}'` }, 400);
|
|
856
|
-
}
|
|
857
864
|
const models = await fetchAllModels(config);
|
|
858
865
|
const catalogIds = models.filter(m => m.provider === provider).map(m => m.id);
|
|
859
866
|
const presetIds = materializeModelPreset(provider, catalogIds);
|
|
@@ -900,6 +907,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
900
907
|
if (!provider || !hasOwnProvider(config.providers, provider)) {
|
|
901
908
|
return jsonResponse({ error: "unknown provider" }, provider ? 404 : 400);
|
|
902
909
|
}
|
|
910
|
+
if (initialModelSelectionPending(config.providers[provider])) {
|
|
911
|
+
return jsonResponse({ error: "Initial model discovery is pending. Refresh the model list and retry.", code: "initial_model_selection_pending" }, 409);
|
|
912
|
+
}
|
|
903
913
|
const models = Array.isArray(body.models)
|
|
904
914
|
? [...new Set(body.models.filter((m): m is string => typeof m === "string"))]
|
|
905
915
|
: [];
|
|
@@ -27,6 +27,9 @@ import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
|
27
27
|
import type { OcxConfig } from "../../types";
|
|
28
28
|
import { ensureCodexEntitlementFreshness } from "../../codex/model-entitlements";
|
|
29
29
|
import { fetchAllModels } from "./shared";
|
|
30
|
+
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
31
|
+
import { catalogFastRowEligible, fastRowId } from "../fast-row";
|
|
32
|
+
import { knownEffortRowIds } from "../effort-row";
|
|
30
33
|
|
|
31
34
|
/**
|
|
32
35
|
* One row of the `/api/models` list. Routed rows spread a `CatalogModel`, so the shape is
|
|
@@ -38,9 +41,11 @@ export type ManagementModelRow = Partial<CatalogModel> & {
|
|
|
38
41
|
id: string;
|
|
39
42
|
namespaced: string;
|
|
40
43
|
disabled: boolean;
|
|
44
|
+
initialSelectionPending?: boolean;
|
|
41
45
|
native?: boolean;
|
|
42
46
|
custom?: boolean;
|
|
43
47
|
customId?: string;
|
|
48
|
+
fastRowAvailable?: boolean;
|
|
44
49
|
displayNameOverride?: string;
|
|
45
50
|
displayNameSource?: "operator" | "provider" | "fallback";
|
|
46
51
|
};
|
|
@@ -164,7 +169,20 @@ export async function listManagementModelRows(
|
|
|
164
169
|
...(contextCap !== undefined ? { contextCap, contextCapped: m.contextCapped === true } : {}),
|
|
165
170
|
};
|
|
166
171
|
}).filter((row): row is ManagementModelRow => row !== null);
|
|
167
|
-
|
|
172
|
+
const rows = [...native, ...dedupedRouted, ...visibleCustomModels];
|
|
173
|
+
// Include disabled rows and configured aliases before the export visibility filter:
|
|
174
|
+
// a hidden real `x--fast` must never become a synthetic selector for another model.
|
|
175
|
+
const knownIds = config.fastRows === false ? new Set<string>() : knownEffortRowIds(config);
|
|
176
|
+
for (const row of rows) knownIds.add(row.namespaced);
|
|
177
|
+
return rows.map(row => {
|
|
178
|
+
const pending = initialModelSelectionPending(config.providers[row.provider]);
|
|
179
|
+
return {
|
|
180
|
+
...row,
|
|
181
|
+
...(pending ? { disabled: true, initialSelectionPending: true } : {}),
|
|
182
|
+
fastRowAvailable: !row.disabled && !pending
|
|
183
|
+
&& !knownIds.has(fastRowId(row.namespaced)) && catalogFastRowEligible(config, row),
|
|
184
|
+
};
|
|
185
|
+
});
|
|
168
186
|
}
|
|
169
187
|
|
|
170
188
|
/** `/api/models` row → the narrower input the client-config serializers accept. */
|
|
@@ -173,6 +191,7 @@ export function toExportModel(row: ManagementModelRow): ExportModel {
|
|
|
173
191
|
namespaced: row.namespaced,
|
|
174
192
|
provider: row.provider,
|
|
175
193
|
id: row.id,
|
|
194
|
+
fastRowAvailable: row.fastRowAvailable === true,
|
|
176
195
|
...(row.native ? { native: true } : {}),
|
|
177
196
|
...(row.displayName && row.displayNameSource !== "fallback" ? { displayName: row.displayName } : {}),
|
|
178
197
|
...(row.contextWindow !== undefined ? { contextWindow: row.contextWindow } : {}),
|
|
@@ -17,9 +17,11 @@
|
|
|
17
17
|
* Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
|
+
import { join } from "node:path";
|
|
20
21
|
import { loadConfig, saveConfigPreservingClaudeCode } from "../../config";
|
|
21
22
|
import { readRuntimePort } from "../../config/process-state";
|
|
22
23
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits } from "../../codex/catalog";
|
|
24
|
+
import { getCodexHome } from "../../codex/paths";
|
|
23
25
|
import { providerContextCap } from "../../providers/context-cap";
|
|
24
26
|
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
25
27
|
import { inspectDesktop3pConfigLibrary, removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
@@ -682,8 +684,9 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
682
684
|
|
|
683
685
|
if (url.pathname === "/api/native-integrations" && req.method === "GET") {
|
|
684
686
|
const { getConfigPath } = await import("../../config");
|
|
687
|
+
const codexConfigPath = join(getCodexHome(), "config.toml");
|
|
685
688
|
return jsonResponse({
|
|
686
|
-
clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config,
|
|
689
|
+
clients: [claudeStatus(config, getConfigPath()), grokStatus(config), codexStatus(config, codexConfigPath), desktopStatus(config)],
|
|
687
690
|
} satisfies NativeStatusListEnvelope);
|
|
688
691
|
}
|
|
689
692
|
|