Documentation
¶
Overview ¶
Package explain turns captured LLM API traffic into compact digests.
The proxy stores raw request and response bodies. LLM responses are very often server-sent event streams made of hundreds of tiny events, which an agent should never have to read. This package:
- detects LLM traffic from the host, path, request body and response content type (Detect);
- reassembles a streamed response into the JSON object the same call would have returned without streaming (Reassemble);
- renders a short, deterministic text digest of the exchange together with normalised token usage and the stop reason (Explain).
Supported providers are the Anthropic Messages API, OpenAI Chat Completions (and the many compatible APIs that share its wire format), the OpenAI Responses API, and, best effort, the Gemini generateContent API.
ParseSSE is a small, tolerant event-stream parser that the reassemblers share; it is exported because other parts of pano render raw streams too.
Index ¶
Constants ¶
const ( // Anthropic is the Messages API: POST /v1/messages, streamed or not. Anthropic = "anthropic" // OpenAIChat is Chat Completions: POST /v1/chat/completions, including the // compatible APIs exposed by Azure, OpenRouter, Groq, Together, DeepSeek, // xAI, Mistral and others. OpenAIChat = "openai-chat" // OpenAIResponses is the Responses API: POST /v1/responses. OpenAIResponses = "openai-responses" // Gemini is generativelanguage.googleapis.com :generateContent and // :streamGenerateContent (best effort). Gemini = "gemini" )
Provider identifiers.
const ( IncludeFinal = "final" // reassembled response content blocks IncludeUsage = "usage" // token usage line IncludeTools = "tools" // tool names in the request line and tool-call inputs IncludeStop = "stop" // stop/finish reason IncludeSystem = "system" // the system prompt (truncated) IncludeMessages = "messages" // one line per request message IncludeThinking = "thinking" // thinking / reasoning text instead of a placeholder IncludeErrors = "errors" // errors line IncludeRequest = "request" // the raw request JSON, pretty-printed (truncated) )
Include names accepted in Options.Include.
const DefaultMaxChars = 4000
DefaultMaxChars is the digest budget used when Options.MaxChars is zero.
Variables ¶
var DefaultInclude = []string{IncludeFinal, IncludeUsage, IncludeTools, IncludeStop, IncludeErrors}
DefaultInclude is the include set used when Options.Include is empty.
var ErrNotLLM = errors.New("explain: not recognised LLM traffic")
ErrNotLLM is returned by Explain when the exchange is not recognised LLM traffic and no provider was forced.
var Providers = []string{Anthropic, OpenAIChat, OpenAIResponses, Gemini}
Providers lists every provider identifier.
Functions ¶
func Detect ¶
Detect identifies the provider from the host, request path, request body shape and response content type. ok is false for traffic that is not a recognised LLM completion call.
Path suffixes are checked first so that gateways and proxies hosted on any domain are recognised; the request body shape is the fallback for providers reached through non-standard paths (Bedrock, Vertex, self-hosted gateways).
func Reassemble ¶
Reassemble turns a provider's streamed response body (SSE, or a JSON array of chunks for Gemini) into the equivalent non-streaming JSON object. partial is true when the stream ended before its terminal event; the object returned then reflects everything received so far.
Types ¶
type Event ¶
type Event struct {
// Name is the value of the "event:" field, or "" when the event has none
// (OpenAI streams, for example, only send "data:" lines).
Name string
// Data is the payload: every "data:" line of the event joined with "\n".
Data string
// ID is the value of the "id:" field, if any.
ID string
}
Event is one server-sent event as split out of a text/event-stream body.
func ParseSSE ¶
ParseSSE splits an event-stream body into events. It follows the WHATWG event-stream grammar loosely: lines end in CRLF, LF or CR; a line starting with ':' is a comment; multiple "data:" lines are joined with a newline; the single optional space after the field colon is dropped; a blank line dispatches the pending event. A pending event at end of input is dispatched too, so a stream that was cut off without its trailing blank line still yields its last event. Events without any field are dropped.
type Options ¶
type Options struct {
// Include selects digest sections; entries may also be comma-separated.
// Defaults to DefaultInclude.
Include []string
// MaxChars caps the digest text. Defaults to DefaultMaxChars.
MaxChars int
// Provider forces a provider instead of detecting one.
Provider string
}
Options controls what Explain renders.
type Result ¶
type Result struct {
Provider string
Model string
Stream bool
Status int
// Text is the rendered digest.
Text string
// Final is the response as the non-streaming API would have returned it:
// the reassembled object for streams, the body itself otherwise. nil when
// not applicable (errors, empty or non-JSON bodies).
Final []byte
// Usage is normalised token usage: input_tokens (all prompt tokens,
// cached ones included), output_tokens, cache_read_input_tokens,
// cache_creation_input_tokens, total_tokens, reasoning_tokens (each only
// when the provider reports it) and the provider's own object under "raw".
Usage map[string]any
StopReason string
// Partial is true when a stream ended before its terminal event.
Partial bool
Errors []string
}
Result is a digest of one LLM exchange.
func Explain ¶
func Explain(host, path string, status int, reqHeaders http.Header, reqBody []byte, respHeaders http.Header, respBody []byte, opts Options) (*Result, error)
Explain builds the digest of one exchange. reqBody and respBody are decoded bytes (any Content-Encoding already removed). status is the HTTP status of the response; bodies of error responses (≥ 400) are summarised into Result.Errors rather than reassembled.