Skip to content

fix(catalog): custom model rows inherit provider reasoning metadata - #965

Closed
Yuxin-Qiao wants to merge 1 commit into
lidge-jun:devfrom
Yuxin-Qiao:codex/260804-issue962-custom-model-reasoning
Closed

fix(catalog): custom model rows inherit provider reasoning metadata#965
Yuxin-Qiao wants to merge 1 commit into
lidge-jun:devfrom
Yuxin-Qiao:codex/260804-issue962-custom-model-reasoning

Conversation

@Yuxin-Qiao

@Yuxin-Qiao Yuxin-Qiao commented Aug 3, 2026

Copy link
Copy Markdown

Summary

Fixes #962. When a provider model also has a dashboard-created customModels row, gatherRoutedModels() replaced the provider-derived catalog row without retaining its provider capability metadata. The custom row reached buildCatalogEntries() without reasoningEfforts, so applyReasoningLevels() synthesized the generic low..ultra ladder plus a medium default. For a local Ollama model configured with noReasoningModels and an empty modelReasoningEfforts, Codex then rejected an ephemeral spawn_agent call during client-side preflight: "Reasoning effort 'none' is not supported".

Fix

In src/codex/catalog/provider-fetch.ts, a custom row now inherits provider capability metadata from the provider-derived row it replaces (same routed provider/model slug):

  • reasoning ladder (reasoningEfforts) and default effort (defaultReasoningEffort)
  • parallel tool calls, verbosity, reasoning-summary support, and normalized capabilities
  • context window / max input tokens / input modalities when the custom row does not declare them explicitly

Explicit custom fields (display name, context window, input modalities) still win verbatim, preserving the existing user-override contract from the vision-sidecar work (#349/#344). A noReasoningModels custom model now yields a catalog entry with no reasoning levels and no default reasoning level.

Reproduction

  1. Configure ollama/qwen-coder-3b with noReasoningModels + empty modelReasoningEfforts and a matching dashboard custom row.
  2. ocx sync previously advertised supported_reasoning_levels: low..ultra and default_reasoning_level: medium; after the fix the entry has an empty reasoning ladder and no default level.

Tests

  • New regression test in tests/codex-catalog.test.ts covering the issue end-to-end through gatherRoutedModels + buildCatalogEntries: empty reasoning ladder, no default level, explicit custom fields preserved.
  • bun run typecheck passes.
  • Catalog suites (codex-catalog, catalog-vision-sidecar-modalities, vertex-catalog, catalog-input-modality-enum, codex-catalog-sync-hardening): 157 tests, 0 fail.
  • Full bun run test: no catalog failures; the only failures reproduce on clean dev in this local environment (management provider-validation HTTP tests and a crash-guard timing test), and upstream CI is green at the same dev base commit.

Summary by CodeRabbit

  • Bug Fixes
    • Custom model configurations now retain provider-derived capabilities when replacing an existing catalog entry.
    • Explicit custom settings remain unchanged while missing metadata, reasoning details, tool support, and modality information are filled in appropriately.
    • Vision-related model handling is now preserved for merged catalog entries.

@github-actions github-actions Bot added the bug Something isn't working label Aug 3, 2026
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Custom catalog rows now inherit missing metadata from matching provider-derived rows before deduplication. Explicit custom values remain unchanged. A regression test verifies reasoning, tool capability, modalities, context, and generated catalog output.

Changes

Catalog metadata handling

Layer / File(s) Summary
Provider metadata merge
src/codex/catalog/provider-fetch.ts
At lines 785-831, provider-derived rows are indexed by routed slug. Matching custom rows inherit missing limits, modalities, reasoning settings, tool and verbosity support, summaries, and capabilities. Vision-sidecar support applies to the merged row.
Catalog regression coverage
tests/codex-catalog.test.ts
At lines 851-906, the test verifies preserved custom fields, inherited empty reasoning metadata and parallel-tool support, and the absence of synthesized reasoning levels and defaults.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested reviewers: lidge-jun, wibias, ingwannu

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: custom catalog rows inherit provider reasoning metadata.
Linked Issues check ✅ Passed The implementation and regression test satisfy issue #962 by preserving custom fields and inheriting provider capabilities, including empty reasoning metadata.
Out of Scope Changes check ✅ Passed The changes are limited to catalog metadata merging and its regression test, which directly support issue #962.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/codex-catalog.test.ts`:
- Around line 851-900: Add a second provider/custom model fixture in the
existing test to cover inherited non-empty reasoning metadata, configuring
provider reasoning efforts and modelDefaultReasoningEfforts. Assert the replaced
custom CatalogModel preserves defaultReasoningEffort and that
buildCatalogEntries emits the corresponding default_reasoning_level, while
retaining the current empty-reasoning assertions for issue `#962`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c718f5e9-51bd-46bf-8ce6-39f79dbc91cd

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb1c0c and c5565d0.

📒 Files selected for processing (2)
  • src/codex/catalog/provider-fetch.ts
  • tests/codex-catalog.test.ts

Comment on lines +851 to +900
test("a custom row inherits provider reasoning metadata from the provider-derived row it replaces (#962)", async () => {
clearModelCache("ollama");
const originalFetch = globalThis.fetch;
globalThis.fetch = (() => { throw new Error("fetch should not be called"); }) as typeof fetch;
try {
const models = await gatherRoutedModels({
port: 10100,
defaultProvider: "ollama",
providers: {
ollama: {
baseUrl: "http://localhost:11434/v1",
adapter: "openai-chat",
authMode: "key",
liveModels: false,
models: ["qwen-coder-3b"],
selectedModels: ["qwen-coder-3b"],
noReasoningModels: ["qwen-coder-3b"],
modelReasoningEfforts: { "qwen-coder-3b": [] },
},
},
customModels: [
{
id: "cm-962",
provider: "ollama",
modelId: "qwen-coder-3b",
displayName: "Qwen Coder 3B (local)",
contextWindow: 32768,
inputModalities: ["text"],
addedAt: "2026-01-01T00:00:00.000Z",
},
],
});

// Explicit custom fields stay verbatim; provider capability metadata is inherited from the
// replaced provider-derived row (noReasoningModels -> empty reasoning ladder, openai-chat
// adapter -> parallel tool calls).
const custom = models.find(m => m.provider === "ollama" && m.id === "qwen-coder-3b");
expect(custom?.displayName).toBe("Qwen Coder 3B (local)");
expect(custom?.contextWindow).toBe(32768);
expect(custom?.inputModalities).toEqual(["text"]);
expect(custom?.reasoningEfforts).toEqual([]);
expect(custom?.parallelToolCalls).toBe(true);

const entries = buildCatalogEntries(nativeTemplate(), [], models);
const row = entries.find(e => e.slug === "ollama/qwen-coder-3b");
expect(row?.display_name).toBe("Qwen Coder 3B (local)");
// The catalog must expose no reasoning levels and no default reasoning level for this model;
// the generic low..ultra ladder and the medium default must not be synthesized.
expect(row?.supported_reasoning_levels).toEqual([]);
expect(row?.default_reasoning_level).toBeUndefined();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for an inherited non-empty default reasoning effort.

This fixture gives the provider-derived row an empty reasoningEfforts list and no defaultReasoningEffort. Therefore, the test cannot detect a regression in src/codex/catalog/provider-fetch.ts line 813.

Add a second provider/custom pair with configured reasoning efforts and modelDefaultReasoningEfforts. Assert both CatalogModel.defaultReasoningEffort and the generated default_reasoning_level. Keep the current no-reasoning case for issue #962.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/codex-catalog.test.ts` around lines 851 - 900, Add a second
provider/custom model fixture in the existing test to cover inherited non-empty
reasoning metadata, configuring provider reasoning efforts and
modelDefaultReasoningEfforts. Assert the replaced custom CatalogModel preserves
defaultReasoningEffort and that buildCatalogEntries emits the corresponding
default_reasoning_level, while retaining the current empty-reasoning assertions
for issue `#962`.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c5565d055c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const enrichedByName = new Map(activeProviders);
// Provider-derived rows keyed by their Codex-facing slug: a custom override replaces the row
// with the same slug below, so that row's provider capability metadata is the inheritance source.
const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match inherited metadata by exact model identity

When a provider exposes colliding native IDs such as a-b and a/b, both produce the same routed slug, and the preceding sort causes this map to retain the slash row. A custom override for the plain a-b model therefore inherits the other model's reasoning ladder, context, and capabilities, even though resolveSlugAliasCollisions() deliberately catalogs the plain-hyphen model as the winner. Index the provider-derived rows by exact provider/native ID first, and use a routed-slug fallback only when that slug is unambiguous.

Useful? React with 👍 / 👎.

Comment on lines +806 to +818
const replaced = replacedByRoutedSlug.get(routedSlug(cm.provider, cm.modelId));
const merged: CatalogModel = replaced ? {
...base,
...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined ? { defaultReasoningEffort: replaced.defaultReasoningEffort } : {}),
...(base.parallelToolCalls === undefined && replaced.parallelToolCalls !== undefined ? { parallelToolCalls: replaced.parallelToolCalls } : {}),
...(base.supportsVerbosity === undefined && replaced.supportsVerbosity !== undefined ? { supportsVerbosity: replaced.supportsVerbosity } : {}),
...(base.supportsReasoningSummaries === undefined && replaced.supportsReasoningSummaries !== undefined ? { supportsReasoningSummaries: replaced.supportsReasoningSummaries } : {}),
...(base.capabilities === undefined && replaced.capabilities !== undefined ? { capabilities: replaced.capabilities } : {}),
} : base;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive capabilities when no provider row exists

When a custom model is intentionally added outside the provider's discovered/static list—a supported case already exercised by the renamed-model custom-model test—this lookup returns undefined, so the row still ignores noReasoningModels, modelReasoningEfforts, defaults, and adapter capabilities and buildCatalogEntries() synthesizes the generic reasoning ladder. This leaves the reported spawn-agent failure unfixed for the normal use case where customModels supplies an otherwise unlisted model; derive the missing capability fields through the provider hint flow even when there is no replaced row, while retaining explicit custom context/modalities.

AGENTS.md reference: src/AGENTS.md:L18-L18

Useful? React with 👍 / 👎.

@Wibias
Wibias marked this pull request as draft August 3, 2026 21:00
@Wibias

Wibias commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

Please put your Pull-Request on Ready for Review, once you are finished.

@lidge-jun

Copy link
Copy Markdown
Owner

Carried into #973 (stack 6/6), with authorship preserved (cherry-pick -x, patch-id verified identical).

#963 landed the same fix for #962 about an hour apart. Yours won on evidence: it inherits from the provider row actually being replaced, so it also retains live /models metadata such as normalized capabilities, whereas #963 recomputes config hints for every custom row including unmatched ones. Your ablation also fails exactly one test — the #962 regression — which is what a focused fix looks like. #963 is closed with that reasoning on record.

Your regression reproduces the reporter's real configuration, including both noReasoningModels and the empty modelReasoningEfforts, which is why it was the one that convinced me.

Verified on the stack: bun x tsc --noEmit exit 0, full suite 7740 pass / 8 skip / 0 fail across 508 files. Closing since it now lives in #973.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants