Skip to content

perf: speed up provider-filtered models list#70632

Merged
shakkernerd merged 11 commits intomainfrom
fix/models-list-auth-cost
Apr 24, 2026
Merged

perf: speed up provider-filtered models list#70632
shakkernerd merged 11 commits intomainfrom
fix/models-list-auth-cost

Conversation

@shakkernerd
Copy link
Copy Markdown
Member

@shakkernerd shakkernerd commented Apr 23, 2026

This PR makes models list --all --provider <id> avoid the broad model registry path when the filtered provider has a safe lightweight static catalog.

Before this change, provider-filtered listing still paid much of the same cost as broad models list --all: registry loading, auth/model discovery, and provider catalog work could run even when the command only needed static rows for one provider. That made simple provider checks unnecessarily slow.

What changed:

  • Adds a generalized static provider-catalog fast path for models list --all --provider <id>.
  • Adds lightweight provider discovery entries for bundled providers whose static catalog is safe to list without live/account discovery.
  • Keeps dynamic/live providers on the registry path when a static catalog would be incomplete or would break auth semantics.
  • Keeps third-party/workspace providers on the registry path unless core can safely use bundled static metadata.
  • Prevents static-only discovery entries from replacing normal implicit provider discovery.
  • Bounds unscoped implicit discovery fallback so it does not full-load every provider plugin when lightweight entries are incomplete.
  • Updates model-list tests for the new fast-path behavior.

Provider behavior:

  • Fast static path: moonshot, deepseek, byteplus, volcengine, tencent-tokenhub, codex.
  • Registry/live path preserved: kilocode, chutes, vercel-ai-gateway, arcee, openrouter.

Measured with direct built CLI (node openclaw.mjs ... --json):

Before, representative provider-filtered commands were still on the broad slow path:

  • moonshot: ~20s class in debug timing before provider-static fast path work.
  • openrouter: ~20s+ class before earlier scoped registry work.

After this PR:

  • moonshot: 1.13s, 5 rows
  • deepseek: 1.08s, 2 rows
  • byteplus: 1.09s, 3 rows
  • volcengine: 1.08s, 5 rows
  • tencent-tokenhub: 1.07s, 1 row
  • codex: 1.09s, 3 rows

Preserved registry/live path timings:

  • kilocode: 10.08s, 1 row
  • chutes: 8.68s, 47 rows
  • vercel-ai-gateway: 13.08s, 156 rows
  • arcee: 5.16s, 0 rows in my local env
  • openrouter: 14.16s, 254 rows

Broad baseline remains slow and is not solved by this PR:

  • models list --all: 30.03s, 937 rows

Why some providers stay on the registry path:

  • kilocode, chutes, and vercel-ai-gateway have live/account-specific discovery or fallback catalogs where static rows are not complete enough.
  • arcee supports OpenRouter-backed auth under the same arcee/* refs, so static listing would incorrectly mark rows unavailable.
  • openrouter still depends on broader registry/catalog loading.

This is intentionally an incremental safe path. The follow-up architecture work should move provider display catalogs, auth availability metadata, and live/account discovery into separate declared lanes so all provider-list commands can become fast without lying about availability or missing account-specific models.

Verification:

  • pnpm test src/plugins/provider-discovery.runtime.test.ts src/commands/models/list.provider-catalog.test.ts src/commands/models/list.list-command.forward-compat.test.ts src/commands/models.list.e2e.test.ts
  • Direct built CLI timing checks for provider-filtered models list --all --provider <id> --json on the providers listed above.

@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Apr 23, 2026

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Path traversal allows arbitrary module execution via providerDiscoveryEntry import
1. 🟠 Path traversal allows arbitrary module execution via providerDiscoveryEntry import
Property Value
Severity High
CWE CWE-22
Location src/plugins/manifest-registry.ts:354-358

Description

providerDiscoveryEntry from a plugin manifest is converted into an absolute path and later loaded/executed via the Jiti-based createPluginSourceLoader() without validating that the resolved file stays inside the plugin root.

This creates a path traversal / arbitrary local module import primitive:

  • Input: providerDiscoveryEntry in a plugin's openclaw.plugin.json (user/workspace/global plugin manifest)
  • Transformation: path.resolve(pluginRoot, providerDiscoveryEntry) in the manifest registry
  • Sink: jiti(modulePath) in createPluginSourceLoader() (executes the loaded module)

An attacker who can place/modify a plugin manifest (notably a workspace plugin when includeUntrustedWorkspacePlugins is enabled by some flows, or any plugin installed from an untrusted source) can set providerDiscoveryEntry to values like ../../../../tmp/evil.js and have that file executed when provider discovery runs (e.g., during listing/catalog operations), bypassing the intended boundary of “relative to the plugin root”.

Vulnerable code:

providerDiscoverySource: params.manifest.providerDiscoveryEntry
  ? resolvePluginSourcePath(
      path.resolve(params.candidate.rootDir, params.manifest.providerDiscoveryEntry),
    )
  : undefined,

Recommendation

Enforce that providerDiscoveryEntry stays within the plugin root after resolving and realpath canonicalization.

  • Reject absolute paths.
  • Resolve against the plugin root and ensure the final path is inside candidate.rootDir.
  • Consider only allowing a constrained subdirectory (e.g., ./dist/ or ./src/) and only .js/.ts extensions.

Example fix:

import { isPathInside, safeRealpathSync } from "./path-safety.js";

function resolveDiscoveryEntry(candidateRoot: string, entry: string): string {
  if (path.isAbsolute(entry)) {
    throw new Error("providerDiscoveryEntry must be relative");
  }
  const resolved = path.resolve(candidateRoot, entry);
  const realRoot = safeRealpathSync(candidateRoot);
  const realResolved = safeRealpathSync(resolved);
  if (!isPathInside(realRoot, realResolved)) {
    throw new Error("providerDiscoveryEntry must stay within plugin root");
  }
  return resolvePluginSourcePath(realResolved);
}

Then use resolveDiscoveryEntry(params.candidate.rootDir, params.manifest.providerDiscoveryEntry) when populating providerDiscoverySource.


Analyzed PR: #70632 at commit a0a4176

Last updated on: 2026-04-24T04:48:16Z

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR adds a static-catalog fast path for models list --all --provider <id>, bypassing registry loading, auth discovery, and catalog work for a set of bundled providers (moonshot, deepseek, byteplus, volcengine, tencent-tokenhub, codex) whose model lists are fully representable via lightweight static entries. Dynamic/live providers (kilocode, chutes, vercel-ai-gateway, arcee, openrouter) remain on the existing registry path. The implementation is well-structured — new providerDiscoveryEntry fields in plugin manifests, a discoveryEntriesOnly flag, and hasProviderStaticCatalogForFilter gating the fast path — and the measured speed improvements (~1.1 s vs ~20 s for affected providers) are substantial.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/behaviour notes that don't block correctness.

No P0 or P1 defects found. The fast-path gating logic in hasProviderStaticCatalogForFilter, the discoveryEntriesOnly flag, and the registry-skip condition are all consistent. The two P2 comments (removed apiKey guard in shouldListConfiguredProviderModel and a dead seenKeys reassignment in the fallback branch) are minor and do not affect the primary use-cases targeted by this PR.

src/commands/models/list.rows.ts — the apiKey guard removal is a silent behaviour change worth a second look before merging if strict listing semantics for partially-configured providers matter.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/commands/models/list.rows.ts
Line: 111

Comment:
**`apiKey` guard silently dropped for configured-provider rows**

Previously `shouldListConfiguredProviderModel` required `providerConfig.apiKey !== undefined` in addition to an `api` setting, so a provider entry that lacked `apiKey` was hidden from the list. That guard is now gone. Any configured provider block that declares an `api` (or per-model `api`) but has no `apiKey` — e.g. a partially-filled stub — will now appear as a visible row in `models list`, which may surprise users who never intended to surface it.

If the intent is to allow non-key auth mechanisms (e.g. header-based or env-var-based auth without an explicit `apiKey` field), consider tightening the guard to also accept other credential forms rather than removing the requirement entirely.

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/commands/models/list.row-sources.ts
Line: 47-53

Comment:
**Dead `seenKeys` reassignment in fast-path fallback**

When `catalogRows === 0` on the static fast path, `seenKeys` is reassigned to the return value of `appendDiscoveredRows`, but then the function immediately `return`s and `seenKeys` is never read again. The reassignment is dead code. Since `params.modelRegistry` is also `undefined` on this path (registry loading was skipped), `appendDiscoveredRows` always receives an empty array and adds nothing, so the fallback is effectively a no-op.

```suggestion
    if (catalogRows === 0) {
      appendDiscoveredRows({
        rows: params.rows,
        models: params.modelRegistry?.getAll() ?? [],
        context: params.context,
      });
    }
```

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

Reviews (3): Last reviewed commit: "fix: bound unscoped provider discovery f..." | Re-trigger Greptile

Copy link
Copy Markdown

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

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: 2da7e94046

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/models/list.list-command.ts Outdated
Comment thread src/plugins/providers.ts Outdated
Copy link
Copy Markdown

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

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: 2b1dacedab

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/commands/models/list.rows.ts Outdated
Comment thread src/plugins/provider-runtime.ts Outdated
@shakkernerd shakkernerd marked this pull request as draft April 23, 2026 22:57
@shakkernerd shakkernerd force-pushed the fix/models-list-auth-cost branch from 2b1dace to daa2a05 Compare April 24, 2026 04:35
@shakkernerd shakkernerd changed the title fix: make models list read-only and provider scoped perf: speed up provider-filtered models list Apr 24, 2026
@shakkernerd shakkernerd marked this pull request as ready for review April 24, 2026 04:36
@shakkernerd shakkernerd self-assigned this Apr 24, 2026
Copy link
Copy Markdown

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

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: daa2a05f8e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

});

if (params.modelRegistry) {
if (params.modelRegistry && !params.context.filter.provider) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep catalog-supplement rows when provider filter is set

Gating appendCatalogSupplementRows on !params.context.filter.provider removes model-catalog supplementation from all models list --all --provider <id> flows. That path is what pulls in fallback/synthetic entries via loadModelCatalog + resolveModelWithRegistry (in src/commands/models/list.rows.ts), so providers whose filtered results depend on catalog supplementation (for example when registry.getAll() is empty and static catalogs do not cover the provider) now silently lose rows and can print incomplete or empty filtered output.

Useful? React with 👍 / 👎.

@shakkernerd shakkernerd force-pushed the fix/models-list-auth-cost branch from daa2a05 to a0a4176 Compare April 24, 2026 04:46
@shakkernerd shakkernerd merged commit 9d445f4 into main Apr 24, 2026
8 checks passed
@shakkernerd shakkernerd deleted the fix/models-list-auth-cost branch April 24, 2026 04:46
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation gateway Gateway runtime labels Apr 24, 2026
@shakkernerd
Copy link
Copy Markdown
Member Author

Landed via temp rebase onto current main.

  • Gate: pnpm test src/plugins/provider-discovery.runtime.test.ts src/commands/models/list.provider-catalog.test.ts src/commands/models/list.list-command.forward-compat.test.ts src/commands/models.list.e2e.test.ts
  • Gate: pnpm check
  • Gate: pnpm build
  • Gate: pnpm check:changed
  • Smoke: node openclaw.mjs models list --all --provider moonshot --json
  • Smoke: node openclaw.mjs models list --all --provider deepseek --json
  • Smoke: node openclaw.mjs models list --all --provider byteplus --json
  • Smoke: node openclaw.mjs models list --all --provider volcengine --json
  • Smoke: node openclaw.mjs models list --all --provider tencent-tokenhub --json
  • Smoke: node openclaw.mjs models list --all --provider codex --json
  • Smoke: node openclaw.mjs models list --all --provider openrouter --json
  • Rebased PR head before merge: a0a4176
  • Merge commit: 9d445f4

Thanks @shakkernerd!

Copy link
Copy Markdown

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

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: a0a41769f5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +82 to +83
const scopedPluginIds = pluginIds.filter((pluginId) => bundledPluginIdSet.has(pluginId));
if (scopedPluginIds.length === 0) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Require exclusive bundled ownership before static fast path

When a provider id is owned by both a workspace plugin and a bundled plugin, resolveProviderCatalogPluginIdsForFilter can return both ids, but this logic keeps only bundled ids and still enables the static fast path. That makes models list --all --provider <id> skip registry loading and silently ignore the workspace plugin’s runtime catalog/availability behavior, showing bundled static rows instead. The fast-path gate should only pass when all owning plugin ids are bundled (or when ownership is unambiguous).

Useful? React with 👍 / 👎.

Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant