fix(sdk): thread response model into after_provider_response context

ExtensionRunner.emitAfterProviderResponse accepted the response model but
discarded it, calling createContext() with no model. Response-scoped hooks
therefore saw the primary session model in ctx.model and ctx.models.current()
even when the response came from a cross-provider side request, so an extension
that revokes a credential on an HTTP 402 could target the wrong provider.

Call createContext(model) to match emitBeforeProviderRequest, plus a regression
test asserting both fields expose the response model.

Fixes #8955
This commit is contained in:
roboomp
2026-08-19 09:44:22 +00:00
parent d94bdfa1bb
commit 78ef6805f3
3 changed files with 78 additions and 2 deletions
@@ -761,6 +761,80 @@ describe("ExtensionRunner", () => {
expect(errors[0]?.event).toBe("after_provider_response");
expect(errors[0]?.error).toContain("response failed");
});
it("exposes the response model instead of the primary session model", async () => {
const primaryModel = getBundledModel("openai-codex", "gpt-5.6-sol");
const requestModel = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!primaryModel || !requestModel) throw new Error("Expected bundled cross-provider models to exist");
const eventsPath = path.join(tempDir.path(), "after-provider-response-model.jsonl");
const extCode = `
import * as fs from "node:fs";
export default function(pi) {
pi.on("after_provider_response", async (_event, ctx) => {
const current = ctx.models.current();
fs.appendFileSync(
${JSON.stringify(eventsPath)},
JSON.stringify({
model: ctx.model && { provider: ctx.model.provider, id: ctx.model.id },
current: current && { provider: current.provider, id: current.id },
}) + "\\n",
);
});
}
`;
fs.writeFileSync(path.join(extensionsDir, "after-response-model.ts"), extCode);
const result = await loadTestExtensions();
const runner = new ExtensionRunner(
result.extensions,
result.runtime,
tempDir.path(),
sessionManager,
modelRegistry,
);
runner.initialize(
{
sendMessage: () => {},
sendUserMessage: () => {},
appendEntry: () => {},
setLabel: () => {},
getActiveTools: () => [],
getAllTools: () => [],
setActiveTools: async () => {},
getCommands: () => [],
setModel: async () => false,
getThinkingLevel: () => undefined,
setThinkingLevel: () => {},
getSessionName: () => undefined,
setSessionName: async () => {},
},
{
getModel: () => primaryModel,
isIdle: () => true,
abort: () => {},
hasPendingMessages: () => false,
shutdown: () => {},
getContextUsage: () => undefined,
compact: async () => {},
getSystemPrompt: () => [],
},
);
await runner.emitAfterProviderResponse(
{ status: 402, headers: {}, requestId: "req_402", metadata: { provider: requestModel.provider } },
requestModel,
);
const expected = { provider: requestModel.provider, id: requestModel.id };
const events = fs
.readFileSync(eventsPath, "utf8")
.trim()
.split("\n")
.map(line => JSON.parse(line));
expect(events).toEqual([{ model: expected, current: expected }]);
});
});
describe("session_stop", () => {