Last updated 2026-05-17
OpenAI-compatible Endpoints
Many providers and self-hosted runtimes implement the OpenAI Chat
Completions API on the wire — POST /chat/completions
that takes an OpenAI-shaped JSON body and returns an OpenAI-shaped
SSE event stream. The OpenAI provider is
byte-passthrough at both ends, so pointing it at any of these
endpoints Just Works: switch the base URL, switch the API key,
keep everything else.
The pattern
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")), llmrouter.WithBaseURL("https://openrouter.ai/api/v1"),)
That's it. The provider issues
POST {BaseURL}/chat/completions with
Authorization: Bearer <key>. If the upstream is
OpenAI-compatible, no further translation is needed.
The rest of this page is a directory of known-compatible endpoints with their base URLs and any per-endpoint quirks. For the application-side semantics — streaming, error handling, multimodal content, token counting — see the OpenAI provider page; everything there applies here.
Hosted endpoints
OpenRouter
OpenRouter is a meta-provider — one base URL, one API key, hundreds of upstream models from dozens of vendors.
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")), llmrouter.WithBaseURL("https://openrouter.ai/api/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "anthropic/claude-3.5-sonnet", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})
OpenRouter recommends sending HTTP-Referer and
X-Title headers so your traffic shows up in their
dashboard with a friendly name. Both are optional. Add them via a
custom HTTP transport — see the full
OpenRouter example below.
Together AI
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("TOGETHER_API_KEY")), llmrouter.WithBaseURL("https://api.together.xyz/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "meta-llama/Llama-3.3-70B-Instruct-Turbo", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})
Models are listed at together.ai/models. The full
model id (with vendor prefix) is what goes in
ChatRequest.Model.
Groq
Groq runs open-weight models on custom LPU silicon for very low
latency. The endpoint URL has an /openai/v1 suffix
that's easy to forget:
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("GROQ_API_KEY")), llmrouter.WithBaseURL("https://api.groq.com/openai/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "llama-3.3-70b-versatile", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})DeepSeek
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("DEEPSEEK_API_KEY")), llmrouter.WithBaseURL("https://api.deepseek.com/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "deepseek-chat", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})Fireworks
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("FIREWORKS_API_KEY")), llmrouter.WithBaseURL("https://api.fireworks.ai/inference/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "accounts/fireworks/models/llama-v3p3-70b-instruct", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})Perplexity
Perplexity's chat completions endpoint sits at the bare
/api.perplexity.ai host with no version suffix:
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("PERPLEXITY_API_KEY")), llmrouter.WithBaseURL("https://api.perplexity.ai"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "llama-3.1-sonar-large-128k-online", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "What happened in tech today?"), },})Self-hosted runtimes
vLLM
vLLM serves any
Hugging Face model with an OpenAI-compatible HTTP API. Start the
server with vllm serve <model> and point the
provider at port 8000:
p, err := openai.New( llmrouter.WithAPIKey("none"), // vLLM doesn't require auth by default llmrouter.WithBaseURL("http://your-host:8000/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "meta-llama/Llama-3.1-8B-Instruct", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})
The Model string is the same HF model id you passed to
vllm serve. WithAPIKey rejects an empty
string, so pass any non-empty placeholder when the server doesn't
require auth.
Ollama
Ollama runs quantized open models locally with a tiny memory
footprint. It exposes an OpenAI-compatibility layer at
localhost:11434/v1:
p, err := openai.New( llmrouter.WithAPIKey("ollama"), // any non-empty placeholder llmrouter.WithBaseURL("http://localhost:11434/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "llama3.2", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})LM Studio
p, err := openai.New( llmrouter.WithAPIKey("lm-studio"), llmrouter.WithBaseURL("http://localhost:1234/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "lmstudio-community/Meta-Llama-3.1-8B-Instruct-GGUF", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})
LM Studio's GUI shows the loaded model identifier; copy that string
into ChatRequest.Model.
LocalAI
p, err := openai.New( llmrouter.WithAPIKey("localai"), llmrouter.WithBaseURL("http://localhost:8080/v1"),)
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{ Model: "gpt-4", // LocalAI aliases configured in your models.yaml Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "Hello!"), },})Self-hosted compatibility notes
Any HTTP server that implements POST /chat/completions
with OpenAI-shaped request and SSE response bodies works. The
minimum the server has to honour is:
- Request body. An OpenAI Chat Completions JSON
object on
POST. The provider always sets"stream": trueand (unless a caller overrides)"stream_options": {"include_usage": true}. - Response framing.
Content-Type: text/event-streamwithdata:lines, each carrying an OpenAIchat.completion.chunkJSON payload, ending with a literaldata: [DONE]line to terminate. - Error responses. Anything ≥ 400 surfaces as
*llmrouter.ErrUpstream; the response body is captured (up to 1 KiB).
A server that supports the basic shape but doesn't emit a usage
block on the final chunk will simply leave Chunk.Usage
nil — everything else still works.
Endpoint reference
| Provider | Base URL | Auth |
|---|---|---|
| OpenAI | https://api.openai.com/v1 | Bearer |
| OpenRouter | https://openrouter.ai/api/v1 | Bearer + optional HTTP-Referer / X-Title |
| Together AI | https://api.together.xyz/v1 | Bearer |
| Groq | https://api.groq.com/openai/v1 | Bearer |
| DeepSeek | https://api.deepseek.com/v1 | Bearer |
| Fireworks | https://api.fireworks.ai/inference/v1 | Bearer |
| Perplexity | https://api.perplexity.ai | Bearer |
| vLLM | http://your-host:8000/v1 | None / Bearer |
| Ollama | http://localhost:11434/v1 | None |
| LM Studio | http://localhost:1234/v1 | None |
| LocalAI | http://localhost:8080/v1 | None / Bearer |
Example: OpenRouter with custom headers
OpenRouter accepts two optional headers that improve their
dashboard analytics: HTTP-Referer (your app URL) and
X-Title (a human-readable app name). Wrap the HTTP
client with a RoundTripper:
package main
import ( "context" "fmt" "log" "net/http" "os" "time"
"github.com/elloloop/llmrouter" "github.com/elloloop/llmrouter/providers/openai")
type openrouterHeaders struct { base http.RoundTripper referer string title string}
func (t *openrouterHeaders) RoundTrip(req *http.Request) (*http.Response, error) { if t.referer != "" { req.Header.Set("HTTP-Referer", t.referer) } if t.title != "" { req.Header.Set("X-Title", t.title) } return t.base.RoundTrip(req)}
func main() { httpClient := &http.Client{ Timeout: 120 * time.Second, Transport: &openrouterHeaders{ base: http.DefaultTransport, referer: "https://my-app.example.com", title: "My Demo App", }, }
p, err := openai.New( llmrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")), llmrouter.WithBaseURL("https://openrouter.ai/api/v1"), llmrouter.WithHTTPClient(httpClient), ) if err != nil { log.Fatal(err) }
stream, err := p.CompletionStream(context.Background(), llmrouter.ChatRequest{ Model: "anthropic/claude-3.5-sonnet", Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "What's the capital of France?"), }, }) if err != nil { log.Fatal(err) }
for chunk := range stream.Chunks() { for _, c := range chunk.Choices { fmt.Print(c.Delta.Content) } } if err := stream.Err(); err != nil { log.Fatal(err) } fmt.Println()}Example: Ollama for local dev
For local development you typically don't want a real API key in code paths under test. Point at Ollama, pass a placeholder key, and your code is unchanged from the production OpenAI path:
package main
import ( "context" "fmt" "log" "os"
"github.com/elloloop/llmrouter" "github.com/elloloop/llmrouter/providers/openai")
func main() { // Pick the upstream from env. Default to OpenAI; override with // OLLAMA_HOST=http://localhost:11434/v1 when developing offline. baseURL := "https://api.openai.com/v1" apiKey := os.Getenv("OPENAI_API_KEY") model := "gpt-4o-mini"
if host := os.Getenv("OLLAMA_HOST"); host != "" { baseURL = host apiKey = "ollama" // non-empty placeholder model = "llama3.2" }
p, err := openai.New( llmrouter.WithAPIKey(apiKey), llmrouter.WithBaseURL(baseURL), ) if err != nil { log.Fatal(err) }
stream, err := p.CompletionStream(context.Background(), llmrouter.ChatRequest{ Model: model, Messages: []llmrouter.Message{ llmrouter.TextMessage("user", "One-line summary of CRDTs?"), }, }) if err != nil { log.Fatal(err) } for chunk := range stream.Chunks() { for _, c := range chunk.Choices { fmt.Print(c.Delta.Content) } } if err := stream.Err(); err != nil { log.Fatal(err) } fmt.Println()}Provider-agnostic factory
For code that swaps providers based on configuration, a tiny
factory keeps the call sites clean. Both providers satisfy the
llmrouter.Provider interface:
package router
import ( "fmt" "os"
"github.com/elloloop/llmrouter" "github.com/elloloop/llmrouter/providers/anthropic" "github.com/elloloop/llmrouter/providers/openai")
// Build returns a Provider keyed by a short label. Add cases as you// adopt more endpoints; the call site never branches on provider.func Build(label string) (llmrouter.Provider, error) { switch label { case "openai": return openai.New(llmrouter.WithAPIKey(os.Getenv("OPENAI_API_KEY")))
case "anthropic": return anthropic.New(llmrouter.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY")))
case "openrouter": return openai.New( llmrouter.WithAPIKey(os.Getenv("OPENROUTER_API_KEY")), llmrouter.WithBaseURL("https://openrouter.ai/api/v1"), )
case "groq": return openai.New( llmrouter.WithAPIKey(os.Getenv("GROQ_API_KEY")), llmrouter.WithBaseURL("https://api.groq.com/openai/v1"), )
case "ollama": return openai.New( llmrouter.WithAPIKey("ollama"), llmrouter.WithBaseURL("http://localhost:11434/v1"), )
default: return nil, fmt.Errorf("unknown provider: %q", label) }}
Call sites pass llmrouter.Provider around and use only
its two methods (Name and
CompletionStream). Swapping the upstream is a config
change, not a code change.
See also
- OpenAI provider — the implementation that powers every endpoint on this page.
- Anthropic provider — the translating sibling.
- Adding a new provider — for endpoints that aren't OpenAI-compatible.
- API reference — every exported type.