From 0102fcb7dd5be920ef92c2bf7a7b8864d1488bf4 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 26 Aug 2026 02:20:05 -0700 Subject: [PATCH] refactor(agents): resolve runtime models through prepared owner (#130007) * refactor(agents): resolve runtime models through prepared owner * chore: tighten agent inventory assertion baseline --- config/assertion-safety-baseline.txt | 2 +- ...orward-compat.errors-and-overrides.test.ts | 145 ++--- .../model.generation-scope.test.ts | 30 +- .../embedded-agent-runner/model.test.ts | 530 +++++++++--------- src/agents/embedded-agent-runner/model.ts | 102 ---- ...-effective-inventory.runtime-model.test.ts | 183 +++++- src/agents/tools-effective-inventory.test.ts | 41 +- src/agents/tools-effective-inventory.ts | 56 +- 8 files changed, 521 insertions(+), 568 deletions(-) diff --git a/config/assertion-safety-baseline.txt b/config/assertion-safety-baseline.txt index 80ba0678b68a..6d616e68058d 100644 --- a/config/assertion-safety-baseline.txt +++ b/config/assertion-safety-baseline.txt @@ -2084,7 +2084,7 @@ src/agents/tool-search-runtime.ts 6 src/agents/tool-search-transcript.ts 3 src/agents/tool-search.ts 1 src/agents/tools-effective-inventory-build.ts 2 -src/agents/tools-effective-inventory.ts 5 +src/agents/tools-effective-inventory.ts 4 src/agents/tools/agent-step.ts 6 src/agents/tools/agents-wait-tool.ts 1 src/agents/tools/ask-user-tool-normalization.ts 3 diff --git a/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts b/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts index f639d9f161ef..524b479dcbbf 100644 --- a/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts +++ b/src/agents/embedded-agent-runner/model.forward-compat.errors-and-overrides.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ModelProviderConfig, OpenClawConfig } from "../../config/config.js"; import { discoverModels } from "../agent-model-discovery.js"; import type { PreparedModelRuntimeSnapshot } from "../prepared-model-runtime.js"; +import { buildInlineProviderModels } from "./model.inline-provider.js"; import { createProviderRuntimeTestMock } from "./model.provider-runtime.test-support.js"; vi.mock("../../plugins/provider-runtime.js", () => ({ @@ -105,7 +106,7 @@ vi.mock("../prepared-model-runtime.js", async () => { }), modelCatalog: { entries: [], routeVariants: [] }, configuredRuntimeModels: [], - inlineProviderModels: [], + inlineProviderModels: buildInlineProviderModels(config.models?.providers ?? {}), createStores: () => { const authStorage = discovery.discoverAuthStorage(input.agentDir); const modelRegistry = discovery.discoverModels(authStorage, input.agentDir, { @@ -135,7 +136,7 @@ import { expectResolvedForwardCompatFallbackResult, expectUnknownModelErrorResult, } from "./model.forward-compat.test-support.js"; -import { resolveModel } from "./model.js"; +import { resolveModelAsync } from "./model.js"; import { buildOpenAICodexForwardCompatExpectation, makeModel, @@ -156,13 +157,13 @@ function createRuntimeHooks() { }); } -function resolveModelForTest( +async function resolveModelForTest( provider: string, modelId: string, agentDir?: string, cfg?: OpenClawConfig, ) { - return resolveModel(provider, modelId, agentDir, cfg, { + return await resolveModelAsync(provider, modelId, agentDir, cfg, { runtimeHooks: createRuntimeHooks(), }); } @@ -182,7 +183,7 @@ function createAnthropicTemplateModel() { }; } -function resolveAnthropicModelWithProviderOverrides(overrides: Partial) { +async function resolveAnthropicModelWithProviderOverrides(overrides: Partial) { // Provider config overrides must merge onto discovered template models without // losing the template's API, cost, and capability metadata. mockDiscoveredModel(discoverModels, { @@ -191,7 +192,7 @@ function resolveAnthropicModelWithProviderOverrides(overrides: Partial { - it("builds a forward-compat fallback for supported antigravity thinking ids", () => { + it("builds a forward-compat fallback for supported antigravity thinking ids", async () => { expectResolvedForwardCompatFallbackResult({ - result: resolveModelForTest("google-antigravity", "claude-opus-4-6-thinking", "/tmp/agent"), + result: await resolveModelForTest( + "google-antigravity", + "claude-opus-4-6-thinking", + "/tmp/agent", + ), expectedModel: { api: "google-gemini-cli", baseUrl: "https://cloudcode-pa.googleapis.com", @@ -214,24 +219,24 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("keeps unknown-model errors when no antigravity non-thinking template exists", () => { + it("keeps unknown-model errors when no antigravity non-thinking template exists", async () => { expectUnknownModelErrorResult( - resolveModelForTest("google-antigravity", "claude-opus-4-6", "/tmp/agent"), + await resolveModelForTest("google-antigravity", "claude-opus-4-6", "/tmp/agent"), "google-antigravity", "claude-opus-4-6", ); }); - it("keeps unknown-model errors for non-gpt-5 openai ids", () => { + it("keeps unknown-model errors for non-gpt-5 openai ids", async () => { expectUnknownModelErrorResult( - resolveModelForTest("openai", "gpt-4.1-mini", "/tmp/agent"), + await resolveModelForTest("openai", "gpt-4.1-mini", "/tmp/agent"), "openai", "gpt-4.1-mini", ); }); - it("rejects direct openai gpt-5.3-codex-spark with a codex-only hint", () => { - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + it("rejects direct openai gpt-5.3-codex-spark with a codex-only hint", async () => { + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -239,7 +244,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("keeps suppressed openai gpt-5.3-codex-spark from falling through provider fallback", () => { + it("keeps suppressed openai gpt-5.3-codex-spark from falling through provider fallback", async () => { const cfg = { models: { providers: { @@ -252,7 +257,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -260,7 +265,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("resolves suppressed openai gpt-5.3-codex-spark through ChatGPT/Codex routing", () => { + it("resolves suppressed openai gpt-5.3-codex-spark through ChatGPT/Codex routing", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg = { @@ -273,7 +278,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject( @@ -281,7 +286,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("resolves suppressed openai gpt-5.3-codex-spark through model-scoped Codex runtime", () => { + it("resolves suppressed openai gpt-5.3-codex-spark through model-scoped Codex runtime", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg: OpenClawConfig = { @@ -295,7 +300,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }, }; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject( @@ -303,7 +308,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("keeps model-scoped Codex runtime blocked for explicit OpenAI API-key provider config", () => { + it("keeps model-scoped Codex runtime blocked for explicit OpenAI API-key provider config", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg: OpenClawConfig = { @@ -327,13 +332,13 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }, }; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); expect(result.error).toContain("OpenAI API-key auth cannot use this model"); }); - it("keeps suppressed stale direct openai gpt-5.3-codex-spark catalog rows blocked", () => { + it("keeps suppressed stale direct openai gpt-5.3-codex-spark catalog rows blocked", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.3-codex-spark", @@ -345,13 +350,13 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toContain("ChatGPT/Codex OAuth"); }); - it("keeps stale persisted openai gpt-5.3-codex-spark rows blocked without transport metadata", () => { + it("keeps stale persisted openai gpt-5.3-codex-spark rows blocked without transport metadata", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.3-codex-spark", @@ -361,13 +366,13 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toContain("ChatGPT/Codex OAuth"); }); - it("keeps configured custom openai gpt-5.3-codex-spark rows when not direct OpenAI API", () => { + it("keeps configured custom openai gpt-5.3-codex-spark rows when not direct OpenAI API", async () => { const cfg = { models: { providers: { @@ -386,7 +391,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject({ @@ -397,7 +402,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("rejects configured direct openai gpt-5.3-codex-spark rows", () => { + it("rejects configured direct openai gpt-5.3-codex-spark rows", async () => { const cfg = { models: { providers: { @@ -416,14 +421,14 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); expect(result.error).toContain("ChatGPT/Codex OAuth"); expect(result.error).toContain("OpenAI API-key auth cannot use this model"); }); - it("keeps configured custom openai gpt-5.3-codex-spark rows that omit api", () => { + it("keeps configured custom openai gpt-5.3-codex-spark rows that omit api", async () => { const cfg = { models: { providers: { @@ -440,7 +445,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject({ @@ -451,7 +456,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("keeps registry openai gpt-5.3-codex-spark rows on custom provider endpoints", () => { + it("keeps registry openai gpt-5.3-codex-spark rows on custom provider endpoints", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.3-codex-spark", @@ -473,7 +478,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject({ @@ -484,7 +489,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("checks registry baseUrl before suppressing openai gpt-5.3-codex-spark rows", () => { + it("checks registry baseUrl before suppressing openai gpt-5.3-codex-spark rows", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.3-codex-spark", @@ -496,7 +501,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.error).toBeUndefined(); expect(result.model).toMatchObject({ @@ -507,8 +512,8 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("rejects azure openai gpt-5.3-codex-spark with a codex-only hint", () => { - const result = resolveModelForTest( + it("rejects azure openai gpt-5.3-codex-spark with a codex-only hint", async () => { + const result = await resolveModelForTest( "azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent", @@ -520,7 +525,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("rejects azure openai gpt-5.3-codex-spark through the openai owner when azure config has no matching model row", () => { + it("rejects azure openai gpt-5.3-codex-spark through the openai owner when azure config has no matching model row", async () => { const cfg = { models: { providers: { @@ -533,7 +538,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest( + const result = await resolveModelForTest( "azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent", @@ -546,7 +551,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("keeps unconditional codex-only suppression on the openai owner when azure config has a matching model row", () => { + it("keeps unconditional codex-only suppression on the openai owner when azure config has a matching model row", async () => { const cfg = { models: { providers: { @@ -559,7 +564,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest( + const result = await resolveModelForTest( "azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent", @@ -572,7 +577,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("keeps provider-level azure deployment names on the azure owner", () => { + it("keeps provider-level azure deployment names on the azure owner", async () => { const cfg = { models: { providers: { @@ -585,7 +590,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest( + const result = await resolveModelForTest( "azure-openai-responses", "customer-gpt-deployment", "/tmp/agent", @@ -601,7 +606,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("uses retained azure alias transport defaults for provider-level deployment names", () => { + it("uses retained azure alias transport defaults for provider-level deployment names", async () => { const cfg = { models: { providers: { @@ -613,7 +618,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest( + const result = await resolveModelForTest( "azure-openai-responses", "customer-gpt-deployment", "/tmp/agent", @@ -629,7 +634,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("rejects provider-level azure codex-only aliases through the openai owner", () => { + it("rejects provider-level azure codex-only aliases through the openai owner", async () => { const cfg = { models: { providers: { @@ -642,7 +647,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest( + const result = await resolveModelForTest( "azure-openai-responses", "gpt-5.3-codex-spark", "/tmp/agent", @@ -655,7 +660,7 @@ describe("resolveModel forward-compat errors and overrides", () => { ); }); - it("uses codex fallback even when openai provider is configured", () => { + it("uses codex fallback even when openai provider is configured", async () => { const cfg: OpenClawConfig = { models: { providers: { @@ -668,7 +673,7 @@ describe("resolveModel forward-compat errors and overrides", () => { } as unknown as OpenClawConfig; expectResolvedForwardCompatFallbackResult({ - result: resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), + result: await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), expectedModel: { api: "openai-chatgpt-responses", id: "gpt-5.4", @@ -677,7 +682,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("uses codex fallback when inline model omits api (#39682)", () => { + it("uses codex fallback when inline model omits api (#39682)", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg: OpenClawConfig = { @@ -692,7 +697,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.api).toBe("openai-chatgpt-responses"); expect(result.model?.baseUrl).toBe("https://custom.example.com"); @@ -703,7 +708,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("keeps openai gpt-5.4 responses overrides on the OpenAI API transport", () => { + it("keeps openai gpt-5.4 responses overrides on the OpenAI API transport", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg: OpenClawConfig = { @@ -718,7 +723,7 @@ describe("resolveModel forward-compat errors and overrides", () => { } as unknown as OpenClawConfig; expectResolvedForwardCompatFallbackResult({ - result: resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), + result: await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), expectedModel: { api: "openai-responses", baseUrl: "https://api.openai.com/v1", @@ -728,7 +733,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("normalizes openai gpt-5.4 completions overrides to the OpenAI API transport", () => { + it("normalizes openai gpt-5.4 completions overrides to the OpenAI API transport", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg: OpenClawConfig = { @@ -743,7 +748,7 @@ describe("resolveModel forward-compat errors and overrides", () => { } as unknown as OpenClawConfig; expectResolvedForwardCompatFallbackResult({ - result: resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), + result: await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg), expectedModel: { api: "openai-responses", baseUrl: "https://api.openai.com/v1", @@ -753,8 +758,8 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("includes auth hint for unknown ollama models (#17328)", () => { - const result = resolveModelForTest("ollama", "gemma3:4b", "/tmp/agent"); + it("includes auth hint for unknown ollama models (#17328)", async () => { + const result = await resolveModelForTest("ollama", "gemma3:4b", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toContain("Unknown model: ollama/gemma3:4b"); @@ -762,31 +767,31 @@ describe("resolveModel forward-compat errors and overrides", () => { expect(result.error).toContain("docs.openclaw.ai/providers/ollama"); }); - it("includes auth hint for unknown vllm models", () => { - const result = resolveModelForTest("vllm", "llama-3-70b", "/tmp/agent"); + it("includes auth hint for unknown vllm models", async () => { + const result = await resolveModelForTest("vllm", "llama-3-70b", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toContain("Unknown model: vllm/llama-3-70b"); expect(result.error).toContain("VLLM_API_KEY"); }); - it("does not add auth hint for non-local providers", () => { - const result = resolveModelForTest("google-antigravity", "some-model", "/tmp/agent"); + it("does not add auth hint for non-local providers", async () => { + const result = await resolveModelForTest("google-antigravity", "some-model", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toBe("Unknown model: google-antigravity/some-model"); }); - it("applies provider baseUrl override to registry-found models", () => { - const result = resolveAnthropicModelWithProviderOverrides({ + it("applies provider baseUrl override to registry-found models", async () => { + const result = await resolveAnthropicModelWithProviderOverrides({ baseUrl: "https://my-proxy.example.com", }); expect(result.error).toBeUndefined(); expect(result.model?.baseUrl).toBe("https://my-proxy.example.com"); }); - it("applies provider headers override to registry-found models", () => { - const result = resolveAnthropicModelWithProviderOverrides({ + it("applies provider headers override to registry-found models", async () => { + const result = await resolveAnthropicModelWithProviderOverrides({ headers: { "X-Custom-Auth": "token-123" }, }); expect(result.error).toBeUndefined(); @@ -795,7 +800,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("lets provider config override registry-found kimi user agent headers", () => { + it("lets provider config override registry-found kimi user agent headers", async () => { mockDiscoveredModel(discoverModels, { provider: "kimi", modelId: "kimi-code", @@ -827,7 +832,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("kimi", "kimi-code", "/tmp/agent", cfg); + const result = await resolveModelForTest("kimi", "kimi-code", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.id).toBe("kimi-code"); expect((result.model as unknown as { headers?: Record }).headers).toEqual({ @@ -836,7 +841,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }); }); - it("does not override when no provider config exists", () => { + it("does not override when no provider config exists", async () => { mockDiscoveredModel(discoverModels, { provider: "anthropic", modelId: "claude-sonnet-4-6", @@ -854,7 +859,7 @@ describe("resolveModel forward-compat errors and overrides", () => { }, }); - const result = resolveModelForTest("anthropic", "claude-sonnet-4-6", "/tmp/agent"); + const result = await resolveModelForTest("anthropic", "claude-sonnet-4-6", "/tmp/agent"); expect(result.error).toBeUndefined(); expect(result.model?.baseUrl).toBe("https://api.anthropic.com"); }); diff --git a/src/agents/embedded-agent-runner/model.generation-scope.test.ts b/src/agents/embedded-agent-runner/model.generation-scope.test.ts index 72060808776a..193864a01446 100644 --- a/src/agents/embedded-agent-runner/model.generation-scope.test.ts +++ b/src/agents/embedded-agent-runner/model.generation-scope.test.ts @@ -1,13 +1,12 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../../config/types.openclaw.js"; import { clearPluginMetadataLifecycleCaches } from "../../plugins/plugin-metadata-lifecycle.js"; -import { withPluginRuntimeGenerationScope } from "../../plugins/runtime/generation-scope.js"; import { createModelGenerationFixture, publishCurrentModelGeneration, resetModelGenerationFixtureState, } from "./model.generation-scope.test-support.js"; -import { resolveModel, resolveModelAsync } from "./model.js"; +import { resolveModelAsync } from "./model.js"; async function resolveGeneration(generation: ReturnType) { const { preparedModelRuntime } = generation; @@ -117,31 +116,4 @@ describe("model runtime generation scope", () => { }); expect(generationB.resolveDynamicModel).not.toHaveBeenCalled(); }); - - it("keeps synchronous resolution on the exact scoped generation", () => { - const config = {} satisfies OpenClawConfig; - const generationA = createModelGenerationFixture({ config, label: "a" }); - const generationB = createModelGenerationFixture({ config, label: "b" }); - publishCurrentModelGeneration(generationB); - const stores = generationA.preparedModelRuntime.createStores(); - - const result = withPluginRuntimeGenerationScope(generationA.preparedModelRuntime, () => - resolveModel( - generationA.requestProvider, - generationA.modelId, - generationA.preparedModelRuntime.agentDir, - config, - { - ...stores, - workspaceDir: generationA.preparedModelRuntime.workspaceDir, - }, - ), - ); - - expect(result.model).toMatchObject({ - provider: generationA.provider, - name: "Runtime A", - }); - expect(generationB.resolveDynamicModel).not.toHaveBeenCalled(); - }); }); diff --git a/src/agents/embedded-agent-runner/model.test.ts b/src/agents/embedded-agent-runner/model.test.ts index 660170d93523..8bf1f7cff00b 100644 --- a/src/agents/embedded-agent-runner/model.test.ts +++ b/src/agents/embedded-agent-runner/model.test.ts @@ -261,7 +261,7 @@ import { getModelProviderLocalService } from "../provider-local-service.js"; import { getModelProviderRequestTransport } from "../provider-request-config.js"; import { buildForwardCompatTemplate } from "./model.forward-compat.test-support.js"; import { buildInlineProviderModels } from "./model.inline-provider.js"; -import { resolveModel, resolveModelAsync, resolveModelWithRegistry } from "./model.js"; +import { resolveModelAsync, resolveModelWithRegistry } from "./model.js"; import { buildOpenAICodexForwardCompatExpectation, makeOpenClawConfigFixture, @@ -308,22 +308,6 @@ beforeEach(() => { }); }); -it("rejects synchronous resolution before lifecycle publication", () => { - preparedSnapshotState.enabled = false; - - expect(() => - resolveModel( - "openai", - "gpt-5.5", - "/tmp/unpublished-agent", - {}, - { - runtimeHooks: createRuntimeHooks(), - }, - ), - ).toThrow("prepared model runtime is not published for synchronous model resolution"); -}); - function createRuntimeHooks() { // Runtime hooks emulate provider plugin model discovery, transport // normalization, and OpenRouter capability loading without plugin imports. @@ -344,7 +328,7 @@ function createRuntimeHooks() { }); } -function resolveModelForTest( +async function resolveModelForTest( provider: string, modelId: string, agentDir?: string, @@ -353,7 +337,7 @@ function resolveModelForTest( // Most tests use fixed auth storage to keep assertions focused on model // resolution rather than auth discovery. const resolvedAgentDir = agentDir ?? "/tmp/agent"; - return resolveModel(provider, modelId, agentDir, cfg, { + return await resolveModelAsync(provider, modelId, agentDir, cfg, { authStorage: { mocked: true } as never, modelRegistry: discoverModels({ mocked: true } as never, resolvedAgentDir), runtimeHooks: createRuntimeHooks(), @@ -401,9 +385,7 @@ function resolveModelAsyncForTest( }); } -type ResolveModelForTestResult = - | ReturnType - | Awaited>; +type ResolveModelForTestResult = Awaited>; function expectResolvedModel(result: ResolveModelForTestResult) { if (result.error !== undefined) { @@ -862,7 +844,7 @@ describe("resolveModel", () => { expect(discoverModels).toHaveBeenCalledTimes(1); }); - it("uses the resolved default agent workspace for prepared model discovery", () => { + it("uses the resolved default agent workspace for prepared model discovery", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-workspace-")); const agentDir = path.join(rootDir, "agent"); const workspaceDir = path.join(rootDir, "workspace"); @@ -874,7 +856,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModel("openai", "gpt-5.5", agentDir, cfg, { + const result = await resolveModelAsync("openai", "gpt-5.5", agentDir, cfg, { runtimeHooks: createRuntimeHooks(), }); @@ -886,42 +868,36 @@ describe("resolveModel", () => { ); }); - it.each(["sync", "async"] as const)( - "passes config into %s model discovery when auth storage is prebuilt", - async (mode) => { - const agentDir = `/tmp/agent-configured-${mode}`; - const workspaceDir = `/tmp/workspace-configured-${mode}`; - const authStorage = { mocked: true } as never; - const cfg = { - models: { - providers: { - openai: { - api: "openai-completions", - baseUrl: "https://api.openai.com/v1", - models: [{ id: "gpt-5.5", baseUrl: "https://api.openai.com/v1" }], - }, + it("passes config into model discovery when auth storage is prebuilt", async () => { + const agentDir = "/tmp/agent-configured"; + const workspaceDir = "/tmp/workspace-configured"; + const authStorage = { mocked: true } as never; + const cfg = { + models: { + providers: { + openai: { + api: "openai-completions", + baseUrl: "https://api.openai.com/v1", + models: [{ id: "gpt-5.5", baseUrl: "https://api.openai.com/v1" }], }, }, - } as unknown as OpenClawConfig; - mockModelDiscovery(); + }, + } as unknown as OpenClawConfig; + mockModelDiscovery(); - const options = { - authStorage, - workspaceDir, - runtimeHooks: createRuntimeHooks(), - }; - const result = - mode === "sync" - ? resolveModel("openai", "gpt-5.5", agentDir, cfg, options) - : await resolveModelAsync("openai", "gpt-5.5", agentDir, cfg, options); + const options = { + authStorage, + workspaceDir, + runtimeHooks: createRuntimeHooks(), + }; + const result = await resolveModelAsync("openai", "gpt-5.5", agentDir, cfg, options); - expectResolvedModel(result); - expect(discoverModels).toHaveBeenCalledWith(authStorage, agentDir, { - config: cfg, - workspaceDir, - }); - }, - ); + expectResolvedModel(result); + expect(discoverModels).toHaveBeenCalledWith(authStorage, agentDir, { + config: cfg, + workspaceDir, + }); + }); it("does not poll implicit main auth during request resolution", async () => { const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-cache-state-")); @@ -1301,7 +1277,7 @@ describe("resolveModel", () => { expect(discoverModels).not.toHaveBeenCalled(); }); - it("keeps a bundled static catalog window for a transport-only configured model", () => { + it("keeps a bundled static catalog window for a transport-only configured model", async () => { resolveBundledStaticCatalogModelMock.mockReturnValue({ provider: "openai", id: "gpt-5.3-codex", @@ -1320,7 +1296,7 @@ describe("resolveModel", () => { models: [{ id: "gpt-5.3-codex", name: "GPT-5.3 Codex" }], }); - const result = resolveModelForTest("openai", "gpt-5.3-codex", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.3-codex", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "openai", @@ -1374,7 +1350,7 @@ describe("resolveModel", () => { ); }); - it("prefers user openclaw.json config over the Fireworks manifest for the same id", () => { + it("prefers user openclaw.json config over the Fireworks manifest for the same id", async () => { resolveBundledStaticCatalogModelMock.mockReturnValue({ ...makeModel("accounts/fireworks/models/kimi-k2p6"), provider: "fireworks", @@ -1404,7 +1380,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest( + const result = await resolveModelForTest( "fireworks", "accounts/fireworks/models/kimi-k2p6", "/tmp/agent", @@ -1691,7 +1667,7 @@ describe("resolveModel", () => { }); }); - it("merges configured media input with discovered model metadata", () => { + it("merges configured media input with discovered model metadata", async () => { mockDiscoveredModel(discoverModels, { provider: "custom", modelId: "vision-model", @@ -1712,7 +1688,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "vision-model", "/tmp/agent", { + const result = await resolveModelForTest("custom", "vision-model", "/tmp/agent", { models: { providers: { custom: { @@ -1754,7 +1730,7 @@ describe("resolveModel", () => { expect(discoverModels).not.toHaveBeenCalled(); }); - it("defaults model input to text when discovery omits input", () => { + it("defaults model input to text when discovery omits input", async () => { mockDiscoveredModel(discoverModels, { provider: "custom", modelId: "missing-input", @@ -1772,7 +1748,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "missing-input", "/tmp/agent", { + const result = await resolveModelForTest("custom", "missing-input", "/tmp/agent", { models: { providers: { custom: { @@ -1788,7 +1764,7 @@ describe("resolveModel", () => { expect(expectResolvedModel(result).input).toEqual(["text"]); }); - it("defaults missing model cost before handing models to OpenClaw", () => { + it("defaults missing model cost before handing models to OpenClaw", async () => { const cfg: OpenClawConfig = { models: { providers: { @@ -1812,7 +1788,7 @@ describe("resolveModel", () => { }, }; - const result = resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "openai", @@ -1821,10 +1797,10 @@ describe("resolveModel", () => { }); }); - it("includes provider baseUrl in fallback model", () => { + it("includes provider baseUrl in fallback model", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000" }); - const result = resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.baseUrl).toBe("http://localhost:9000"); @@ -1833,12 +1809,12 @@ describe("resolveModel", () => { expect(model.api).toBe("openai-completions"); }); - it("defaults baseUrl-only Google fallback models to native Gemini transport", () => { + it("defaults baseUrl-only Google fallback models to native Gemini transport", async () => { const cfg = makeProviderConfig("google", { baseUrl: "https://generativelanguage.googleapis.com", }); - const result = resolveModelForTest("google", "gemini-2.5-flash-lite", "/tmp/agent", cfg); + const result = await resolveModelForTest("google", "gemini-2.5-flash-lite", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.provider).toBe("google"); @@ -1847,12 +1823,17 @@ describe("resolveModel", () => { expect(model.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta"); }); - it("defaults baseUrl-only Google Vertex fallback models to native Vertex transport", () => { + it("defaults baseUrl-only Google Vertex fallback models to native Vertex transport", async () => { const cfg = makeProviderConfig("google-vertex", { baseUrl: "https://aiplatform.googleapis.com", }); - const result = resolveModelForTest("google-vertex", "gemini-2.5-flash", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "google-vertex", + "gemini-2.5-flash", + "/tmp/agent", + cfg, + ); const model = expectResolvedModel(result); expect(model.provider).toBe("google-vertex"); @@ -1861,7 +1842,7 @@ describe("resolveModel", () => { expect(model.baseUrl).toBe("https://aiplatform.googleapis.com"); }); - it("clamps per-model maxTokens to the per-model context window", () => { + it("clamps per-model maxTokens to the per-model context window", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ provider: "xiaomi-token-plan", id: "mimo-v2.5-pro", @@ -1887,7 +1868,12 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("xiaomi-token-plan", "mimo-v2.5-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "xiaomi-token-plan", + "mimo-v2.5-pro", + "/tmp/agent", + cfg, + ); const model = expectResolvedModel(result); expect(model.name).toBe("Xiaomi MiMo V2.5 Pro"); @@ -1904,10 +1890,10 @@ describe("resolveModel", () => { }); }); - it("preserves configured maxTokens provenance from model discovery", () => { + it("preserves configured maxTokens provenance from model discovery", async () => { mockDiscoveredGroqModel("configured"); - const result = resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent"); + const result = await resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent"); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -1916,7 +1902,7 @@ describe("resolveModel", () => { }); }); - it("marks a provider-level maxTokens override as configured", () => { + it("marks a provider-level maxTokens override as configured", async () => { mockDiscoveredGroqModel(); const cfg = makeProviderConfig("groq", { baseUrl: "https://api.groq.com/openai/v1", @@ -1924,7 +1910,7 @@ describe("resolveModel", () => { maxTokens: 2_048, }); - const result = resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent", cfg); + const result = await resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -1933,7 +1919,7 @@ describe("resolveModel", () => { }); }); - it("marks a configured-model top-level maxTokens override as configured", () => { + it("marks a configured-model top-level maxTokens override as configured", async () => { mockDiscoveredGroqModel(); const cfg = makeProviderConfig("groq", { baseUrl: "https://api.groq.com/openai/v1", @@ -1947,7 +1933,7 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent", cfg); + const result = await resolveModelForTest("groq", "llama-3.3-70b-versatile", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -1956,7 +1942,7 @@ describe("resolveModel", () => { }); }); - it("leaves maxTokens undefined when no configured or catalog value is available (regression: #98295)", () => { + it("leaves maxTokens undefined when no configured or catalog value is available (regression: #98295)", async () => { // Regression for https://github.com/openclaw/openclaw/issues/98295. // A custom provider entry without maxTokens (and no matching bundled // static catalog row) must not synthesize an oversized output cap from @@ -1970,7 +1956,7 @@ describe("resolveModel", () => { models: [{ id: "mimo-v2.5-pro", name: "mimo-v2.5-pro" }], }); - const result = resolveModelForTest("xiaomi", "mimo-v2.5-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("xiaomi", "mimo-v2.5-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.id).toBe("mimo-v2.5-pro"); @@ -1978,13 +1964,13 @@ describe("resolveModel", () => { expect(model.maxTokens).toBeUndefined(); }); - it("inherits bundled static transport for configured provider fallback models", () => { + it("inherits bundled static transport for configured provider fallback models", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce( makeDeepSeekCatalogModel({ compat: deepSeekCatalogCompat }), ); const cfg = makeDeepSeekConfig({ compat: { supportsReasoningEffort: false } }, { baseUrl: "" }); - const result = resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -2113,7 +2099,7 @@ describe("resolveModel", () => { expect(runProviderDynamicModel).toHaveBeenCalled(); }); - it("keeps configured transport overrides ahead of bundled static fallback metadata", () => { + it("keeps configured transport overrides ahead of bundled static fallback metadata", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce(makeDeepSeekCatalogModel()); const cfg = makeDeepSeekConfig( { @@ -2126,7 +2112,7 @@ describe("resolveModel", () => { }, ); - const result = resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -2137,7 +2123,7 @@ describe("resolveModel", () => { }); }); - it("keeps bundled static baseUrl when provider api is configured without a baseUrl", () => { + it("keeps bundled static baseUrl when provider api is configured without a baseUrl", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce( makeDeepSeekCatalogModel({ api: "openai-responses" }), ); @@ -2146,7 +2132,7 @@ describe("resolveModel", () => { { api: "openai-completions" }, ); - const result = resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("deepseek", "deepseek-v4-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -2158,7 +2144,7 @@ describe("resolveModel", () => { expect(model.thinkingLevelMap).toEqual({ off: null }); }); - it("keeps per-model token overrides ahead of bundled static fallback metadata", () => { + it("keeps per-model token overrides ahead of bundled static fallback metadata", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ provider: "xiaomi-token-plan", id: "mimo-v2.5-pro", @@ -2186,7 +2172,12 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("xiaomi-token-plan", "mimo-v2.5-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "xiaomi-token-plan", + "mimo-v2.5-pro", + "/tmp/agent", + cfg, + ); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -2196,16 +2187,16 @@ describe("resolveModel", () => { }); }); - it("does not synthesize unknown models from timeout-only provider overlays", () => { + it("does not synthesize unknown models from timeout-only provider overlays", async () => { const cfg = makeProviderConfig("openai", { timeoutSeconds: 300, baseUrl: "" }); - const result = resolveModelForTest("openai", "typo-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "typo-model", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); expect(result.error).toBe("Unknown model: openai/typo-model"); }); - it("does not create fallback models from provider overlays alone", () => { + it("does not create fallback models from provider overlays alone", async () => { const cfg = { models: { providers: { @@ -2216,7 +2207,7 @@ describe("resolveModel", () => { }, } satisfies OpenClawConfigInput; - const result = resolveModelForTest( + const result = await resolveModelForTest( "typoProvider", "typoed-model", "/tmp/agent", @@ -2227,7 +2218,7 @@ describe("resolveModel", () => { expect(result.error).toBe("Unknown model: typoProvider/typoed-model"); }); - it("does not create fallback models from built-in provider api overlays", () => { + it("does not create fallback models from built-in provider api overlays", async () => { const cfg = { models: { providers: { @@ -2238,7 +2229,7 @@ describe("resolveModel", () => { }, } satisfies OpenClawConfigInput; - const result = resolveModelForTest( + const result = await resolveModelForTest( "openai", "typoed-model", "/tmp/agent", @@ -2249,7 +2240,7 @@ describe("resolveModel", () => { expect(result.error).toBe("Unknown model: openai/typoed-model"); }); - it("resolves per-model api and baseUrl override in fallback model", () => { + it("resolves per-model api and baseUrl override in fallback model", async () => { const cfg = { models: { providers: { @@ -2278,19 +2269,19 @@ describe("resolveModel", () => { }, } as unknown as OpenClawConfig; - const claude = resolveModelForTest("my-router", "my-router/claude", "/tmp/agent", cfg); + const claude = await resolveModelForTest("my-router", "my-router/claude", "/tmp/agent", cfg); const claudeModel = expectResolvedModel(claude); expect(claudeModel.api).toBe("anthropic-messages"); expect(claudeModel.baseUrl).toBe("http://localhost:8080"); expect(claudeModel.maxTokens).toBeUndefined(); - const gpt = resolveModelForTest("my-router", "my-router/gpt", "/tmp/agent", cfg); + const gpt = await resolveModelForTest("my-router", "my-router/gpt", "/tmp/agent", cfg); const gptModel = expectResolvedModel(gpt); expect(gptModel.api).toBe("openai-completions"); expect(gptModel.baseUrl).toBe("http://localhost:8080/v1"); }); - it("preserves normalized inline provider transport when static metadata is merged", () => { + it("preserves normalized inline provider transport when static metadata is merged", async () => { const cfg = makeProviderConfig("my-gemini", { api: "google-generative-ai", baseUrl: "https://generativelanguage.googleapis.com", @@ -2304,14 +2295,14 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("my-gemini", "gemini-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("my-gemini", "gemini-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.api).toBe("google-generative-ai"); expect(model.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta"); }); - it("defaults baseUrl-only local custom fallback models to chat completions", () => { + it("defaults baseUrl-only local custom fallback models to chat completions", async () => { const cfg = makeOpenClawConfigFixture({ agents: { defaults: { @@ -2328,7 +2319,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("local-agent-proxy", "gpt-5.2", "/tmp/agent", cfg); + const result = await resolveModelForTest("local-agent-proxy", "gpt-5.2", "/tmp/agent", cfg); const model = expectResolvedModel(result); expectRecordFields(model, { @@ -2340,7 +2331,7 @@ describe("resolveModel", () => { expect(getModelProviderRequestTransport(model)).toBeUndefined(); }); - it("attaches provider localService metadata to configured fallback models", () => { + it("attaches provider localService metadata to configured fallback models", async () => { const cfg = makeProviderConfig("ds4", { baseUrl: "http://127.0.0.1:18000/v1", api: "openai-completions", @@ -2353,7 +2344,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("ds4", "deepseek-v4-flash", "/tmp/agent", cfg); + const result = await resolveModelForTest("ds4", "deepseek-v4-flash", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(getModelProviderLocalService(model)).toEqual({ @@ -2365,7 +2356,7 @@ describe("resolveModel", () => { }); }); - it("resolves explicitly configured qwen3.6-plus before Coding Plan built-in suppression", () => { + it("resolves explicitly configured qwen3.6-plus before Coding Plan built-in suppression", async () => { const cfg = { models: { providers: { @@ -2387,7 +2378,7 @@ describe("resolveModel", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg); + const result = await resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "qwen", @@ -2400,13 +2391,13 @@ describe("resolveModel", () => { }); }); - it("keeps unconfigured qwen3.6-plus suppressed on Coding Plan endpoints", () => { + it("keeps unconfigured qwen3.6-plus suppressed on Coding Plan endpoints", async () => { const cfg = makeProviderConfig("qwen", { baseUrl: "https://coding-intl.dashscope.aliyuncs.com/v1", api: "openai-completions", }); - const result = resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg); + const result = await resolveModelForTest("qwen", "qwen3.6-plus", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -2414,7 +2405,7 @@ describe("resolveModel", () => { ); }); - it("#74451: resolves explicitly configured openai/gpt-5.4-mini inline entries", () => { + it("#74451: resolves explicitly configured openai/gpt-5.4-mini inline entries", async () => { const cfg = { models: { providers: { @@ -2434,7 +2425,7 @@ describe("resolveModel", () => { }, } as unknown as OpenClawConfig; - const result = resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "openai", @@ -2445,20 +2436,20 @@ describe("resolveModel", () => { }); }); - it("normalizes Google fallback baseUrls for custom providers", () => { + it("normalizes Google fallback baseUrls for custom providers", async () => { const cfg = makeProviderConfig("google-paid", { baseUrl: "https://generativelanguage.googleapis.com", api: "google-generative-ai", }); - const result = resolveModelForTest("google-paid", "missing-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("google-paid", "missing-model", "/tmp/agent", cfg); expect(expectResolvedModel(result).baseUrl).toBe( "https://generativelanguage.googleapis.com/v1beta", ); }); - it("normalizes configured Google override baseUrls when provider api is omitted", () => { + it("normalizes configured Google override baseUrls when provider api is omitted", async () => { mockMinimalModelDiscovery("google", "gemini-2.5-pro", { api: "google-generative-ai", baseUrl: "https://generativelanguage.googleapis.com/v1beta", @@ -2469,21 +2460,21 @@ describe("resolveModel", () => { models: [{ id: "gemini-2.5-pro", name: "gemini-2.5-pro" }], }); - const result = resolveModelForTest("google", "gemini-2.5-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("google", "gemini-2.5-pro", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.api).toBe("google-generative-ai"); expect(model.baseUrl).toBe("https://generativelanguage.googleapis.com/v1beta"); }); - it("normalizes custom api.openai.com providers to responses transport", () => { + it("normalizes custom api.openai.com providers to responses transport", async () => { const cfg = makeProviderConfig("custom-openai", { baseUrl: "https://api.openai.com/v1", api: "openai-completions", models: [{ ...makeModel("gpt-5.4"), provider: "custom-openai" }], }); - const result = resolveModelForTest("custom-openai", "gpt-5.4", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom-openai", "gpt-5.4", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "custom-openai", @@ -2493,14 +2484,14 @@ describe("resolveModel", () => { }); }); - it("normalizes custom api.x.ai providers to responses transport", () => { + it("normalizes custom api.x.ai providers to responses transport", async () => { const cfg = makeProviderConfig("custom-xai", { baseUrl: "https://api.x.ai/v1", api: "openai-completions", models: [{ ...makeModel("grok-4.1-fast"), provider: "custom-xai" }], }); - const result = resolveModelForTest("custom-xai", "grok-4.1-fast", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom-xai", "grok-4.1-fast", "/tmp/agent", cfg); expectRecordFields(expectResolvedModel(result), { provider: "custom-xai", @@ -2510,27 +2501,27 @@ describe("resolveModel", () => { }); }); - it("leaves dynamic GitHub Copilot request identity to runtime auth preparation", () => { - const result = resolveModelForTest("github-copilot", "gpt-5.5", "/tmp/agent"); + it("leaves dynamic GitHub Copilot request identity to runtime auth preparation", async () => { + const result = await resolveModelForTest("github-copilot", "gpt-5.5", "/tmp/agent"); const model = expectResolvedModel(result) as unknown as { headers?: Record }; expect(model.headers).toBeUndefined(); }); - it("leaves configured GitHub Copilot request identity to runtime auth preparation", () => { + it("leaves configured GitHub Copilot request identity to runtime auth preparation", async () => { const cfg = makeProviderConfig("github-copilot", { baseUrl: "https://api.githubcopilot.com", api: "openai-responses", models: [makeModel("gpt-5.5")], }); - const result = resolveModelForTest("github-copilot", "gpt-5.5", "/tmp/agent", cfg); + const result = await resolveModelForTest("github-copilot", "gpt-5.5", "/tmp/agent", cfg); const model = expectResolvedModel(result) as unknown as { headers?: Record }; expect(model.headers).toBeUndefined(); }); - it("includes provider headers in provider fallback model", () => { + it("includes provider headers in provider fallback model", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", headers: { "X-Custom-Auth": "token-123" }, @@ -2538,7 +2529,7 @@ describe("resolveModel", () => { }); // Requesting a non-listed model forces the providerCfg fallback branch. - const result = resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); const model = expectResolvedModel(result) as unknown as { headers?: Record }; expect(model.headers).toEqual({ @@ -2546,7 +2537,7 @@ describe("resolveModel", () => { }); }); - it("drops SecretRef marker provider headers in fallback models", () => { + it("drops SecretRef marker provider headers in fallback models", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", headers: { @@ -2557,7 +2548,7 @@ describe("resolveModel", () => { models: [makeModel("listed-model")], }); - const result = resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "missing-model", "/tmp/agent", cfg); const model = expectResolvedModel(result) as unknown as { headers?: Record }; expect(model.headers).toEqual({ @@ -2565,7 +2556,7 @@ describe("resolveModel", () => { }); }); - it("drops marker headers from discovered models.json entries", () => { + it("drops marker headers from discovered models.json entries", async () => { mockMinimalModelDiscovery("custom", "listed-model", { headers: { Authorization: "secretref-env:OPENAI_HEADER_TOKEN", @@ -2574,7 +2565,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "listed-model", "/tmp/agent"); + const result = await resolveModelForTest("custom", "listed-model", "/tmp/agent"); const model = expectResolvedModel(result) as unknown as { headers?: Record }; expect(model.headers).toEqual({ @@ -2582,7 +2573,7 @@ describe("resolveModel", () => { }); }); - it("prefers matching configured model metadata for fallback token limits", () => { + it("prefers matching configured model metadata for fallback token limits", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", models: [ @@ -2591,14 +2582,14 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); const model = expectResolvedModel(result); expect(model.contextWindow).toBe(262144); expect(model.maxTokens).toBe(32768); }); - it("merges configured model params with agent defaults for resolved models", () => { + it("merges configured model params with agent defaults for resolved models", async () => { mockMinimalModelDiscovery("ollama", "qwen3:32b", { params: { num_ctx: 4096, keep_alive: "1m" }, }); @@ -2627,7 +2618,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect((result.model as { params?: Record } | undefined)?.params).toEqual({ @@ -2637,14 +2628,14 @@ describe("resolveModel", () => { }); }); - it("applies configured provider params to resolved models", () => { + it("applies configured provider params to resolved models", async () => { mockMinimalModelDiscovery("ollama", "qwen3:32b", { params: { keep_alive: "1m" } }); const cfg = makeProviderConfig("ollama", { baseUrl: "http://localhost:11434", params: { num_ctx: 65536, top_p: 0.9 }, }); - const result = resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect((result.model as { params?: Record } | undefined)?.params).toEqual({ @@ -2654,7 +2645,7 @@ describe("resolveModel", () => { }); }); - it("resolves provider request timeout metadata for configured provider models", () => { + it("resolves provider request timeout metadata for configured provider models", async () => { mockMinimalModelDiscovery("ollama", "qwen3:32b"); const cfg = makeProviderConfig("ollama", { baseUrl: "http://localhost:11434", @@ -2662,7 +2653,7 @@ describe("resolveModel", () => { models: [makeModel("qwen3:32b")], }); - const result = resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "qwen3:32b", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect((result.model as { requestTimeoutMs?: number } | undefined)?.requestTimeoutMs).toBe( @@ -2670,7 +2661,7 @@ describe("resolveModel", () => { ); }); - it("resolves provider request timeout metadata from built-in provider overlays", () => { + it("resolves provider request timeout metadata from built-in provider overlays", async () => { mockMinimalModelDiscovery("openai", "gpt-5.5"); const cfg = { models: { @@ -2682,7 +2673,7 @@ describe("resolveModel", () => { }, } satisfies OpenClawConfigInput; - const result = resolveModelForTest( + const result = await resolveModelForTest( "openai", "gpt-5.5", "/tmp/agent", @@ -2695,7 +2686,7 @@ describe("resolveModel", () => { ); }); - it("caps oversized provider request timeout metadata at the timer-safe ceiling", () => { + it("caps oversized provider request timeout metadata at the timer-safe ceiling", async () => { mockMinimalModelDiscovery("openai", "gpt-5.5"); const cfg = { models: { @@ -2707,7 +2698,7 @@ describe("resolveModel", () => { }, } satisfies OpenClawConfigInput; - const result = resolveModelForTest( + const result = await resolveModelForTest( "openai", "gpt-5.5", "/tmp/agent", @@ -2720,7 +2711,7 @@ describe("resolveModel", () => { ); }); - it("uses per-model context config over discovered metadata", () => { + it("uses per-model context config over discovered metadata", async () => { mockMinimalModelDiscovery("ollama", "qwen3.5:9b", { contextWindow: 216_000, contextTokens: 216_000, @@ -2733,7 +2724,7 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("ollama", "qwen3.5:9b", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "qwen3.5:9b", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.contextWindow).toBe(8_192); @@ -2741,7 +2732,7 @@ describe("resolveModel", () => { expect(result.model?.maxTokens).toBe(8_192); }); - it("keeps per-model context values with a provider output-token default", () => { + it("keeps per-model context values with a provider output-token default", async () => { mockMinimalModelDiscovery("ollama", "qwen3.5:9b", { contextWindow: 216_000, maxTokens: 65_536, @@ -2759,14 +2750,14 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("ollama", "qwen3.5:9b", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "qwen3.5:9b", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.contextWindow).toBe(16_384); expect(result.model?.maxTokens).toBe(12_000); }); - it("applies agent default model params without explicit provider config", () => { + it("applies agent default model params without explicit provider config", async () => { mockMinimalModelDiscovery("ollama", "llama3.2"); const cfg = makeOpenClawConfigFixture({ agents: { @@ -2780,7 +2771,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("ollama", "llama3.2", "/tmp/agent", cfg); + const result = await resolveModelForTest("ollama", "llama3.2", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect((result.model as { params?: Record } | undefined)?.params).toEqual({ @@ -2788,7 +2779,7 @@ describe("resolveModel", () => { }); }); - it("propagates reasoning from matching configured fallback model", () => { + it("propagates reasoning from matching configured fallback model", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", models: [ @@ -2797,12 +2788,12 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); expect(result.model?.reasoning).toBe(true); }); - it("propagates compat from matching configured fallback model", () => { + it("propagates compat from matching configured fallback model", async () => { const cfg = makeProviderConfig("vllm", { baseUrl: "http://localhost:9000", api: "openai-completions", @@ -2814,7 +2805,7 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); + const result = await resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.compat).toEqual( @@ -2823,7 +2814,7 @@ describe("resolveModel", () => { expect(result.model?.reasoning).toBe(false); }); - it("lets configured vLLM Qwen compat override stale discovered reasoning", () => { + it("lets configured vLLM Qwen compat override stale discovered reasoning", async () => { mockMinimalModelDiscovery("vllm", "Qwen/Qwen3-8B", { api: "openai-completions", baseUrl: "http://localhost:9000", @@ -2832,7 +2823,7 @@ describe("resolveModel", () => { }); const cfg = makeVllmQwenConfig(); - const result = resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); + const result = await resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.reasoning).toBe(true); @@ -2844,7 +2835,7 @@ describe("resolveModel", () => { ); }); - it("does not derive reasoning from ignored compat on a catalog-owned vLLM route", () => { + it("does not derive reasoning from ignored compat on a catalog-owned vLLM route", async () => { resolveBundledStaticCatalogModelMock.mockReturnValueOnce({ ...makeModel("Qwen/Qwen3-8B"), provider: "vllm", @@ -2855,7 +2846,7 @@ describe("resolveModel", () => { }); const cfg = makeVllmQwenConfig(); - const result = resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); + const result = await resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.reasoning).toBe(false); @@ -2863,16 +2854,16 @@ describe("resolveModel", () => { expect(result.model?.compat).not.toHaveProperty("thinkingFormat"); }); - it("infers reasoning for matching vLLM Qwen compat fallback models", () => { + it("infers reasoning for matching vLLM Qwen compat fallback models", async () => { const cfg = makeVllmQwenConfig(); - const result = resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); + const result = await resolveModelForTest("vllm", "Qwen/Qwen3-8B", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expect(result.model?.reasoning).toBe(true); }); - it("propagates image input capability from matching configured fallback model", () => { + it("propagates image input capability from matching configured fallback model", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", models: [ @@ -2881,19 +2872,19 @@ describe("resolveModel", () => { ], }); - const result = resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "model-b", "/tmp/agent", cfg); expect(result.model?.input).toEqual(["text", "image"]); }); - it("propagates image input when configured model ids include the provider prefix", () => { + it("propagates image input when configured model ids include the provider prefix", async () => { const cfg = makeProviderConfig("custom", { baseUrl: "http://localhost:9000", api: "openai-completions", models: [{ ...makeModel("custom/vision-model"), input: ["text", "image"] }], }); - const result = resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -2903,26 +2894,26 @@ describe("resolveModel", () => { }); }); - it("does not match provider-prefixed configured model ids through core provider aliases", () => { + it("does not match provider-prefixed configured model ids through core provider aliases", async () => { const cfg = makeProviderConfig("volcengine", { baseUrl: "http://localhost:9000", api: "openai-completions", models: [{ ...makeModel("volcengine/vision-model"), input: ["text", "image"] }], }); - const result = resolveModelForTest("bytedance", "vision-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("bytedance", "vision-model", "/tmp/agent", cfg); expect(result.error).toBe("Unknown model: bytedance/vision-model"); }); - it("resolves direct moonshotai refs through manifest-owned provider aliases", () => { + it("resolves direct moonshotai refs through manifest-owned provider aliases", async () => { const cfg = makeProviderConfig("moonshot", { baseUrl: "https://api.moonshot.ai/v1", api: "openai-completions", models: [{ ...makeModel("kimi-k2.6"), name: "Kimi K2.6", input: ["text", "image"] }], }); - const result = resolveModelForTest("moonshotai", "kimi-k2.6", "/tmp/agent", cfg); + const result = await resolveModelForTest("moonshotai", "kimi-k2.6", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -2931,14 +2922,14 @@ describe("resolveModel", () => { }); }); - it("resolves direct moonshot-ai refs through manifest-owned provider aliases", () => { + it("resolves direct moonshot-ai refs through manifest-owned provider aliases", async () => { const cfg = makeProviderConfig("moonshot", { baseUrl: "https://api.moonshot.ai/v1", api: "openai-completions", models: [makeModel("kimi-k2.6")], }); - const result = resolveModelForTest("moonshot-ai", "kimi-k2.6", "/tmp/agent", cfg); + const result = await resolveModelForTest("moonshot-ai", "kimi-k2.6", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -2947,7 +2938,7 @@ describe("resolveModel", () => { }); }); - it("keeps transport-overriding manifest aliases on the requested provider", () => { + it("keeps transport-overriding manifest aliases on the requested provider", async () => { const cfg = { models: { providers: { @@ -2965,7 +2956,12 @@ describe("resolveModel", () => { }, } satisfies OpenClawConfig; - const result = resolveModelForTest("azure-openai-responses", "gpt-5.5", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "azure-openai-responses", + "gpt-5.5", + "/tmp/agent", + cfg, + ); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3192,7 +3188,7 @@ describe("resolveModel", () => { const result = resolver === "sync" - ? resolveModelForTest("azure-openai-responses", "gpt-5.5", "/tmp/agent", cfg) + ? await resolveModelForTest("azure-openai-responses", "gpt-5.5", "/tmp/agent", cfg) : await resolveModelAsyncForTest("azure-openai-responses", "gpt-5.5", "/tmp/agent", cfg); expect(result.model).toBeUndefined(); @@ -3202,7 +3198,7 @@ describe("resolveModel", () => { }, ); - it("does not treat arbitrary namespaced model ids as provider prefixes", () => { + it("does not treat arbitrary namespaced model ids as provider prefixes", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -3220,13 +3216,13 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); expect(result.model?.id).toBe("vision-model"); expect(result.model?.input).toEqual(["text"]); }); - it("resolves custom MLX-style Hugging Face ids without adding the provider prefix", () => { + it("resolves custom MLX-style Hugging Face ids without adding the provider prefix", async () => { const modelId = "mlx-community/Qwen3-30B-A3B-6bit"; const cfg = makeOpenClawConfigFixture({ agents: { @@ -3252,7 +3248,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("mlx", modelId, "/tmp/agent", cfg); + const result = await resolveModelForTest("mlx", modelId, "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3263,7 +3259,7 @@ describe("resolveModel", () => { }); }); - it("prefers provider-prefixed configured metadata over discovered text-only models", () => { + it("prefers provider-prefixed configured metadata over discovered text-only models", async () => { mockMinimalModelDiscovery("custom", "vision-model", { input: ["text"] }); const cfg = makeOpenClawConfigFixture({ models: { @@ -3282,7 +3278,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "vision-model", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3292,7 +3288,7 @@ describe("resolveModel", () => { }); }); - it("keeps unknown fallback models text-only instead of borrowing image input from another configured model", () => { + it("keeps unknown fallback models text-only instead of borrowing image input from another configured model", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -3309,7 +3305,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("custom", "typoed-model", "/tmp/agent", cfg); + const result = await resolveModelForTest("custom", "typoed-model", "/tmp/agent", cfg); expect(result.model?.id).toBe("typoed-model"); expect(result.model?.input).toEqual(["text"]); @@ -3425,7 +3421,7 @@ describe("resolveModel", () => { ); }); - it("repairs stale text-only Foundry fallback rows for GPT-family models", () => { + it("repairs stale text-only Foundry fallback rows for GPT-family models", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -3445,12 +3441,12 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent", cfg); + const result = await resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent", cfg); expect(result.model?.input).toEqual(["text", "image"]); }); - it("repairs stale text-only Anthropic fallback rows for Claude vision models", () => { + it("repairs stale text-only Anthropic fallback rows for Claude vision models", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -3470,12 +3466,12 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg); + const result = await resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", cfg); expect(result.model?.input).toEqual(["text", "image"]); }); - it("repairs stale text-only Foundry discovered rows for GPT-family models", () => { + it("repairs stale text-only Foundry discovered rows for GPT-family models", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -3512,12 +3508,12 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent", cfg); + const result = await resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent", cfg); expect(result.model?.input).toEqual(["text", "image"]); }); - it("repairs stale text-only Foundry discovered rows without config overrides", () => { + it("repairs stale text-only Foundry discovered rows without config overrides", async () => { mockDiscoveredModel(discoverModels, { provider: "microsoft-foundry", modelId: "gpt-5.4", @@ -3535,7 +3531,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("microsoft-foundry", "gpt-5.4", "/tmp/agent"); expect(result.model?.input).toEqual(["text", "image"]); }); @@ -3573,7 +3569,7 @@ describe("resolveModel", () => { }); }); - it("uses OpenRouter API capabilities for unknown models when cache is populated", () => { + it("uses OpenRouter API capabilities for unknown models when cache is populated", async () => { mockGetOpenRouterModelCapabilities.mockReturnValue({ name: "Healer Alpha", input: ["text", "image"], @@ -3584,7 +3580,7 @@ describe("resolveModel", () => { cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, }); - const result = resolveModelForTest("openrouter", "openrouter/healer-alpha", "/tmp/agent"); + const result = await resolveModelForTest("openrouter", "openrouter/healer-alpha", "/tmp/agent"); expect(result.error).toBeUndefined(); const resolvedModel = expectRecordFields(result.model, { @@ -3601,10 +3597,10 @@ describe("resolveModel", () => { ); }); - it("falls back to text-only when OpenRouter API cache is empty", () => { + it("falls back to text-only when OpenRouter API cache is empty", async () => { mockGetOpenRouterModelCapabilities.mockReturnValue(undefined); - const result = resolveModelForTest("openrouter", "openrouter/healer-alpha", "/tmp/agent"); + const result = await resolveModelForTest("openrouter", "openrouter/healer-alpha", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3615,7 +3611,7 @@ describe("resolveModel", () => { }); }); - it("uses provider-normalized model ids for OpenRouter transport", () => { + it("uses provider-normalized model ids for OpenRouter transport", async () => { const modelId = "openrouter/anthropic/claude-sonnet-4.6"; mockDiscoveredModel(discoverModels, { provider: "openrouter", @@ -3635,7 +3631,7 @@ describe("resolveModel", () => { }), ); - const result = resolveModel("openrouter", modelId, "/tmp/agent", undefined, { + const result = await resolveModelAsync("openrouter", modelId, "/tmp/agent", undefined, { authStorage: { mocked: true } as never, modelRegistry: discoverModels({ mocked: true } as never, "/tmp/agent"), runtimeHooks: { @@ -3661,14 +3657,14 @@ describe("resolveModel", () => { }); }); - it("matches prefixed Hugging Face ids against discovered registry models", () => { + it("matches prefixed Hugging Face ids against discovered registry models", async () => { mockMinimalModelDiscovery("huggingface", "deepseek-ai/DeepSeek-R1", { baseUrl: "https://router.huggingface.co/v1", reasoning: true, input: ["text"], }); - const result = resolveModelForTest( + const result = await resolveModelForTest( "huggingface", "huggingface/deepseek-ai/DeepSeek-R1", "/tmp/agent", @@ -3750,7 +3746,7 @@ describe("resolveModel", () => { }); }); - it("threads the model id through inline configured transport normalization", () => { + it("threads the model id through inline configured transport normalization", async () => { const normalizeProviderTransportWithPlugin = vi.fn(() => undefined); const cfg = makeOpenClawConfigFixture({ models: { @@ -3770,7 +3766,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModel("openai", "gpt-5.5", "/tmp/agent", cfg, { + const result = await resolveModelAsync("openai", "gpt-5.5", "/tmp/agent", cfg, { authStorage: { mocked: true } as never, modelRegistry: discoverModels({ mocked: true } as never, "/tmp/agent"), runtimeHooks: { @@ -3788,7 +3784,7 @@ describe("resolveModel", () => { ); }); - it("prefers configured provider api metadata over discovered registry model", () => { + it("prefers configured provider api metadata over discovered registry model", async () => { mockDiscoveredModel(discoverModels, { provider: "onehub", modelId: "glm-5", @@ -3826,7 +3822,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("onehub", "glm-5", "/tmp/agent", cfg); + const result = await resolveModelForTest("onehub", "glm-5", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3840,7 +3836,7 @@ describe("resolveModel", () => { }); }); - it("prefers exact provider config over normalized alias match when both keys exist", () => { + it("prefers exact provider config over normalized alias match when both keys exist", async () => { mockDiscoveredModel(discoverModels, { provider: "bedrock", modelId: "bedrock-alias-exact-test", @@ -3885,7 +3881,12 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("bedrock", "bedrock-alias-exact-test", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "bedrock", + "bedrock-alias-exact-test", + "/tmp/agent", + cfg, + ); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3900,16 +3901,16 @@ describe("resolveModel", () => { }); }); - it("builds an openai fallback for gpt-5.4", () => { + it("builds an openai fallback for gpt-5.4", async () => { mockOpenAICodexTemplateModel(discoverModels); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, buildOpenAICodexForwardCompatExpectation("gpt-5.4")); }); - it("upgrades stale exact openai gpt-5.4 registry metadata via forward-compat", () => { + it("upgrades stale exact openai gpt-5.4 registry metadata via forward-compat", async () => { vi.mocked(discoverModels).mockReturnValue({ find: vi.fn((provider: string, modelId: string) => { if (provider !== "openai") { @@ -3934,7 +3935,7 @@ describe("resolveModel", () => { }), } as unknown as ReturnType); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3945,7 +3946,7 @@ describe("resolveModel", () => { }); }); - it("accepts available exact openai gpt-5.3-codex registry metadata", () => { + it("accepts available exact openai gpt-5.3-codex registry metadata", async () => { vi.mocked(discoverModels).mockReturnValue({ find: vi.fn((provider: string, modelId: string) => { if (provider !== "openai") { @@ -3963,7 +3964,7 @@ describe("resolveModel", () => { }), } as unknown as ReturnType); - const result = resolveModelForTest("openai", "gpt-5.3-codex", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -3973,10 +3974,10 @@ describe("resolveModel", () => { }); }); - it("canonicalizes the legacy openai gpt-5.4-codex alias at runtime", () => { + it("canonicalizes the legacy openai gpt-5.4-codex alias at runtime", async () => { mockOpenAICodexTemplateModel(discoverModels); - const result = resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, buildOpenAICodexForwardCompatExpectation("gpt-5.4")); @@ -3984,7 +3985,7 @@ describe("resolveModel", () => { expect(result.model?.name).toBe("gpt-5.4"); }); - it("applies canonical openai overrides when resolving the gpt-5.4-codex alias", () => { + it("applies canonical openai overrides when resolving the gpt-5.4-codex alias", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg = makeOpenClawConfigFixture({ @@ -4007,7 +4008,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4022,7 +4023,7 @@ describe("resolveModel", () => { }); }); - it("prefers alias-specific overrides over canonical ones for gpt-5.4-codex", () => { + it("prefers alias-specific overrides over canonical ones for gpt-5.4-codex", async () => { mockOpenAICodexTemplateModel(discoverModels); const cfg = makeOpenClawConfigFixture({ @@ -4047,7 +4048,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.4-codex", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4058,10 +4059,10 @@ describe("resolveModel", () => { }); }); - it("builds an openai fallback for gpt-5.4-mini", () => { + it("builds an openai fallback for gpt-5.4-mini", async () => { mockOpenAICodexTemplateModel(discoverModels); - const result = resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4071,10 +4072,10 @@ describe("resolveModel", () => { }); }); - it("does not build an openai fallback for removed gpt-5.3-codex-spark", () => { + it("does not build an openai fallback for removed gpt-5.3-codex-spark", async () => { mockOpenAICodexTemplateModel(discoverModels); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -4082,7 +4083,7 @@ describe("resolveModel", () => { ); }); - it("does not build a configured fallback for unsupported xAI multi-agent models", () => { + it("does not build a configured fallback for unsupported xAI multi-agent models", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -4095,7 +4096,12 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("xai", "grok-4.20-multi-agent-0309", "/tmp/agent", cfg); + const result = await resolveModelForTest( + "xai", + "grok-4.20-multi-agent-0309", + "/tmp/agent", + cfg, + ); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -4103,10 +4109,10 @@ describe("resolveModel", () => { ); }); - it("rejects stale openai gpt-5.3-codex-spark discovery rows", () => { + it("rejects stale openai gpt-5.3-codex-spark discovery rows", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.3-codex-spark", { input: ["text"] }); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -4114,14 +4120,14 @@ describe("resolveModel", () => { ); }); - it("prefers runtime-resolved openai gpt-5.4 metadata when it has a larger context window", () => { + it("prefers runtime-resolved openai gpt-5.4 metadata when it has a larger context window", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.4", { contextWindow: 128_000, contextTokens: 32_000, input: ["text"], }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4134,7 +4140,7 @@ describe("resolveModel", () => { }); }); - it("lets official openai metadata override stale configured model rows", () => { + it("lets official openai metadata override stale configured model rows", async () => { mockOpenAIForwardCompatDiscovery(); const cfg = makeOpenClawConfigFixture({ @@ -4161,7 +4167,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.5-pro", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.5-pro", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4178,8 +4184,8 @@ describe("resolveModel", () => { }); }); - it("resolves openai gpt-5.5 through the direct API fallback when discovery omits OAuth metadata", () => { - const result = resolveModelForTest("openai", "gpt-5.5"); + it("resolves openai gpt-5.5 through the direct API fallback when discovery omits OAuth metadata", async () => { + const result = await resolveModelForTest("openai", "gpt-5.5"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4195,7 +4201,7 @@ describe("resolveModel", () => { }); }); - it("preserves unmarked manual openai metadata overrides", () => { + it("preserves unmarked manual openai metadata overrides", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.5", { cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 }, contextWindow: 400_000, @@ -4224,7 +4230,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.5", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4254,12 +4260,12 @@ describe("resolveModel", () => { }); }); - it("normalizes stale discovered openai /backend-api/v1 metadata", () => { + it("normalizes stale discovered openai /backend-api/v1 metadata", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.4", { baseUrl: "https://chatgpt.com/backend-api/v1", }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4270,7 +4276,7 @@ describe("resolveModel", () => { }); }); - it("normalizes stale discovered openrouter /v1 metadata", () => { + it("normalizes stale discovered openrouter /v1 metadata", async () => { mockDiscoveredModel(discoverModels, { provider: "openrouter", modelId: "openai/gpt-5.4", @@ -4288,7 +4294,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openrouter", "openai/gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openrouter", "openai/gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4299,10 +4305,10 @@ describe("resolveModel", () => { }); }); - it("normalizes discovered openai metadata when api is missing", () => { + it("normalizes discovered openai metadata when api is missing", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.4", { api: undefined }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4313,7 +4319,7 @@ describe("resolveModel", () => { }); }); - it("passes configured workspaceDir to runtime preference hooks", () => { + it("passes configured workspaceDir to runtime preference hooks", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.4", { contextWindow: 128_000, contextTokens: 32_000, @@ -4347,7 +4353,7 @@ describe("resolveModel", () => { }, } as OpenClawConfig; - const result = resolveModel("openai", "gpt-5.4", "/tmp/agent-state", cfg, { + const result = await resolveModelAsync("openai", "gpt-5.4", "/tmp/agent-state", cfg, { authStorage: { mocked: true } as never, modelRegistry: discoverModels({ mocked: true } as never, "/tmp/agent-state"), runtimeHooks, @@ -4434,13 +4440,13 @@ describe("resolveModel", () => { }); }); - it("resolves discovered openai gpt-5.4-mini rows", () => { + it("resolves discovered openai gpt-5.4-mini rows", async () => { mockOpenAIForwardCompatDiscovery("gpt-5.4-mini", { contextWindow: 64_000, input: ["text"], }); - const result = resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4452,7 +4458,7 @@ describe("resolveModel", () => { }); }); - it("rejects stale direct openai gpt-5.3-codex-spark discovery rows", () => { + it("rejects stale direct openai gpt-5.3-codex-spark discovery rows", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.3-codex-spark", @@ -4465,7 +4471,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.3-codex-spark", "/tmp/agent"); expect(result.model).toBeUndefined(); expect(result.error).toBe( @@ -4473,7 +4479,7 @@ describe("resolveModel", () => { ); }); - it("applies provider overrides to openai gpt-5.4 forward-compat models", () => { + it("applies provider overrides to openai gpt-5.4 forward-compat models", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.4", @@ -4497,7 +4503,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4511,7 +4517,7 @@ describe("resolveModel", () => { }); }); - it("applies configured overrides to github-copilot dynamic models", () => { + it("applies configured overrides to github-copilot dynamic models", async () => { const cfg = makeOpenClawConfigFixture({ models: { providers: { @@ -4533,7 +4539,7 @@ describe("resolveModel", () => { }, }); - const result = resolveModelForTest("github-copilot", "gpt-5.4-mini", "/tmp/agent", cfg); + const result = await resolveModelForTest("github-copilot", "gpt-5.4-mini", "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4551,8 +4557,8 @@ describe("resolveModel", () => { }); }); - it("resolves github-copilot Claude dynamic models to anthropic-messages by default", () => { - const result = resolveModelForTest("github-copilot", "claude-sonnet-4.6", "/tmp/agent"); + it("resolves github-copilot Claude dynamic models to anthropic-messages by default", async () => { + const result = await resolveModelForTest("github-copilot", "claude-sonnet-4.6", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4568,7 +4574,7 @@ describe("resolveModel", () => { { modelId: "gpt-5.4-mini", expectedApi: "openai-responses" }, ] as const)( "preserves discovered $expectedApi transport for params-only github-copilot $modelId", - ({ modelId, expectedApi }) => { + async ({ modelId, expectedApi }) => { mockDiscoveredModel(discoverModels, { provider: "github-copilot", modelId, @@ -4591,7 +4597,7 @@ describe("resolveModel", () => { }, }; - const result = resolveModelForTest("github-copilot", modelId, "/tmp/agent", cfg); + const result = await resolveModelForTest("github-copilot", modelId, "/tmp/agent", cfg); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4603,8 +4609,8 @@ describe("resolveModel", () => { }, ); - it("builds an openai fallback for gpt-5.5 when the live catalog cache is cold", () => { - const result = resolveModelForTest("openai", "gpt-5.5", "/tmp/agent"); + it("builds an openai fallback for gpt-5.5 when the live catalog cache is cold", async () => { + const result = await resolveModelForTest("openai", "gpt-5.5", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4623,7 +4629,7 @@ describe("resolveModel", () => { }); }); - it("builds an openai fallback for gpt-5.4 mini from the gpt-5.4-mini template", () => { + it("builds an openai fallback for gpt-5.4 mini from the gpt-5.4-mini template", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.4-mini", @@ -4640,7 +4646,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4-mini", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4655,7 +4661,7 @@ describe("resolveModel", () => { }); }); - it("builds an openai fallback for gpt-5.4 nano from the gpt-5.4-nano template", () => { + it("builds an openai fallback for gpt-5.4 nano from the gpt-5.4-nano template", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.4-nano", @@ -4672,7 +4678,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("openai", "gpt-5.4-nano", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4-nano", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4687,7 +4693,7 @@ describe("resolveModel", () => { }); }); - it("normalizes stale native openai gpt-5.4 completions transport to responses", () => { + it("normalizes stale native openai gpt-5.4 completions transport to responses", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.4", @@ -4700,7 +4706,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4711,7 +4717,7 @@ describe("resolveModel", () => { }); }); - it("keeps proxied openai completions transport untouched", () => { + it("keeps proxied openai completions transport untouched", async () => { mockDiscoveredModel(discoverModels, { provider: "openai", modelId: "gpt-5.4", @@ -4724,7 +4730,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); + const result = await resolveModelForTest("openai", "gpt-5.4", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4735,7 +4741,7 @@ describe("resolveModel", () => { }); }); - it("normalizes stale native xai completions transport to responses", () => { + it("normalizes stale native xai completions transport to responses", async () => { mockDiscoveredModel(discoverModels, { provider: "xai", modelId: "grok-4.20-0309-reasoning", @@ -4748,7 +4754,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModelForTest("xai", "grok-4.20-0309-reasoning", "/tmp/agent"); + const result = await resolveModelForTest("xai", "grok-4.20-0309-reasoning", "/tmp/agent"); expect(result.error).toBeUndefined(); expectRecordFields(result.model, { @@ -4759,7 +4765,7 @@ describe("resolveModel", () => { }); }); - it("normalizes stale native xai completions transport after plugin model normalization", () => { + it("normalizes stale native xai completions transport after plugin model normalization", async () => { mockDiscoveredModel(discoverModels, { provider: "xai", modelId: "grok-4.3", @@ -4772,7 +4778,7 @@ describe("resolveModel", () => { }), }); - const result = resolveModel("xai", "grok-4.3-latest", "/tmp/agent", undefined, { + const result = await resolveModelAsync("xai", "grok-4.3-latest", "/tmp/agent", undefined, { authStorage: { mocked: true } as never, modelRegistry: discoverModels({ mocked: true } as never, "/tmp/agent"), runtimeHooks: { diff --git a/src/agents/embedded-agent-runner/model.ts b/src/agents/embedded-agent-runner/model.ts index b65e4ddd80d8..96c46116edff 100644 --- a/src/agents/embedded-agent-runner/model.ts +++ b/src/agents/embedded-agent-runner/model.ts @@ -10,7 +10,6 @@ import { modelKey } from "../model-ref-shared.js"; import { findNormalizedProviderValue, normalizeProviderId } from "../model-selection.js"; import { buildSuppressedBuiltInModelError } from "../model-suppression.js"; import { - PreparedModelRuntimeOwnerNotPublishedError, getPreparedModelRuntimeSnapshot, loadPreparedModelRuntimeSnapshot, type PreparedModelRuntimeSnapshot, @@ -111,107 +110,6 @@ function resolvePreparedAgentSnapshot( return getPreparedModelRuntimeSnapshot({ ...base, workspaceDir: derivedWorkspaceDir }); } -export function resolveModel( - provider: string, - modelId: string, - agentDir?: string, - cfg?: OpenClawConfig, - options?: CommonModelResolutionOptions, -): { - model?: Model; - error?: string; - authStorage: AuthStorage; - modelRegistry: ModelRegistry; -} { - const resolvedAgentDir = agentDir ?? resolveDefaultAgentDir(cfg ?? {}); - const derivedWorkspaceDir = resolveModelWorkspaceDir( - cfg, - options?.workspaceDir, - options?.agentId, - ); - const preparedSnapshot = - !options?.authStorage || !options?.modelRegistry - ? resolvePreparedAgentSnapshot( - resolvedAgentDir, - cfg, - options?.workspaceDir, - derivedWorkspaceDir, - options?.agentId, - ) - : undefined; - if ((!options?.authStorage || !options?.modelRegistry) && !preparedSnapshot) { - // Synchronous callers must enter through a lifecycle that already published discovery. - // Falling back to an empty registry turns a stale/pending generation into a false model miss. - throw new PreparedModelRuntimeOwnerNotPublishedError( - `prepared model runtime is not published for synchronous model resolution (${resolvedAgentDir}); use resolveModelAsync before lifecycle publication`, - ); - } - const preparedModelRuntime = preparedSnapshot; - const resolve = () => { - const workspaceDir = - options?.workspaceDir ?? preparedModelRuntime?.workspaceDir ?? derivedWorkspaceDir; - const normalizedRef = normalizeProviderModelRef({ provider, modelId, cfg, workspaceDir }); - const preparedStores = - !options?.authStorage || !options?.modelRegistry - ? preparedModelRuntime?.createStores() - : undefined; - const authStorage = options?.authStorage ?? preparedStores!.authStorage; - const modelRegistry = - options?.modelRegistry ?? - (options?.authStorage - ? preparedStores!.modelRegistry.fork(authStorage) - : preparedStores!.modelRegistry); - const runtimeHooks = resolveRuntimeHooks(options); - let staticCatalogResolved = false; - let staticCatalogModel: StaticCatalogFallbackModel | undefined; - const getStaticCatalogModel = () => { - if (!staticCatalogResolved) { - staticCatalogResolved = true; - staticCatalogModel = resolveBundledStaticCatalogModel({ - provider: normalizedRef.provider, - modelId: normalizedRef.model, - cfg, - workspaceDir, - includeRuntimeDiscovery: true, - }); - } - return staticCatalogModel; - }; - const model = resolveModelWithPreparedRegistry({ - provider: normalizedRef.provider, - modelId: normalizedRef.model, - modelRegistry, - cfg, - agentDir: resolvedAgentDir, - manifestAlias: normalizedRef.manifestAlias, - workspaceDir, - authProfileId: options?.authProfileId, - authProfileMode: options?.authProfileMode, - preferredProfile: options?.preferredProfile, - runtimeHooks, - getStaticCatalogModel, - }); - if (model) { - return { model, authStorage, modelRegistry }; - } - return { - error: buildUnknownModelError({ - provider: normalizedRef.provider, - modelId: normalizedRef.model, - cfg, - agentDir: resolvedAgentDir, - workspaceDir, - runtimeHooks, - }), - authStorage, - modelRegistry, - }; - }; - return preparedModelRuntime - ? withPluginRuntimeGenerationScope(preparedModelRuntime, resolve) - : resolve(); -} - export async function resolveModelAsync( provider: string, modelId: string, diff --git a/src/agents/tools-effective-inventory.runtime-model.test.ts b/src/agents/tools-effective-inventory.runtime-model.test.ts index 0955c337201a..d02ba3a81ea9 100644 --- a/src/agents/tools-effective-inventory.runtime-model.test.ts +++ b/src/agents/tools-effective-inventory.runtime-model.test.ts @@ -1,15 +1,25 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { makeOpenClawConfigFixture } from "./embedded-agent-runner/model.test-harness.js"; const runtimeMocks = vi.hoisted(() => { - class OwnerNotPublishedError extends Error {} + const createLease = (owner: string) => { + const authStorage = { owner }; + const modelRegistry = { owner }; + return { + authStorage, + modelRegistry, + snapshot: { + createStores: vi.fn(() => ({ authStorage, modelRegistry })), + }, + release: vi.fn(), + }; + }; + const requestLease = createLease("request"); + const publishedLease = createLease("published"); return { - OwnerNotPublishedError, - authStorage: {}, - modelRegistry: {}, - preparedSnapshot: { - createStores: vi.fn(), - }, - release: vi.fn(), + acquire: vi.fn(async () => requestLease), + publishedLease, + requestLease, resolveModelAsync: vi.fn(async () => ({ model: { id: "chat-latest", @@ -19,31 +29,24 @@ const runtimeMocks = vi.hoisted(() => { baseUrl: "https://api.openai.com/v1", }, })), + staticCatalogModel: vi.fn(), }; }); -runtimeMocks.preparedSnapshot.createStores.mockReturnValue({ - authStorage: runtimeMocks.authStorage, - modelRegistry: runtimeMocks.modelRegistry, -}); - vi.mock("./prepared-model-runtime.js", () => ({ - PreparedModelRuntimeOwnerNotPublishedError: runtimeMocks.OwnerNotPublishedError, - acquireReadOnlyPreparedModelRuntime: vi.fn(async () => ({ - snapshot: runtimeMocks.preparedSnapshot, - release: runtimeMocks.release, - })), + acquireReadOnlyPreparedModelRuntime: runtimeMocks.acquire, })); vi.mock("./embedded-agent-runner/model.js", () => ({ - resolveModel: () => { - throw new runtimeMocks.OwnerNotPublishedError("owner missing"); - }, resolveModelAsync: runtimeMocks.resolveModelAsync, })); vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({ - resolveBundledStaticCatalogModel: () => undefined, + resolveBundledStaticCatalogModel: runtimeMocks.staticCatalogModel, +})); + +vi.mock("../plugins/provider-runtime.js", () => ({ + normalizeProviderTransportWithPlugin: () => undefined, })); vi.mock("./agent-scope.js", () => ({ @@ -54,13 +57,28 @@ vi.mock("./agent-scope.js", () => ({ })); describe("resolveEffectiveToolInventoryRuntimeModelContextAsync", () => { - it("prepares dynamic model context when no lifecycle owner exists", async () => { + beforeEach(() => { + runtimeMocks.acquire.mockReset().mockResolvedValue(runtimeMocks.requestLease); + runtimeMocks.requestLease.snapshot.createStores.mockClear(); + runtimeMocks.publishedLease.snapshot.createStores.mockClear(); + runtimeMocks.resolveModelAsync.mockClear(); + runtimeMocks.requestLease.release.mockClear(); + runtimeMocks.publishedLease.release.mockClear(); + runtimeMocks.staticCatalogModel.mockReset(); + }); + + it.each([ + { owner: "request-owned", lease: runtimeMocks.requestLease }, + { owner: "published", lease: runtimeMocks.publishedLease }, + ])("prepares dynamic model context with a $owner runtime lease", async ({ lease }) => { + runtimeMocks.acquire.mockResolvedValueOnce(lease); const { resolveEffectiveToolInventoryRuntimeModelContextAsync } = await import("./tools-effective-inventory.js"); + const cfg = makeOpenClawConfigFixture(); await expect( resolveEffectiveToolInventoryRuntimeModelContextAsync({ - cfg: {}, + cfg, agentId: "main", agentDir: "/tmp/agents/main/agent", workspaceDir: "/tmp/workspace-main", @@ -75,15 +93,120 @@ describe("resolveEffectiveToolInventoryRuntimeModelContextAsync", () => { "openai", "chat-latest", "/tmp/agents/main/agent", - {}, + cfg, { agentId: "main", workspaceDir: "/tmp/workspace-main", - authStorage: runtimeMocks.authStorage, - modelRegistry: runtimeMocks.modelRegistry, - preparedModelRuntime: runtimeMocks.preparedSnapshot, + authStorage: lease.authStorage, + modelRegistry: lease.modelRegistry, + preparedModelRuntime: lease.snapshot, }, ); - expect(runtimeMocks.release).toHaveBeenCalledTimes(1); + expect(runtimeMocks.acquire).toHaveBeenCalledWith({ + agentId: "main", + agentDir: "/tmp/agents/main/agent", + config: cfg, + workspaceDir: "/tmp/workspace-main", + }); + expect(lease.release).toHaveBeenCalledTimes(1); + }); + + it.each([ + { modelProvider: "", modelId: "chat-latest" }, + { modelProvider: "openai", modelId: " " }, + ])("skips runtime preparation for invalid model input", async (input) => { + const { resolveEffectiveToolInventoryRuntimeModelContextAsync } = + await import("./tools-effective-inventory.js"); + + await expect( + resolveEffectiveToolInventoryRuntimeModelContextAsync({ + cfg: {}, + ...input, + }), + ).resolves.toEqual({}); + expect(runtimeMocks.acquire).not.toHaveBeenCalled(); + expect(runtimeMocks.resolveModelAsync).not.toHaveBeenCalled(); + expect(runtimeMocks.requestLease.release).not.toHaveBeenCalled(); + }); + + it("uses configured model context without acquiring a runtime lease", async () => { + const { resolveEffectiveToolInventoryRuntimeModelContextAsync } = + await import("./tools-effective-inventory.js"); + const cfg = makeOpenClawConfigFixture({ + models: { + providers: { + custom: { + api: "anthropic-messages", + models: [ + { + id: "configured", + name: "Configured", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 1024, + }, + ], + }, + }, + }, + }); + + await expect( + resolveEffectiveToolInventoryRuntimeModelContextAsync({ + cfg, + modelProvider: "custom", + modelId: "configured", + }), + ).resolves.toMatchObject({ + modelApi: "anthropic-messages", + runtimeModel: { id: "configured", provider: "custom" }, + }); + expect(runtimeMocks.acquire).not.toHaveBeenCalled(); + expect(runtimeMocks.resolveModelAsync).not.toHaveBeenCalled(); + expect(runtimeMocks.requestLease.release).not.toHaveBeenCalled(); + }); + + it("uses bundled model context without acquiring a runtime lease", async () => { + runtimeMocks.staticCatalogModel.mockReturnValue({ + id: "bundled", + name: "Bundled", + provider: "openai", + api: "openai-responses", + baseUrl: "https://api.openai.com/v1", + }); + const { resolveEffectiveToolInventoryRuntimeModelContextAsync } = + await import("./tools-effective-inventory.js"); + + await expect( + resolveEffectiveToolInventoryRuntimeModelContextAsync({ + cfg: {}, + modelProvider: "openai", + modelId: "bundled", + }), + ).resolves.toMatchObject({ + modelApi: "openai-responses", + runtimeModel: { id: "bundled", provider: "openai" }, + }); + expect(runtimeMocks.acquire).not.toHaveBeenCalled(); + expect(runtimeMocks.resolveModelAsync).not.toHaveBeenCalled(); + expect(runtimeMocks.requestLease.release).not.toHaveBeenCalled(); + }); + + it("releases the runtime lease when dynamic model resolution fails", async () => { + const failure = new Error("dynamic model failed"); + runtimeMocks.resolveModelAsync.mockRejectedValueOnce(failure); + const { resolveEffectiveToolInventoryRuntimeModelContextAsync } = + await import("./tools-effective-inventory.js"); + + await expect( + resolveEffectiveToolInventoryRuntimeModelContextAsync({ + cfg: {}, + modelProvider: "openai", + modelId: "chat-latest", + }), + ).rejects.toBe(failure); + expect(runtimeMocks.requestLease.release).toHaveBeenCalledTimes(1); }); }); diff --git a/src/agents/tools-effective-inventory.test.ts b/src/agents/tools-effective-inventory.test.ts index 4c06bdbb77c9..ee075ac7165f 100644 --- a/src/agents/tools-effective-inventory.test.ts +++ b/src/agents/tools-effective-inventory.test.ts @@ -3,6 +3,7 @@ * Verifies grouped tool sources, plugin registry inputs, and session-context filters. */ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import type { ProviderRuntimeModel } from "../plugins/provider-runtime-model.types.js"; import { createEmptyPluginRegistry } from "../plugins/registry-empty.js"; import { setActivePluginRegistry } from "../plugins/runtime.js"; import type { createOpenClawCodingTools } from "./agent-tools.js"; @@ -39,7 +40,6 @@ const effectiveInventoryState = vi.hoisted(() => ({ effectivePolicy: {} as { profile?: string; providerProfile?: string }, normalizeToolsMock: vi.fn((options: { tools: AnyAgentTool[] }) => options.tools), staticCatalogModelMock: vi.fn((_options: unknown) => undefined as unknown), - dynamicModelMock: vi.fn((_options: unknown) => undefined as unknown), normalizeTransportMock: vi.fn((_options: unknown) => undefined as unknown), createToolsMock: vi.fn( (_options) => @@ -91,21 +91,7 @@ vi.mock("./embedded-agent-runner/model.static-catalog.js", () => ({ })); vi.mock("./embedded-agent-runner/model.js", () => ({ - resolveModel: ( - provider: unknown, - modelId: unknown, - agentDir: unknown, - cfg: unknown, - options: unknown, - ) => ({ - model: effectiveInventoryState.dynamicModelMock({ - provider, - modelId, - agentDir, - cfg, - options, - }), - }), + resolveModelAsync: vi.fn(), })); vi.mock("../plugins/provider-runtime.js", () => ({ @@ -133,7 +119,6 @@ async function loadHarness(options?: { effectiveInventoryState.normalizeToolsMock = options?.normalizeToolsMock ?? vi.fn((normalizeOptions) => normalizeOptions.tools); effectiveInventoryState.staticCatalogModelMock = vi.fn((_options: unknown) => undefined); - effectiveInventoryState.dynamicModelMock = vi.fn((_options: unknown) => undefined); effectiveInventoryState.normalizeTransportMock = vi.fn((_options: unknown) => undefined); effectiveInventoryState.createToolsMock = options?.createToolsMock ?? @@ -159,7 +144,6 @@ describe("resolveEffectiveToolInventory", () => { effectiveInventoryState.effectivePolicy = {}; effectiveInventoryState.normalizeToolsMock = vi.fn((options) => options.tools); effectiveInventoryState.staticCatalogModelMock = vi.fn((_options: unknown) => undefined); - effectiveInventoryState.dynamicModelMock = vi.fn((_options: unknown) => undefined); effectiveInventoryState.normalizeTransportMock = vi.fn((_options: unknown) => undefined); effectiveInventoryState.createToolsMock = vi.fn( (_options) => effectiveInventoryState.tools, @@ -778,7 +762,7 @@ describe("resolveEffectiveToolInventory", () => { ); }); - it("uses dynamic provider model context before quarantining runtime-normalized tools", async () => { + it("uses prepared model context before quarantining runtime-normalized tools", async () => { const normalizeToolsMock = vi.fn((options: { tools: AnyAgentTool[]; modelApi?: string }) => options.tools.map((entry) => entry.name === "parameter_free" && options.modelApi === "openai-responses" @@ -808,18 +792,25 @@ describe("resolveEffectiveToolInventory", () => { normalizeToolsMock, }, ); - effectiveInventoryState.dynamicModelMock.mockReturnValue({ + const runtimeModel: ProviderRuntimeModel = { id: "chat-latest", name: "chat-latest", provider: "openai", api: "openai-responses", baseUrl: "https://api.openai.com/v1", - }); + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 8192, + maxTokens: 1024, + }; const result = resolveEffectiveToolInventoryInner({ cfg: {}, modelProvider: "openai", modelId: "chat-latest", + modelApi: runtimeModel.api, + runtimeModel, }); expect(result.groups[0]?.tools[0]).toMatchObject({ @@ -828,14 +819,6 @@ describe("resolveEffectiveToolInventory", () => { pluginId: "normalized-plugin", }); expect(result.notices).toBeUndefined(); - expect(effectiveInventoryState.dynamicModelMock).toHaveBeenCalledWith( - expect.objectContaining({ - provider: "openai", - modelId: "chat-latest", - agentDir: "/tmp/agents/main/agent", - options: expect.objectContaining({ workspaceDir: "/tmp/workspace-main" }), - }), - ); expect(normalizeToolsMock).toHaveBeenCalledWith( expect.objectContaining({ provider: "openai", diff --git a/src/agents/tools-effective-inventory.ts b/src/agents/tools-effective-inventory.ts index 187a95db56f2..d0e99673b66e 100644 --- a/src/agents/tools-effective-inventory.ts +++ b/src/agents/tools-effective-inventory.ts @@ -18,13 +18,10 @@ import { normalizeProviderTransportWithPlugin } from "../plugins/provider-runtim import { resolveAgentDir, resolveAgentWorkspaceDir, resolveSessionAgentId } from "./agent-scope.js"; import { createOpenClawCodingTools } from "./agent-tools.js"; import { resolveEffectiveToolPolicy } from "./agent-tools.policy.js"; -import { resolveModel, resolveModelAsync } from "./embedded-agent-runner/model.js"; +import { resolveModelAsync } from "./embedded-agent-runner/model.js"; import { resolveBundledStaticCatalogModel } from "./embedded-agent-runner/model.static-catalog.js"; import { normalizeStaticProviderModelId } from "./model-ref-shared.js"; -import { - PreparedModelRuntimeOwnerNotPublishedError, - acquireReadOnlyPreparedModelRuntime, -} from "./prepared-model-runtime.js"; +import { acquireReadOnlyPreparedModelRuntime } from "./prepared-model-runtime.js"; import { normalizeToolPolicyName } from "./tool-policy.js"; import { buildRuntimeCompatibleToolInventory } from "./tools-effective-inventory-build.js"; import { buildEffectiveToolInventoryGroups } from "./tools-effective-inventory-groups.js"; @@ -156,29 +153,8 @@ function resolveConfiguredFallbackApi( : "openai-responses"; } -function resolveDynamicRuntimeModelContext(params: { - cfg: OpenClawConfig; - agentId?: string; - agentDir?: string; - workspaceDir?: string; - provider: string; - modelId: string; -}): { modelApi?: string; runtimeModel?: ProviderRuntimeModel } { - const runtimeModel = resolveModel(params.provider, params.modelId, params.agentDir, params.cfg, { - agentId: params.agentId, - workspaceDir: params.workspaceDir, - }).model as ProviderRuntimeModel | undefined; - if (!runtimeModel) { - return {}; - } - return { - modelApi: runtimeModel.api, - runtimeModel, - }; -} - -/** Resolves the runtime model metadata needed to filter model-compatible tools. */ -function resolveEffectiveToolInventoryRuntimeModelContext(params: { +/** Resolves configured or bundled metadata without starting provider discovery. */ +function resolveStaticToolInventoryRuntimeModelContext(params: { cfg: OpenClawConfig; agentId?: string; agentDir?: string; @@ -241,14 +217,7 @@ function resolveEffectiveToolInventoryRuntimeModelContext(params: { }; } if (!bundledStaticModel) { - return resolveDynamicRuntimeModelContext({ - cfg: params.cfg, - agentId, - agentDir: params.agentDir, - workspaceDir, - provider, - modelId, - }); + return {}; } const runtimeModel = applyProviderTransportNormalization({ cfg: params.cfg, @@ -268,14 +237,11 @@ function resolveEffectiveToolInventoryRuntimeModelContext(params: { /** Resolves dynamic model metadata after publishing a request-owned read snapshot when needed. */ export async function resolveEffectiveToolInventoryRuntimeModelContextAsync( - params: Parameters[0], -): Promise> { - try { - return resolveEffectiveToolInventoryRuntimeModelContext(params); - } catch (error) { - if (!(error instanceof PreparedModelRuntimeOwnerNotPublishedError)) { - throw error; - } + params: Parameters[0], +): Promise> { + const staticContext = resolveStaticToolInventoryRuntimeModelContext(params); + if (staticContext.runtimeModel) { + return staticContext; } const provider = normalizeProviderId(params.modelProvider ?? ""); @@ -352,7 +318,7 @@ export function resolveEffectiveToolInventory( modelApi: params.modelApi ?? params.runtimeModel?.api, runtimeModel: params.runtimeModel, } - : resolveEffectiveToolInventoryRuntimeModelContext({ + : resolveStaticToolInventoryRuntimeModelContext({ cfg: params.cfg, agentId, agentDir,