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.
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).
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.
Replace the global factory once at main() startup. Every AI op uses it
unless overridden.
library.SetDefaultAIClientFactory(myFactory)
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)
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",
})
A sketch: ref is the Vault path for the credential pair; the factory fetches
both keys, builds the SDK clients, caches them.
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.
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.
How EmbeddingClientFactory, WithEmbeddingCredentials, and
ResolveEmbeddingClient compose with RetrieveOp to plug in
any embedding provider.
Deployment posture: how factories interact with timeouts, web-service request scopes, and the multi-tenant patterns the framework supports.