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
- func Unmeasured(family 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 Gemini
- func (Gemini) AcceptsImages() bool
- func (Gemini) DefaultLimits() Limits
- func (f Gemini) Encode(req Request, transport string) (WireRequest, error)
- func (Gemini) Models() []string
- func (Gemini) Name() string
- func (Gemini) NewDecoder(tools []ce.ToolDef) Decoder
- func (Gemini) Transports() []string
- func (Gemini) Window(string) (int, error)
- 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 ClaudeName = "claude"
ClaudeName is the family, and the credential store with it.
const GeminiName = "gemini"
GeminiName is the family, and it is also the credential store: a key for gemini-2.5-pro reaches the same account as one for a later model.
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.
func Unmeasured ¶ added in v0.18.0
Unmeasured reports what a session using this family has to say out loud, or "" when the family has measurements behind it.
A named family in this repository reads as a measured one. `Measurement.Model` exists precisely because a threshold belongs to a model and says nothing about another, so adding a family and saying nothing would put a name where the verification is not — the defect this project keeps finding in itself, wearing a new hat.
This is not a list to be maintained by hand: `TestEveryUnmeasuredFamilySaysSo` checks it against the measurements actually recorded, in both directions. A family that gets measured and stays on this list fails, and so does one that leaves it without a measurement.
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 Gemini ¶ added in v0.18.0
type Gemini struct{ MiniMaxM3 }
Gemini speaks the OpenAI dialect, through Google's compatibility surface.
It embeds MiniMaxM3 for the encoding and the decoder, the same way Generic does and for the same reason: the wire format is the wire format, and a second copy of it would be a second thing to keep in step. What Gemini overrides is everything the FAMILY axis is for — the name, the models, the window, the limits and what it can read.
The native surface (`:streamGenerateContent`) is deliberately not this. It puts the model in the URL, authenticates with a header of its own, frames its stream differently, and encodes calls as `functionCall`/`functionResponse` with `user`/`model` roles and a separate `systemInstruction`. That is a TRANSPORT, and writing one before anybody has run this family against a real key would be building the harder half first on a guess.
func (Gemini) AcceptsImages ¶ added in v0.18.0
AcceptsImages is true. Gemini is natively multimodal and the compatibility surface takes a picture the same way the OpenAI dialect does, as a data URL, so the encoding this family inherits already produces the right bytes.
Declared rather than attempted, which is the rule for this field — and declared without having been run against a key here, which is the honest caveat. Saying false would be the larger error: it would make the one property this model family is best known for unavailable, silently.
func (Gemini) DefaultLimits ¶ added in v0.18.0
DefaultLimits is the cautious ceiling, because the horizon is unmeasured.
MiniMax's 2000 is justified by a cited long-horizon run and by a real session this ceiling truncated. Nothing of the sort exists for Gemini, and copying a number across families is how a limit stops meaning anything. Fifty is what Claude uses and what Generic uses, sized from a refactor across ten files.
func (Gemini) Encode ¶ added in v0.18.0
func (f Gemini) Encode(req Request, transport string) (WireRequest, error)
Encode refuses a transport this family does not declare.
Inherited from MiniMaxM3 it would happily serialize the Anthropic dialect, for a family whose Transports() says it speaks one. The registry would not compose that pair, so this is not reachable today — and an unreachable disagreement between two methods of the same type is exactly the kind that becomes reachable later without anyone noticing.
func (Gemini) Transports ¶ added in v0.18.0
func (Gemini) Window ¶ added in v0.18.0
Window is one conservative number rather than a table.
The current line is documented at 1,048,576 input tokens; this returns a million flat. Under-guessing compacts early and costs a summary, over-guessing overruns the window and loses the turn, and the asymmetry decides which side to be wrong on. A per-model table would be a list of numbers nobody here can check, going stale on Google's release schedule rather than on ours.
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
// CallID and CallName identify a call still arriving; Bytes is how much of
// its arguments have landed. Set only on the two opened/progress types.
CallID string
CallName string
Bytes int
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.
ToolCallOpened and ToolCallProgress may appear before the ToolCall they describe, and a consumer that ignores them still sees exactly the sequence it saw before — which is what lets a provider that cannot report them stay silent rather than pretend.
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" // EventToolCallOpened says a tool call has begun arriving. The name and // the id are known long before the arguments finish, and throwing them // away is what makes a screen sit still while a model writes a file. EventToolCallOpened StreamEventType = "tool_call_opened" // EventToolCallProgress is how much of a call's arguments have arrived. EventToolCallProgress StreamEventType = "tool_call_progress" 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.