lex-llm-openai 0.6.3 → 0.6.6
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 +4 -4
- data/.rubocop.yml +4 -1
- data/CHANGELOG.md +35 -0
- data/Gemfile +7 -2
- data/RULES.md +97 -0
- data/lex-llm-openai.gemspec +1 -1
- data/lib/legion/extensions/llm/openai/actors/discovery.rb +23 -0
- data/lib/legion/extensions/llm/openai/helpers/callable.rb +164 -0
- data/lib/legion/extensions/llm/openai/instance_discovery.rb +17 -4
- data/lib/legion/extensions/llm/openai/provider.rb +28 -201
- data/lib/legion/extensions/llm/openai/runners/discovery.rb +270 -0
- data/lib/legion/extensions/llm/openai/runners/fleet_worker.rb +1 -3
- data/lib/legion/extensions/llm/openai/translator.rb +13 -18
- data/lib/legion/extensions/llm/openai/version.rb +1 -1
- data/lib/legion/extensions/llm/openai.rb +1 -1
- metadata +7 -11
- data/lib/legion/extensions/llm/openai/actors/discovery_refresh.rb +0 -352
- data/lib/legion/extensions/llm/openai/discovery_drafts.rb +0 -113
- data/lib/legion/extensions/llm/openai/discovery_evidence_builders.rb +0 -140
- data/lib/legion/extensions/llm/openai/discovery_health_display.rb +0 -92
- data/lib/legion/extensions/llm/openai/discovery_identity.rb +0 -69
- data/lib/legion/extensions/llm/openai/discovery_probing.rb +0 -135
- data/lib/legion/extensions/llm/openai/discovery_transport.rb +0 -37
- data/lib/legion/extensions/llm/openai/openai_callable.rb +0 -135
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac62eb3f6a56c5f0b41acde2dd6a2e5e4d6f2bfe7453752514d9c8c3749dde2b
|
|
4
|
+
data.tar.gz: dd0d4d972f11c95d278256cdd0a739dea0de4f411f182efc657c2f4123d2c727
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 326a853929579295e753e33689aec41d7b202b811d972ebbc463e312d8e82cfcdaaa60b8b6bfcadfc9262cbd1b5c0b3871c0c9e79159c507217302ba37ac235b
|
|
7
|
+
data.tar.gz: b14ec226c2d960d06cf9af29d79fafe655102627d516d342d6b31fe64a8c8e4939e059a8eb1d12bab56e4f7d81dc1361d4f23bd1e3138421c5aebcbc827e80be
|
data/.rubocop.yml
CHANGED
|
@@ -14,8 +14,11 @@ Metrics/BlockLength:
|
|
|
14
14
|
- spec/**/*
|
|
15
15
|
Metrics/ClassLength:
|
|
16
16
|
Max: 350
|
|
17
|
+
# Aligned with the sibling lex-llm-* providers (vllm/gemini/mlx): the rubocop
|
|
18
|
+
# default ModuleLength is too strict for a discovery runner carrying the full
|
|
19
|
+
# evidence-building surface. Length is still bounded (Max 350).
|
|
17
20
|
Metrics/ModuleLength:
|
|
18
|
-
Max:
|
|
21
|
+
Max: 350
|
|
19
22
|
Metrics/MethodLength:
|
|
20
23
|
Enabled: false
|
|
21
24
|
RSpec/ExampleLength:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.6.6] - 2026-08-25
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Thinking contract aligned to lex-llm 0.8.x** — removed dead `params.max_thinking_tokens` reference in `apply_format_params` (NoMethodError on every request reaching this path since Params dropped that member). `apply_thinking` now gates on `thinking.enabled?` and uses `thinking.resolved_effort` so a budget-only client still yields `reasoning_effort` on the wire (never silently dropped). The `respond_to?(:effort)` Hash shim is removed — `thinking` is always a `Canonical::Thinking::Config` or nil.
|
|
7
|
+
|
|
3
8
|
## [Unreleased]
|
|
4
9
|
|
|
10
|
+
### Changed
|
|
11
|
+
- **lex-llm 0.8.0 conformance** — the gem now runs against the 0.8.0 contract cut; lex-llm floor bumped to `>= 0.8.0`. Version remains 0.6.5 per the 0.8.0-conformant release law.
|
|
12
|
+
- **Legacy offering production ripped** — the provider's legacy `discover_offerings` override and its `Routing::ModelOffering` production path (offering attrs, capability-policy resolution, instance-id derivation, filter helpers) are deleted; `discover_offerings` is now the base 0.8.0 read path serving activated offerings from the `Inventory::Registry` snapshot (07 C5). The `DiscoveryRefresh` writer (`OfferingDraft` + `WeightReconciler` + `Publisher`) remains the sole publication path.
|
|
13
|
+
- **Legacy coordinator wiring ripped** — the `ScopedRefresher::LegacyCoordinatorAdapter` bridge is removed from the discovery actor (the file is gone from lex-llm 0.8.0); the `Publisher` is constructed without a compatibility adapter.
|
|
14
|
+
- **Callable aligned to the 0.8.0 fleet dispatch shape** — `chat`/`stream_chat` take positional canonical `messages` (the 0.8.0 funnel signature) and hand the Selection-derived model to the wire UNCHANGED (B4): the `Model::Info` wrap for chat/stream_chat is deleted because the 0.8.0 renderer consumes `model:` verbatim (a wrapped value would serialize a Data object into the request payload). `moderate` now takes `input:` as a keyword, matching the base `Provider#moderate(input:, model:)` contract (the previous positional call raised `ArgumentError` against 0.8.0). Fleet wire params are folded into `Canonical::Params` at the dispatch boundary (`Canonical::Params.from_hash`; `temperature` is a params member, 05 O4 — it is no longer a named completion kwarg on any provider path); base named kwargs (`tools`/`schema`/`thinking`/`tool_prefs`/`headers`, and `dimensions`/`headers` for embed) pass through as-is. No tools/thinking wire rehydration — logged pre-existing gap, out of this wave.
|
|
15
|
+
- **Renderer/parser on the 0.8.0 canonical contract** — `render_payload` renders from canonical values (`params:` replaces the deleted `temperature:` kwarg); the o-series/gpt-5 temperature clamp and `-search` suppression (previously reachable through the 0.7.x funnel hook) are preserved at the render path as `normalize_openai_temperature`. The local render-seam message check is deleted — canonical input is enforced centrally in the base `complete` funnel (08 F2). `Translator#parse_usage` translates the OpenAI wire spellings (`prompt_tokens`/`completion_tokens` and the Responses API `input_tokens`/`output_tokens`) to canonical keys at the edge (O03a: canonical types accept canonical keys only).
|
|
16
|
+
- **RULES.md installed** at the repo root (byte-for-byte, the 0.8.0 architecture law mirror).
|
|
17
|
+
|
|
18
|
+
### Removed
|
|
19
|
+
- `Provider#enforce_render_messages!` and the legacy `Llm::Message` acceptance at the render seam (the type is deleted in 0.8.0; the Chat-facade shape is gone).
|
|
20
|
+
- The capability-policy read-path spec (it pinned the deleted `Routing::ModelOffering` surface; the writer-path capability evidence is pinned by the discovery and conformance specs).
|
|
21
|
+
|
|
22
|
+
## [0.6.5] - 2026-08-19
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- **Canonical dispatch boundary enforced** — `OpenaiCallable#chat`, `#stream_chat`, and `#count_tokens` now call `Provider#enforce_canonical_messages!` before delegating, and the provider's render seam accepts only `Canonical::Message` (pipeline dispatch) or provider-native `Legion::Extensions::Llm::Message` (Chat facade); anything else raises a loud `ArgumentError` at the boundary instead of failing opaquely downstream and being misclassified as a provider error. The 2026-08-19 incident: SSOT v3 local dispatch passed executor Hash messages straight to the provider callable, bypassing the canonical contract (25/25 failed openai dispatches). N x N law: client translator = client <-> Canonical, executor = Canonical throughout, provider translator = Canonical <-> provider wire — anything half-translated between Canonical and a legacy shape is the defect class this boundary kills.
|
|
26
|
+
- **lex-llm floor bumped to >= 0.7.7** — for `Provider#enforce_canonical_messages!` and the canonical-only `count_tokens`.
|
|
27
|
+
- **Local-tree lex-llm path dependency added to the test group** so the adjacent checkout resolves against unreleased 0.7.7 during development.
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- Dispatch-boundary regression guards: plain-Hash input raises `ArgumentError` at both the fleet callable and the provider render seam; provider-native `Message` input renders to the unchanged OpenAI wire. Client request formats and the provider wire format are unchanged.
|
|
31
|
+
|
|
32
|
+
## [0.6.4] - 2026-08-19
|
|
33
|
+
|
|
5
34
|
### Fixed
|
|
35
|
+
- **Write-time lane weights** — every discovered offering now carries the validated four-axis `weight_inputs` and `base_weight` pair from live settings. The existing periodic actor cadence atomically compares, publishes, sequences, and caches catalog or weight-only changes; unchanged passes stay no-op, initializing instances cache without replacing, and removals cannot race into late activation.
|
|
36
|
+
- **Dormant weight visibility** — ordinary discovery passes emit one info log when a configured provider, instance, model, or offering weight has no published lane; the state clears when the lane appears and logs once again if it later disappears.
|
|
37
|
+
- **D14 callable wire conformance** — an actual `OpenaiCallable#chat` request with a folded leading system message is captured at the provider connection and verified as OpenAI's native leading `role: system` message.
|
|
38
|
+
- **Malformed discovery failures fail loud** — programming/configuration errors from draft construction are no longer swallowed as an empty catalog; only Faraday transport failures and JSON parse failures take the existing empty-discovery recovery path.
|
|
39
|
+
- **lex-llm floor bumped to >= 0.7.6** — requires the shared `WeightSchema`, atomic `WeightReconciler`, weighted offering records, and operation-to-lane taxonomy used by this writer. The `legion-settings >= 1.4.2` floor and lifecycle remain unchanged.
|
|
6
40
|
- **Credential-less configured instances are discovered normally** — removed the synthetic-default suppression and its one-time warning cache. A configured instance, including one named `default`, now follows the ordinary discovery and readiness path without a skip warning.
|
|
41
|
+
- **Complete offering equivalence** — ordinary discovery now compares the complete `OfferingDraft` contract as an order-insensitive, duplicate-sensitive multiset, ignoring only evidence `observed_at` timestamps. Provider-native identity, publication source, tokenizer evidence, and all future draft fields trigger exactly one replacement when they change, while catalog reordering remains a no-op.
|
|
7
42
|
|
|
8
43
|
## [0.6.2] - 2026-08-17
|
|
9
44
|
|
data/Gemfile
CHANGED
|
@@ -2,10 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
source 'https://rubygems.org'
|
|
4
4
|
|
|
5
|
-
gem 'lex-llm'
|
|
6
|
-
|
|
7
5
|
gemspec
|
|
8
6
|
|
|
7
|
+
# Local-tree resolution: use the adjacent lex-llm checkout when present so the
|
|
8
|
+
# canonical-boundary work resolves against the unreleased 0.7.7.
|
|
9
|
+
group :test do
|
|
10
|
+
lex_llm_path = File.expand_path('../lex-llm', __dir__)
|
|
11
|
+
gem 'lex-llm', path: lex_llm_path if Dir.exist?(lex_llm_path)
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
group :development do
|
|
10
15
|
gem 'bundler', '>= 2.0'
|
|
11
16
|
gem 'rake', '>= 13.0'
|
data/RULES.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
RULES.md — Legion LLM Architecture Law
|
|
2
|
+
These rules apply to every task, file, repository, agent, model, session, test, refactor, migration, incident, and release.
|
|
3
|
+
The requested task defines what may change. These rules define how the system ALWAYS works.
|
|
4
|
+
Every rule remains active 100% of the time. If requested work conflicts with these rules, stop and surface the conflict before changing code.
|
|
5
|
+
These are architecture laws. Scope, compatibility, urgency, convenience, tests, existing behavior, and model judgment do not change them.
|
|
6
|
+
1. Canonical is the only internal language.
|
|
7
|
+
Every client translates client wire -> Canonical before shared execution.
|
|
8
|
+
Shared execution carries Canonical through context, tools, routing, direct dispatch, fleet dispatch, and response handling.
|
|
9
|
+
Every provider translates Canonical -> provider wire at the provider boundary, then provider wire -> Canonical before returning to shared execution.
|
|
10
|
+
Every internal boundary validates the Canonical type it is defined to receive and raises immediately when that contract is violated.
|
|
11
|
+
Client Wire -> Client Translator -> Canonical -> Shared Execution -> Canonical -> Provider Translator -> Provider Wire.
|
|
12
|
+
2. Serialization preserves Canonical.
|
|
13
|
+
Transport may serialize Canonical state. The receiving transport boundary ALWAYS rehydrates the exact Canonical type before execution continues.
|
|
14
|
+
Fleet follows Canonical -> serialize -> wire -> deserialize -> rehydrate Canonical -> Canonical.
|
|
15
|
+
Serialization changes encoding only. Ownership, identity, model, operation, capability, selection, and meaning remain exactly the same.
|
|
16
|
+
After rehydration, shared execution continues only with Canonical objects.
|
|
17
|
+
3. Every authoritative fact has exactly one owner.
|
|
18
|
+
The owner creates the fact once. Every downstream layer carries, projects, serializes, rehydrates, verifies, or executes that exact fact.
|
|
19
|
+
A downstream layer receiving missing or contradictory authoritative state raises and returns the defect to the owning layer.
|
|
20
|
+
Authority ALWAYS moves forward by preservation.
|
|
21
|
+
Authority is created once and is never recreated downstream.
|
|
22
|
+
4. Requirements describe the request. Inventory describes reality. Router chooses. Dispatch executes.
|
|
23
|
+
Canonical request construction owns request semantics. RequestRequirements expresses operation, capabilities, modality, context, output, tools, and explicit pins.
|
|
24
|
+
Providers publish exact executable facts into Inventory. Inventory owns canonical instance, offering, lane, capability, context, quota, health, and published weight state.
|
|
25
|
+
Router.next_lane consumes Requirements plus one immutable Inventory snapshot and produces one authoritative Selection.
|
|
26
|
+
Dispatch executes that Selection exactly. Once Selection exists, routing is finished.
|
|
27
|
+
5. Inventory facts are immutable executable facts.
|
|
28
|
+
Providers publish exact instances and complete offering snapshots through the Inventory publication contract.
|
|
29
|
+
Identity, capability evidence, context evidence, quota domains, availability, and write-time weights are consumed from published Inventory state.
|
|
30
|
+
A changed fact becomes authoritative only through the owning publication or reconciliation path and a new Inventory snapshot.
|
|
31
|
+
Routing reads Inventory. Dispatch verifies and executes Inventory-backed Selection.
|
|
32
|
+
6. Identity, capabilities, weights, and context policy retain exact ownership.
|
|
33
|
+
Inventory::Identity owns instance, offering, and lane identity; canonical instance identity is provider family plus the operator/configured instance name; physical endpoint data remains secondary.
|
|
34
|
+
Providers publish capability evidence. Requirements state required capabilities. Candidate evaluation compares the two and determines capability eligibility.
|
|
35
|
+
The weight owner computes lane weight at publication time; Inventory stores it; ranking consumes that stored weight; a stored zero disables the lane.
|
|
36
|
+
Preferred-context binning orders eligible candidates into preference bands and preserves eligibility. Capability, health, binning, and weight ALWAYS retain distinct meanings.
|
|
37
|
+
7. Routing chooses exactly once.
|
|
38
|
+
Router.next_lane is the sole routing authority.
|
|
39
|
+
Candidate evaluation determines eligibility from Requirements and Inventory. Ranking orders eligible candidates from published routing facts.
|
|
40
|
+
Selection freezes the exact provider, instance, offering, lane, model, operation, and routing identity required for execution.
|
|
41
|
+
Every downstream component consumes the Selection it receives.
|
|
42
|
+
Selection is preserved, not reconstructed.
|
|
43
|
+
8. Exact execution stays exact through every boundary.
|
|
44
|
+
Direct dispatch executes the exact Selection-derived binding it receives.
|
|
45
|
+
Fleet dispatch serializes and signs that exact binding; fleet validation verifies it; fleet rehydration restores it; worker resolution verifies it against authoritative Inventory.
|
|
46
|
+
The selected provider, instance, offering, lane, model, and operation remain identical through projection, signing, transport, validation, rehydration, resolution, and callable invocation.
|
|
47
|
+
A mismatch raises before provider execution.
|
|
48
|
+
An exact execution request ALWAYS remains exact execution.
|
|
49
|
+
9. Health and errors preserve one authoritative meaning.
|
|
50
|
+
Inventory owns exact-instance availability. An authoritative instance-unavailable result removes that exact instance; readiness probing owns recovery; successful readiness republish re-admits it.
|
|
51
|
+
Overload, timeout, rate limit, model-not-ready, and transient provider failures remain request-local according to ProviderOutcome semantics.
|
|
52
|
+
The first layer that can authoritatively classify an error performs that classification once. Every downstream layer preserves it.
|
|
53
|
+
Programming errors remain programming errors. Contract violations remain contract violations. Routing exhaustion remains the defined typed Rejection.
|
|
54
|
+
10. Compatibility exists only at explicit edges.
|
|
55
|
+
Supported legacy clients and protocols are translated into the current Canonical and SSOT architecture at explicit compatibility boundaries.
|
|
56
|
+
Shared execution remains Canonical. Routing remains SSOT-driven. Exact execution remains exact.
|
|
57
|
+
Compatibility code adapts an external contract to the current internal architecture.
|
|
58
|
+
The current internal architecture ALWAYS has one representation, one routing authority, one identity system, and one execution truth.
|
|
59
|
+
11. Fix every defect at its owner.
|
|
60
|
+
Trace the incorrect value to the layer that owns it, then fix that owner.
|
|
61
|
+
Fix client wire in the client translator; Canonical shape in Canonical construction; Requirements in Requirements construction; provider facts in publication; identity in Inventory identity; weights in publication/reconciliation.
|
|
62
|
+
Fix eligibility in candidate evaluation; ordering in ranking; choice in Router.next_lane; execution preservation in dispatch; provider wire in the provider translator.
|
|
63
|
+
The layer where a defect becomes visible is evidence. The owning layer is where the correction belongs.
|
|
64
|
+
12. A discovered issue remains in its owning domain.
|
|
65
|
+
Complete the requested task inside its stated scope.
|
|
66
|
+
When investigation exposes a separate defect owned by another architectural domain, record and surface it as separate work unless the requested task is explicitly expanded.
|
|
67
|
+
Routing work consumes existing Canonical Requirements and Inventory facts. Canonical work changes Canonical contracts. Provider work changes publication or translation. Transport work changes transport.
|
|
68
|
+
Nearby code never changes ownership. “While we are here” never changes architecture.
|
|
69
|
+
13. N x N ALWAYS converges through Canonical.
|
|
70
|
+
Equivalent client semantics produce equivalent Canonical state before shared execution. Every provider consumes the same Canonical semantics for the same request.
|
|
71
|
+
When two paths disagree, capture the state at every involved boundary and locate the FIRST point where Canonical meaning diverges.
|
|
72
|
+
Fix that first divergent boundary, then run the exact failing path again.
|
|
73
|
+
Client behavior is proven at client-wire <-> Canonical. Provider behavior is proven at Canonical <-> provider-wire. Shared execution is proven with Canonical throughout.
|
|
74
|
+
14. Debug from captured authoritative state.
|
|
75
|
+
Capture the actual input at the failing boundary before reasoning from symptoms.
|
|
76
|
+
For translation or transport defects, capture Canonical immediately before and after every involved boundary.
|
|
77
|
+
For routing or dispatch defects, capture Requirements, relevant Inventory facts, Selection, execution binding, and ProviderOutcome.
|
|
78
|
+
Compare each captured value to the contract owned by that layer. Find the first divergence. Fix its owner. Re-run the exact path.
|
|
79
|
+
Then inspect sibling implementations for the same defect class.
|
|
80
|
+
15. Tests prove the real boundary and the invariant.
|
|
81
|
+
A boundary test exercises the real boundary it claims to protect.
|
|
82
|
+
Fleet tests exercise real serialization, deserialization, Canonical rehydration, signing, validation, exact resolution, and callable dispatch.
|
|
83
|
+
Provider tests exercise the real callable boundary and provider translator. Routing tests exercise real Requirements, Inventory records, candidate evaluation, ranking, and Selection.
|
|
84
|
+
Regression tests prove the violated invariant, not only the observed symptom.
|
|
85
|
+
A green suite is release evidence only when the tested path traverses the real architecture.
|
|
86
|
+
16. Shared contracts are consumed directly.
|
|
87
|
+
Shared Canonical types own execution representation. Shared Inventory types own inventory state. Shared Routing types own routing state.
|
|
88
|
+
Shared taxonomy owns canonical mappings. Shared ProviderOutcome owns provider-neutral outcomes. Shared fleet protocol owns exact execution claims.
|
|
89
|
+
Every repository consumes these shared owners directly.
|
|
90
|
+
A defect in one shared boundary triggers an audit of every sibling implementation of that boundary. Fix the shared owner centrally whenever the defect belongs to a shared contract.
|
|
91
|
+
17. Architecture is the release gate.
|
|
92
|
+
Every change preserves every rule in this file.
|
|
93
|
+
Tests, compatibility, historical behavior, migration phase, patch urgency, nearby code, task wording, and model judgment are evaluated UNDER these rules.
|
|
94
|
+
A contradiction between existing behavior and these rules is surfaced as an architecture conflict and resolved at the owning boundary before release.
|
|
95
|
+
Limited scope means do less. Limited scope NEVER means fewer rules apply.
|
|
96
|
+
These rules apply 100% of the time.
|
|
97
|
+
These are the law.
|
data/lex-llm-openai.gemspec
CHANGED
|
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
|
|
|
27
27
|
spec.add_dependency 'legion-logging', '>= 1.3.2'
|
|
28
28
|
spec.add_dependency 'legion-settings', '>= 1.4.2'
|
|
29
29
|
spec.add_dependency 'legion-transport', '>= 1.4.14'
|
|
30
|
-
spec.add_dependency 'lex-llm', '>= 0.
|
|
30
|
+
spec.add_dependency 'lex-llm', '>= 0.8.0'
|
|
31
31
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'legion/extensions/llm/discovery/actor'
|
|
4
|
+
|
|
5
|
+
# The base discovery actor only exists inside the daemon (it inherits the
|
|
6
|
+
# LegionIO time-based Every actor). In a standalone load, define nothing.
|
|
7
|
+
return unless defined?(Legion::Extensions::Llm::Discovery::Actor)
|
|
8
|
+
|
|
9
|
+
module Legion
|
|
10
|
+
module Extensions
|
|
11
|
+
module Llm
|
|
12
|
+
module Openai
|
|
13
|
+
module Actor
|
|
14
|
+
# OpenAI discovery actor: an EMPTY subclass of the shared base. The
|
|
15
|
+
# timer, dispatch, and runner-resolution convention are inherited —
|
|
16
|
+
# this class redefines nothing. The OpenAI-specific work lives in
|
|
17
|
+
# Openai::Runners::Discovery, resolved by the base from this namespace.
|
|
18
|
+
class Discovery < Legion::Extensions::Llm::Discovery::Actor; end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'faraday'
|
|
4
|
+
require 'legion/extensions/llm/routing/provider_outcome'
|
|
5
|
+
|
|
6
|
+
module Legion
|
|
7
|
+
module Extensions
|
|
8
|
+
module Llm
|
|
9
|
+
module Openai
|
|
10
|
+
module Helpers
|
|
11
|
+
# Callable wrapper for an OpenAI provider instance. Implements the
|
|
12
|
+
# fleet dispatch operations by delegating to the per-instance
|
|
13
|
+
# Openai::Provider (errors propagate so normalize_dispatch_error can
|
|
14
|
+
# classify them), plus the `disconnect` and `normalize_dispatch_error
|
|
15
|
+
# (error:)` contracts required by Inventory::CallableHandle and
|
|
16
|
+
# Routing::ProviderOutcome.
|
|
17
|
+
#
|
|
18
|
+
# Defined in its own file so the actor's runtime guard (the Every base
|
|
19
|
+
# is absent in a standalone load) does not prevent specs from loading it.
|
|
20
|
+
class Callable
|
|
21
|
+
# Keys the base Provider exposes as named kwargs for the
|
|
22
|
+
# completion operations. Anything else the fleet passes (sampling
|
|
23
|
+
# scalars, `temperature` — a Canonical::Params member, 05 O4) is
|
|
24
|
+
# folded into Canonical::Params at the dispatch boundary.
|
|
25
|
+
COMPLETION_NAMED_KEYS = %i[tools schema thinking tool_prefs headers].freeze
|
|
26
|
+
EMBED_NAMED_KEYS = %i[dimensions headers].freeze
|
|
27
|
+
|
|
28
|
+
def initialize(instance_cfg:, logger:, provider: nil)
|
|
29
|
+
@instance_cfg = instance_cfg
|
|
30
|
+
@logger = logger
|
|
31
|
+
@injected_provider = provider
|
|
32
|
+
@disconnected = false
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def disconnected?
|
|
36
|
+
@disconnected
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def disconnect
|
|
40
|
+
@disconnected = true
|
|
41
|
+
@provider&.disconnect
|
|
42
|
+
@logger.debug { '[openai][callable] disconnected' }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def provider
|
|
46
|
+
@provider ||= @injected_provider || Legion::Extensions::Llm::Openai::Provider.new(@instance_cfg)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# -- Fleet dispatch operations ----------------------------------------
|
|
50
|
+
# The fleet passes `model:` as the offering's raw model id (String).
|
|
51
|
+
# B4: the callable hands it to the wire unchanged — no wrapping, no
|
|
52
|
+
# default, no fallback. The 0.8.0 funnel renders `model:` verbatim
|
|
53
|
+
# into the payload, so a wrapped value would corrupt the request.
|
|
54
|
+
|
|
55
|
+
def chat(messages, model:, **rest)
|
|
56
|
+
# Canonical boundary (N x N law): pipeline dispatch delivers
|
|
57
|
+
# Canonical::Message objects only. Hash/legacy shapes are the
|
|
58
|
+
# bypass class — reject loudly, never coerce.
|
|
59
|
+
provider.enforce_canonical_messages!(messages)
|
|
60
|
+
named, params = split_fleet_kwargs(rest, COMPLETION_NAMED_KEYS)
|
|
61
|
+
provider.chat(messages, model: model, params: canonical_params(params), **named)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def stream_chat(messages, model:, **rest, &)
|
|
65
|
+
provider.enforce_canonical_messages!(messages)
|
|
66
|
+
named, params = split_fleet_kwargs(rest, COMPLETION_NAMED_KEYS)
|
|
67
|
+
provider.stream_chat(messages, model: model, params: canonical_params(params), **named, &)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def embed(text:, model:, **rest)
|
|
71
|
+
named, params = split_fleet_kwargs(rest, EMBED_NAMED_KEYS)
|
|
72
|
+
provider.embed(text: text, model: model, params: params, **named)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def count_tokens(messages:, model:, **rest)
|
|
76
|
+
provider.enforce_canonical_messages!(messages)
|
|
77
|
+
_named, params = split_fleet_kwargs(rest, [])
|
|
78
|
+
provider.count_tokens(messages: messages, model: model, params: params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def image(prompt:, model:, **rest)
|
|
82
|
+
provider.image(prompt: prompt, model: model, **rest)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def moderate(input:, model:, **rest)
|
|
86
|
+
provider.moderate(input: input, model: model, **rest)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def normalize_dispatch_error(error:)
|
|
90
|
+
reason = error.message.to_s[0, 512]
|
|
91
|
+
|
|
92
|
+
kind = case error
|
|
93
|
+
when Faraday::ConnectionFailed
|
|
94
|
+
:connection_failure
|
|
95
|
+
when Faraday::TimeoutError
|
|
96
|
+
:timeout
|
|
97
|
+
when Faraday::ClientError
|
|
98
|
+
classify_client_error(error: error)
|
|
99
|
+
when Faraday::ServerError
|
|
100
|
+
classify_server_error(error: error)
|
|
101
|
+
when Legion::Extensions::Llm::OverloadedError
|
|
102
|
+
:overloaded
|
|
103
|
+
else
|
|
104
|
+
# ServiceUnavailableError and all other error types map to
|
|
105
|
+
# :provider_error. OpenAI does not produce a distinct flat
|
|
106
|
+
# instance-down signal separate from overload; only an
|
|
107
|
+
# authoritative explicit instance_unavailable condition
|
|
108
|
+
# (which OpenAI does not emit via normal dispatch) would
|
|
109
|
+
# map to :instance_unavailable.
|
|
110
|
+
:provider_error
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
Legion::Extensions::Llm::Routing::ProviderOutcome.new(
|
|
114
|
+
kind: kind,
|
|
115
|
+
reason: reason.empty? ? 'unknown dispatch error' : reason
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
# The 0.8.0 completion funnel receives canonical values only
|
|
122
|
+
# (08 F3): the folded wire params become a Canonical::Params at
|
|
123
|
+
# the dispatch boundary — temperature is a params member (05 O4),
|
|
124
|
+
# never a kwarg.
|
|
125
|
+
def canonical_params(params)
|
|
126
|
+
Legion::Extensions::Llm::Canonical::Params.from_hash(params)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Split the fleet's **rest into the base Provider's named kwargs
|
|
130
|
+
# and a payload params hash (any passed :params merged with
|
|
131
|
+
# unknown keys).
|
|
132
|
+
def split_fleet_kwargs(rest, named_keys)
|
|
133
|
+
named = rest.slice(*named_keys)
|
|
134
|
+
extra = rest.reject { |key, _| named.key?(key) }
|
|
135
|
+
params = (extra.delete(:params) || {}).to_h.merge(extra)
|
|
136
|
+
[named, params]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def classify_client_error(error:)
|
|
140
|
+
status = error.respond_to?(:response_status) ? error.response_status : nil
|
|
141
|
+
case status
|
|
142
|
+
when 401 then :authentication
|
|
143
|
+
when 403 then :authorization
|
|
144
|
+
when 404 then :model_missing
|
|
145
|
+
when 429 then :rate_limited
|
|
146
|
+
else :invalid_request
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def classify_server_error(error:)
|
|
151
|
+
# NEVER classify raw 503/529/5xx as instance_unavailable by status alone.
|
|
152
|
+
# OpenAI 503 means overload/maintenance, not a permanent instance death.
|
|
153
|
+
status = error.respond_to?(:response_status) ? error.response_status : nil
|
|
154
|
+
case status
|
|
155
|
+
when 503, 529 then :overloaded
|
|
156
|
+
else :provider_error
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -7,8 +7,9 @@ module Legion
|
|
|
7
7
|
# Candidate collection and normalization for Openai.discover_instances.
|
|
8
8
|
# Extracted from the Openai module to keep it within Metrics limits.
|
|
9
9
|
# The synthetic instances.default entry (placeholder env:// key nested
|
|
10
|
-
# under credentials) can surface here without a top-level api_key;
|
|
11
|
-
#
|
|
10
|
+
# under credentials) can surface here without a top-level api_key;
|
|
11
|
+
# dedup_and_log_candidates skips disabled and credential-less
|
|
12
|
+
# candidates so discover_instances only returns claimable instances.
|
|
12
13
|
module InstanceDiscovery
|
|
13
14
|
extend Legion::Logging::Helper
|
|
14
15
|
|
|
@@ -67,13 +68,25 @@ module Legion
|
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def dedup_and_log_candidates(candidates)
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
deduped = CredentialSources.dedup_credentials(candidates)
|
|
72
|
+
discovered = deduped.reject { |_, config| config[:enabled] == false || unresolved_credential?(config) }
|
|
73
|
+
.transform_values { |cfg| sanitize_instance_config(cfg) }
|
|
72
74
|
instance_names = discovered.keys.sort_by(&:to_s).join(', ')
|
|
73
75
|
log.debug { "Discovered #{discovered.size} OpenAI provider instance candidate(s): #{instance_names}" }
|
|
74
76
|
discovered
|
|
75
77
|
end
|
|
76
78
|
|
|
79
|
+
# enabled: false is a skip, not a credential: a disabled instance is
|
|
80
|
+
# never claimed (the discovery pipeline reads this method as the
|
|
81
|
+
# single claimable source). A nil credential or an unresolved
|
|
82
|
+
# env:// / vault:// placeholder (the synthetic provider_settings
|
|
83
|
+
# default nests env://OPENAI_API_KEY) cannot authenticate and is
|
|
84
|
+
# never claimed.
|
|
85
|
+
def unresolved_credential?(config)
|
|
86
|
+
cred = config[:openai_api_key]
|
|
87
|
+
cred.nil? || cred.to_s.match?(%r{\A(vault|env)://})
|
|
88
|
+
end
|
|
89
|
+
|
|
77
90
|
def settings_instances(config)
|
|
78
91
|
return {} unless config.is_a?(Hash)
|
|
79
92
|
|