๐ 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 - Always emits choice index
0to 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
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.
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
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
๐งฎ 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.usageis a JSON object, adds any missing canonical fields (input_tokens,output_tokens,cache_creation_input_tokens,cache_read_input_tokens) as0. Leaves responses without ausageblock alone. - Streaming: rewrites
message_start.message.usageto include all four canonical fields and synthesizes ausageblock inmessage_deltaif the upstream omitted it (carrying the start-of-stream counts forward). - Records
input_tokens/output_tokensfrommessage_startand applies them to a synthesizedmessage_delta.usageif needed. State resets onmessage_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 OpenAImessages[]format - Converts
instructionsto system messages - Maps
function_callitems to tool_calls,function_call_outputto tool messages - Converts
input_text/input_imagecontent parts to OpenAI format - Maps
reasoning.efforttoreasoning_effort - Maps
text.formattoresponse_format - Rewrites URL path from
/v1/responsesto/v1/chat/completions
Response transform:
- Converts OpenAI Chat Completions response to Responses API
output[]array - Maps text content to
output_textitems - Maps tool_calls to
function_callitems
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.completedlifecycle 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 Anthropicmessages[]format - Converts
instructionsto Anthropicsystemfield - Maps
function_callitems totool_usecontent blocks - Maps
function_call_outputitems totool_resultcontent blocks - Converts OpenAI tools to Anthropic format (
input_schemainstead ofparameters) - Maps
reasoning.efforttothinking.budget_tokens - Rewrites URL path from
/v1/responsesto/v1/messages - Sets Anthropic-specific headers (
anthropic-version,x-api-key)
Response transform:
- Converts Anthropic Messages response to Responses API
output[]array - Maps
textcontent blocks tooutput_textitems - Maps
tool_usecontent blocks tofunction_callitems
Streaming transform:
- Converts Anthropic SSE events to Responses API named events
- Maps
message_starttoresponse.created+response.in_progress(withcreated_at/modelfields) - Maps
content_block_delta(text) toresponse.output_text.delta(withitem_id/output_index/logprobs) - Maps
content_block_delta(input_json) toresponse.function_call_arguments.delta - Surfaces real
input_tokens/output_tokensfrom upstream Anthropicmessage_deltaonresponse.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 APIinput[]array - Extracts system messages into
instructionsfield - Converts assistant
tool_callstofunction_callitems - Converts
toolmessages tofunction_call_outputitems - Handles multi-modal content (text + images) as
input_text/input_imageparts - Maps
reasoning_efforttoreasoning.effort - Maps
response_formattotext.format - Rewrites URL path from
/v1/chat/completionsto/v1/responses
Response transform:
- Converts Responses API
output[]array to OpenAI Chat Completions format - Maps
output_textitems to assistant message content - Maps
function_callitems to tool_calls
Streaming transform:
- Converts Responses API SSE events to OpenAI SSE chunks
- Maps
response.output_text.deltato content delta chunks - Maps
response.output_item.added/response.function_call_arguments.deltato tool call chunks - Maps
response.completedto 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 APIinput[]array - Extracts
systemfield toinstructions - Converts
tool_usecontent blocks tofunction_callitems - Converts
tool_resultcontent blocks tofunction_call_outputitems - Handles image content blocks as
input_imageparts - Converts Anthropic tools to OpenAI format
- Maps
thinking.budget_tokenstoreasoning.effort - Rewrites URL path from
/v1/messagesto/v1/responses
Response transform:
- Converts Responses API
output[]array to Anthropic Messages format - Maps
output_textitems totextcontent blocks - Maps
function_callitems totool_usecontent blocks
Streaming transform:
- Converts Responses API SSE events to Anthropic SSE events
- Maps
response.createdtomessage_start - Maps
response.output_text.deltatocontent_block_delta(text) - Maps
response.output_item.added/response.function_call_arguments.deltatocontent_block_start/content_block_delta(tool_use) - Maps
response.completedtomessage_delta+message_stop
๐ Config schema: No config required ({})
๐ openai2geminiโ
Converts OpenAI Chat Completion format to Google Gemini generateContent format.
Request transform:
- Maps
messages[]to Geminicontents[]array - Extracts system messages into
systemInstruction - Converts
tool_callsto GeminifunctionDeclarations - Maps
response_formattotext.format - Rewrites URL path to
/v1beta/models/{model}:generateContent(or:streamGenerateContent?alt=ssefor streaming)
Response transform:
- Converts Gemini JSON response to OpenAI Chat Completions format
- Maps
candidates[].content.partsto 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 Geminicontents[]array - Extracts
systemfield intosystemInstruction - Converts
tool_useto GeminifunctionDeclarations - Maps
thinking.budget_tokenstothinkingConfig.budgetTokens - Rewrites URL path to
/v1beta/models/{model}:generateContent
Response transform:
- Converts Gemini JSON response to Anthropic Messages format
- Maps
candidates[].content.partsto 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 OpenAImessages[] - Rewrites URL path from
/v1beta/models/{model}:generateContentto/v1/chat/completions - Sets
Authorization: Bearerheader
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 Anthropicmessages[] - 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 APIinput[]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 Format | Downstream Has | Plugin Selected |
|---|---|---|
openai | openai_responses | openai2responses |
openai | gemini | openai2gemini |
openai | anthropic (not openai_responses) | openai2anthropic |
anthropic | openai_responses | anthropic2responses |
anthropic | gemini | anthropic2gemini |
anthropic | openai (not openai_responses) | anthropic2openai |
openai_responses | gemini | responses2gemini |
openai_responses | openai | responses2openai |
openai_responses | anthropic | responses2anthropic |
gemini | openai | gemini2openai |
gemini | anthropic | gemini2anthropic |
gemini | openai_responses | gemini2responses |
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 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{})
}