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:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user