refactor(models): share strict model ref parsing (#99790)

This commit is contained in:
Dallin Romney 2026-07-04 12:10:06 -07:00 committed by GitHub
parent bd3ad2feb0
commit d102715bb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 44 additions and 52 deletions

View file

@ -1,23 +1,13 @@
// Media Generation Core module implements model ref behavior.
import { normalizeOptionalString } from "./string.js";
import {
parseProviderModelRef,
type ProviderModelRef,
} from "@openclaw/model-catalog-core/model-catalog-refs";
/** Provider/model pair parsed from a generation model reference like `provider/model`. */
export type ParsedGenerationModelRef = {
provider: string;
model: string;
};
export type ParsedGenerationModelRef = ProviderModelRef;
/** Parses strict generation model refs and rejects missing provider or model segments. */
export function parseGenerationModelRef(raw: string | undefined): ParsedGenerationModelRef | null {
const trimmed = normalizeOptionalString(raw);
if (!trimmed) {
return null;
}
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex === trimmed.length - 1) {
return null;
}
const provider = normalizeOptionalString(trimmed.slice(0, slashIndex));
const model = normalizeOptionalString(trimmed.slice(slashIndex + 1));
return provider && model ? { provider, model } : null;
return raw === undefined ? null : parseProviderModelRef(raw);
}

View file

@ -4,6 +4,7 @@ import {
buildModelCatalogMergeKey,
buildModelCatalogRef,
parseModelCatalogRef,
parseProviderModelRef,
} from "./model-catalog-refs.js";
describe("model catalog refs", () => {
@ -19,6 +20,13 @@ describe("model catalog refs", () => {
});
});
it("parses strict refs without normalizing provider or model casing", () => {
expect(parseProviderModelRef(" OpenRouter / Meta-Llama/Llama-3.3 ")).toEqual({
provider: "OpenRouter",
model: "Meta-Llama/Llama-3.3",
});
});
it.each(["", "openai", "/gpt-5.4", "openai/", " / gpt-5.4 "])(
"rejects incomplete ref %j",
(value) => {

View file

@ -8,6 +8,11 @@ export type ModelCatalogRef = {
modelId: string;
};
export type ProviderModelRef = {
provider: string;
model: string;
};
/** Normalize provider ids for catalog refs. */
export function normalizeModelCatalogProviderId(provider: string): string {
return normalizeLowercaseStringOrEmpty(provider);
@ -18,16 +23,28 @@ export function buildModelCatalogRef(provider: string, modelId: string): string
return `${normalizeModelCatalogProviderId(provider)}/${modelId}`;
}
/** Parse a strict provider/model catalog reference. */
export function parseModelCatalogRef(value: string): ModelCatalogRef | null {
/** Parse a strict provider/model reference without normalizing either segment. */
export function parseProviderModelRef(value: string): ProviderModelRef | null {
const trimmed = value.trim();
const slashIndex = trimmed.indexOf("/");
if (slashIndex <= 0 || slashIndex >= trimmed.length - 1) {
return null;
}
const provider = normalizeModelCatalogProviderId(trimmed.slice(0, slashIndex));
const modelId = trimmed.slice(slashIndex + 1).trim();
return provider && modelId ? { provider, modelId } : null;
const provider = trimmed.slice(0, slashIndex).trim();
const model = trimmed.slice(slashIndex + 1).trim();
return provider && model ? { provider, model } : null;
}
/** Parse a strict provider/model catalog reference. */
export function parseModelCatalogRef(value: string): ModelCatalogRef | null {
const parsed = parseProviderModelRef(value);
if (!parsed) {
return null;
}
return {
provider: normalizeModelCatalogProviderId(parsed.provider),
modelId: parsed.model,
};
}
/** Build a case-insensitive merge key for provider/model rows. */

View file

@ -1,4 +1,6 @@
// Voice model catalog helpers shared by TTS and realtime voice plugins.
import { parseModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
export type VoiceModelCapability = "tts" | "realtime_transcription" | "realtime_voice";
/** Capability flags advertised by a voice model catalog entry. */
@ -61,17 +63,8 @@ function normalizeTimeoutMs(value: unknown): number | undefined {
}
function parseVoiceModelRef(value: unknown): VoiceModelRef | undefined {
const raw = normalizeString(value);
if (!raw) {
return undefined;
}
const slashIndex = raw.indexOf("/");
if (slashIndex <= 0 || slashIndex === raw.length - 1) {
return undefined;
}
const provider = normalizeLowercaseString(raw.slice(0, slashIndex));
const model = normalizeString(raw.slice(slashIndex + 1));
return provider && model ? { provider, model } : undefined;
const parsed = typeof value === "string" ? parseModelCatalogRef(value) : null;
return parsed ? { provider: parsed.provider, model: parsed.modelId } : undefined;
}
function sameProvider(left: string | undefined, right: string | undefined): boolean {

View file

@ -4,6 +4,7 @@
* Agent execution uses this to choose a model/provider-specific runtime policy
* from agent entries, model catalog config, provider config, or QA overrides.
*/
import { parseModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import type { AgentModelEntryConfig } from "../config/types.agent-defaults.js";
import type { AgentRuntimePolicyConfig } from "../config/types.agents-shared.js";
@ -79,13 +80,7 @@ function normalizeModelIdForProvider(
}
function parseProviderModelKey(key: string): { provider: string; modelId: string } | undefined {
const slash = key.indexOf("/");
if (slash <= 0) {
return undefined;
}
const provider = normalizeProviderId(key.slice(0, slash));
const modelId = key.slice(slash + 1).trim();
return provider && modelId ? { provider, modelId } : undefined;
return parseModelCatalogRef(key) ?? undefined;
}
function resolveEffectiveProvider(

View file

@ -1,5 +1,5 @@
// Doctor checks for context engine host requirements against configured agent runtimes.
import { normalizeProviderId } from "@openclaw/model-catalog-core/provider-id";
import { parseModelCatalogRef } from "@openclaw/model-catalog-core/model-catalog-refs";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
import { normalizeEmbeddedAgentRuntime } from "../../../agents/agent-runtime-id.js";
import { resolveDefaultAgentDir } from "../../../agents/agent-scope-config.js";
@ -53,18 +53,7 @@ function normalizeRuntimeId(value: unknown): string | undefined {
}
function parseModelRef(value: unknown): { provider: string; modelId: string } | undefined {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
const slash = trimmed.indexOf("/");
if (slash <= 0 || slash >= trimmed.length - 1) {
return undefined;
}
return {
provider: normalizeProviderId(trimmed.slice(0, slash)),
modelId: trimmed.slice(slash + 1).trim(),
};
return typeof value === "string" ? (parseModelCatalogRef(value) ?? undefined) : undefined;
}
function listModelRefs(value: unknown): string[] {