openclaw - ✅(Solved) Fix feat: dynamic catalog refresh from configured provider /v1/models [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#74481Fetched 2026-04-30 06:23:39
View on GitHub
Comments
1
Participants
2
Timeline
2
Reactions
2
Author
Timeline (top)
commented ×1cross-referenced ×1

Fix Action

Fixed

PR fix notes

PR #74488: feat(openai): dynamic model catalog discovery from upstream /v1/models

Description (problem / solution / changelog)

First slice of the design proposed in #74481 — adds the discovery primitive without yet wiring it into catalog resolution. Keeping infrastructure and integration in separate PRs so the cache strategy / response mapping can be reviewed independently of the bigger plumbing question (where in the model-resolution path discovery should be invoked, merge-vs-replace semantics, Control UI refresh trigger).

What this PR does

Adds extensions/openai/discovery.ts exposing discoverOpenAIModels({baseUrl, apiKey, fetchFn?, now?}):

  • Fetches ${baseUrl}/models with Authorization: Bearer ${apiKey}.
  • Maps the response (strict superset of OpenAI's /v1/models envelope — gateways like AceTeam ship extra fields like context_window, max_output_tokens, modalities, cost_per_million_tokens and we honor them when present).
  • Falls back to per-id heuristics (default 128k context, 16k max output, text-only, zero cost; reasoning=true for gpt-5.x / o[1,3,4] families).
  • Caches per (baseUrl, last-8-of-apiKey) for 1h. Rotating the key invalidates.
  • Fail-soft: returns last-known-good on transient failure, returns [] when there's no cache and the fetch fails. Never throws at the caller.

Mirrors existing precedent

This is the same shape as extensions/amazon-bedrock-mantle/discovery.ts:265 — same TTL, same cache-on-success / cache-on-failure semantics, same ModelDefinitionConfig[] return type. Makes it familiar to anyone who has read that file.

What this PR does NOT do (intentionally)

  • Doesn't wire discovery into catalog resolution. The static catalog in openai-provider.ts is unchanged. To exercise the discovered list you'd call discoverOpenAIModels() from your own integration today; a follow-up PR will plumb it into the dynamic-model resolution path.
  • Doesn't touch the ProviderPlugin interface. The proposed resolveDynamicCatalog hook in #74481 needs maintainer agreement on shape before it lands. If reviewers prefer that path, this discovery function is what the OpenAI plugin's resolveDynamicCatalog would call.
  • Doesn't address Anthropic. Anthropic's /v1/models shape and auth header conventions differ; that's a separate PR mirroring this one.

Test plan

  • pnpm test extensions/openai/discovery.test.ts → 12/12 pass.
  • Coverage: missing inputs, header/URL shape, trailing-slash normalization, AceTeam superset fields, modality/cost/reasoning mapping, sort, cache hit, TTL expiry, transient-failure fallback, network-error fallback, no-cache failure path.
  • Integration: hit a real /v1/models endpoint and confirm the mapped output is sensible.

Discussion

Design questions (from #74481) still open:

  1. Hook name/shape for plumbing into catalog resolution.
  2. Merge-vs-replace semantics — does dynamic discovery need its own setting, or does models.modelCatalogMode cover it?
  3. Cache scoping for multi-tenant gateways where the same baseUrl returns different lists per org.

Happy to revise this PR or split further once those land.

Forks affected

aceteam-ai/safeclaw (OpenClaw + AEP safety-proxy fork) needs this for its model picker to reflect what its proxy actually accepts. Will consume the same discovery function.

Changed files

  • extensions/openai/api.ts (modified, +1/-0)
  • extensions/openai/discovery.test.ts (added, +235/-0)
  • extensions/openai/discovery.ts (added, +215/-0)

Code Example

// New optional hook on ProviderPlugin
interface ProviderPlugin {
  // ...existing fields...
  resolveDynamicCatalog?(ctx: ProviderResolveCatalogContext): Promise<ProviderRuntimeModel[]>;
}

interface ProviderResolveCatalogContext {
  providerConfig: ProviderConfig;  // baseUrl, apiKey, request headers, etc.
  fetchFn?: typeof fetch;
  now?: () => number;
}
RAW_BUFFERClick to expand / collapse

Problem

OpenClaw's Control UI model picker is built from the bundled catalog in extensions/openai/openai-provider.ts — hardcoded entries like gpt-5.5, gpt-5.4-pro, etc., each with a baked-in baseUrl, cost, contextWindow, maxTokens. This means:

  1. Wrong list when behind a proxy. Users running OpenClaw against a LiteLLM/vLLM/local-gateway/AceTeam-style upstream see picker options that the upstream doesn't actually accept — and pick one, only to get a 404 from the upstream when they hit send.
  2. Stale cost/context. When OpenAI changes pricing or releases a new model, the bundled catalog has to ship a release before users see it. Even then, gateways with custom pricing (volume discounts, per-tenant caps) can't reflect that anywhere.
  3. No source of truth. The OPENAI_BASE_URL env-var fallback (#74427, merged) and the whisper precedent (#55597, merged) accept that the upstream is the source of truth for where to send the call. The catalog should follow the same principle for what the call can be.

Proposal

Add an opt-in dynamic-catalog refresh path that mirrors the existing Bedrock-Mantle discovery pattern (extensions/amazon-bedrock-mantle/discovery.ts). At a high level:

// New optional hook on ProviderPlugin
interface ProviderPlugin {
  // ...existing fields...
  resolveDynamicCatalog?(ctx: ProviderResolveCatalogContext): Promise<ProviderRuntimeModel[]>;
}

interface ProviderResolveCatalogContext {
  providerConfig: ProviderConfig;  // baseUrl, apiKey, request headers, etc.
  fetchFn?: typeof fetch;
  now?: () => number;
}

For OpenAI provider:

  1. Implement resolveDynamicCatalog to GET ${providerConfig.baseUrl}/models with the configured apiKey as Authorization: Bearer.
  2. Parse the response — strict superset of OpenAI's {object: "list", data: [{id, object, created, owned_by}]}. Optional fields like context_window, max_output_tokens, modalities, cost_per_million_tokens (already shipped by some gateways) get mapped into ProviderRuntimeModel.contextWindow / maxTokens / cost when present, falling back to the bundled defaults otherwise.
  3. Cache per providerConfig.baseUrl + apiKey with a 1h TTL (matches Mantle).
  4. Surface the discovered models alongside the static catalog. With models.modelCatalogMode = "merge" (default), discovered + bundled deduped by id; with "replace", discovered wins entirely.

For the Control UI:

  • On models.providers.{openai,anthropic,…} config change OR explicit refresh, call the dynamic resolver and rebuild the picker.
  • Existing static catalog stays the fallback when the provider has no baseUrl + apiKey configured (i.e. default "I'll use the bundled OpenAI defaults" mode).

Why this matches existing patterns

  • #55597 (whisper) — env-var-based base URL honoring. Merged.
  • #74427 (openai env fallback) — config-vs-env precedence for OPENAI_BASE_URL. Merged.
  • extensions/amazon-bedrock-mantle/discovery.ts — exact same pattern (fetch /v1/models, cache by key, normalize entries) for a different provider family.

This is a generalization of (3) to the OpenAI/Anthropic providers, gated behind an opt-in hook so providers that don't want dynamic discovery (raw Ollama running on localhost, SDK-only providers like Vertex) keep their existing static-catalog behavior.

Scope

  • New hook on ProviderPlugin interface (additive, backwards-compatible)
  • Implementation for extensions/openai/openai-provider.ts
  • Implementation for extensions/anthropic/ (separate PR, mirror)
  • Control UI hook to call resolveDynamicCatalog on config change / startup
  • Cache invalidation on apiKey / baseUrl change

Each piece is a small focused PR; the hook itself can land first.

Asks for maintainers

  1. Is the proposed hook name/shape acceptable, or do you prefer a different seam (e.g. extending the existing ProviderRuntimeModel template lookup)?
  2. Is merge vs replace the right axis, or should dynamic-catalog have its own setting?
  3. Anything about the cache key (per-baseUrl-and-key) that needs to factor in tenant context (e.g. for multi-org gateways where the same baseUrl returns different lists per org)?

Happy to write the implementation once we agree on the shape — would rather not write code that gets rewritten in review.

Forks affected

aceteam-ai/safeclaw (OpenClaw + AEP safety proxy) needs this for its model picker to reflect what its proxy actually accepts. Would consume the same hook.

extent analysis

TL;DR

Implement a dynamic catalog refresh path for the OpenAI provider to fetch models from the upstream source, ensuring the model picker reflects the actual available models.

Guidance

  • Introduce a new optional hook resolveDynamicCatalog on the ProviderPlugin interface to enable dynamic catalog refresh.
  • Implement the resolveDynamicCatalog hook for the OpenAI provider to fetch models from the upstream source using the configured baseUrl and apiKey.
  • Cache the discovered models with a 1h TTL to reduce the load on the upstream source.
  • Surface the discovered models alongside the static catalog, allowing for either merging or replacing the static catalog.

Example

interface ProviderPlugin {
  // ...
  resolveDynamicCatalog?(ctx: ProviderResolveCatalogContext): Promise<ProviderRuntimeModel[]>;
}

// Implementation for OpenAI provider
const openaiProvider: ProviderPlugin = {
  // ...
  resolveDynamicCatalog: async (ctx: ProviderResolveCatalogContext) => {
    const response = await ctx.fetchFn(`${ctx.providerConfig.baseUrl}/models`, {
      headers: {
        Authorization: `Bearer ${ctx.providerConfig.apiKey}`,
      },
    });
    const models = await response.json();
    // Parse and map models to ProviderRuntimeModel
    return models.data.map((model) => ({
      id: model.id,
      contextWindow: model.context_window,
      maxTokens: model.max_output_tokens,
      cost: model.cost_per_million_tokens,
    }));
  },
};

Notes

The proposed solution assumes that the upstream source provides a list of available models in a compatible format. Additional error handling and logging may be necessary to ensure a smooth user experience.

Recommendation

Apply the proposed workaround by implementing the resolveDynamicCatalog hook for the OpenAI provider, as it provides a more accurate and up-to-date model picker.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix feat: dynamic catalog refresh from configured provider /v1/models [1 pull requests, 1 comments, 2 participants]