Packages
llm_db
2026.6.3
2026.7.3
2026.7.2
2026.7.1
2026.7.0
2026.6.4
2026.6.3
2026.6.2
2026.6.1
2026.6.0
2026.5.2
2026.5.1
2026.5.0
2026.4.8
2026.4.7
2026.4.6
2026.4.5
2026.4.4
2026.4.3
2026.4.2
2026.4.1
2026.4.0
2026.3.3
2026.3.2
2026.3.1
2026.3.0
2026.2.9
2026.2.8
2026.2.7
2026.2.6
2026.2.5
2026.2.4
2026.2.3
2026.2.2
2026.2.1
2026.2.0
2026.1.5
2026.1.4
2026.1.3
2026.1.2
2026.1.1
2026.1.0
2025.12.4
2025.12.3
2025.12.2
2025.12.1
2025.11.18-preview
2025.11.14-preview
2025.11.7-preview
LLM model metadata catalog with fast, capability-aware lookups.
Current section
Files
Jump to
Current section
Files
lib/llm_db/enrich/azure_wire_protocol.ex
defmodule LLMDB.Enrich.AzureWireProtocol do
@moduledoc """
Wire protocol enrichment using Azure AI Foundry inference task metadata.
Fetches model catalog data from Azure AI Foundry's public API and uses
the `inferenceTasks` field to determine which wire protocol each Azure
model supports (openai_responses, openai_completion, anthropic_messages).
Only enriches models from Azure providers since models.dev already syncs
model data but lacks wire protocol information for Azure's model catalog.
- `pull/1` fetches the Azure catalog and caches locally
- `build_lookup/0` reads cached data and builds a model -> wire_protocol map
- `enrich_models/1` applies wire protocol to Azure models using the lookup
"""
require Logger
@api_url "https://ai.azure.com/api/eastus/ux/v1.0/entities/crossRegion"
@default_cache_dir "priv/llm_db/remote"
@cache_id "azure-foundry"
@page_size 200
@registries [
"azure-openai",
"azureml",
"azureml-meta",
"azureml-mistral",
"azureml-cohere",
"azureml-deepseek",
"azureml-xai",
"azureml-anthropic",
"azureml-moonshotai",
"azureml-nvidia",
"azureml-ai21",
"azureml-alibaba"
]
@chat_tasks ["chat-completion", "completions", "text-generation"]
@lookup_suffixes ["-instruct"]
@azure_providers [:azure, :azure_cognitive_services]
@spec pull(map()) :: :noop | {:ok, String.t()} | {:error, term()}
def pull(opts \\ %{}) do
req_opts = Map.get(opts, :req_opts, [])
cache_dir = get_cache_dir()
cache_path = Path.join(cache_dir, "#{@cache_id}.json")
manifest_path = Path.join(cache_dir, "#{@cache_id}.manifest.json")
case fetch_all_pages(req_opts) do
{:ok, models} ->
bin = Jason.encode!(models, pretty: true)
write_cache(cache_path, manifest_path, bin)
{:ok, cache_path}
{:error, {:http_status, 429}} ->
keep_existing_cache(cache_path, {:http_status, 429})
{:error, reason} ->
{:error, reason}
end
end
@spec build_lookup() :: %{String.t() => atom()}
def build_lookup do
cache_path = Path.join(get_cache_dir(), "#{@cache_id}.json")
case File.read(cache_path) do
{:ok, bin} ->
case Jason.decode(bin) do
{:ok, models} ->
build_lookup_from_models(models)
{:error, err} ->
Logger.warning("Failed to decode Azure Foundry cache: #{inspect(err)}")
%{}
end
{:error, :enoent} ->
Logger.debug("No Azure Foundry cache found, wire protocol lookup will be empty")
%{}
{:error, reason} ->
Logger.warning("Failed to read Azure Foundry cache: #{inspect(reason)}")
%{}
end
end
@spec enrich_models([map()]) :: [map()]
def enrich_models(models) when is_list(models) do
lookup = build_lookup()
if map_size(lookup) == 0 do
models
else
Enum.map(models, &enrich_model(&1, lookup))
end
end
defp enrich_model(%{provider: provider, extra: %{wire_protocol: _}} = model, _lookup)
when provider in @azure_providers,
do: model
defp enrich_model(%{provider: provider} = model, lookup)
when provider in @azure_providers do
set_wire_protocol(model, lookup)
end
defp enrich_model(model, _lookup), do: model
defp set_wire_protocol(%{id: model_id} = model, lookup) do
case resolve_wire_protocol(model_id, lookup) do
nil ->
model
protocol ->
extra = Map.get(model, :extra) || %{}
Map.put(model, :extra, Map.put(extra, :wire_protocol, protocol))
end
end
defp resolve_wire_protocol(model_id, lookup) do
model_id
|> lookup_candidates()
|> Enum.find_value(&Map.get(lookup, &1))
end
@date_suffix ~r/-\d{4}-\d{2}-\d{2}$/
defp strip_date_suffix(model_id) do
Regex.replace(@date_suffix, model_id, "")
end
defp lookup_candidates(model_id) do
canonical_id = strip_date_suffix(model_id)
[model_id, canonical_id]
|> Enum.flat_map(fn candidate ->
[candidate | Enum.map(@lookup_suffixes, &"#{candidate}#{&1}")]
end)
|> Enum.uniq()
end
defp build_lookup_from_models(models) do
Enum.reduce(models, %{}, fn model, acc ->
name = model_name(model)
tasks = get_in(model, ["annotations", "systemCatalogData", "inferenceTasks"]) || []
case derive_wire_protocol(tasks) do
nil -> acc
protocol -> Map.put(acc, name, protocol)
end
end)
end
defp model_name(model) do
model
|> get_in(["annotations", "name"])
|> to_string()
|> String.downcase()
end
defp derive_wire_protocol(tasks) do
cond do
"messages" in tasks -> :anthropic_messages
"responses" in tasks -> :openai_responses
Enum.any?(tasks, &(&1 in @chat_tasks)) -> :openai_completion
true -> nil
end
end
# Pull helpers
defp fetch_all_pages(req_opts, continuation_token \\ nil, acc \\ []) do
body = build_request_body(continuation_token)
headers = [{"user-agent", "AzureAiStudio"}]
merged_headers = headers ++ Keyword.get(req_opts, :headers, [])
merged_opts = Keyword.put(req_opts, :headers, merged_headers)
case Req.post(@api_url, [json: body] ++ merged_opts) do
{:ok, %Req.Response{status: 200, body: resp_body}} ->
ier = Map.get(resp_body, "indexEntitiesResponse", %{})
entities = Map.get(ier, "value", [])
acc = Enum.reverse(entities) ++ acc
case Map.get(ier, "continuationToken") do
nil -> {:ok, Enum.reverse(acc)}
"" -> {:ok, Enum.reverse(acc)}
token -> fetch_all_pages(req_opts, token, acc)
end
{:ok, %Req.Response{status: status}} ->
{:error, {:http_status, status}}
{:error, reason} ->
{:error, reason}
end
end
defp build_request_body(continuation_token) do
resource_ids =
Enum.map(@registries, fn registry ->
%{"resourceId" => registry, "entityContainerType" => "Registry"}
end)
index_request = %{
"filters" => [
%{"field" => "type", "operator" => "eq", "values" => ["models"]},
%{"field" => "kind", "operator" => "eq", "values" => ["Versioned"]},
%{"field" => "labels", "operator" => "eq", "values" => ["latest"]}
],
"pageSize" => @page_size,
"skip" => nil,
"continuationToken" => continuation_token
}
%{
"resourceIds" => resource_ids,
"indexEntitiesRequest" => index_request
}
end
defp get_cache_dir do
Application.get_env(:llm_db, :azure_foundry_cache_dir, @default_cache_dir)
end
defp keep_existing_cache(cache_path, reason) do
if File.exists?(cache_path) do
Logger.warning(
"Azure Foundry pull failed with #{format_http_status(reason)}; keeping existing cache at #{cache_path}"
)
:noop
else
{:error, reason}
end
end
defp format_http_status({:http_status, status}), do: "HTTP #{status}"
defp write_cache(cache_path, manifest_path, content) do
File.mkdir_p!(Path.dirname(cache_path))
File.write!(cache_path, content)
manifest = %{
source_url: @api_url,
sha256: :crypto.hash(:sha256, content) |> Base.encode16(case: :lower),
size_bytes: byte_size(content),
downloaded_at: DateTime.utc_now() |> DateTime.to_iso8601()
}
File.write!(manifest_path, Jason.encode!(manifest, pretty: true))
end
end