mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
refactor(agents): resolve runtime models through prepared owner (#130007)
* refactor(agents): resolve runtime models through prepared owner * chore: tighten agent inventory assertion baseline
This commit is contained in:
committed by
GitHub
parent
33a24b6d8e
commit
0102fcb7dd
@@ -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
|
||||
|
||||
+75
-70
@@ -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<ModelProviderConfig>) {
|
||||
async function resolveAnthropicModelWithProviderOverrides(overrides: Partial<ModelProviderConfig>) {
|
||||
// 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<ModelProv
|
||||
templateModel: createAnthropicTemplateModel(),
|
||||
});
|
||||
|
||||
return resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", {
|
||||
return await resolveModelForTest("anthropic", "claude-sonnet-4-5", "/tmp/agent", {
|
||||
models: {
|
||||
providers: {
|
||||
anthropic: overrides,
|
||||
@@ -201,9 +202,13 @@ function resolveAnthropicModelWithProviderOverrides(overrides: Partial<ModelProv
|
||||
}
|
||||
|
||||
describe("resolveModel forward-compat errors and overrides", () => {
|
||||
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<string, string> }).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");
|
||||
});
|
||||
|
||||
@@ -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<typeof createModelGenerationFixture>) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<typeof createOpenClawCodingTools>(
|
||||
(_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<typeof createOpenClawCodingTools>(
|
||||
(_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",
|
||||
|
||||
@@ -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<typeof resolveEffectiveToolInventoryRuntimeModelContext>[0],
|
||||
): Promise<ReturnType<typeof resolveEffectiveToolInventoryRuntimeModelContext>> {
|
||||
try {
|
||||
return resolveEffectiveToolInventoryRuntimeModelContext(params);
|
||||
} catch (error) {
|
||||
if (!(error instanceof PreparedModelRuntimeOwnerNotPublishedError)) {
|
||||
throw error;
|
||||
}
|
||||
params: Parameters<typeof resolveStaticToolInventoryRuntimeModelContext>[0],
|
||||
): Promise<ReturnType<typeof resolveStaticToolInventoryRuntimeModelContext>> {
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user