skyl ships four adapters. Three are native — written against a specific vendor's API, with full fidelity. One is generic, and reaches roughly eighteen hosts that speak OpenAI's wire format. This page is about picking correctly.
You will learn
- The difference between a native adapter and
openaicompat - Which vendors each one reaches
- How to choose a provider at runtime, since
Provideris just an interface - When to run two providers side by side
The four adapters#
| Adapter | Module | Go | Reaches |
|---|---|---|---|
| provider/anthropic | its own | 1.24 | Claude Opus 5, Fable 5, Sonnet 5, Haiku 4.5, and the 4.x family |
| provider/openai | core | 1.22 | GPT-5.6 (Sol, Terra, Luna), GPT-5.5, GPT-5.4 nano, the o-series, and gpt-oss |
| provider/gemini | core | 1.22 | Gemini 3.6 Flash, 3.5/3.1 Flash-Lite, and 3 Pro |
| provider/openaicompat | core | 1.22 | ~18 hosts including xAI, DeepSeek, Groq, OpenRouter, Ollama, vLLM and LM Studio |
Native adapters#
Use a native adapter when you want a vendor's deep features and its exact error semantics.
import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/anthropic"
p := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/anthropic"
p := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/openai"
p := openai.New(os.Getenv("OPENAI_API_KEY"))import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/openai"
p := openai.New(os.Getenv("OPENAI_API_KEY"))import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/gemini"
p := gemini.New(os.Getenv("GEMINI_API_KEY"))import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/gemini"
p := gemini.New(os.Getenv("GEMINI_API_KEY"))What "native" buys you concretely:
- Anthropic is the only adapter that emits
EventThinkingDelta, the only one that reportsCacheWriteTokens, and the only one whereToolResult.IsErrorreaches the model as a real boolean. - Gemini is the only adapter where
Request.Thinkingmaps completely, including an explicit zero budget. - OpenAI maps
Thinking.Effortontoreasoning_effort, which Anthropic cannot do.
Those differences are not marketing — they are rows in the feature matrix, and each one is a place where picking the wrong adapter silently costs you a capability.
The compatible adapter#
A large part of the industry serves OpenAI's wire format. One adapter reaches all of it:
import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/openaicompat"
p := openaicompat.New(
openaicompat.WithBaseURL("https://api.groq.com/openai/v1"),
openaicompat.WithAPIKey(os.Getenv("GROQ_API_KEY")),
openaicompat.WithName("groq"),
)import "github.com/BAGOMBEKA-JOB-DEV/skyl/provider/openaicompat"
p := openaicompat.New(
openaicompat.WithBaseURL("https://api.groq.com/openai/v1"),
openaicompat.WithAPIKey(os.Getenv("GROQ_API_KEY")),
openaicompat.WithName("groq"),
)| Host | Base URL | Key? | Notes |
|---|---|---|---|
| xAI (Grok) | https://api.x.ai/v1 | Required | |
| DeepSeek | https://api.deepseek.com/v1 | Required | |
| Mistral | https://api.mistral.ai/v1 | Required | |
| Groq | https://api.groq.com/openai/v1 | Required | |
| Together | https://api.together.xyz/v1 | Required | |
| Fireworks | https://api.fireworks.ai/inference/v1 | Required | |
| OpenRouter | https://openrouter.ai/api/v1 | Required | Brokers 300+ models on its own, and supplies DisplayName and ContextWindow on model listing. |
| Perplexity | https://api.perplexity.ai | Required | |
| Cerebras | https://api.cerebras.ai/v1 | Required | |
| DeepInfra | https://api.deepinfra.com/v1/openai | Required | |
| Qwen / DashScope | https://dashscope.aliyuncs.com/compatible-mode/v1 | Required | |
| Moonshot (Kimi) | https://api.moonshot.cn/v1 | Required | |
| Z.ai (GLM) | https://open.bigmodel.cn/api/paas/v4 | Required | |
| Nvidia NIM | https://integrate.api.nvidia.com/v1 | Required | |
| Ollama | http://localhost:11434/v1 | None | Local. Needs no credential — omit WithAPIKey entirely. |
| vLLM | http://localhost:8000/v1 | None | Self-hosted. Returns content as an array of blocks on some builds, which skyl handles. |
| LM Studio | http://localhost:1234/v1 | None | Local. |
| llama.cpp | http://localhost:8080/v1 | None | Local. |
Set WithName#
It defaults to openai-compatible. If you run more than one compatible host,
leaving the default means every one of them reports the same label in
Response.Provider, in errors, and in hook events — so your metrics cannot tell
Groq from DeepSeek.
Fidelity caveat#
These endpoints implement OpenAI's format, not necessarily its features. Tool calling, streaming, and multimodal support vary by host and by model. skyl surfaces what the endpoint returns; where a host rejects a feature you get that host's error, classified, rather than a skyl-invented one.
Local models#
Three of the compatible hosts run on your own machine and need no credential at all:
// Ollama. Note there is no WithAPIKey — it authenticates nothing.
p := openaicompat.New(
openaicompat.WithBaseURL("http://localhost:11434/v1"),
openaicompat.WithName("ollama"),
)
resp, err := skyl.New(p).Complete(ctx, &skyl.Request{
Model: "llama3.3",
Messages: []skyl.Message{skyl.UserText("Hello")},
})// Ollama. Note there is no WithAPIKey — it authenticates nothing.
p := openaicompat.New(
openaicompat.WithBaseURL("http://localhost:11434/v1"),
openaicompat.WithName("ollama"),
)
resp, err := skyl.New(p).Complete(ctx, &skyl.Request{
Model: "llama3.3",
Messages: []skyl.Message{skyl.UserText("Hello")},
})This is the case that makes "develop locally, deploy against a frontier model" a configuration change rather than two code paths.
Choosing at runtime#
Provider is an interface, so this is ordinary Go — no registry, no plugin
system, no reflection.
func pick(name string) (skyl.Provider, error) {
switch name {
case "anthropic":
return anthropic.New(os.Getenv("ANTHROPIC_API_KEY")), nil
case "openai":
return openai.New(os.Getenv("OPENAI_API_KEY")), nil
case "gemini":
return gemini.New(os.Getenv("GEMINI_API_KEY")), nil
case "ollama":
return openaicompat.New(
openaicompat.WithBaseURL("http://localhost:11434/v1"),
openaicompat.WithName("ollama"),
), nil
default:
return nil, fmt.Errorf("unknown provider %q", name)
}
}func pick(name string) (skyl.Provider, error) {
switch name {
case "anthropic":
return anthropic.New(os.Getenv("ANTHROPIC_API_KEY")), nil
case "openai":
return openai.New(os.Getenv("OPENAI_API_KEY")), nil
case "gemini":
return gemini.New(os.Getenv("GEMINI_API_KEY")), nil
case "ollama":
return openaicompat.New(
openaicompat.WithBaseURL("http://localhost:11434/v1"),
openaicompat.WithName("ollama"),
), nil
default:
return nil, fmt.Errorf("unknown provider %q", name)
}
}Running two at once#
There is nothing special about holding several clients. A common shape is a cheap model for classification and an expensive one for reasoning:
type Models struct {
Fast *skyl.Client // cheap, high volume
Smart *skyl.Client // expensive, low volume
}
func New() Models {
return Models{
Fast: skyl.New(gemini.New(os.Getenv("GEMINI_API_KEY"))),
Smart: skyl.New(anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))),
}
}type Models struct {
Fast *skyl.Client // cheap, high volume
Smart *skyl.Client // expensive, low volume
}
func New() Models {
return Models{
Fast: skyl.New(gemini.New(os.Getenv("GEMINI_API_KEY"))),
Smart: skyl.New(anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))),
}
}Both clients are safe for concurrent use, and both build the same Request
type — so the code that constructs a prompt does not need to know which one
will serve it.
Deep diveShould I add fallback between providers?
skyl deliberately does not do this for you. Client retries the same
provider, because that is a well-defined operation with well-defined
idempotency. Failing over to a different vendor is a product decision: the
second model will answer differently, may cost differently, and may have
different data-residency implications.
If you want it, it is a short function you write and control:
func completeWithFallback(ctx context.Context, primary, backup *skyl.Client, req *skyl.Request) (*skyl.Response, error) {
resp, err := primary.Complete(ctx, req)
if err == nil {
return resp, nil
}
// Only fail over on the provider's problems, never on yours.
if !errors.Is(err, skyl.ErrServer) && !errors.Is(err, skyl.ErrRateLimit) {
return nil, err
}
return backup.Complete(ctx, req)
}func completeWithFallback(ctx context.Context, primary, backup *skyl.Client, req *skyl.Request) (*skyl.Response, error) {
resp, err := primary.Complete(ctx, req)
if err == nil {
return resp, nil
}
// Only fail over on the provider's problems, never on yours.
if !errors.Is(err, skyl.ErrServer) && !errors.Is(err, skyl.ErrRateLimit) {
return nil, err
}
return backup.Complete(ctx, req)
}Note the classification check. Falling over on ErrBadRequest would just send a
malformed request to a second vendor and get a second rejection.
Recap
- Native adapters (
anthropic,openai,gemini) give full vendor fidelity and deep features. openaicompatreaches ~18 hosts including Ollama, vLLM and LM Studio.openaicompat.Newpanics withoutWithBaseURL; always setWithNametoo.- Compatible hosts implement OpenAI's format, not necessarily its features.
- Selecting a provider at runtime is a plain
switch—Provideris just an interface. - Cross-provider fallback is deliberately yours to write, because it is a product decision.
Try out some challenges
Each of these is solvable with what this page covered. Run them against the sandbox — no API key needed.
Name your compatible providers
Write a helper that builds an openaicompat provider for any host, so nobody
can forget WithName.
Show hint
Make the name a required parameter of your own function rather than an option.
Show solution
func compat(name, baseURL, key string) skyl.Provider {
opts := []openaicompat.Option{
openaicompat.WithBaseURL(baseURL),
openaicompat.WithName(name),
}
// Local runtimes authenticate nothing; do not send an empty credential.
if key != "" {
opts = append(opts, openaicompat.WithAPIKey(key))
}
return openaicompat.New(opts...)
}func compat(name, baseURL, key string) skyl.Provider {
opts := []openaicompat.Option{
openaicompat.WithBaseURL(baseURL),
openaicompat.WithName(name),
}
// Local runtimes authenticate nothing; do not send an empty credential.
if key != "" {
opts = append(opts, openaicompat.WithAPIKey(key))
}
return openaicompat.New(opts...)
}Because name is positional, the compiler enforces what an option cannot.
Pick the right adapter for a requirement
You need ToolResult.IsError to reach the model faithfully, so it can recover
when a tool fails. Which adapters can do that?
Show hint
Check the ToolResult.IsError row of the feature matrix.
Show solution
Only Anthropic. It maps to a real is_error boolean.
The OpenAI-format adapters are lossy — they prefix "error: " to the content
and drop the flag entirely when the content is empty. Gemini drops it
completely; the signal never reaches the wire.
The practical workaround for the other three is to put the failure in the result text, where the model will read it:
skyl.ToolResultMessage(call.ID, "ERROR: the weather service returned 503. Do not retry.")skyl.ToolResultMessage(call.ID, "ERROR: the weather service returned 503. Do not retry.")