fix(extensions): scoped provider hooks to request model
Passed the per-request model through SDK payload callbacks and ExtensionRunner context creation. Added regression coverage for Codex-primary and Anthropic-request contexts. Fixes #6006
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `before_provider_request` extension contexts exposing the primary session model for cross-provider Advisor requests instead of the request model ([#6006](https://github.com/can1357/oh-my-pi/issues/6006)).
|
||||
|
||||
## [17.0.4] - 2026-07-18
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -534,8 +534,9 @@ export class ExtensionRunner {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
createContext(): ExtensionContext {
|
||||
const getModel = this.#getModel;
|
||||
/** Creates an extension context, optionally scoped to a provider request model. */
|
||||
createContext(model?: Model): ExtensionContext {
|
||||
const getModel = model ? () => model : this.#getModel;
|
||||
return {
|
||||
ui: this.#uiContext,
|
||||
getContextUsage: () => this.#getContextUsageFn(),
|
||||
@@ -965,8 +966,9 @@ export class ExtensionRunner {
|
||||
return currentMessages;
|
||||
}
|
||||
|
||||
async emitBeforeProviderRequest(payload: unknown): Promise<BeforeProviderRequestEventResult> {
|
||||
const ctx = this.createContext();
|
||||
/** Runs request payload hooks with the model used for that provider request. */
|
||||
async emitBeforeProviderRequest(payload: unknown, model?: Model): Promise<BeforeProviderRequestEventResult> {
|
||||
const ctx = this.createContext(model);
|
||||
let currentPayload = payload;
|
||||
|
||||
for (const ext of this.extensions) {
|
||||
|
||||
@@ -2698,8 +2698,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
if (snapcompactInline) transformed = await snapcompactInline.transform(transformed, transformModel);
|
||||
return clampProviderContextImages(transformed, transformModel);
|
||||
};
|
||||
const onPayload = async (payload: unknown, _model?: Model) => {
|
||||
return await extensionRunner.emitBeforeProviderRequest(payload);
|
||||
const onPayload = async (payload: unknown, model?: Model) => {
|
||||
return await extensionRunner.emitBeforeProviderRequest(payload, model);
|
||||
};
|
||||
const onResponse: SimpleStreamOptions["onResponse"] = async (response, model) => {
|
||||
await extensionRunner.emitAfterProviderResponse(response, model);
|
||||
|
||||
@@ -7,6 +7,7 @@ import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import type { AgentMessage, AgentTool } from "@oh-my-pi/pi-agent-core";
|
||||
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
||||
import { getBundledModel } from "@oh-my-pi/pi-catalog/models";
|
||||
import { ModelRegistry } from "@oh-my-pi/pi-coding-agent/config/model-registry";
|
||||
import { discoverAndLoadExtensions, ExtensionRuntime } from "@oh-my-pi/pi-coding-agent/extensibility/extensions/loader";
|
||||
import {
|
||||
@@ -481,6 +482,78 @@ describe("ExtensionRunner", () => {
|
||||
});
|
||||
|
||||
describe("before_provider_request chaining", () => {
|
||||
it("exposes the request 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 extCode = `
|
||||
export default function(pi) {
|
||||
pi.on("before_provider_request", async (_event, ctx) => {
|
||||
const current = ctx.models.current();
|
||||
return {
|
||||
model: ctx.model && {
|
||||
provider: ctx.model.provider,
|
||||
id: ctx.model.id,
|
||||
api: ctx.model.api,
|
||||
},
|
||||
current: current && {
|
||||
provider: current.provider,
|
||||
id: current.id,
|
||||
api: current.api,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(path.join(extensionsDir, "request-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: () => [],
|
||||
},
|
||||
);
|
||||
|
||||
const payload = await runner.emitBeforeProviderRequest({}, requestModel);
|
||||
|
||||
const expected = {
|
||||
provider: requestModel.provider,
|
||||
id: requestModel.id,
|
||||
api: requestModel.api,
|
||||
};
|
||||
expect(payload).toEqual({ model: expected, current: expected });
|
||||
});
|
||||
|
||||
it("chains payload replacements across handlers in load order", async () => {
|
||||
const extCode1 = `
|
||||
export default function(pi) {
|
||||
|
||||
Reference in New Issue
Block a user