diff --git a/extensions/google/provider-models.ts b/extensions/google/provider-models.ts index eddda4a9f9a..ddb0446c2b9 100644 --- a/extensions/google/provider-models.ts +++ b/extensions/google/provider-models.ts @@ -1,39 +1,14 @@ +import { cloneFirstTemplateModel } from "../../src/plugins/provider-model-helpers.js"; import type { ProviderResolveDynamicModelContext, ProviderRuntimeModel, } from "openclaw/plugin-sdk/core"; -import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models"; const GEMINI_3_1_PRO_PREFIX = "gemini-3.1-pro"; const GEMINI_3_1_FLASH_PREFIX = "gemini-3.1-flash"; const GEMINI_3_1_PRO_TEMPLATE_IDS = ["gemini-3-pro-preview"] as const; const GEMINI_3_1_FLASH_TEMPLATE_IDS = ["gemini-3-flash-preview"] as const; -function cloneFirstTemplateModel(params: { - providerId: string; - modelId: string; - templateIds: readonly string[]; - ctx: ProviderResolveDynamicModelContext; -}): ProviderRuntimeModel | undefined { - const trimmedModelId = params.modelId.trim(); - for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { - const template = params.ctx.modelRegistry.find( - params.providerId, - templateId, - ) as ProviderRuntimeModel | null; - if (!template) { - continue; - } - return normalizeModelCompat({ - ...template, - id: trimmedModelId, - name: trimmedModelId, - reasoning: true, - } as ProviderRuntimeModel); - } - return undefined; -} - export function resolveGoogle31ForwardCompatModel(params: { providerId: string; ctx: ProviderResolveDynamicModelContext; @@ -55,6 +30,7 @@ export function resolveGoogle31ForwardCompatModel(params: { modelId: trimmed, templateIds, ctx: params.ctx, + patch: { reasoning: true }, }); } diff --git a/extensions/openai/shared.ts b/extensions/openai/shared.ts index 2b67454fc07..673a6bdeb24 100644 --- a/extensions/openai/shared.ts +++ b/extensions/openai/shared.ts @@ -1,8 +1,5 @@ -import type { - ProviderResolveDynamicModelContext, - ProviderRuntimeModel, -} from "openclaw/plugin-sdk/core"; -import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-models"; +import { findCatalogTemplate } from "../../src/plugins/provider-catalog.js"; +import { cloneFirstTemplateModel } from "../../src/plugins/provider-model-helpers.js"; export const OPENAI_API_BASE_URL = "https://api.openai.com/v1"; @@ -22,44 +19,5 @@ export function isOpenAIApiBaseUrl(baseUrl?: string): boolean { return /^https?:\/\/api\.openai\.com(?:\/v1)?\/?$/i.test(trimmed); } -export function cloneFirstTemplateModel(params: { - providerId: string; - modelId: string; - templateIds: readonly string[]; - ctx: ProviderResolveDynamicModelContext; - patch?: Partial; -}): ProviderRuntimeModel | undefined { - const trimmedModelId = params.modelId.trim(); - for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { - const template = params.ctx.modelRegistry.find( - params.providerId, - templateId, - ) as ProviderRuntimeModel | null; - if (!template) { - continue; - } - return normalizeModelCompat({ - ...template, - id: trimmedModelId, - name: trimmedModelId, - ...params.patch, - } as ProviderRuntimeModel); - } - return undefined; -} - -export function findCatalogTemplate(params: { - entries: ReadonlyArray<{ provider: string; id: string }>; - providerId: string; - templateIds: readonly string[]; -}) { - return params.templateIds - .map((templateId) => - params.entries.find( - (entry) => - entry.provider.toLowerCase() === params.providerId.toLowerCase() && - entry.id.toLowerCase() === templateId.toLowerCase(), - ), - ) - .find((entry) => entry !== undefined); -} +export { cloneFirstTemplateModel }; +export { findCatalogTemplate }; diff --git a/src/plugins/provider-model-helpers.test.ts b/src/plugins/provider-model-helpers.test.ts new file mode 100644 index 00000000000..905195775fe --- /dev/null +++ b/src/plugins/provider-model-helpers.test.ts @@ -0,0 +1,56 @@ +import type { ModelRegistry } from "@mariozechner/pi-coding-agent"; +import { describe, expect, it } from "vitest"; +import { cloneFirstTemplateModel } from "./provider-model-helpers.js"; +import type { ProviderResolveDynamicModelContext, ProviderRuntimeModel } from "./types.js"; + +function createContext(models: ProviderRuntimeModel[]): ProviderResolveDynamicModelContext { + return { + provider: "test-provider", + modelId: "next-model", + modelRegistry: { + find(providerId: string, modelId: string) { + return ( + models.find((model) => model.provider === providerId && model.id === modelId) ?? null + ); + }, + } as ModelRegistry, + }; +} + +describe("cloneFirstTemplateModel", () => { + it("clones the first matching template and applies patches", () => { + const model = cloneFirstTemplateModel({ + providerId: "test-provider", + modelId: " next-model ", + templateIds: ["missing", "template-a", "template-b"], + ctx: createContext([ + { + id: "template-a", + name: "Template A", + provider: "test-provider", + api: "openai-completions", + } as ProviderRuntimeModel, + ]), + patch: { reasoning: true }, + }); + + expect(model).toMatchObject({ + id: "next-model", + name: "next-model", + provider: "test-provider", + api: "openai-completions", + reasoning: true, + }); + }); + + it("returns undefined when no template exists", () => { + const model = cloneFirstTemplateModel({ + providerId: "test-provider", + modelId: "next-model", + templateIds: ["missing"], + ctx: createContext([]), + }); + + expect(model).toBeUndefined(); + }); +}); diff --git a/src/plugins/provider-model-helpers.ts b/src/plugins/provider-model-helpers.ts new file mode 100644 index 00000000000..8ffd8d18be7 --- /dev/null +++ b/src/plugins/provider-model-helpers.ts @@ -0,0 +1,28 @@ +import { normalizeModelCompat } from "../agents/model-compat.js"; +import type { ProviderResolveDynamicModelContext, ProviderRuntimeModel } from "./types.js"; + +export function cloneFirstTemplateModel(params: { + providerId: string; + modelId: string; + templateIds: readonly string[]; + ctx: ProviderResolveDynamicModelContext; + patch?: Partial; +}): ProviderRuntimeModel | undefined { + const trimmedModelId = params.modelId.trim(); + for (const templateId of [...new Set(params.templateIds)].filter(Boolean)) { + const template = params.ctx.modelRegistry.find( + params.providerId, + templateId, + ) as ProviderRuntimeModel | null; + if (!template) { + continue; + } + return normalizeModelCompat({ + ...template, + id: trimmedModelId, + name: trimmedModelId, + ...params.patch, + } as ProviderRuntimeModel); + } + return undefined; +}