@bitkyc08/opencodex 2.48.0 → 2.50.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 +9 -1
- package/README.md +11 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-C39tnjXO.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/claude/inbound.ts +17 -5
- package/src/cli/account-api.ts +18 -3
- package/src/cli/account-auth.ts +8 -1
- package/src/cli/account-extended.ts +2 -1
- package/src/cli/account.ts +1 -0
- package/src/cli/capabilities.ts +15 -1
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +29 -2
- package/src/cli/models-runtime.ts +8 -3
- package/src/cli/observe.ts +13 -3
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/clients/config-export/zcode.ts +24 -0
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/account-runtime-state.ts +6 -1
- package/src/codex/account-store.ts +72 -9
- package/src/codex/account-usability.ts +3 -2
- package/src/codex/auth-api.ts +113 -26
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +96 -7
- package/src/codex/catalog/parsing.ts +23 -0
- package/src/codex/catalog/provider-fetch.ts +144 -11
- package/src/codex/catalog/sync.ts +14 -0
- package/src/codex/inject.ts +128 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota-auto-refresh.ts +6 -1
- package/src/codex/quota.ts +71 -15
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/combos/index.ts +2 -0
- package/src/combos/resolve.ts +52 -0
- package/src/config.ts +59 -0
- package/src/generated/compatibility-version.json +178 -114
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/errors.ts +8 -0
- package/src/lib/privacy.ts +25 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/health.ts +47 -12
- package/src/oauth/index.ts +46 -8
- package/src/oauth/token-guardian.ts +32 -6
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/google-ai-studio-model-discovery.ts +74 -0
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/opencode-zen-rate-limit.ts +75 -0
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/quota.ts +15 -0
- package/src/providers/registry.ts +140 -1
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/auth-cors.ts +6 -0
- package/src/server/chat-completions.ts +30 -13
- package/src/server/chat-native.ts +10 -1
- package/src/server/claude-messages.ts +17 -7
- package/src/server/images.ts +3 -2
- package/src/server/index.ts +25 -2
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +5 -1
- package/src/server/management/model-rows.ts +16 -1
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +6 -2
- package/src/server/management/provider-routes.ts +33 -2
- package/src/server/management/request-history-routes.ts +4 -2
- package/src/server/management/route-registry.ts +5 -4
- package/src/server/management/shared.ts +66 -3
- package/src/server/management-api.ts +15 -1
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-decompress.ts +91 -3
- package/src/server/request-log.ts +16 -0
- package/src/server/responses/codex-ws-wire.ts +1 -1
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/compact.ts +8 -2
- package/src/server/responses/context-overflow.ts +11 -0
- package/src/server/responses/core.ts +285 -57
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +18 -2
- package/src/server/search.ts +2 -2
- package/src/service.ts +128 -9
- package/src/storage/cleanup.ts +77 -45
- package/src/types/accounts.ts +18 -0
- package/src/types/config.ts +43 -1
- package/src/types/provider.ts +56 -0
- package/src/types.ts +4 -0
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/ollama-executor.ts +127 -0
- package/src/web-search/passthrough-bridge.ts +761 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
|
@@ -52,12 +52,15 @@ import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
|
52
52
|
import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
|
|
53
53
|
import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
|
|
54
54
|
import { recordLiveCursorClaudeModels, recordLiveCursorMaxModeModels } from "../../adapters/cursor/catalog";
|
|
55
|
+
import { fetchQoderModels } from "../../adapters/qoder/live-models";
|
|
56
|
+
import { resolveQoderProfile } from "../../adapters/qoder/profiles";
|
|
55
57
|
import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
56
58
|
import {
|
|
57
59
|
COMBO_NAMESPACE,
|
|
58
60
|
comboModelId,
|
|
59
61
|
getCombo,
|
|
60
62
|
listComboIds,
|
|
63
|
+
quotaInactiveReason,
|
|
61
64
|
targetKey,
|
|
62
65
|
} from "../../combos";
|
|
63
66
|
import type { NormalizedComboConfig } from "../../combos/types";
|
|
@@ -77,6 +80,7 @@ import {
|
|
|
77
80
|
type ProviderModelsApiItem,
|
|
78
81
|
type ResolvedProviderModelDiscovery,
|
|
79
82
|
} from "../../providers/model-discovery";
|
|
83
|
+
import { extractGoogleAiStudioModelItems } from "../../providers/google-ai-studio-model-discovery";
|
|
80
84
|
import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show";
|
|
81
85
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
82
86
|
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
@@ -952,16 +956,21 @@ function comboMemberVendorMetadata(provider: string, modelId: string): ModelMeta
|
|
|
952
956
|
*/
|
|
953
957
|
function vendorMetadataComboFallback(target: { provider: string; model: string }): ComboCatalogMemberFallback | undefined {
|
|
954
958
|
const metadataProvider = resolveMetadataProvider(target.provider);
|
|
955
|
-
|
|
959
|
+
// Custom OpenAI-compatible routes commonly retain the canonical OpenAI model id
|
|
960
|
+
// while using a provider name that has no metadata alias. Reuse only its effort
|
|
961
|
+
// ladder below; context/modality rows remain provider-owned.
|
|
962
|
+
const metadata = metadataProvider
|
|
963
|
+
? comboMemberVendorMetadata(metadataProvider, target.model)
|
|
964
|
+
: comboMemberVendorMetadata("openai", target.model);
|
|
956
965
|
if (!metadata) return undefined;
|
|
957
966
|
return {
|
|
958
|
-
...(typeof metadata.contextWindow === "number" && metadata.contextWindow > 0
|
|
967
|
+
...(metadataProvider && typeof metadata.contextWindow === "number" && metadata.contextWindow > 0
|
|
959
968
|
? { contextWindow: metadata.contextWindow }
|
|
960
969
|
: {}),
|
|
961
|
-
...(typeof metadata.maxTokens === "number" && metadata.maxTokens > 0
|
|
970
|
+
...(metadataProvider && typeof metadata.maxTokens === "number" && metadata.maxTokens > 0
|
|
962
971
|
? { maxOutputTokens: metadata.maxTokens }
|
|
963
972
|
: {}),
|
|
964
|
-
...(Array.isArray(metadata.input) && metadata.input.length > 0
|
|
973
|
+
...(metadataProvider && Array.isArray(metadata.input) && metadata.input.length > 0
|
|
965
974
|
? { inputModalities: [...metadata.input] }
|
|
966
975
|
: {}),
|
|
967
976
|
...(metadata.reasoning === true ? { reasoningEfforts: [...ROUTED_COMBO_MEMBER_REASONING_EFFORTS] } : {}),
|
|
@@ -1038,15 +1047,21 @@ export function resolveComboCatalogMember(
|
|
|
1038
1047
|
&& typeof existing.contextWindow === "number"
|
|
1039
1048
|
&& existing.contextWindow > 0
|
|
1040
1049
|
) {
|
|
1041
|
-
|
|
1050
|
+
// Live discovery can explicitly say text-only even when configured routing
|
|
1051
|
+
// supplies a vision sidecar. Apply the same provider hints used for thin
|
|
1052
|
+
// rows before deriving a combo from this complete row.
|
|
1053
|
+
const hinted = prov && isModelVisionSidecarConsumer(prov, existing.id)
|
|
1054
|
+
? applyProviderConfigHints(target.provider, prov, existing, contextCap, metadataModelIdCaseFold)
|
|
1055
|
+
: existing;
|
|
1056
|
+
const capped = applyProviderContextCap(hinted.contextWindow, contextCap);
|
|
1042
1057
|
if (capped === undefined || capped === existing.contextWindow) {
|
|
1043
|
-
return withFallbackMetadata(
|
|
1058
|
+
return withFallbackMetadata(hinted);
|
|
1044
1059
|
}
|
|
1045
|
-
const maxInput = typeof
|
|
1046
|
-
? Math.min(
|
|
1060
|
+
const maxInput = typeof hinted.maxInputTokens === "number" && hinted.maxInputTokens > 0
|
|
1061
|
+
? Math.min(hinted.maxInputTokens, capped)
|
|
1047
1062
|
: Math.min(fallback?.maxInputTokens ?? capped, capped);
|
|
1048
1063
|
return withFallbackMetadata({
|
|
1049
|
-
...
|
|
1064
|
+
...hinted,
|
|
1050
1065
|
contextWindow: capped,
|
|
1051
1066
|
maxInputTokens: maxInput,
|
|
1052
1067
|
contextCap,
|
|
@@ -1387,6 +1402,51 @@ function modelInputModalities(
|
|
|
1387
1402
|
return undefined;
|
|
1388
1403
|
}
|
|
1389
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* A per-token rate exactly as a /models row publishes it, or undefined when the value is not a
|
|
1407
|
+
* usable non-negative number. Providers ship these both as JSON numbers and as decimal strings —
|
|
1408
|
+
* OpenRouter encodes free as the string `"0.00000000"` — so both shapes are accepted and nothing
|
|
1409
|
+
* else is. The explicit numeric-shape test has to run BEFORE any coercion: `Number("")` and
|
|
1410
|
+
* `Number(" ")` are both 0 and `Number(true)` is 1, so a bare `Number(value)` would classify a
|
|
1411
|
+
* row with an empty price string as free.
|
|
1412
|
+
*/
|
|
1413
|
+
const DISCOVERED_PRICING_RATE_PATTERN = /^-?\d+(?:\.\d+)?(?:[eE][-+]?\d+)?$/;
|
|
1414
|
+
|
|
1415
|
+
function discoveredPricingRate(value: unknown): number | undefined {
|
|
1416
|
+
const numeric = typeof value === "number"
|
|
1417
|
+
? value
|
|
1418
|
+
: typeof value === "string" && DISCOVERED_PRICING_RATE_PATTERN.test(value.trim())
|
|
1419
|
+
? Number(value.trim())
|
|
1420
|
+
: undefined;
|
|
1421
|
+
if (numeric === undefined || !Number.isFinite(numeric) || numeric < 0) return undefined;
|
|
1422
|
+
return numeric;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
1427
|
+
*
|
|
1428
|
+
* Fail closed. Only a complete pair of non-negative numeric rates classifies at all; a missing,
|
|
1429
|
+
* one-sided, non-numeric, or negative rate is "unknown" and therefore excluded from a free-only
|
|
1430
|
+
* filter. Showing a paid model under a Free filter spends the user's money, while hiding a free
|
|
1431
|
+
* one costs a click.
|
|
1432
|
+
*
|
|
1433
|
+
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
1434
|
+
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
1435
|
+
* purpose. And the operator's own `modelCosts` overlay is an estimate they typed, not something
|
|
1436
|
+
* the provider published, so a zeroed overlay never reaches this field either.
|
|
1437
|
+
*
|
|
1438
|
+
* Classification is on numeric zero and never on a unit conversion: OpenRouter quotes USD per
|
|
1439
|
+
* token while the cost overlays and the jawcode bundle quote per 1M, and zero is zero in both.
|
|
1440
|
+
*/
|
|
1441
|
+
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
1442
|
+
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
1443
|
+
if (!pricing) return "unknown";
|
|
1444
|
+
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
1445
|
+
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
1446
|
+
if (prompt === undefined || completion === undefined) return "unknown";
|
|
1447
|
+
return prompt === 0 && completion === 0 ? "free" : "paid";
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1390
1450
|
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
1391
1451
|
const metadata = plainRecord(item.metadata);
|
|
1392
1452
|
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
@@ -1412,6 +1472,13 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1412
1472
|
// supplying a recognized field changes behavior (#1797).
|
|
1413
1473
|
plainRecord(item.meta)?.n_ctx,
|
|
1414
1474
|
plainRecord(item.meta)?.n_ctx_train,
|
|
1475
|
+
// A chained OpenCodex hub (and other re-serving gateways) reports the per-model
|
|
1476
|
+
// window on the same capability record this function already reads for
|
|
1477
|
+
// `max_output_tokens` below (#4032). Without it every routed row fell through to
|
|
1478
|
+
// the 128k compatibility floor in parsing.ts while local forward rows kept their
|
|
1479
|
+
// real values. Appended after the recognized fields for the same reason as the
|
|
1480
|
+
// llama.cpp entries above: no provider that already resolves changes behavior.
|
|
1481
|
+
capabilityRecord?.context_length,
|
|
1415
1482
|
);
|
|
1416
1483
|
const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
|
|
1417
1484
|
const maxOutputTokens = positiveSafeInteger(
|
|
@@ -1444,6 +1511,7 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1444
1511
|
: undefined;
|
|
1445
1512
|
const capabilities = modelCapabilities(item);
|
|
1446
1513
|
const inputModalities = modelInputModalities(item, capabilities);
|
|
1514
|
+
const pricingStatus = discoveredPricingStatus(item);
|
|
1447
1515
|
return {
|
|
1448
1516
|
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
1449
1517
|
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
@@ -1451,6 +1519,11 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1451
1519
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
1452
1520
|
...(inputModalities ? { inputModalities } : {}),
|
|
1453
1521
|
...(capabilities ? { capabilities } : {}),
|
|
1522
|
+
// Omitted when the classification is "unknown", following this function's existing
|
|
1523
|
+
// contract that an unknown property is absent rather than present-and-empty. Callers
|
|
1524
|
+
// that need to tell "provider published no prices" from "this build does not classify"
|
|
1525
|
+
// call discoveredPricingStatus directly.
|
|
1526
|
+
...(pricingStatus !== "unknown" ? { pricingStatus } : {}),
|
|
1454
1527
|
};
|
|
1455
1528
|
}
|
|
1456
1529
|
|
|
@@ -1582,6 +1655,50 @@ async function fetchProviderModelsWithAuth(
|
|
|
1582
1655
|
? [...models, vertexDefaultSeed]
|
|
1583
1656
|
: models
|
|
1584
1657
|
);
|
|
1658
|
+
if (prov.adapter === "qoder") {
|
|
1659
|
+
if (!apiKey) return observed(configured, "degraded");
|
|
1660
|
+
const profile = resolveQoderProfile(prov.baseUrl);
|
|
1661
|
+
if (!profile) return observed(configured, "degraded");
|
|
1662
|
+
// Qoder's model list is entitlement-specific. Bind cache reads/writes to an irreversible PAT
|
|
1663
|
+
// fingerprint so an account switch cannot observe another account's roster, even if a caller
|
|
1664
|
+
// bypasses the normal config mutation path that clears provider caches.
|
|
1665
|
+
const authorityIdentity = createHash("sha256").update(apiKey).digest("hex");
|
|
1666
|
+
const fresh = getFreshCached(name, ttlMs, Date.now(), authorityIdentity);
|
|
1667
|
+
if (fresh) {
|
|
1668
|
+
return observed(withConfiguredRetention(
|
|
1669
|
+
applyConfigHintsToCachedModels(name, prov, fresh, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
1670
|
+
), "authoritative");
|
|
1671
|
+
}
|
|
1672
|
+
const scopedStale = getStaleCached(name, authorityIdentity);
|
|
1673
|
+
if (isModelsFetchCoolingDown(name) && scopedStale) {
|
|
1674
|
+
return observed(withConfiguredRetention(
|
|
1675
|
+
applyConfigHintsToCachedModels(name, prov, scopedStale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
1676
|
+
), "degraded");
|
|
1677
|
+
}
|
|
1678
|
+
const live = await fetchQoderModels(profile, apiKey);
|
|
1679
|
+
if (live.ok) {
|
|
1680
|
+
const discovered = live.models.map(id => ({
|
|
1681
|
+
id,
|
|
1682
|
+
provider: name,
|
|
1683
|
+
...catalogHintsFromProviderConfig(name, prov, id, contextCap, metadataModelIdCaseFold, captured.effectiveAlias),
|
|
1684
|
+
}));
|
|
1685
|
+
const forCache = withConfiguredRetention(discovered, { retainComboTargets: false });
|
|
1686
|
+
if (!setCached(name, forCache, Date.now(), cacheGeneration, authorityIdentity)) {
|
|
1687
|
+
return observed(withConfiguredRetention(configured), "degraded");
|
|
1688
|
+
}
|
|
1689
|
+
markProviderDiscoveryOk(name, live.models.length);
|
|
1690
|
+
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
1691
|
+
}
|
|
1692
|
+
if (isCurrentCacheGeneration()) {
|
|
1693
|
+
markModelsFetchFailure(name);
|
|
1694
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
1695
|
+
console.warn(`[opencodex] Qoder model discovery for "${name}" failed [${live.error}]${live.detail ? `: ${live.detail}` : ""}; using stale/static catalog degradation.`);
|
|
1696
|
+
}
|
|
1697
|
+
const stale = getStaleCached(name, authorityIdentity);
|
|
1698
|
+
return observed(withConfiguredRetention(
|
|
1699
|
+
stale ? applyConfigHintsToCachedModels(name, prov, stale, contextCap, metadataModelIdCaseFold, captured.effectiveAlias) : configured,
|
|
1700
|
+
), "degraded");
|
|
1701
|
+
}
|
|
1585
1702
|
if (prov.adapter === "cursor") {
|
|
1586
1703
|
if (!apiKey) return observed(configured, "degraded");
|
|
1587
1704
|
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
@@ -1807,7 +1924,14 @@ async function fetchProviderModelsWithAuth(
|
|
|
1807
1924
|
markProviderDiscoveryOk(name, live.length);
|
|
1808
1925
|
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
|
|
1809
1926
|
}
|
|
1810
|
-
const
|
|
1927
|
+
const googleAiStudio = effectiveGoogleMode(name, prov) === "ai-studio"
|
|
1928
|
+
? extractGoogleAiStudioModelItems(bounded.value, discovery.maxModels)
|
|
1929
|
+
: undefined;
|
|
1930
|
+
// Native /v1beta/models wins; a google row served by an OpenAI-compatible
|
|
1931
|
+
// gateway keeps the generic data[] / top-level-array contract.
|
|
1932
|
+
const extracted = googleAiStudio?.ok
|
|
1933
|
+
? googleAiStudio
|
|
1934
|
+
: extractProviderModelItems(bounded.value, discovery);
|
|
1811
1935
|
if (!extracted.ok) {
|
|
1812
1936
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
1813
1937
|
const diagnostic: Record<ModelDiscoveryResponseFailure, string> = {
|
|
@@ -2558,7 +2682,16 @@ async function gatherRoutedModelsUncached(
|
|
|
2558
2682
|
return {
|
|
2559
2683
|
models: models.map(model => {
|
|
2560
2684
|
const displayName = aliasDisplayNames.get(`${model.provider}/${model.id}`);
|
|
2561
|
-
|
|
2685
|
+
// #1711: one stamping point for every row this gather produces — routed, combo, and custom
|
|
2686
|
+
// alike — because it is the only place that has both the finished list and the config the
|
|
2687
|
+
// quota rules need. A combo votes over its own targets; anything else votes over the single
|
|
2688
|
+
// provider that would serve it.
|
|
2689
|
+
const targets = model.provider === COMBO_NAMESPACE
|
|
2690
|
+
? config.combos?.[model.id]?.targets ?? []
|
|
2691
|
+
: [{ provider: model.provider }];
|
|
2692
|
+
const inactive = quotaInactiveReason(config, targets);
|
|
2693
|
+
const named = displayName && !model.displayName ? { ...model, displayName } : model;
|
|
2694
|
+
return inactive ? { ...named, quotaInactiveReason: inactive } : named;
|
|
2562
2695
|
}),
|
|
2563
2696
|
comboOmissions: localOmissions,
|
|
2564
2697
|
providerAuthOutcomes: localProviderAuthOutcomes,
|
|
@@ -99,6 +99,12 @@ export const PICKER_ORDER_PRIORITY_BASE = 1_000;
|
|
|
99
99
|
// independent of display order. It does not freeze native advertisements. Absent on unmoved rows.
|
|
100
100
|
export const SPAWN_PRIORITY_FIELD = "opencodex_spawn_priority";
|
|
101
101
|
|
|
102
|
+
// OpenCodex-private catalog field: this row is listed but currently unable to serve (#1711).
|
|
103
|
+
// Codex ignores unknown catalog fields (same as opencodex_catalog_kind and the spawn priority
|
|
104
|
+
// above) and ensureStrictCatalogFields does not strip extras, so this is invisible to the native
|
|
105
|
+
// picker and cannot change what Codex offers. It never touches `visibility`.
|
|
106
|
+
export const CATALOG_INACTIVE_REASON_FIELD = "opencodex_inactive_reason";
|
|
107
|
+
|
|
102
108
|
export type SpawnAgentSurface = "v1" | "v2";
|
|
103
109
|
|
|
104
110
|
export type SubagentRosterExclusionReason =
|
|
@@ -383,6 +389,10 @@ export function deriveEntry(
|
|
|
383
389
|
if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
|
|
384
390
|
applyCatalogModelMetadata(e, model);
|
|
385
391
|
if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
|
|
392
|
+
// Additive only. `visibility` is untouched: an inactive row must still be OFFERED, which is
|
|
393
|
+
// the whole point of #1711 — operator disable is what removes rows, and it stays a separate
|
|
394
|
+
// path from this one.
|
|
395
|
+
if (model?.quotaInactiveReason) e[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
386
396
|
} else {
|
|
387
397
|
applyNativeOpenAiContextOverride(e, contextCap);
|
|
388
398
|
if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
|
|
@@ -430,6 +440,10 @@ export function deriveEntry(
|
|
|
430
440
|
if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
|
|
431
441
|
applyCatalogModelMetadata(entry, model);
|
|
432
442
|
if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
|
|
443
|
+
// Same additive stamp as the templated path above. A routed row that reaches the no-template
|
|
444
|
+
// fallback is still a served row, so omitting it here would make the field depend on whether a
|
|
445
|
+
// template happened to be cached — which is exactly what the regression test caught.
|
|
446
|
+
if (model?.quotaInactiveReason) entry[CATALOG_INACTIVE_REASON_FIELD] = model.quotaInactiveReason;
|
|
433
447
|
if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
|
|
434
448
|
return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
|
|
435
449
|
preserveExactInputModalities: preserveExact,
|
package/src/codex/inject.ts
CHANGED
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
resolveEffectiveUserIdentity,
|
|
32
32
|
} from "./user-identity";
|
|
33
33
|
import {
|
|
34
|
+
hasUnverifiedJournalBaseline,
|
|
34
35
|
markJournalInjectedState,
|
|
35
36
|
journaledInjectedOpenaiBaseUrl,
|
|
36
37
|
journaledInjectedRealtimeWsBaseUrl,
|
|
@@ -174,6 +175,8 @@ export interface CodexRoutingTarget {
|
|
|
174
175
|
* and is never weakened by this flag.
|
|
175
176
|
*/
|
|
176
177
|
desktopAuthless?: boolean;
|
|
178
|
+
/** Select the dedicated provider identity so Codex owns compaction locally. */
|
|
179
|
+
clientCompaction?: boolean;
|
|
177
180
|
}
|
|
178
181
|
|
|
179
182
|
function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTarget {
|
|
@@ -197,14 +200,19 @@ function validateCodexRoutingTarget(target: CodexRoutingTarget): CodexRoutingTar
|
|
|
197
200
|
return { ...target, baseUrl: `${parsed.origin}/v1` };
|
|
198
201
|
}
|
|
199
202
|
|
|
200
|
-
/** Provider-table form is used
|
|
203
|
+
/** Provider-table form is used when auth, admission, or compaction policy needs a dedicated provider. */
|
|
201
204
|
function usesProviderTable(target: CodexRoutingTarget): boolean {
|
|
202
|
-
return target.requiresAdmissionToken
|
|
205
|
+
return target.requiresAdmissionToken
|
|
206
|
+
|| target.desktopAuthless === true
|
|
207
|
+
|| target.clientCompaction === true;
|
|
203
208
|
}
|
|
204
209
|
|
|
205
210
|
export function standaloneCodexRoutingTarget(
|
|
206
211
|
port: number,
|
|
207
|
-
config?: Pick<
|
|
212
|
+
config?: Pick<
|
|
213
|
+
OcxConfig,
|
|
214
|
+
"hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless" | "codexClientCompaction"
|
|
215
|
+
>,
|
|
208
216
|
): CodexRoutingTarget {
|
|
209
217
|
const loopback = config?.unauthenticatedLoopbackListener;
|
|
210
218
|
const effectivePort = loopback?.enabled ? loopback.port : port;
|
|
@@ -217,6 +225,9 @@ export function standaloneCodexRoutingTarget(
|
|
|
217
225
|
...(config?.codexDesktopAuthless === true && !requiresAdmissionToken
|
|
218
226
|
? { desktopAuthless: true }
|
|
219
227
|
: {}),
|
|
228
|
+
...(config?.codexClientCompaction === true && !requiresAdmissionToken
|
|
229
|
+
? { clientCompaction: true }
|
|
230
|
+
: {}),
|
|
220
231
|
};
|
|
221
232
|
}
|
|
222
233
|
|
|
@@ -833,7 +844,7 @@ function buildProfileFileForTarget(
|
|
|
833
844
|
const host = new URL(origin).host;
|
|
834
845
|
// Design B (loopback): the reference/fallback file documents the root override form.
|
|
835
846
|
// Non-loopback keeps the legacy provider-table shape (built-in provider cannot carry
|
|
836
|
-
// the x-opencodex-api-key env header);
|
|
847
|
+
// the x-opencodex-api-key env header); explicit Desktop policies share that shape.
|
|
837
848
|
if (!usesProviderTable(target)) {
|
|
838
849
|
const lines = [
|
|
839
850
|
"# OpenCodex proxy fallback config (Design B)",
|
|
@@ -1014,11 +1025,37 @@ export async function injectCodexConfig(
|
|
|
1014
1025
|
? setRootModelCatalogPath(content, catalogPath)
|
|
1015
1026
|
: stripOpencodexCatalogPath(content);
|
|
1016
1027
|
|
|
1017
|
-
// Provider-table form: non-loopback admission
|
|
1018
|
-
const
|
|
1028
|
+
// Provider-table form: non-loopback admission or an explicit Desktop policy.
|
|
1029
|
+
const providerTableMode = usesProviderTable(routingTarget);
|
|
1030
|
+
// Client compaction is the one table form that must not orphan existing threads. It changes
|
|
1031
|
+
// the DEFAULT provider to `opencodex`, but a thread already tagged `openai` keeps resolving
|
|
1032
|
+
// to Codex's built-in entry, and without the root override that entry is api.openai.com —
|
|
1033
|
+
// the thread would resume outside this proxy and outside configured routing. Keeping the
|
|
1034
|
+
// marker-owned root override alongside the table fixes that at the source: codex builds its
|
|
1035
|
+
// provider map as merge_configured_model_providers(built_in_model_providers(openai_base_url),
|
|
1036
|
+
// model_providers), so the override lands on the built-in `openai` entry when the map is
|
|
1037
|
+
// built, independent of which id is the default, and the merge leaves that entry alone for
|
|
1038
|
+
// every id except the two Amazon Bedrock ones. With the managed override in place both
|
|
1039
|
+
// entries point at this proxy. That is a guarantee about the line we own: when the user owns
|
|
1040
|
+
// the root line we inject nothing, and the built-in entry keeps whatever destination they
|
|
1041
|
+
// chose, so an `openai`-tagged thread follows their configuration rather than this proxy.
|
|
1042
|
+
//
|
|
1043
|
+
// Re-tagging history was the alternative and it cannot be made durable: the length-preserving
|
|
1044
|
+
// first-line repair cannot grow "openai" into "opencodex" without pre-existing padding, and
|
|
1045
|
+
// codex re-appends that stale first line whenever it writes git or memory-mode metadata.
|
|
1046
|
+
//
|
|
1047
|
+
// Authless is excluded on purpose: its whole point is a provider that carries
|
|
1048
|
+
// requires_openai_auth = false, and admission-token forms cannot use the root key at all.
|
|
1049
|
+
// Those two forms therefore keep their existing behaviour, forward-tagging resume history with
|
|
1050
|
+
// originals backed up, and that includes the case where a user enables authless and client
|
|
1051
|
+
// compaction together. Only the compaction-only form skips the history unit.
|
|
1052
|
+
const keepRootOverrideAlongsideTable = providerTableMode
|
|
1053
|
+
&& routingTarget.clientCompaction === true
|
|
1054
|
+
&& routingTarget.desktopAuthless !== true
|
|
1055
|
+
&& routingTarget.requiresAdmissionToken !== true;
|
|
1019
1056
|
let keptUserBaseUrl = false;
|
|
1020
1057
|
let keptUserRealtimeWsBaseUrl = false;
|
|
1021
|
-
if (
|
|
1058
|
+
if (providerTableMode) {
|
|
1022
1059
|
// Legacy (non-loopback) injection: the built-in openai provider cannot carry the
|
|
1023
1060
|
// x-opencodex-api-key env header, so keep the opencodex provider table + root re-tag.
|
|
1024
1061
|
// The authless opt-in needs the same table because only a dedicated provider can carry
|
|
@@ -1030,6 +1067,14 @@ export async function injectCodexConfig(
|
|
|
1030
1067
|
content.trimEnd() +
|
|
1031
1068
|
"\n" +
|
|
1032
1069
|
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
|
|
1070
|
+
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
1071
|
+
// are the Design B ones: a user's own root line is never replaced.
|
|
1072
|
+
if (keepRootOverrideAlongsideTable) {
|
|
1073
|
+
content = stripInjectedOpenaiBaseUrl(content);
|
|
1074
|
+
const rootFallback = setRootOpenaiBaseUrlForTarget(content, routingTarget);
|
|
1075
|
+
content = rootFallback.content;
|
|
1076
|
+
keptUserBaseUrl = rootFallback.keptUserBaseUrl;
|
|
1077
|
+
}
|
|
1033
1078
|
} else {
|
|
1034
1079
|
// Design B (loopback): a single root override; codex keeps its native `openai` provider id
|
|
1035
1080
|
// so thread history is never remapped. Any legacy form was already stripped above.
|
|
@@ -1149,6 +1194,24 @@ export async function injectCodexConfig(
|
|
|
1149
1194
|
};
|
|
1150
1195
|
}
|
|
1151
1196
|
|
|
1197
|
+
const journalBaselineIsNative = (): boolean => {
|
|
1198
|
+
// Value evidence survives an app rewrite that removes the ownership comments.
|
|
1199
|
+
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl({ readOnly: true });
|
|
1200
|
+
const journaledRealtimeWsBaseUrl = journaledInjectedRealtimeWsBaseUrl({ readOnly: true });
|
|
1201
|
+
const looksInjectedByValue =
|
|
1202
|
+
(journaledBaseUrl !== null && rootTomlString(rawContent, "openai_base_url") === journaledBaseUrl)
|
|
1203
|
+
|| (journaledRealtimeWsBaseUrl !== null
|
|
1204
|
+
&& rootTomlString(rawContent, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
|
|
1205
|
+
return !hasInjectedCodexRouting(rawContent) && !looksInjectedByValue;
|
|
1206
|
+
};
|
|
1207
|
+
const readCurrentProfile = (): string | null => existsSync(CODEX_PROFILE_PATH)
|
|
1208
|
+
? readFileSync(CODEX_PROFILE_PATH, "utf-8")
|
|
1209
|
+
: null;
|
|
1210
|
+
const unverifiedJournalMessage = "Codex configuration was not written: the journal has no verified baseline for the current config/profile. Current files and the journal were preserved.";
|
|
1211
|
+
if (!journalBaselineIsNative() && hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) {
|
|
1212
|
+
return { success: false, message: unverifiedJournalMessage };
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1152
1215
|
if (options.validateOnly) {
|
|
1153
1216
|
return {
|
|
1154
1217
|
success: true,
|
|
@@ -1157,31 +1220,29 @@ export async function injectCodexConfig(
|
|
|
1157
1220
|
}
|
|
1158
1221
|
|
|
1159
1222
|
const applyNativeArtifacts = (): void => {
|
|
1160
|
-
// #1798 again: a Codex app rewrite keeps values and drops the ownership comments, so
|
|
1161
|
-
// marker evidence alone would classify our own routed config as the user's native
|
|
1162
|
-
// baseline and replace the real original snapshot. Value evidence from the journal
|
|
1163
|
-
// (the URLs the last injection recorded writing) blocks that misclassification.
|
|
1164
|
-
const journaledBaseUrl = journaledInjectedOpenaiBaseUrl();
|
|
1165
|
-
const journaledRealtimeWsBaseUrl = journaledInjectedRealtimeWsBaseUrl();
|
|
1166
|
-
const looksInjectedByValue =
|
|
1167
|
-
(journaledBaseUrl !== null && rootTomlString(rawContent, "openai_base_url") === journaledBaseUrl)
|
|
1168
|
-
|| (journaledRealtimeWsBaseUrl !== null
|
|
1169
|
-
&& rootTomlString(rawContent, REALTIME_WS_BASE_URL_KEY) === journaledRealtimeWsBaseUrl);
|
|
1170
1223
|
writeJournal({
|
|
1171
|
-
currentStateIsNative:
|
|
1224
|
+
currentStateIsNative: journalBaselineIsNative(),
|
|
1172
1225
|
configContent: baselineContent,
|
|
1173
1226
|
owner: options.journalOwner,
|
|
1174
1227
|
});
|
|
1228
|
+
// A native snapshot may have been refreshed above. An older hashless routed snapshot
|
|
1229
|
+
// must not gain the new injection's hash and later overwrite preserved user edits.
|
|
1230
|
+
if (hasUnverifiedJournalBaseline(baselineContent, readCurrentProfile())) throw new Error(unverifiedJournalMessage);
|
|
1175
1231
|
atomicWriteFile(CODEX_CONFIG_PATH, content);
|
|
1176
1232
|
atomicWriteFile(CODEX_PROFILE_PATH, profileContent);
|
|
1177
1233
|
markJournalInjectedState(content, profileContent, {
|
|
1178
|
-
// A root override is ours
|
|
1179
|
-
|
|
1234
|
+
// A root override is ours whenever we wrote one and no user-owned value won. That is
|
|
1235
|
+
// loopback Design B, and now also the client-compaction form, which keeps the same
|
|
1236
|
+
// marker-owned root line beside its provider table. Journaling it matters because the
|
|
1237
|
+
// marker comment is not durable: the Codex app can reserialize config.toml and drop
|
|
1238
|
+
// comments, and restore then has only the journaled value to tell our line from a user's
|
|
1239
|
+
// (#1798). The other table forms never write the key, so they still record null.
|
|
1240
|
+
injectedOpenaiBaseUrl: (providerTableMode && !keepRootOverrideAlongsideTable) || keptUserBaseUrl
|
|
1180
1241
|
? null
|
|
1181
1242
|
: rootTomlString(content, "openai_base_url"),
|
|
1182
1243
|
// The sideband override is ours only when we wrote it this pass (never in legacy mode,
|
|
1183
1244
|
// never when the user owns either key).
|
|
1184
|
-
injectedRealtimeWsBaseUrl:
|
|
1245
|
+
injectedRealtimeWsBaseUrl: providerTableMode || keptUserBaseUrl || keptUserRealtimeWsBaseUrl
|
|
1185
1246
|
? null
|
|
1186
1247
|
: rootTomlString(content, REALTIME_WS_BASE_URL_KEY),
|
|
1187
1248
|
// This is the catalog artifact selected for this injection, even when config.toml
|
|
@@ -1318,7 +1379,11 @@ export async function injectCodexConfig(
|
|
|
1318
1379
|
}
|
|
1319
1380
|
// Legacy mode still forward-tags history so re-tagged threads stay listable. Design B needs
|
|
1320
1381
|
// the opposite: a one-time migration of previously re-tagged threads BACK to openai (restore
|
|
1321
|
-
// machinery; cheap no-op when there is nothing to migrate).
|
|
1382
|
+
// machinery; cheap no-op when there is nothing to migrate). The client-compaction opt-in keeps
|
|
1383
|
+
// the root override alongside its table precisely so it does NOT have to touch history: an
|
|
1384
|
+
// existing `openai`-tagged thread still reaches this proxy through the built-in entry. So it
|
|
1385
|
+
// skips this unit, and future-only means what it says — no provider metadata is rewritten and
|
|
1386
|
+
// no `ocx1:` payload is touched.
|
|
1322
1387
|
// History runs in a Worker under H, not on this thread.
|
|
1323
1388
|
//
|
|
1324
1389
|
// The three surfaces it touches — the SQLite rows, the backup manifest, and the
|
|
@@ -1331,8 +1396,8 @@ export async function injectCodexConfig(
|
|
|
1331
1396
|
expectedDesiredEnabled: true,
|
|
1332
1397
|
operation: deriveCodexHistoryOperation({
|
|
1333
1398
|
direction: "apply",
|
|
1334
|
-
resumeHistory: config?.syncResumeHistory !== false,
|
|
1335
|
-
legacyMode,
|
|
1399
|
+
resumeHistory: config?.syncResumeHistory !== false && !keepRootOverrideAlongsideTable,
|
|
1400
|
+
legacyMode: providerTableMode,
|
|
1336
1401
|
}),
|
|
1337
1402
|
});
|
|
1338
1403
|
// A blocked or failed unit is reported, not silently counted as zero work:
|
|
@@ -1363,17 +1428,42 @@ export async function injectCodexConfig(
|
|
|
1363
1428
|
const ejected = (history as { ejectedRows?: number }).ejectedRows ?? 0;
|
|
1364
1429
|
const migratedRows = (history.rows ?? 0) + ejected;
|
|
1365
1430
|
const historyMessage =
|
|
1366
|
-
|
|
1431
|
+
keepRootOverrideAlongsideTable
|
|
1432
|
+
? (keptUserBaseUrl
|
|
1433
|
+
? ` Codex resume history: left unchanged; threads already tagged openai follow your configured root openai_base_url.\n`
|
|
1434
|
+
: ` Codex resume history: left unchanged; existing threads keep reaching the proxy through the retained openai_base_url override.\n`)
|
|
1435
|
+
: config?.syncResumeHistory === false
|
|
1367
1436
|
? ` Codex resume history: left unchanged (syncResumeHistory=false).\n`
|
|
1368
1437
|
: history.failed
|
|
1369
|
-
? formatApplyHistoryFailure(historyOutcome,
|
|
1370
|
-
:
|
|
1438
|
+
? formatApplyHistoryFailure(historyOutcome, providerTableMode)
|
|
1439
|
+
: providerTableMode
|
|
1371
1440
|
? ` Codex resume history: ${history.rows} thread(s) made visible for opencodex; originals backed up for restore.\n`
|
|
1372
1441
|
: migratedRows > 0
|
|
1373
1442
|
? ` Codex resume history: restored original provider metadata for ${migratedRows} manifest-backed thread(s) (one-time).\n`
|
|
1374
1443
|
: ` Codex resume history: no backed-up metadata pending; untracked routed history left unchanged.\n`;
|
|
1375
|
-
// A user-owned root openai_base_url means we did NOT install routing — say so honestly
|
|
1444
|
+
// A user-owned root openai_base_url means we did NOT install root routing — say so honestly
|
|
1376
1445
|
// instead of claiming the proxy route is active (catalog/fast_mode were still written).
|
|
1446
|
+
//
|
|
1447
|
+
// The client-compaction form writes a provider table as well, so "nothing was injected" would
|
|
1448
|
+
// misdescribe the file it just produced: new threads do use the injected table. Report that
|
|
1449
|
+
// mixed result on its own terms, and never tell the operator to delete a setting of theirs.
|
|
1450
|
+
// Ownership alone says nothing about destination: their line may already target this proxy.
|
|
1451
|
+
if (keptUserBaseUrl && keepRootOverrideAlongsideTable) {
|
|
1452
|
+
return {
|
|
1453
|
+
success: true,
|
|
1454
|
+
...(nativeSubagentDefaultsWarning ? { nativeSubagentDefaultsWarning } : {}),
|
|
1455
|
+
message:
|
|
1456
|
+
`Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n` +
|
|
1457
|
+
` Your root openai_base_url was left exactly as you set it, so opencodex did not add its own.\n` +
|
|
1458
|
+
catalogMessage +
|
|
1459
|
+
historyMessage +
|
|
1460
|
+
managedDefaultsMessage +
|
|
1461
|
+
` New threads use the injected opencodex provider and route through the proxy.\n` +
|
|
1462
|
+
` Threads already tagged openai resolve through Codex's built-in provider, which your root openai_base_url points at.\n` +
|
|
1463
|
+
` No root URL change is required to enable client-side compaction for new threads.\n` +
|
|
1464
|
+
` Fallback: codex --profile opencodex (same behavior)`,
|
|
1465
|
+
};
|
|
1466
|
+
}
|
|
1377
1467
|
if (keptUserBaseUrl) {
|
|
1378
1468
|
return {
|
|
1379
1469
|
success: true,
|
|
@@ -1391,7 +1481,9 @@ export async function injectCodexConfig(
|
|
|
1391
1481
|
}
|
|
1392
1482
|
const headline = routingTarget.desktopAuthless === true
|
|
1393
1483
|
? `Injected opencodex as default provider into Codex config (authless Desktop mode: requires_openai_auth = false).\n`
|
|
1394
|
-
:
|
|
1484
|
+
: routingTarget.clientCompaction === true
|
|
1485
|
+
? `Injected opencodex as default provider into Codex config (client-side compaction mode; ChatGPT auth remains required).\n`
|
|
1486
|
+
: providerTableMode
|
|
1395
1487
|
? `Injected opencodex as default provider into Codex config.\n`
|
|
1396
1488
|
: `Pointed Codex's built-in openai provider at the opencodex proxy (openai_base_url + realtime sideband override).\n`;
|
|
1397
1489
|
return {
|
|
@@ -1405,7 +1497,7 @@ export async function injectCodexConfig(
|
|
|
1405
1497
|
` All models now route through opencodex proxy (like OpenRouter).\n` +
|
|
1406
1498
|
` OpenAI models (gpt-5.5, etc.) are passed through to OpenAI.\n` +
|
|
1407
1499
|
` Custom models route to their configured providers.\n` +
|
|
1408
|
-
(
|
|
1500
|
+
(providerTableMode
|
|
1409
1501
|
? ` Fallback: codex --profile opencodex (same behavior)`
|
|
1410
1502
|
: ` Fallback reference: ${CODEX_PROFILE_PATH}`),
|
|
1411
1503
|
};
|
|
@@ -1733,6 +1825,12 @@ export function skippedRestoreEnvelope(success: boolean, message: string): Codex
|
|
|
1733
1825
|
function restoreCodexConfigInline(): CodexRestoreConfigResult {
|
|
1734
1826
|
try {
|
|
1735
1827
|
const journal = restoreJournalState();
|
|
1828
|
+
if (journal.unverified) {
|
|
1829
|
+
return {
|
|
1830
|
+
state: "failed", changed: false, action: "failed",
|
|
1831
|
+
message: "Codex journal recovery was not verified; current configuration files and the journal were preserved.",
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1736
1834
|
const restored = journal.configRestored
|
|
1737
1835
|
? { success: true, message: "Codex config restored from opencodex journal." }
|
|
1738
1836
|
: removeCodexConfig({ preserveProfile: journal.profileRestored || journal.profileChanged });
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
forgetEphemeralSecretPath,
|
|
17
17
|
hardenSecretPath,
|
|
18
18
|
} from "../../lib/windows-secret-acl";
|
|
19
|
+
import { resetCodexAppServerCatalogStateCache } from "../app-server-processes";
|
|
19
20
|
|
|
20
21
|
export interface PreparedCatalogFileWrite {
|
|
21
22
|
readonly path: string;
|
|
@@ -167,6 +168,7 @@ export function replaceActiveCodexCatalog(
|
|
|
167
168
|
): void {
|
|
168
169
|
assertCatalogWritePermit(permit, owningCodexHome);
|
|
169
170
|
atomicWriteFile(prepared.path, prepared.content, io);
|
|
171
|
+
resetCodexAppServerCatalogStateCache();
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
/** Atomically publish the catalog-path-keyed immutable backup without clobbering. */
|
|
@@ -200,4 +202,5 @@ export function replaceCodexModelsCache(
|
|
|
200
202
|
): void {
|
|
201
203
|
assertCatalogWritePermit(permit, owningCodexHome);
|
|
202
204
|
atomicWriteFile(prepared.path, prepared.content, io);
|
|
205
|
+
resetCodexAppServerCatalogStateCache();
|
|
203
206
|
}
|