Unified, SDK-free, zero-dependency Go wrapper for chat completions
(with streaming) and embeddings across 18 LLM providers. Switch vendor by
changing the provider id + model; the input and output shapes stay identical.
Stateless — bring your own *http.Client, no vendor SDKs.
import "github.com/thinwrap/llm-go"Requires Go ≥ 1.18. No third-party dependencies — Bedrock's SigV4 is hand-rolled
on crypto/*, SSE streaming on bufio.
chat, err := llm.NewChat(llm.OpenAI, llm.OpenAICompatConfig{APIKey: os.Getenv("OPENAI_API_KEY")})
if err != nil { log.Fatal(err) }
res, err := chat.Complete(ctx, llm.ChatInput{
Model: "gpt-4o-mini",
Messages: []llm.ChatMessage{{Role: "user", Content: "Say hi in one word."}},
})
if err != nil {
var ce *llm.ConnectorError
if errors.As(err, &ce) { log.Printf("%s: %s", ce.ProviderCode, ce.ProviderMessage) }
return
}
fmt.Println(res.Message.Content, res.Usage.TotalTokens)stream, err := chat.Stream(ctx, in)
if err != nil { return }
defer stream.Close()
for {
delta, err := stream.Recv()
if err == io.EOF { break }
if err != nil { return }
fmt.Print(delta.ContentDelta)
}The provider id selects the connector; ChatInput / ChatResult are
identical across all of them.
Chat (18): the 15 first-class OpenAI-compatible providers — OpenAI,
AzureOpenAI, OpenRouter, Groq, Together, Fireworks, DeepSeek, XAI,
Mistral, Perplexity, DeepInfra, Cloudflare, VLLM, Ollama, LMStudio
— all take OpenAICompatConfig; plus 3 natives: Anthropic (AnthropicConfig),
Bedrock (BedrockConfig), Gemini (GeminiConfig).
Embeddings (11): OpenAI, AzureOpenAI, OpenRouter, Together,
Fireworks, Mistral, DeepInfra, Cloudflare, VLLM, Ollama, LMStudio
(the OpenAI-float subset).
emb, _ := llm.NewEmbeddings(llm.OpenAI, llm.OpenAICompatConfig{APIKey: key})
out, _ := emb.Create(ctx, llm.EmbeddingsInput{Model: "text-embedding-3-small", Input: []string{"hello", "world"}})
// out.Embeddings is [][]float64 in input orderPer-provider details (endpoint, auth, quirks, passthrough keys) live in
docs/providers/ — one page per provider.
// Same Complete(ctx, in) call, same ChatResult — id + model change only.
chat, _ := llm.NewChat(llm.Anthropic, llm.AnthropicConfig{APIKey: key})
// then in.Model = "claude-sonnet-4-5", etc.OpenAICompatConfig covers all 15 first-class providers (APIKey, BaseURL,
Headers); BaseURL is required for AzureOpenAI / Cloudflare and any
self-host provider on a non-default host.
Only fields ≥90% of providers support normalizably are first-class on
ChatInput / ChatResult. Everything else rides through Passthrough (request)
/ Raw (response) and is never emulated:
res, _ := chat.Complete(ctx, llm.ChatInput{
Model: "...",
Messages: msgs,
Passthrough: &llm.Passthrough{Body: map[string]any{"logprobs": true}},
})
_ = res.Raw // verbatim vendor body — reasoning CoT, cached-token counts, cost, ...Every failure is a *ConnectorError (errors.As): ProviderCode is one of
rate_limited, auth_failed, provider_unavailable, invalid_request,
context_length_exceeded, content_filtered, unknown. The raw Retry-After
rides in Cause["retryAfter"] (+ parsed Cause["retryAfterSeconds"]) — there is
no top-level retry field.
One of four thinwrap llm libraries sharing a normalized surface and byte-identical
provider ids / error codes: TypeScript (@thinwrap/llm), PHP (thinwrap/llm),
Go (this package), Python (thinwrap-llm).
Report vulnerabilities privately — please do not open a public issue. Preferred: a private security advisory on this repository. Alternatively, email security@thinwrap.dev. Include the affected versions and a minimal reproduction if you have one.
A vulnerability in a provider's own API or service belongs to that vendor rather than to this wrapper — please report those upstream.
MIT © Dmitry Polyanovsky & contributors