fix(runtime-fallback): handle object-shaped event models

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-05-25 11:29:36 +09:00
parent 9fd52cb609
commit df087d230a
3 changed files with 70 additions and 3 deletions
+24 -3
View File
@@ -18,6 +18,27 @@ function resolveEventModel(props: Record<string, unknown> | undefined): string |
return model
}
if (model && typeof model === "object") {
const modelRecord = model as Record<string, unknown>
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<string, unknown> | undefined) => {
const sessionInfo = props?.info as { id?: string; model?: string } | undefined
const sessionInfo = props?.info as Record<string, unknown> | 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,
})
@@ -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)
+44
View File
@@ -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"