Module: Legion::Extensions::Llm::AzureFoundry::ModelCatalogParser

Defined in:
lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb

Overview

Parses the live model catalog returned by the Azure AI Foundry inference surface's discovery endpoint. The SSOT v3 discovery runner (Runners::Discovery#fetch_raw_models) parses the wire response through this module — the single catalog parse path.

Endpoint per surface (Provider#models_url / runner catalog_path):

model_inference -> GET models/info?api-version=<api_version>
openai_v1       -> GET models  (OpenAI-compatible shape)

The model_inference envelope is parsed defensively: a list under data / models / value / deployments (or a bare array) yields that list; a single model object (no list key, but carrying a model identity) yields a one-element catalog. A body that is neither an array, a recognizable list, nor a recognizable model object raises — a silent empty catalog is exactly how the old config-only discovery hid from every consumer. No captured fixture of the model_inference envelope exists in the monorepo, so live confirmation of the wire shape is still required (see PR).

Constant Summary collapse

CATALOG_LIST_KEYS =
%i[data models value models_list deployments].freeze
MODEL_ID_KEYS =

Deployment-unique identity keys, most-specific first. An Azure Foundry deployment carries a unique id / deployment_name; the base-model keys (model_name / model) are deliberately EXCLUDED — multiple deployments of one base model share them, so resolving the identity from them collapses two DISTINCT deployments onto one provider_native_key and the registry Store#build_records raises ValidationError: duplicate provider_native_key. The routable id is the deployment, never the shared base model (bedrock's single :model_id in spirit — one unambiguous key per offering).

%i[id deployment_name name].freeze
BASE_NAME_KEYS =
%i[model_name base_model base_model_name].freeze
CONTEXT_KEYS =
%i[context_window max_input_tokens context_length].freeze
MODEL_SHAPE_KEYS =

A catalog entry is model-shaped when it carries ANY recognized model field — INCLUDING the base-model keys. Envelope recognition (looks_like_model?) is deliberately broader than identity resolution: a model-shaped entry that lacks a unique deployment id is still recognized, then dropped by build_offerings' empty-id filter rather than raising an unrecognized-envelope error.

(MODEL_ID_KEYS + BASE_NAME_KEYS + %i[model]).uniq.freeze

Class Method Summary collapse

Class Method Details

.base_model_name_for(entry) ⇒ Object

The base/underlying model name, when the catalog reports one distinct from the routable id (e.g. Foundry model_name).



82
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 82

def base_model_name_for(entry) = lookup(entry, BASE_NAME_KEYS)&.to_s

.context_window_for(entry) ⇒ Object



84
85
86
87
88
89
90
91
92
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 84

def context_window_for(entry)
  CONTEXT_KEYS.each do |key|
    value = entry[key] || entry[key.to_s]
    next unless value.is_a?(String) || value.is_a?(Numeric)

    return value.to_i
  end
  nil
end

.extract_entries(body) ⇒ Object

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
64
65
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 55

def extract_entries(body)
  return body if body.is_a?(Array)
  raise ArgumentError, "Azure Foundry catalog response must be a Hash or Array, got #{body.class}" unless
    body.is_a?(Hash)

  key = list_key_for(body)
  return body[key] || body[key.to_s] if key
  return [body] if looks_like_model?(body)

  raise ArgumentError, "unrecognized Azure Foundry catalog envelope: #{body.keys.first(5).inspect}"
end

.list_key_for(body) ⇒ Object



67
68
69
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 67

def list_key_for(body)
  CATALOG_LIST_KEYS.find { |k| body.key?(k) || body.key?(k.to_s) }
end

.looks_like_model?(body) ⇒ Boolean

Returns:

  • (Boolean)


71
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 71

def looks_like_model?(body) = MODEL_SHAPE_KEYS.any? { |k| body.key?(k) || body.key?(k.to_s) }

.lookup(entry, keys) ⇒ Object



108
109
110
111
112
113
114
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 108

def lookup(entry, keys)
  keys.each do |key|
    value = entry[key] || entry[key.to_s]
    return value if value.is_a?(String) && !value.strip.empty?
  end
  nil
end

.model_entries(body) ⇒ Object

Extracts the raw model-entry list from a parsed catalog response body. Raises on an unrecognized envelope.



51
52
53
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 51

def model_entries(body)
  Array(extract_entries(body)).grep(Hash)
end

.model_family_for(name) ⇒ Object

Infers the model family from a model name. Name inference is a metadata hint only, never authoritative capability evidence.



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 96

def model_family_for(name)
  id = name.to_s.downcase
  return :openai if id.match?(/gpt|o\d|text-embedding|dall-e/)
  return :mistral if id.include?('mistral')
  return :meta if id.match?(/llama|meta/)
  return :xai if id.match?(/grok|xai/)
  return :anthropic if id.include?('claude')
  return :microsoft if id.match?(/phi|microsoft/)

  nil
end

.model_id_for(entry) ⇒ Object

Resolves the routable, unique-per-deployment identity for a raw catalog entry. On the Foundry surface this is the deployment name (what the API accepts as the model field); on the OpenAI surface it is the model id. Never the shared base model — that collapses two distinct deployments onto one provider_native_key.



78
# File 'lib/legion/extensions/llm/azure_foundry/model_catalog_parser.rb', line 78

def model_id_for(entry) = lookup(entry, MODEL_ID_KEYS)&.to_s