Skip to content

Commit f2e28fc

Browse files
fix(telegram): allow fallback models in /model validation (openclaw#40105)
Merged via squash. Prepared head SHA: de07585 Co-authored-by: avirweb <[email protected]> Co-authored-by: velvet-shark <[email protected]> Reviewed-by: @velvet-shark
1 parent 171d2df commit f2e28fc

File tree

12 files changed

+313
-45
lines changed

12 files changed

+313
-45
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
3333
- Gateway/main-session routing: keep TUI and other `mode:UI` main-session sends on the internal surface when `deliver` is enabled, so replies no longer inherit the session's persisted Telegram/WhatsApp route. (#43918) Thanks @obviyus.
3434
- Doctor/gateway service audit: canonicalize service entrypoint paths before comparing them so symlink-vs-realpath installs no longer trigger false "entrypoint does not match the current install" repair prompts. (#43882) Thanks @ngutman.
3535
- Doctor/gateway service audit: earlier groundwork for this fix landed in the superseded #28338 branch. Thanks @realriphub.
36+
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
3637

3738
## 2026.3.11
3839

@@ -234,6 +235,7 @@ Docs: https://docs.openclaw.ai
234235
- CLI/skills tables: keep terminal table borders aligned for wide graphemes, use full reported terminal width, and switch a few ambiguous skill icons to Terminal-safe emoji so `openclaw skills` renders more consistently in Terminal.app and iTerm. Thanks @vincentkoc.
235236
- Memory/Gemini: normalize returned Gemini embeddings across direct query, direct batch, and async batch paths so memory search uses consistent vector handling for Gemini too. (#43409) Thanks @gumadeiras.
236237
- Agents/failover: recognize additional serialized network errno strings plus `EHOSTDOWN` and `EPIPE` structured codes so transient transport failures trigger timeout failover more reliably. (#42830) Thanks @jnMetaCode.
238+
- Telegram/model picker: make inline model button selections persist the chosen session model correctly, clear overrides when selecting the configured default, and include effective fallback models in `/models` button validation. (#40105) Thanks @avirweb.
237239

238240
## 2026.3.7
239241

src/agents/model-selection.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,98 @@ describe("model-selection", () => {
322322
{ provider: "anthropic", id: "claude-sonnet-4-6", name: "claude-sonnet-4-6" },
323323
]);
324324
});
325+
326+
it("includes fallback models in allowed set", () => {
327+
const cfg: OpenClawConfig = {
328+
agents: {
329+
defaults: {
330+
models: {
331+
"openai/gpt-4o": {},
332+
},
333+
model: {
334+
primary: "openai/gpt-4o",
335+
fallbacks: ["anthropic/claude-sonnet-4-6", "google/gemini-3-pro"],
336+
},
337+
},
338+
},
339+
} as OpenClawConfig;
340+
341+
const result = buildAllowedModelSet({
342+
cfg,
343+
catalog: [],
344+
defaultProvider: "openai",
345+
defaultModel: "gpt-4o",
346+
});
347+
348+
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
349+
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
350+
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true);
351+
expect(result.allowAny).toBe(false);
352+
});
353+
354+
it("handles empty fallbacks gracefully", () => {
355+
const cfg: OpenClawConfig = {
356+
agents: {
357+
defaults: {
358+
models: {
359+
"openai/gpt-4o": {},
360+
},
361+
model: {
362+
primary: "openai/gpt-4o",
363+
fallbacks: [],
364+
},
365+
},
366+
},
367+
} as OpenClawConfig;
368+
369+
const result = buildAllowedModelSet({
370+
cfg,
371+
catalog: [],
372+
defaultProvider: "openai",
373+
defaultModel: "gpt-4o",
374+
});
375+
376+
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
377+
expect(result.allowAny).toBe(false);
378+
});
379+
380+
it("prefers per-agent fallback overrides when agentId is provided", () => {
381+
const cfg: OpenClawConfig = {
382+
agents: {
383+
defaults: {
384+
models: {
385+
"openai/gpt-4o": {},
386+
},
387+
model: {
388+
primary: "openai/gpt-4o",
389+
fallbacks: ["google/gemini-3-pro"],
390+
},
391+
},
392+
list: [
393+
{
394+
id: "coder",
395+
model: {
396+
primary: "openai/gpt-4o",
397+
fallbacks: ["anthropic/claude-sonnet-4-6"],
398+
},
399+
},
400+
],
401+
},
402+
} as OpenClawConfig;
403+
404+
const result = buildAllowedModelSet({
405+
cfg,
406+
catalog: [],
407+
defaultProvider: "openai",
408+
defaultModel: "gpt-4o",
409+
agentId: "coder",
410+
});
411+
412+
expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
413+
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
414+
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(false);
415+
expect(result.allowAny).toBe(false);
416+
});
325417
});
326418

327419
describe("resolveAllowedModelRef", () => {

src/agents/model-selection.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,16 @@
11
import type { OpenClawConfig } from "../config/config.js";
2-
import { resolveAgentModelPrimaryValue, toAgentModelListLike } from "../config/model-input.js";
2+
import {
3+
resolveAgentModelFallbackValues,
4+
resolveAgentModelPrimaryValue,
5+
toAgentModelListLike,
6+
} from "../config/model-input.js";
37
import { createSubsystemLogger } from "../logging/subsystem.js";
48
import { sanitizeForLog } from "../terminal/ansi.js";
5-
import { resolveAgentConfig, resolveAgentEffectiveModelPrimary } from "./agent-scope.js";
9+
import {
10+
resolveAgentConfig,
11+
resolveAgentEffectiveModelPrimary,
12+
resolveAgentModelFallbacksOverride,
13+
} from "./agent-scope.js";
614
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "./defaults.js";
715
import type { ModelCatalogEntry } from "./model-catalog.js";
816
import { splitTrailingAuthProfile } from "./model-ref-profile.js";
@@ -382,6 +390,16 @@ export function resolveDefaultModelForAgent(params: {
382390
});
383391
}
384392

393+
function resolveAllowedFallbacks(params: { cfg: OpenClawConfig; agentId?: string }): string[] {
394+
if (params.agentId) {
395+
const override = resolveAgentModelFallbacksOverride(params.cfg, params.agentId);
396+
if (override !== undefined) {
397+
return override;
398+
}
399+
}
400+
return resolveAgentModelFallbackValues(params.cfg.agents?.defaults?.model);
401+
}
402+
385403
export function resolveSubagentConfiguredModelSelection(params: {
386404
cfg: OpenClawConfig;
387405
agentId: string;
@@ -419,6 +437,7 @@ export function buildAllowedModelSet(params: {
419437
catalog: ModelCatalogEntry[];
420438
defaultProvider: string;
421439
defaultModel?: string;
440+
agentId?: string;
422441
}): {
423442
allowAny: boolean;
424443
allowedCatalog: ModelCatalogEntry[];
@@ -469,6 +488,25 @@ export function buildAllowedModelSet(params: {
469488
}
470489
}
471490

491+
for (const fallback of resolveAllowedFallbacks({
492+
cfg: params.cfg,
493+
agentId: params.agentId,
494+
})) {
495+
const parsed = parseModelRef(String(fallback), params.defaultProvider);
496+
if (parsed) {
497+
const key = modelKey(parsed.provider, parsed.model);
498+
allowedKeys.add(key);
499+
500+
if (!catalogKeys.has(key) && !syntheticCatalogEntries.has(key)) {
501+
syntheticCatalogEntries.set(key, {
502+
id: parsed.model,
503+
name: parsed.model,
504+
provider: parsed.provider,
505+
});
506+
}
507+
}
508+
}
509+
472510
if (defaultKey) {
473511
allowedKeys.add(defaultKey);
474512
}

src/agents/tools/session-status-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ async function resolveModelOverride(params: {
151151
catalog,
152152
defaultProvider: currentProvider,
153153
defaultModel: currentModel,
154+
agentId: params.agentId,
154155
});
155156

156157
const resolved = resolveModelRefFromString({

src/auto-reply/reply/commands-models.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export async function buildModelsProviderData(
4949
catalog,
5050
defaultProvider: resolvedDefault.provider,
5151
defaultModel: resolvedDefault.model,
52+
agentId,
5253
});
5354

5455
const aliasIndex = buildModelAliasIndex({

src/auto-reply/reply/get-reply-directives.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ export async function resolveReplyDirectives(params: {
373373

374374
const modelState = await createModelSelectionState({
375375
cfg,
376+
agentId,
376377
agentCfg,
377378
sessionEntry,
378379
sessionStore,

src/auto-reply/reply/get-reply.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,7 @@ export async function getReplyFromConfig(
175175

176176
await applyResetModelOverride({
177177
cfg,
178+
agentId,
178179
resetTriggered,
179180
bodyStripped,
180181
sessionCtx,

src/auto-reply/reply/model-selection.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ function scoreFuzzyMatch(params: {
263263

264264
export async function createModelSelectionState(params: {
265265
cfg: OpenClawConfig;
266+
agentId?: string;
266267
agentCfg: NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]> | undefined;
267268
sessionEntry?: SessionEntry;
268269
sessionStore?: Record<string, SessionEntry>;
@@ -315,6 +316,7 @@ export async function createModelSelectionState(params: {
315316
catalog: modelCatalog,
316317
defaultProvider,
317318
defaultModel,
319+
agentId: params.agentId,
318320
});
319321
allowedModelCatalog = allowed.allowedCatalog;
320322
allowedModelKeys = allowed.allowedKeys;

src/auto-reply/reply/session-reset-model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ function applySelectionToSession(params: {
8787

8888
export async function applyResetModelOverride(params: {
8989
cfg: OpenClawConfig;
90+
agentId?: string;
9091
resetTriggered: boolean;
9192
bodyStripped?: string;
9293
sessionCtx: TemplateContext;
@@ -118,6 +119,7 @@ export async function applyResetModelOverride(params: {
118119
catalog,
119120
defaultProvider: params.defaultProvider,
120121
defaultModel: params.defaultModel,
122+
agentId: params.agentId,
121123
});
122124
const allowedModelKeys = allowed.allowedKeys;
123125
if (allowedModelKeys.size === 0) {

src/commands/agent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -950,6 +950,7 @@ async function agentCommandInternal(
950950
catalog: modelCatalog,
951951
defaultProvider,
952952
defaultModel,
953+
agentId: sessionAgentId,
953954
});
954955
allowedModelKeys = allowed.allowedKeys;
955956
allowedModelCatalog = allowed.allowedCatalog;

0 commit comments

Comments
 (0)