fix(tui): update model display during fallback (#82296)

* fix(tui): update fallback model display

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refresh checks after proof update

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore: refresh CI after main repairs

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Gio Della-Libera
2026-05-16 07:35:43 -07:00
committed by GitHub
parent 9dedc4d95c
commit 22858769e4
2 changed files with 116 additions and 1 deletions

View File

@@ -213,6 +213,82 @@ describe("tui-event-handlers: handleAgentEvent", () => {
expect(tui.requestRender).toHaveBeenCalledTimes(1);
});
it("updates the displayed model from fallback lifecycle steps", () => {
const { state, tui, handleAgentEvent } = createHandlersHarness({
state: {
activeChatRunId: "run-fallback",
sessionInfo: {
verboseLevel: "on",
modelProvider: "llamaforge",
model: "qwen/qwen3.5-9b",
},
},
});
handleAgentEvent({
runId: "run-fallback",
stream: "lifecycle",
data: {
phase: "fallback_step",
fallbackStepFinalOutcome: "next_fallback",
fallbackStepFromModel: "openai-codex/gpt-5.5",
fallbackStepToModel: "openrouter/meta-llama/llama-3.1-70b",
},
});
expect(state.sessionInfo.modelProvider).toBe("openrouter");
expect(state.sessionInfo.model).toBe("meta-llama/llama-3.1-70b");
expect(tui.requestRender).toHaveBeenCalled();
});
it("accepts fallback model updates for the pending run before chat registration", () => {
const { state, tui, handleAgentEvent } = createHandlersHarness({
state: {
activeChatRunId: null,
pendingChatRunId: "run-pending",
sessionInfo: {
verboseLevel: "on",
modelProvider: "llamaforge",
model: "qwen/qwen3.5-9b",
},
},
});
handleAgentEvent({
runId: "run-pending",
stream: "lifecycle",
data: {
phase: "fallback_step",
fallbackStepFinalOutcome: "succeeded",
fallbackStepFromModel: "openrouter/meta-llama/llama-3.1-70b",
fallbackStepToModel: "nvidia/deepseek-ai/deepseek-v3.2",
},
});
expect(state.sessionInfo.modelProvider).toBe("nvidia");
expect(state.sessionInfo.model).toBe("deepseek-ai/deepseek-v3.2");
expect(tui.requestRender).toHaveBeenCalled();
});
it("ignores fallback model updates for unrelated runs", () => {
const { state, tui, handleAgentEvent } = createHandlersHarness({
state: {
activeChatRunId: "run-active",
sessionInfo: { verboseLevel: "on", modelProvider: "openai", model: "gpt-5.5" },
},
});
handleAgentEvent({
runId: "run-other",
stream: "lifecycle",
data: { phase: "fallback_step", fallbackStepToModel: "openrouter/other-model" },
});
expect(state.sessionInfo.modelProvider).toBe("openai");
expect(state.sessionInfo.model).toBe("gpt-5.5");
expect(tui.requestRender).not.toHaveBeenCalled();
});
it("captures runId from chat events when activeChatRunId is unset", () => {
const { state, chatLog, handleChatEvent, handleAgentEvent } = createHandlersHarness({
state: { activeChatRunId: null },

View File

@@ -191,6 +191,36 @@ export function createEventHandlers(context: EventHandlerContext) {
: "auth or provider access failed for the current provider. Run /auth to refresh credentials; if you already re-authed, switch models/providers because this account may still be blocked for inference.";
};
const parseProviderModelRef = (
modelRef: unknown,
): { provider: string; model: string } | undefined => {
if (typeof modelRef !== "string") {
return undefined;
}
const trimmed = modelRef.trim();
const separator = trimmed.indexOf("/");
if (separator <= 0 || separator >= trimmed.length - 1) {
return undefined;
}
const provider = trimmed.slice(0, separator).trim();
const model = trimmed.slice(separator + 1).trim();
return provider && model ? { provider, model } : undefined;
};
const applyFallbackStepModelUpdate = (evt: AgentEvent): boolean => {
const data = evt.data ?? {};
if (evt.stream !== "lifecycle" || asString(data.phase, "") !== "fallback_step") {
return false;
}
const target = parseProviderModelRef(data.fallbackStepToModel);
if (!target) {
return false;
}
state.sessionInfo.modelProvider = target.provider;
state.sessionInfo.model = target.model;
return true;
};
const noteSessionRun = (runId: string) => {
sessionRuns.set(runId, Date.now());
pruneRunMap(sessionRuns);
@@ -471,7 +501,16 @@ export function createEventHandlers(context: EventHandlerContext) {
// active chat run id, not the session id. Tool results can arrive after the chat
// final event, so accept finalized runs for tool updates.
const isActiveRun = evt.runId === state.activeChatRunId;
const isKnownRun = isActiveRun || sessionRuns.has(evt.runId) || finalizedRuns.has(evt.runId);
const isPendingRun = evt.runId === state.pendingChatRunId;
const isSessionRun = sessionRuns.has(evt.runId);
if ((isActiveRun || isPendingRun || isSessionRun) && applyFallbackStepModelUpdate(evt)) {
if (isActiveRun) {
armStreamingWatchdog(evt.runId);
}
tui.requestRender();
return;
}
const isKnownRun = isActiveRun || isSessionRun || finalizedRuns.has(evt.runId);
if (!isKnownRun) {
return;
}