feat(plugins): pass attachment metadata to before_model_resolve hook

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.
This commit is contained in:
竹田賢史@セントラルホールディングス
2026-04-13 18:12:01 +09:00
committed by sallyom
parent 97492cf602
commit 66b85fe649
3 changed files with 16 additions and 2 deletions

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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 = {