Drivers
The engine is provider-neutral; drivers are where the vendor lives. elelem ships three, and if you’re adding another one, the rest of this page is the contract.
Contents
- Using the shipped drivers
- The interface
- The one rule
- Reasoning on compatible endpoints
- Z.ai Coding
- Capabilities are a promise
- Errors
- The conformance suite
- Wire tests
Using the shipped drivers
drivers/openai | drivers/anthropic | drivers/zaicoding | |
|---|---|---|---|
| Talks to | OpenAI and anything OpenAI-shaped, including vLLM, Ollama, OpenRouter, LM Studio, and compatible proxies | The Anthropic Messages API | Z.ai Coding’s OpenAI-shaped endpoint |
| Model discovery | ListModels(ctx) live, plus KnownModels() / LookupModel(id) | same | same |
| Unknown model ids | accepted, the provider decides | accepted | accepted when no reasoning control is set |
The OpenAI-compatible and Anthropic drivers take the same options:
openai.NewDriver(
openai.WithAPIKey(apiKey),
openai.WithBaseURL("https://your-openai-compatible-endpoint/v1"),
openai.WithHTTPClient(httpClient),
openai.WithSDKOptions(/* raw SDK options */),
)
NewDriver is convenient for a one-provider program, because the official SDK can read its usual environment variables (OPENAI_* or ANTHROPIC_*). A multi-provider application that owns credential resolution should start every configured driver with WithoutEnvironmentDefaults(), then pass only that upstream’s own key and HTTP client plus a URL when it is not the driver’s official default. That stops one provider’s environment credential from quietly reaching a keyless local endpoint. zaicoding.NewDriver never reads the environment and has no such option.
WithBaseURL strips userinfo credentials from the URL before the SDK sees it, because the SDKs put the request URL into the text of every error they build. The OpenAI driver also has WithAssistantMessageExtra and WithFinishReasonMapper for specialized drivers built on its transport; the Z.ai Coding driver is one.
Capabilities are per MODEL, not per provider. Driver.Capabilities(model) reports what one model supports: seed, tool choice, parallel tool calls, strict tool arguments, JSON schema, sampling parameters, reasoning effort and its ceiling. Anthropic rejects a non-default temperature on newer models while accepting it on older ones, so a single per-provider table would be wrong. The engine reads that struct and rejects an unsupported parameter locally, before any network call, instead of sending it and getting a cryptic 400 back.
Streaming is on by default. Some OpenAI- and Anthropic-compatible backends can’t do it, so WithStreaming(false) sends the same request with streaming off and feeds the finished response through the same callbacks. See Requests.
The interface
type Driver interface {
Stream(context.Context, DriverRequest, func(Delta) error) (Usage, error)
Complete(context.Context, DriverRequest, func(Delta) error) (Usage, error)
ListModels(context.Context) ([]string, error)
Capabilities(Model) Capabilities
TokenCounter() TokenCounter
}
Five methods. TokenCounter may return nil, in which case the engine falls back through its own resolution order (see history.md).
A driver’s job: translate portable requests and streams, validate provider transcript constraints, normalize finish reasons and usage, and report conservative model capabilities.
Complete is the same call with streaming off, and it takes the same delta callback deliberately. Everything downstream of a driver is delta-shaped (the engine’s tool-call assembler, every On* callback, the content-block protocol a consumer builds on top), and none of it has a second code path for a non-streaming turn. So Complete issues the finished request, then feeds the response through the callback as one delta per piece of content, and the caller cannot tell which path ran except by timing. Returning a whole Message instead would have forced a parallel implementation of all of it.
Two details a new driver gets wrong in the same way both shipped ones did:
- Tool-call indices are the ordinal among TOOL CALLS, not the position of the block in the response. Number them by their position in the content array and the engine pairs results to calls that do not exist.
- Validate the transcript on this path too. Skipping it ships an orphaned tool result that the provider rejects on the NEXT request, which is the worst possible place to find out.
If the provider cannot stream at all, say so with Capabilities.StreamingUnsupported and the engine takes Complete regardless of what the caller asked for. See requests.md.
The one rule
Provider-specific behavior must not leak into the engine API.
The moment elelem.Request grows a field only one vendor understands, you have a wrapper instead of an engine, and the caller may as well have imported that SDK directly. Anything genuinely vendor-shaped goes through WithParam / WithParams rather than into a portable type.
The shipped drivers keep their SDK types entirely inside their own packages. Nothing from openai-go or anthropic-sdk-go appears in a signature the caller touches.
Reasoning on compatible endpoints
The OpenAI-compatible driver preserves returned reasoning, reasoning_content, reasoning_details, and thinking_blocks extensions in Message.ProviderReasoning, in both streaming and non-streaming mode. Tool continuation replays those fields with the assistant message. Streamed text and signature fragments are assembled in block order; signed and encrypted state stays opaque to the engine. No endpoint name or model-name heuristic enables this behavior. The driver preserves only fields the upstream actually returns. Ordinary responses add no reasoning fields to later requests.
Persist ProviderReasoning unchanged alongside the message. Its envelope binds replay to the transport and requested model ID; foreign or mismatched state is omitted. Malformed matching state and unsupported replay fields fail locally with ErrInvalidTranscript. Plain Message.Reasoning is display text and is never promoted into a replay field by guessing a provider contract. Specialized transports may continue using WithAssistantMessageExtra to supply their own replay dialect.
Each driver’s counter budgets only the reasoning its translator replays. These counts remain local estimates, not provider billing. See history counting.
Z.ai Coding
drivers/zaicoding is the provider-specific choice for Z.ai Coding’s OpenAI-shaped endpoint. It uses the Coding endpoint by default, sends Z.ai’s thinking object, and preserves reasoning_content on the next request after a tool call.
driver := zaicoding.NewDriver(zaicoding.WithAPIKey(apiKey))
model := zaicoding.LookupModel("glm-5.3")
The generic OpenAI driver is still the one to use for ordinary OpenAI-compatible endpoints. The maintained Coding Plan catalog contains GLM 5.3 and GLM 5.3 Flash, both with Z.ai’s documented 1,000,000-token context window. LookupModel also gives GLM 5.3 Flash X and the retired IDs the Coding Plan routes (glm-5.2 and glm-5.1 to GLM 5.3, glm-4.7 to GLM 5.3 Flash) the metadata of the model that serves them.
What the driver sends and checks:
- Thinking is always enabled with
clear_thinking: false, so earlier turns’ reasoning stays in the conversation. PassWithParam("thinking", map[string]any{"clear_thinking": true})to drop it. Disabling thinking is refused. - Both models take reasoning effort with the Coding Plan mapping:
minimalandlowsendlow,mediumandhighsendhigh,xhighandmaxsendmax.noneis refused, because thinking cannot be turned off. MaxOutputTokensis sent asmax_tokens, the field the endpoint honors, and may not exceed 131,072 on a known model.- A streaming request with tools asks for
tool_stream, so tool-call arguments arrive in fragments. - Tool choice is not claimed. The endpoint treats
requiredand a named tool asauto, and the driver refuses them locally. More than four stop words are refused. - GLM 5.3 is text-only. GLM 5.3 Flash takes images, audio, and files.
Z.ai’s own finish reasons map onto the portable ones: sensitive is FinishReasonContentFilter and model_context_window_exceeded is FinishReasonContextExceeded. network_error ends the call with a retryable server error. Business codes become sentinels. The balance, plan-window, plan-expiry and plan-entitlement 429s (1113, 1308 to 1311, 1313 to 1321) are elelem.ErrQuotaExhausted and are never retried, 1261 is elelem.ErrContextExceeded, and 1301 is zaicoding.ErrSensitiveContent. Request rate limits (1302) and overload (1305) stay retryable.
Unsupported values fail locally with zaicoding.ErrUnsupportedParameter. Specialized drivers over the OpenAI transport add provider finish reasons with openai.WithFinishReasonMapper, which the transport consults only for values it does not recognize.
Capabilities are a promise
Report conservatively. A capability you claim and don’t enforce is worse than one you don’t claim. The engine will let a request through on the strength of it and the provider will reject it.
- Context and output limits are NOT capabilities. They live on
Model(ContextSize,Pricing) because they’re caller-supplied facts, not driver-discovered ones. MaxReasoningEffortis a CEILING, not a whitelist. A model’s supported effort set can be non-contiguous (one may acceptmaxwhile rejectingxhigh), and a single ceiling can’t express that. Passing the rank check is necessary but not sufficient; the driver makes the final call and returns its ownErrUnsupportedParameterfor a level inside the ceiling the model doesn’t actually take. That sentinel is per-driver.openai.ErrUnsupportedParameterandanthropic.ErrUnsupportedParameterare distinct values, since each names its own provider in the message.- Unknown model ids stay usable. If the provider accepts an id you’ve never heard of, let it through. Just don’t guess its optional capabilities.
Errors
Build failures as ProviderError carrying the provider’s own code, and join ProviderSentinel(status, code):
return &elelem.ProviderError{ /* ... */ }
The & is load-bearing. Error() is declared on the pointer receiver, so a bare elelem.ProviderError{} value does not satisfy error and will not compile.
That’s what makes this work for a caller who doesn’t know or care which provider served the request:
errors.Is(err, commonerrors.ErrRateLimited)
Helpers you should use rather than reimplement:
| Helper | Does |
|---|---|
ParseRetryAfter | Both Retry-After forms; never returns a negative or unbounded delay. |
SanitizeBaseURL | Strips userinfo credentials from an endpoint before the SDK can bake them into error text that gets logged. |
Remember that both shipped providers report mid-stream failures in band inside an HTTP 200. Surface the provider’s error code; the retry decorator consults it ahead of the status precisely because the status is lying in that case. See retries.md.
The conformance suite
elelemtest/conformance.Run is aimed at you, and all shipped drivers run it, so it’s a live contract, not a document that drifted.
func TestDriverConformance(t *testing.T) {
server := httptest.NewServer(/* a fake provider endpoint */)
t.Cleanup(server.Close)
conformance.Run(t,
func() elelem.Driver {
return NewDriver(WithBaseURL(server.URL), WithAPIKey("test-key"))
},
conformance.Options{
Request: elelem.DriverRequest{
Model: elelem.Model{ID: "some-model"},
Messages: []elelem.Message{{
Role: elelem.RoleUser,
Content: elelem.Text("hi"),
}},
},
NetworkCalls: func() int64 { return atomic.LoadInt64(&calls) },
Models: []elelem.Model{modelA, modelB},
},
)
}
What it checks:
- stream contract: delta ordering, usage invariants, normalized finish reasons, and that the deltas and the usage agree about how the run ended
- model listing: skippable via
Options.SkipListModels - cancelled context: a cancelled ctx stops work rather than completing
- invalid transcript is local: a malformed transcript is rejected WITHOUT a network call, which is what
NetworkCallsis for - capabilities are honest: every advertised capability is actually enforced, per model
Options.Models re-runs the capability contract against each model in place of Request.Model. Capabilities is a per-model function, so a suite pinned to one model can’t see a driver that gates correctly for the model it was handed and wrongly for its siblings. That is exactly where the real bugs are.
The subtests deliberately do NOT call t.Parallel(). NetworkCalls is a single shared counter and the transcript and capability cases assert on its delta across one call; running them concurrently would interleave increments and make those assertions meaningless. Serial is a correctness requirement here, not an oversight.
Wire tests
Use in-process fake HTTP servers and no live credentials. The shipped drivers’ tests replay recorded SSE against httptest.NewServer, which is fast, hermetic, and can reproduce the failure shapes a real provider only emits occasionally: mid-stream errors inside a 200, truncated streams, malformed tool-call fragments.