Skip to content

fix(agents): honor models.mode='replace' by skipping implicit provider discovery#68978

Closed
hclsys wants to merge 1 commit into
openclaw:mainfrom
hclsys:fix/models-mode-replace-excludes-implicit-68965
Closed

fix(agents): honor models.mode='replace' by skipping implicit provider discovery#68978
hclsys wants to merge 1 commit into
openclaw:mainfrom
hclsys:fix/models-mode-replace-excludes-implicit-68965

Conversation

@hclsys

@hclsys hclsys commented Apr 19, 2026

Copy link
Copy Markdown

Problem

`resolveProvidersForModelsJsonWithDeps` in `src/agents/models-config.plan.ts` always invoked `resolveImplicitProviders` and merged the result with the explicit providers from config. That meant `models.mode: "replace"` — documented as 'only use explicitly configured providers' — still pulled in implicit providers contributed by bundled plugin `catalog` hooks.

Reporter @Tony-ooo (#68965) traced this to the `codex` bundled plugin, which defines a `catalog` hook and ends up in `models.json` even when `mode: "replace"` is set. Other bundled plugins like `anthropic` aren't affected because they don't define a `catalog` hook, which is why the symptom looks codex-specific.

Fix

In `resolveProvidersForModelsJsonWithDeps`, check `cfg.models?.mode` before the implicit fetch. When it's `"replace"`, skip `resolveImplicitProviders` entirely and return a `mergeProviders` result with `implicit: {}`. Default (undefined) and `"merge"` paths are unchanged.

This is a single edit at the one source of truth. Downstream `mergeProviders` keeps the same explicit-wins semantics.

Testing

Added `src/agents/models-config.mode-replace.test.ts` with three cases:

  • default (no mode) → implicit fetch runs, both providers emitted
  • `mode: "merge"` → implicit fetch runs, both providers emitted
  • `mode: "replace"` → implicit fetch is never called, only explicit providers emitted

`tsc --noEmit` and `oxlint` clean on the touched files.

Fixes #68965

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S labels Apr 19, 2026
@greptile-apps

greptile-apps Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds a guard in resolveProvidersForModelsJsonWithDeps to short-circuit the implicit-provider fetch when models.mode is \"replace\", fixing the bug reported in #68965 where bundled plugin catalog hooks (e.g. codex) leaked providers into models.json even under replace mode. The change is minimal and surgically placed at the single source of truth, with three focused test cases covering the default, merge, and replace paths.

Confidence Score: 5/5

Safe to merge; the fix is correct and well-tested with only minor P2 style nits in the test file.

The production code change is a two-line early-return that directly implements the documented contract. The existing mergeProviders semantics are preserved for the default and merge paths. All remaining findings are P2 style suggestions in the test file.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/models-config.mode-replace.test.ts
Line: 4

Comment:
**Indirect import path for `ProviderConfig`**

The test imports `ProviderConfig` from `"./models-config.providers.secrets.js"`, which re-exports it from `models-config.providers.secret-helpers.js`. The production code (`models-config.plan.ts`) sources it from `"./models-config.providers.js"`. If these two re-export chains ever diverge, the test fixture types could silently drift from the actual interface under test. Prefer the same import path used in the production file.

```suggestion
import type { ProviderConfig } from "./models-config.providers.js";
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/agents/models-config.mode-replace.test.ts
Line: 89

Comment:
**Inconsistent key-ordering assertion**

The other two test cases use `.toSorted()` to make key-order comparisons stable, but this assertion relies on implicit insertion order. For a single-entry object it's harmless today, but it's inconsistent with the pattern established by the other cases in this `describe` block.

```suggestion
    expect(Object.keys(providers).toSorted()).toEqual(["bailian"]);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(agents): honor models.mode='replace'..." | Re-trigger Greptile

import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
import { resolveProvidersForModelsJsonWithDeps } from "./models-config.plan.js";
import type { ProviderConfig } from "./models-config.providers.secrets.js";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Indirect import path for ProviderConfig

The test imports ProviderConfig from "./models-config.providers.secrets.js", which re-exports it from models-config.providers.secret-helpers.js. The production code (models-config.plan.ts) sources it from "./models-config.providers.js". If these two re-export chains ever diverge, the test fixture types could silently drift from the actual interface under test. Prefer the same import path used in the production file.

Suggested change
import type { ProviderConfig } from "./models-config.providers.secrets.js";
import type { ProviderConfig } from "./models-config.providers.js";
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.mode-replace.test.ts
Line: 4

Comment:
**Indirect import path for `ProviderConfig`**

The test imports `ProviderConfig` from `"./models-config.providers.secrets.js"`, which re-exports it from `models-config.providers.secret-helpers.js`. The production code (`models-config.plan.ts`) sources it from `"./models-config.providers.js"`. If these two re-export chains ever diverge, the test fixture types could silently drift from the actual interface under test. Prefer the same import path used in the production file.

```suggestion
import type { ProviderConfig } from "./models-config.providers.js";
```

How can I resolve this? If you propose a fix, please make it concise.

// The fix: implicit fetch is skipped entirely, and the result contains
// ONLY the explicit providers from config.
expect(resolveImplicit).not.toHaveBeenCalled();
expect(Object.keys(providers)).toEqual(["bailian"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Inconsistent key-ordering assertion

The other two test cases use .toSorted() to make key-order comparisons stable, but this assertion relies on implicit insertion order. For a single-entry object it's harmless today, but it's inconsistent with the pattern established by the other cases in this describe block.

Suggested change
expect(Object.keys(providers)).toEqual(["bailian"]);
expect(Object.keys(providers).toSorted()).toEqual(["bailian"]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/models-config.mode-replace.test.ts
Line: 89

Comment:
**Inconsistent key-ordering assertion**

The other two test cases use `.toSorted()` to make key-order comparisons stable, but this assertion relies on implicit insertion order. For a single-entry object it's harmless today, but it's inconsistent with the pattern established by the other cases in this `describe` block.

```suggestion
    expect(Object.keys(providers).toSorted()).toEqual(["bailian"]);
```

How can I resolve this? If you propose a fix, please make it concise.

…r discovery

resolveProvidersForModelsJsonWithDeps() always fetched implicit providers
from bundled plugin catalog hooks, then merged them with the explicit
providers from config. This meant `models.mode: "replace"` — documented
as excluding implicit providers — still emitted implicit entries (e.g.
`codex` from the bundled codex plugin) into the generated models.json.

When mode is 'replace', skip the implicit fetch entirely and return only
the explicit providers. 'merge' and default unset behavior are unchanged.

Fixes openclaw#68965
@hclsys

This comment was marked as low quality.

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

Labels

agents Agent runtime and tooling size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: models.mode: replace does not exclude implicit providers from bundled plugins

1 participant