diff --git a/src/hooks/runtime-fallback/event-handler.ts b/src/hooks/runtime-fallback/event-handler.ts index b79d511a7..96a9c4a98 100644 --- a/src/hooks/runtime-fallback/event-handler.ts +++ b/src/hooks/runtime-fallback/event-handler.ts @@ -18,6 +18,27 @@ function resolveEventModel(props: Record | undefined): string | return model } + if (model && typeof model === "object") { + const modelRecord = model as Record + const provider = modelRecord.provider + const providerID = modelRecord.providerID + const modelName = modelRecord.model + const modelID = modelRecord.modelID + const id = modelRecord.id + + if (typeof provider === "string" && typeof modelName === "string") { + return `${provider}/${modelName}` + } + + if (typeof providerID === "string" && typeof modelID === "string") { + return `${providerID}/${modelID}` + } + + if (typeof id === "string") { + return id + } + } + const providerID = props?.providerID const modelID = props?.modelID if (typeof providerID === "string" && typeof modelID === "string") { @@ -45,9 +66,9 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { } const handleSessionCreated = (props: Record | undefined) => { - const sessionInfo = props?.info as { id?: string; model?: string } | undefined + const sessionInfo = props?.info as Record | undefined const sessionID = resolveSessionEventID(props) - const model = sessionInfo?.model + const model = resolveEventModel(sessionInfo) if (sessionID && model) { log(`[${HOOK_NAME}] Session created with model`, { sessionID, model }) @@ -209,7 +230,7 @@ export function createEventHandler(deps: HookDeps, helpers: AutoRetryHelpers) { const initialModel = resolveFallbackBootstrapModel({ sessionID, source: "session.error", - eventModel: props?.model as string | undefined, + eventModel: resolveEventModel(props), resolvedAgent, pluginConfig, }) diff --git a/src/hooks/runtime-fallback/fallback-state.ts b/src/hooks/runtime-fallback/fallback-state.ts index af1b2cbb8..35f69fdfe 100644 --- a/src/hooks/runtime-fallback/fallback-state.ts +++ b/src/hooks/runtime-fallback/fallback-state.ts @@ -50,6 +50,8 @@ function parseCanonicalModel(model: string): { providerID: string; modelID: stri } function isEquivalentModel(candidate: string, current: string): boolean { + if (typeof current !== "string") return false + const parsedCandidate = parseCanonicalModel(candidate) const parsedCurrent = parseCanonicalModel(current) diff --git a/src/hooks/runtime-fallback/index.test.ts b/src/hooks/runtime-fallback/index.test.ts index 9a9a1ab86..9649e0893 100644 --- a/src/hooks/runtime-fallback/index.test.ts +++ b/src/hooks/runtime-fallback/index.test.ts @@ -1024,6 +1024,50 @@ describe("runtime-fallback", () => { expect(createLog?.data).toMatchObject({ sessionID, model }) }) + test("#given session.created carries an object-shaped model #when fallback is prepared #then it does not crash", async () => { + //#given + const promptCalls: unknown[] = [] + const sessionID = "test-session-object-model" + const hook = createRuntimeFallbackHook( + createMockPluginInput({ + session: { + messages: async () => ({ + data: [{ info: { role: "user" }, parts: [{ type: "text", text: "continue" }] }], + }), + promptAsync: async (args: unknown) => { + promptCalls.push(args) + return {} + }, + }, + }), + { + config: createMockConfig({ notify_on_fallback: false }), + pluginConfig: createMockPluginConfigWithCategoryFallback(["openai/gpt-5.4"]), + }, + ) + SessionCategoryRegistry.register(sessionID, "test") + + await hook.event({ + event: { + type: "session.created", + properties: { info: { id: sessionID, model: { provider: "openai", model: "gpt-5.5-fast" } } }, + }, + }) + + //#when + await hook.event({ + event: { + type: "session.error", + properties: { sessionID, error: { statusCode: 429, message: "Rate limit" } }, + }, + }) + + //#then + expect(promptCalls).toHaveLength(1) + const createLog = logCalls.find((c) => c.msg.includes("Session created with model")) + expect(createLog?.data).toMatchObject({ sessionID, model: "openai/gpt-5.5-fast" }) + }) + test("should cleanup state on session.deleted", async () => { const hook = createRuntimeFallbackHook(createMockPluginInput(), { config: createMockConfig() }) const sessionID = "test-session-delete"