Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/codex/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import { admitCodexWrite, type CodexAdmission } from "./admission";
import type { CodexCatalogSyncOptions } from "./catalog/sync";
import { resetCodexAppServerCatalogStateCache } from "./app-server-processes";
import { providerUsesReasoningMetadata, refreshReasoningMetadata } from "../providers/reasoning-metadata";

export interface CodexSyncResult {
/**
Expand Down Expand Up @@ -67,13 +68,21 @@ interface CodexSyncDeps {
admitCodexWrite?: () => CodexSyncAdmission;
currentExternalCodexModelProvider?: typeof currentExternalCodexModelProvider;
collectCodexHomeDiagnostic?: typeof collectOrcaCodexHomeDiagnostic;
refreshReasoningMetadata?: typeof refreshReasoningMetadata;
}

const defaultDeps: CodexSyncDeps = {
refreshCodexModelCatalog,
injectCodexConfig,
refreshReasoningMetadata,
};

async function refreshReasoningMetadataForSync(config: OcxConfig, deps: CodexSyncDeps): Promise<void> {
if (Object.values(config.providers).some(providerUsesReasoningMetadata)) {
await deps.refreshReasoningMetadata?.();
}
}

function reportCodexHomeTarget(
log: Pick<Console, "log" | "error"> | null,
collectDiagnostic: typeof collectOrcaCodexHomeDiagnostic,
Expand Down Expand Up @@ -234,6 +243,9 @@ export async function syncModelsToCodex(
}

applyProxyEnv(config); // `ocx ensure`/`ocx sync` fetch provider models outside the server process
// Bootstrap the optional ladder snapshot before gathering the catalog. Keeping this in the
// sync plane prevents an unrelated models.dev fetch from interleaving with a routed turn.
await refreshReasoningMetadataForSync(config, deps);
let added = 0;
let catalogPath: string | null = null;
let catalogPathForInjection: string | null | undefined;
Expand Down Expand Up @@ -339,6 +351,7 @@ async function refreshCatalogForSync(
let refreshOutcome: "committed" | "refused" | undefined;
let comboOmissions: ComboCatalogOmission[] = [];
try {
await refreshReasoningMetadataForSync(config, deps);
const cat = await deps.refreshCodexModelCatalog(config, undefined, catalogOptions);
refreshOutcome = cat.refreshOutcome;
added = cat.added;
Expand Down
5 changes: 5 additions & 0 deletions src/providers/reasoning-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ function metadataProviderKey(provider: OcxProviderConfig): string | undefined {
return undefined;
}

/** Whether catalog synchronization should fetch models.dev metadata for this destination. */
export function providerUsesReasoningMetadata(provider: OcxProviderConfig): boolean {
return metadataProviderKey(provider) !== undefined;
}

/**
* Local mirror of `modelRecordValue()` from `src/reasoning-effort.ts`, which imports this
* module and so cannot be imported back. Exact id, then the `family:` prefix, then a
Expand Down
14 changes: 3 additions & 11 deletions src/reasoning-effort.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { OcxProviderConfig } from "./types";
import { modelInList } from "./types";
import { dropLearnedUnsupportedReasoningEfforts, ensureReasoningMetadataSnapshot, reasoningEffortsFromMetadata } from "./providers/reasoning-metadata";
import { dropLearnedUnsupportedReasoningEfforts, reasoningEffortsFromMetadata } from "./providers/reasoning-metadata";

// Descriptions mirror the upstream bundled models.json canonical wording (openai/codex PR #31684).
export const CODEX_REASONING_LEVELS: { effort: string; description: string }[] = [
Expand Down Expand Up @@ -168,18 +168,10 @@ export function configuredReasoningEfforts(provider: OcxProviderConfig, modelId:
}
// models.dev publishes the per-model ladder that routed providers never expose on /models.
// (OpenCode Zen Go answers ids only). Only consulted when nothing was configured for this
// model, so every hand-written contract stays authoritative. The snapshot refreshes itself in
// the background; no snapshot means the previous behaviour.
// The refresh is asked for only once a snapshot has already answered, which means it only ever
// refreshes a STALE snapshot. Review asked for the opposite — refresh when the snapshot is
// missing or corrupt, since that is the case this lookup cannot serve. That is declined here:
// a missing snapshot is the default state of every fresh install and every test process, so
// requesting the fetch here puts a models.dev request on the request path of the first routed
// turn to a gated destination. Refreshing a snapshot that does not exist is catalog-sync work,
// not request work.
// model, so every hand-written contract stays authoritative. Catalog sync owns snapshot
// refresh; no snapshot means the previous behaviour.
const fromMetadata = reasoningEffortsFromMetadata(provider, modelId);
if (fromMetadata !== undefined) {
ensureReasoningMetadataSnapshot();
return dropLearnedUnsupportedReasoningEfforts(provider, modelId, healMappedTiers(provider, modelId, fromMetadata));
}
return undefined;
Expand Down
4 changes: 4 additions & 0 deletions structure/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ irreversible credential fingerprint: a credential switch observes neither the fr
roster recorded under the previous credential, and a failed discovery's cooldown neither supplies
the previous credential's stale roster nor suppresses the next credential's first discovery.

Synchronizing a supported routed provider first refreshes its models.dev effort snapshot through
`src/codex/sync.ts` and `src/providers/reasoning-metadata.ts`; this keeps missing-cache network work
out of request-time ladder resolution.

A Devin live row spreads its measured `inputModalities` before
`catalogHintsFromProviderConfig`, so exact `modelCapabilities` declarations, the legacy
`modelInputModalities` record and the vision-sidecar rewrite keep precedence and the live
Expand Down
3 changes: 3 additions & 0 deletions structure/codex-home.md
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ management read degrades instead of returning an error page.

`src/codex/history-provider.ts` rejects provider-history changes with `history_paginated_requires_native_writer` when a target begins with an ordinal-bearing record, later contains a paginated record after a legacy start (#4311), or declares `history_mode=paginated`. The first line alone is not sufficient: a rollout that started unnumbered and was later migrated is also refused. Apply, manifest-backed restore, and explicit legacy recovery preflight all selected targets before changing database rows or manifests. The append boundary checks again. Codex owns ordinal allocation and the live projection cursor; reading the last ordinal and appending N+1 is not safe concurrent coordination. Legacy unnumbered rollouts retain their existing behavior. History Worker targets resolve the canonical manifest first and an existing pre-normalization Windows filename second, so passing an explicit target cannot bypass upgrade recovery. This guard prevents the observed stable-format corruption; it does not implement native-writer integration.

`src/codex/sync.ts` owns startup and explicit-sync bootstrap of the optional
`src/providers/reasoning-metadata.ts` snapshot; request handling never owns that disk refresh.

Injection preflights affected history using the normalized config candidate before writing config/profile/journal, then checks again after the complete artifact write. Native restore also rechecks after successful journal restoration or fallback removal, while exact config/profile/journal preimages and any coordinated remove transaction remain available for compensation.

The preflight opens the state store read-write-free and in that order deliberately. `{ readonly: true }` is the primary open and the only one that joins a live writer's WAL shared memory, so a thread another process just migrated to paginated history is visible and refuses here. A WAL store whose last writer closed cleanly has no `-shm` to join and a read-only connection may not create one, so that open fails `SQLITE_CANTOPEN` on a perfectly healthy store and the catch-all turned it into `history_injection_preflight_unavailable` on every attempt (#4943). The immutable fallback (`immutable=1` over a `file:` URI, the same idiom as the storage scanner and the log-guard inspector) is admitted only when neither `-wal` nor `-shm` is on disk, because that is the state in which the main database is the whole store and an immutable read is exact rather than stale. Either sidecar present, or any other open failure, keeps the original error and the refusal that follows: an immutable read is a snapshot, and a refusal this preflight fails to observe is a config transition over history Codex owns. The guard covers the whole primary attempt, not only the constructor. `sqlite3_open_v2` never reads page 1, so a WAL header is not inspected until the first prepare, and on macOS that is where the absent `-shm` is raised; the first read therefore happens inside the attempt, where the error can still be classified. Bun's bundled SQLite on Linux materializes both sidecars on that same read and never fails, which is why Linux and Windows evidence could not see this gap.
Expand Down
3 changes: 3 additions & 0 deletions structure/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -474,6 +474,9 @@ management handler's own unknown-id answer, and it names the id and `ocx models

Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee.

Catalog synchronization in `src/codex/sync.ts` also bootstraps reasoning metadata only when the
configured providers contain a destination supported by `src/providers/reasoning-metadata.ts`.

Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it.

Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings.
Expand Down
2 changes: 1 addition & 1 deletion structure/gui-and-management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,7 @@ its defaults and exclusions are owned by [Responses transport](transports/respon

The provider editor field policy exposes `showThinkingSummary` as a boolean provider option; it controls Responses summary defaults without a dashboard rendering change. See [Google provider](providers/google.md).

Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee.
Paginated and migration-capable history follows the [authoritative writer contract](codex-home.md#paginated-history-writer-boundary); this document adds no independent writer guarantee. Management-triggered catalog synchronization uses `src/codex/sync.ts` to bootstrap the optional `src/providers/reasoning-metadata.ts` snapshot before catalog gathering for supported destinations.

Codex pool settings and their consumers follow the [reset-first ordering contract](providers/openai-tiers.md#reset-first-account-ordering), including independent-quota fallback, preserved affinity, strategy-specific threshold summaries, and shared short-observation freshness for switch warnings. Codex account DTOs and cards expose the routing-plan exclusion separately from credential health; the [plan exclusion contract](providers/openai-tiers.md#automatic-pool-plan-exclusions) also governs CLI projection. Private pool credential metadata follows the [quota-history publication identity contract](providers/openai-tiers.md#quota-history-publication-identity); credential-only and account DTO projections omit it.

Expand Down
4 changes: 4 additions & 0 deletions structure/ops/docs-and-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,3 +487,7 @@ Native steering generation overrides, explicit public-API eligibility and the co

The public server configuration reference documents the optional
[compaction routing override](../transports/responses.md#compaction-routing-overrides). Its regression file is registered in both test-layout inventories.

Startup and explicit catalog synchronization in `src/codex/sync.ts` bootstrap supported-provider
effort metadata through `src/providers/reasoning-metadata.ts`; routed requests do not trigger that
network refresh.
4 changes: 4 additions & 0 deletions structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,3 +731,7 @@ often that refusal fires and can never replace it.

Upstream API-key usage follows the [physical-attempt account attribution contract](../gui-and-management-api.md#upstream-key-account-attribution), independently of subscription quota observations.
`src/codex/auth-api/login-flow.ts` distinguishes HTTP 429 from an attempted warmup as `codex_warmup_rate_limited` and preserves that code in OAuth status. Failed attempted warmup does not persist replacement credentials; quota-confirmed deferred registration and HTTP 401/403 handling remain separate. `src/codex/warmup.ts` retains a known 429 when bounded error-body draining times out.

Catalog synchronization in `src/codex/sync.ts` bootstraps models.dev effort metadata through
`src/providers/reasoning-metadata.ts` only for supported routed destinations; request-time effort
mapping does not initiate that network operation.
2 changes: 2 additions & 0 deletions structure/providers/xai-grok.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ Devin CLI credential path composition in `src/oauth/devin/cli-import.ts` follows
[Anthropic seed image metadata](../runtime.md#capability-aware-image-admission) is provider-scoped; xAI model metadata and transport behavior remain unchanged.

Provider-scoped catalog hints remain isolated by provider in `src/providers/registry/entries-core.ts`. The
models.dev effort snapshot is likewise destination-gated by `src/providers/reasoning-metadata.ts`
and bootstrapped from `src/codex/sync.ts`, not from a routed request.
OpenCode Go `deepseek-v4.1-flash` 1,048,576-token context hint does not change xAI model metadata or
transport behavior.
The first-party DeepSeek `deepseek-flash` native `text`/`image` declaration is likewise scoped to
Expand Down
4 changes: 4 additions & 0 deletions structure/runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,10 @@ Provider-scoped approval reviewer settings are projected by the [catalog owner](

Renamed fixed-key providers receive [missing reasoning metadata](catalog.md#renamed-destination-reasoning-metadata) during derivation; explicit per-model entries and provider defaults retain precedence.

`src/codex/sync.ts` refreshes the `src/providers/reasoning-metadata.ts` models.dev snapshot for
supported routed destinations before catalog gathering, so missing and corrupt snapshots bootstrap
without adding network work to `src/reasoning-effort.ts` request-time ladder reads.

Translated audio/file admission follows the [final-adapter input contract](adapters/registry.md#untranslated-input-media); native raw passthrough remains separate.
## Request-local target compatibility

Expand Down
3 changes: 3 additions & 0 deletions structure/subagents.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ final catalog merge fences pending retained rows, including delete/re-add recove
Raw management rows remain visible as pending/OFF. Config listener bindings are
excluded from inventory identity because live and persisted bindings may differ.

Supported routed-provider reasoning snapshots are bootstrapped by `src/codex/sync.ts` through
`src/providers/reasoning-metadata.ts`, rather than by a model request.

Codex `spawn_agent` advertises only the highest-priority first five picker-visible catalog rows.
Use at most five configured `subagentModels` ids; they may contain bare catalog ids, routed
`provider/model` ids, or exact account-qualified `<selector>/<native-openai-model>` ids. The
Expand Down
4 changes: 4 additions & 0 deletions structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ surface is listed here so a maintainer can find the owner without grepping:
| Alibaba regions | `src/providers/alibaba-region-backup.ts`, `src/providers/alibaba-region-migration.ts`, `src/providers/alibaba-region-startup.ts` | Region migration backs up before rewriting and is idempotent across restarts. |
| Discovery and quota | `src/providers/model-discovery.ts`, `src/providers/quota.ts`, `src/providers/registry.ts` | Discovery rejects a response over 4 MiB or past 2,000 raw rows before caching it. Provider-scoped hints fill capabilities omitted by live rosters; OpenCode Go's `deepseek-v4.1-flash` keeps its 1,048,576-token context window. The fixed-key Opper preset uses the shared OpenAI Chat adapter at `https://api.opper.ai/v3/compat`, discovers models through its conventional authenticated `/models` path, preserves an older same-named custom destination, and falls back to bare pool ids while passing vendor-prefixed ids through unchanged. Codex quota DTOs suppress retired Spark evidence under the [OpenAI scope contract](../providers/openai-tiers.md#public-provider-contract), retaining ordinary custom windows. |

`src/codex/sync.ts` bootstraps models.dev effort ladders through
`src/providers/reasoning-metadata.ts` for supported destinations before catalog gathering, never
from the routed request transport.

The registry's first-party `deepseek-flash` row declares native `text` and `image` input, so image
requests bypass the vision sidecar by default; explicit `noVisionModels` or text-only declarations
remain authoritative. First-party `deepseek-chat`, `deepseek-reasoner`, and `deepseek-v4-flash`
Expand Down
36 changes: 36 additions & 0 deletions tests/codex-integration/codex-sync-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,42 @@ describe("GUI/CLI Codex sync backend", () => {
expect(errors).toEqual([]);
});

test("bootstraps reasoning metadata for gated providers before catalog gathering", async () => {
const calls: string[] = [];
const zenConfig = {
...config,
providers: {
zen: {
...config.providers.fixture,
baseUrl: "https://opencode.ai/zen/go/v1",
},
},
} as OcxConfig;

await syncModelsToCodex(12345, zenConfig, null, {
admitCodexWrite: admittedSync,
refreshReasoningMetadata: async () => {
calls.push("reasoning");
return { ok: true as const, reason: "refreshed", providers: 1, models: 1 };
},
refreshCodexModelCatalog: async () => {
calls.push("catalog");
return {
added: 1,
path: "/tmp/opencodex-catalog.json",
catalogExists: true,
catalogWritten: true,
cacheSynced: true,
comboOmissions: [],
};
},
injectCodexConfig: async () => ({ success: true, message: "injected" }),
currentExternalCodexModelProvider: () => null,
});

expect(calls).toEqual(["reasoning", "catalog"]);
});

test("refuses during injection preflight before catalog or cache mutation", async () => {
let refreshCalls = 0;
let injectCalls = 0;
Expand Down
Loading