Concepts AI credential routing
Concept

AI credential routing

Sparsi makes it easy to manage AI credentials at scale. Every AI op resolves its provider SDK client through an AIClientFactory, allowing you to source credentials from HashiCorp Vault, AWS Secrets Manager, workload identity, or per-tenant vaults without touching any graph code. The bundled factory reads CLAUDE_API_KEY and GEMINI_API_KEY from the environment.

Interface

Two methods, one factory

One factory implementation serves both providers. The ref parameter is opaque to Sparsi: empty means "default"; non-empty is yours to interpret (tenant id, Vault path, region, environment).

library/ai_factory.go
type AIClientFactory interface {
    Anthropic(ctx context.Context, ref string) (*anthropic.Client, error)
    Gemini(ctx context.Context, ref string) (*genai.Client,    error)
}

The library never sees the API key. Your factory builds the provider SDK client however it wants and hands it back. Cache aggressively at your end — clients are expensive to construct and trivially safe to reuse.

Wiring

Three patterns, three call sites

Process-wide default

Replace the global factory once at main() startup. Every AI op uses it unless overridden.

library.SetDefaultAIClientFactory(myFactory)

Registered by id

Register multiple factories under string ids. Pick one per vertex via client_factory_id in the vertex params.

library.RegisterAIClientFactory("prod", prodFactory)
library.RegisterAIClientFactory("sandbox", sandboxFactory)

Per-vertex ref

The credential_ref param routes inside the chosen factory — typically a tenant id, environment name, or Vault path.

Params(map[string]string{
    "client_factory_id": "prod",
    "credential_ref":    "tenant-acme",
})
Worked example

Vault-backed AIClientFactory

A sketch: ref is the Vault path for the credential pair; the factory fetches both keys, builds the SDK clients, caches them.

vault_factory.go (sketch)
type VaultFactory struct {
    vault *vault.Client
    mu        sync.Mutex
    anthropic map[string]*anthropic.Client
    gemini    map[string]*genai.Client
}

func (f *VaultFactory) Anthropic(ctx context.Context, ref string) (*anthropic.Client, error) {
    f.mu.Lock()
    defer f.mu.Unlock()
    if c, ok := f.anthropic[ref]; ok {
        return c, nil
    }
    secret, err := f.vault.Logical().ReadWithContext(ctx, ref)
    if err != nil {
        return nil, fmt.Errorf("vault: %s: %w", ref, err)
    }
    apiKey := secret.Data["claude_api_key"].(string)
    c := anthropic.NewClient(option.WithAPIKey(apiKey))
    if f.anthropic == nil { f.anthropic = map[string]*anthropic.Client{} }
    f.anthropic[ref] = &c
    return &c, nil
}

// Gemini() follows the same shape.

Security: the per-ref cache has no eviction. Never derive ref from per-request input (tenant id from a request header, user-supplied query parameter) — doing so produces an unbounded cache and a DoS vector. Use ref only for the small, named credential sets the application itself controls (e.g. "prod", "staging", "tenant-acme"), and define that set at deploy time.

Pairs with

The same shape covers embeddings

Retrievers that need embedding lookups (vector-store backends) consume an EmbeddingClientFactory with an analogous interface. Same routing pattern: process default, register-by-id, per-vertex credential_ref.