Skip to content
Zever
Home

AI

Generate text, stream tokens, and create embeddings with Anthropic, OpenAI, Gemini, or Ollama behind one interface.

4 min readEdit on GitHub

ai.AI is one facade (Generate, Stream, Embed) with typed messages, tools, and usage accounting, so the provider is a config value, not an import. Pick anthropic, openai, or gemini by which models you want, and ollama for self-hosted inference. Switching later is a config change.

Unlike most services, ai has no zero-infra default. config.Default() selects anthropic and Validate requires an API key (or a base_url plus key), so tests that touch ai must supply a key or point at a fake endpoint.

Terminal window
AI_ADAPTER=anthropic
AI_API_KEY=sk-...
AI_MODEL=claude-opus-4
AI_TIMEOUT=30s
Terminal window
AI_ADAPTER=openai
AI_API_KEY=sk-...
AI_MODEL=gpt-5
AI_TIMEOUT=30s
AI_BASE_URL=https://proxy.internal/v1

base_url is optional (a proxy or self-hosted gateway). It must be a valid https URL with scheme and host. timeout bounds each request, and a negative value is rejected. See the ai config reference.

main.go
package main
import (
"context"
"fmt"
aianthropic "github.com/zenta-dev/zever/adapters/ai/anthropic"
"github.com/zenta-dev/zever/config"
"github.com/zenta-dev/zever/container"
"github.com/zenta-dev/zever/core/ai"
)
func main() {
aianthropic.Register() // container wires nothing
c := container.New(config.Default())
defer c.Close(context.Background())
backend, err := c.AI() // fails on a missing key, bad base_url, or unknown adapter
if err != nil {
fmt.Println("resolve error:", err)
return
}
out, err := backend.Generate(context.Background(), "", []ai.Message{
{Role: ai.RoleUser, Content: "Say hi in five words."},
}, ai.GenerateOptions{})
if err != nil {
fmt.Println("generate error:", err)
return
}
fmt.Println("text:", out.Content)
fmt.Println("tokens:", out.Usage.PromptTokens+out.Usage.CompletionTokens)
}

Expected output (text varies by model):

text: Hello, glad you are here.
tokens: 24

For streaming, call Stream and range over the returned channel until the producer closes it. The channel is bounded, so a stalled consumer cannot leak a goroutine on an unbounded buffer.

Adapter Package Notes
anthropic adapters/ai/anthropic Default selection. Needs api_key.
openai adapters/ai/openai Needs api_key.
gemini adapters/ai/gemini Needs api_key.
ollama adapters/ai/ollama Self-hosted. base_url bridges to its Addr.

ollama.New takes ollama.Options with an Addr field. You only write ai: config: with adapter: ollama, the container passes base_url through as Addr and carries over model, timeout, allow_insecure, allow_private, and insecure_skip_verify. Non-loopback endpoints must use https. Outbound requests dial only globally routable addresses and never follow redirects; a local Ollama sidecar (loopback Addr) needs allow_private: true, which logs a line at startup. TLS is always verified unless insecure_skip_verify: true, which logs a warning at startup — allow_insecure only permits the http scheme and never disables verification.

Two batteries build on ai without a provider dependency. Neither has a config entry. Resolve c.Agent() and c.RAG() (which build and cache the loop and engine over the shared backend), or call New with the resolved AI() and VectorStore().

core/agent runs a bounded tool-calling loop: generate, dispatch tool calls, append results, repeat until the model answers or the step bound hits. Unknown tools fail closed, and handler errors go back to the model as tool turns so it can self-correct.

agent.go
loop, err := agent.New(backend, agent.Options{
Model: "gpt-5",
Tools: []agent.Tool{{
Name: "lookup_order",
Description: "Look up an order by ID.",
Parameters: map[string]any{"type": "object"},
Handler: func(ctx context.Context, args map[string]any) (string, error) {
return "order 42", nil
},
}},
})

core/rag composes embeddings with a vectorstore: ingest and embed chunks, then retrieve context and generate a grounded answer.

rag.go
engine, err := rag.New(backend, store, rag.Options{Model: "gpt-5"})

tools/zever-mcp is a stdio Model Context Protocol server that exposes the .zen compiler. It implements initialize, tools/list, and tools/call over newline-delimited JSON-RPC 2.0, with seven tools. The first six are read-only; zever_generate writes, gated by dry-run preview plus human elicitation approval:

Tool Purpose
zever_compile Compile .zen sources and return the merged OpenAPI document.
zever_schema Return a JSON overview of modules, entities, services, messages, and enums.
zever_explain Describe a module, entity, message, service, or operation by dotted path.
zever_check Validate .zen sources (parse and resolve, no codegen); returns ok or the file:line:col diagnostics.
zever_routes List the HTTP routes the schema declares as JSON (method, path, Module.Service.RPC target).
zever_doctor Resolve every battery and report OK/FAIL rows.
zever_generate Scaffold via dry-run plan, then apply on elicited approval.

It also serves static reference resources and prompt templates for scaffolding, triage, and diagnostics.

Terminal window
go build -o zever-mcp ./tools/zever-mcp
{ "mcpServers": { "zever": { "command": "/path/to/zever-mcp" } } }
  • Keep keys in file config or env (AI_API_KEY), never in code. Display config through config.RedactedServices. APIKey never appears in logs or error values.
  • Set timeout explicitly per deploy. Zero falls back to the adapter default, which may exceed your handler deadline.
  • Pin model so a provider-side default change does not move you to a model with different pricing.
  • Unsupported features fail closed with ErrNotSupported at the call site instead of silently dropping a tool call or stream flag.
AI() fails with an invalid-options error at startup

The key is missing or base_url failed validation. Check AI_API_KEY, and that AI_BASE_URL (if used) includes the https scheme and a host.

Generate returns ErrNotSupported for a call that worked on another adapter

The provider has no native shape for that feature (tool-call form, streaming mode). Gate the feature per adapter or switch adapters. The facade never downgrades silently.

Token usage looks wrong after switching providers

Usage accounting follows each provider’s response shape. Compare Usage per adapter in a test before relying on totals for billing.