lex-llm-azure-foundry 0.2.13 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76e76ee4edefeb764f3d1829a900e1bf60bf9ccc598532feb0d6033680af329f
4
- data.tar.gz: 778667f3720744c2b5ea38e96d52db29d22c4228f4247a55e9976fd2e1b85612
3
+ metadata.gz: e53b1d826dd7b500516b5d81d4d1502f4cc0c3edbe9453fe80a2436343fdece9
4
+ data.tar.gz: 4b9206ecc5905a2325b4a626901ea4e2cdb5166dba0c9b679a91668a584d5fd1
5
5
  SHA512:
6
- metadata.gz: f1facb1bad185f1b7d2429213869f637598802d078ea1bf453c670de3ff5426f56e763d0102e3b15ce78f4afb6bd36788bf7269918416191b0daa483e5ae6ca4
7
- data.tar.gz: aa1a7c88250da3f1d676ef92fbb0f46522d44994e54d49707daad0ec38eb0e940341ea4f9b0661af9d117771b4fe5d3e4156a3c29d21a11b74446dd9fdbd9570
6
+ metadata.gz: 4b8fce48bc00ff40a0d8e04f731ee2773b0f70bf0fa4e78e212065b063b9631a5f1af639f2ed9e11ba73945f8af47e36008f9b4c62782fe82669f7b6e9e91c24
7
+ data.tar.gz: 6d1a10eb83f5d62c833c51ef3ed4499b71f0d942938cca357747fbfc4bf935a5d46be0c3a0e0f052fbe42cdfcd6d9fa32610a63967ff291ff9a6b2cb18cb0774
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.15] - 2026-07-09
4
+
5
+ ### Fixed
6
+ - Offerings now populate `limits[:context_window]` (and `max_output_tokens`), so the router sees real capacity for Azure lanes instead of nil/unbounded. `build_offering` previously never set `limits` — deployment config context sizes landed in `metadata` and were invisible to routing (a request could then mis-route to an Azure lane the router thought had unlimited context). `context_window` is sourced from live catalog when the endpoint reports it, else per-deployment instance config (`context_window`/`max_input_tokens`), else nil (a genuine per-instance gap — never a hardcoded guess). Azure's inference-plane endpoints (`model_inference GET /info`, `openai_v1 GET /models`) do not report per-model context length, mirroring the OpenAI/Bedrock cloud providers. `Model::Info#context_length` (models API) is populated the same way.
7
+
8
+ ## [0.2.14] - 2026-07-03
9
+
10
+ ### Fixed
11
+ - Emit relative request paths from `path_for` (no leading slash). `Connection` builds Faraday with `api_base` as the base URL; on the `openai_v1` surface that base carries the `/openai/v1` path, and a leading-slash path was treated as absolute and dropped it — 404ing live discovery (empty offerings) and chat. Paths are now relative so the base path survives on both surfaces.
12
+ - Resolve `models_url`/`health_url` per surface: `models` on `openai_v1`, `models/info` on `model_inference`. Previously always `info`, which 404s on the `openai_v1` surface.
13
+
3
14
  ## [0.2.13] - 2026-06-20
4
15
 
5
16
  ### Fixed
@@ -15,6 +15,9 @@ module Legion
15
15
  DEFAULT_API_VERSION = '2024-05-01-preview'
16
16
  MODEL_INFERENCE_SURFACE = :model_inference
17
17
  OPENAI_V1_SURFACE = :openai_v1
18
+ # Keys a live model catalog might use to report context length. Azure's own
19
+ # endpoints rarely do (see deployment_limits), so this is a best-effort read.
20
+ CATALOG_CONTEXT_KEYS = %i[context_window max_input_tokens context_length].freeze
18
21
 
19
22
  class << self
20
23
  def slug = 'azure_foundry'
@@ -143,7 +146,7 @@ module Legion
143
146
  def completion_url = path_for('chat/completions')
144
147
  def chat_url = completion_url
145
148
  def stream_url = completion_url
146
- def models_url = path_for('info')
149
+ def models_url = surface == MODEL_INFERENCE_SURFACE ? path_for('info') : path_for('models')
147
150
  def embedding_url(**) = path_for('embeddings')
148
151
  def health_url = models_url
149
152
 
@@ -161,7 +164,8 @@ module Legion
161
164
  model_family: normalize_family(model_family || configured_family || infer_model_family(model_id)),
162
165
  canonical_model_alias: canonical_model_alias || configured_alias,
163
166
  usage_type: usage_type || value_for(deployment, :usage_type) || usage_type_for(model_id),
164
- metadata: metadata.merge(deployment_metadata(deployment))
167
+ metadata: metadata.merge(deployment_metadata(deployment)),
168
+ limits: deployment_limits(deployment)
165
169
  )
166
170
  end
167
171
 
@@ -275,6 +279,7 @@ module Legion
275
279
  provider: :azure_foundry,
276
280
  family: offering.metadata[:model_family],
277
281
  capabilities: capabilities,
282
+ context_length: offering.context_window,
278
283
  modalities_input: modalities[:input],
279
284
  modalities_output: modalities[:output],
280
285
  metadata: offering.to_h
@@ -285,10 +290,14 @@ module Legion
285
290
  config.azure_foundry_api_version || DEFAULT_API_VERSION
286
291
  end
287
292
 
293
+ # Paths MUST be relative (no leading slash). Faraday builds the
294
+ # connection with api_base as the base URL — on the openai_v1 surface
295
+ # that base carries the /openai/v1 path, and a leading-slash path would
296
+ # be treated as absolute and drop it, 404ing discovery and chat.
288
297
  def path_for(path)
289
- prefix = surface == MODEL_INFERENCE_SURFACE ? '/models' : ''
298
+ prefix = surface == MODEL_INFERENCE_SURFACE ? 'models/' : ''
290
299
  suffix = surface == MODEL_INFERENCE_SURFACE ? "?api-version=#{api_version}" : ''
291
- "#{prefix}/#{path}#{suffix}"
300
+ "#{prefix}#{path}#{suffix}"
292
301
  end
293
302
 
294
303
  def bearer_header
@@ -358,7 +367,8 @@ module Legion
358
367
  )
359
368
  end
360
369
 
361
- def build_offering(model:, model_family:, usage_type:, instance_id:, canonical_model_alias:, metadata:) # rubocop:disable Metrics/ParameterLists
370
+ def build_offering(model:, model_family:, usage_type:, instance_id:, canonical_model_alias:, # rubocop:disable Metrics/ParameterLists
371
+ metadata:, limits: {})
362
372
  policy = resolve_capability_policy(model, usage_type)
363
373
  Legion::Extensions::Llm::Routing::ModelOffering.new(
364
374
  provider_family: :azure_foundry,
@@ -369,6 +379,7 @@ module Legion
369
379
  usage_type: usage_type.to_sym,
370
380
  capabilities: policy[:capabilities],
371
381
  capability_sources: policy[:sources],
382
+ limits: limits,
372
383
  metadata: metadata.merge(
373
384
  model_family: model_family,
374
385
  canonical_model_alias: canonical_model_alias,
@@ -380,14 +391,29 @@ module Legion
380
391
  def with_live_metadata(offering)
381
392
  response = connection.get(models_url)
382
393
  metadata = offering.metadata.merge(model_info: response.body)
383
- with_health(offering, ready: true, checked: true, metadata:)
394
+ # Prefer a context_window the live catalog actually reports; fall back to
395
+ # whatever the deployment config already resolved (Azure endpoints usually
396
+ # omit context length, so config remains authoritative).
397
+ limits = offering.to_h[:limits].to_h
398
+ catalog_window = catalog_context_window(response.body)
399
+ limits = limits.merge(context_window: catalog_window) if catalog_window
400
+ with_health(offering, ready: true, checked: true, metadata:, limits:)
384
401
  end
385
402
 
386
- def with_health(offering, ready:, checked:, error: nil, metadata: offering.metadata)
403
+ def catalog_context_window(body)
404
+ return nil unless body.is_a?(Hash)
405
+
406
+ value = CATALOG_CONTEXT_KEYS.filter_map { |key| body[key] || body[key.to_s] }.first
407
+ value&.to_i
408
+ end
409
+
410
+ def with_health(offering, ready:, checked:, error: nil, metadata: offering.metadata, limits: nil) # rubocop:disable Metrics/ParameterLists
387
411
  health = { ready: ready, checked: checked }
388
412
  health = health.merge(error: error.class.name, message: error.message) if error
389
413
 
390
- Legion::Extensions::Llm::Routing::ModelOffering.new(offering.to_h.merge(health:, metadata:))
414
+ data = offering.to_h.merge(health:, metadata:)
415
+ data[:limits] = limits if limits
416
+ Legion::Extensions::Llm::Routing::ModelOffering.new(data)
391
417
  end
392
418
 
393
419
  def filter_offerings(offerings, model_family: nil, usage_type: nil, **)
@@ -398,6 +424,24 @@ module Legion
398
424
  end
399
425
  end
400
426
 
427
+ # Azure's inference-plane endpoints do NOT report per-model context length
428
+ # (model_inference GET /info returns only model_name/model_type/
429
+ # model_provider_name; the openai_v1 GET /models is an OpenAI-style
430
+ # id/created/owned_by list). So context_window is sourced from the
431
+ # per-deployment instance config (keys :context_window / :max_input_tokens);
432
+ # live discovery (with_live_metadata) can override it when a catalog entry
433
+ # actually carries it. Absent both, the window is nil — a per-instance gap,
434
+ # never a hardcoded guess.
435
+ def deployment_limits(deployment)
436
+ return {} unless deployment
437
+
438
+ context_window = value_for(deployment, :context_window) || value_for(deployment, :max_input_tokens)
439
+ {
440
+ context_window: context_window&.to_i,
441
+ max_output_tokens: value_for(deployment, :max_output_tokens)&.to_i
442
+ }.compact
443
+ end
444
+
401
445
  def deployment_metadata(deployment)
402
446
  return {} unless deployment
403
447
 
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Llm
6
6
  module AzureFoundry
7
- VERSION = '0.2.13'
7
+ VERSION = '0.2.15'
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm-azure-foundry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.13
4
+ version: 0.2.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO