@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
|
@@ -148,30 +148,51 @@ export function hasFailoverAccountQuorum(providerName: string, now = Date.now())
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
/**
|
|
151
|
-
* Whether
|
|
151
|
+
* Whether REACTIVE 429 rotation is active for this provider.
|
|
152
152
|
*
|
|
153
|
-
*
|
|
153
|
+
* Presence is the only rule: two or more eligible stored accounts. The
|
|
154
|
+
* `oauthAccountFailover.enabled` booleans no longer suppress it.
|
|
154
155
|
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
156
|
+
* That is a deliberate narrowing of #2568d. Rotation here runs only after upstream has already
|
|
157
|
+
* refused the request, so the choice the old knob offered was between "retry on the second
|
|
158
|
+
* account you deliberately logged in" and "return a 429 while that account sits idle". The
|
|
159
|
+
* second is a defect, not a preference — and an operator who does not want rotation expresses
|
|
160
|
+
* that by not storing a second account, exactly as they do for `apiKeyPool`.
|
|
160
161
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
162
|
+
* The knob is not gone. It still governs {@link isProactivePreferenceEnabled}, which decides
|
|
163
|
+
* whether a HEALTHY request may be steered to a different account before dispatch — a real
|
|
164
|
+
* behavioural choice that remains refusable — and it still carries `strategy` and
|
|
165
|
+
* `autoSwitchThreshold`.
|
|
163
166
|
*/
|
|
164
167
|
export function isGenericOAuthFailoverEnabled(
|
|
165
168
|
config: OcxConfig,
|
|
166
169
|
providerName: string,
|
|
167
170
|
now = Date.now(),
|
|
168
171
|
): boolean {
|
|
172
|
+
const provider = config.providers?.[providerName];
|
|
173
|
+
if (!provider || !isGenericFailoverProvider(providerName, provider)) return false;
|
|
174
|
+
return hasFailoverAccountQuorum(providerName, now);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Whether the pre-dispatch account PREFERENCE may run for this provider.
|
|
179
|
+
*
|
|
180
|
+
* Unlike reactive rotation, this moves a request that upstream has not refused, so it stays
|
|
181
|
+
* refusable: an explicit provider value wins over the global default, and a global `false`
|
|
182
|
+
* turns it off only when the provider has no override. A malformed value falls through rather
|
|
183
|
+
* than taking a provider out of service.
|
|
184
|
+
*/
|
|
185
|
+
function isProactivePreferenceEnabled(config: OcxConfig, providerName: string, now: number): boolean {
|
|
169
186
|
const provider = config.providers?.[providerName];
|
|
170
187
|
if (!provider || !isGenericFailoverProvider(providerName, provider)) return false;
|
|
171
188
|
const perProvider = provider.oauthAccountFailover?.enabled;
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
189
|
+
// Preserve the published narrow-over-broad precedence. A provider-specific true may
|
|
190
|
+
// opt this provider into proactive preference even when the global default is false;
|
|
191
|
+
// a provider-specific false refuses it even when the global setting is true.
|
|
192
|
+
if (typeof perProvider === "boolean") {
|
|
193
|
+
return perProvider && hasFailoverAccountQuorum(providerName, now);
|
|
194
|
+
}
|
|
195
|
+
if (config.oauthAccountFailover?.enabled === false) return false;
|
|
175
196
|
return hasFailoverAccountQuorum(providerName, now);
|
|
176
197
|
}
|
|
177
198
|
|
|
@@ -265,7 +286,9 @@ export function preferredInitialAccount(
|
|
|
265
286
|
providerName: string,
|
|
266
287
|
now = Date.now(),
|
|
267
288
|
): string | null {
|
|
268
|
-
|
|
289
|
+
// The PROACTIVE predicate, not the reactive one: this steers a request upstream has not
|
|
290
|
+
// refused, so `oauthAccountFailover.enabled: false` must still be able to refuse it.
|
|
291
|
+
if (!isProactivePreferenceEnabled(config, providerName, now)) return null;
|
|
269
292
|
// This runs on the initial resolution of EVERY request, and `loadAuthStore` has no
|
|
270
293
|
// cache: each call chmods the config dir, chmods the secret, reads the whole file and
|
|
271
294
|
// normalizes it (store.ts:136-151). So the store is consulted at most ONCE here, behind
|
package/src/oauth/index.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { KiroOAuthMetadata, OAuthController, OAuthCredentials } from "./types";
|
|
2
|
+
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
|
|
2
3
|
import { parseCallbackInput } from "./callback-server";
|
|
3
4
|
import type { OcxConfig, OcxProviderConfig, RefreshPolicy } from "../types";
|
|
4
|
-
import { ConfigMutationLockError, loadConfig, saveConfig } from "../config";
|
|
5
|
+
import { ConfigMutationLockError, loadConfig, mutatePersistedConfig, saveConfig } from "../config";
|
|
5
6
|
import { resolveProviderApiKey } from "../providers/key-store";
|
|
6
7
|
import { maskEmail } from "../lib/privacy";
|
|
7
8
|
import { KiroTokenRefreshError, environmentKiroRoutingMetadata, loginKiro, refreshKiroToken, settleKiroLoginTransaction } from "./kiro";
|
|
@@ -1247,42 +1248,134 @@ function migrateLegacyAntigravityStaticCatalog(config: OcxConfig): boolean {
|
|
|
1247
1248
|
return true;
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1251
|
+
interface OAuthReconcileProjection {
|
|
1252
|
+
config: OcxConfig;
|
|
1253
|
+
changed: boolean;
|
|
1254
|
+
touchedProviders: string[];
|
|
1255
|
+
touchedAntigravityVersion: boolean;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/** Pure projection over a clone: apply every reconciliation rule and report what it touched. */
|
|
1259
|
+
function projectOAuthProviderReconciliation(config: OcxConfig): OAuthReconcileProjection {
|
|
1260
|
+
const projected = structuredClone(config);
|
|
1261
|
+
const touchedProviders = new Set<string>();
|
|
1262
|
+
const beforeAntigravity = JSON.stringify(projected.providers[GOOGLE_ANTIGRAVITY_PROVIDER]);
|
|
1263
|
+
const beforeAntigravityVersion = projected.googleAntigravityStaticCatalogVersion;
|
|
1264
|
+
let changed = migrateLegacyAntigravityStaticCatalog(projected);
|
|
1265
|
+
if (JSON.stringify(projected.providers[GOOGLE_ANTIGRAVITY_PROVIDER]) !== beforeAntigravity) {
|
|
1266
|
+
touchedProviders.add(GOOGLE_ANTIGRAVITY_PROVIDER);
|
|
1267
|
+
}
|
|
1268
|
+
const touchedAntigravityVersion = projected.googleAntigravityStaticCatalogVersion !== beforeAntigravityVersion;
|
|
1269
|
+
|
|
1270
|
+
for (const [name, prov] of Object.entries(projected.providers)) {
|
|
1271
|
+
const beforeProvider = JSON.stringify(prov);
|
|
1253
1272
|
const def = OAUTH_PROVIDERS[name];
|
|
1254
1273
|
if (name === "command-code" && isLegacyCommandCodeStaticCatalog(prov)) {
|
|
1255
1274
|
// The former experimental preset was the exact three-model seed above. It was not a user
|
|
1256
1275
|
// choice to disable discovery, so promote only that shape to the account live catalog.
|
|
1257
1276
|
prov.liveModels = true;
|
|
1258
|
-
changed = true;
|
|
1259
1277
|
}
|
|
1260
|
-
if (
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1278
|
+
if (def && prov.authMode === "oauth") {
|
|
1279
|
+
const preset = def.providerConfig;
|
|
1280
|
+
for (const field of OAUTH_RECONCILE_FIELDS) {
|
|
1281
|
+
if (JSON.stringify(prov[field]) === JSON.stringify(preset[field])) continue;
|
|
1282
|
+
if (preset[field] !== undefined) {
|
|
1283
|
+
prov[field] = cloneProviderField(preset[field]) as never;
|
|
1284
|
+
} else {
|
|
1285
|
+
delete prov[field];
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
1289
|
+
prov.liveModels = preset.liveModels;
|
|
1290
|
+
}
|
|
1291
|
+
// Heal a defaultModel that no longer exists in the refreshed list (e.g. a deprecated snapshot).
|
|
1292
|
+
// Skip providers without a static preset `models` list: for live-discovery providers
|
|
1293
|
+
// (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
|
|
1294
|
+
// persisted defaultModel is a user selection and must not be overwritten by the seed.
|
|
1295
|
+
if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
1296
|
+
prov.defaultModel = preset.defaultModel;
|
|
1268
1297
|
}
|
|
1269
|
-
changed = true;
|
|
1270
|
-
}
|
|
1271
|
-
if (prov.liveModels === undefined && preset.liveModels !== undefined) {
|
|
1272
|
-
prov.liveModels = preset.liveModels;
|
|
1273
|
-
changed = true;
|
|
1274
1298
|
}
|
|
1275
|
-
|
|
1276
|
-
// Skip providers without a static preset `models` list: for live-discovery providers
|
|
1277
|
-
// (e.g. command-code OAuth) the account-scoped catalog is not enumerable here, so any
|
|
1278
|
-
// persisted defaultModel is a user selection and must not be overwritten by the seed.
|
|
1279
|
-
if (prov.defaultModel && preset.defaultModel && preset.models && preset.models.length > 0 && !(prov.models ?? []).includes(prov.defaultModel)) {
|
|
1280
|
-
prov.defaultModel = preset.defaultModel;
|
|
1299
|
+
if (JSON.stringify(prov) !== beforeProvider) {
|
|
1281
1300
|
changed = true;
|
|
1301
|
+
touchedProviders.add(name);
|
|
1282
1302
|
}
|
|
1283
1303
|
}
|
|
1284
|
-
|
|
1285
|
-
return
|
|
1304
|
+
|
|
1305
|
+
return {
|
|
1306
|
+
config: projected,
|
|
1307
|
+
changed,
|
|
1308
|
+
touchedProviders: [...touchedProviders],
|
|
1309
|
+
touchedAntigravityVersion,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Copy only the keys the projection actually touched back onto the caller's live object.
|
|
1315
|
+
*
|
|
1316
|
+
* Deliberately key-by-key rather than a wholesale clear-and-reassign: a live reference held
|
|
1317
|
+
* elsewhere to an untouched provider sub-object must survive startup reconciliation.
|
|
1318
|
+
*/
|
|
1319
|
+
function adoptOAuthReconciliation(config: OcxConfig, projection: OAuthReconcileProjection): void {
|
|
1320
|
+
for (const name of projection.touchedProviders) {
|
|
1321
|
+
const provider = projection.config.providers[name];
|
|
1322
|
+
if (provider) config.providers[name] = structuredClone(provider);
|
|
1323
|
+
else delete config.providers[name];
|
|
1324
|
+
}
|
|
1325
|
+
if (projection.touchedAntigravityVersion) {
|
|
1326
|
+
config.googleAntigravityStaticCatalogVersion = projection.config.googleAntigravityStaticCatalogVersion;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* Union the keys the on-disk rebase touched with the keys the live projection touched.
|
|
1332
|
+
*
|
|
1333
|
+
* The rebase runs against the persisted snapshot, which may already carry a reconciliation
|
|
1334
|
+
* another process committed. Adopting only its touched set would leave the live object stale
|
|
1335
|
+
* for a key it decided was already correct on disk.
|
|
1336
|
+
*/
|
|
1337
|
+
function withOAuthReconciliationTouchedKeys(
|
|
1338
|
+
projection: OAuthReconcileProjection,
|
|
1339
|
+
required: OAuthReconcileProjection,
|
|
1340
|
+
): OAuthReconcileProjection {
|
|
1341
|
+
return {
|
|
1342
|
+
...projection,
|
|
1343
|
+
touchedProviders: [...new Set([...projection.touchedProviders, ...required.touchedProviders])],
|
|
1344
|
+
touchedAntigravityVersion: projection.touchedAntigravityVersion || required.touchedAntigravityVersion,
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
/**
|
|
1349
|
+
* Refresh OAuth provider presets against the registry, rebasing the write on the persisted config.
|
|
1350
|
+
*
|
|
1351
|
+
* This runs on the boot path (`startServer`), so persistence failure must never be fatal: a
|
|
1352
|
+
* missing, malformed or contended config degrades to a warning plus an in-memory adopt, exactly
|
|
1353
|
+
* as every other `mutatePersistedConfig` consumer does (`src/storage/policy.ts`,
|
|
1354
|
+
* `src/codex/plan-from-token.ts`, `src/server/management/agent-settings-routes.ts`). Throwing
|
|
1355
|
+
* here would take the whole proxy down over a config file the operator can still repair.
|
|
1356
|
+
*/
|
|
1357
|
+
export function reconcileOAuthProviders(config: OcxConfig, persist = true): boolean {
|
|
1358
|
+
const projection = projectOAuthProviderReconciliation(config);
|
|
1359
|
+
if (!projection.changed) return false;
|
|
1360
|
+
if (!persist) {
|
|
1361
|
+
adoptOAuthReconciliation(config, projection);
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
1364
|
+
const outcome = mutatePersistedConfig(fresh => {
|
|
1365
|
+
const next = projectOAuthProviderReconciliation(fresh);
|
|
1366
|
+
if (next.changed) adoptOAuthReconciliation(fresh, next);
|
|
1367
|
+
return { changed: next.changed, value: next };
|
|
1368
|
+
});
|
|
1369
|
+
if (outcome.status === "unavailable") {
|
|
1370
|
+
console.warn(
|
|
1371
|
+
`[opencodex] OAuth provider reconciliation could not be persisted (${outcome.reason}); `
|
|
1372
|
+
+ "applying it in memory for this run only.",
|
|
1373
|
+
);
|
|
1374
|
+
adoptOAuthReconciliation(config, projection);
|
|
1375
|
+
return true;
|
|
1376
|
+
}
|
|
1377
|
+
adoptOAuthReconciliation(config, withOAuthReconciliationTouchedKeys(outcome.value, projection));
|
|
1378
|
+
return true;
|
|
1286
1379
|
}
|
|
1287
1380
|
|
|
1288
1381
|
/** Runtime guards: provider config is intentionally passthrough, so persisted fields may be malformed. */
|
|
@@ -1353,6 +1446,13 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1353
1446
|
if (existing?.commandCodeVersion !== undefined) {
|
|
1354
1447
|
next.commandCodeVersion = existing.commandCodeVersion;
|
|
1355
1448
|
}
|
|
1449
|
+
// Reauth/add-account refreshes credentials, not the operator's post-upgrade wire choice.
|
|
1450
|
+
if (provider === "xai") {
|
|
1451
|
+
if (existing?.modelAdapters !== undefined) next.modelAdapters = { ...existing.modelAdapters };
|
|
1452
|
+
if (existing?.xaiResponsesDefaultVersion !== undefined) {
|
|
1453
|
+
next.xaiResponsesDefaultVersion = existing.xaiResponsesDefaultVersion;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1356
1456
|
// User-configured price overlays are operator data, not preset state; a
|
|
1357
1457
|
// re-login, add-account, or reauth must not silently drop them from the
|
|
1358
1458
|
// Logs/Usage estimates.
|
|
@@ -1389,6 +1489,7 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
|
|
|
1389
1489
|
if (previousModeAllowsKey) next.authMode = "key";
|
|
1390
1490
|
}
|
|
1391
1491
|
}
|
|
1492
|
+
initializeProviderModelSelection(provider, next, existing, config);
|
|
1392
1493
|
config.providers[provider] = next;
|
|
1393
1494
|
}
|
|
1394
1495
|
|
package/src/oauth/login-cli.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import * as readline from "node:readline";
|
|
2
|
+
import { modelSelectionGuidance } from "../cli/model-selection-guidance";
|
|
3
|
+
import { initializeProviderModelSelection } from "../providers/initial-model-selection";
|
|
2
4
|
import { openUrl } from "../lib/open-url";
|
|
3
5
|
import { loadConfig, saveConfig } from "../config";
|
|
4
6
|
import { findLiveProxy } from "../server/proxy-liveness";
|
|
@@ -93,6 +95,7 @@ async function handleOAuthLogin(name: string): Promise<void> {
|
|
|
93
95
|
}
|
|
94
96
|
const reload = await notifyRunningProxyAfterOAuthLogin(name);
|
|
95
97
|
console.log(`\n✅ Logged in to ${name}. Try: ocx sync`);
|
|
98
|
+
for (const line of modelSelectionGuidance(name)) console.log(line);
|
|
96
99
|
warnIfLiveReloadSkipped(reload);
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -156,6 +159,7 @@ export async function commitKeyLoginProvider(
|
|
|
156
159
|
onLiveReload?: (result: LocalProviderReloadResult | null) => void,
|
|
157
160
|
): Promise<OcxProviderConfig> {
|
|
158
161
|
const mergedProvider = mergeKeyLoginProviderRow(provider, config.providers[name]);
|
|
162
|
+
initializeProviderModelSelection(name, mergedProvider, config.providers[name], config);
|
|
159
163
|
config.providers[name] = mergedProvider;
|
|
160
164
|
saveConfig(config);
|
|
161
165
|
// Evaluate the reload BEFORE the optional call: `onLiveReload?.(await ...)` short-circuits
|
|
@@ -211,6 +215,7 @@ async function handleKeyLogin(name: string): Promise<void> {
|
|
|
211
215
|
let reload: LocalProviderReloadResult | null = null;
|
|
212
216
|
await commitKeyLoginProvider(config, name, provider, result => { reload = result; });
|
|
213
217
|
console.log(`✅ ${def.label} added. Try: ocx sync`);
|
|
218
|
+
for (const line of modelSelectionGuidance(name)) console.log(line);
|
|
214
219
|
warnIfLiveReloadSkipped(reload);
|
|
215
220
|
}
|
|
216
221
|
|
package/src/oauth/meta-muse.ts
CHANGED
|
@@ -45,7 +45,7 @@ const CONSENT_WARNING = [
|
|
|
45
45
|
"Meta scopes the Muse Code credential to the Muse Code CLI.",
|
|
46
46
|
"Using it here is UNSUPPORTED: Meta does not authorize subscription coverage outside its own CLI,",
|
|
47
47
|
"how these calls settle is not observable from the API, and you should treat every call as billable.",
|
|
48
|
-
"The
|
|
48
|
+
"The key you import or paste is copied into OpenCodex's auth store (~/.opencodex/auth.json, 0600).",
|
|
49
49
|
"Supported alternative: the meta-model provider with your own key (META_MODEL_API_KEY).",
|
|
50
50
|
].join(" ");
|
|
51
51
|
|
|
@@ -114,6 +114,43 @@ async function defaultReadKeychain(signal?: AbortSignal): Promise<string | null>
|
|
|
114
114
|
const INSTALL_HINT =
|
|
115
115
|
"Install it from https://dev.meta.ai/install.sh, run `muse login`, then retry.";
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Where a user without the CLI gets a key by hand.
|
|
119
|
+
*
|
|
120
|
+
* The Muse Code API key is visible in Meta's own developer console, so a host with no
|
|
121
|
+
* CLI is not out of options ??it is out of an IMPORT path. That distinction is the whole
|
|
122
|
+
* reason this branch exists: refusing a platform because our importer cannot read its
|
|
123
|
+
* store, while the vendor hands the same key to the user in a browser, is a limitation
|
|
124
|
+
* of the importer being reported as a limitation of the platform.
|
|
125
|
+
*/
|
|
126
|
+
const MANUAL_KEY_URL = "https://dev.meta.ai";
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Accept a hand-entered Muse Code API key.
|
|
130
|
+
*
|
|
131
|
+
* Every guarantee the import path makes still holds here, because they are enforced
|
|
132
|
+
* BELOW this function rather than inside it: the same `LLM|` grammar check, the same
|
|
133
|
+
* live validation against the Model API, and the same consent warning, which has
|
|
134
|
+
* already fired before any of this runs. What is missing is only the pointer, so the
|
|
135
|
+
* credential carries no email and `source` is `manual` rather than `local-cli`.
|
|
136
|
+
*/
|
|
137
|
+
async function manualKeyCredential(
|
|
138
|
+
ctrl: OAuthController,
|
|
139
|
+
reason: string,
|
|
140
|
+
): Promise<string | null> {
|
|
141
|
+
if (!ctrl.onManualCodeInput) return null;
|
|
142
|
+
// Resolve the login flow first so the GUI renders its paste field; otherwise
|
|
143
|
+
// onManualCodeInput blocks and the dashboard never sees a response (kiro.ts:405).
|
|
144
|
+
ctrl.onAuth?.({
|
|
145
|
+
url: MANUAL_KEY_URL,
|
|
146
|
+
instructions:
|
|
147
|
+
`${reason} Sign in at ${MANUAL_KEY_URL}, copy your Muse Code API key, and paste it below.`,
|
|
148
|
+
});
|
|
149
|
+
ctrl.onProgress?.(`Paste a Muse Code API key from ${MANUAL_KEY_URL} (it starts with "LLM|").`);
|
|
150
|
+
const pasted = (await ctrl.onManualCodeInput()).trim();
|
|
151
|
+
return pasted.length > 0 ? pasted : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
function normalizedEmail(value: unknown): string | undefined {
|
|
118
155
|
if (typeof value !== "string") return undefined;
|
|
119
156
|
const trimmed = value.trim().toLowerCase();
|
|
@@ -133,11 +170,29 @@ export async function loginMetaMuse(
|
|
|
133
170
|
ctrl.onProgress?.(CONSENT_WARNING);
|
|
134
171
|
|
|
135
172
|
const platform = deps.platform ?? process.platform;
|
|
173
|
+
// Off darwin there is no store this importer can read: Meta ships no native Windows
|
|
174
|
+
// CLI, and the Linux credential shape has never been measured. That is a limitation
|
|
175
|
+
// of the IMPORT, not of the platform ??the same key is visible in Meta's console ??
|
|
176
|
+
// so these hosts get a paste field instead of a dead end. The pasted key then goes
|
|
177
|
+
// through the identical grammar check and live validation as an imported one.
|
|
136
178
|
if (platform !== "darwin") {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
179
|
+
// The two platforms are unavailable for DIFFERENT reasons, and saying so matters:
|
|
180
|
+
// Meta ships no Windows build at all, while the Linux CLI exists and only its
|
|
181
|
+
// credential storage is unmeasured. Collapsing them into "no CLI here" would tell
|
|
182
|
+
// a Linux user something false about their own machine.
|
|
183
|
+
const reason = platform === "win32"
|
|
184
|
+
? "Meta ships no native Windows Muse Code CLI, so there is no credential to import."
|
|
185
|
+
: "The Muse Code CLI runs here, but where it stores its credential has not been measured, "
|
|
186
|
+
+ "so importing one is refused rather than guessed.";
|
|
187
|
+
const pasted = await manualKeyCredential(ctrl, reason);
|
|
188
|
+
if (pasted === null) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`${reason} This client cannot prompt for a key, so run \`ocx login meta-muse\` from the CLI `
|
|
191
|
+
+ `or the dashboard and paste yours from ${MANUAL_KEY_URL}, `
|
|
192
|
+
+ "or use the meta-model provider with your own key (META_MODEL_API_KEY).",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return await validatedMetaMuseCredential(pasted, ctrl, deps, undefined, "manual");
|
|
141
196
|
}
|
|
142
197
|
|
|
143
198
|
const pointerRaw = await (deps.readPointer ?? defaultReadPointer)();
|
|
@@ -178,15 +233,48 @@ export async function loginMetaMuse(
|
|
|
178
233
|
}
|
|
179
234
|
|
|
180
235
|
// access_token is present but 401s against the Model API (003 §B) — never fall back to it.
|
|
181
|
-
|
|
236
|
+
return await validatedMetaMuseCredential(
|
|
237
|
+
secret.api_key,
|
|
238
|
+
ctrl,
|
|
239
|
+
deps,
|
|
240
|
+
normalizedEmail(meta.user_email),
|
|
241
|
+
"local-cli",
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* The single gate every credential passes, imported or pasted.
|
|
247
|
+
*
|
|
248
|
+
* Both paths share it deliberately. A pasted key that skipped the grammar check or the
|
|
249
|
+
* live validation would be a weaker credential wearing the same provider id, and the
|
|
250
|
+
* difference would surface only as a 401 in the middle of a session.
|
|
251
|
+
*/
|
|
252
|
+
async function validatedMetaMuseCredential(
|
|
253
|
+
candidate: unknown,
|
|
254
|
+
ctrl: OAuthController,
|
|
255
|
+
deps: MuseImportDeps,
|
|
256
|
+
email: string | undefined,
|
|
257
|
+
source: "local-cli" | "manual",
|
|
258
|
+
): Promise<OAuthCredentials> {
|
|
259
|
+
const retry = source === "manual"
|
|
260
|
+
? `Copy it again from ${MANUAL_KEY_URL}.`
|
|
261
|
+
: "Run `muse login` again.";
|
|
262
|
+
const apiKey = sanitizeApiKeyValue(candidate);
|
|
182
263
|
if (!apiKey) {
|
|
183
|
-
|
|
264
|
+
// The macOS wording is preserved verbatim. Extraction is a refactor, and a refactor
|
|
265
|
+
// that quietly rewrites a user-facing error is a behavior change in disguise.
|
|
266
|
+
throw new Error(source === "manual"
|
|
267
|
+
? `The pasted Muse Code credential carries no usable API key. ${retry}`
|
|
268
|
+
: "The Muse Code Keychain entry carries no usable API key. Run `muse login` again.");
|
|
184
269
|
}
|
|
185
270
|
if (!/^LLM\|\d+\|[A-Za-z0-9_-]{10,}$/.test(apiKey)) {
|
|
186
|
-
throw new Error(
|
|
271
|
+
throw new Error(source === "manual"
|
|
272
|
+
? `The pasted Muse Code credential is not in the expected Meta API key format. ${retry}`
|
|
273
|
+
: "The Muse Code credential is not in the expected Meta API key format. Run `muse login` again.");
|
|
187
274
|
}
|
|
188
|
-
|
|
189
|
-
|
|
275
|
+
ctrl.onProgress?.(source === "manual"
|
|
276
|
+
? "Validating the pasted Meta credential..."
|
|
277
|
+
: "Validating the imported Meta credential...");
|
|
190
278
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
191
279
|
// ctrl.signal is OPTIONAL and the CLI controller supplies none: AbortSignal.any([undefined])
|
|
192
280
|
// throws a TypeError, which would fail every CLI login right after the warning printed.
|
|
@@ -205,7 +293,7 @@ export async function loginMetaMuse(
|
|
|
205
293
|
}
|
|
206
294
|
if (!response.ok) {
|
|
207
295
|
throw new Error(
|
|
208
|
-
`The Muse Code credential was rejected by the Meta Model API (HTTP ${response.status}).
|
|
296
|
+
`The Muse Code credential was rejected by the Meta Model API (HTTP ${response.status}). ${retry}`,
|
|
209
297
|
);
|
|
210
298
|
}
|
|
211
299
|
|
|
@@ -216,8 +304,8 @@ export async function loginMetaMuse(
|
|
|
216
304
|
expires: Number.MAX_SAFE_INTEGER,
|
|
217
305
|
// `email`, not `accountId`: the account list masks email for display, and store.ts
|
|
218
306
|
// already falls back to it for slot identity, so multi-account still works.
|
|
219
|
-
...(
|
|
220
|
-
source
|
|
307
|
+
...(email ? { email } : {}),
|
|
308
|
+
source,
|
|
221
309
|
};
|
|
222
310
|
}
|
|
223
311
|
|
|
@@ -228,8 +316,22 @@ export async function loginMetaMuse(
|
|
|
228
316
|
* the slot being refreshed, so if the user ran `muse login` with a DIFFERENT account in
|
|
229
317
|
* between, a re-import would silently overwrite one stored identity with another. Only an
|
|
230
318
|
* explicit login may import.
|
|
319
|
+
*
|
|
320
|
+
* It also does not assert a source. `merged()` in index.ts keeps the previous source for
|
|
321
|
+
* anything that is not `local-cli`, so returning `local-cli` here would relabel a
|
|
322
|
+
* hand-pasted key as an imported one on its first refresh and misreport where the
|
|
323
|
+
* credential came from.
|
|
231
324
|
*/
|
|
232
|
-
export async function refreshMetaMuseToken(
|
|
325
|
+
export async function refreshMetaMuseToken(
|
|
326
|
+
apiKey: string,
|
|
327
|
+
_signal?: AbortSignal,
|
|
328
|
+
credential?: OAuthCredentials,
|
|
329
|
+
): Promise<OAuthCredentials> {
|
|
233
330
|
if (!apiKey) throw new Error("Meta Muse Code API key missing; run `ocx login meta-muse`");
|
|
234
|
-
return {
|
|
331
|
+
return {
|
|
332
|
+
access: apiKey,
|
|
333
|
+
refresh: apiKey,
|
|
334
|
+
expires: Number.MAX_SAFE_INTEGER,
|
|
335
|
+
source: credential?.source === "manual" ? "manual" : "local-cli",
|
|
336
|
+
};
|
|
235
337
|
}
|
|
@@ -6,8 +6,13 @@ import type { OcxProviderConfig } from "../types";
|
|
|
6
6
|
*
|
|
7
7
|
* `codex` and `anthropic` keep their own routes and storage untouched. `generic` is every
|
|
8
8
|
* other OAuth provider the generic failover module admits; its settings persist on
|
|
9
|
-
* `providers.<name>.oauthAccountFailover`.
|
|
10
|
-
*
|
|
9
|
+
* `providers.<name>.oauthAccountFailover`.
|
|
10
|
+
*
|
|
11
|
+
* `strategy` and `autoSwitchThreshold` are still a declared contract the selector does not
|
|
12
|
+
* consume — that is what `inert` reports. `enabled` is NOT inert any more: an explicit
|
|
13
|
+
* `false` refuses the pre-dispatch account preference (`preferredInitialAccount`). What it can
|
|
14
|
+
* no longer do is refuse reactive 429 rotation, which activates on account presence and is not
|
|
15
|
+
* disableable.
|
|
11
16
|
*/
|
|
12
17
|
export type PoolSettingsKind = "codex" | "anthropic" | "generic";
|
|
13
18
|
|
|
@@ -37,7 +42,14 @@ export interface GenericPoolSettingsDto {
|
|
|
37
42
|
enabled: boolean | null;
|
|
38
43
|
strategy: GenericPoolStrategy | null;
|
|
39
44
|
autoSwitchThreshold: number | null;
|
|
40
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Slice-1 marker for `strategy` and `autoSwitchThreshold` only: persisted, not yet consumed
|
|
47
|
+
* by the selector.
|
|
48
|
+
*
|
|
49
|
+
* It deliberately does NOT describe `enabled`, which governs the pre-dispatch preference.
|
|
50
|
+
* Widening it to the whole DTO would tell a dashboard that `enabled` changes nothing, which
|
|
51
|
+
* has been false since reactive and proactive activation were split.
|
|
52
|
+
*/
|
|
41
53
|
inert: true;
|
|
42
54
|
}
|
|
43
55
|
|
|
@@ -52,4 +64,3 @@ export function genericPoolSettingsDto(name: string, provider: OcxProviderConfig
|
|
|
52
64
|
inert: true,
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
|
-
|
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
* `provider.apiKey` stays the single source of truth for routing — it always mirrors the
|
|
5
5
|
* ACTIVE pool entry, so the router/adapters never learn about the pool. The pool itself
|
|
6
6
|
* lives in `provider.apiKeyPool` in config.json (same file that already holds apiKey).
|
|
7
|
-
* A
|
|
7
|
+
* A legacy bare `apiKey` is projected as one row on reads and seeded on first mutation.
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
11
11
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
12
|
+
import type { AccountQuotaFields } from "./quota-types";
|
|
12
13
|
|
|
13
|
-
export interface ProviderApiKeyInfo {
|
|
14
|
+
export interface ProviderApiKeyInfo extends AccountQuotaFields {
|
|
14
15
|
id: string;
|
|
15
16
|
label?: string;
|
|
16
17
|
/** First/last 4 chars only; env references (`${VAR}`) are shown verbatim (not secrets). */
|
|
@@ -57,17 +58,14 @@ function ensurePool(provider: OcxProviderConfig): NonNullable<OcxProviderConfig[
|
|
|
57
58
|
return provider.apiKeyPool;
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function activeEntryId(provider: OcxProviderConfig): string | null {
|
|
61
|
-
const pool = provider.apiKeyPool ?? [];
|
|
62
|
-
if (pool.length === 0) return null;
|
|
63
|
-
return (pool.find(e => e.key === provider.apiKey) ?? pool[0]!).id;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
61
|
export function listProviderApiKeys(config: OcxConfig, name: string): { activeId: string | null; keys: ProviderApiKeyInfo[] } {
|
|
67
62
|
const provider = config.providers[name];
|
|
68
63
|
if (!provider || !isKeyAuthProvider(provider)) return { activeId: null, keys: [] };
|
|
69
|
-
|
|
70
|
-
const
|
|
64
|
+
// A GET projects a legacy key without seeding/mutating live configuration.
|
|
65
|
+
const pool = provider.apiKeyPool?.length
|
|
66
|
+
? provider.apiKeyPool
|
|
67
|
+
: provider.apiKey ? [{ id: apiKeyPoolEntryId(provider.apiKey), key: provider.apiKey }] : [];
|
|
68
|
+
const activeId = (pool.find(entry => entry.key === provider.apiKey) ?? pool[0])?.id ?? null;
|
|
71
69
|
return {
|
|
72
70
|
activeId,
|
|
73
71
|
keys: pool.map(entry => ({
|
|
@@ -1,3 +1,42 @@
|
|
|
1
|
+
|
|
2
|
+
import { PROVIDER_REGISTRY } from "./registry";
|
|
3
|
+
|
|
4
|
+
export function effectiveProviderAlias(
|
|
5
|
+
providerName: string,
|
|
6
|
+
provider?: Pick<OcxProviderConfig, "alias">,
|
|
7
|
+
config?: Pick<OcxConfig, "providers">,
|
|
8
|
+
): string | undefined {
|
|
9
|
+
if (provider && provider.alias !== undefined) {
|
|
10
|
+
const trimmed = provider.alias.trim();
|
|
11
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
12
|
+
}
|
|
13
|
+
const regAlias = PROVIDER_REGISTRY.find(e => e.id === providerName)?.alias;
|
|
14
|
+
if (!regAlias) return undefined;
|
|
15
|
+
if (config?.providers) {
|
|
16
|
+
const lower = regAlias.toLowerCase();
|
|
17
|
+
const claimedByOther = Object.entries(config.providers).some(([name, p]) =>
|
|
18
|
+
name !== providerName && typeof p.alias === "string" && p.alias.trim().toLowerCase() === lower
|
|
19
|
+
);
|
|
20
|
+
if (claimedByOther) return undefined;
|
|
21
|
+
}
|
|
22
|
+
return regAlias;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function effectiveProviderAliasDecision(
|
|
26
|
+
providerName: string,
|
|
27
|
+
provider?: Pick<OcxProviderConfig, "alias">,
|
|
28
|
+
config?: Pick<OcxConfig, "providers">,
|
|
29
|
+
): string | null | undefined {
|
|
30
|
+
const active = effectiveProviderAlias(providerName, provider, config);
|
|
31
|
+
if (active !== undefined) return active;
|
|
32
|
+
const hasRegistryAlias = Boolean(PROVIDER_REGISTRY.find(e => e.id === providerName)?.alias);
|
|
33
|
+
const hasConfiguredAlias = provider?.alias !== undefined;
|
|
34
|
+
if (hasRegistryAlias || hasConfiguredAlias) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
|
|
1
40
|
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
2
41
|
|
|
3
42
|
export const MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
package/src/providers/derive.ts
CHANGED
|
@@ -218,6 +218,7 @@ export function providerConfigSeed(entry: ProviderRegistryEntry): OcxProviderCon
|
|
|
218
218
|
baseUrl: entry.baseUrl,
|
|
219
219
|
...(entry.apiKeyTransport !== undefined ? { apiKeyTransport: entry.apiKeyTransport } : {}),
|
|
220
220
|
...(entry.responsesPath ? { responsesPath: entry.responsesPath } : {}),
|
|
221
|
+
...(entry.alias ? { alias: entry.alias } : {}),
|
|
221
222
|
// Preserve the registry auth kind verbatim (including "local") so fail-closed gates that
|
|
222
223
|
// distinguish local runtimes from API-key providers keep working after the seed round-trip.
|
|
223
224
|
authMode: entry.authKind,
|
|
@@ -403,7 +404,7 @@ function serviceTierModelDefaultsFor(
|
|
|
403
404
|
* The catalog hint pass must not read PROVIDER_REGISTRY: a gather flight captures its registry
|
|
404
405
|
* authority up front and forbids any later read, so consulting the registry per model turned
|
|
405
406
|
* every hint pass into a post-lookup read and dropped a custom-destination flight's own
|
|
406
|
-
* discovery result (tests/codex-gather-authority.test.ts).
|
|
407
|
+
* discovery result (tests/codex-integration/codex-gather-authority.test.ts).
|
|
407
408
|
*
|
|
408
409
|
* Registry values go in first so an explicit user entry still wins, matching
|
|
409
410
|
* `applyReasoningSummaryDefaults`. `supportsVerbosity` is the provider-wide default, expanded
|
|
@@ -490,7 +491,14 @@ export function enrichProviderFromRegistry(name: string, prov: OcxProviderConfig
|
|
|
490
491
|
if ((!prov.reasoningEfforts || hasLegacyClinePassReasoningEfforts(name, prov)) && seed.reasoningEfforts) {
|
|
491
492
|
prov.reasoningEfforts = [...seed.reasoningEfforts];
|
|
492
493
|
}
|
|
493
|
-
|
|
494
|
+
// Per-model fill for the same reason as modelInputModalities above: an all-or-nothing
|
|
495
|
+
// copy let ONE customized model hide the registry's ladder for every other model on the
|
|
496
|
+
// provider. That split the two planes apart — routing merges these maps per key
|
|
497
|
+
// (mergeRecordFill in src/router.ts), so the wire honored the effort while /v1/models and
|
|
498
|
+
// every client export showed no effort control at all.
|
|
499
|
+
if (seed.modelReasoningEfforts) {
|
|
500
|
+
prov.modelReasoningEfforts = fillRecordOfArrays(seed.modelReasoningEfforts, prov.modelReasoningEfforts);
|
|
501
|
+
}
|
|
494
502
|
if (!prov.modelDefaultReasoningEfforts && seed.modelDefaultReasoningEfforts) prov.modelDefaultReasoningEfforts = { ...seed.modelDefaultReasoningEfforts };
|
|
495
503
|
if (!prov.reasoningEffortMap && seed.reasoningEffortMap) prov.reasoningEffortMap = { ...seed.reasoningEffortMap };
|
|
496
504
|
if (!prov.modelReasoningEffortMap && seed.modelReasoningEffortMap) prov.modelReasoningEffortMap = cloneNestedRecord(seed.modelReasoningEffortMap);
|