๐ 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.Variablesmap 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
systemfield - Converts message roles and content blocks
- Handles multi-modal content (text + images)
- Sets Anthropic-specific headers (
anthropic-version,x-api-keyauth)
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_stopevents to OpenAI's chunk format - Converts
end_turnโstopfor finish reasons
๐ 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
systemfield 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.
Behavior:
- Identifies image blocks inside
tool_result.content[]arrays - Promotes them to the message's top-level
contentarray - 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
โก 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) and compares it against the downstream's format list. If there's a mismatch, the appropriate plugin (openai2anthropic or anthropic2openai) is automatically prepended to the request pipeline and appended to the response/stream pipelines โ without any explicit rule.
๐ 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": "openai2anthropic"}
]
Each entry has:
plugin_id(required): Registry ID of the pluginconfig(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:
- Create a struct in
internal/plugins/that implements one or more of the transformer interfaces - Register it by calling
registry.Register("my_plugin", &MyPlugin{})in aninit()function - 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{})
}