From 66b85fe6493ab42e45679f9d795d02105daa0e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=AB=B9=E7=94=B0=E8=B3=A2=E5=8F=B2=40=E3=82=BB=E3=83=B3?= =?UTF-8?q?=E3=83=88=E3=83=A9=E3=83=AB=E3=83=9B=E3=83=BC=E3=83=AB=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=83=B3=E3=82=B0=E3=82=B9?= Date: Mon, 13 Apr 2026 18:12:01 +0900 Subject: [PATCH] feat(plugins): pass attachment metadata to before_model_resolve hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional `attachments` field to `PluginHookBeforeModelResolveEvent` so plugins can route models based on file type (images, documents, etc.). This enables file-aware model routing — e.g. routing image-heavy messages to vision-optimized models like Gemini Flash instead of defaulting to Sonnet, significantly reducing cost without quality loss. The change is fully backwards-compatible: `attachments` is optional and existing plugins continue to work without modification. --- src/agents/pi-embedded-runner/run.ts | 6 ++++++ src/agents/pi-embedded-runner/run/setup.ts | 5 +++-- src/plugins/hook-before-agent-start.types.ts | 7 +++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/agents/pi-embedded-runner/run.ts b/src/agents/pi-embedded-runner/run.ts index 24e05cc1fd2..d5f18acffcf 100644 --- a/src/agents/pi-embedded-runner/run.ts +++ b/src/agents/pi-embedded-runner/run.ts @@ -300,8 +300,14 @@ export async function runEmbeddedPiAgent( channelId: params.messageChannel ?? params.messageProvider ?? undefined, }; + const attachments = (params.images ?? []).map((img) => ({ + kind: "image" as const, + mimeType: img.media_type, + })); + const hookSelection = await resolveHookModelSelection({ prompt: params.prompt, + attachments: attachments.length > 0 ? attachments : undefined, provider, modelId, hookRunner, diff --git a/src/agents/pi-embedded-runner/run/setup.ts b/src/agents/pi-embedded-runner/run/setup.ts index 6a52bbf7cb8..2134dc6f631 100644 --- a/src/agents/pi-embedded-runner/run/setup.ts +++ b/src/agents/pi-embedded-runner/run/setup.ts @@ -28,7 +28,7 @@ type HookContext = { type HookRunnerLike = { hasHooks(hookName: string): boolean; runBeforeModelResolve( - input: { prompt: string }, + input: { prompt: string; attachments?: { kind: string; mimeType?: string }[] }, context: HookContext, ): Promise<{ providerOverride?: string; modelOverride?: string } | undefined>; runBeforeAgentStart( @@ -39,6 +39,7 @@ type HookRunnerLike = { export async function resolveHookModelSelection(params: { prompt: string; + attachments?: { kind: string; mimeType?: string }[]; provider: string; modelId: string; hookRunner?: HookRunnerLike | null; @@ -58,7 +59,7 @@ export async function resolveHookModelSelection(params: { if (hookRunner?.hasHooks("before_model_resolve")) { try { modelResolveOverride = await hookRunner.runBeforeModelResolve( - { prompt: params.prompt }, + { prompt: params.prompt, attachments: params.attachments }, params.hookContext, ); } catch (hookErr) { diff --git a/src/plugins/hook-before-agent-start.types.ts b/src/plugins/hook-before-agent-start.types.ts index 00b2dc6a900..928107f17d4 100644 --- a/src/plugins/hook-before-agent-start.types.ts +++ b/src/plugins/hook-before-agent-start.types.ts @@ -1,7 +1,14 @@ // before_model_resolve hook +export type PluginHookBeforeModelResolveAttachment = { + kind: "image" | "video" | "audio" | "document" | "other"; + mimeType?: string; +}; + export type PluginHookBeforeModelResolveEvent = { /** User prompt for this run. No session messages are available yet in this phase. */ prompt: string; + /** Attachment metadata for file-aware model routing. */ + attachments?: PluginHookBeforeModelResolveAttachment[]; }; export type PluginHookBeforeModelResolveResult = {