# `LLMDB.Spec`
[🔗](https://github.com/agentjido/llmdb/blob/main/lib/llm_db/spec.ex#L1)

Canonical "provider:model" spec parsing and resolution.

This module provides functions to parse and resolve model specifications in various formats,
including "provider:model" strings, "model@provider" strings (filename-safe), tuples,
and bare model IDs with provider scope.

## String Formats

Two string formats are supported:

- `"provider:model"` - Traditional colon separator (default)
- `"model@provider"` - Email-like format, filesystem-safe for filenames

Both formats parse to the same internal representation and can be used interchangeably.
The @ format is recommended when model specs are used in filenames, CI artifact names,
or other filesystem contexts.

## Amazon Bedrock Inference Profiles

For Amazon Bedrock models, inference profile IDs with region prefixes (us., eu., ap., apac., ca.,
au., jp., us-gov., global.) are supported. A regional catalog entry supplies its own metadata
and pricing. If no regional entry exists, lookup can use an alias or the base model, while
preserving the region prefix in the returned model ID. For example:

    iex> LLMDB.Spec.resolve("amazon_bedrock:us.anthropic.claude-opus-4-1-20250805-v1:0")
    {:ok, {:amazon_bedrock, "us.anthropic.claude-opus-4-1-20250805-v1:0", %LLMDB.Model{}}}

When the catalog has only the base entry "anthropic.claude-opus-4-1-20250805-v1:0", the lookup
uses its metadata, but the returned model ID retains the "us." prefix for API routing purposes.

# `build_spec`

```elixir
@spec build_spec(String.t() | {atom(), String.t()}, keyword()) :: String.t()
```

Builds a model specification string from various inputs.

Accepts strings (in any supported format) or tuples and outputs a string
in the desired format.

## Parameters

- `input` - Model spec as string or tuple
- `opts` - Keyword list with optional `:format` for output format

## Examples

    iex> LLMDB.Spec.build_spec("openai:gpt-4", format: :filename_safe)
    "gpt-4@openai"

    iex> LLMDB.Spec.build_spec({:openai, "gpt-4"}, format: :model_at_provider)
    "gpt-4@openai"

# `format_spec`

```elixir
@spec format_spec({atom(), String.t()}, atom() | nil) :: String.t()
```

Formats a model specification as a string.

Converts a {provider, model_id} tuple to string format. The output format can be
controlled via the `format` parameter or falls back to the application config
`:llm_db, :model_spec_format` (default: `:provider_colon_model`).

## Parameters

- `spec` - {provider_atom, model_id} tuple
- `format` - Optional format override (atom)

## Supported Formats

- `:provider_colon_model` - "provider:model" (default)
- `:model_at_provider` - "model@provider" (filename-safe)
- `:filename_safe` - alias for `:model_at_provider`

## Examples

    iex> LLMDB.Spec.format_spec({:openai, "gpt-4"})
    "openai:gpt-4"

    iex> LLMDB.Spec.format_spec({:openai, "gpt-4"}, :model_at_provider)
    "gpt-4@openai"

    iex> LLMDB.Spec.format_spec({:openai, "gpt-4o-mini"}, :filename_safe)
    "gpt-4o-mini@openai"

# `normalize_spec`

```elixir
@spec normalize_spec(String.t() | {atom(), String.t()}) :: {atom(), String.t()}
```

Normalizes a model specification to tuple format.

Accepts either a string (in any supported format) or a tuple and returns
a normalized {provider, model_id} tuple.

## Examples

    iex> LLMDB.Spec.normalize_spec("openai:gpt-4")
    {:openai, "gpt-4"}

    iex> LLMDB.Spec.normalize_spec("gpt-4@openai")
    {:openai, "gpt-4"}

    iex> LLMDB.Spec.normalize_spec({:openai, "gpt-4"})
    {:openai, "gpt-4"}

# `parse_provider`

```elixir
@spec parse_provider(atom() | binary()) ::
  {:ok, atom()} | {:error, :unknown_provider | :bad_provider}
```

Parses and validates a provider identifier.

Accepts atom or binary input, normalizes to atom, and verifies the provider
exists in the current catalog.

## Parameters

- `input` - Provider identifier as atom or binary

## Returns

- `{:ok, atom}` - Normalized provider atom if valid and exists in catalog
- `{:error, :unknown_provider}` - Provider not found in catalog
- `{:error, :bad_provider}` - Invalid provider format

## Examples

    iex> LLMDB.Spec.parse_provider(:openai)
    {:ok, :openai}

    iex> LLMDB.Spec.parse_provider("google-vertex")
    {:ok, :google_vertex}

    iex> LLMDB.Spec.parse_provider("nonexistent")
    {:error, :unknown_provider}

# `parse_spec`

```elixir
@spec parse_spec(String.t() | {atom() | String.t(), String.t()}, keyword()) ::
  {:ok, {atom(), String.t()}}
  | {:error,
     :invalid_format
     | :ambiguous_format
     | :unknown_provider
     | :bad_provider
     | :invalid_chars
     | :empty_segment}
```

Parses a model specification string in either "provider:model" or "model@provider" format.

Automatically detects the format based on separators present. Validates the provider
exists in the catalog and checks for reserved characters in segments.

## Parameters

- `spec` - String in "provider:model" or "model@provider" format, or a
  `{provider, model_id}` tuple whose provider is an atom or string
- `opts` - Keyword list with optional `:format` to explicitly specify format

## Options

- `:format` - Explicitly specify the format as `:colon` or `:at`. Required when both separators present.

## Returns

- `{:ok, {provider_atom, model_id}}` - Parsed and normalized spec
- `{:error, :invalid_format}` - No valid separator found
- `{:error, :ambiguous_format}` - Both separators present without explicit format
- `{:error, :unknown_provider}` - Provider not found in catalog
- `{:error, :bad_provider}` - Invalid provider format
- `{:error, :invalid_chars}` - Reserved characters in provider or model segments
- `{:error, :empty_segment}` - Provider or model segment is empty

## Examples

    iex> LLMDB.Spec.parse_spec("openai:gpt-4")
    {:ok, {:openai, "gpt-4"}}

    iex> LLMDB.Spec.parse_spec("gpt-4@openai")
    {:ok, {:openai, "gpt-4"}}

    iex> LLMDB.Spec.parse_spec("google-vertex:gemini-pro")
    {:ok, {:google_vertex, "gemini-pro"}}

    iex> LLMDB.Spec.parse_spec("provider:model@ambiguous", format: :colon)
    {:ok, {:provider, "model@ambiguous"}}

    iex> LLMDB.Spec.parse_spec("gpt-4")
    {:error, :invalid_format}

# `parse_spec!`

```elixir
@spec parse_spec!(String.t() | {atom(), String.t()}, keyword()) ::
  {atom(), String.t()}
```

Parses a model specification string, raising on error.

Same as `parse_spec/2` but raises `ArgumentError` instead of returning error tuple.

## Examples

    iex> LLMDB.Spec.parse_spec!("openai:gpt-4")
    {:openai, "gpt-4"}

    iex> LLMDB.Spec.parse_spec!("gpt-4@openai")
    {:openai, "gpt-4"}

# `resolve`

```elixir
@spec resolve(String.t() | {atom() | String.t(), String.t()}, keyword()) ::
  {:ok, {atom(), String.t(), LLMDB.Model.t()}} | {:error, term()}
```

Resolves a model specification to a canonical model record.

Accepts multiple input formats:
- "provider:model" string
- "model@provider" string
- {provider, model_id} tuple with an atom or string provider
- Bare "model" string with opts[:scope] = provider_atom

Handles alias resolution and validates the model exists in the catalog.

## Parameters

- `input` - Model specification in one of the supported formats
- `opts` - Keyword list with optional `:scope` for bare model resolution

## Returns

- `{:ok, {provider, canonical_id, Model.t()}}` - Resolved model
- `{:error, :not_found}` - Model doesn't exist
- `{:error, :ambiguous}` - Bare model ID exists under multiple providers without scope
- `{:error, :invalid_format}` - Malformed input
- `{:error, term}` - Other parsing errors

## Examples

    iex> LLMDB.Spec.resolve("openai:gpt-4")
    {:ok, {:openai, "gpt-4", %LLMDB.Model{}}}

    iex> LLMDB.Spec.resolve({:openai, "gpt-4"})
    {:ok, {:openai, "gpt-4", %LLMDB.Model{}}}

    iex> LLMDB.Spec.resolve("gpt-4@openai")
    {:ok, {:openai, "gpt-4", %LLMDB.Model{}}}

    iex> LLMDB.Spec.resolve("gpt-4", scope: :openai)
    {:ok, {:openai, "gpt-4", %LLMDB.Model{}}}

    iex> LLMDB.Spec.resolve("gpt-4")
    {:error, :ambiguous}

# `strip_prefix`

```elixir
@spec strip_prefix(atom(), String.t()) :: {String.t(), String.t() | nil}
```

Splits a declared provider prefix from a model ID.

Uses the provider's `extra.model_id_prefixes` metadata from the loaded catalog.
Amazon Bedrock retains its compatibility defaults when no override is declared.
Returns `{base_id, prefix}` using the longest matching prefix. If the provider
has no prefix rules or none matches, returns `{model_id, nil}` unchanged.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
