provider

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 14 Imported by: 0

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

View Source
const (
	TransportOpenAI    = "openai"
	TransportAnthropic = "anthropic"
)

Wire names.

View Source
const GenericName = "generic"

GenericName is the escape hatch a user names explicitly.

View Source
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

func ParseSSE(body string) []string

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

func RetryAfterOf(header string, now time.Time) time.Duration

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 Sanitize

func Sanitize(s string) string

Sanitize is the exported form, for transports and families building messages.

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

func (b Backoff) Wait(attempt int, e *ProviderError) (time.Duration, bool)

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

func (Claude) AcceptsImages() bool

AcceptsImages is true, through a source block rather than a data URL.

func (Claude) DefaultLimits

func (Claude) DefaultLimits() Limits

DefaultLimits is sized from the case that shaped it: a refactor across ten files runs thirty to fifty tool calls.

func (Claude) Encode

func (f Claude) Encode(req Request, transport string) (WireRequest, error)

func (Claude) Models

func (Claude) Models() []string

func (Claude) Name

func (Claude) Name() string

func (Claude) NewDecoder

func (Claude) NewDecoder(tools []ce.ToolDef) Decoder

func (Claude) Transports

func (Claude) Transports() []string

func (Claude) Window

func (Claude) Window(string) (int, error)

type Decision

type Decision string

Decision is what the loop should do about an error.

const (
	DecisionAbort      Decision = "abort"
	DecisionRetry      Decision = "retry"
	DecisionWait       Decision = "wait"
	DecisionCompact    Decision = "compact"
	DecisionFeedToolIn Decision = "feed_to_model"
	DecisionSilent     Decision = "silent"
)

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

func FamilyFor(model string, families []Family) (Family, bool)

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

func (Generic) AcceptsImages() bool

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

func (Generic) DefaultLimits() Limits

DefaultLimits are the cautious ones, for the same reason.

func (Generic) Models

func (Generic) Models() []string

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) Name

func (Generic) Name() string

func (Generic) Transports

func (Generic) Transports() []string

func (Generic) Window

func (Generic) Window(string) (int, error)

Window is the conservative guess.

Under-guessing compacts early and costs a summary; over-guessing overruns the window and loses the turn. The asymmetry decides the number.

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

func (MiniMaxM3) AcceptsImages() bool

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

func (MiniMaxM3) DefaultLimits() Limits

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) Models

func (MiniMaxM3) Models() []string

func (MiniMaxM3) Name

func (MiniMaxM3) Name() string

func (MiniMaxM3) NewDecoder

func (f MiniMaxM3) NewDecoder(tools []ce.ToolDef) Decoder

func (MiniMaxM3) Transports

func (MiniMaxM3) Transports() []string

func (MiniMaxM3) Window

func (MiniMaxM3) Window(string) (int, error)

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 NewRegistry

func NewRegistry() *Registry

NewRegistry returns an empty registry.

func (*Registry) FamilyNames

func (r *Registry) FamilyNames() []string

FamilyNames lists registered families, sorted for a stable error message.

func (*Registry) RegisterFamily

func (r *Registry) RegisterFamily(f Family) error

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

func (r *Registry) RegisterTransport(t Transport)

RegisterTransport adds a wire format.

func (*Registry) Resolve

func (r *Registry) Resolve(model, transportOverride string) (Provider, error)

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

func (r *Registry) ResolveFamily(familyName, transportOverride string) (Provider, error)

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

func (r *Registry) TransportNames() []string

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 Request

type Request struct {
	Model     string
	Messages  []ce.Message
	Tools     []ce.ToolDef
	MaxTokens int
}

Request is the neutral call. Only contextengine types cross this boundary.

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

type Usage struct {
	InputTokens      int
	OutputTokens     int
	CacheReadTokens  int
	CacheWriteTokens int
}

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL