Skip to content

[FEATURE]: Plugin hook that fires before provider/inference API calls #21240

Closed as duplicate

Description

@dru89

Feature hasn't been suggested before.

  • I have verified this feature I'm about to request hasn't been suggested before.

Describe the enhancement you want to request

OpenCode's plugin system has tool.execute.before for intercepting tool calls, but no hook that fires before the model inference call. Plugins that need to validate something before the provider API is called (credential freshness, rate limits, cost budgets) have no way to do it.

Use case: Enterprise credential rotation

When using AWS Bedrock with temporary credentials (via SAML/SSO federation), credentials expire periodically. A plugin needs to check credential validity and attempt refresh before the Bedrock API call. tool.execute.before doesn't cover inference, so expired credentials produce a raw provider error instead of a helpful message.

Claude Code solves this with UserPromptSubmit (fires before the model is called). pi-mono solves it with before_provider_request (fires right before the HTTP request).

The same hook would also cover rate limiting, request logging, cost controls, or any other pre-flight check.

Proposed hook: chat.request.before

A trigger-style hook that fires after chat.params and chat.headers resolve but before streamText() is called. It would follow the existing Plugin.trigger() pattern.

The existing trigger chain in llm.ts already has two hooks right before the inference call:

const params = await Plugin.trigger(
"chat.params",
{
sessionID: input.sessionID,
agent: input.agent.name,
model: input.model,
provider,
message: input.user,
},
{
temperature: input.model.capabilities.temperature
? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
: undefined,
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
topK: ProviderTransform.topK(input.model),
maxOutputTokens,
options,
},
)
const { headers } = await Plugin.trigger(
"chat.headers",
{
sessionID: input.sessionID,
agent: input.agent.name,
model: input.model,
provider,
message: input.user,
},
{
headers: {},
},
)

The streamText() call (the actual provider API request) happens after tool resolution:

return streamText({

A new Plugin.trigger("chat.request.before", ...) between tool resolution and streamText() (around line 260) would let plugins inspect the assembled request and throw to block it, same as throwing from tool.execute.before blocks a tool call. The input would mirror the existing chat.params/chat.headers shape (sessionID, agent, model, provider).

The type definition would sit alongside the existing chat.headers definition:

"chat.params"?: (
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
output: {
temperature: number
topP: number
topK: number
maxOutputTokens: number | undefined
options: Record<string, any>
},
) => Promise<void>
"chat.headers"?: (
input: { sessionID: string; agent: string; model: Model; provider: ProviderContext; message: UserMessage },
output: { headers: Record<string, string> },
) => Promise<void>

Proposed interface

For reference, here's what the equivalent hooks expose in other systems:

  • Claude Code's UserPromptSubmit receives the user's prompt text, session ID, and working directory. It can block the prompt (with a reason shown to the user) or inject additional context. It fires once per user prompt, before any processing.
  • pi-mono's before_provider_request receives the fully serialized provider payload (messages, temperature, tools, etc.). It can inspect the payload for debugging or replace it entirely. It fires once per LLM call, including retries.

A useful middle ground for OpenCode, following the existing Plugin.trigger() pattern:

"chat.request.before"?: (
  input: {
    sessionID: string
    agent: string
    model: Model
    provider: ProviderContext
    messages: ModelMessage[]
    tools: string[]
  },
  output: {},
) => Promise<void>

input gives the plugin enough to make decisions: which provider and model are being called, what messages are being sent, and which tools are active. messages and tools are read-only context (not mutable through this hook, since experimental.chat.messages.transform and chat.params already cover mutation). output is empty since this is a validation hook.

To block a request, a plugin throws. To allow it, it returns normally:

export const CredentialCheck: Plugin = async () => {
  return {
    "chat.request.before": async (input) => {
      if (input.provider.info.id === "amazon-bedrock") {
        const remaining = getCredentialTTL()
        if (remaining <= 0) {
          throw new Error("AWS credentials have expired. Refresh credentials and try again.")
        }
      }
    },
  }
}

Activity

  1. added
    discussionUsed for feature requests, proposals, ideas, etc. Open discussion
    on Apr 6, 2026
  2. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    on Apr 6, 2026
  3. dru89 commented on Apr 6, 2026

    @dru89
    Author

    I'm happy to tackle this myself with a pull request, but I didn't want to necessarily just open one out of nowhere since it's a feature request and not just a bug fix.

    Seemed better to have the discussion on whether or not this was worthwhile before making the change.

  4. github-actions commented on Apr 6, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  5. dru89 commented on Apr 6, 2026

    @dru89
    Author

    This issue might be a duplicate of existing issues. Please check:

    I definitely missed this in my search! It does seem related, and if that existed it would probably replace my request for it here. I'll close this issue and follow that one instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)discussionUsed for feature requests, proposals, ideas, etc. Open discussion

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions