diff --git a/src/agents/auth-profiles.cooldown-auto-expiry.test.ts b/src/agents/auth-profiles.cooldown-auto-expiry.test.ts index 90c0609f731..c61fc4c427d 100644 --- a/src/agents/auth-profiles.cooldown-auto-expiry.test.ts +++ b/src/agents/auth-profiles.cooldown-auto-expiry.test.ts @@ -1,8 +1,12 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; import { isProfileInCooldown } from "./auth-profiles/usage-state.js"; +vi.mock("./provider-auth-aliases.js", () => ({ + resolveProviderIdForAuth: (provider: string) => provider.trim().toLowerCase(), +})); + /** * Integration tests for cooldown auto-expiry through resolveAuthProfileOrder. * Verifies that profiles with expired cooldowns are treated as available and diff --git a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts index d8fcb094e21..ce4d92e4eb5 100644 --- a/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts +++ b/src/agents/auth-profiles.resolve-auth-profile-order.does-not-prioritize-lastgood-round-robin-ordering.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { ANTHROPIC_CFG, ANTHROPIC_STORE, @@ -6,6 +6,11 @@ import { import { resolveAuthProfileOrder } from "./auth-profiles/order.js"; import type { AuthProfileStore } from "./auth-profiles/types.js"; +vi.mock("./provider-auth-aliases.js", () => ({ + resolveProviderIdForAuth: (provider: string) => + provider.trim().toLowerCase() === "z.ai" ? "zai" : provider.trim().toLowerCase(), +})); + function makeApiKeyStore(provider: string, profileIds: string[]): AuthProfileStore { return { version: 1, diff --git a/src/agents/model-auth-markers.ts b/src/agents/model-auth-markers.ts index d6bbaf8e3d4..547b6d6160b 100644 --- a/src/agents/model-auth-markers.ts +++ b/src/agents/model-auth-markers.ts @@ -1,5 +1,5 @@ import type { SecretRefSource } from "../config/types.secrets.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; +import { listOpenClawPluginManifestMetadata } from "../plugins/manifest-metadata-scan.js"; import { listKnownProviderEnvApiKeyNames } from "./model-auth-env-vars.js"; export const MINIMAX_OAUTH_MARKER = "minimax-oauth"; @@ -35,6 +35,13 @@ const LEGACY_ENV_API_KEY_MARKERS = [ "MINIMAX_CODE_PLAN_KEY", ]; +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value.map((entry) => (typeof entry === "string" ? entry.trim() : "")).filter(Boolean); +} + function listKnownEnvApiKeyMarkers(): Set { knownEnvApiKeyMarkersCache ??= new Set([ ...listKnownProviderEnvApiKeyNames(), @@ -48,8 +55,10 @@ export function listKnownNonSecretApiKeyMarkers(): string[] { knownNonSecretApiKeyMarkersCache ??= [ ...new Set([ ...CORE_NON_SECRET_API_KEY_MARKERS, - ...loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }).plugins.flatMap( - (plugin) => (plugin.origin === "bundled" ? (plugin.nonSecretAuthMarkers ?? []) : []), + ...listOpenClawPluginManifestMetadata().flatMap((plugin) => + plugin.origin === "bundled" + ? normalizeStringList(plugin.manifest.nonSecretAuthMarkers) + : [], ), ]), ]; diff --git a/src/agents/provider-attribution.ts b/src/agents/provider-attribution.ts index b5f1da16ef3..51941d9ed0c 100644 --- a/src/agents/provider-attribution.ts +++ b/src/agents/provider-attribution.ts @@ -1,5 +1,6 @@ -import { loadPluginManifestRegistryForPluginRegistry } from "../plugins/plugin-registry.js"; +import { listOpenClawPluginManifestMetadata } from "../plugins/manifest-metadata-scan.js"; import { + normalizeLowercaseStringOrEmpty, normalizeOptionalLowercaseString, normalizeOptionalString, } from "../shared/string-coerce.js"; @@ -224,57 +225,123 @@ function isManifestProviderEndpointClass(value: string): value is ProviderEndpoi return MANIFEST_PROVIDER_ENDPOINT_CLASSES.has(value as ProviderEndpointClass); } +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((entry) => normalizeOptionalString(entry)) + .filter((entry): entry is string => entry !== undefined); +} + +function readManifestProviderEndpoints( + manifest: Record, +): ManifestProviderEndpointCacheEntry[] { + if (!Array.isArray(manifest.providerEndpoints)) { + return []; + } + const entries: ManifestProviderEndpointCacheEntry[] = []; + for (const rawEndpoint of manifest.providerEndpoints) { + if (!isRecord(rawEndpoint)) { + continue; + } + const endpointClassRaw = normalizeOptionalString(rawEndpoint.endpointClass); + if (!endpointClassRaw || !isManifestProviderEndpointClass(endpointClassRaw)) { + continue; + } + entries.push({ + endpointClass: endpointClassRaw, + hosts: normalizeStringList(rawEndpoint.hosts).map((host) => host.toLowerCase()), + hostSuffixes: normalizeStringList(rawEndpoint.hostSuffixes).map((host) => host.toLowerCase()), + normalizedBaseUrls: normalizeStringList(rawEndpoint.baseUrls) + .map((baseUrl) => normalizeComparableBaseUrl(baseUrl)) + .filter((baseUrl): baseUrl is string => baseUrl !== undefined), + ...(normalizeOptionalString(rawEndpoint.googleVertexRegion) + ? { googleVertexRegion: normalizeOptionalString(rawEndpoint.googleVertexRegion) } + : {}), + ...(normalizeOptionalString(rawEndpoint.googleVertexRegionHostSuffix) + ? { + googleVertexRegionHostSuffix: normalizeOptionalString( + rawEndpoint.googleVertexRegionHostSuffix, + ), + } + : {}), + }); + } + return entries; +} + +function readManifestProviderRequests( + manifest: Record, +): Array<[string, ManifestProviderRequestCacheEntry]> { + const providerRequest = manifest.providerRequest; + if (!isRecord(providerRequest) || !isRecord(providerRequest.providers)) { + return []; + } + const entries: Array<[string, ManifestProviderRequestCacheEntry]> = []; + for (const [providerRaw, requestRaw] of Object.entries(providerRequest.providers)) { + if (!isRecord(requestRaw)) { + continue; + } + const provider = normalizeLowercaseStringOrEmpty(providerRaw); + if (!provider) { + continue; + } + const compatibilityFamily = + normalizeOptionalString(requestRaw.compatibilityFamily) === "moonshot" + ? "moonshot" + : undefined; + const supportsStreamingUsage = isRecord(requestRaw.openAICompletions) + ? requestRaw.openAICompletions.supportsStreamingUsage + : undefined; + entries.push([ + provider, + { + ...(normalizeOptionalString(requestRaw.family) + ? { family: normalizeOptionalString(requestRaw.family) } + : {}), + ...(compatibilityFamily ? { compatibilityFamily } : {}), + ...(typeof supportsStreamingUsage === "boolean" + ? { supportsOpenAICompletionsStreamingUsageCompat: supportsStreamingUsage } + : {}), + }, + ]); + } + return entries; +} + +function collectManifestProviderEndpoints(): ManifestProviderEndpointCacheEntry[] { + const entries: ManifestProviderEndpointCacheEntry[] = []; + for (const { manifest } of listOpenClawPluginManifestMetadata()) { + entries.push(...readManifestProviderEndpoints(manifest)); + } + return entries; +} + +function collectManifestProviderRequests(): Map { + const entries = new Map(); + for (const { manifest } of listOpenClawPluginManifestMetadata()) { + for (const [provider, request] of readManifestProviderRequests(manifest)) { + entries.set(provider, request); + } + } + return entries; +} + function loadManifestProviderEndpointCache(): ManifestProviderEndpointCacheEntry[] { if (!manifestProviderEndpointCache) { - const registry = loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }); - const entries: ManifestProviderEndpointCacheEntry[] = []; - for (const plugin of registry.plugins) { - for (const endpoint of plugin.providerEndpoints ?? []) { - if (!isManifestProviderEndpointClass(endpoint.endpointClass)) { - continue; - } - entries.push({ - endpointClass: endpoint.endpointClass, - hosts: (endpoint.hosts ?? []).map((host) => host.toLowerCase()), - hostSuffixes: (endpoint.hostSuffixes ?? []).map((host) => host.toLowerCase()), - normalizedBaseUrls: (endpoint.baseUrls ?? []) - .map((baseUrl) => normalizeComparableBaseUrl(baseUrl)) - .filter((baseUrl): baseUrl is string => baseUrl !== undefined), - ...(endpoint.googleVertexRegion - ? { googleVertexRegion: endpoint.googleVertexRegion } - : {}), - ...(endpoint.googleVertexRegionHostSuffix - ? { googleVertexRegionHostSuffix: endpoint.googleVertexRegionHostSuffix } - : {}), - }); - } - } - manifestProviderEndpointCache = entries; + manifestProviderEndpointCache = collectManifestProviderEndpoints(); } return manifestProviderEndpointCache; } function loadManifestProviderRequestCache(): Map { if (!manifestProviderRequestCache) { - const registry = loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }); - const entries = new Map(); - for (const plugin of registry.plugins) { - for (const [provider, request] of Object.entries(plugin.providerRequest?.providers ?? {})) { - entries.set(provider, { - ...(request.family ? { family: request.family } : {}), - ...(request.compatibilityFamily - ? { compatibilityFamily: request.compatibilityFamily } - : {}), - ...(request.openAICompletions?.supportsStreamingUsage !== undefined - ? { - supportsOpenAICompletionsStreamingUsageCompat: - request.openAICompletions.supportsStreamingUsage, - } - : {}), - }); - } - } - manifestProviderRequestCache = entries; + manifestProviderRequestCache = collectManifestProviderRequests(); } return manifestProviderRequestCache; } diff --git a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts index 8c4acf6828c..b1d783ada22 100644 --- a/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts +++ b/src/agents/skills.build-workspace-skills-prompt.prefers-workspace-skills-managed-skills.test.ts @@ -1,5 +1,5 @@ import path from "node:path"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { withEnv } from "../test-utils/env.js"; import { createFixtureSuite } from "../test-utils/fixture-suite.js"; import { writeSkill } from "./skills.e2e-test-helpers.js"; @@ -7,6 +7,10 @@ import { createSyntheticSourceInfo } from "./skills/skill-contract.js"; import type { OpenClawSkillMetadata, SkillEntry } from "./skills/types.js"; import { buildWorkspaceSkillsPrompt } from "./skills/workspace.js"; +vi.mock("./skills/plugin-skills.js", () => ({ + resolvePluginSkillDirs: () => [], +})); + const fixtureSuite = createFixtureSuite("openclaw-skills-prompt-suite-"); beforeAll(async () => { diff --git a/src/plugin-sdk/provider-catalog-shared.ts b/src/plugin-sdk/provider-catalog-shared.ts index 968f3a7e9af..3c7e561701d 100644 --- a/src/plugin-sdk/provider-catalog-shared.ts +++ b/src/plugin-sdk/provider-catalog-shared.ts @@ -3,10 +3,10 @@ // Keep provider-owned exports out of this subpath so plugin loaders can import it // without recursing through provider-specific facades. +import { resolveProviderRequestCapabilities } from "../agents/provider-attribution.js"; import { findNormalizedProviderKey } from "../agents/provider-id.js"; import type { ModelDefinitionConfig } from "../config/types.models.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; -import { resolveProviderRequestCapabilities } from "./provider-http.js"; import type { ModelProviderConfig } from "./provider-model-shared.js"; export type { ProviderCatalogContext, ProviderCatalogResult } from "../plugins/types.js"; diff --git a/src/plugins/manifest-metadata-scan.ts b/src/plugins/manifest-metadata-scan.ts new file mode 100644 index 00000000000..30627eb5af4 --- /dev/null +++ b/src/plugins/manifest-metadata-scan.ts @@ -0,0 +1,188 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { parseJsonWithJson5Fallback } from "../utils/parse-json-compat.js"; + +type PluginManifestMetadataRecord = { + pluginDir: string; + manifest: Record; + origin?: string; +}; + +type CandidateDir = { + pluginDir: string; + rank: number; + order: number; + origin?: string; +}; + +const OPENCLAW_PACKAGE_ROOT = fileURLToPath(new URL("../..", import.meta.url)); +const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json"; + +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function normalizeTrimmedString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function resolveUserPath(value: string, env: NodeJS.ProcessEnv): string { + if (value === "~" || value.startsWith("~/")) { + const home = env.OPENCLAW_HOME ?? env.HOME ?? env.USERPROFILE ?? os.homedir(); + return path.join(home, value.slice(2)); + } + return path.resolve(value); +} + +function resolveStateDir(env: NodeJS.ProcessEnv): string { + const override = normalizeTrimmedString(env.OPENCLAW_STATE_DIR); + if (override) { + return resolveUserPath(override, env); + } + const home = env.OPENCLAW_HOME ?? env.HOME ?? env.USERPROFILE ?? os.homedir(); + return path.join(home, ".openclaw"); +} + +function areBundledPluginsDisabled(env: NodeJS.ProcessEnv): boolean { + const value = normalizeTrimmedString(env.OPENCLAW_DISABLE_BUNDLED_PLUGINS)?.toLowerCase(); + return value === "1" || value === "true"; +} + +function hasManifestDir(root: string | undefined): root is string { + return Boolean(root && fs.existsSync(root)); +} + +function resolveBundledPluginRoot(env: NodeJS.ProcessEnv): string | undefined { + if (areBundledPluginsDisabled(env)) { + return undefined; + } + + const override = normalizeTrimmedString(env.OPENCLAW_BUNDLED_PLUGINS_DIR); + if (override) { + return resolveUserPath(override, env); + } + + const sourceRoot = path.join(OPENCLAW_PACKAGE_ROOT, "extensions"); + const runtimeRoot = path.join(OPENCLAW_PACKAGE_ROOT, "dist-runtime", "extensions"); + const distRoot = path.join(OPENCLAW_PACKAGE_ROOT, "dist", "extensions"); + return [sourceRoot, runtimeRoot, distRoot].find(hasManifestDir); +} + +function listChildPluginDirs( + root: string | undefined, + rank: number, + startOrder: number, + origin: string, +): CandidateDir[] { + if (!root || !fs.existsSync(root)) { + return []; + } + const dirs: CandidateDir[] = []; + let order = startOrder; + try { + for (const entry of fs.readdirSync(root, { withFileTypes: true })) { + if (entry.isDirectory()) { + dirs.push({ pluginDir: path.join(root, entry.name), rank, order: order++, origin }); + } + } + } catch { + return []; + } + return dirs; +} + +function readJsonObject(filePath: string): Record | undefined { + try { + const parsed = parseJsonWithJson5Fallback(fs.readFileSync(filePath, "utf8")); + return isRecord(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} + +function readManifestObject(pluginDir: string): Record | undefined { + return readJsonObject(path.join(pluginDir, PLUGIN_MANIFEST_FILENAME)); +} + +function listPersistedIndexPluginDirs(env: NodeJS.ProcessEnv, startOrder: number): CandidateDir[] { + const index = readJsonObject(path.join(resolveStateDir(env), "plugins", "installs.json")); + if (!index || !Array.isArray(index.plugins)) { + return []; + } + + const dirs: CandidateDir[] = []; + let order = startOrder; + for (const rawPlugin of index.plugins) { + if (!isRecord(rawPlugin)) { + continue; + } + const rootDir = normalizeTrimmedString(rawPlugin.rootDir); + if (!rootDir) { + continue; + } + dirs.push({ + pluginDir: resolveUserPath(rootDir, env), + rank: rawPlugin.origin === "bundled" ? 2 : 1, + order: order++, + origin: normalizeTrimmedString(rawPlugin.origin), + }); + } + return dirs; +} + +function resolveComparablePath(filePath: string): string { + try { + return fs.realpathSync(filePath); + } catch { + return path.resolve(filePath); + } +} + +function uniqueCandidateDirs(candidates: CandidateDir[]): CandidateDir[] { + const byPath = new Map(); + for (const candidate of candidates) { + const key = resolveComparablePath(candidate.pluginDir); + const existing = byPath.get(key); + if (!existing || candidate.rank < existing.rank || candidate.order < existing.order) { + byPath.set(key, candidate); + } + } + return [...byPath.values()].toSorted( + (left, right) => left.rank - right.rank || left.order - right.order, + ); +} + +export function listOpenClawPluginManifestMetadata( + env: NodeJS.ProcessEnv = process.env, +): PluginManifestMetadataRecord[] { + const candidates: CandidateDir[] = []; + let order = 0; + candidates.push(...listPersistedIndexPluginDirs(env, order)); + order = candidates.length; + candidates.push(...listChildPluginDirs(resolveBundledPluginRoot(env), 2, order, "bundled")); + order = candidates.length; + candidates.push( + ...listChildPluginDirs(path.join(resolveStateDir(env), "extensions"), 4, order, "global"), + ); + + const byManifestId = new Map(); + const records: PluginManifestMetadataRecord[] = []; + for (const candidate of uniqueCandidateDirs(candidates)) { + const manifest = readManifestObject(candidate.pluginDir); + if (!manifest) { + continue; + } + const manifestId = normalizeTrimmedString(manifest.id); + if (manifestId) { + const existing = byManifestId.get(manifestId); + if (existing && existing.rank <= candidate.rank) { + continue; + } + byManifestId.set(manifestId, candidate); + } + records.push({ pluginDir: candidate.pluginDir, manifest, origin: candidate.origin }); + } + return records; +} diff --git a/src/plugins/manifest-model-id-normalization.ts b/src/plugins/manifest-model-id-normalization.ts index 720bb831ccc..1edf5aabcf5 100644 --- a/src/plugins/manifest-model-id-normalization.ts +++ b/src/plugins/manifest-model-id-normalization.ts @@ -1,11 +1,115 @@ import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; +import { listOpenClawPluginManifestMetadata } from "./manifest-metadata-scan.js"; import type { PluginManifestModelIdNormalizationProvider } from "./manifest.js"; -import { loadPluginManifestRegistryForPluginRegistry } from "./plugin-registry.js"; let manifestModelIdNormalizationCache: | Map | undefined; +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === "object" && !Array.isArray(value); +} + +function normalizeTrimmedString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} + +function normalizeStringList(value: unknown): string[] { + if (!Array.isArray(value)) { + return []; + } + return value + .map((entry) => normalizeTrimmedString(entry)) + .filter((entry): entry is string => entry !== undefined); +} + +function normalizePrefixRules( + value: unknown, +): PluginManifestModelIdNormalizationProvider["prefixWhenBareAfterAliasStartsWith"] { + if (!Array.isArray(value)) { + return undefined; + } + const rules: NonNullable< + PluginManifestModelIdNormalizationProvider["prefixWhenBareAfterAliasStartsWith"] + > = []; + for (const rawRule of value) { + if (!isRecord(rawRule)) { + continue; + } + const modelPrefix = normalizeTrimmedString(rawRule.modelPrefix); + const prefix = normalizeTrimmedString(rawRule.prefix); + if (modelPrefix && prefix) { + rules.push({ modelPrefix, prefix }); + } + } + return rules.length > 0 ? rules : undefined; +} + +function normalizeModelIdNormalizationPolicy( + value: unknown, +): PluginManifestModelIdNormalizationProvider | undefined { + if (!isRecord(value)) { + return undefined; + } + + const aliases: Record = {}; + if (isRecord(value.aliases)) { + for (const [aliasRaw, canonicalRaw] of Object.entries(value.aliases)) { + const alias = normalizeLowercaseStringOrEmpty(aliasRaw); + const canonical = normalizeTrimmedString(canonicalRaw); + if (alias && canonical) { + aliases[alias] = canonical; + } + } + } + + const stripPrefixes = normalizeStringList(value.stripPrefixes); + const prefixWhenBare = normalizeTrimmedString(value.prefixWhenBare); + const prefixWhenBareAfterAliasStartsWith = normalizePrefixRules( + value.prefixWhenBareAfterAliasStartsWith, + ); + const policy = { + ...(Object.keys(aliases).length > 0 ? { aliases } : {}), + ...(stripPrefixes.length > 0 ? { stripPrefixes } : {}), + ...(prefixWhenBare ? { prefixWhenBare } : {}), + ...(prefixWhenBareAfterAliasStartsWith ? { prefixWhenBareAfterAliasStartsWith } : {}), + } satisfies PluginManifestModelIdNormalizationProvider; + + return Object.keys(policy).length > 0 ? policy : undefined; +} + +function readManifestModelIdNormalizationPolicies( + manifest: Record, +): Array<[string, PluginManifestModelIdNormalizationProvider]> { + const modelIdNormalization = manifest.modelIdNormalization; + if (!isRecord(modelIdNormalization) || !isRecord(modelIdNormalization.providers)) { + return []; + } + + const entries: Array<[string, PluginManifestModelIdNormalizationProvider]> = []; + for (const [providerRaw, rawPolicy] of Object.entries(modelIdNormalization.providers)) { + const provider = normalizeLowercaseStringOrEmpty(providerRaw); + const policy = normalizeModelIdNormalizationPolicy(rawPolicy); + if (provider && policy) { + entries.push([provider, policy]); + } + } + return entries; +} + +function collectManifestModelIdNormalizationPolicies(): Map< + string, + PluginManifestModelIdNormalizationProvider +> { + const policies = new Map(); + for (const { manifest } of listOpenClawPluginManifestMetadata()) { + for (const [provider, policy] of readManifestModelIdNormalizationPolicies(manifest)) { + policies.set(provider, policy); + } + } + return policies; +} + function loadManifestModelIdNormalizationPolicies(): Map< string, PluginManifestModelIdNormalizationProvider @@ -14,17 +118,18 @@ function loadManifestModelIdNormalizationPolicies(): Map< return manifestModelIdNormalizationCache; } - const policies = new Map(); - const registry = loadPluginManifestRegistryForPluginRegistry({ includeDisabled: true }); - for (const plugin of registry.plugins) { - for (const [provider, policy] of Object.entries(plugin.modelIdNormalization?.providers ?? {})) { - policies.set(provider, policy); - } - } + const policies = collectManifestModelIdNormalizationPolicies(); manifestModelIdNormalizationCache = policies; return policies; } +function resolveManifestModelIdNormalizationPolicy( + provider: string, +): PluginManifestModelIdNormalizationProvider | undefined { + const providerId = normalizeLowercaseStringOrEmpty(provider); + return loadManifestModelIdNormalizationPolicies().get(providerId); +} + function hasProviderPrefix(modelId: string): boolean { return modelId.includes("/"); } @@ -40,7 +145,7 @@ export function normalizeProviderModelIdWithManifest(params: { modelId: string; }; }): string | undefined { - const policy = loadManifestModelIdNormalizationPolicies().get(params.provider); + const policy = resolveManifestModelIdNormalizationPolicy(params.provider); if (!policy) { return undefined; }