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
-
.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).
- .context_window_for(entry) ⇒ Object
- .extract_entries(body) ⇒ Object
- .list_key_for(body) ⇒ Object
- .looks_like_model?(body) ⇒ Boolean
- .lookup(entry, keys) ⇒ Object
-
.model_entries(body) ⇒ Object
Extracts the raw model-entry list from a parsed catalog response body.
-
.model_family_for(name) ⇒ Object
Infers the model family from a model name.
-
.model_id_for(entry) ⇒ Object
Resolves the routable, unique-per-deployment identity for a raw catalog entry.
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
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
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 |