Skip to main content

guides/consumer-integration.md

# Consumer Integration Guide

Guide for libraries and applications consuming llm_db for model metadata.

## Overview

llm_db provides model metadata through a simple `model_spec` interface with alias resolution. This guide covers best practices for integrating llm_db into your library or application.

## Model Aliases and Canonical IDs

### Understanding the Alias System

llm_db uses **canonical IDs** with **alias resolution** to handle model naming variations:

- **Canonical ID**: The primary, immutable identifier for a model (typically the dated version)
  - Example: `claude-haiku-4-5-20251001`
- **Aliases**: Alternative names that resolve to the canonical ID
  - Examples: `claude-haiku-4-5`, `claude-haiku-4.5`, `claude-haiku-4-5@latest`

```elixir
# All of these resolve to the same model
LLMDB.model("anthropic:claude-haiku-4-5-20251001")  #=> canonical
LLMDB.model("anthropic:claude-haiku-4-5")            #=> alias → canonical
LLMDB.model("anthropic:claude-haiku-4.5")            #=> alias → canonical
# All return: %LLMDB.Model{id: "claude-haiku-4-5-20251001", ...}
```

### Why Canonical IDs?

1. **Immutable**: Dated versions represent a single, specific model release
2. **Deduplication**: Prevents duplicate metadata/fixtures for the same model
3. **Clarity**: Explicit about which version you're using
4. **Stability**: Won't change when "latest" changes

### Alias Resolution Flow

```
User Request → Alias Resolution → Canonical Model → Metadata
"claude-haiku-4.5" → "claude-haiku-4-5-20251001" → %LLMDB.Model{...}
```

## Configuration: allow/deny Filters

### Critical Rule: Filters Use Canonical IDs Only

**Filters are applied BEFORE alias resolution**, so they match against canonical IDs, not aliases.

```elixir
# ✓ CORRECT - Use canonical IDs in filters
config :llm_db,
  filter: %{
    allow: %{
      anthropic: [
        "claude-haiku-4-5-20251001",      # Canonical ID
        "claude-opus-4-1-20250805",       # Canonical ID
        "claude-sonnet-4-5-20250929"      # Canonical ID
      ]
    },
    deny: %{}
  }

# ✗ INCORRECT - Aliases won't match
config :llm_db,
  filter: %{
    allow: %{
      anthropic: [
        "claude-haiku-4.5",    # This is an alias - won't work!
        "claude-opus-4.1",     # This is an alias - won't work!
        "claude-sonnet-4.5"    # This is an alias - won't work!
      ]
    },
    deny: %{}
  }
# This will eliminate ALL models because aliases don't match filters
```

### Using Glob Patterns

Glob patterns work with canonical IDs:

```elixir
config :llm_db,
  filter: %{
    allow: %{
      anthropic: ["claude-haiku-*"],    # Matches claude-haiku-4-5-20251001
      openai: ["gpt-4o-*"]               # Matches gpt-4o-2024-11-20, etc.
    },
    deny: %{
      anthropic: ["*-thinking"]          # Deny thinking modes
    }
  }
```

### Finding Canonical IDs

Use llm_db to discover canonical IDs for configuration:

```elixir
# List all models for a provider
anthropic_models = LLMDB.models(:anthropic)
Enum.each(anthropic_models, fn m ->
  IO.puts("#{m.id} → aliases: #{inspect(m.aliases)}")
end)

# Output:
# claude-haiku-4-5-20251001 → aliases: ["claude-haiku-4-5", "claude-haiku-4.5"]
# claude-opus-4-1-20250805 → aliases: ["claude-opus-4-1", "claude-opus-4.1"]
```

## Fixture Management

### Best Practice: One Fixture Set per Canonical ID

Since aliases resolve to canonical IDs, you only need ONE fixture set per unique model.

**Example Directory Structure:**

```
fixtures/
├── anthropic/
│   ├── claude-haiku-4-5-20251001/    # ✓ One canonical fixture
│   │   ├── basic.json
│   │   ├── tools.json
│   │   └── streaming.json
│   ├── claude-opus-4-1-20250805/      # ✓ One canonical fixture
│   │   └── basic.json
│   └── claude-sonnet-4-5-20250929/    # ✓ One canonical fixture
│       └── basic.json
```

**Anti-pattern (duplicates):**

```
fixtures/
├── anthropic/
│   ├── claude-haiku-4-5-20251001/    # ✗ Duplicate
│   ├── claude-haiku-4-5/             # ✗ Duplicate (alias)
│   └── claude-haiku-4.5/             # ✗ Duplicate (alias)
```

### Fixture Lookup Strategy

When looking up fixtures, resolve to canonical ID first:

```elixir
defmodule MyApp.Fixtures do
  def load_fixture(model_spec) do
    # Resolve to canonical model
    {:ok, model} = LLMDB.model(model_spec)
    
    # Use canonical ID for fixture path
    provider = model.provider
    canonical_id = model.id
    
    fixture_path = "test/fixtures/#{provider}/#{canonical_id}/basic.json"
    File.read!(fixture_path)
  end
end

# All of these load the same fixture
MyApp.Fixtures.load_fixture("anthropic:claude-haiku-4-5-20251001")
MyApp.Fixtures.load_fixture("anthropic:claude-haiku-4-5")
MyApp.Fixtures.load_fixture("anthropic:claude-haiku-4.5")
```

## Runtime Model Resolution

### Accept Any Variant, Use Canonical Internally

Allow users to specify models using aliases, but resolve to canonical IDs internally:

```elixir
defmodule MyApp.LLMClient do
  def chat(model_spec, messages) do
    # Resolve alias to canonical model
    {:ok, model} = LLMDB.model(model_spec)
    
    # Use canonical ID internally
    provider = model.provider
    canonical_id = model.id
    
    # Make API call with metadata
    request_body = %{
      model: model.provider_model_id || canonical_id,  # Use provider-specific ID
      messages: messages,
      max_tokens: model.limits.output
    }
    
    make_request(provider, request_body)
  end
end

# All variants work
MyApp.LLMClient.chat("anthropic:claude-haiku-4.5", messages)  # Alias
MyApp.LLMClient.chat("anthropic:claude-haiku-4-5-20251001", messages)  # Canonical
```

## Migration: Consolidating Duplicates

If you have existing code/fixtures using aliases, migrate to canonical IDs:

### Step 1: Identify Duplicates

```elixir
# Find models with aliases
anthropic_models = LLMDB.models(:anthropic)
duplicates = Enum.filter(anthropic_models, fn m -> length(m.aliases) > 0 end)

Enum.each(duplicates, fn m ->
  IO.puts("Canonical: #{m.id}")
  IO.puts("  Aliases: #{inspect(m.aliases)}")
end)
```

### Step 2: Update Configuration

Replace aliases with canonical IDs in `config/*.exs`:

```elixir
# Before
config :my_app,
  allowed_models: [
    "anthropic:claude-haiku-4.5",     # Alias
    "anthropic:claude-opus-4.1"       # Alias
  ]

# After
config :my_app,
  allowed_models: [
    "anthropic:claude-haiku-4-5-20251001",    # Canonical
    "anthropic:claude-opus-4-1-20250805"      # Canonical
  ]
```

### Step 3: Consolidate Fixtures

Rename fixture directories to canonical IDs and remove duplicates:

```bash
# Rename to canonical
mv fixtures/anthropic/claude_haiku_4_5 fixtures/anthropic/claude-haiku-4-5-20251001

# Remove duplicates
rm -rf fixtures/anthropic/claude-haiku-4.5
rm -rf fixtures/anthropic/claude_haiku_4.5_20251001
```

### Step 4: Update Tests

Update test references to use canonical IDs:

```elixir
# Before
test "claude haiku generates text" do
  response = MyApp.chat("anthropic:claude-haiku-4.5", "Hello")
  assert response.text
end

# After (optional - aliases still work at runtime)
test "claude haiku generates text" do
  response = MyApp.chat("anthropic:claude-haiku-4-5-20251001", "Hello")
  assert response.text
end

# Or keep using aliases - both work!
test "claude haiku generates text" do
  # This still works - llm_db resolves the alias
  response = MyApp.chat("anthropic:claude-haiku-4.5", "Hello")
  assert response.text
end
```

## Common Patterns

### Allow User Preferences, Resolve Internally

```elixir
defmodule MyApp.Config do
  def get_preferred_model do
    # Users configure with any variant
    model_spec = Application.get_env(:my_app, :default_model, "anthropic:claude-haiku-4.5")
    
    # Resolve to canonical for internal use
    case LLMDB.model(model_spec) do
      {:ok, model} -> {:ok, {model.provider, model.id}}
      error -> error
    end
  end
end
```

### Validate Model Availability

```elixir
defmodule MyApp.Setup do
  def validate_config! do
    configured_models = Application.get_env(:my_app, :allowed_models, [])
    
    Enum.each(configured_models, fn model_spec ->
      case LLMDB.model(model_spec) do
        {:ok, model} ->
          unless LLMDB.allowed?(model) do
            raise "Model #{model_spec} is filtered out by llm_db configuration"
          end
          IO.puts("✓ #{model_spec} → #{model.provider}:#{model.id}")
          
        {:error, :not_found} ->
          raise "Model #{model_spec} not found in llm_db catalog"
      end
    end)
  end
end
```

## Recommendations Summary

1. **Filters**: Always use canonical IDs in allow/deny patterns
2. **Fixtures**: One fixture set per canonical model ID
3. **Configuration**: Use canonical IDs in application config
4. **Runtime**: Accept user input with aliases, resolve to canonical internally
5. **Documentation**: Document canonical IDs in user-facing docs, mention alias support
6. **Migration**: Consolidate duplicate fixtures to canonical IDs
7. **Validation**: Check both model existence and filter status at startup

## See Also

- [Using the Data](using-the-data.md) - Alias resolution examples
- [Runtime Filters](runtime-filters.md) - Filter configuration details
- [Schema System](schema-system.md) - Model alias field documentation