← Back to PRs

#14647: feat(plugins): allow before_agent_start hook to override model (#14585)

by lailoo open 2026-02-12 12:59 View on GitHub →
agents stale size: S trusted-contributor
## Summary Fixes #14585 ## Problem The `before_agent_start` plugin hook can inject `systemPrompt` and `prependContext` into agent turns, but cannot override the model. This prevents plugins from implementing intent-based model routing — e.g., routing simple questions to a cheap model and complex tasks to a powerful one. **Before:** A plugin returning `{ model: "openai/gpt-4.1" }` from `before_agent_start` is silently ignored. The configured default model is always used. ## Fix 1. **Plugin types** (`src/plugins/types.ts`): Add optional `model?: string` field to `PluginHookBeforeAgentStartResult`. 2. **Hook merge logic** (`src/plugins/hooks.ts`): Add `model` to the merge function — last plugin wins (same semantics as `systemPrompt`). 3. **Run entry point** (`src/agents/pi-embedded-runner/run.ts`): Call `before_agent_start` hook **before** `resolveModel()`. If the hook returns a `model` string (format: `provider/modelId`), override the `provider` and `modelId` variables so the correct model is resolved and used for session creation. **After:** A plugin can return `{ model: "openai/gpt-4.1" }` and the agent turn will use that model instead of the configured default. ### Design decisions - **Hook runs twice per turn**: once in `run.ts` (model phase, no messages) and once in `attempt.ts` (prompt phase, with messages). This preserves the existing `systemPrompt`/`prependContext` behavior which needs access to session messages, while allowing model override before session creation. - **Model format**: `provider/modelId` string, consistent with the existing model reference format used throughout the codebase (e.g., `anthropic/claude-sonnet-4`, `openrouter/anthropic/claude-opus-4`). - **Graceful degradation**: If the hook fails or returns an invalid model string (no `/`), the original configured model is used. ### Example plugin usage ```typescript export default { hooks: { before_agent_start: async (event, ctx) => { if (event.prompt.length < 100) { return { model: "openai/gpt-4.1-mini" }; } return { model: "anthropic/claude-sonnet-4" }; }, }, }; ``` ## Testing - ✅ 5 tests pass (`pnpm vitest run src/plugins/hooks.test.ts`) — model merge logic: single hook, last-wins, preserve-on-undefined, merge-with-other-fields, empty-registry - ✅ Lint passes (`pnpm lint`) <!-- greptile_comment --> <h2>Greptile Overview</h2> <h3>Greptile Summary</h3> This PR extends the `before_agent_start` plugin hook to support per-turn model routing by adding an optional `model` field to the hook result, merging it with “last-wins” semantics in the hook runner, and invoking the hook earlier in `pi-embedded-runner/run.ts` so model selection occurs before session creation. Key flow change: `runEmbeddedPiAgent` now runs `before_agent_start` (model phase) to potentially override `provider/modelId`, then later `run/attempt.ts` still runs `before_agent_start` (prompt phase) to apply `prependContext`/`systemPrompt` against the live session messages. <h3>Confidence Score: 4/5</h3> - This PR is largely safe to merge, with one edge case that can break model override behavior. - Core type and merge changes are straightforward and tested; the only verified issue is that an invalid-but-plausible hook return like `"provider/"` will override to an empty modelId and then hard-fail instead of degrading to defaults. - src/agents/pi-embedded-runner/run.ts <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs