From c4014c0092b9fbc9f8ad676fde8fdb5fbecc9137 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Mon, 12 Jan 2026 22:48:37 -0500 Subject: [PATCH 1/2] fix: treat credential validation failures as auth errors for fallback (#761) --- CHANGELOG.md | 3 ++- src/agents/model-fallback.test.ts | 22 ++++++++++++++++++++++ src/agents/pi-embedded-helpers.ts | 3 +++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 336792653..8ea80235a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,13 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes +- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#761 — thanks @pilkster) - Telegram: persist polling update offsets across restarts to avoid duplicate updates. (#739 — thanks @thewilloftheshadow) - Discord: avoid duplicate message/reaction listeners on monitor reloads. (#744 — thanks @thewilloftheshadow) - System events: include local timestamps when events are injected into prompts. (#245 — thanks @thewilloftheshadow) - Cron: accept `jobId` aliases for cron update/run/remove params in gateway validation. (#252 — thanks @thewilloftheshadow) - Models/Google: normalize Gemini 3 model ids to preview variants before runtime selection. (#795 — thanks @thewilloftheshadow) -- TUI: keep the last streamed response instead of replacing it with “(no output)”. (#747 — thanks @thewilloftheshadow) +- TUI: keep the last streamed response instead of replacing it with "(no output)". (#747 — thanks @thewilloftheshadow) - Slack: accept slash commands with or without leading `/` for custom command configs. (#798 — thanks @thewilloftheshadow) - Onboarding/Configure: refuse to proceed with invalid configs; run `clawdbot doctor` first to avoid wiping custom fields. (#764 — thanks @mukhtharcm) - Anthropic: merge consecutive user turns (preserve newest metadata) before validation to avoid “Incorrect role information” errors. (#804 — thanks @ThomsenDrake) diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 77da25937..6f0b3fb30 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -102,6 +102,28 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("falls back on credential validation errors", async () => { + const cfg = makeCfg(); + const run = vi + .fn() + .mockRejectedValueOnce( + new Error('No credentials found for profile "anthropic:claude-cli".'), + ) + .mockResolvedValueOnce("ok"); + + const result = await runWithModelFallback({ + cfg, + provider: "anthropic", + model: "claude-opus-4", + run, + }); + + expect(result.result).toBe("ok"); + expect(run).toHaveBeenCalledTimes(2); + expect(run.mock.calls[1]?.[0]).toBe("anthropic"); + expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); + }); + it("appends the configured primary as a last fallback", async () => { const cfg = makeCfg({ agents: { diff --git a/src/agents/pi-embedded-helpers.ts b/src/agents/pi-embedded-helpers.ts index 8a9d01284..b3b797230 100644 --- a/src/agents/pi-embedded-helpers.ts +++ b/src/agents/pi-embedded-helpers.ts @@ -361,6 +361,9 @@ const ERROR_PATTERNS = { "token has expired", /\b401\b/, /\b403\b/, + // Credential validation failures should trigger fallback (#761) + "no credentials found", + "no api key found", ], format: [ "invalid_request_error", From 2c2ca7f03bfb26f3cb77abcb162477a256fa0444 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 13 Jan 2026 04:02:47 +0000 Subject: [PATCH 2/2] fix: treat credential validation errors as auth errors (#822) (thanks @sebslight) --- CHANGELOG.md | 2 +- src/agents/model-fallback.test.ts | 20 ++++++++++++++++++++ src/telegram/monitor.ts | 2 +- src/telegram/update-offset-store.test.ts | 6 +++--- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ea80235a..85d231035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ - Memory: allow custom OpenAI-compatible embedding endpoints for memory search (remote baseUrl/apiKey/headers). (#819 — thanks @mukhtharcm) ### Fixes -- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#761 — thanks @pilkster) +- Fallback: treat credential validation failures ("no credentials found", "no API key found") as auth errors that trigger model fallback. (#822 — thanks @sebslight) - Telegram: persist polling update offsets across restarts to avoid duplicate updates. (#739 — thanks @thewilloftheshadow) - Discord: avoid duplicate message/reaction listeners on monitor reloads. (#744 — thanks @thewilloftheshadow) - System events: include local timestamps when events are injected into prompts. (#245 — thanks @thewilloftheshadow) diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 6f0b3fb30..b9786f4b8 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -124,6 +124,26 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("falls back on missing API key errors", async () => { + const cfg = makeCfg(); + const run = vi + .fn() + .mockRejectedValueOnce(new Error("No API key found for profile openai.")) + .mockResolvedValueOnce("ok"); + + const result = await runWithModelFallback({ + cfg, + provider: "openai", + model: "gpt-4.1-mini", + run, + }); + + expect(result.result).toBe("ok"); + expect(run).toHaveBeenCalledTimes(2); + expect(run.mock.calls[1]?.[0]).toBe("anthropic"); + expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); + }); + it("appends the configured primary as a last fallback", async () => { const cfg = makeCfg({ agents: { diff --git a/src/telegram/monitor.ts b/src/telegram/monitor.ts index 908c8de39..a14cda10d 100644 --- a/src/telegram/monitor.ts +++ b/src/telegram/monitor.ts @@ -6,11 +6,11 @@ import { formatDurationMs } from "../infra/format-duration.js"; import type { RuntimeEnv } from "../runtime.js"; import { resolveTelegramAccount } from "./accounts.js"; import { createTelegramBot } from "./bot.js"; +import { makeProxyFetch } from "./proxy.js"; import { readTelegramUpdateOffset, writeTelegramUpdateOffset, } from "./update-offset-store.js"; -import { makeProxyFetch } from "./proxy.js"; import { startTelegramWebhook } from "./webhook.js"; export type MonitorTelegramOpts = { diff --git a/src/telegram/update-offset-store.test.ts b/src/telegram/update-offset-store.test.ts index 2483c4101..b028802bc 100644 --- a/src/telegram/update-offset-store.test.ts +++ b/src/telegram/update-offset-store.test.ts @@ -34,9 +34,9 @@ describe("telegram update offset store", () => { updateId: 421, }); - expect( - await readTelegramUpdateOffset({ accountId: "primary" }), - ).toBe(421); + expect(await readTelegramUpdateOffset({ accountId: "primary" })).toBe( + 421, + ); }); }); });