Skip to content

fix(models): keep bundled provider catalog when configured base URL is blank (#91270)#91292

Merged
vincentkoc merged 2 commits into
openclaw:mainfrom
yetval:fix/gemini-blank-baseurl-catalog
Jun 10, 2026
Merged

fix(models): keep bundled provider catalog when configured base URL is blank (#91270)#91292
vincentkoc merged 2 commits into
openclaw:mainfrom
yetval:fix/gemini-blank-baseurl-catalog

Conversation

@yetval

@yetval yetval commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #91270. On the embedded runtime, Google Gemini models stop resolving with model_not_found:

Unknown model: google/gemini-2.5-flash. Found agents.defaults.models["google/gemini-2.5-flash"],
but no matching models.providers["google"].models[] entry.

even though the bundled Google catalog and forward-compat resolver know gemini-2.5-flash and gemini-flash-latest. The reporter hit this with models.mode: "merge" and a config edited by a plugin; their workaround was to add explicit models.providers.google.models[] rows.

Root cause

When a models.providers.<id> entry exists with a blank baseUrl: "" (as written by a plugin or a partial hand edit), that empty string flows into models.json planning:

  • It is spread into the provider-discovery config (buildPluginCatalogConfig) and the catalog merge, where it overrides the bundled transport base URL, so the discovered Google catalog ends up with baseUrl: "".
  • isWritableProviderConfig then treats the provider as non-writable (baseUrl?.trim() && apiKey is false), so filterWritableProviders drops the whole Google provider from the generated registry, including its bundled catalog and forward-compat templates.
  • With no Google rows in the registry, the direct lookup (gemini-2.5-flash) and the forward-compat clone (gemini-flash-latest, which clones a gemini-2.5-flash template) both fail, so resolution falls through to the model_not_found registration hint.

A blank baseUrl should mean "use the provider default", not "erase the bundled catalog".

Fix

resolveProvidersForModelsJsonWithDeps strips blank provider baseUrl values from the explicit config before they reach discovery and the merge, so the bundled catalog base URL survives and the provider stays writable. The change is provider-agnostic.

Real behavior proof

Behavior addressed: Embedded runtime fails to resolve google/gemini-2.5-flash / google/gemini-flash-latest with model_not_found when a models.providers.google entry exists with a blank baseUrl and no/partial models[] (merge mode, plugin-edited config).

Real environment tested: Local checkout at origin/main 5c53918, Node 22.19, GEMINI_API_KEY set; drove the production ensureOpenClawModelsJson then resolveModelAsync("google", id, ...) decision path for the reporter-shaped config across three variants (C1 no explicit google provider; C2 explicit google with model rows; C3 explicit google { api, baseUrl: "" } with no model rows).

Exact steps or command run after this patch: generated models.json then resolved google/gemini-2.5-flash and google/gemini-flash-latest for each of C1, C2, C3 through the real resolver decision path.

Evidence after fix: copied live terminal output from the real resolver decision path.

Before the fix (C3 = blank-baseUrl plugin-written google entry):

[C3 explicit google EMPTY models] google/gemini-2.5-flash    => ERR: Unknown model: google/gemini-2.5-flash. Found agents.defaults.models["google/gemini-2.5-flash"], but no matching models.providers["google"].models[] entry.
[C3 explicit google EMPTY models] google/gemini-flash-latest => ERR: Unknown model: google/gemini-flash-latest. Found agents.defaults.models["google/gemini-flash-latest"], but no matching models.providers["google"].models[] entry.

After the fix:

[C1 NO explicit google]          google/gemini-2.5-flash    => OK api=google-generative-ai
[C1 NO explicit google]          google/gemini-flash-latest => OK api=google-generative-ai
[C2 explicit google WITH models] google/gemini-2.5-flash    => OK api=google-generative-ai
[C2 explicit google WITH models] google/gemini-flash-latest => OK api=google-generative-ai
[C3 explicit google EMPTY models] google/gemini-2.5-flash    => OK api=google-generative-ai
[C3 explicit google EMPTY models] google/gemini-flash-latest => OK api=google-generative-ai

Observed result after fix: all three configs resolve both Gemini ids on the embedded runtime; C1 and C2 were already resolving and stayed green (no regression).

What was not tested: a live request against the Google Gemini API (resolution only, no network call); non-Google providers were not exercised live, though the fix is provider-agnostic.

Verification

Local (Node 22.19.0): pnpm tsgo:core exit 0; oxfmt --check src/agents/models-config.plan.ts clean; type-aware oxlint on the changed file clean.

Related (not duplicates)

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: XS triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 8, 2026
@clawsweeper

clawsweeper Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs maintainer review before merge. Reviewed June 7, 2026, 9:36 PM ET / 01:36 UTC.

Summary
The PR strips blank models.providers.*.baseUrl values before model-catalog discovery and merge so bundled provider defaults survive generated models.json planning.

PR surface: Source +20. Total +20 across 1 file.

Reproducibility: yes. The source path on current main shows blank explicit provider baseUrl can flow through discovery/merge and then fail the writable-provider filter; the PR body also supplies before/after terminal output for the production resolver path.

Review metrics: 1 noteworthy metric.

  • Provider config semantics: 1 changed. The meaning of a blank models.providers.*.baseUrl during generated catalog planning changes, which matters for upgrades and plugin-edited configs.

Merge readiness
Overall: 🐚 platinum hermit
Proof: 🦞 diamond lobster
Patch quality: 🐚 platinum hermit
Result: ready for maintainer review.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P2] Add a focused regression test for blank models.providers.google.baseUrl preserving bundled catalog rows during models.json planning.
  • Have a maintainer explicitly confirm that blank provider baseUrl should be treated as omitted/default during upgrades.

Risk before merge

  • [P1] Compatibility/auth-provider risk: existing blank models.providers.<id>.baseUrl values would change from a persisted clobbering value into omitted/default overlay behavior during generated catalog planning.
  • [P1] The branch does not add committed regression coverage for the blank-baseUrl generated-registry path, so future provider-catalog refactors could reintroduce the issue despite the supplied resolver proof.

Maintainer options:

  1. Confirm Blank-As-Default With Coverage (recommended)
    Accept blank provider baseUrl as omitted/default behavior and add a focused regression test before merge so the upgrade semantics stay pinned.
  2. Accept The Resolver Proof
    Merge as-is if maintainers consider the PR body’s before/after resolver output sufficient for this small compatibility repair.
  3. Pause For Provider Policy
    Pause if blank baseUrl is intended to remain an explicit invalid or suppressing value, because then the fix likely belongs in doctor/config migration policy instead.

Next step before merge

  • [P2] Maintainer review is needed because the remaining blocker is accepting compatibility-sensitive blank baseUrl semantics, not a purely mechanical code repair.

Security
Cleared: The diff only normalizes blank provider base URLs inside model planning and does not add dependencies, scripts, permissions, or new secret-handling surfaces.

Review details

Best possible solution:

Land the narrow planner fix after maintainers confirm blank provider baseUrl should mean default/omitted, ideally with a focused regression test for generated Google catalog preservation.

Do we have a high-confidence way to reproduce the issue?

Yes. The source path on current main shows blank explicit provider baseUrl can flow through discovery/merge and then fail the writable-provider filter; the PR body also supplies before/after terminal output for the production resolver path.

Is this the best way to solve the issue?

Yes, with maintainer confirmation. Stripping blank baseUrl before discovery/merge is a narrow provider-agnostic fix, but the compatibility-sensitive semantics should be explicitly accepted and regression-covered.

AGENTS.md: found and applied where relevant.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 3ffb3609a141.

Label changes

Label changes:

  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after terminal output from the real ensureOpenClawModelsJson plus resolveModelAsync decision path for the reporter-shaped config; no live Google API call was needed for this resolver-only change.

Label justifications:

  • P2: This is a normal-priority regression fix for model/provider resolution with limited but real user impact.
  • merge-risk: 🚨 compatibility: The PR changes how existing blank provider baseUrl config values are interpreted during upgrade/runtime planning.
  • merge-risk: 🚨 auth-provider: The affected path controls provider catalog generation, API/base URL selection, and model resolution for Google Gemini.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (live_output): The PR body includes before/after terminal output from the real ensureOpenClawModelsJson plus resolveModelAsync decision path for the reporter-shaped config; no live Google API call was needed for this resolver-only change.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body includes before/after terminal output from the real ensureOpenClawModelsJson plus resolveModelAsync decision path for the reporter-shaped config; no live Google API call was needed for this resolver-only change.
Evidence reviewed

PR surface:

Source +20. Total +20 across 1 file.

View PR surface stats
Area Files Added Removed Net
Source 1 22 2 +20
Tests 0 0 0 0
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 1 22 2 +20

What I checked:

  • Current main passes explicit providers unchanged: resolveProvidersForModelsJsonWithDeps currently reads cfg.models?.providers directly, passes that object to implicit provider discovery, and merges it back over discovered providers. (src/agents/models-config.plan.ts:110, 3ffb3609a141)
  • Catalog config overlays explicit provider fields: buildPluginCatalogConfig overlays ctx.explicitProviders onto config.models.providers, so explicit fields such as blank baseUrl participate in discovery context and later merge decisions. (src/agents/models-config.providers.implicit.ts:476, 3ffb3609a141)
  • Writable filter drops provider rows without usable transport: filterWritableProviders keeps providers with model rows only when baseUrl?.trim() and apiKey are both truthy, matching the reported unknown-model failure mode after blank baseUrl survives planning. (src/agents/models-config.plan.ts:160, 3ffb3609a141)
  • Google catalog has the needed default rows: The bundled Google static catalog defines gemini-2.5-flash and the default Google Generative AI base URL that the PR is trying to preserve. (extensions/google/provider-catalog.ts:67, 3ffb3609a141)
  • PR patch is focused and still open: Live unauthenticated GitHub API data shows the PR is open, cleanly mergeable, and changes only src/agents/models-config.plan.ts at head 5a5c8bf8d6a4627772975ba8a02ca57be8074e6f. (src/agents/models-config.plan.ts:110, 5a5c8bf8d6a4)
  • History pass: Local blame attributes the current planner block to the recent checked-out history, and GitHub path history shows recent model-config/provider-discovery work by steipete, vincentkoc, brokemac79, and yetval on the central files. (src/agents/models-config.plan.ts:95, 3dc6ac3802)

Likely related people:

  • steipete: Recent GitHub path history shows repeated work on models-config.plan.ts, models-config.providers.implicit.ts, and model catalog/provider discovery refactors that shape this planning boundary. (role: recent area contributor; confidence: high; commits: 51b5f75b92f7, bb46b79d3c14, 3e8fd4944fe1; files: src/agents/models-config.plan.ts, src/agents/models-config.providers.implicit.ts, src/agents/models-config.merge.ts)
  • vincentkoc: Local blame for the current planner block points to Vincent Koc in the shallow checkout, and recent history includes provider/runtime fanout work in the affected model-resolution area. (role: line-history and adjacent runtime contributor; confidence: medium; commits: 3dc6ac3802, d07ba5f2656d; files: src/agents/models-config.plan.ts, src/agents/embedded-agent-runner/model.ts)
  • brokemac79: Recent path history shows provider-discovery scoping work that threads discovery options through models-config planning, adjacent to the discovery boundary this PR changes. (role: adjacent provider discovery contributor; confidence: medium; commits: 20c7a98fb8b3; files: src/agents/models-config.plan.ts, src/agents/models-config.providers.implicit.ts)
  • yetval: Besides this PR, current-main history shows prior model-resolution/static-catalog work by the same contributor in embedded-agent-runner/model.ts, so they are relevant to this resolver path beyond being the PR author. (role: recent adjacent model-resolution contributor; confidence: medium; commits: be2909608157; files: src/agents/embedded-agent-runner/model.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added proof: sufficient ClawSweeper judged the real behavior proof convincing. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. P2 Normal backlog priority with limited blast radius. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. labels Jun 8, 2026
@openclaw-barnacle openclaw-barnacle Bot added proof: supplied External PR includes structured after-fix real behavior proof. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. proof: sufficient ClawSweeper judged the real behavior proof convincing. labels Jun 8, 2026
@clawsweeper clawsweeper Bot added the proof: sufficient ClawSweeper judged the real behavior proof convincing. label Jun 8, 2026
@vincentkoc vincentkoc self-assigned this Jun 10, 2026
yetval and others added 2 commits June 11, 2026 08:37
…s blank

A models.providers.<id> entry with a blank baseUrl ("") erased the
bundled provider catalog from the generated model registry. The empty
base URL flowed into provider discovery and the catalog merge, where it
overrode the bundled transport URL; the resulting provider then failed
isWritableProviderConfig and was dropped from models.json entirely.

For Google this meant gemini-2.5-flash and gemini-flash-latest stopped
resolving on the embedded runtime with model_not_found, even though the
bundled Google catalog and forward-compat resolver know those ids. This
hit users in merge mode whose config was partially written by a plugin.

Strip blank provider base URLs before discovery and merge so a blank
value means "use the provider default" instead of clobbering it.

Fixes openclaw#91270
@vincentkoc
vincentkoc force-pushed the fix/gemini-blank-baseurl-catalog branch from 5a5c8bf to c71a867 Compare June 10, 2026 23:40
@vincentkoc

Copy link
Copy Markdown
Member

Maintainer verification complete.

  • Focused proof: node scripts/run-vitest.mjs src/agents/models-config.applies-config-env-vars.test.ts — 14 passed.
  • Fresh Codex autoreview: clean; no accepted/actionable findings.
  • Best-fix judgment: sanitize only blank endpoint overrides before discovery/merge, preserving all other explicit provider fields.

@vincentkoc
vincentkoc merged commit 98e239d into openclaw:main Jun 10, 2026
19 of 21 checks passed
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request Jun 11, 2026
…s blank (openclaw#91270) (openclaw#91292)

* fix(models): keep bundled provider catalog when configured base URL is blank

A models.providers.<id> entry with a blank baseUrl ("") erased the
bundled provider catalog from the generated model registry. The empty
base URL flowed into provider discovery and the catalog merge, where it
overrode the bundled transport URL; the resulting provider then failed
isWritableProviderConfig and was dropped from models.json entirely.

For Google this meant gemini-2.5-flash and gemini-flash-latest stopped
resolving on the embedded runtime with model_not_found, even though the
bundled Google catalog and forward-compat resolver know those ids. This
hit users in merge mode whose config was partially written by a plugin.

Strip blank provider base URLs before discovery and merge so a blank
value means "use the provider default" instead of clobbering it.

Fixes openclaw#91270

* test(models): cover blank provider base url

---------

Co-authored-by: Vincent Koc <[email protected]>
badgerbees pushed a commit to badgerbees/openclaw that referenced this pull request Jul 8, 2026
…s blank (openclaw#91270) (openclaw#91292)

* fix(models): keep bundled provider catalog when configured base URL is blank

A models.providers.<id> entry with a blank baseUrl ("") erased the
bundled provider catalog from the generated model registry. The empty
base URL flowed into provider discovery and the catalog merge, where it
overrode the bundled transport URL; the resulting provider then failed
isWritableProviderConfig and was dropped from models.json entirely.

For Google this meant gemini-2.5-flash and gemini-flash-latest stopped
resolving on the embedded runtime with model_not_found, even though the
bundled Google catalog and forward-compat resolver know those ids. This
hit users in merge mode whose config was partially written by a plugin.

Strip blank provider base URLs before discovery and merge so a blank
value means "use the provider default" instead of clobbering it.

Fixes openclaw#91270

* test(models): cover blank provider base url

---------

Co-authored-by: Vincent Koc <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling merge-risk: 🚨 auth-provider 🚨 May break OAuth, tokens, provider routing, model choice, or credentials. merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. P2 Normal backlog priority with limited blast radius. proof: sufficient ClawSweeper judged the real behavior proof convincing. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. size: S status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Gemini can't resolve on embedded runtime

2 participants