A provider-agnostic LLM gateway for Go — the "LiteLLM for Go" that the engine uses internally and that stands alone as a separate OSS library.
Give any Go program a single interface for talking to multiple LLM providers, with production features bolted in: router, fallback, cost tracking, retries, prompt caching, capability flags.
Vectorless uses it internally. Its existence as a separate library is deliberate — other Go projects will want the same thing, and having it standalone (a) generates community interest, (b) forces cleaner boundaries, (c) keeps the engine repo focused on retrieval.
llmgate (no vectorless- prefix — it stands alone).
Lives in-tree as pkg/llm/ inside vectorless-engine today. Extracts
to its own repo after the internal -> pkg refactor proves the
boundary.
- Provide a small interface that abstracts over providers: Anthropic, OpenAI, Gemini, Bedrock, Ollama, etc.
- Route requests across models based on policy: "prefer Sonnet, fall back to GPT-4o on 5xx, fall back to Haiku on budget breach."
- Track cost per call with a unified
Usagestruct. - Surface capability flags so callers branch on features, not vendor names.
- Wrap providers in middleware: retries with jitter, rate limits, prompt caching, PII redaction.
- Model inference. It's a client library, not a model runner.
- Streaming UI. It passes streams through; rendering them is the caller's job.
- Prompt templating. No Jinja, no Handlebars. Go strings are fine.
- Agent orchestration. Tool-calling loops, ReAct agents, planner chains — not in scope. That's what langchaingo or framework-level code is for.
- Vector operations. We're vectorless. The gateway does not do embeddings unless a genuine use-case appears later, and even then it would be a separate sub-package.
The tedious part of any LLM gateway is maintaining 15+ HTTP clients for each vendor's quirky API. Rewriting that is pure drudgery.
llmgate depends on github.com/tmc/langchaingo/llms for the
provider-adapter layer. langchaingo already ships clean Model
implementations for OpenAI, Anthropic, Bedrock, Google, Cohere,
Mistral, Ollama, HuggingFace, Cloudflare Workers AI, Ernie,
Llamafile, Maritaca, Watsonx, and a fake model for testing.
What langchaingo does well:
- Small interface:
GenerateContent(ctx, []MessageContent, ...CallOption). - Prompt caching, token counting, reasoning-model support are in separate files at the package root.
- Actively maintained, MIT, Go 1.24+.
What langchaingo does not do (and where llmgate earns its keep):
- No router. Each
Modelis standalone. - No fallback logic.
- No cost tracking. Usage is returned from each provider in its own shape.
- No unified capability discovery. You have to know "model X supports JSON mode" by reading the code.
llmgate composes langchaingo providers behind its own interface and adds those missing pieces as middleware.
- Bifrost (
maximhq/bifrost) — a 7k-line framework, multi-module repo, forces a specific worldview (Accountinterface, fasthttp, sonic, plugin pipelines). An app, not a library. Excellent as a standalone sidecar gateway; wrong shape as a Go import. - go-litellm — a client for the Python LiteLLM proxy. Wrong layer — it requires running the Python gateway.
- litellm-go — a 45-line round-robin weekend project. Too small.
- Rolling our own provider HTTP clients — rewriting the boring 80% with no upside.
The shape that should stay stable for a long time:
// Client is the one interface everything composes over.
type Client interface {
Complete(ctx context.Context, req Request) (*Response, error)
Stream(ctx context.Context, req Request) (<-chan Event, error)
CountTokens(ctx context.Context, text string) (int, error)
Capabilities() Capabilities
}
type Request struct {
Model string
Messages []Message
MaxTokens int
Temperature float64
JSONMode bool
JSONSchema []byte
Tools []Tool // for function calling
}
type Response struct {
Content string
Usage Usage
Model string
FinishReason string
ToolCalls []ToolCall
}
type Usage struct {
InputTokens int
OutputTokens int
CacheReadTokens int
CacheWriteTokens int
CostUSD float64
}
type Capabilities struct {
MaxContext int
SupportsJSONMode bool
SupportsStreaming bool
SupportsToolUse bool
SupportsCaching bool
}Each provider is a Client implementation that wraps the corresponding
langchaingo/llms Model. The adapter translates between llmgate's
request/response types and langchaingo's, plus fills in cost and
capabilities from a static table.
llmgate/
providers/
anthropic/ - wraps llms.anthropic
openai/ - wraps llms.openai
gemini/ - wraps llms.googleai
bedrock/ - wraps llms.bedrock
ollama/ - wraps llms.ollama
mock/ - canned responses for tests
type Router struct {
Primary Client
Fallbacks []Fallback
}
type Fallback struct {
Client Client
TriggerOn func(err error, usage Usage) bool // 5xx, 429, over-budget, etc.
}The router tries Primary; on an error matching any Fallback.TriggerOn
predicate, it retries with that fallback. Preserves the original error
if all fallbacks fail.
A standalone middleware that wraps any Client, observes the Usage
on each response, and emits metrics. Zero dependencies; just accepts a
callback.
func WithCostTracking(c Client, onUsage func(Usage)) ClientThe pricing table is shipped as a Go map keyed by (provider, model),
updated as vendor prices change. Not fetched at runtime — we want
deterministic behaviour and no external dependency for billing
calculations.
Exponential backoff with jitter on 429 and 5xx. Respects
Retry-After headers when present. Cap at MaxRetries, default 3.
Content-addressed result cache keyed by the hash of (model, messages, max_tokens, temperature, json_mode). In-memory LRU by default;
pluggable Redis for multi-replica deploys. Short TTL (minutes) —
LLM outputs are not cache-friendly for long, but a hot-path dashboard
can benefit.
Distinct from provider-native prompt caching (Anthropic's
cache_control), which is a flag on the provider, not something
llmgate implements itself.
A map of (provider, model) -> Capabilities shipped as data. Callers
can ask "does this model support JSON mode?" without knowing the vendor.
Updated as vendors add features.
The engine never constructs providers directly. It takes a
llmgate.Client in its config and calls Complete / CountTokens:
pipeline := ingest.NewPipeline(ingest.Pipeline{
LLM: llmgate.NewRouter(
llmgate.Anthropic(anthConfig),
llmgate.WithFallback(
llmgate.OpenAI(oaConfig),
llmgate.OnStatus(429, 500, 502, 503, 504),
),
),
...
})The engine doesn't know or care which provider actually handled the call.
llmgate/providers/mock returns canned responses based on predicates.
The engine's retrieval tests use this today (see
internal/retrieval/retrieval_test.go) and will continue to after the
extraction.
Integration tests against real providers live behind an
LLMGATE_INTEGRATION_TESTS=1 env flag so CI doesn't spend money by
default.
Apache-2.0. Permissive, explicit patent grant, enterprise-friendly.
- Embeddings sub-package. If llmgate ever gains an
Embedinterface, it should live behind a separate build tag so consumers who never need embeddings don't pull in the dep graph. - Tool use / function calling. Anthropic, OpenAI, and Gemini all
support it, each with a different shape. A unified
Tooltype is straightforward; the hard part is making the response stream work consistently across them. - Observability. Currently the only observability is the
onUsagecallback. A proper OTel instrumentation package should land once the interface settles. - Budget guardrails. Per-request and per-day dollar caps as middleware, so a misconfigured router can't burn $10k overnight.