diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 4522715df..1e5a310ef 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/extensibility/extensions/runner.ts b/packages/coding-agent/src/extensibility/extensions/runner.ts index b552ca9f7..f2f1e0b25 100644 --- a/packages/coding-agent/src/extensibility/extensions/runner.ts +++ b/packages/coding-agent/src/extensibility/extensions/runner.ts @@ -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 { - const ctx = this.createContext(); + /** Runs request payload hooks with the model used for that provider request. */ + async emitBeforeProviderRequest(payload: unknown, model?: Model): Promise { + const ctx = this.createContext(model); let currentPayload = payload; for (const ext of this.extensions) { diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index e7130b753..fa08891b2 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -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); diff --git a/packages/coding-agent/test/extensions-runner.test.ts b/packages/coding-agent/test/extensions-runner.test.ts index 9255c98ff..27069ccfa 100644 --- a/packages/coding-agent/test/extensions-runner.test.ts +++ b/packages/coding-agent/test/extensions-runner.test.ts @@ -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) {