refactor: use sqlite model catalog at runtime

This commit is contained in:
Peter Steinberger
2026-05-09 22:58:40 +01:00
parent ab01130dcc
commit 29fe040cda
23 changed files with 74 additions and 67 deletions

View File

@@ -86,8 +86,8 @@ removes the marker from the credential store.
## Probe target resolution
- Probe targets can come from auth profiles, environment credentials, or
`models.json`.
- Probe targets can come from auth profiles, environment credentials, or the
stored model catalog.
- If a provider has credentials but OpenClaw cannot resolve a probeable model
candidate for it, `models status --probe` reports `status: no_model` with
`reasonCode: no_model`.

View File

@@ -60,7 +60,7 @@ openclaw agent --agent ops --message "Run locally" --local
- `--json` keeps stdout reserved for the JSON response. Gateway, plugin, and embedded-fallback diagnostics are routed to stderr so scripts can parse stdout directly.
- Embedded fallback JSON includes `meta.transport: "embedded"` and `meta.fallbackFrom: "gateway"` so scripts can distinguish fallback runs from Gateway runs.
- If the Gateway accepts an agent run but the CLI times out waiting for the final reply, embedded fallback uses a fresh explicit `gateway-fallback-*` session/run id and reports `meta.fallbackReason: "gateway_timeout"` plus the fallback session fields. This avoids racing the Gateway-owned transcript lock or silently replacing the original routed conversation session.
- When this command triggers `models.json` regeneration, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names, `secretref-env:ENV_VAR_NAME`, or `secretref-managed`), not resolved secret plaintext.
- When this command materializes the stored model catalog, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names, `secretref-env:ENV_VAR_NAME`, or `secretref-managed`), not resolved secret plaintext.
- Marker writes are source-authoritative: OpenClaw persists markers from the active source config snapshot, not from resolved runtime secret values.
## Related

View File

@@ -39,7 +39,7 @@ Probes are real requests (may consume tokens and trigger rate limits).
Use `--agent <id>` to inspect a configured agent's model/auth state. When omitted,
the command uses `OPENCLAW_AGENT_DIR`/`PI_CODING_AGENT_DIR` if set, otherwise the
configured default agent.
Probe rows can come from auth profiles, env credentials, or `models.json`.
Probe rows can come from auth profiles, env credentials, or the stored model catalog.
For Codex OAuth troubleshooting, `openclaw models status`,
`openclaw models auth list --provider openai-codex`, and
`openclaw config get agents.defaults.model --json` are the quickest way to
@@ -50,8 +50,8 @@ Notes:
- `models set <model-or-alias>` accepts `provider/model` or an alias.
- `models list` is read-only: it reads config, auth profiles, existing catalog
state, and provider-owned catalog rows, but it does not rewrite
`models.json`.
state, and provider-owned catalog rows, but it does not rewrite the stored
model catalog.
- The `Auth` column is provider-level and read-only. It is computed from local
auth profile metadata, env markers, configured provider keys, local-provider
markers, AWS Bedrock env/profile markers, and plugin synthetic-auth metadata;

View File

@@ -72,7 +72,7 @@ Scan OpenClaw state for:
- plaintext secret storage
- unresolved refs
- precedence drift (`auth-profiles.json` credentials shadowing `openclaw.json` refs)
- generated `agents/*/agent/models.json` residues (provider `apiKey` values and sensitive provider headers)
- stored model catalog residues (provider `apiKey` values and sensitive provider headers)
- legacy residues (legacy auth store entries, OAuth reminders)
Header residue note:

View File

@@ -342,7 +342,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details.
## Providers via `models.providers` (custom/base URL)
Use `models.providers` (or `models.json`) to add **custom** providers or OpenAI/Anthropic-compatible proxies.
Use `models.providers` to add **custom** providers or OpenAI/Anthropic-compatible proxies. Older `models.json` files are imported by `openclaw doctor --fix`.
Many of the bundled provider plugins below already publish a default catalog. Use explicit `models.providers.<id>` entries only when you want to override the default base URL, headers, or model list.

View File

@@ -108,7 +108,7 @@ openclaw models auth paste-token --provider openrouter
}
```
OpenClaw expects the canonical `version` + `profiles` shape at runtime. If an older install still has a flat file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to rewrite it as an `openrouter:default` API-key profile; doctor keeps a `.legacy-flat.*.bak` copy beside the original. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or `models.json`, not in `auth-profiles.json`.
OpenClaw expects the canonical `version` + `profiles` shape at runtime. If an older install still has a flat file such as `{ "openrouter": { "apiKey": "..." } }`, run `openclaw doctor --fix` to rewrite it as an `openrouter:default` API-key profile; doctor keeps a `.legacy-flat.*.bak` copy beside the original. Endpoint details such as `baseUrl`, `api`, model ids, headers, and timeouts belong under `models.providers.<id>` in `openclaw.json` or the stored model catalog, not in `auth-profiles.json`.
External auth routes such as Bedrock `auth: "aws-sdk"` are also not credentials. If you want a named Bedrock route, put `auth.profiles.<id>.mode: "aws-sdk"` in `openclaw.json`; do not write `type: "aws-sdk"` into `auth-profiles.json`. `openclaw doctor --fix` moves legacy AWS SDK markers from the credential store into config metadata.
@@ -132,7 +132,7 @@ openclaw models status --probe
Notes:
- Probe rows can come from auth profiles, env credentials, or `models.json`.
- Probe rows can come from auth profiles, env credentials, or the stored model catalog.
- If explicit `auth.order.<provider>` omits a stored profile, probe reports
`excluded_by_auth_order` for that profile instead of trying it.
- If auth exists but OpenClaw cannot resolve a probeable model candidate for

View File

@@ -387,7 +387,7 @@ Experimental built-in tool flags. Default off unless a strict-agentic GPT-5 auto
## Custom providers and base URLs
OpenClaw uses the built-in model catalog. Add custom providers via `models.providers` in config or `~/.openclaw/agents/<agentId>/agent/models.json`.
OpenClaw uses the built-in model catalog. Add custom providers via `models.providers` in config; doctor imports old `~/.openclaw/agents/<agentId>/agent/models.json` files into the stored model catalog.
```json5
{
@@ -421,14 +421,14 @@ OpenClaw uses the built-in model catalog. Add custom providers via `models.provi
- Use `authHeader: true` + `headers` for custom auth needs.
- Override agent config root with `OPENCLAW_AGENT_DIR` (or `PI_CODING_AGENT_DIR`, a legacy environment variable alias).
- Merge precedence for matching provider IDs:
- Non-empty agent `models.json` `baseUrl` values win.
- Non-empty stored agent catalog `baseUrl` values win.
- Non-empty agent `apiKey` values win only when that provider is not SecretRef-managed in current config/auth-profile context.
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
- SecretRef-managed provider header values are refreshed from source markers (`secretref-env:ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs).
- Empty or missing agent `apiKey`/`baseUrl` fall back to `models.providers` in config.
- Matching model `contextWindow`/`maxTokens` use the higher value between explicit config and implicit catalog values.
- Matching model `contextTokens` preserves an explicit runtime cap when present; use it to limit effective context without changing native model metadata.
- Use `models.mode: "replace"` when you want config to fully rewrite `models.json`.
- Use `models.mode: "replace"` when you want config to fully rewrite the stored model catalog.
- Marker persistence is source-authoritative: markers are written from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
</Accordion>

View File

@@ -469,8 +469,8 @@ Default operator flow:
<Accordion title="secrets audit">
Findings include:
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`, and generated `agents/*/agent/models.json`)
- plaintext sensitive provider header residues in generated `models.json` entries
- plaintext values at rest (`openclaw.json`, `auth-profiles.json`, `.env`, and the stored model catalog)
- plaintext sensitive provider header residues in stored model catalog entries
- unresolved refs
- precedence shadowing (`auth-profiles.json` taking priority over `openclaw.json` refs)
- legacy residues (`auth.json`, OAuth reminders)

View File

@@ -96,7 +96,7 @@ src/agents/
├── model-auth.ts # Auth profile resolution
├── auth-profiles.ts # Profile store, cooldown, failover
├── model-selection.ts # Default model resolution
├── models-config.ts # models.json generation
├── models-config.ts # SQLite model catalog materialization
├── model-catalog.ts # Model catalog cache
├── context-window-guard.ts # Context window validation
├── failover-error.ts # FailoverError class

View File

@@ -256,7 +256,7 @@ listed here.
| # | Hook | What it does | When to use |
| --- | --------------------------------- | -------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | `catalog` | Publish provider config into `models.providers` during `models.json` generation | Provider owns a catalog or base URL defaults |
| 1 | `catalog` | Publish provider config into `models.providers` during model catalog materialization | Provider owns a catalog or base URL defaults |
| 2 | `applyConfigDefaults` | Apply provider-owned global config defaults during config materialization | Defaults depend on auth mode, env, or provider model-family semantics |
| -- | _(built-in model lookup)_ | OpenClaw tries the normal registry/catalog path first | _(not a plugin hook)_ |
| 3 | `normalizeModelId` | Normalize legacy or preview model-id aliases before lookup | Provider owns alias cleanup before canonical model resolution |

View File

@@ -126,7 +126,7 @@ You can append `:fastest` or `:cheapest` to any model id. Set your default order
You can add these as separate entries in `models.providers.huggingface.models` or set `model.primary` with the suffix. You can also set your default provider order in [Inference Provider settings](https://hf.co/settings/inference-providers) (no suffix = use that order).
- **Config merge:** Existing entries in `models.providers.huggingface.models` (e.g. in `models.json`) are kept when config is merged. So any custom `name`, `alias`, or model options you set there are preserved.
- **Config merge:** Existing entries in `models.providers.huggingface.models` and the stored model catalog are kept when config is merged. So any custom `name`, `alias`, or model options you set there are preserved.
</Accordion>

View File

@@ -441,7 +441,7 @@ See [MiniMax Search](/tools/minimax-search) for full web search configuration an
- Alternate chat model: `MiniMax-M2.7-highspeed`
- Onboarding and direct API-key setup write text-only model definitions for both M2.7 variants
- Image understanding uses the plugin-owned `MiniMax-VL-01` media provider
- Update pricing values in `models.json` if you need exact cost tracking
- Update pricing values in `models.providers` if you need exact cost tracking
- Use `openclaw models list` to confirm the current provider id, then switch with `openclaw models set minimax/MiniMax-M2.7` or `openclaw models set minimax-portal/MiniMax-M2.7`
<Tip>

View File

@@ -190,7 +190,7 @@ When you set `OLLAMA_API_KEY` (or an auth profile) and **do not** define `models
| Token limits | Sets `maxTokens` to the default Ollama max-token cap used by OpenClaw |
| Costs | Sets all costs to `0` |
This avoids manual model entries while keeping the catalog aligned with the local Ollama instance. You can use a full ref such as `ollama/<pulled-model>:latest` in local `infer model run`; OpenClaw resolves that installed model from Ollama's live catalog without requiring a hand-written `models.json` entry.
This avoids manual model entries while keeping the catalog aligned with the local Ollama instance. You can use a full ref such as `ollama/<pulled-model>:latest` in local `infer model run`; OpenClaw resolves that installed model from Ollama's live catalog without requiring a hand-written model catalog entry.
For signed-in Ollama hosts, some `:cloud` models may be usable through `/api/chat`
and `/api/show` before they appear in `/api/tags`. When you explicitly select a

View File

@@ -122,7 +122,7 @@ Notes:
- Auth-profile refs are included in runtime resolution and audit coverage.
- In `openclaw.json`, SecretRefs must use structured objects such as `{"source":"env","provider":"default","id":"DISCORD_BOT_TOKEN"}`. Legacy `secretref-env:<ENV_VAR>` marker strings are rejected on SecretRef credential paths; run `openclaw doctor --fix` to migrate valid markers.
- OAuth policy guard: `auth.profiles.<id>.mode = "oauth"` cannot be combined with SecretRef inputs for that profile. Startup/reload and auth-profile resolution fail fast when this policy is violated.
- For SecretRef-managed model providers, generated `agents/*/agent/models.json` entries persist non-secret markers (not resolved secret values) for `apiKey`/header surfaces.
- For SecretRef-managed model providers, stored model catalog entries persist non-secret markers (not resolved secret values) for `apiKey`/header surfaces.
- Marker persistence is source-authoritative: OpenClaw writes markers from the active source config snapshot (pre-resolution), not from resolved runtime secret values.
- For web search:
- In explicit provider mode (`tools.web.search.provider` set), only the selected provider key is active.

View File

@@ -868,7 +868,7 @@ export const FIELD_HELP: Record<string, string> = {
models:
"Model catalog root for provider definitions, merge/replace behavior, and optional Bedrock discovery integration. Keep provider definitions explicit and validated before relying on production failover paths.",
"models.mode":
'Controls provider catalog behavior: "merge" keeps built-ins and overlays your custom providers, while "replace" uses only your configured providers. In "merge", matching provider IDs preserve non-empty agent models.json baseUrl values, while apiKey values are preserved only when the provider is not SecretRef-managed in current config/auth-profile context; SecretRef-managed providers refresh apiKey from current source markers, and matching model contextWindow/maxTokens use the higher value between explicit and implicit entries.',
'Controls provider catalog behavior: "merge" keeps built-ins and overlays your custom providers, while "replace" uses only your configured providers. In "merge", matching provider IDs preserve non-empty stored agent catalog baseUrl values, while apiKey values are preserved only when the provider is not SecretRef-managed in current config/auth-profile context; SecretRef-managed providers refresh apiKey from current source markers, and matching model contextWindow/maxTokens use the higher value between explicit and implicit entries.',
"models.providers":
"Provider map keyed by provider ID containing connection/auth settings and concrete model definitions. Use stable provider keys so references from agents and tooling remain portable across environments.",
"models.pricing":

View File

@@ -31,7 +31,11 @@ import { isLiveProfileKeyModeEnabled, isLiveTestEnabled } from "../agents/live-t
import { getApiKeyForModel, resolveEnvApiKey } from "../agents/model-auth.js";
import { normalizeProviderId } from "../agents/model-selection.js";
import { shouldSuppressBuiltInModel } from "../agents/model-suppression.js";
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
import {
readStoredModelsConfigRaw,
writeStoredModelsConfigRaw,
} from "../agents/models-config-store.js";
import { ensureOpenClawModelCatalog } from "../agents/models-config.js";
import {
clampThinkingLevel,
type Api,
@@ -1557,10 +1561,13 @@ async function loadProviderScopedConfiguredModels(params: {
agentDir: string;
providerList: readonly string[];
}): Promise<Array<Model<Api>>> {
const modelsPath = path.join(params.agentDir, "models.json");
let parsed: { providers?: Record<string, ModelProviderConfig> };
try {
parsed = JSON.parse(await fs.readFile(modelsPath, "utf8")) as {
const stored = readStoredModelsConfigRaw(params.agentDir);
if (!stored) {
return [];
}
parsed = JSON.parse(stored.raw) as {
providers?: Record<string, ModelProviderConfig>;
};
} catch {
@@ -1942,9 +1949,11 @@ async function runGatewayModelSuite(params: GatewayModelSuiteParams) {
const liveProviders = nextCfg.models?.providers;
if (liveProviders && Object.keys(liveProviders).length > 0) {
const modelsPath = path.join(tempAgentDir, "models.json");
await fs.mkdir(tempAgentDir, { recursive: true });
await fs.writeFile(modelsPath, `${JSON.stringify({ providers: liveProviders }, null, 2)}\n`);
writeStoredModelsConfigRaw(
tempAgentDir,
`${JSON.stringify({ providers: liveProviders }, null, 2)}\n`,
);
}
// Keep the broad live Docker suite on the impl entrypoint. The lazy public
@@ -2639,14 +2648,14 @@ describeLive("gateway live (dev agent, profile keys)", () => {
"[all-models] load config",
);
const workspaceDir = resolveAgentWorkspaceDir(cfg, DEFAULT_AGENT_ID);
logProgress("[all-models] preparing models.json");
logProgress("[all-models] preparing model catalog");
await withGatewayLiveSetupTimeout(
ensureOpenClawModelsJson(cfg, undefined, {
ensureOpenClawModelCatalog(cfg, undefined, {
workspaceDir,
...(providerList ? { providerDiscoveryProviderIds: providerList } : {}),
providerDiscoveryEntriesOnly: true,
}),
"[all-models] prepare models.json",
"[all-models] prepare model catalog",
);
const agentDir = resolveDefaultAgentDir(cfg);
@@ -2865,7 +2874,7 @@ describeLive("gateway live (dev agent, profile keys)", () => {
process.env.OPENCLAW_GATEWAY_TOKEN = token;
const cfg = getRuntimeConfig();
await ensureOpenClawModelsJson(cfg);
await ensureOpenClawModelCatalog(cfg);
const agentDir = resolveDefaultAgentDir(cfg);
const authStorage = discoverAuthStorage(agentDir);

View File

@@ -45,7 +45,7 @@ const hoisted = vi.hoisted(() => {
model: "gpt-5.4",
}));
const resolveEmbeddedAgentRuntime = vi.fn(() => "pi");
const ensureOpenClawModelsJson = vi.fn(async () => undefined);
const ensureOpenClawModelCatalog = vi.fn(async () => undefined);
return {
startPluginServices,
startGmailWatcherWithLogs,
@@ -71,7 +71,7 @@ const hoisted = vi.hoisted(() => {
isCliProvider,
resolveConfiguredModelRef,
resolveEmbeddedAgentRuntime,
ensureOpenClawModelsJson,
ensureOpenClawModelCatalog,
};
});
@@ -170,7 +170,7 @@ vi.mock("../agents/pi-embedded-runner/runtime.js", () => ({
}));
vi.mock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson: hoisted.ensureOpenClawModelsJson,
ensureOpenClawModelCatalog: hoisted.ensureOpenClawModelCatalog,
}));
vi.mock("./server-tailscale.js", () => ({
@@ -218,8 +218,8 @@ describe("startGatewayPostAttachRuntime", () => {
hoisted.resolveConfiguredModelRef.mockClear();
hoisted.resolveEmbeddedAgentRuntime.mockReset();
hoisted.resolveEmbeddedAgentRuntime.mockReturnValue("pi");
hoisted.ensureOpenClawModelsJson.mockReset();
hoisted.ensureOpenClawModelsJson.mockResolvedValue(undefined);
hoisted.ensureOpenClawModelCatalog.mockReset();
hoisted.ensureOpenClawModelCatalog.mockResolvedValue(undefined);
});
afterEach(() => {
@@ -551,7 +551,7 @@ describe("startGatewayPostAttachRuntime", () => {
}
});
it("prewarms models.json in the configured default agent dir", async () => {
it("prewarms the model catalog in the configured default agent dir", async () => {
const cfg = {
agents: {
defaults: { model: "openai/gpt-5.4" },
@@ -568,7 +568,7 @@ describe("startGatewayPostAttachRuntime", () => {
});
expect(hoisted.resolveDefaultAgentDir).toHaveBeenCalledWith(cfg);
expect(hoisted.ensureOpenClawModelsJson).toHaveBeenCalledWith(
expect(hoisted.ensureOpenClawModelCatalog).toHaveBeenCalledWith(
cfg,
"/tmp/openclaw-state/agents/ops/agent",
expect.objectContaining({

View File

@@ -251,12 +251,12 @@ async function prewarmConfiguredPrimaryModel(params: {
return;
}
// Keep startup prewarm metadata-only; resolving models can import provider runtimes and block readiness.
const { ensureOpenClawModelsJson } = await import("../agents/models-config.js");
const { ensureOpenClawModelCatalog } = await import("../agents/models-config.js");
const agentDir = resolveDefaultAgentDir(params.cfg);
const workspaceDir =
params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
try {
await ensureOpenClawModelsJson(params.cfg, agentDir, {
await ensureOpenClawModelCatalog(params.cfg, agentDir, {
workspaceDir,
providerDiscoveryProviderIds: [provider],
providerDiscoveryTimeoutMs: STARTUP_PROVIDER_DISCOVERY_TIMEOUT_MS,

View File

@@ -1,7 +1,7 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/config.js";
const ensureOpenClawModelsJsonMock = vi.fn<
const ensureOpenClawModelCatalogMock = vi.fn<
(
config: unknown,
agentDir: unknown,
@@ -18,8 +18,8 @@ vi.mock("../agents/agent-scope.js", () => ({
}));
vi.mock("../agents/models-config.js", () => ({
ensureOpenClawModelsJson: (config: unknown, agentDir: unknown, options?: unknown) =>
ensureOpenClawModelsJsonMock(config, agentDir, options),
ensureOpenClawModelCatalog: (config: unknown, agentDir: unknown, options?: unknown) =>
ensureOpenClawModelCatalogMock(config, agentDir, options),
}));
vi.mock("../agents/pi-embedded-runner/model.js", () => {
@@ -44,7 +44,7 @@ describe("gateway startup primary model warmup", () => {
});
beforeEach(() => {
ensureOpenClawModelsJsonMock.mockClear();
ensureOpenClawModelCatalogMock.mockClear();
piModelModuleLoadedMock.mockClear();
resolveEmbeddedAgentRuntimeMock.mockClear();
resolveEmbeddedAgentRuntimeMock.mockReturnValue("auto");
@@ -66,7 +66,7 @@ describe("gateway startup primary model warmup", () => {
log: { warn: vi.fn() },
});
expect(ensureOpenClawModelsJsonMock).toHaveBeenCalledWith(
expect(ensureOpenClawModelCatalogMock).toHaveBeenCalledWith(
cfg,
"/tmp/agent",
expect.objectContaining({
@@ -85,7 +85,7 @@ describe("gateway startup primary model warmup", () => {
log: { warn: vi.fn() },
});
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
expect(ensureOpenClawModelCatalogMock).not.toHaveBeenCalled();
expect(piModelModuleLoadedMock).not.toHaveBeenCalled();
});
@@ -123,7 +123,7 @@ describe("gateway startup primary model warmup", () => {
log: { warn: vi.fn() },
});
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
expect(ensureOpenClawModelCatalogMock).not.toHaveBeenCalled();
expect(piModelModuleLoadedMock).not.toHaveBeenCalled();
});
@@ -142,7 +142,7 @@ describe("gateway startup primary model warmup", () => {
log: { warn: vi.fn() },
});
expect(ensureOpenClawModelsJsonMock).not.toHaveBeenCalled();
expect(ensureOpenClawModelCatalogMock).not.toHaveBeenCalled();
expect(piModelModuleLoadedMock).not.toHaveBeenCalled();
});
@@ -163,7 +163,7 @@ describe("gateway startup primary model warmup", () => {
log: { warn: vi.fn() },
});
expect(ensureOpenClawModelsJsonMock).toHaveBeenCalledWith(
expect(ensureOpenClawModelCatalogMock).toHaveBeenCalledWith(
cfg,
"/tmp/agent",
expect.objectContaining({
@@ -176,8 +176,8 @@ describe("gateway startup primary model warmup", () => {
expect(piModelModuleLoadedMock).not.toHaveBeenCalled();
});
it("warns when scoped models.json preparation fails", async () => {
ensureOpenClawModelsJsonMock.mockRejectedValueOnce(new Error("models write failed"));
it("warns when scoped model catalog preparation fails", async () => {
ensureOpenClawModelCatalogMock.mockRejectedValueOnce(new Error("models write failed"));
const warn = vi.fn();
await prewarmConfiguredPrimaryModel({

View File

@@ -1,4 +1,3 @@
import path from "node:path";
import { vi } from "vitest";
import {
getTestPluginRegistry,
@@ -99,11 +98,10 @@ vi.mock("../agents/pi-model-discovery.js", async () => {
);
const createActualRegistry = (...args: Parameters<typeof actual.discoverModels>) => {
const modelsFile = path.join(args[1], "models.json");
const Registry = actual.ModelRegistry as unknown as {
create?: (
authStorage: unknown,
modelsFile: string,
agentDir?: string,
) => {
getAll: () => Array<{ provider?: string; id?: string }>;
getAvailable: () => Array<{ provider?: string; id?: string }>;
@@ -111,7 +109,7 @@ vi.mock("../agents/pi-model-discovery.js", async () => {
};
new (
authStorage: unknown,
modelsFile: string,
agentDir?: string,
): {
getAll: () => Array<{ provider?: string; id?: string }>;
getAvailable: () => Array<{ provider?: string; id?: string }>;
@@ -119,17 +117,17 @@ vi.mock("../agents/pi-model-discovery.js", async () => {
};
};
if (typeof Registry.create === "function") {
return Registry.create(args[0], modelsFile);
return Registry.create(args[0], args[1]);
}
return new Registry(args[0], modelsFile);
return new Registry(args[0], args[1]);
};
class MockModelRegistry {
private readonly actualRegistry?: ReturnType<typeof createActualRegistry>;
constructor(authStorage: unknown, modelsFile: string) {
constructor(authStorage: unknown, agentDir: string) {
if (!piSdkMock.enabled) {
this.actualRegistry = createActualRegistry(authStorage as never, path.dirname(modelsFile));
this.actualRegistry = createActualRegistry(authStorage as never, agentDir);
}
}

View File

@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const hoisted = vi.hoisted(() => ({
completeMock: vi.fn(),
ensureOpenClawModelsJsonMock: vi.fn(async () => {}),
ensureOpenClawModelCatalogMock: vi.fn(async () => {}),
getApiKeyForModelMock: vi.fn(async () => ({
apiKey: "oauth-test", // pragma: allowlist secret
source: "test",
@@ -24,7 +24,7 @@ const hoisted = vi.hoisted(() => ({
}));
const {
completeMock,
ensureOpenClawModelsJsonMock,
ensureOpenClawModelCatalogMock,
getApiKeyForModelMock,
resolveApiKeyForProviderMock,
requireApiKeyMock,
@@ -60,7 +60,7 @@ vi.mock("../agents/models-config.js", async () => ({
...(await vi.importActual<typeof import("../agents/models-config.js")>(
"../agents/models-config.js",
)),
ensureOpenClawModelsJson: ensureOpenClawModelsJsonMock,
ensureOpenClawModelCatalog: ensureOpenClawModelCatalogMock,
}));
vi.mock("../agents/model-auth.js", () => ({
@@ -174,7 +174,7 @@ describe("describeImageWithModel", () => {
text: "portal ok",
model: "MiniMax-VL-01",
});
expect(ensureOpenClawModelsJsonMock).toHaveBeenCalled();
expect(ensureOpenClawModelCatalogMock).toHaveBeenCalled();
const authRequest = getApiKeyForModelCall();
expect(authRequest?.store).toBe(authStore);
expect(requireApiKeyMock).toHaveBeenCalled();
@@ -635,7 +635,7 @@ describe("describeImageWithModel", () => {
it("rejects when image runtime setup exceeds the request timeout", async () => {
vi.useFakeTimers();
ensureOpenClawModelsJsonMock.mockImplementationOnce(() => new Promise(() => {}));
ensureOpenClawModelCatalogMock.mockImplementationOnce(() => new Promise(() => {}));
const result = describeImageWithModel({
cfg: {},

View File

@@ -5,7 +5,7 @@ import {
resolveApiKeyForProvider,
} from "../agents/model-auth.js";
import { findNormalizedProviderValue, normalizeModelRef } from "../agents/model-selection.js";
import { ensureOpenClawModelsJson } from "../agents/models-config.js";
import { ensureOpenClawModelCatalog } from "../agents/models-config.js";
import type { Api, Context, Model, ProviderStreamOptions } from "../agents/pi-ai-contract.js";
import { complete } from "../agents/pi-ai-contract.js";
import { resolveModelWithRegistry } from "../agents/pi-embedded-runner/model.js";
@@ -142,7 +142,7 @@ async function resolveImageRuntime(params: {
preferredProfile?: string;
authStore?: ImageDescriptionRequest["authStore"];
}): Promise<{ apiKey: string; model: Model<Api> }> {
await ensureOpenClawModelsJson(params.cfg, params.agentDir);
await ensureOpenClawModelCatalog(params.cfg, params.agentDir);
const { discoverAuthStorage, discoverModels } = await loadPiModelDiscoveryRuntime();
const authStorage = discoverAuthStorage(params.agentDir);
const modelRegistry = discoverModels(authStorage, params.agentDir);

View File

@@ -79,7 +79,7 @@ function sanitizeProviderHeaders(
}
// Intentionally preserve marker-shaped values here. This path handles
// explicit config/runtime provider headers, where literal values may
// legitimately match marker patterns; discovered models.json entries are
// legitimately match marker patterns; discovered model catalog entries are
// sanitized separately in the model registry path.
next[key] = value;
}