Documentation
¶
Overview ¶
Package provider is the layer between dcode and the language models.
It has two orthogonal axes, and keeping them apart is the point:
- Transport is the wire format (openai, anthropic). Reusable across families, carries no thresholds.
- Family is the adaptation — system prompt shape, tool schema, edit strategy. Carries the measured behavioral thresholds and turn limits.
"OpenAI-compatible" describes serialization, not behavior. Two models behind the same endpoint can have wildly different tool-calling quality, so treating a wire format as a family would apply one model's measured thresholds to another and ship unvalidated behavior that looks validated.
MiniMax M3 is why this is not theoretical: it speaks both dialects, so one axis would mean duplicating the family, thresholds included.
Spec: docs/specs/architecture/provider-adapter/202608072334-*.
Index ¶
- Constants
- func ClearSecrets()
- func ParseSSE(body string) []string
- func RegisterSecret(v string)
- func RetryAfterOf(header string, now time.Time) time.Duration
- func Sanitize(s string) string
- type Backoff
- type Claude
- func (Claude) AcceptsImages() bool
- func (Claude) DefaultLimits() Limits
- func (f Claude) Encode(req Request, transport string) (WireRequest, error)
- func (Claude) Models() []string
- func (Claude) Name() string
- func (Claude) NewDecoder(tools []ce.ToolDef) Decoder
- func (Claude) Transports() []string
- func (Claude) Window(string) (int, error)
- type Decision
- type Decoder
- type ErrorClass
- type Family
- type Generic
- type Limits
- type MiniMaxM3
- func (MiniMaxM3) AcceptsImages() bool
- func (MiniMaxM3) DefaultLimits() Limits
- func (f MiniMaxM3) Encode(req Request, transport string) (WireRequest, error)
- func (MiniMaxM3) Models() []string
- func (MiniMaxM3) Name() string
- func (f MiniMaxM3) NewDecoder(tools []ce.ToolDef) Decoder
- func (MiniMaxM3) Transports() []string
- func (MiniMaxM3) Window(string) (int, error)
- type Provider
- type ProviderError
- type RecordedError
- type Registry
- func (r *Registry) FamilyNames() []string
- func (r *Registry) RegisterFamily(f Family) error
- func (r *Registry) RegisterTransport(t Transport)
- func (r *Registry) Resolve(model, transportOverride string) (Provider, error)
- func (r *Registry) ResolveFamily(familyName, transportOverride string) (Provider, error)
- func (r *Registry) TransportNames() []string
- type ReplayTransport
- type Request
- type StreamEvent
- type StreamEventType
- type Transcript
- type Transport
- type Usage
- type WireEvent
- type WireRequest
Constants ¶
const ( TransportOpenAI = "openai" TransportAnthropic = "anthropic" )
Wire names.
const GenericName = "generic"
GenericName is the escape hatch a user names explicitly.
const GenericWarning = "using --family generic: the behavioural thresholds in this " +
"product were measured against named model families and none of them applies " +
"to this model. It will work; how well is not something dcode knows."
Warning is what a session using this family has to say.
Every behavioural threshold in the specs was measured against a named family. None of them applies here, and a user who is not told that will read a difference in behaviour as a defect.
Variables ¶
This section is empty.
Functions ¶
func ClearSecrets ¶
func ClearSecrets()
ClearSecrets drops every registered secret.
It exists for tests, including tests in other packages — which is why it is exported rather than unexported beside them. A running process registers the credential once and has no reason to forget it, so nothing in production calls this and nothing should.
func ParseSSE ¶
ParseSSE splits an SSE body into the data payloads of each event, which is the shape a recorded transcript stores.
func RegisterSecret ¶
func RegisterSecret(v string)
RegisterSecret marks a value to be redacted from any outgoing text. Called once at startup with the API key. Values shorter than 8 characters are ignored: redacting a short string would blank out unrelated text.
func RetryAfterOf ¶
RetryAfterOf reads an HTTP Retry-After value.
Both forms the header allows: delta-seconds, and an HTTP date. The date form is why this takes a clock — a deadline is only a wait relative to now, and a date already past is not a wait at all.
Anything absent, unparseable or negative answers zero, which the backoff reads as "no instruction" and falls back to its own schedule. An absent signal must not become a number: a wait invented from a header nobody sent is worse than the default it replaced.
Types ¶
type Backoff ¶
type Backoff struct {
// Base is the first delay. Each attempt doubles it.
Base time.Duration
// Max caps a single wait. Without it, exponential growth turns a long
// outage into a process that appears hung.
Max time.Duration
// Tries is how many attempts are made in total, the first included.
Tries int
}
Backoff is how long to wait before trying again.
Pure: it computes a duration and never sleeps, and it holds no clock. The caller waits, which is what keeps this decidable in a test without one — and what keeps the wait out of a package that must stay reproducible.
func DefaultBackoff ¶
func DefaultBackoff() Backoff
DefaultBackoff is the shipped policy.
Five attempts over roughly fifteen seconds. Enough to ride out a rate limit or a dropped connection, short enough that a real outage is reported rather than waited on — a user watching a cursor learns nothing from the sixth silent retry.
func (Backoff) Wait ¶
Wait returns how long to pause before attempt, and whether to try at all.
attempt is 1-based and counts the attempt about to be made, so Wait(1, …) is the pause before the first retry.
A rate limit that names its own delay wins over the computed one, always, including when it is longer than Max. The server knows when it will accept the next request and this does not; guessing shorter turns one refusal into several, which is how a rate limit becomes a ban.
type Claude ¶
type Claude struct{}
Claude is the second family. It exists to prove the axes are orthogonal: one implementation never validates an abstraction.
func (Claude) AcceptsImages ¶
AcceptsImages is true, through a source block rather than a data URL.
func (Claude) DefaultLimits ¶
DefaultLimits is sized from the case that shaped it: a refactor across ten files runs thirty to fifty tool calls.
func (Claude) Transports ¶
type Decision ¶
type Decision string
Decision is what the loop should do about an error.
func Decide ¶
func Decide(e *ProviderError) Decision
Decide maps a class to the loop's action. Section 4 of the planning spec.
type Decoder ¶
type Decoder interface {
Decode(ev WireEvent) ([]StreamEvent, error)
// Close reports what the end of the stream means.
//
// A dialect can finish a message and then keep talking: the OpenAI one
// repeats finish_reason and attaches the token usage to the last frame, so
// terminating on the first finish throws the accounting away. The decoder
// therefore holds the terminal event until the transport says there is
// nothing more coming.
Close() []StreamEvent
}
Decoder turns one stream's raw frames into neutral events.
It is stateful and single-use: one per stream, never shared. Decode returns zero or more events for a frame — zero when the frame only carried a fragment, several when the end of the stream flushes calls that were being assembled.
type ErrorClass ¶
type ErrorClass string
ErrorClass is the stable classification the loop decides on.
const ( ErrClassAuth ErrorClass = "auth" ErrClassQuota ErrorClass = "quota" ErrClassRateLimit ErrorClass = "rate_limit" ErrClassContextSize ErrorClass = "context_size" ErrClassBadRequest ErrorClass = "bad_request" ErrClassToolSchema ErrorClass = "tool_schema" ErrClassTransport ErrorClass = "transport" ErrClassProvider ErrorClass = "provider" ErrClassCanceled ErrorClass = "canceled" )
type Family ¶
type Family interface {
Name() string
// Transports lists compatible wire formats, most preferred first.
Transports() []string
// Models lists the model-name prefixes this family claims.
Models() []string
Window(model string) (int, error)
DefaultLimits() Limits
// Encode takes the transport name because a family that speaks two
// dialects serializes differently into each. That parameter is exactly
// what a single-axis design could not express.
Encode(req Request, transport string) (WireRequest, error)
// AcceptsImages reports whether this family's models read pictures.
//
// Declared rather than attempted. dcode speaks to several providers, and a
// capability that some have is exactly the kind of thing that works on the
// machine it was written on and fails on somebody else's — as a request
// rejected thirty seconds later, for a reason they cannot connect to what
// they did.
AcceptsImages() bool
// NewDecoder builds a decoder for one stream.
//
// Decoding cannot be a pure function of one frame: a tool call's arguments
// arrive split across frames, so the whole call only exists once the stream
// says it is finished. The decoder is what holds that partial state, and it
// belongs to the family because how a call is split is dialect-specific.
NewDecoder(tools []ce.ToolDef) Decoder
}
Family is the adaptation layer.
func FamilyFor ¶
FamilyFor reports which family claims a model.
Exported because a caller may need the family *before* a provider exists — resolving a credential, for one, since the credential is what the transport is built with. Two implementations of this matching would eventually disagree about which key a model uses, which is a failure with no visible symptom until an auth error.
type Generic ¶
type Generic struct{ MiniMaxM3 }
Generic is the family for a model nobody has measured.
An unknown model does NOT resolve to this by accident. It fails at session creation listing the families that exist, because silently treating an unrecognised name as generic is how someone runs for a week against thresholds that were never measured for what they are using, and reads every oddity as a bug in dcode.
Reaching it requires typing `--family generic`, and every session that does carries a warning saying what is not known. The escape hatch exists because refusing outright would make dcode unusable against a local model or a new release on the day it appears — which is a real cost, paid by the people most likely to be trying it.
It speaks the OpenAI dialect, which is what a new endpoint almost always implements first, and it borrows MiniMax's encoding for exactly that reason.
func (Generic) AcceptsImages ¶
AcceptsImages is false, and the reason is that nothing here can know.
Generic points at whatever OpenAI-compatible endpoint somebody configured, and half of those serve text-only models. Saying no is not a claim that the endpoint cannot; it is a refusal to guess on the user's behalf, made where they can read it instead of thirty seconds later in a provider error.
A generic endpoint that does read images is a reason to add a family for it, which is a decision with a name on it rather than a hope.
func (Generic) DefaultLimits ¶
DefaultLimits are the cautious ones, for the same reason.
func (Generic) Models ¶
Models is empty on purpose: nothing resolves to generic by prefix. The list being empty is what makes the escape hatch explicit rather than a fallback.
func (Generic) Transports ¶
type Limits ¶
type Limits struct {
MaxIterations int
}
Limits are the turn defaults a family declares. The loop reads them instead of carrying a fixed number, because the work horizon is a property of the model: a model trained for long-horizon loops needs a higher iteration cap than one tuned for short tasks, and one global number serves both badly.
type MiniMaxM3 ¶
type MiniMaxM3 struct{}
MiniMaxM3 is the project's primary family.
It speaks both dialects, which is the concrete reason transport and family are separate axes: with one axis, supporting both would mean two families with identical adaptation and identical thresholds, and the copies would diverge at the first maintenance.
func (MiniMaxM3) AcceptsImages ¶
AcceptsImages is true: M3 is natively multimodal and its OpenAI-compatible surface takes an image as a data URL, up to ten megabytes each.
func (MiniMaxM3) DefaultLimits ¶
DefaultLimits allows far more iterations than a short-task model would. M3 is trained for long-horizon agent loops — MiniMax demonstrated a run with 1,959 tool calls — and a cap sized for a ten-file refactor would truncate legitimate work. The repeat detector remains the real defence; this is the backstop, and a backstop tracks the model's horizon.
200 was below that horizon, and the citation above already said so: the run this number was justified by is ten times the number that was written down. It truncated a real one — an unattended session on this repository wrote the fix it was asked for, complete and passing the gate, and then hit the ceiling before it could say it was done. The work survived; the answer did not.
func (MiniMaxM3) Encode ¶
func (f MiniMaxM3) Encode(req Request, transport string) (WireRequest, error)
func (MiniMaxM3) Transports ¶
type Provider ¶
type Provider interface {
Family() Family
Transport() Transport
Window(model string) (int, error)
Limits() Limits
Stream(ctx context.Context, req Request) (<-chan StreamEvent, error)
}
Provider is the composition of a transport and a family. Built by the registry, never by hand.
type ProviderError ¶
type ProviderError struct {
Class ErrorClass
Message string
RetryAfter time.Duration
Retryable bool
}
ProviderError is a classified failure. A bare string would force the loop to guess between retry, wait, alternative and abort, and guessing is how an agent corrupts a file.
Message never contains a credential.
func ClassifyStatus ¶
func ClassifyStatus(status int, body, retryAfter string) *ProviderError
ClassifyStatus maps an HTTP status to a class. Shared by every transport so the loop sees one classification regardless of dialect. retryAfter is the raw Retry-After header, empty when the response had none. Threaded through rather than attached by the caller afterwards so the invariant is structural: only the rate-limit branch can carry a wait, and no later caller can attach one to an error that has none to give.
func (*ProviderError) Error ¶
func (e *ProviderError) Error() string
type RecordedError ¶
type RecordedError struct {
Status int `json:"status,omitempty"`
Body string `json:"body,omitempty"`
Class string `json:"class,omitempty"`
}
RecordedError reproduces a classified failure.
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry resolves a model name to a Provider.
func (*Registry) FamilyNames ¶
FamilyNames lists registered families, sorted for a stable error message.
func (*Registry) RegisterFamily ¶
RegisterFamily adds an adaptation layer. Returns an error when its model prefixes overlap one already registered: an ambiguous prefix would silently resolve to whichever family happened to be added first.
func (*Registry) RegisterTransport ¶
RegisterTransport adds a wire format.
func (*Registry) Resolve ¶
Resolve composes a Provider for model. transportOverride selects a dialect; empty uses the family's preferred one.
An unknown model is an error listing the available families. It never falls back to a generic family, because a silent default ships bad tool-calling with no signal that anything is wrong.
func (*Registry) ResolveFamily ¶
ResolveFamily composes a Provider with an explicitly named family, bypassing prefix resolution. This is the escape hatch for an unsupported model; the caller is expected to warn that no thresholds were measured for it.
func (*Registry) TransportNames ¶
TransportNames lists registered transports, sorted.
type ReplayTransport ¶
type ReplayTransport struct {
// Sent records what the family encoded, so tests can assert on the body
// without a second mechanism.
Sent []WireRequest
// contains filtered or unexported fields
}
ReplayTransport serves recorded frames. Deterministic by construction: the same transcript always produces the same sequence.
func NewReplayTransport ¶
func NewReplayTransport(name string, tr Transcript) *ReplayTransport
NewReplayTransport builds a transport that serves tr under the wire name.
func (*ReplayTransport) Do ¶
func (r *ReplayTransport) Do(ctx context.Context, wire WireRequest) (<-chan WireEvent, error)
Do serves the recorded frames, honouring cancellation between each so interrupt behaviour is exercised by the same fixtures.
func (*ReplayTransport) Name ¶
func (r *ReplayTransport) Name() string
type StreamEvent ¶
type StreamEvent struct {
Type StreamEventType
Text string
ToolCall *ce.ToolCall
Usage *Usage
Err *ProviderError
}
StreamEvent is the neutral event the loop consumes.
Ordering is guaranteed: zero or more TextDelta and ToolCall interleaved, terminated by exactly one Done or one Error. Never both, never neither.
type StreamEventType ¶
type StreamEventType string
StreamEventType identifies what a StreamEvent carries.
const ( EventTextDelta StreamEventType = "text_delta" // EventReasoningDelta carries the model's thinking, which is not its // answer. A client may show it; it never enters the history, because a // model that reads its own reasoning back as something it said out loud // starts defending it. EventReasoningDelta StreamEventType = "reasoning_delta" EventToolCall StreamEventType = "tool_call" EventDone StreamEventType = "done" EventError StreamEventType = "error" )
type Transcript ¶
type Transcript struct {
// Name identifies the fixture in failure output.
Name string `json:"name"`
// Transport is the wire format the frames were recorded from.
Transport string `json:"transport"`
// Frames are the raw payloads, in order.
Frames []string `json:"frames"`
// FailWith, when set, makes Do return this error instead of frames.
FailWith *RecordedError `json:"fail_with,omitempty"`
}
Transcript is a recorded exchange: the frames a transport produced.
func LoadTranscript ¶
func LoadTranscript(path string) (Transcript, error)
LoadTranscript reads a transcript from disk.
type Transport ¶
type Transport interface {
Name() string
Do(ctx context.Context, wire WireRequest) (<-chan WireEvent, error)
}
Transport is the wire format. It knows nothing about prompts, tool schemas or thresholds; a `if family == X` inside a transport means the axes collapsed back into one, and the symptom only shows up at the third family.
type Usage ¶
Usage reports what the call consumed.
CacheReadTokens is not decorative telemetry: it is the only direct measure that append-only context is working. If it stays near zero across a long session, the context engine has regressed and nothing else will say so.
type WireEvent ¶
type WireEvent struct {
// Data is the frame payload. Empty Data with Done set marks end of stream.
Data []byte
Done bool
// Err is a transport-level failure: connection, timeout, status code.
Err error
}
WireEvent is one raw frame off the wire, before the family decodes it.
type WireRequest ¶
type WireRequest struct {
Model string
Body json.RawMessage
Stream bool
}
WireRequest is a family-serialized request, opaque to the transport beyond its envelope.