Documentation
¶
Overview ¶
Package llmclient provides a base HTTP client for LLM providers with: - Request marshaling/unmarshaling - Retries with exponential backoff and jitter - Standardized error parsing, including errors embedded in 200-status bodies - Circuit breaking with half-open state protection
Index ¶
- Constants
- type Client
- func (c *Client) BaseURL() string
- func (c *Client) Do(ctx context.Context, req Request, result any) error
- func (c *Client) DoPassthrough(ctx context.Context, req Request) (*http.Response, error)
- func (c *Client) DoRaw(ctx context.Context, req Request) (*Response, error)
- func (c *Client) DoStream(ctx context.Context, req Request) (io.ReadCloser, error)
- func (c *Client) DoStreamResponse(ctx context.Context, req Request) (*Response, error)
- func (c *Client) SetBaseURL(url string)
- type Config
- type EmptyResponseInfo
- type HeaderSetter
- type Hooks
- type Request
- type RequestInfo
- type Response
- type ResponseInfo
Constants ¶
const ( EmptyReasonNoChoices = "no_choices" // chat completion with no choices EmptyReasonNoOutput = "no_output" // completed Responses API call with no output items EmptyReasonNoUsage = "no_usage" // content returned without any token usage )
Empty response reasons reported through Hooks.OnEmptyResponse.
const ( OperationChat = "chat" OperationGenerateContent = "generate_content" OperationTextCompletion = "text_completion" OperationEmbeddings = "embeddings" )
Well-known GenAI operation names carried through observability hooks. Provider adapters select these explicitly so instrumentation never has to infer semantics from provider-specific URL shapes.
const UnknownModel = "unknown"
extractModel attempts to extract the model name from a request body UnknownModel is the model label reported for requests whose model cannot be recovered from the request body (body-less discovery GETs, availability probes, multipart uploads). Hook consumers that attribute traffic per model should treat it as "not model-attributed".
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a base HTTP client for LLM providers
func New ¶
func New(cfg Config, headerSetter HeaderSetter) *Client
New creates a new LLM client with the given configuration
func NewWithHTTPClient ¶
func NewWithHTTPClient(httpClient *http.Client, cfg Config, headerSetter HeaderSetter) *Client
NewWithHTTPClient creates a new LLM client with a custom HTTP client
func NewWithOptionalHTTPClient ¶ added in v0.1.95
func NewWithOptionalHTTPClient(httpClient *http.Client, cfg Config, headerSetter HeaderSetter) *Client
NewWithOptionalHTTPClient creates a client on httpClient when one is given and on the shared pooled default otherwise. Providers take their transport from ProviderOptions, which is nil in production, so this keeps the pooling there while letting a test point the same constructor at its own server.
func (*Client) Do ¶
Do executes a request with retries and circuit breaking, then unmarshals the response
func (*Client) DoPassthrough ¶
DoPassthrough executes a request and returns the raw upstream HTTP response. Unlike DoRaw, it preserves non-200 responses for the caller to proxy unchanged.
func (*Client) DoRaw ¶
DoRaw executes a request with retries and circuit breaking, returning the raw response.
Metrics Behavior ¶
Metrics hooks (OnRequestStart/OnRequestEnd) are called at this level to track logical requests from the caller's perspective, not individual retry attempts. This ensures:
- Request counts reflect user-facing requests, not internal HTTP calls
- Duration metrics include total time across all retries (useful for SLOs)
- In-flight gauge accurately reflects concurrent logical requests
Behavior comparison (hooks at DoRaw vs per-attempt):
| Scenario | Per-attempt (old) | DoRaw level (current) | |--------------------------------------|-----------------------------|----------------------------------| | 1 request, succeeds first try | 1 observation | 1 observation | | 1 request, fails twice then succeeds | 3 observations | 1 observation (success) | | 1 request, fails all 3 retries | 3 observations | 1 observation (error) | | Duration metric | Each attempt's duration | Total duration including retries | | In-flight gauge | Bounces up/down per attempt | Accurate concurrent count |
The final status code and error in metrics reflect the outcome after all retry attempts.
func (*Client) DoStream ¶
DoStream executes a streaming request, returning a ReadCloser Note: Streaming requests do NOT retry (as partial data may have been sent) Metrics note: Duration is measured from start to stream establishment, not stream close
func (*Client) DoStreamResponse ¶ added in v0.1.95
DoStreamResponse is DoStream with the upstream response metadata alongside the body. A relay that forwards the bytes verbatim rather than re-encoding them (audio) needs the upstream Content-Type to describe what it hands on.
func (*Client) SetBaseURL ¶
SetBaseURL updates the base URL (thread-safe)
type Config ¶
type Config struct {
// ProviderName is the identifier used in logs and metrics (e.g., "openai", "anthropic").
ProviderName string
// BaseURL is the base URL for the provider's API (e.g., "https://api.openai.com/v1").
BaseURL string
// Retry specifies retry behaviour for failed requests, including backoff and jitter settings.
Retry config.RetryConfig
// CircuitBreaker configures the circuit breaker that prevents cascading failures by
// stopping requests to an unhealthy provider until it recovers.
CircuitBreaker config.CircuitBreakerConfig
// Hooks provides optional observability callbacks invoked on request start and end.
Hooks Hooks
}
Config holds configuration for the LLM client
func DefaultConfig ¶
DefaultConfig returns default client configuration
type EmptyResponseInfo ¶ added in v0.1.92
type EmptyResponseInfo struct {
Provider string // Configured provider name
ProviderType string // Provider implementation type
Model string // Upstream model name, as sent to the provider
Operation string // Semantic GenAI operation
Reason string // One of the EmptyReason* values
}
EmptyResponseInfo describes a provider response that returned 200 without usable content or usage.
type HeaderSetter ¶
HeaderSetter is a function that sets headers on an HTTP request
type Hooks ¶
type Hooks struct {
// OnRequestStart is called before a request is sent.
// The returned context can be used to propagate trace spans or request IDs.
OnRequestStart func(ctx context.Context, info RequestInfo) context.Context
// OnRequestEnd is called after a request completes (success or failure).
// For streaming requests, this is called when the stream starts, not when it closes.
OnRequestEnd func(ctx context.Context, info ResponseInfo)
// OnStreamFirstChunk is called once when a successful streaming response
// body first returns bytes. It is not called for empty or unread streams.
OnStreamFirstChunk func(ctx context.Context, info ResponseInfo)
// OnStreamEmpty is called once when a successful streaming response body
// ends (EOF or read error) before returning any bytes: the stream was
// established but never delivered. OnStreamFirstChunk is not called for
// it. Error carries the read error, io.EOF for a clean empty stream.
OnStreamEmpty func(ctx context.Context, info ResponseInfo)
// OnEmptyResponse is called when a buffered inference call succeeded at
// the HTTP level but its normalized response carries no choices, output,
// or usage. The provider router fires it after decoding, so it follows
// the OnRequestEnd call that recorded the same attempt as a success.
OnEmptyResponse func(ctx context.Context, info EmptyResponseInfo)
}
Hooks defines observability callbacks for request lifecycle events. These hooks enable instrumentation without polluting business logic.
type Request ¶
type Request struct {
Method string
Endpoint string
Model string
// Operation explicitly identifies model inference semantics for
// observability. Leave empty for control-plane and other non-inference calls.
Operation string
Stream bool // explicit stream intent; Accept: text/event-stream remains a fallback
StreamUncertain bool // bounded opaque-body inspection could not determine stream intent
Body any // Will be JSON marshaled if not nil
RawBody []byte // Used as-is (e.g., multipart form bodies). Mutually exclusive with Body and RawBodyReader.
// RawBodyReader streams the request body without buffering it in memory.
// It is intended for one-shot passthrough requests and is not replayable for retries.
RawBodyReader io.Reader
Headers http.Header
}
Request represents an HTTP request to be made
type RequestInfo ¶
type RequestInfo struct {
Provider string // Configured provider name
ProviderType string // Provider implementation type (e.g., "openai", "anthropic")
Model string // Model name (e.g., "gpt-4", "claude-3-opus")
Operation string // Semantic GenAI operation; empty for non-inference calls
Endpoint string // API endpoint (e.g., "/chat/completions", "/models")
Method string // HTTP method (e.g., "POST", "GET")
Stream bool // Whether this is a streaming request
// StreamUncertain means a bounded opaque-body inspection could not
// determine intent before the upstream call began.
StreamUncertain bool
}
RequestInfo contains metadata about a request for observability hooks
type Response ¶
type Response struct {
StatusCode int
// ContentType is the upstream response Content-Type header, preserved so
// callers can describe the bytes actually returned (e.g. audio formats).
ContentType string
// Header carries the upstream response headers. It is used to audit failed
// provider attempts; it is not relayed to API clients.
Header http.Header
Body []byte
// Stream is the live upstream body of a streaming response (DoStreamResponse),
// where Body stays nil. The caller owns closing it.
Stream io.ReadCloser
}
Response represents an HTTP response
type ResponseInfo ¶
type ResponseInfo struct {
Provider string // Configured provider name
ProviderType string // Provider implementation type
Model string // Model name
Operation string // Semantic GenAI operation
Endpoint string // API endpoint
Method string // HTTP method
StatusCode int // HTTP status code (0 if network error)
Duration time.Duration // Request duration
Stream bool // Whether this was a streaming request
StreamUncertain bool // Whether request stream intent was unknown at dispatch
Error error // Error if request failed (nil on success)
// CircuitState is the selected provider or model breaker state after this request
// completed ("closed", "half-open", "open"); empty when the breaker is
// disabled. It reflects the moment of completion, so metrics built from it
// update as traffic flows.
CircuitState string
}
ResponseInfo contains metadata about a response for observability hooks