fix(FN-5584): restore planning fallback when primary provider API key is missing
The top-level promptWithFallback bypassed the session-attached rich fallback path that runs isRetryableModelSelectionError + swapPromptSession, so errors like "No API key for provider: anthropic" propagated without trying the configured planning fallback. Restore the dispatch with a WeakSet re-entry guard that preserves the FN-4900 recursion fix for plugin-runtime sessions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix triage/executor not swapping to the configured planning fallback model when the primary provider's API key is missing (or returns 401/403/rate-limit). The top-level `promptWithFallback` now delegates to the rich session-attached path (which runs `isRetryableModelSelectionError` and `swapPromptSession`), with a WeakSet re-entry guard preserving the FN-4900 recursion fix.
|
||||||
@@ -571,7 +571,14 @@ describe("promptWithFallback auto-compaction", () => {
|
|||||||
expect(mockPrompt.mock.calls[1]).toEqual(["test prompt", options]);
|
expect(mockPrompt.mock.calls[1]).toEqual(["test prompt", options]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses standalone prompt path even when session.promptWithFallback exists", async () => {
|
it("delegates to session.promptWithFallback when available so rich fallback logic runs", async () => {
|
||||||
|
// The session-attached promptWithFallback (set by createFnAgent at pi.ts:2012)
|
||||||
|
// is the only path that swaps to the configured fallbackModel on
|
||||||
|
// isRetryableModelSelectionError matches like "api key", 401/403, rate-limit, etc.
|
||||||
|
// Bypassing it (as the old standalone-only behavior did) silently dropped
|
||||||
|
// missing-API-key triage failures with no fallback attempt — see FN-5584.
|
||||||
|
// A re-entry guard in promptWithFallback prevents the recursion that
|
||||||
|
// FN-4900 originally guarded against.
|
||||||
const mockSessionPromptWithFallback = vi.fn().mockResolvedValue(undefined);
|
const mockSessionPromptWithFallback = vi.fn().mockResolvedValue(undefined);
|
||||||
const mockPrompt = vi.fn().mockResolvedValue(undefined);
|
const mockPrompt = vi.fn().mockResolvedValue(undefined);
|
||||||
const mockCompact = vi.fn();
|
const mockCompact = vi.fn();
|
||||||
@@ -583,8 +590,9 @@ describe("promptWithFallback auto-compaction", () => {
|
|||||||
|
|
||||||
await promptWithFallback(session, "test prompt");
|
await promptWithFallback(session, "test prompt");
|
||||||
|
|
||||||
expect(mockPrompt).toHaveBeenCalledTimes(1);
|
expect(mockSessionPromptWithFallback).toHaveBeenCalledTimes(1);
|
||||||
expect(mockSessionPromptWithFallback).not.toHaveBeenCalled();
|
expect(mockSessionPromptWithFallback).toHaveBeenCalledWith("test prompt", undefined);
|
||||||
|
expect(mockPrompt).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("handles context error patterns from various providers", async () => {
|
it("handles context error patterns from various providers", async () => {
|
||||||
|
|||||||
@@ -313,7 +313,32 @@ export async function promptSessionAndCheck(session: AgentSession, prompt: strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Re-entry guard for the top-level dispatcher below. When `session.promptWithFallback`
|
||||||
|
// is attached (e.g. by `createFnAgent` at pi.ts:2012, where the rich model-swap +
|
||||||
|
// `isRetryableModelSelectionError` path lives), we want top-level callers like
|
||||||
|
// triage/executor to flow through it so missing-API-key, 401/403, rate-limit, etc.
|
||||||
|
// trigger the configured `fallbackModel`. The WeakSet prevents infinite recursion
|
||||||
|
// in case a session-attached `promptWithFallback` ever calls back into the
|
||||||
|
// top-level export with the same session.
|
||||||
|
const promptWithFallbackInFlight = new WeakSet<object>();
|
||||||
|
|
||||||
export async function promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
|
export async function promptWithFallback(session: AgentSession, prompt: string, options?: unknown): Promise<void> {
|
||||||
|
const sessionWithDispatch = session as AgentSession & {
|
||||||
|
promptWithFallback?: (prompt: string, options?: unknown) => Promise<void>;
|
||||||
|
};
|
||||||
|
if (
|
||||||
|
typeof sessionWithDispatch.promptWithFallback === "function" &&
|
||||||
|
!promptWithFallbackInFlight.has(session as unknown as object)
|
||||||
|
) {
|
||||||
|
promptWithFallbackInFlight.add(session as unknown as object);
|
||||||
|
try {
|
||||||
|
await sessionWithDispatch.promptWithFallback(prompt, options);
|
||||||
|
return;
|
||||||
|
} finally {
|
||||||
|
promptWithFallbackInFlight.delete(session as unknown as object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
piLog.log(`promptWithFallback: calling session.prompt (prompt length=${prompt.length})`);
|
piLog.log(`promptWithFallback: calling session.prompt (prompt length=${prompt.length})`);
|
||||||
try {
|
try {
|
||||||
await promptSessionAndCheck(session, prompt, options);
|
await promptSessionAndCheck(session, prompt, options);
|
||||||
|
|||||||
Reference in New Issue
Block a user