mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix(models): resolve openrouter compat aliases (#68579)
* fix(models): resolve openrouter compat aliases * fix(models): cover openrouter free interactive alias * fix(models): mirror openrouter compat aliases in runtime resolver * fix(models): align openrouter free allowlist aliases
This commit is contained in:
@@ -173,6 +173,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Codex/gateway: fix gateway crash when the codex-acp subprocess terminates abruptly; an unhandled EPIPE on the child stdin stream now routes through graceful client shutdown, rejecting pending requests instead of propagating as an uncaught exception that crashes the entire gateway daemon and all connected channels. Fixes #67886. (#67947) thanks @openperf
|
||||
- Slack/streaming: resolve native streaming recipient teams from the inbound user when available, with a monitor-team fallback, so DM and shared-workspace streams target the right recipient more reliably.
|
||||
- OpenRouter/streaming: treat `reasoning_details.response.output_text` and `reasoning_details.response.text` as visible assistant output on OpenRouter-compatible completions streams, while keeping `reasoning.text` hidden and refusing to surface ambiguous bare `text` items by default so visible replies, thinking blocks, and tool calls can coexist in the same chunk. (#67410) Thanks @neeravmakwana.
|
||||
- Models/OpenRouter aliases: resolve `openrouter:auto` to the canonical `openrouter/auto` model and map `openrouter:free` to the first configured concrete `openrouter/...:free` model instead of mis-resolving these compatibility aliases under the default provider. (#57066) Thanks @sumiisiaran.
|
||||
|
||||
## 2026.4.14
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js";
|
||||
import { modelKey as sharedModelKey, normalizeStaticProviderModelId } from "./model-ref-shared.js";
|
||||
import {
|
||||
findNormalizedProviderKey,
|
||||
@@ -69,6 +70,7 @@ export function normalizeModelRef(
|
||||
}
|
||||
|
||||
type ParseModelRefOptions = ModelRefNormalizeOptions;
|
||||
const OPENROUTER_AUTO_COMPAT_ALIAS = "openrouter:auto";
|
||||
|
||||
export function parseModelRef(
|
||||
raw: string,
|
||||
@@ -79,6 +81,9 @@ export function parseModelRef(
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
if (normalizeLowercaseStringOrEmpty(trimmed) === OPENROUTER_AUTO_COMPAT_ALIAS) {
|
||||
return normalizeModelRef("openrouter", "auto", options);
|
||||
}
|
||||
const slash = trimmed.indexOf("/");
|
||||
if (slash === -1) {
|
||||
return normalizeModelRef(defaultProvider, trimmed, options);
|
||||
|
||||
58
src/agents/model-selection-resolve.test.ts
Normal file
58
src/agents/model-selection-resolve.test.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { resolveAllowedModelRef, resolveConfiguredModelRef } from "./model-selection-resolve.js";
|
||||
|
||||
describe("model-selection-resolve OpenRouter compat aliases", () => {
|
||||
it("resolves openrouter:auto through the canonical OpenRouter auto model", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter:auto" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-sonnet-4-6",
|
||||
}),
|
||||
).toEqual({ provider: "openrouter", model: "openrouter/auto" });
|
||||
});
|
||||
|
||||
it("resolves openrouter:free through the runtime allowlist path", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openrouter/meta-llama/llama-3.3-70b-instruct:free": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const catalog = [
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "meta-llama/llama-3.3-70b-instruct:free",
|
||||
name: "Llama 3.3 70B Free",
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveAllowedModelRef({
|
||||
cfg,
|
||||
catalog,
|
||||
raw: "openrouter:free",
|
||||
defaultProvider: "anthropic",
|
||||
}),
|
||||
).toEqual({
|
||||
ref: {
|
||||
provider: "openrouter",
|
||||
model: "meta-llama/llama-3.3-70b-instruct:free",
|
||||
},
|
||||
key: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,13 +15,17 @@ import type { ModelCatalogEntry } from "./model-catalog.types.js";
|
||||
import { splitTrailingAuthProfile } from "./model-ref-profile.js";
|
||||
import {
|
||||
type ModelRef,
|
||||
findNormalizedProviderValue,
|
||||
modelKey,
|
||||
normalizeModelRef,
|
||||
normalizeProviderId,
|
||||
parseModelRef,
|
||||
} from "./model-selection-normalize.js";
|
||||
|
||||
let log: ReturnType<typeof createSubsystemLogger> | null = null;
|
||||
|
||||
const OPENROUTER_COMPAT_FREE_ALIAS = "openrouter:free";
|
||||
|
||||
function getLog(): ReturnType<typeof createSubsystemLogger> {
|
||||
log ??= createSubsystemLogger("model-selection");
|
||||
return log;
|
||||
@@ -32,6 +36,81 @@ export type ModelAliasIndex = {
|
||||
byKey: Map<string, string[]>;
|
||||
};
|
||||
|
||||
function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean {
|
||||
return ref.provider === "openrouter" && ref.model.includes("/") && ref.model.endsWith(":free");
|
||||
}
|
||||
|
||||
function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const raw of Object.keys(configuredModels)) {
|
||||
if (!raw.includes("/")) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseModelRef(raw, params.defaultProvider, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (parsed && isConcreteOpenRouterFreeModelRef(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
const openrouterProviderConfig = findNormalizedProviderValue(
|
||||
params.cfg.models?.providers,
|
||||
"openrouter",
|
||||
);
|
||||
for (const entry of openrouterProviderConfig?.models ?? []) {
|
||||
const modelId = entry?.id?.trim();
|
||||
if (!modelId || !modelId.includes("/") || !modelId.endsWith(":free")) {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelRef("openrouter", modelId, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(params.raw);
|
||||
if (normalized === "openrouter:auto") {
|
||||
return normalizeModelRef("openrouter", "auto", {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
if (normalized !== OPENROUTER_COMPAT_FREE_ALIAS || !params.cfg) {
|
||||
return null;
|
||||
}
|
||||
return resolveConfiguredOpenRouterCompatFreeRef({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
function parseModelRefWithCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
return (
|
||||
resolveConfiguredOpenRouterCompatAlias(params) ??
|
||||
parseModelRef(params.raw, params.defaultProvider, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function sanitizeModelWarningValue(value: string): string {
|
||||
const stripped = value ? stripAnsi(value) : "";
|
||||
let controlBoundary = -1;
|
||||
@@ -113,8 +192,16 @@ export function inferUniqueProviderFromConfiguredModels(params: {
|
||||
return providers.values().next().value;
|
||||
}
|
||||
|
||||
function resolveAllowlistModelKey(raw: string, defaultProvider: string): string | null {
|
||||
const parsed = parseModelRef(raw, defaultProvider);
|
||||
function resolveAllowlistModelKey(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
}): string | null {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: params.raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
@@ -132,7 +219,11 @@ export function buildConfiguredAllowlistKeys(params: {
|
||||
|
||||
const keys = new Set<string>();
|
||||
for (const raw of rawAllowlist) {
|
||||
const key = resolveAllowlistModelKey(raw, params.defaultProvider);
|
||||
const key = resolveAllowlistModelKey({
|
||||
cfg: params.cfg,
|
||||
raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (key) {
|
||||
keys.add(key);
|
||||
}
|
||||
@@ -150,7 +241,10 @@ export function buildModelAliasIndex(params: {
|
||||
|
||||
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||
const parsed = parseModelRef(keyRaw, params.defaultProvider, {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: keyRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -189,7 +283,11 @@ function buildModelCatalogMetadata(params: {
|
||||
const aliasByKey = new Map<string, string>();
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const [rawKey, entryRaw] of Object.entries(configuredModels)) {
|
||||
const key = resolveAllowlistModelKey(rawKey, params.defaultProvider);
|
||||
const key = resolveAllowlistModelKey({
|
||||
cfg: params.cfg,
|
||||
raw: rawKey,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
@@ -251,6 +349,7 @@ function buildSyntheticAllowedCatalogEntry(params: {
|
||||
}
|
||||
|
||||
export function resolveModelRefFromString(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
@@ -267,7 +366,10 @@ export function resolveModelRefFromString(params: {
|
||||
return { ref: aliasMatch.ref, alias: aliasMatch.alias };
|
||||
}
|
||||
}
|
||||
const parsed = parseModelRef(model, params.defaultProvider, {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: model,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -291,6 +393,16 @@ export function resolveConfiguredModelRef(params: {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!trimmed.includes("/")) {
|
||||
const openRouterCompatRef = resolveConfiguredOpenRouterCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (openRouterCompatRef) {
|
||||
return openRouterCompatRef;
|
||||
}
|
||||
|
||||
const aliasKey = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
|
||||
if (aliasMatch) {
|
||||
@@ -314,6 +426,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
}
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
@@ -362,7 +475,11 @@ export function buildAllowedModelSet(params: {
|
||||
const defaultModel = params.defaultModel?.trim();
|
||||
const defaultRef =
|
||||
defaultModel && params.defaultProvider
|
||||
? parseModelRef(defaultModel, params.defaultProvider)
|
||||
? parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: defaultModel,
|
||||
defaultProvider: params.defaultProvider,
|
||||
})
|
||||
: null;
|
||||
const defaultKey = defaultRef ? modelKey(defaultRef.provider, defaultRef.model) : undefined;
|
||||
const catalogKeys = new Set(catalog.map((entry) => modelKey(entry.provider, entry.id)));
|
||||
@@ -381,7 +498,11 @@ export function buildAllowedModelSet(params: {
|
||||
const allowedKeys = new Set<string>();
|
||||
const syntheticCatalogEntries = new Map<string, ModelCatalogEntry>();
|
||||
for (const raw of rawAllowlist) {
|
||||
const parsed = parseModelRef(raw, params.defaultProvider);
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
@@ -394,7 +515,11 @@ export function buildAllowedModelSet(params: {
|
||||
}
|
||||
|
||||
for (const fallback of resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model)) {
|
||||
const parsed = parseModelRef(fallback, params.defaultProvider);
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: fallback,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
@@ -523,6 +648,7 @@ export function resolveAllowedModelRef(params: {
|
||||
: params.defaultProvider;
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: effectiveDefaultProvider,
|
||||
aliasIndex,
|
||||
@@ -560,6 +686,7 @@ export function resolveHooksGmailModel(params: {
|
||||
});
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: hooksModel,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
|
||||
@@ -261,6 +261,12 @@ describe("model-selection", () => {
|
||||
defaultProvider: "openai",
|
||||
expected: { provider: "openai", model: "gpt-5.4" },
|
||||
},
|
||||
{
|
||||
name: "normalizes the openrouter:auto compatibility alias",
|
||||
variants: ["openrouter:auto"],
|
||||
defaultProvider: "anthropic",
|
||||
expected: { provider: "openrouter", model: "openrouter/auto" },
|
||||
},
|
||||
{
|
||||
name: "preserves openrouter native model prefixes",
|
||||
variants: ["openrouter/aurora-alpha"],
|
||||
@@ -1118,6 +1124,175 @@ describe("model-selection", () => {
|
||||
resetLogger();
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves openrouter:auto through the canonical OpenRouter auto model", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter:auto" },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(result).toEqual({ provider: "openrouter", model: "openrouter/auto" });
|
||||
});
|
||||
|
||||
it("resolves openrouter:free to the first configured concrete OpenRouter free model", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter:free" },
|
||||
models: {
|
||||
"openrouter/meta-llama/llama-3.3-70b-instruct:free": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
provider: "openrouter",
|
||||
model: "meta-llama/llama-3.3-70b-instruct:free",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves openrouter:free from configured OpenRouter provider models when needed", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: { primary: "openrouter:free" },
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openrouter: {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
models: [
|
||||
{
|
||||
id: "deepseek/deepseek-r1-0528:free",
|
||||
name: "DeepSeek R1 Free",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const result = resolveConfiguredModelRef({
|
||||
cfg,
|
||||
defaultProvider: "anthropic",
|
||||
defaultModel: "claude-sonnet-4-6",
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
provider: "openrouter",
|
||||
model: "deepseek/deepseek-r1-0528:free",
|
||||
});
|
||||
});
|
||||
|
||||
it("resolves openrouter:free through the allowed-model interactive path", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openrouter/meta-llama/llama-3.3-70b-instruct:free": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const catalog = [
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "meta-llama/llama-3.3-70b-instruct:free",
|
||||
name: "Llama 3.3 70B Free",
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveAllowedModelRef({
|
||||
cfg,
|
||||
catalog,
|
||||
raw: "openrouter:free",
|
||||
defaultProvider: "anthropic",
|
||||
}),
|
||||
).toEqual({
|
||||
ref: {
|
||||
provider: "openrouter",
|
||||
model: "meta-llama/llama-3.3-70b-instruct:free",
|
||||
},
|
||||
key: "openrouter/meta-llama/llama-3.3-70b-instruct:free",
|
||||
});
|
||||
});
|
||||
|
||||
it("treats raw openrouter:free allowlist entries as allowed in the legacy resolver path", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openrouter:free": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
models: {
|
||||
providers: {
|
||||
openrouter: {
|
||||
baseUrl: "https://openrouter.ai/api/v1",
|
||||
models: [
|
||||
{
|
||||
id: "deepseek/deepseek-r1-0528:free",
|
||||
name: "DeepSeek R1 Free",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
const catalog = [
|
||||
{
|
||||
provider: "openrouter",
|
||||
id: "deepseek/deepseek-r1-0528:free",
|
||||
name: "DeepSeek R1 Free",
|
||||
},
|
||||
];
|
||||
|
||||
expect(
|
||||
resolveAllowedModelRef({
|
||||
cfg,
|
||||
catalog,
|
||||
raw: "openrouter:free",
|
||||
defaultProvider: "anthropic",
|
||||
}),
|
||||
).toEqual({
|
||||
ref: {
|
||||
provider: "openrouter",
|
||||
model: "deepseek/deepseek-r1-0528:free",
|
||||
},
|
||||
key: "openrouter/deepseek/deepseek-r1-0528:free",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveThinkingDefault", () => {
|
||||
|
||||
@@ -39,6 +39,8 @@ function getLog(): ReturnType<typeof createSubsystemLogger> {
|
||||
return log;
|
||||
}
|
||||
|
||||
const OPENROUTER_COMPAT_FREE_ALIAS = "openrouter:free";
|
||||
|
||||
export type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "adaptive";
|
||||
|
||||
export type ModelAliasIndex = {
|
||||
@@ -241,14 +243,104 @@ export function inferUniqueProviderFromConfiguredModels(params: {
|
||||
return providers.values().next().value;
|
||||
}
|
||||
|
||||
export function resolveAllowlistModelKey(raw: string, defaultProvider: string): string | null {
|
||||
const parsed = parseModelRef(raw, defaultProvider);
|
||||
export function resolveAllowlistModelKey(
|
||||
raw: string,
|
||||
defaultProvider: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): string | null {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg,
|
||||
raw,
|
||||
defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
return null;
|
||||
}
|
||||
return modelKey(parsed.provider, parsed.model);
|
||||
}
|
||||
|
||||
function isConcreteOpenRouterFreeModelRef(ref: ModelRef): boolean {
|
||||
return ref.provider === "openrouter" && ref.model.includes("/") && ref.model.endsWith(":free");
|
||||
}
|
||||
|
||||
function resolveConfiguredOpenRouterCompatFreeRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const raw of Object.keys(configuredModels)) {
|
||||
if (!raw.includes("/")) {
|
||||
continue;
|
||||
}
|
||||
const parsed = parseModelRef(raw, params.defaultProvider, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (parsed && isConcreteOpenRouterFreeModelRef(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
|
||||
const openrouterProviderConfig = findNormalizedProviderValue(
|
||||
params.cfg.models?.providers,
|
||||
"openrouter",
|
||||
);
|
||||
for (const entry of openrouterProviderConfig?.models ?? []) {
|
||||
const modelId = entry?.id?.trim();
|
||||
if (!modelId || !modelId.includes("/") || !modelId.endsWith(":free")) {
|
||||
continue;
|
||||
}
|
||||
return normalizeModelRef("openrouter", modelId, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveConfiguredOpenRouterCompatAlias(params: {
|
||||
cfg: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
const normalized = normalizeLowercaseStringOrEmpty(params.raw);
|
||||
if (normalized === "openrouter:auto") {
|
||||
return normalizeModelRef("openrouter", "auto", {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
if (normalized !== OPENROUTER_COMPAT_FREE_ALIAS) {
|
||||
return null;
|
||||
}
|
||||
return resolveConfiguredOpenRouterCompatFreeRef({
|
||||
cfg: params.cfg,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
}
|
||||
|
||||
function parseModelRefWithCompatAlias(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
allowPluginNormalization?: boolean;
|
||||
}): ModelRef | null {
|
||||
return (
|
||||
(params.cfg
|
||||
? resolveConfiguredOpenRouterCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: params.raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})
|
||||
: null) ??
|
||||
parseModelRef(params.raw, params.defaultProvider, {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function buildConfiguredAllowlistKeys(params: {
|
||||
cfg: OpenClawConfig | undefined;
|
||||
defaultProvider: string;
|
||||
@@ -260,7 +352,7 @@ export function buildConfiguredAllowlistKeys(params: {
|
||||
|
||||
const keys = new Set<string>();
|
||||
for (const raw of rawAllowlist) {
|
||||
const key = resolveAllowlistModelKey(raw, params.defaultProvider);
|
||||
const key = resolveAllowlistModelKey(raw, params.defaultProvider, params.cfg);
|
||||
if (key) {
|
||||
keys.add(key);
|
||||
}
|
||||
@@ -278,7 +370,10 @@ export function buildModelAliasIndex(params: {
|
||||
|
||||
const rawModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const [keyRaw, entryRaw] of Object.entries(rawModels)) {
|
||||
const parsed = parseModelRef(keyRaw, params.defaultProvider, {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: keyRaw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -317,7 +412,7 @@ function buildModelCatalogMetadata(params: {
|
||||
const aliasByKey = new Map<string, string>();
|
||||
const configuredModels = params.cfg.agents?.defaults?.models ?? {};
|
||||
for (const [rawKey, entryRaw] of Object.entries(configuredModels)) {
|
||||
const key = resolveAllowlistModelKey(rawKey, params.defaultProvider);
|
||||
const key = resolveAllowlistModelKey(rawKey, params.defaultProvider, params.cfg);
|
||||
if (!key) {
|
||||
continue;
|
||||
}
|
||||
@@ -379,6 +474,7 @@ function buildSyntheticAllowedCatalogEntry(params: {
|
||||
}
|
||||
|
||||
export function resolveModelRefFromString(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
raw: string;
|
||||
defaultProvider: string;
|
||||
aliasIndex?: ModelAliasIndex;
|
||||
@@ -395,7 +491,10 @@ export function resolveModelRefFromString(params: {
|
||||
return { ref: aliasMatch.ref, alias: aliasMatch.alias };
|
||||
}
|
||||
}
|
||||
const parsed = parseModelRef(model, params.defaultProvider, {
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: model,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!parsed) {
|
||||
@@ -419,6 +518,16 @@ export function resolveConfiguredModelRef(params: {
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (!trimmed.includes("/")) {
|
||||
const openrouterCompatRef = resolveConfiguredOpenRouterCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
allowPluginNormalization: params.allowPluginNormalization,
|
||||
});
|
||||
if (openrouterCompatRef) {
|
||||
return openrouterCompatRef;
|
||||
}
|
||||
|
||||
const aliasKey = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
const aliasMatch = aliasIndex.byAlias.get(aliasKey);
|
||||
if (aliasMatch) {
|
||||
@@ -443,6 +552,7 @@ export function resolveConfiguredModelRef(params: {
|
||||
}
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
@@ -569,7 +679,11 @@ export function buildAllowedModelSet(params: {
|
||||
const defaultModel = params.defaultModel?.trim();
|
||||
const defaultRef =
|
||||
defaultModel && params.defaultProvider
|
||||
? parseModelRef(defaultModel, params.defaultProvider)
|
||||
? parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: defaultModel,
|
||||
defaultProvider: params.defaultProvider,
|
||||
})
|
||||
: null;
|
||||
const defaultKey = defaultRef ? modelKey(defaultRef.provider, defaultRef.model) : undefined;
|
||||
const catalogKeys = new Set(catalog.map((entry) => modelKey(entry.provider, entry.id)));
|
||||
@@ -588,7 +702,11 @@ export function buildAllowedModelSet(params: {
|
||||
const allowedKeys = new Set<string>();
|
||||
const syntheticCatalogEntries = new Map<string, ModelCatalogEntry>();
|
||||
for (const raw of rawAllowlist) {
|
||||
const parsed = parseModelRef(raw, params.defaultProvider);
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
@@ -606,7 +724,11 @@ export function buildAllowedModelSet(params: {
|
||||
cfg: params.cfg,
|
||||
agentId: params.agentId,
|
||||
})) {
|
||||
const parsed = parseModelRef(fallback, params.defaultProvider);
|
||||
const parsed = parseModelRefWithCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: fallback,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (parsed) {
|
||||
const key = modelKey(parsed.provider, parsed.model);
|
||||
allowedKeys.add(key);
|
||||
@@ -728,6 +850,25 @@ export function resolveAllowedModelRef(params: {
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
|
||||
const openrouterCompatRef = resolveConfiguredOpenRouterCompatAlias({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: params.defaultProvider,
|
||||
});
|
||||
if (openrouterCompatRef) {
|
||||
const status = getModelRefStatus({
|
||||
cfg: params.cfg,
|
||||
catalog: params.catalog,
|
||||
ref: openrouterCompatRef,
|
||||
defaultProvider: params.defaultProvider,
|
||||
defaultModel: params.defaultModel,
|
||||
});
|
||||
if (!status.allowed) {
|
||||
return { error: `model not allowed: ${status.key}` };
|
||||
}
|
||||
return { ref: openrouterCompatRef, key: status.key };
|
||||
}
|
||||
|
||||
// When the model string has no provider prefix ("/"), try to infer the
|
||||
// correct provider from the configured allowlist before falling back to the
|
||||
// session's current default provider. This prevents provider prefix drift
|
||||
@@ -738,6 +879,7 @@ export function resolveAllowedModelRef(params: {
|
||||
: params.defaultProvider;
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: trimmed,
|
||||
defaultProvider: effectiveDefaultProvider,
|
||||
aliasIndex,
|
||||
@@ -794,6 +936,7 @@ export function resolveHooksGmailModel(params: {
|
||||
});
|
||||
|
||||
const resolved = resolveModelRefFromString({
|
||||
cfg: params.cfg,
|
||||
raw: hooksModel,
|
||||
defaultProvider: params.defaultProvider,
|
||||
aliasIndex,
|
||||
|
||||
Reference in New Issue
Block a user