# `LLMDB.Spec`
[🔗](https://github.com/agentjido/llm_db/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. The region prefix is stripped for catalog lookup but
preserved 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{}}}

The lookup uses "anthropic.claude-opus-4-1-20250805-v1:0" to find 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()},
  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 {provider, model_id} tuple
- `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()},
  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
- {provider, model_id} tuple
- 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", 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}
```

Strips any inference profile prefix from a model ID.

For Amazon Bedrock, splits prefixes like `"us."`, `"eu."`, `"au."` etc. from the model ID
so the base ID can be used for catalog lookup. Returns `{base_id, prefix}` where prefix
is `nil` if no prefix was found.

For other providers, returns `{model_id, nil}` unchanged.

---

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