Universal LLM provider runtime. One interface for every model. Authentication, routing, streaming, retries, caching — handled.
- Model-agnostic — single interface for 75+ LLM providers
- Zero opinions — consumers control routing, caching, and retry strategies
- Streaming-first — all responses are streamed; blocking is opt-in
See hawk/docs/OTEL-CONVENTIONS.md for the shared OpenTelemetry attribute vocabulary (gen_ai.*, cost.usd, etc.) used across all GrayCodeAI repos.
go test ./... # Run all tests
go test -race ./... # Race detector
go test -coverprofile=c.out ./... # Coverage
go vet ./... # Static analysis
gofumpt -w . # Format
make ci # Full CI suiteprovider.go— Provider interface and registryrouting.go— Model routing and fallback chainsstreaming.go— SSE streaming with backpressureauth.go— API key management and rotationcache.go— Response caching (optional)retry.go— Retry with exponential backoff + Retry-Aftercatalog.go— Model catalog and capability discovery
- Go 1.26+, pure Go, no CGO
- Table-driven tests
- Conventional Commits:
feat:,fix:,docs:,refactor:,test: - No
Co-authored-by:trailers (auto-stripped by githook) gofumptformatting enforced in CI- Credential setup flow documented in
docs/guides/CREDENTIAL-SETUP-FLOW.md
- Provider interface is the boundary — keep it stable
- Streaming tests need careful goroutine management
go.workhere should stay minimal; hawk's owngo.workadds anexternal/eyriereplace so hawk can develop against a local eyrie checkout. Do not add extra localreplacedirectives here without coordinating with hawk's workspace.
- Provider interface:
client.ProviderwithChat(),StreamChat(),Ping(),Name()— implemented per LLM vendor - Client types:
EyrieClient,EyrieMessage,EyrieResponse,EyrieTool,EyrieUsage—Eyrieprefix for public types - Config struct:
EyrieConfigwithProvider,APIKey,BaseURL,Model,MaxRetriesfields - Provider implementations:
AnthropicClient,OpenAIClient,GeminiClient,BedrockClient, etc. — inclient/package - Compatibility configs:
OpenAICompat,GrokCompat,OpenRouterCompat—Compatsuffix for provider quirks - Error type:
EyrieErrorwithProvider,Op,StatusCode,RequestID,Message,Errfields - Stream types:
StreamResult,SSEEvent,StreamEvent— streaming is SSE-based - Retry config:
RetryConfigembedstypes.RetryConfig+ addsRetryOn []intfor HTTP status codes - Version wiring:
client.Versionset viaSetVersion()from root package — avoids circular import
- Provider auto-detection:
DetectProvider()checks env vars in priority order (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) - Client creation:
client.NewEyrieClient(&EyrieConfig{...})orclient.Client(&EyrieConfig{...})— both work - Chat method:
c.Chat(ctx, messages, opts)— non-streaming, returns*EyrieResponse - Stream method:
c.StreamChat(ctx, messages, opts)— returns*StreamResult, caller mustdefer sr.Close() - Auto-continuation:
StreamChatContinue()transparently retries whenstop_reason == max_tokens - Provider fallback:
fallback.goimplements fallback chains across providers - Rate limiting:
ratelimit.goimplements token bucket per provider — prevents hitting API limits - Semantic caching:
semantic_cache.gocaches similar prompts — optional, configurable TTL - Retry with backoff:
retry.go— exponential backoff + jitter, respectsRetry-Afterheader, retries on 429/500/502/503/529 - Error hierarchy:
EyrieErrorhasIsRetriable(),IsAuthError(),IsRateLimited()methods for programmatic handling - SSE parsing:
parseSSEStream()readsbufio.Scannerwith 2MB buffer, emitsSSEEventto channel
- httptest.NewServer for provider tests: mock LLM API responses with realistic JSON
- Provider-specific test files:
anthropic_test.go,openai_test.go,gemini_test.go— each provider tested independently - Env var manipulation:
os.Setenv/os.Unsetenvin tests withdefercleanup — test provider detection - Credentials store:
credentials.MapStore{}for testing —credentials.SetDefaultStore(store)+t.Cleanup(func() { ... }) - Client test structure: create mock server, create client with server URL, call Chat/StreamChat, assert response
- Header assertions: verify
X-Api-Key,Anthropic-Version,User-Agentheaders are sent correctly - Stream tests: parse SSE events from mock server, verify content/tool_call/done event types
- Retry tests: mock server returning 429/500, verify retry count and backoff behavior
- Cache tests: call same prompt twice, verify second call hits cache (no second HTTP request)
- Fuzz tests:
fuzz_test.gofor input parsing robustness
- Safe to refactor:
retry.go,ratelimit.go,cache.go— internal infrastructure, no public API changes - Safe to refactor:
stream.goSSE parsing — internal implementation detail - Safe to refactor:
fallback.go,weighted.go— routing strategies, extend with new strategies - Safe to refactor:
cost_estimator.go,cache_analytics.go— metrics and tracking - Do not touch:
Providerinterface (Chat,StreamChat,Ping,Name) — breaking change for all implementations - Do not touch:
EyrieMessage,EyrieResponse,ChatOptionsstruct field names — serialization contract - Do not touch:
EyrieErrorstruct — used by consumers for error type assertions - Do not touch:
client.EyrieConfig— constructor contract for all consumers - Safe to extend: add new provider implementations, new SSE event types, new cache strategies
- When adding a provider: create
client/<provider>.go, implementProviderinterface, register inprovider_registry.go
| What | Where |
|---|---|
| Provider interface | client/client.go (Provider, EyrieConfig, EyrieMessage, ContentPart) |
| Chat implementation | client/chat.go (Chat(), StreamChat(), StreamChatContinue()) |
| Anthropic provider | client/anthropic.go |
| OpenAI provider | client/openai.go |
| Gemini provider | client/gemini.go |
| Bedrock provider | client/bedrock.go |
| Vertex provider | client/vertex.go |
| Azure provider | client/azure.go |
| Provider registry | client/provider_registry.go |
| Provider compatibility | client/compat.go (OpenAICompat, GrokCompat, etc.) |
| SSE streaming | client/stream.go (parseSSEStream(), SSEEvent) |
| Retry logic | client/retry.go (RetryConfig, backoffDelay(), shouldRetry()) |
| Rate limiting | client/ratelimit.go, client/adaptive_ratelimit.go |
| Caching | client/cache.go, client/semantic_cache.go, client/cache_analytics.go |
| Fallback chains | client/fallback.go |
| Auto-continuation | client/continuation.go |
| Error types | client/errors.go (EyrieError, IsRetriable(), IsAuthError()) |
| Error constants | errors/errors.go (API error messages, prompt-too-long parsing) |
| Model catalog | catalog/ (pricing, context windows, capabilities per provider) |
| Credentials | credentials/ (key storage, env detection, scrubbing) — HasSecret is silent on miss (boolean predicate); LookupSecret logs Debug on ErrNotFound and Warn on real backend errors |
| Mock provider | client/mock.go |
| Main test file | client/client_test.go (httptest servers, provider detection) |
| Linter config | .golangci.yml (govet, ineffassign, misspell — minimal) |