Skip to main content

๐Ÿ”Œ Plugin System

The plugin system is the transformation layer of Tresor. Plugins implement Go interfaces to modify requests before forwarding and responses before returning to the client.

๐Ÿงฉ Core Interfacesโ€‹

RequestTransformerโ€‹

Modifies outgoing requests (body + headers):

type RequestTransformer interface {
TransformRequest(req *http.Request, body []byte, ctx *PipelineContext) (*http.Request, []byte, error)
}
  • Receives the original request, raw body bytes, and shared pipeline context
  • Returns a modified request (may be a copy), transformed body, and any error
  • Common transformations: format conversion, header injection, model name rewriting

ResponseTransformerโ€‹

Modifies non-streaming responses:

type ResponseTransformer interface {
TransformResponse(resp *http.Response, body []byte, ctx *PipelineContext) ([]byte, error)
}
  • Receives the downstream's response and raw body bytes
  • Returns transformed body bytes
  • Common transformations: format conversion back to client-expected format

StreamResponseTransformerโ€‹

Handles streaming (SSE) responses event-by-event:

type StreamResponseTransformer interface {
TransformStreamChunk(chunk SSEChunk, ctx *PipelineContext) (SSEChunk, error)
}
  • Receives individual SSE chunks from the downstream
  • Returns transformed chunks for the client
  • Use ctx.Variables map for state tracking across events (e.g., accumulating content, tracking role)

The SSEChunk type represents a single SSE event:

type SSEChunk struct {
EventType string // e.g. "message_start", "content_block_delta" โ€” empty for unnamed events
Data []byte // the JSON payload
}

A plugin can implement any combination of these interfaces. The pipeline parser checks each plugin against all three and registers only the applicable steps.

๐Ÿ“ฆ PipelineContextโ€‹

Carries shared state through a request's lifecycle:

type PipelineContext struct {
TargetDownstream *Downstream // Resolved downstream for this request
Variables map[string]any // Inter-plugin communication / state tracking
}

The Variables map enables plugins to share state โ€” particularly important for streaming transforms that need to track context across SSE events.

๐Ÿ“‹ Plugin Registryโ€‹

Plugins are registered at startup in internal/plugins/registry.go:

var registry = make(map[string]any)

func Register(id string, plugin any) {
registry[id] = plugin
}

func Get(id string) (any, bool) {
plugin, ok := registry[id]
return plugin, ok
}

func List() []PluginInfo {
// Returns [{ID, Description, ConfigSchema}] for each registered plugin
}

All built-in plugins are registered in init() functions. The /api/plugins endpoint exposes the registry for web UI consumption.

๐Ÿ› ๏ธ Built-in Pluginsโ€‹

โž• custom_headerโ€‹

Injects arbitrary HTTP headers into forwarded requests.

๐Ÿ“ Config schema:

{
"type": "object",
"properties": {
"headers": {
"type": "object",
"additionalProperties": {"type": "string"}
}
},
"required": ["headers"]
}

๐Ÿ“ Usage:

pipeline_config:
- plugin_id: custom_header
config:
headers:
X-Custom-Header: my-value
X-Request-ID: abc-123

โš™๏ธ Transforms: Request only


๐Ÿ”„ openai2anthropicโ€‹

Converts OpenAI Chat Completion format to Anthropic Messages format (and vice versa for responses).

Request transform:

  • Maps model names via configurable mapping table
  • Extracts system prompts into Anthropic's dedicated system field
  • Converts message roles and content blocks
  • Handles multi-modal content (text + images)
  • Sets Anthropic-specific headers (anthropic-version, x-api-key auth)

Response transform:

  • Maps Anthropic response fields to OpenAI format
  • Converts content blocks to OpenAI message format

Streaming transform:

  • Tracks state across SSE events (role deltas, content accumulation, finish reason)
  • Maps Anthropic's message_start, content_block_start/delta/end, message_delta, message_stop events to OpenAI's chunk format
  • Converts end_turn โ†’ stop for finish reasons
  • Always emits choice index 0 to ensure reasoning content and text aggregate on a single choice
  • Passes through thinking blocks for reasoning model compatibility

๐Ÿ“ Config schema: No config required ({})


โ†ฉ๏ธ anthropic2openaiโ€‹

The reverse of openai2anthropic โ€” converts Anthropic Messages format to OpenAI Chat Completion format.

Request transform:

  • Converts Anthropic messages array to OpenAI messages format
  • Merges Anthropic's system field into the first message
  • Sets OpenAI auth header (Authorization: Bearer)

Response transform:

  • Maps OpenAI response fields back to Anthropic format

Streaming transform:

  • Parses OpenAI SSE chunks (data: {...} with [DONE] marker)
  • Produces Anthropic's event-stream format (event: message_start, content_block_delta, etc.)

๐Ÿ“ Config schema: No config required ({})


๐Ÿ–ผ๏ธ fix_anthropic_imagesโ€‹

Extracts images from nested tool_result.content[] arrays and promotes them to top-level message content. Designed for llama.cpp-compatible backends that expect flat message structures. Refer to https://github.com/ggml-org/llama.cpp/pull/22536

Behavior:

  • Identifies image blocks inside tool_result.content[] arrays
  • Promotes them to the message's top-level content array
  • Handles edge cases: mixed content (text + tool_result), empty base64 data (skipped), string-valued tool_result content (preserved as-is)

Config schema: No config required ({})

โš™๏ธ Transforms: Request only


๐Ÿงฎ fix_anthropic_usageโ€‹

Normalizes the Anthropic Messages usage block. Some Anthropic-compatible providers (e.g. MiniMax-M3) emit responses whose usage reporting is incomplete: message_start.usage is missing cache_creation_input_tokens / cache_read_input_tokens fields entirely, and message_delta omits the cumulative usage block that downstream SDKs (Anthropic TypeScript SDK, pi-agent) read for token counts โ€” leading to Cannot read properties of undefined (reading 'input_tokens').

Behavior:

  • Non-streaming: when response.usage is a JSON object, adds any missing canonical fields (input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens) as 0. Leaves responses without a usage block alone.
  • Streaming: rewrites message_start.message.usage to include all four canonical fields and synthesizes a usage block in message_delta if the upstream omitted it (carrying the start-of-stream counts forward).
  • Records input_tokens / output_tokens from message_start and applies them to a synthesized message_delta.usage if needed. State resets on message_stop.
  • No-op on responses that already conform (Deepseek-shaped providers); safe to leave attached.

Config schema: No config required ({})

โš™๏ธ Transforms: Response + streaming

๐Ÿ“ Usage:

pipeline_config:
- plugin_id: fix_anthropic_usage

๐Ÿ”„ responses2openaiโ€‹

Converts OpenAI Responses API requests to Chat Completions format (and vice versa for responses).

Request transform:

  • Maps input[] array items to OpenAI messages[] format
  • Converts instructions to system messages
  • Maps function_call items to tool_calls, function_call_output to tool messages
  • Converts input_text/input_image content parts to OpenAI format
  • Maps reasoning.effort to reasoning_effort
  • Maps text.format to response_format
  • Rewrites URL path from /v1/responses to /v1/chat/completions

Response transform:

  • Converts OpenAI Chat Completions response to Responses API output[] array
  • Maps text content to output_text items
  • Maps tool_calls to function_call items

Streaming transform:

  • Converts OpenAI SSE chunks to Responses API named events
  • Maps content deltas to response.output_text.delta
  • Maps tool call deltas to response.output_item.added / response.function_call_arguments.delta
  • Emits response.created, response.in_progress, response.completed lifecycle events

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ responses2anthropicโ€‹

Converts OpenAI Responses API requests to Anthropic Messages format (and vice versa for responses).

Request transform:

  • Maps input[] array items to Anthropic messages[] format
  • Converts instructions to Anthropic system field
  • Maps function_call items to tool_use content blocks
  • Maps function_call_output items to tool_result content blocks
  • Converts OpenAI tools to Anthropic format (input_schema instead of parameters)
  • Maps reasoning.effort to thinking.budget_tokens
  • Rewrites URL path from /v1/responses to /v1/messages
  • Sets Anthropic-specific headers (anthropic-version, x-api-key)

Response transform:

  • Converts Anthropic Messages response to Responses API output[] array
  • Maps text content blocks to output_text items
  • Maps tool_use content blocks to function_call items

Streaming transform:

  • Converts Anthropic SSE events to Responses API named events
  • Maps message_start to response.created + response.in_progress (with created_at/model fields)
  • Maps content_block_delta (text) to response.output_text.delta (with item_id/output_index/logprobs)
  • Maps content_block_delta (input_json) to response.function_call_arguments.delta
  • Surfaces real input_tokens/output_tokens from upstream Anthropic message_delta on response.completed.usage
  • Includes output array in response.completed
  • Passes through thinking blocks for reasoning model compatibility

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ openai2responsesโ€‹

Converts OpenAI Chat Completions requests to Responses API format (and vice versa for responses).

Request transform:

  • Maps OpenAI messages[] to Responses API input[] array
  • Extracts system messages into instructions field
  • Converts assistant tool_calls to function_call items
  • Converts tool messages to function_call_output items
  • Handles multi-modal content (text + images) as input_text/input_image parts
  • Maps reasoning_effort to reasoning.effort
  • Maps response_format to text.format
  • Rewrites URL path from /v1/chat/completions to /v1/responses

Response transform:

  • Converts Responses API output[] array to OpenAI Chat Completions format
  • Maps output_text items to assistant message content
  • Maps function_call items to tool_calls

Streaming transform:

  • Converts Responses API SSE events to OpenAI SSE chunks
  • Maps response.output_text.delta to content delta chunks
  • Maps response.output_item.added / response.function_call_arguments.delta to tool call chunks
  • Maps response.completed to final chunk with [DONE]

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ anthropic2responsesโ€‹

Converts Anthropic Messages requests to Responses API format (and vice versa for responses).

Request transform:

  • Maps Anthropic messages[] to Responses API input[] array
  • Extracts system field to instructions
  • Converts tool_use content blocks to function_call items
  • Converts tool_result content blocks to function_call_output items
  • Handles image content blocks as input_image parts
  • Converts Anthropic tools to OpenAI format
  • Maps thinking.budget_tokens to reasoning.effort
  • Rewrites URL path from /v1/messages to /v1/responses

Response transform:

  • Converts Responses API output[] array to Anthropic Messages format
  • Maps output_text items to text content blocks
  • Maps function_call items to tool_use content blocks

Streaming transform:

  • Converts Responses API SSE events to Anthropic SSE events
  • Maps response.created to message_start
  • Maps response.output_text.delta to content_block_delta (text)
  • Maps response.output_item.added / response.function_call_arguments.delta to content_block_start / content_block_delta (tool_use)
  • Maps response.completed to message_delta + message_stop

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ openai2geminiโ€‹

Converts OpenAI Chat Completion format to Google Gemini generateContent format.

Request transform:

  • Maps messages[] to Gemini contents[] array
  • Extracts system messages into systemInstruction
  • Converts tool_calls to Gemini functionDeclarations
  • Maps response_format to text.format
  • Rewrites URL path to /v1beta/models/{model}:generateContent (or :streamGenerateContent?alt=sse for streaming)

Response transform:

  • Converts Gemini JSON response to OpenAI Chat Completions format
  • Maps candidates[].content.parts to assistant message content
  • Converts usage metadata

Streaming transform:

  • Converts Gemini SSE chunks to OpenAI SSE chunks

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ anthropic2geminiโ€‹

Converts Anthropic Messages format to Google Gemini generateContent format.

Request transform:

  • Maps messages[] to Gemini contents[] array
  • Extracts system field into systemInstruction
  • Converts tool_use to Gemini functionDeclarations
  • Maps thinking.budget_tokens to thinkingConfig.budgetTokens
  • Rewrites URL path to /v1beta/models/{model}:generateContent

Response transform:

  • Converts Gemini JSON response to Anthropic Messages format
  • Maps candidates[].content.parts to text content blocks

Streaming transform:

  • Converts Gemini SSE chunks to Anthropic SSE events
  • Passes through thinking blocks for reasoning model compatibility

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ gemini2openaiโ€‹

Converts Google Gemini generateContent format to OpenAI Chat Completion format (and vice versa for responses).

Request transform:

  • Maps Gemini contents[] to OpenAI messages[]
  • Rewrites URL path from /v1beta/models/{model}:generateContent to /v1/chat/completions
  • Sets Authorization: Bearer header

Response transform:

  • Converts OpenAI Chat Completions response to Gemini JSON format

Streaming transform:

  • Converts OpenAI SSE chunks to Gemini SSE events

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ gemini2anthropicโ€‹

Converts Google Gemini generateContent format to Anthropic Messages format (and vice versa for responses).

Request transform:

  • Maps Gemini contents[] to Anthropic messages[]
  • Converts Gemini function declarations to Anthropic tools
  • Rewrites URL path to /v1/messages
  • Sets Anthropic-specific headers

Response transform:

  • Converts Anthropic Messages response to Gemini JSON format

Streaming transform:

  • Converts Anthropic SSE events to Gemini SSE events
  • Passes through thinking blocks

๐Ÿ“ Config schema: No config required ({})


๐Ÿ”„ gemini2responsesโ€‹

Converts Google Gemini generateContent format to OpenAI Responses API format (and vice versa for responses).

Request transform:

  • Maps Gemini contents[] to Responses API input[] array
  • Rewrites URL path to /v1/responses

Response transform:

  • Converts Responses API output[] to Gemini JSON format

Streaming transform:

  • Converts Responses API SSE events to Gemini SSE events

๐Ÿ“ Config schema: No config required ({})

โšก Auto-Translationโ€‹

Tresor can automatically insert format converters when the request format doesn't match the downstream's declared api_formats. The engine detects the input format from the request path (/v1/chat/completions โ†’ OpenAI, /v1/messages โ†’ Anthropic, /v1/responses โ†’ OpenAI Responses API) and compares it against the downstream's format list. If there's a mismatch, the appropriate plugin is automatically selected and prepended to the request pipeline and appended to the response/stream pipelines โ€” without any explicit rule.

Auto-translation decision tree:

Input FormatDownstream HasPlugin Selected
openaiopenai_responsesopenai2responses
openaigeminiopenai2gemini
openaianthropic (not openai_responses)openai2anthropic
anthropicopenai_responsesanthropic2responses
anthropicgeminianthropic2gemini
anthropicopenai (not openai_responses)anthropic2openai
openai_responsesgeminiresponses2gemini
openai_responsesopenairesponses2openai
openai_responsesanthropicresponses2anthropic
geminiopenaigemini2openai
geminianthropicgemini2anthropic
geminiopenai_responsesgemini2responses

When the downstream supports openai_responses, that format takes priority over direct OpenAI/Anthropic translation. This reflects the design principle that the Responses API serves as a hub format.

๐Ÿ“ Pipeline Configuration Formatโ€‹

Pipeline config is stored as JSON in the rules.pipeline_config column:

[
{"plugin_id": "custom_header", "config": {"headers": {"X-Custom": "value"}}},
{"plugin_id": "openai2responses"}
]

Each entry has:

  • plugin_id (required): Registry ID of the plugin
  • config (optional): Plugin-specific configuration object

Plugins execute sequentially โ€” each plugin's output becomes the next plugin's input. Order matters.

โœ๏ธ Writing a Custom Pluginโ€‹

To add a new plugin:

  1. Create a struct in internal/plugins/ that implements one or more of the transformer interfaces
  2. Register it by calling registry.Register("my_plugin", &MyPlugin{}) in an init() function
  3. Define a config schema as a JSON Schema object for web UI consumption

Example skeleton:

package plugins

import (
"net/http"
"encoding/json"
)

type MyTransformer struct {
Config map[string]any
}

func (t *MyTransformer) TransformRequest(req *http.Request, body []byte, ctx *engine.PipelineContext) (*http.Request, []byte, error) {
// Parse body, apply transformation, return modified request + body
return req, body, nil
}

func (t *MyTransformer) TransformResponse(resp *http.Response, body []byte, ctx *engine.PipelineContext) ([]byte, error) {
// Parse response body, apply transformation, return modified body
return body, nil
}

func (t *MyTransformer) TransformStreamChunk(chunk engine.SSEChunk, ctx *engine.PipelineContext) (engine.SSEChunk, error) {
// Transform SSE event, return modified chunk
return chunk, nil
}

func init() {
Register("my_transformer", &MyTransformer{})
}