Thanks to visit codestin.com
Credit goes to github.com

Skip to content

garyblankenship/wormhole

Repository files navigation

Wormhole

Wormhole portal banner with a Go gopher and AI-themed circuitry

A portal-grade Go SDK for LLM apps that would prefer not to learn six provider dialects before lunch.

OpenAI lives in one dimension. Anthropic insists the couch goes over there. Gemini brought its own adapter. Ollama is running locally in the garage and refuses to put on shoes. Wormhole gives your Go application one control panel for the useful parts: text, streaming, structured output, embeddings, tool calling, image generation, audio, middleware, fallback, batching, and an OpenAI-compatible proxy.

It is not trying to replace every official provider SDK. That way lies a wall of generated clients, beta endpoints, and a whiteboard that just says "why?" in three colors. Wormhole handles the app-facing path. If you need provider-admin resources like files, batches, fine-tuning, vector stores, assistants, or realtime APIs, use the provider SDKs or REST APIs directly.

Go License

Open A Portal

go get github.com/garyblankenship/wormhole@latest
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/garyblankenship/wormhole/pkg/wormhole"
)

func main() {
	ctx := context.Background()

	client := wormhole.New(
		wormhole.WithDefaultProvider("openai"),
		wormhole.WithOpenAI(os.Getenv("OPENAI_API_KEY")),
	)
	defer client.Close()

	resp, err := client.Text().
		Model("gpt-5.2").
		Prompt("Explain wormholes in one sentence.").
		Generate(ctx)
	if err != nil {
		panic(err)
	}

	fmt.Println(resp.Content())
}

For one-off experiments, QuickText opens a temporary portal, sends the prompt, and closes the blast door:

resp, err := wormhole.QuickText("gpt-5.2", "Hello", os.Getenv("OPENAI_API_KEY"))

Dashboard In The Garage

The common stuff is one builder chain away. No provider SDK séance, no "just this one special client" taped to the side of your service.

Workflow API
Text generation client.Text().Model("gpt-5.2").Prompt("...").Generate(ctx)
Streaming client.Text().Model("gpt-5.2").Prompt("...").Stream(ctx)
Stream and collect chunks, fullText, err := builder.StreamAndAccumulate(ctx)
Structured output client.Structured().Model("gpt-5.2").Schema(schema).GenerateAs(ctx, &out)
Embeddings client.Embeddings().Model("text-embedding-3-small").Input("...").Generate(ctx)
Image generation client.Image().Model("gpt-image-1").Prompt("...").Generate(ctx)
Speech to text client.Audio().SpeechToText().Model("whisper-1").Audio(data, "wav").Transcribe(ctx)
Text to speech client.Audio().TextToSpeech().Model("tts-1").Input("...").Voice("alloy").Generate(ctx)
Tool calling wormhole.RegisterTypedTool(client, name, desc, handler)
Agent loop client.Agent().Model("gpt-5.2").Run(ctx, "task")
Model fallback client.Text().Model("gpt-5.2").WithFallback("gpt-5-mini").Generate(ctx)
Batch execution client.Batch().Add(req1).Add(req2).Concurrency(5).Execute(ctx)
OpenAI-compatible endpoint client.Text().BaseURL("http://localhost:11434/v1").Generate(ctx)
Provider capabilities client.ProviderCapabilities("openai").SupportsToolCalling()

Provider Dimensions

Wormhole talks to providers through local HTTP adapters. No official OpenAI, Anthropic, Gemini, or Ollama SDK is bolted into the runtime. The dependency tree does not need a second garage.

Provider Configuration Supported core resources
OpenAI WithOpenAI(key) text, streaming, structured output, embeddings, images, audio, tools
Anthropic WithAnthropic(key) text, streaming, structured output, tools, vision input
Gemini WithGemini(key) text, streaming, structured output, embeddings, tools, vision input
Ollama WithOllama(config) text, streaming, structured output, embeddings, local model helpers
OpenRouter WithOpenAICompatible(...) or QuickOpenRouter() OpenAI-compatible text, streaming, structured output, tools where supported
Groq WithGroq(key) OpenAI-compatible text and streaming
Mistral WithMistral(config) OpenAI-compatible text and streaming
LM Studio WithLMStudio(config) OpenAI-compatible local text and streaming
vLLM WithVLLM(config) OpenAI-compatible local text and streaming
Custom WithCustomProvider(name, factory) whatever your provider implements

What The Portal Does Not Do

Use Wormhole when you want a common application API across providers. Use the official provider SDKs or direct REST calls when you need provider-admin or platform-specific resources:

  • OpenAI Responses, Assistants, Threads, Runs, Files, Vector Stores, Batches, Fine-tuning, Moderation, Realtime, image edits, or audio translation.
  • Anthropic Files API, Message Batches, provider beta resources, Bedrock, Vertex, or AWS platform adapters.
  • Gemini Enterprise or Vertex-specific resources, files/caches beyond the core generation and embedding paths.
  • Full Ollama model administration beyond the helper methods exposed on the concrete Ollama provider.

That boundary is deliberate. A stable app-facing API is useful. Rebuilding every provider's entire space station by hand is how a normal Thursday becomes a three-week expedition into final_final_rewrite_3.

Configuration: Put The Keys In The Right Drawer

client := wormhole.New(
	wormhole.WithDefaultProvider("anthropic"),
	wormhole.WithOpenAI(os.Getenv("OPENAI_API_KEY")),
	wormhole.WithAnthropic(os.Getenv("ANTHROPIC_API_KEY")),
	wormhole.WithGemini(os.Getenv("GEMINI_API_KEY")),
	wormhole.WithTimeout(30*time.Second),
)

Environment-backed helpers are available when you want the client to assemble itself from the usual keys and stop asking where the screwdriver went:

client := wormhole.New(wormhole.WithAllProvidersFromEnv())

Common environment variables:

Variable Used for
OPENAI_API_KEY OpenAI
ANTHROPIC_API_KEY Anthropic
GEMINI_API_KEY or GOOGLE_API_KEY Gemini
OPENROUTER_API_KEY OpenRouter
GROQ_API_KEY Groq
MISTRAL_API_KEY Mistral
OLLAMA_BASE_URL Ollama native API
LMSTUDIO_BASE_URL LM Studio
WORMHOLE_API_KEY Optional proxy bearer token

Never hardcode provider keys in source code. The multiverse already has enough ways to ruin your week; leaked credentials do not need to audition.

Text, Streaming, and Conversations

resp, err := client.Text().
	Model("gpt-5.2").
	SystemPrompt("You are concise.").
	Prompt("Summarize Go interfaces.").
	MaxTokens(200).
	Temperature(0.2).
	Generate(ctx)
stream, err := client.Text().
	Model("gpt-5.2").
	Prompt("Write a short haiku about latency.").
	Stream(ctx)
if err != nil {
	return err
}

for chunk := range stream {
	if chunk.HasError() {
		return chunk.Error
	}
	fmt.Print(chunk.Content())
}
conv := types.NewConversation().
	System("You are a careful code reviewer.").
	User("Review this function.").
	Assistant("Paste the function.").
	User("func add(a, b int) int { return a + b }")

resp, err := client.Text().Conversation(conv).Model("gpt-5.2").Generate(ctx)

Structured Output: Make The Model Use The Measuring Cup

type Verdict struct {
	Status     string  `json:"status"`
	Confidence float64 `json:"confidence"`
	Reason     string  `json:"reason"`
}

var out Verdict
err := client.Structured().
	Model("gpt-5.2").
	Prompt("Classify this bug report as valid or invalid.").
	Schema(wormhole.MustSchemaFromStruct(Verdict{})).
	GenerateAs(ctx, &out)

Structured output uses the best provider-specific path available: JSON mode, tool calling, or schema-backed generation depending on the provider. You ask for a shape; Wormhole handles the provider dialect and tries to keep the glowing liquid in the beaker.

Embeddings: Vectors Without The Ritual Circle

resp, err := client.Embeddings().
	Model("text-embedding-3-small").
	Input("first document", "second document").
	Dimensions(512).
	Generate(ctx)

for _, emb := range resp.Embeddings {
	fmt.Println(emb.Index, len(emb.Embedding))
}

Provider notes:

Provider Notes
OpenAI Supports Dimensions() for compatible embedding models.
Gemini Supports embedding task metadata through ProviderOptions.
Ollama Processes local embedding models through the native Ollama API.
OpenAI-compatible Works when the endpoint implements /embeddings.

Images and Audio: The Portal Has Speakers Now

OpenAI image generation:

img, err := client.Image().
	Using("openai").
	Model("gpt-image-1").
	Prompt("A clean architecture diagram for an LLM gateway").
	Size("1024x1024").
	ResponseFormat("url").
	Generate(ctx)

OpenAI speech to text:

transcript, err := client.Audio().
	Using("openai").
	SpeechToText().
	Model("whisper-1").
	Audio(wavBytes, "wav").
	Language("en").
	Transcribe(ctx)

OpenAI text to speech:

speech, err := client.Audio().
	Using("openai").
	TextToSpeech().
	Model("tts-1").
	Input("Ship small diffs.").
	Voice("alloy").
	ResponseFormat("mp3").
	Generate(ctx)

Type-Safe Tool Calling

Define a Go struct and register a typed handler. Wormhole derives the tool schema, executes tool calls, and feeds results back to the model. No hand-rolled JSON schema séance required. The model asks for a tool, your Go code runs, the result goes back through the portal, and nobody has to cast map[string]any under fluorescent lighting.

type WeatherArgs struct {
	City string `json:"city" tool:"required" desc:"City name"`
	Unit string `json:"unit" tool:"enum=celsius,fahrenheit" desc:"Temperature unit"`
}

type WeatherResult struct {
	Summary string `json:"summary"`
}

err := wormhole.RegisterTypedTool(client, "get_weather", "Get current weather",
	func(ctx context.Context, args WeatherArgs) (WeatherResult, error) {
		return WeatherResult{Summary: "Foggy, 58F"}, nil
	},
)
if err != nil {
	return err
}

resp, err := client.Text().
	Model("gpt-5.2").
	Prompt("What is the weather in San Francisco?").
	WithToolsEnabled().
	Generate(ctx)

For manual tool handling, call WithToolsDisabled() and inspect resp.ToolCalls.

Agent Loop

Agents run multiple tool-use steps until the model reaches a final answer or the step limit is hit. Think less "magic autonomous intern" and more "bounded loop with tools, telemetry, and a stop condition." The important part is the stop condition. The garage has rules now.

result, err := client.Agent().
	Model("gpt-5.2").
	System("You are a research assistant.").
	MaxSteps(10).
	OnStep(func(e wormhole.StepEvent) {
		fmt.Printf("step=%d tools=%d done=%v\n", e.Step, len(e.ToolCalls), e.Done)
	}).
	Run(ctx, "Compare today's provider options for a low-latency chat app.")

Agent-scoped tools are available through AgentAddTool:

builder := client.Agent().Model("gpt-5.2")
err := wormhole.AgentAddTool(builder, "search", "Search local docs",
	func(ctx context.Context, args SearchArgs) (string, error) {
		return searchDocs(args.Query), nil
	},
)

Middleware and Production Controls

Wormhole has provider middleware for retrying, timeouts, metrics, logging, rate-limiting, circuit breaking, caching, and health-aware routing. This is the part where the prototype gets seatbelts, brakes, and a dashboard light that means something.

openAIConfig := types.NewProviderConfig(os.Getenv("OPENAI_API_KEY")).
	WithRetries(2, 200*time.Millisecond).
	WithMaxRetryDelay(5 * time.Second)

client := wormhole.New(
	wormhole.WithDefaultProvider("openai"),
	wormhole.WithOpenAI(os.Getenv("OPENAI_API_KEY"), openAIConfig),
	wormhole.WithProviderMiddleware(
		middleware.NewTypedTimeoutMiddleware(30*time.Second),
	),
)

Adaptive concurrency can be enabled per client. It watches latency and adjusts capacity instead of sleeping for a random second and hoping the universe becomes emotionally available:

client.EnableAdaptiveConcurrency(&wormhole.EnhancedAdaptiveConfig{
	MinCapacity:   2,
	MaxCapacity:   50,
	TargetLatency: 500 * time.Millisecond,
})

Graceful shutdown drains in-flight requests:

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = client.Shutdown(ctx)

Idempotency caches duplicate requests with the same key for the configured TTL:

client := wormhole.New(
	wormhole.WithOpenAI(os.Getenv("OPENAI_API_KEY")),
	wormhole.WithIdempotencyKey("request-123", 5*time.Minute),
)

OpenAI-Compatible Proxy: One Door, Many Dimensions

The wormhole binary can run a local OpenAI-compatible proxy. Point OpenAI-style clients at one address and route models across provider dimensions with prefixes.

go build -o wormhole ./cmd/wormhole

export OPENAI_API_KEY=sk-...
export ANTHROPIC_API_KEY=sk-ant-...
export GEMINI_API_KEY=...
export OLLAMA_BASE_URL=http://localhost:11434

./wormhole serve --addr :8080 --default-provider openai

Model prefixes select a provider:

Request model Provider Sent model
anthropic/claude-sonnet-4-5 Anthropic claude-sonnet-4-5
gemini/gemini-2.5-pro Gemini gemini-2.5-pro
ollama/llama3.2 Ollama llama3.2
gpt-5.2 default provider gpt-5.2

Supported proxy endpoints:

Method Path
POST /v1/chat/completions
POST /v1/embeddings
GET /v1/models
GET /health

Set WORMHOLE_API_KEY to require Authorization: Bearer <token> on proxy requests. Do that before exposing the portal outside localhost unless your incident-review strategy is "pretend the timeline never happened."

Custom Providers

OpenAI-compatible providers only need a name and base URL. Congratulations, you have installed a new dimension:

client := wormhole.New(
	wormhole.WithOpenAICompatible("perplexity", "https://api.perplexity.ai", types.ProviderConfig{
		APIKey: os.Getenv("PERPLEXITY_API_KEY"),
	}),
)

For non-compatible providers, implement types.Provider and register a factory. Yes, you can bring your own weird machine:

client := wormhole.New(
	wormhole.WithCustomProvider("internal", func(config types.ProviderConfig) (types.Provider, error) {
		return NewInternalProvider(config), nil
	}),
	wormhole.WithProviderConfig("internal", types.ProviderConfig{}),
	wormhole.WithDefaultProvider("internal"),
)

Testing: Simulate The Universe First

Use the mock provider to test application logic without network calls. Burning real tokens to unit-test branching logic is not science; it is a billing event.

import wmtest "github.com/garyblankenship/wormhole/pkg/testing"

func TestSummarize(t *testing.T) {
	mock := wmtest.NewMockProvider("openai").
		WithTextResponse(wmtest.TextResponseWith("mock response"))

	client := wormhole.New(
		wormhole.WithCustomProvider("openai", wmtest.MockProviderFactory(mock)),
		wormhole.WithProviderConfig("openai", types.ProviderConfig{}),
		wormhole.WithDefaultProvider("openai"),
	)

	resp, err := client.Text().Model("test-model").Prompt("test").Generate(context.Background())
	require.NoError(t, err)
	require.Equal(t, "mock response", resp.Content())
}

Project checks:

make test-short
make test
go test ./...

Development

git clone https://github.com/garyblankenship/wormhole
cd wormhole
go test -short ./...
go test ./...

Benchmarks:

make bench
go test -bench=. -benchmem ./pkg/wormhole

Security: Do Not Lick The Glowing Cable

  • Read provider credentials from environment variables or a secret manager.
  • Do not log raw request headers or API keys.
  • Use HTTPS for remote provider endpoints.
  • Set context deadlines or client timeouts for production requests.
  • Use WORMHOLE_API_KEY before exposing the proxy outside localhost.
  • Treat logs like hazardous waste: useful, but not where secrets belong.

License

MIT. See LICENSE. If the portal opens, that part is on you.

About

High-performance, multi-provider LLM SDK for Go. Unified access to OpenAI, Anthropic, Gemini, Ollama, OpenRouter with adaptive rate limiting and concurrency control.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors