- integrate a provider that ai-query does not support yet
- wrap an internal gateway or model router
- target an OpenAI-compatible API with custom auth or routing
- run ai-query in a custom runtime with a custom transport
The Extension Points
The provider system is built from a small set of primitives:BaseProvider: implement provider behaviorLanguageModel: wrap a provider + model ID for text generationEmbeddingModel: wrap a provider + model ID for embeddingsHTTPTransport: optionally replace the networking layerGenerateTextResult,StreamChunk,Usage,ToolCall: the result types your provider returns
Two Common Patterns
Most custom providers fit one of these patterns:- OpenAI-compatible wrapper
- Full custom provider
/chat/completions and /embeddings, prefer the first pattern. It is much smaller.
Pattern 1: OpenAI-Compatible Wrapper
This is the simplest approach. SubclassOpenAIProvider, then customize auth and base URL.
Pattern 2: Full Custom Provider
If the API is not OpenAI-compatible, subclassBaseProvider directly.
You must implement:
generate()stream()
embed()embed_many()
Minimal Example
Provider Contracts
generate()
generate() returns a GenerateTextResult.
At minimum, set:
textfinish_reasonresponse
usage when the upstream API exposes token information.
If the model requests tools, place parsed ToolCall values in result.response["tool_calls"].
stream()
stream() yields StreamChunk objects.
Typical shape:
- yield chunks with
text=... - accumulate any tool call state if the API streams it incrementally
- yield one final chunk with:
is_final=Trueusage=...finish_reason=...tool_calls=[...]when present
Tool Calling
If your provider supports tool calling:- accept
tools: ToolSet | None - convert
ToolSetinto the provider’s tool schema - parse the provider’s tool-call response into ai-query
ToolCallobjects - return them in
response["tool_calls"]forgenerate()or in the finalStreamChunk.tool_callsforstream()
generate_text() / stream_text() handles the rest.
Provider Options
ai-query passes all provider-specific options throughprovider_options.
Inside your provider, use:
self.name.
Example:
Embeddings
If your provider supports embeddings, implementembed() and optionally embed_many().
Factory pattern:
Custom Transports
If you need a custom runtime or HTTP stack, pass a customHTTPTransport to your provider.
Recommended Workflow
- Start with an OpenAI-compatible wrapper if possible.
- Only subclass
BaseProviderdirectly when the upstream API shape is meaningfully different. - Implement
generate()first. - Add
stream()once non-streaming works. - Add embeddings only if the provider supports them.
- Add tests that validate message conversion, tool call parsing, and usage extraction.