Skip to content

fix(telegram): allow fallback models in /model validation#40105

Merged
velvet-shark merged 4 commits intoopenclaw:mainfrom
avirweb:fix/telegram-model-button-fallback-allowlist
Mar 12, 2026
Merged

fix(telegram): allow fallback models in /model validation#40105
velvet-shark merged 4 commits intoopenclaw:mainfrom
avirweb:fix/telegram-model-button-fallback-allowlist

Conversation

@avirweb
Copy link
Contributor

@avirweb avirweb commented Mar 8, 2026

PR Description: Fix Telegram Model Button Selection

Problem Statement

Telegram inline button model selection was broken in two ways:

Issue 1: Model buttons don't actually change the model (CRITICAL BUG)

When clicking a model button in Telegram (e.g., "gpt-5.3-codex"), the button click was registered but the model never actually changed. The synthetic message approach (/model provider/model via processMessage()) silently failed - the callback query was answered, no error was shown, but the model remained unchanged.

Root Cause: The synthetic message processing pipeline doesn't properly trigger model overrides in the session store. The code created a fake /model message and passed it through processMessage(), but this never actually updated the session's model override fields.

Issue 2: Fallback models not shown in model picker

Fallback models configured in agents.defaults.model.fallbacks were not included in the /models command output, making them inaccessible via the Telegram UI.

Solution

Fix 1: Direct Session Store Model Override (bot-handlers.ts)

Replaced the broken synthetic message approach with direct session store updates:

Key Changes:

  • Replaced synthetic /model command with updateSessionStore() + applyModelOverrideToSessionEntry()
  • Added visual feedback: success message with ✅ emoji, error with ❌
  • Inline keyboard removed after selection
  • Auth preserved: All authorization checks (inlineButtonsScope, sender authorization) are applied before reaching the model handler (lines 1108-1157)
  • Default behavior preserved: Selecting the default model clears the override (isDefault: true), matching /model command behavior

Code:

const isDefaultSelection =
  `${selection.provider}/${selection.model}` === defaultModelRef;

await updateSessionStore(storePath, (store) => {
  const entry = store[sessionKey] ?? {};
  store[sessionKey] = entry;
  applyModelOverrideToSessionEntry({
    entry,
    selection: {
      provider: selection.provider,
      model: selection.model,
      isDefault: isDefaultSelection, // Clear override if default selected
    },
  });
});

Fix 2: Include Fallback Models in Allowed Set (model-selection.ts)

Added fallback models to the allowed model set so they appear in the /models command.

Files Changed

  1. src/telegram/bot-handlers.ts - Fixed model button click with proper auth and default handling
  2. src/agents/model-selection.ts - Added fallback models to allowed set
  3. src/agents/model-selection.test.ts - Added tests for fallback model inclusion

Testing

Test Scenario 1: Model Button Click

  1. Type /models in Telegram
  2. Click a provider (e.g., "openai-codex")
  3. Click a model (e.g., "gpt-5.3-codex")
  4. Expected: "✅ Model changed to openai-codex/gpt-5.3-codex"
  5. Expected: Buttons disappear, next message uses selected model

Test Scenario 2: Select Default Model

  1. With default model "openai-codex/gpt-5.3-codex"
  2. Click that model in the picker
  3. Expected: "✅ Model reset to default" (clears override, doesn't pin)

Test Scenario 3: After /reset

  1. Type /reset to clear session
  2. Type /models
  3. Click a model
  4. Expected: Model changes successfully

Security Notes

  • Authorization is enforced via existing inlineButtonsScope and authorizeTelegramEventSender checks (lines 1108-1157)
  • Callbacks respect the same DM/group/allowlist policies as regular commands
  • No bypass of command access controls

Related Issues

Checklist

  • Bug fix (non-breaking)
  • Code follows project style
  • Tests added
  • Manual testing in Telegram
  • Auth checks preserved
  • Default behavior preserved

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Mar 8, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 8, 2026

Greptile Summary

This PR fixes a bug where fallback models configured under agents.defaults.model.fallbacks were not included in the allowed model set computed by buildAllowedModelSet, causing Telegram model picker validation to reject those models. The fix mirrors the existing explicit-allowlist loop and uses the same parseModelRef / syntheticCatalogEntries pattern, keeping the change minimal and consistent.

  • src/agents/model-selection.ts: After processing agents.defaults.models (explicit allowlist), a new loop now iterates over agents.defaults.model.fallbacks and inserts each normalized model key into allowedKeys and syntheticCatalogEntries. The typeof fallbackConfig === "object" guard correctly handles the case where model is configured as a plain string.
  • src/agents/model-selection.test.ts: Two regression tests are added. One test covers the happy path (fallback models appear in allowedKeys), and the other verifies that an empty fallbacks array is handled without error. The assertion allowedKeys.has("google/gemini-3-pro-preview") for a configured value of "google/gemini-3-pro" is correct but could use an inline comment to explain the normalization step (see inline comment).

Confidence Score: 5/5

  • Safe to merge — the change is minimal, consistent with existing patterns, and backed by passing regression tests.
  • The new block in buildAllowedModelSet follows the identical structure already used for the explicit-allowlist loop (same parseModelRef, same syntheticCatalogEntries guard, same allowedKeys.add). The typeof guard for the string-vs-object model config is correct. No existing behavior is altered; the change only widens the allowed set when fallbacks are configured. Two regression tests cover the new path and the empty-fallback edge case.
  • No files require special attention.

Last reviewed commit: 8ca7cb1


expect(result.allowedKeys.has("openai/gpt-4o")).toBe(true);
expect(result.allowedKeys.has("anthropic/claude-sonnet-4-6")).toBe(true);
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assertion key differs silently from configured fallback

The fallback is configured as "google/gemini-3-pro" but the assertion checks for "google/gemini-3-pro-preview". This is correct because normalizeGoogleModelId maps gemini-3-progemini-3-pro-preview, but without a comment a future reader may think there is a typo or that the assertion is wrong. Consider adding a brief inline comment to make the normalization explicit:

Suggested change
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true);
expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true); // normalizeGoogleModelId maps gemini-3-pro → gemini-3-pro-preview
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/model-selection.test.ts
Line: 350

Comment:
**Assertion key differs silently from configured fallback**

The fallback is configured as `"google/gemini-3-pro"` but the assertion checks for `"google/gemini-3-pro-preview"`. This is correct because `normalizeGoogleModelId` maps `gemini-3-pro``gemini-3-pro-preview`, but without a comment a future reader may think there is a typo or that the assertion is wrong. Consider adding a brief inline comment to make the normalization explicit:

```suggestion
      expect(result.allowedKeys.has("google/gemini-3-pro-preview")).toBe(true); // normalizeGoogleModelId maps gemini-3-pro → gemini-3-pro-preview
```

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@avirweb avirweb force-pushed the fix/telegram-model-button-fallback-allowlist branch from 8ca7cb1 to 6b244fa Compare March 8, 2026 17:27
@avirweb
Copy link
Contributor Author

avirweb commented Mar 8, 2026

CI note: current failing checks appear unrelated to this PR’s diff.\n\nThis PR changes only:\n- src/agents/model-selection.ts\n- src/agents/model-selection.test.ts\n\nCurrent failures from run 22826081850:\n- check: TypeScript error at src/agents/models-config.merge.test.ts:55 (missing baseUrl in ModelProviderConfig cast)\n- secrets: detect-secrets flags in unrelated existing files (e.g. src/agents/tools/web-search.ts, docs/perplexity.md, docs/tools/web.md, src/infra/git-commit.test.ts, test-fixtures/talk-config-contract.json, src/agents/tools/web-search.test.ts)\n\nI’m keeping this PR scoped to the Telegram fallback allowlist fix; happy to open a separate follow-up PR for the unrelated CI issues if preferred.

@openclaw-barnacle openclaw-barnacle bot added the channel: telegram Channel integration: telegram label Mar 10, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af799ba2a8

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1323 to +1327
await updateSessionStore(storePath, (store) => {
const sessionKey = sessionState.sessionKey;
const entry = store[sessionKey] ?? {};
store[sessionKey] = entry;
applyModelOverrideToSessionEntry({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Route model-selection callbacks through message auth gates

Persisting the selection directly with updateSessionStore bypasses the processMessage flow that applies Telegram command/DM access checks (buildTelegramMessageContext and enforceTelegramDmAccess). In configurations where inline buttons use callback-scope (inlineButtonsScope set to all, dm, or group), this lets a callback mutate session model state even when sending /model ... as a message would be rejected by policy. Please run the same auth path before writing overrides so callback actions cannot sidestep command access controls.

Useful? React with 👍 / 👎.

Comment on lines +1329 to +1333
selection: {
provider: selection.provider,
model: selection.model,
isDefault: false,
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear override when picker selects the default model

The callback path now hardcodes isDefault: false, so selecting the agent’s current default model via the picker always stores an explicit override instead of resetting to default behavior. Previously, the /model provider/model path derived isDefault and cleared overrides when the chosen model matched defaults; without that, sessions can get pinned to an old model and stop following later default-model updates.

Useful? React with 👍 / 👎.

@avirweb avirweb force-pushed the fix/telegram-model-button-fallback-allowlist branch from af799ba to a79ff5b Compare March 10, 2026 02:08
@avirweb
Copy link
Contributor Author

avirweb commented Mar 10, 2026

Thanks for the thorough review! I've addressed both issues:

1. Auth Checks

You're right to flag this. Looking at the code flow, the authorization checks are already performed before the model callback handler is reached:

  • Lines 1108-1114: inlineButtonsScope check (off/dm/group/all)
  • Lines 1119-1123: Scope enforcement (dm vs group)
  • Lines 1128-1141: Event authorization context + requireTopic check
  • Lines 1146-1157: Sender authorization via authorizeTelegramEventSender

By the time we reach the model selection code at line 1300+, all the same auth checks that would block a /model message have already been applied. The callback is only processed if the sender is authorized.

2. isDefault Behavior

Fixed! The code now properly detects when the selected model IS the default and sets isDefault: true, which tells applyModelOverrideToSessionEntry to clear the override rather than pinning it.

const isDefaultSelection =
  `${selection.provider}/${selection.model}` === defaultModelRef ||
  (selection.provider === "openai-codex" &&
    selection.model === defaultModelRef?.split("/")?.[1]);

This matches the /model command behavior: selecting the default model clears any explicit override so the session follows future default-model updates.

Updated commit: a79ff5b

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a79ff5b26a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1324 to +1330
const defaultModelRef =
typeof modelConfig === "string"
? modelConfig
: (modelConfig?.primary ?? sessionState.model);
const isDefaultSelection =
`${selection.provider}/${selection.model}` === defaultModelRef ||
(selection.provider === "openai-codex" &&

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve default model before deciding callback is a reset

The default check is using raw cfg.agents.defaults.model text (or sessionState.model) instead of the routed agent’s resolved default model ref, so valid default selections can be misclassified as non-default. In particular, agent-specific defaults (agents.list[*].model) and shorthand defaults like primary: "gpt-4o" won’t match ${selection.provider}/${selection.model}, which causes picker clicks on the effective default to persist an override instead of clearing it; that pins sessions and prevents them from following later default-model updates.

Useful? React with 👍 / 👎.

@avirweb avirweb force-pushed the fix/telegram-model-button-fallback-allowlist branch from a79ff5b to d66ad55 Compare March 10, 2026 02:20
@avirweb
Copy link
Contributor Author

avirweb commented Mar 10, 2026

Fixed! Now properly resolving the agent's default model before checking if the selection is a reset.

Changes:

  1. Look up agent-specific model config from agents.list first, then fall back to agents.defaults
  2. Resolve shorthand model refs (like "gpt-4o") to full "provider/model" format using the session's current provider
  3. Compare against the resolved default, not the raw config text

This handles:

  • Agent-specific defaults (agents.list[*].model)
  • Shorthand defaults (primary: "gpt-4o" → resolves to openai/gpt-4o)
  • Falls back to session's current model if no default configured

Code now properly detects when user clicks the effective default model and clears the override instead of persisting it.

Updated commit: d66ad55

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d66ad55acd

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const isDefaultSelection =
`${selection.provider}/${selection.model}` === resolvedDefaultRef;

await updateSessionStore(storePath, (store) => {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate callback model against current allowlist

Persisting selection.provider/selection.model directly to the session store here skips the allowlist validation that /model applies via the model-selection path (which rejects refs not in allowedModelKeys). This means a stale inline button can still pin a model after policy changes (for example, when agents.defaults.models is tightened or catalog entries are removed), because the callback payload is accepted and written without re-checking current allowed models.

Useful? React with 👍 / 👎.

@avirweb avirweb force-pushed the fix/telegram-model-button-fallback-allowlist branch from d66ad55 to 2d5c316 Compare March 10, 2026 02:47
@avirweb
Copy link
Contributor Author

avirweb commented Mar 10, 2026

Fixed! Added allowlist validation before persisting the model selection.

Changes:

const modelSet = byProvider.get(selection.provider);
if (!modelSet?.has(selection.model)) {
  await editMessageWithButtons(
    `❌ Model "${selection.provider}/${selection.model}" is not allowed.`,
    [],
  );
  return;
}

The byProvider map comes from buildModelsProviderData() which already filters to only allowed models. This ensures stale callback payloads for models that are no longer in the allowlist are rejected with an error message, matching the /model command behavior.

Updated commit: 2d5c316

@velvet-shark velvet-shark self-assigned this Mar 12, 2026
@velvet-shark velvet-shark force-pushed the fix/telegram-model-button-fallback-allowlist branch from 2d5c316 to 10674f2 Compare March 12, 2026 12:28
@openclaw-barnacle openclaw-barnacle bot added commands Command implementations size: M and removed size: S labels Mar 12, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 10674f2cbe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

MAG-4 and others added 4 commits March 12, 2026 13:51
The synthetic message approach used for Telegram model button callbacks
was silently failing - the callback was answered but the model never
actually changed. This replaces the broken synthetic /model command
with direct session store updates.

Changes:
- Replace processMessage() synthetic approach with updateSessionStore()
- Add applyModelOverrideToSessionEntry() to directly set model override
- Update resolveTelegramSessionState() to return sessionKey
- Add visual feedback with ✅/❌ emojis and bold text
- Remove inline keyboard after successful selection
- Fix isDefault detection: selecting default model now clears override
  instead of pinning it, matching /model command behavior
- Auth checks already applied before callback handler (lines 1108-1157)
- Properly resolve agent-specific default model from agents.list or
  agents.defaults to handle shorthand refs like 'gpt-4o'
- Add allowlist validation: verify model is in byProvider before
  persisting to prevent stale buttons from bypassing policy changes

Fixes the bug where clicking model buttons appeared to work but
didn't actually change the active model.

Test: Click /models → select provider → select model → verify
message shows 'Model changed to X' and next message uses that model.
@velvet-shark velvet-shark force-pushed the fix/telegram-model-button-fallback-allowlist branch from d9eaea1 to de07585 Compare March 12, 2026 12:55
@velvet-shark velvet-shark merged commit f2e28fc into openclaw:main Mar 12, 2026
12 checks passed
@velvet-shark
Copy link
Member

Merged via squash.

Thanks @avirweb!

gumadeiras pushed a commit that referenced this pull request Mar 12, 2026
Merged via squash.

Prepared head SHA: de07585
Co-authored-by: avirweb <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark
mrosmarin added a commit to mrosmarin/openclaw that referenced this pull request Mar 12, 2026
* main: (120 commits)
  docs: sync Feishu secretref credential matrix
  Subagents: stop retrying external completion timeouts (openclaw#41235) (openclaw#43847)
  Security: require Feishu webhook encrypt key (openclaw#44087)
  Hardening: normalize Unicode command obfuscation detection (openclaw#44091)
  Hardening: tighten preauth WebSocket handshake limits (openclaw#44089)
  Security: preserve Feishu reaction chat type (openclaw#44088)
  Hardening: require LINE webhook signatures (openclaw#44090)
  fix(mattermost): pass mediaLocalRoots through reply delivery (openclaw#44021)
  ci: tighten cache docs and node22 gate
  ci: restore PR pnpm cache fallback
  ci: harden pnpm sticky cache on PRs
  build: align Node 22 guidance with 22.16 minimum
  build: raise Node 22 compatibility floor to 22.16
  build: default to Node 24 and keep Node 22 compat
  Agents: adapt pi-ai oauth and payload hooks
  fix(status): resolve context window by provider-qualified key, prefer max on bare-id collision, solve openclaw#35976 (openclaw#36389)
  fix: carry observed overflow token counts into compaction (openclaw#40357)
  fix(telegram): allow fallback models in /model validation (openclaw#40105)
  feat(mattermost): add replyToMode support (off | first | all) (openclaw#29587)
  docs: add Kubernetes install guide, setup script, and manifests (openclaw#34492)
  ...
steipete pushed a commit to BruceMacD/openclaw that referenced this pull request Mar 13, 2026
…0105)

Merged via squash.

Prepared head SHA: de07585
Co-authored-by: avirweb <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark
Interstellar-code pushed a commit to Interstellar-code/operator1 that referenced this pull request Mar 16, 2026
…0105)

Merged via squash.

Prepared head SHA: de07585
Co-authored-by: avirweb <[email protected]>
Co-authored-by: velvet-shark <[email protected]>
Reviewed-by: @velvet-shark

(cherry picked from commit f2e28fc)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: telegram Channel integration: telegram commands Command implementations size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants