refactor: extract model catalog core package

* refactor: extract model catalog core package

* refactor: route model catalog imports through package boundary

* build: include model catalog in plugin sdk package dts

* fix: preserve static fallback model metadata
This commit is contained in:
Peter Steinberger
2026-05-30 16:33:45 +01:00
committed by GitHub
parent ec15f90a55
commit 30e1556cda
32 changed files with 824 additions and 427 deletions
+8 -2
View File
@@ -601,7 +601,7 @@ jobs:
uses: actions/cache@v5
with:
path: .artifacts/build-all-cache
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
key: ${{ runner.os }}-build-all-v3-${{ hashFiles('package.json', 'pnpm-lock.yaml', 'npm-shrinkwrap.json', 'packages/plugin-sdk/package.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'packages/memory-host-sdk/package.json', 'scripts/build-all.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entries.mjs', 'tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'src/plugin-sdk/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'packages/memory-host-sdk/src/**', 'src/types/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'scripts/copy-export-html-templates.ts', 'scripts/lib/copy-assets.ts', 'src/auto-reply/reply/export-html/**') }}
restore-keys: |
${{ runner.os }}-build-all-v3-
@@ -1403,7 +1403,7 @@ jobs:
packages/plugin-sdk/dist
extensions/*/dist/.boundary-tsc.tsbuildinfo
extensions/*/dist/.boundary-tsc.stamp
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
key: ${{ runner.os }}-extension-package-boundary-v1-${{ hashFiles('tsconfig.json', 'tsconfig.plugin-sdk.dts.json', 'packages/plugin-sdk/tsconfig.json', 'packages/llm-core/package.json', 'packages/model-catalog-core/package.json', 'scripts/check-extension-package-tsc-boundary.mjs', 'scripts/prepare-extension-package-boundary-artifacts.mjs', 'scripts/write-plugin-sdk-entry-dts.ts', 'scripts/lib/plugin-sdk-entrypoints.json', 'scripts/lib/plugin-sdk-entries.mjs', 'src/plugin-sdk/**', 'src/auto-reply/**', 'packages/llm-core/src/**', 'packages/model-catalog-core/src/**', 'src/video-generation/dashscope-compatible.ts', 'src/video-generation/types.ts', 'src/types/**', 'extensions/**', 'extensions/tsconfig.package-boundary*.json', 'package.json', 'pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-extension-package-boundary-v1-
@@ -1425,11 +1425,17 @@ jobs:
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
if [ -d packages/model-catalog-core/src ]; then
find packages/model-catalog-core/src \
-type f \( -name '*.ts' -o -name '*.tsx' -o -name '*.mts' -o -name '*.cts' -o -name '*.js' -o -name '*.mjs' -o -name '*.json' \) \
-exec touch -t 200001010000 {} +
fi
cache_inputs=(
tsconfig.json \
tsconfig.plugin-sdk.dts.json \
packages/plugin-sdk/tsconfig.json \
packages/llm-core/package.json \
packages/model-catalog-core/package.json \
scripts/check-extension-package-tsc-boundary.mjs \
scripts/prepare-extension-package-boundary-artifacts.mjs \
scripts/write-plugin-sdk-entry-dts.ts \
@@ -342,7 +342,7 @@ export function renderMattermostModelsPickerView(params: {
const page = paginateItems(models, params.page);
const rows: MattermostInteractiveButtonInput[][] = page.items.map((model) => {
const isCurrent = current?.provider === provider && current.model === model;
const isCurrent = current?.provider === provider && current?.model === model;
return [
buildButton({
action: "select",
@@ -102,6 +102,24 @@
"../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"
],
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
"@openclaw/model-catalog-core": [
"../dist/plugin-sdk/packages/model-catalog-core/src/index.d.ts"
],
"@openclaw/model-catalog-core/configured-model-refs": [
"../dist/plugin-sdk/packages/model-catalog-core/src/configured-model-refs.d.ts"
],
"@openclaw/model-catalog-core/provider-id": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalization": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalize": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts"
],
"@openclaw/model-catalog-core/*": [
"../dist/plugin-sdk/packages/model-catalog-core/src/*.d.ts"
],
"@openclaw/markdown-core": [
"../dist/plugin-sdk/packages/markdown-core/src/index.d.ts"
],
+18
View File
@@ -111,6 +111,24 @@
"@openclaw/llm-core/*": [
"../../dist/plugin-sdk/packages/llm-core/src/*.d.ts"
],
"@openclaw/model-catalog-core": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/index.d.ts"
],
"@openclaw/model-catalog-core/configured-model-refs": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/configured-model-refs.d.ts"
],
"@openclaw/model-catalog-core/provider-id": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalization": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalize": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts"
],
"@openclaw/model-catalog-core/*": [
"../../dist/plugin-sdk/packages/model-catalog-core/src/*.d.ts"
],
"@openclaw/markdown-core": [
"../../dist/plugin-sdk/packages/markdown-core/src/index.d.ts"
],
+38
View File
@@ -0,0 +1,38 @@
{
"name": "@openclaw/model-catalog-core",
"version": "0.0.0-private",
"private": true,
"files": [
"dist"
],
"type": "module",
"main": "./dist/index.mjs",
"types": "./dist/index.d.mts",
"exports": {
".": {
"types": "./dist/index.d.mts",
"import": "./dist/index.mjs",
"default": "./dist/index.mjs"
},
"./configured-model-refs": {
"types": "./dist/configured-model-refs.d.mts",
"import": "./dist/configured-model-refs.mjs",
"default": "./dist/configured-model-refs.mjs"
},
"./provider-id": {
"types": "./dist/provider-id.d.mts",
"import": "./dist/provider-id.mjs",
"default": "./dist/provider-id.mjs"
},
"./provider-model-id-normalization": {
"types": "./dist/provider-model-id-normalization.d.mts",
"import": "./dist/provider-model-id-normalization.mjs",
"default": "./dist/provider-model-id-normalization.mjs"
},
"./provider-model-id-normalize": {
"types": "./dist/provider-model-id-normalize.d.mts",
"import": "./dist/provider-model-id-normalize.mjs",
"default": "./dist/provider-model-id-normalize.mjs"
}
}
}
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import {
collectConfiguredModelRefs,
collectConfiguredModelRefValues,
extractProviderFromModelRef,
} from "./configured-model-refs.js";
describe("configured model refs", () => {
it("collects agent, hook, message, and channel model refs with config paths", () => {
expect(
collectConfiguredModelRefs({
agents: {
defaults: {
model: { primary: "openai/gpt-5.5", fallbacks: ["anthropic/claude-sonnet-4-6"] },
compaction: { memoryFlush: { model: "openai/gpt-5.5-mini" } },
},
list: [{ id: "custom", model: "xai/grok-4-fast" }],
},
hooks: {
mappings: [{ model: "openai/gpt-5.5-nano" }],
},
messages: {
tts: { summaryModel: "openai/gpt-5.5-mini" },
},
channels: {
modelByChannel: {
discord: {
guild: "anthropic/claude-opus-4-8",
},
},
},
}),
).toEqual([
{ path: "agents.defaults.model.primary", value: "openai/gpt-5.5" },
{ path: "agents.defaults.model.fallbacks.0", value: "anthropic/claude-sonnet-4-6" },
{ path: "agents.defaults.compaction.memoryFlush.model", value: "openai/gpt-5.5-mini" },
{ path: "agents.list.0.model", value: "xai/grok-4-fast" },
{ path: "channels.modelByChannel.discord.guild", value: "anthropic/claude-opus-4-8" },
{ path: "hooks.mappings.0.model", value: "openai/gpt-5.5-nano" },
{ path: "messages.tts.summaryModel", value: "openai/gpt-5.5-mini" },
]);
});
it("can exclude channel model overrides from configured refs", () => {
expect(
collectConfiguredModelRefValues(
{
agents: { defaults: { model: "openai/gpt-5.5" } },
channels: { modelByChannel: { discord: { guild: "anthropic/claude-sonnet-4-6" } } },
},
{ includeChannelModelOverrides: false },
),
).toEqual(["openai/gpt-5.5"]);
});
it("ignores array-shaped malformed records", () => {
expect(
collectConfiguredModelRefs({
agents: {
defaults: {
models: ["openai/gpt-5.5"],
},
},
}),
).toEqual([]);
});
it("extracts normalized providers from provider-prefixed refs", () => {
expect(extractProviderFromModelRef(" OpenAI/gpt-5.5 ")).toBe("openai");
expect(extractProviderFromModelRef("gpt-5.5")).toBeNull();
});
});
@@ -0,0 +1,134 @@
import { normalizeProviderId } from "./provider-id.js";
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
export type ConfiguredModelRef = {
path: string;
value: string;
};
export const AGENT_MODEL_CONFIG_KEYS = [
"model",
"imageModel",
"imageGenerationModel",
"videoGenerationModel",
"musicGenerationModel",
"voiceModel",
"pdfModel",
] as const;
export function collectConfiguredModelRefs(
config: unknown,
options: { includeChannelModelOverrides?: boolean } = {},
): ConfiguredModelRef[] {
const refs: ConfiguredModelRef[] = [];
const pushModelRef = (path: string, value: unknown) => {
if (typeof value === "string" && value.trim()) {
refs.push({ path, value: value.trim() });
}
};
const collectModelConfig = (path: string, value: unknown) => {
if (typeof value === "string") {
pushModelRef(path, value);
return;
}
if (!isRecord(value)) {
return;
}
pushModelRef(`${path}.primary`, value.primary);
if (Array.isArray(value.fallbacks)) {
for (const [index, entry] of value.fallbacks.entries()) {
pushModelRef(`${path}.fallbacks.${index}`, entry);
}
}
};
const collectFromAgent = (path: string, agent: unknown) => {
if (!isRecord(agent)) {
return;
}
for (const key of AGENT_MODEL_CONFIG_KEYS) {
collectModelConfig(`${path}.${key}`, agent[key]);
}
pushModelRef(
`${path}.heartbeat.model`,
isRecord(agent.heartbeat) ? agent.heartbeat.model : undefined,
);
collectModelConfig(
`${path}.subagents.model`,
isRecord(agent.subagents) ? agent.subagents.model : undefined,
);
if (isRecord(agent.compaction)) {
pushModelRef(`${path}.compaction.model`, agent.compaction.model);
pushModelRef(
`${path}.compaction.memoryFlush.model`,
isRecord(agent.compaction.memoryFlush) ? agent.compaction.memoryFlush.model : undefined,
);
}
if (isRecord(agent.models)) {
for (const modelRef of Object.keys(agent.models)) {
pushModelRef(`${path}.models.${modelRef}`, modelRef);
}
}
};
const root = isRecord(config) ? config : {};
const agents = isRecord(root.agents) ? root.agents : {};
collectFromAgent("agents.defaults", agents.defaults);
if (Array.isArray(agents.list)) {
for (const [index, entry] of agents.list.entries()) {
collectFromAgent(`agents.list.${index}`, entry);
}
}
if (options.includeChannelModelOverrides !== false) {
const channels = isRecord(root.channels) ? root.channels : {};
const modelByChannel = isRecord(channels.modelByChannel) ? channels.modelByChannel : {};
for (const [channelId, channelMap] of Object.entries(modelByChannel)) {
if (!isRecord(channelMap)) {
continue;
}
for (const [targetId, modelRef] of Object.entries(channelMap)) {
pushModelRef(`channels.modelByChannel.${channelId}.${targetId}`, modelRef);
}
}
}
const hooks = isRecord(root.hooks) ? root.hooks : {};
if (Array.isArray(hooks.mappings)) {
for (const [index, mapping] of hooks.mappings.entries()) {
pushModelRef(`hooks.mappings.${index}.model`, isRecord(mapping) ? mapping.model : undefined);
}
}
pushModelRef("hooks.gmail.model", isRecord(hooks.gmail) ? hooks.gmail.model : undefined);
pushModelRef(
"messages.tts.summaryModel",
isRecord(root.messages) && isRecord(root.messages.tts)
? root.messages.tts.summaryModel
: undefined,
);
pushModelRef(
"channels.discord.voice.model",
isRecord(root.channels) &&
isRecord(root.channels.discord) &&
isRecord(root.channels.discord.voice)
? root.channels.discord.voice.model
: undefined,
);
return refs;
}
export function collectConfiguredModelRefValues(
config: unknown,
options?: { includeChannelModelOverrides?: boolean },
): string[] {
return collectConfiguredModelRefs(config, options).map((ref) => ref.value);
}
export function extractProviderFromModelRef(value: string): string | null {
const trimmed = value.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0) {
return null;
}
return normalizeProviderId(trimmed.slice(0, slash));
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./configured-model-refs.js";
export * from "./provider-id.js";
export * from "./provider-model-id-normalization.js";
export * from "./provider-model-id-normalize.js";
@@ -0,0 +1,39 @@
export function normalizeLowercaseStringOrEmpty(value: unknown): string {
return typeof value === "string" ? value.trim().toLowerCase() : "";
}
export function normalizeProviderId(provider: string): string {
return normalizeLowercaseStringOrEmpty(provider);
}
/** Normalize provider ID before manifest-owned auth alias lookup. */
export function normalizeProviderIdForAuth(provider: string): string {
return normalizeProviderId(provider);
}
export function findNormalizedProviderValue<T>(
entries: Record<string, T> | undefined,
provider: string,
): T | undefined {
if (!entries) {
return undefined;
}
const providerKey = normalizeProviderId(provider);
for (const [key, value] of Object.entries(entries)) {
if (normalizeProviderId(key) === providerKey) {
return value;
}
}
return undefined;
}
export function findNormalizedProviderKey(
entries: Record<string, unknown> | undefined,
provider: string,
): string | undefined {
if (!entries) {
return undefined;
}
const providerKey = normalizeProviderId(provider);
return Object.keys(entries).find((key) => normalizeProviderId(key) === providerKey);
}
@@ -0,0 +1,37 @@
import { describe, expect, it } from "vitest";
import {
collectManifestModelIdNormalizationPolicies,
normalizeConfiguredProviderCatalogModelId,
normalizeStaticProviderModelIdWithPolicies,
} from "./provider-model-id-normalization.js";
describe("provider model id policy normalization", () => {
it("applies manifest policies before built-in provider normalization", () => {
const policies = collectManifestModelIdNormalizationPolicies([
{
modelIdNormalization: {
providers: {
"Google-Vertex": {
aliases: {
pro: "gemini-3-pro",
},
},
},
},
},
]);
expect(normalizeStaticProviderModelIdWithPolicies("google-vertex", "pro", policies)).toBe(
"gemini-3.1-pro-preview",
);
});
it("normalizes provider-prefixed Google catalog refs behind gateway prefixes", () => {
expect(
normalizeConfiguredProviderCatalogModelId(
"openrouter",
"openrouter/google/gemini-3-pro-preview",
),
).toBe("openrouter/google/gemini-3.1-pro-preview");
});
});
@@ -0,0 +1,206 @@
import { normalizeLowercaseStringOrEmpty } from "./provider-id.js";
import {
normalizeGooglePreviewModelId,
normalizeTogetherModelId,
} from "./provider-model-id-normalize.js";
export type ManifestModelIdNormalizationProvider = {
aliases?: Record<string, string>;
stripPrefixes?: string[];
prefixWhenBare?: string;
prefixWhenBareAfterAliasStartsWith?: {
modelPrefix: string;
prefix: string;
}[];
};
export type ManifestModelIdNormalizationRecord = {
modelIdNormalization?: {
providers?: Record<string, ManifestModelIdNormalizationProvider>;
};
};
let currentManifestModelIdNormalizationPolicies:
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined;
export function collectManifestModelIdNormalizationPolicies(
plugins: readonly ManifestModelIdNormalizationRecord[],
): Map<string, ManifestModelIdNormalizationProvider> {
const policies = new Map<string, ManifestModelIdNormalizationProvider>();
for (const plugin of plugins) {
for (const [provider, policy] of Object.entries(plugin.modelIdNormalization?.providers ?? {})) {
policies.set(normalizeLowercaseStringOrEmpty(provider), policy);
}
}
return policies;
}
export function setCurrentManifestModelIdNormalizationRecords(
plugins: readonly ManifestModelIdNormalizationRecord[] | undefined,
): void {
currentManifestModelIdNormalizationPolicies = plugins
? collectManifestModelIdNormalizationPolicies(plugins)
: undefined;
}
export function getCurrentManifestModelIdNormalizationPolicies():
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined {
return currentManifestModelIdNormalizationPolicies;
}
function hasProviderPrefix(modelId: string): boolean {
return modelId.includes("/");
}
function formatPrefixedModelId(prefix: string, modelId: string): string {
return `${prefix.replace(/\/+$/u, "")}/${modelId.replace(/^\/+/u, "")}`;
}
export function normalizeProviderModelIdWithPolicies(params: {
provider: string;
policies: ReadonlyMap<string, ManifestModelIdNormalizationProvider>;
context: {
modelId: string;
};
}): string | undefined {
const policy = params.policies.get(normalizeLowercaseStringOrEmpty(params.provider));
if (!policy) {
return undefined;
}
let modelId = params.context.modelId.trim();
if (!modelId) {
return modelId;
}
for (const prefix of policy.stripPrefixes ?? []) {
const normalizedPrefix = normalizeLowercaseStringOrEmpty(prefix);
if (normalizedPrefix && normalizeLowercaseStringOrEmpty(modelId).startsWith(normalizedPrefix)) {
modelId = modelId.slice(prefix.length);
break;
}
}
modelId = policy.aliases?.[normalizeLowercaseStringOrEmpty(modelId)] ?? modelId;
if (!hasProviderPrefix(modelId)) {
for (const rule of policy.prefixWhenBareAfterAliasStartsWith ?? []) {
if (normalizeLowercaseStringOrEmpty(modelId).startsWith(rule.modelPrefix.toLowerCase())) {
return formatPrefixedModelId(rule.prefix, modelId);
}
}
if (policy.prefixWhenBare) {
return formatPrefixedModelId(policy.prefixWhenBare, modelId);
}
}
return modelId;
}
export function normalizeBuiltInProviderModelId(provider: string, model: string): string {
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
if (
normalizedProvider === "google" ||
normalizedProvider === "google-gemini-cli" ||
normalizedProvider === "google-vertex"
) {
return normalizeGooglePreviewModelId(model);
}
if (normalizedProvider === "openrouter") {
const trimmed = model.trim();
return trimmed && !trimmed.includes("/") ? `openrouter/${trimmed}` : model;
}
if (normalizedProvider === "anthropic") {
const anthropicAliases: Record<string, string> = {
"opus-4.8": "claude-opus-4-8",
opus: "claude-opus-4-8",
"opus-4.6": "claude-opus-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
};
return anthropicAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
}
if (normalizedProvider === "vercel-ai-gateway") {
const vercelAliases: Record<string, string> = {
"opus-4.6": "claude-opus-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
};
const aliased = vercelAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
return normalizeLowercaseStringOrEmpty(aliased).startsWith("claude-")
? `anthropic/${aliased}`
: aliased;
}
if (normalizedProvider === "huggingface") {
const prefix = "huggingface/";
return normalizeLowercaseStringOrEmpty(model).startsWith(prefix)
? model.slice(prefix.length)
: model;
}
if (normalizedProvider === "nvidia") {
const trimmed = model.trim();
return trimmed && !trimmed.includes("/") ? `nvidia/${trimmed}` : model;
}
if (normalizedProvider === "xai") {
const xaiAliases: Record<string, string> = {
"grok-4-fast-reasoning": "grok-4-fast",
"grok-4-1-fast-reasoning": "grok-4-1-fast",
"grok-4.20-experimental-beta-0304-reasoning": "grok-4.20-beta-latest-reasoning",
"grok-4.20-experimental-beta-0304-non-reasoning": "grok-4.20-beta-latest-non-reasoning",
"grok-4.20-reasoning": "grok-4.20-beta-latest-reasoning",
"grok-4.20-non-reasoning": "grok-4.20-beta-latest-non-reasoning",
};
return xaiAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
}
if (normalizedProvider === "together") {
return normalizeTogetherModelId(model);
}
return model;
}
export function normalizeStaticProviderModelIdWithPolicies(
provider: string,
model: string,
policies?: ReadonlyMap<string, ManifestModelIdNormalizationProvider>,
): string {
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
const manifestModelId = policies
? (normalizeProviderModelIdWithPolicies({
provider: normalizedProvider,
policies,
context: {
modelId: model,
},
}) ?? model)
: model;
return normalizeBuiltInProviderModelId(normalizedProvider, manifestModelId);
}
export function normalizeConfiguredProviderCatalogModelId(
provider: string,
model: string,
policies = getCurrentManifestModelIdNormalizationPolicies(),
): string {
const providerModel = normalizeStaticProviderModelIdWithPolicies(provider, model, policies);
return normalizeConfiguredProviderCatalogModelRef(providerModel);
}
export function normalizeConfiguredProviderCatalogModelRef(providerModel: string): string {
const googlePrefix = "google/";
if (!providerModel.startsWith(googlePrefix)) {
const slash = providerModel.indexOf("/");
if (slash <= 0 || slash >= providerModel.length - 1) {
return providerModel;
}
const prefix = providerModel.slice(0, slash + 1);
const suffix = providerModel.slice(slash + 1);
if (!suffix.startsWith(googlePrefix)) {
return providerModel;
}
const normalizedSuffix = normalizeGooglePreviewModelId(suffix);
return normalizedSuffix === suffix ? providerModel : `${prefix}${normalizedSuffix}`;
}
const modelId = providerModel.slice(googlePrefix.length);
const normalizedModelId = normalizeGooglePreviewModelId(modelId);
return normalizedModelId === modelId ? providerModel : `${googlePrefix}${normalizedModelId}`;
}
@@ -0,0 +1,43 @@
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
const GOOGLE_PROVIDER_PREFIX = "google/";
export function normalizeGooglePreviewModelId(id: string): string {
if (id.startsWith(GOOGLE_PROVIDER_PREFIX)) {
const modelId = id.slice(GOOGLE_PROVIDER_PREFIX.length);
const normalizedModelId = normalizeGooglePreviewModelId(modelId);
return normalizedModelId === modelId ? id : `${GOOGLE_PROVIDER_PREFIX}${normalizedModelId}`;
}
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3-flash") {
return "gemini-3-flash-preview";
}
if (id === "gemini-3.1-pro") {
return "gemini-3.1-pro-preview";
}
// Gemini 3.1 Flash Lite graduated to GA on 2026-05-07; the -preview
// endpoint is deprecated (shutdown 2026-05-25). Map old preview name
// to the stable GA id.
if (id === "gemini-3.1-flash-lite-preview") {
return "gemini-3.1-flash-lite";
}
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
return "gemini-3-flash-preview";
}
return id;
}
export function normalizeTogetherModelId(id: string): string {
if (id === "moonshotai/Kimi-K2.5") {
return "moonshotai/Kimi-K2.6";
}
return id;
}
export function normalizeAntigravityPreviewModelId(id: string): string {
if (ANTIGRAVITY_BARE_PRO_IDS.has(id)) {
return `${id}-low`;
}
return id;
}
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist"
},
"include": ["src/**/*"]
}
+1
View File
@@ -15,6 +15,7 @@
"include": [
"../../packages/markdown-core/src/**/*.ts",
"../../packages/media-generation-core/src/**/*.ts",
"../../packages/model-catalog-core/src/**/*.ts",
"../../packages/terminal-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",
+2
View File
@@ -1830,6 +1830,8 @@ importers:
packages/memory-host-sdk: {}
packages/model-catalog-core: {}
packages/net-policy:
dependencies:
ipaddr.js:
+2
View File
@@ -50,12 +50,14 @@ export const BUILD_ALL_STEPS = [
"packages/markdown-core/package.json",
"packages/media-understanding-common/package.json",
"packages/terminal-core/package.json",
"packages/model-catalog-core/package.json",
"packages/memory-host-sdk/package.json",
"tsconfig.json",
"tsconfig.plugin-sdk.dts.json",
"src/plugin-sdk",
"packages/llm-core/src",
"packages/markdown-core/src",
"packages/model-catalog-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
+14
View File
@@ -59,6 +59,20 @@ export const EXTENSION_PACKAGE_BOUNDARY_BASE_PATHS = {
"@openclaw/llm-core/types": ["../dist/plugin-sdk/packages/llm-core/src/types.d.ts"],
"@openclaw/llm-core/validation": ["../dist/plugin-sdk/packages/llm-core/src/validation.d.ts"],
"@openclaw/llm-core/*": ["../dist/plugin-sdk/packages/llm-core/src/*.d.ts"],
"@openclaw/model-catalog-core": ["../dist/plugin-sdk/packages/model-catalog-core/src/index.d.ts"],
"@openclaw/model-catalog-core/configured-model-refs": [
"../dist/plugin-sdk/packages/model-catalog-core/src/configured-model-refs.d.ts",
],
"@openclaw/model-catalog-core/provider-id": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts",
],
"@openclaw/model-catalog-core/provider-model-id-normalization": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
],
"@openclaw/model-catalog-core/provider-model-id-normalize": [
"../dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
],
"@openclaw/model-catalog-core/*": ["../dist/plugin-sdk/packages/model-catalog-core/src/*.d.ts"],
"@openclaw/markdown-core": ["../dist/plugin-sdk/packages/markdown-core/src/index.d.ts"],
"@openclaw/markdown-core/code-spans": [
"../dist/plugin-sdk/packages/markdown-core/src/code-spans.d.ts",
@@ -17,6 +17,7 @@ const PLUGIN_SDK_TYPE_INPUTS = [
"src/auto-reply",
"packages/llm-core/src",
"packages/markdown-core/src",
"packages/model-catalog-core/src",
"packages/memory-host-sdk/src",
"packages/media-generation-core/src",
"packages/media-understanding-common/src",
@@ -68,6 +69,10 @@ const ROOT_DTS_REQUIRED_OUTPUTS = [
"dist/plugin-sdk/packages/terminal-core/src/table.d.ts",
"dist/plugin-sdk/packages/terminal-core/src/terminal-link.d.ts",
"dist/plugin-sdk/packages/terminal-core/src/theme.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/configured-model-refs.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/provider-id.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
"dist/plugin-sdk/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
"dist/plugin-sdk/error-runtime.d.ts",
"dist/plugin-sdk/plugin-entry.d.ts",
"dist/plugin-sdk/provider-auth.d.ts",
@@ -90,6 +95,10 @@ const PACKAGE_DTS_REQUIRED_OUTPUTS = [
"packages/plugin-sdk/dist/packages/media-generation-core/src/index.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/model-ref.d.ts",
"packages/plugin-sdk/dist/packages/media-generation-core/src/normalization.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/configured-model-refs.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-id.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalization.d.ts",
"packages/plugin-sdk/dist/packages/model-catalog-core/src/provider-model-id-normalize.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/ansi.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/decorative-emoji.d.ts",
"packages/plugin-sdk/dist/packages/terminal-core/src/health-style.d.ts",
+14 -6
View File
@@ -74,6 +74,12 @@ type ProviderRuntimeHooks = {
normalizeProviderTransportWithPlugin: typeof normalizeProviderTransportWithPlugin;
};
type StaticCatalogFallbackModel = Model & {
compat?: ModelCompatConfig;
contextTokens?: number;
mediaInput?: ModelMediaInputConfig;
};
const TARGET_PROVIDER_RUNTIME_HOOKS: ProviderRuntimeHooks = {
buildProviderUnknownModelHintWithPlugin,
prepareProviderDynamicModel,
@@ -989,14 +995,16 @@ function resolveConfiguredFallbackModel(params: {
}
const staticCatalogModel = configuredModel
? undefined
: resolveBundledStaticCatalogModel({
: (resolveBundledStaticCatalogModel({
provider,
modelId,
cfg,
workspaceDir,
includeRuntimeDiscovery: true,
});
}) as StaticCatalogFallbackModel | undefined);
const metadataModel = configuredModel ?? staticCatalogModel;
const fallbackCompat = configuredModel?.compat ?? staticCatalogModel?.compat;
const fallbackMediaInput = configuredModel?.mediaInput ?? staticCatalogModel?.mediaInput;
const providerHeaders = sanitizeModelHeaders(providerConfig?.headers, {
stripSecretRefMarkers: true,
});
@@ -1009,7 +1017,7 @@ function resolveConfiguredFallbackModel(params: {
provider,
modelId,
providerParams: providerConfig?.params,
configuredParams: metadataModel?.params,
configuredParams: configuredModel?.params,
});
const fallbackTransport = resolveProviderTransport({
provider,
@@ -1036,7 +1044,7 @@ function resolveConfiguredFallbackModel(params: {
});
const fallbackReasoning = resolveConfiguredFallbackReasoning({
provider,
compat: metadataModel?.compat,
compat: fallbackCompat,
reasoning: metadataModel?.reasoning,
});
return normalizeResolvedModel({
@@ -1080,8 +1088,8 @@ function resolveConfiguredFallbackModel(params: {
...(resolvedParams ? { params: resolvedParams } : {}),
...(requestTimeoutMs !== undefined ? { requestTimeoutMs } : {}),
headers: requestConfig.headers,
compat: metadataModel?.compat,
mediaInput: metadataModel?.mediaInput,
compat: fallbackCompat,
mediaInput: fallbackMediaInput,
} as Model,
providerRequest,
),
+6 -37
View File
@@ -1,37 +1,6 @@
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
export function normalizeProviderId(provider: string): string {
return normalizeLowercaseStringOrEmpty(provider);
}
/** Normalize provider ID before manifest-owned auth alias lookup. */
export function normalizeProviderIdForAuth(provider: string): string {
return normalizeProviderId(provider);
}
export function findNormalizedProviderValue<T>(
entries: Record<string, T> | undefined,
provider: string,
): T | undefined {
if (!entries) {
return undefined;
}
const providerKey = normalizeProviderId(provider);
for (const [key, value] of Object.entries(entries)) {
if (normalizeProviderId(key) === providerKey) {
return value;
}
}
return undefined;
}
export function findNormalizedProviderKey(
entries: Record<string, unknown> | undefined,
provider: string,
): string | undefined {
if (!entries) {
return undefined;
}
const providerKey = normalizeProviderId(provider);
return Object.keys(entries).find((key) => normalizeProviderId(key) === providerKey);
}
export {
findNormalizedProviderKey,
findNormalizedProviderValue,
normalizeProviderId,
normalizeProviderIdForAuth,
} from "../../packages/model-catalog-core/src/provider-id.js";
+1 -1
View File
@@ -43,7 +43,7 @@ function resolveConfiguredTextModelFromConfig(params: {
const providerCfg = findNormalizedProviderValue(providers, params.providerId);
const modelId = providerCfg?.models
?.find(
(model) =>
(model: { id?: string; input?: readonly string[] }) =>
Boolean(model?.id?.trim()) && Array.isArray(model?.input) && model.input.includes("text"),
)
?.id?.trim();
+7 -131
View File
@@ -1,131 +1,7 @@
import { normalizeProviderId } from "../agents/provider-id.js";
import { isRecord } from "../utils.js";
export type ConfiguredModelRef = {
path: string;
value: string;
};
export const AGENT_MODEL_CONFIG_KEYS = [
"model",
"imageModel",
"imageGenerationModel",
"videoGenerationModel",
"musicGenerationModel",
"voiceModel",
"pdfModel",
] as const;
export function collectConfiguredModelRefs(
config: unknown,
options: { includeChannelModelOverrides?: boolean } = {},
): ConfiguredModelRef[] {
const refs: ConfiguredModelRef[] = [];
const pushModelRef = (path: string, value: unknown) => {
if (typeof value === "string" && value.trim()) {
refs.push({ path, value: value.trim() });
}
};
const collectModelConfig = (path: string, value: unknown) => {
if (typeof value === "string") {
pushModelRef(path, value);
return;
}
if (!isRecord(value)) {
return;
}
pushModelRef(`${path}.primary`, value.primary);
if (Array.isArray(value.fallbacks)) {
for (const [index, entry] of value.fallbacks.entries()) {
pushModelRef(`${path}.fallbacks.${index}`, entry);
}
}
};
const collectFromAgent = (path: string, agent: unknown) => {
if (!isRecord(agent)) {
return;
}
for (const key of AGENT_MODEL_CONFIG_KEYS) {
collectModelConfig(`${path}.${key}`, agent[key]);
}
pushModelRef(
`${path}.heartbeat.model`,
isRecord(agent.heartbeat) ? agent.heartbeat.model : undefined,
);
collectModelConfig(
`${path}.subagents.model`,
isRecord(agent.subagents) ? agent.subagents.model : undefined,
);
if (isRecord(agent.compaction)) {
pushModelRef(`${path}.compaction.model`, agent.compaction.model);
pushModelRef(
`${path}.compaction.memoryFlush.model`,
isRecord(agent.compaction.memoryFlush) ? agent.compaction.memoryFlush.model : undefined,
);
}
if (isRecord(agent.models)) {
for (const modelRef of Object.keys(agent.models)) {
pushModelRef(`${path}.models.${modelRef}`, modelRef);
}
}
};
const root = isRecord(config) ? config : {};
const agents = isRecord(root.agents) ? root.agents : {};
collectFromAgent("agents.defaults", agents.defaults);
if (Array.isArray(agents.list)) {
for (const [index, entry] of agents.list.entries()) {
collectFromAgent(`agents.list.${index}`, entry);
}
}
if (options.includeChannelModelOverrides !== false) {
const channels = isRecord(root.channels) ? root.channels : {};
const modelByChannel = isRecord(channels.modelByChannel) ? channels.modelByChannel : {};
for (const [channelId, channelMap] of Object.entries(modelByChannel)) {
if (!isRecord(channelMap)) {
continue;
}
for (const [targetId, modelRef] of Object.entries(channelMap)) {
pushModelRef(`channels.modelByChannel.${channelId}.${targetId}`, modelRef);
}
}
}
const hooks = isRecord(root.hooks) ? root.hooks : {};
if (Array.isArray(hooks.mappings)) {
for (const [index, mapping] of hooks.mappings.entries()) {
pushModelRef(`hooks.mappings.${index}.model`, isRecord(mapping) ? mapping.model : undefined);
}
}
pushModelRef("hooks.gmail.model", isRecord(hooks.gmail) ? hooks.gmail.model : undefined);
pushModelRef(
"messages.tts.summaryModel",
isRecord(root.messages) && isRecord(root.messages.tts)
? root.messages.tts.summaryModel
: undefined,
);
pushModelRef(
"channels.discord.voice.model",
isRecord(root.channels) &&
isRecord(root.channels.discord) &&
isRecord(root.channels.discord.voice)
? root.channels.discord.voice.model
: undefined,
);
return refs;
}
export function collectConfiguredModelRefValues(
config: unknown,
options?: { includeChannelModelOverrides?: boolean },
): string[] {
return collectConfiguredModelRefs(config, options).map((ref) => ref.value);
}
export function extractProviderFromModelRef(value: string): string | null {
const trimmed = value.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0) {
return null;
}
return normalizeProviderId(trimmed.slice(0, slash));
}
export {
AGENT_MODEL_CONFIG_KEYS,
collectConfiguredModelRefs,
collectConfiguredModelRefValues,
extractProviderFromModelRef,
type ConfiguredModelRef,
} from "../../packages/model-catalog-core/src/configured-model-refs.js";
+5 -43
View File
@@ -1,43 +1,5 @@
const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]);
const GOOGLE_PROVIDER_PREFIX = "google/";
export function normalizeGooglePreviewModelId(id: string): string {
if (id.startsWith(GOOGLE_PROVIDER_PREFIX)) {
const modelId = id.slice(GOOGLE_PROVIDER_PREFIX.length);
const normalizedModelId = normalizeGooglePreviewModelId(modelId);
return normalizedModelId === modelId ? id : `${GOOGLE_PROVIDER_PREFIX}${normalizedModelId}`;
}
if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") {
return "gemini-3.1-pro-preview";
}
if (id === "gemini-3-flash") {
return "gemini-3-flash-preview";
}
if (id === "gemini-3.1-pro") {
return "gemini-3.1-pro-preview";
}
// Gemini 3.1 Flash Lite graduated to GA on 2026-05-07; the -preview
// endpoint is deprecated (shutdown 2026-05-25). Map old preview name
// to the stable GA id.
if (id === "gemini-3.1-flash-lite-preview") {
return "gemini-3.1-flash-lite";
}
if (id === "gemini-3.1-flash" || id === "gemini-3.1-flash-preview") {
return "gemini-3-flash-preview";
}
return id;
}
export function normalizeTogetherModelId(id: string): string {
if (id === "moonshotai/Kimi-K2.5") {
return "moonshotai/Kimi-K2.6";
}
return id;
}
export function normalizeAntigravityPreviewModelId(id: string): string {
if (ANTIGRAVITY_BARE_PRO_IDS.has(id)) {
return `${id}-low`;
}
return id;
}
export {
normalizeAntigravityPreviewModelId,
normalizeGooglePreviewModelId,
normalizeTogetherModelId,
} from "../../packages/model-catalog-core/src/provider-model-id-normalize.js";
@@ -196,6 +196,7 @@ describe("opt-in extension package boundaries", () => {
expect(tsconfig.include).toEqual([
"../../packages/markdown-core/src/**/*.ts",
"../../packages/media-generation-core/src/**/*.ts",
"../../packages/model-catalog-core/src/**/*.ts",
"../../packages/terminal-core/src/**/*.ts",
"../../src/plugin-sdk/**/*.ts",
"../../src/video-generation/dashscope-compatible.ts",
+19
View File
@@ -1420,6 +1420,12 @@ describe("plugin sdk alias helpers", () => {
srcFile: "ip.ts",
distFile: "ip.mjs",
});
const modelCatalogProviderId = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "model-catalog-core",
srcFile: "provider-id.ts",
distFile: "provider-id.mjs",
});
fs.rmSync(gatewayClient.distFile);
fs.rmSync(gatewayClientTimeouts.distFile);
fs.rmSync(gatewayProtocol.distFile);
@@ -1432,6 +1438,7 @@ describe("plugin sdk alias helpers", () => {
fs.rmSync(terminalCoreTheme.distFile);
fs.rmSync(netPolicy.distFile);
fs.rmSync(netPolicyIp.distFile);
fs.rmSync(modelCatalogProviderId.distFile);
const sourcePluginEntry = writePluginEntry(
fixture.root,
bundledPluginFile("demo", "src/index.ts"),
@@ -1477,6 +1484,9 @@ describe("plugin sdk alias helpers", () => {
expect(fs.realpathSync(aliases["@openclaw/net-policy/ip"] ?? "")).toBe(
fs.realpathSync(netPolicyIp.srcFile),
);
expect(fs.realpathSync(aliases["@openclaw/model-catalog-core/provider-id"] ?? "")).toBe(
fs.realpathSync(modelCatalogProviderId.srcFile),
);
});
it("aliases workspace package subpaths to dist when available", () => {
@@ -1520,6 +1530,12 @@ describe("plugin sdk alias helpers", () => {
srcFile: "redact-sensitive-url.ts",
distFile: "redact-sensitive-url.mjs",
});
const modelCatalogCore = writeWorkspacePackageEntry({
root: fixture.root,
packageDir: "model-catalog-core",
srcFile: "provider-model-id-normalize.ts",
distFile: "provider-model-id-normalize.mjs",
});
const sourcePluginEntry = writePluginEntry(
fixture.root,
bundledPluginFile("demo", "src/index.ts"),
@@ -1547,6 +1563,9 @@ describe("plugin sdk alias helpers", () => {
expect(fs.realpathSync(aliases["@openclaw/net-policy/redact-sensitive-url"] ?? "")).toBe(
fs.realpathSync(netPolicy.distFile),
);
expect(
fs.realpathSync(aliases["@openclaw/model-catalog-core/provider-model-id-normalize"] ?? ""),
).toBe(fs.realpathSync(modelCatalogCore.distFile));
});
it("aliases bundled plugin package public surfaces for source plugin transforms", () => {
+35
View File
@@ -819,6 +819,41 @@ const WORKSPACE_PACKAGE_ALIAS_ENTRIES = [
srcFile: "url-userinfo.ts",
distFile: "url-userinfo.mjs",
},
{
packageName: "@openclaw/model-catalog-core",
packageDir: "model-catalog-core",
subpath: "",
srcFile: "index.ts",
distFile: "index.mjs",
},
{
packageName: "@openclaw/model-catalog-core",
packageDir: "model-catalog-core",
subpath: "configured-model-refs",
srcFile: "configured-model-refs.ts",
distFile: "configured-model-refs.mjs",
},
{
packageName: "@openclaw/model-catalog-core",
packageDir: "model-catalog-core",
subpath: "provider-id",
srcFile: "provider-id.ts",
distFile: "provider-id.mjs",
},
{
packageName: "@openclaw/model-catalog-core",
packageDir: "model-catalog-core",
subpath: "provider-model-id-normalization",
srcFile: "provider-model-id-normalization.ts",
distFile: "provider-model-id-normalization.mjs",
},
{
packageName: "@openclaw/model-catalog-core",
packageDir: "model-catalog-core",
subpath: "provider-model-id-normalize",
srcFile: "provider-model-id-normalize.ts",
distFile: "provider-model-id-normalize.mjs",
},
] as const;
function isUsableDistPluginSdkArtifact(candidate: string): boolean {
+12 -206
View File
@@ -1,206 +1,12 @@
import {
normalizeGooglePreviewModelId,
normalizeTogetherModelId,
} from "../plugin-sdk/provider-model-id-normalize.js";
import { normalizeLowercaseStringOrEmpty } from "./string-coerce.js";
export type ManifestModelIdNormalizationProvider = {
aliases?: Record<string, string>;
stripPrefixes?: string[];
prefixWhenBare?: string;
prefixWhenBareAfterAliasStartsWith?: {
modelPrefix: string;
prefix: string;
}[];
};
export type ManifestModelIdNormalizationRecord = {
modelIdNormalization?: {
providers?: Record<string, ManifestModelIdNormalizationProvider>;
};
};
let currentManifestModelIdNormalizationPolicies:
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined;
export function collectManifestModelIdNormalizationPolicies(
plugins: readonly ManifestModelIdNormalizationRecord[],
): Map<string, ManifestModelIdNormalizationProvider> {
const policies = new Map<string, ManifestModelIdNormalizationProvider>();
for (const plugin of plugins) {
for (const [provider, policy] of Object.entries(plugin.modelIdNormalization?.providers ?? {})) {
policies.set(normalizeLowercaseStringOrEmpty(provider), policy);
}
}
return policies;
}
export function setCurrentManifestModelIdNormalizationRecords(
plugins: readonly ManifestModelIdNormalizationRecord[] | undefined,
): void {
currentManifestModelIdNormalizationPolicies = plugins
? collectManifestModelIdNormalizationPolicies(plugins)
: undefined;
}
export function getCurrentManifestModelIdNormalizationPolicies():
| ReadonlyMap<string, ManifestModelIdNormalizationProvider>
| undefined {
return currentManifestModelIdNormalizationPolicies;
}
function hasProviderPrefix(modelId: string): boolean {
return modelId.includes("/");
}
function formatPrefixedModelId(prefix: string, modelId: string): string {
return `${prefix.replace(/\/+$/u, "")}/${modelId.replace(/^\/+/u, "")}`;
}
export function normalizeProviderModelIdWithPolicies(params: {
provider: string;
policies: ReadonlyMap<string, ManifestModelIdNormalizationProvider>;
context: {
modelId: string;
};
}): string | undefined {
const policy = params.policies.get(normalizeLowercaseStringOrEmpty(params.provider));
if (!policy) {
return undefined;
}
let modelId = params.context.modelId.trim();
if (!modelId) {
return modelId;
}
for (const prefix of policy.stripPrefixes ?? []) {
const normalizedPrefix = normalizeLowercaseStringOrEmpty(prefix);
if (normalizedPrefix && normalizeLowercaseStringOrEmpty(modelId).startsWith(normalizedPrefix)) {
modelId = modelId.slice(prefix.length);
break;
}
}
modelId = policy.aliases?.[normalizeLowercaseStringOrEmpty(modelId)] ?? modelId;
if (!hasProviderPrefix(modelId)) {
for (const rule of policy.prefixWhenBareAfterAliasStartsWith ?? []) {
if (normalizeLowercaseStringOrEmpty(modelId).startsWith(rule.modelPrefix.toLowerCase())) {
return formatPrefixedModelId(rule.prefix, modelId);
}
}
if (policy.prefixWhenBare) {
return formatPrefixedModelId(policy.prefixWhenBare, modelId);
}
}
return modelId;
}
export function normalizeBuiltInProviderModelId(provider: string, model: string): string {
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
if (
normalizedProvider === "google" ||
normalizedProvider === "google-gemini-cli" ||
normalizedProvider === "google-vertex"
) {
return normalizeGooglePreviewModelId(model);
}
if (normalizedProvider === "openrouter") {
const trimmed = model.trim();
return trimmed && !trimmed.includes("/") ? `openrouter/${trimmed}` : model;
}
if (normalizedProvider === "anthropic") {
const anthropicAliases: Record<string, string> = {
"opus-4.8": "claude-opus-4-8",
opus: "claude-opus-4-8",
"opus-4.6": "claude-opus-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
};
return anthropicAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
}
if (normalizedProvider === "vercel-ai-gateway") {
const vercelAliases: Record<string, string> = {
"opus-4.6": "claude-opus-4-6",
"sonnet-4.6": "claude-sonnet-4-6",
};
const aliased = vercelAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
return normalizeLowercaseStringOrEmpty(aliased).startsWith("claude-")
? `anthropic/${aliased}`
: aliased;
}
if (normalizedProvider === "huggingface") {
const prefix = "huggingface/";
return normalizeLowercaseStringOrEmpty(model).startsWith(prefix)
? model.slice(prefix.length)
: model;
}
if (normalizedProvider === "nvidia") {
const trimmed = model.trim();
return trimmed && !trimmed.includes("/") ? `nvidia/${trimmed}` : model;
}
if (normalizedProvider === "xai") {
const xaiAliases: Record<string, string> = {
"grok-4-fast-reasoning": "grok-4-fast",
"grok-4-1-fast-reasoning": "grok-4-1-fast",
"grok-4.20-experimental-beta-0304-reasoning": "grok-4.20-beta-latest-reasoning",
"grok-4.20-experimental-beta-0304-non-reasoning": "grok-4.20-beta-latest-non-reasoning",
"grok-4.20-reasoning": "grok-4.20-beta-latest-reasoning",
"grok-4.20-non-reasoning": "grok-4.20-beta-latest-non-reasoning",
};
return xaiAliases[normalizeLowercaseStringOrEmpty(model)] ?? model;
}
if (normalizedProvider === "together") {
return normalizeTogetherModelId(model);
}
return model;
}
export function normalizeStaticProviderModelIdWithPolicies(
provider: string,
model: string,
policies?: ReadonlyMap<string, ManifestModelIdNormalizationProvider>,
): string {
const normalizedProvider = normalizeLowercaseStringOrEmpty(provider);
const manifestModelId = policies
? (normalizeProviderModelIdWithPolicies({
provider: normalizedProvider,
policies,
context: {
modelId: model,
},
}) ?? model)
: model;
return normalizeBuiltInProviderModelId(normalizedProvider, manifestModelId);
}
export function normalizeConfiguredProviderCatalogModelId(
provider: string,
model: string,
policies = getCurrentManifestModelIdNormalizationPolicies(),
): string {
const providerModel = normalizeStaticProviderModelIdWithPolicies(provider, model, policies);
return normalizeConfiguredProviderCatalogModelRef(providerModel);
}
export function normalizeConfiguredProviderCatalogModelRef(providerModel: string): string {
const googlePrefix = "google/";
if (!providerModel.startsWith(googlePrefix)) {
const slash = providerModel.indexOf("/");
if (slash <= 0 || slash >= providerModel.length - 1) {
return providerModel;
}
const prefix = providerModel.slice(0, slash + 1);
const suffix = providerModel.slice(slash + 1);
if (!suffix.startsWith(googlePrefix)) {
return providerModel;
}
const normalizedSuffix = normalizeGooglePreviewModelId(suffix);
return normalizedSuffix === suffix ? providerModel : `${prefix}${normalizedSuffix}`;
}
const modelId = providerModel.slice(googlePrefix.length);
const normalizedModelId = normalizeGooglePreviewModelId(modelId);
return normalizedModelId === modelId ? providerModel : `${googlePrefix}${normalizedModelId}`;
}
export {
collectManifestModelIdNormalizationPolicies,
getCurrentManifestModelIdNormalizationPolicies,
normalizeBuiltInProviderModelId,
normalizeConfiguredProviderCatalogModelId,
normalizeConfiguredProviderCatalogModelRef,
normalizeProviderModelIdWithPolicies,
normalizeStaticProviderModelIdWithPolicies,
setCurrentManifestModelIdNormalizationRecords,
type ManifestModelIdNormalizationProvider,
type ManifestModelIdNormalizationRecord,
} from "../../packages/model-catalog-core/src/provider-model-id-normalization.js";
+38
View File
@@ -237,6 +237,44 @@ export const sharedVitestConfig = {
find: "@openclaw/llm-core",
replacement: path.join(repoRoot, "packages", "llm-core", "src", "index.ts"),
},
{
find: "@openclaw/model-catalog-core/configured-model-refs",
replacement: path.join(
repoRoot,
"packages",
"model-catalog-core",
"src",
"configured-model-refs.ts",
),
},
{
find: "@openclaw/model-catalog-core/provider-id",
replacement: path.join(repoRoot, "packages", "model-catalog-core", "src", "provider-id.ts"),
},
{
find: "@openclaw/model-catalog-core/provider-model-id-normalization",
replacement: path.join(
repoRoot,
"packages",
"model-catalog-core",
"src",
"provider-model-id-normalization.ts",
),
},
{
find: "@openclaw/model-catalog-core/provider-model-id-normalize",
replacement: path.join(
repoRoot,
"packages",
"model-catalog-core",
"src",
"provider-model-id-normalize.ts",
),
},
{
find: "@openclaw/model-catalog-core",
replacement: path.join(repoRoot, "packages", "model-catalog-core", "src", "index.ts"),
},
{
find: "@openclaw/net-policy/ip",
replacement: path.join(repoRoot, "packages", "net-policy", "src", "ip.ts"),
+14
View File
@@ -36,6 +36,20 @@
"@openclaw/llm-core/*": ["./packages/llm-core/src/*"],
"@openclaw/llm-runtime": ["./packages/llm-runtime/src/index.ts"],
"@openclaw/llm-runtime/*": ["./packages/llm-runtime/src/*"],
"@openclaw/model-catalog-core": ["./packages/model-catalog-core/src/index.ts"],
"@openclaw/model-catalog-core/configured-model-refs": [
"./packages/model-catalog-core/src/configured-model-refs.ts"
],
"@openclaw/model-catalog-core/provider-id": [
"./packages/model-catalog-core/src/provider-id.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalization": [
"./packages/model-catalog-core/src/provider-model-id-normalization.ts"
],
"@openclaw/model-catalog-core/provider-model-id-normalize": [
"./packages/model-catalog-core/src/provider-model-id-normalize.ts"
],
"@openclaw/model-catalog-core/*": ["./packages/model-catalog-core/src/*"],
"@openclaw/gateway-client": ["./packages/gateway-client/src/index.ts"],
"@openclaw/gateway-client/*": ["./packages/gateway-client/src/*"],
"@openclaw/gateway-protocol": ["./packages/gateway-protocol/src/index.ts"],
+1
View File
@@ -16,6 +16,7 @@
"packages/llm-core/src/**/*.ts",
"packages/markdown-core/src/**/*.ts",
"packages/media-generation-core/src/**/*.ts",
"packages/model-catalog-core/src/**/*.ts",
"packages/memory-host-sdk/src/**/*.ts",
"packages/terminal-core/src/**/*.ts",
"src/video-generation/dashscope-compatible.ts",
+17
View File
@@ -466,6 +466,17 @@ function buildLlmCoreDistEntries(): Record<string, string> {
};
}
function buildModelCatalogCoreDistEntries(): Record<string, string> {
return {
index: "packages/model-catalog-core/src/index.ts",
"configured-model-refs": "packages/model-catalog-core/src/configured-model-refs.ts",
"provider-id": "packages/model-catalog-core/src/provider-id.ts",
"provider-model-id-normalization":
"packages/model-catalog-core/src/provider-model-id-normalization.ts",
"provider-model-id-normalize": "packages/model-catalog-core/src/provider-model-id-normalize.ts",
};
}
function buildLlmRuntimeDistEntries(): Record<string, string> {
return {
index: "packages/llm-runtime/src/index.ts",
@@ -650,6 +661,12 @@ export default defineConfig([
neverBundle: shouldExternalizeLlmCoreDependency,
},
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,
entry: buildModelCatalogCoreDistEntries(),
outDir: "packages/model-catalog-core/dist",
}),
nodeWorkspacePackageBuildConfig({
clean: true,
dts: RUN_NODE_SKIP_DTS_BUILD ? false : undefined,