legionio 1.9.3 → 1.9.23

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: 0c184a34aab52a0fc1efa955381cc6a4fc0044cbdc4b2781169e4d8708d2be92
4
- data.tar.gz: 50d1fd3ba6a2360e1d07fb6a6c028d7a667748ac9ff6b62f85badb5c2726d393
3
+ metadata.gz: e6be612db3e6de32b7c4baf82b6a38aac19a0a0bb53d198f736ae4d2d638829d
4
+ data.tar.gz: bf294cc20dbb5701b7bd4f5fd474dbc27d6eae37d2c084cf7b06ec2cbb99f2dc
5
5
  SHA512:
6
- metadata.gz: 9d708aeeb13ea3107e7ff0002072e460c33d742c1a3883929d45db889e2198d6de9ed1c71bbfcb7b0ab2322a4875f6a4cbdb3026d59ae64ca3d3bb71af529a83
7
- data.tar.gz: e87eda562703def914551720779496733a10387e9355d73547f904785c7907170ca0d8f25c2a794c600fb94458f75bc4d1c8662a6331640e9b2ca2805b5a707d
6
+ metadata.gz: 3968a2056a7f9c5c2bdbaeff4934165c0a0abd47bde56202f91befeac65c3bd5e38e321de7df3056f2fb71ff66016a89fc2b1803288b598da8fbdaedcc628e77
7
+ data.tar.gz: cc371efeb8f0ad2b32bccfe8140a3ebba0310bded8718c5736c9338fbf088f407d27f58a27d05ad2f6d371b5be45d3d5d50dfddbb4a5d4edb9888a44430a5d9d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,146 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.9.23] - 2026-05-07
6
+
7
+ ### Fixed
8
+ - Fixed encrypted subscription handling to accept both string-keyed and symbol-keyed IV headers before decrypting `encrypted/cs` AMQP payloads.
9
+
10
+ ## [1.9.22] - 2026-05-06
11
+
12
+ ### Changed
13
+ - Hot-reloading a `lex-llm-*` provider extension now asks `Legion::LLM::Call::Providers` to rediscover loaded provider modules, keeping LLM provider instances aligned after extension updates.
14
+ - Bumped the packaged `legion-llm` dependency floor to `>= 0.9.1` for LLM-owned provider registration and reload-safe registry rebuilds.
15
+
16
+ ## [1.9.21] - 2026-05-06
17
+
18
+ ### Changed
19
+ - LegionIO now mounts `Legion::LLM::Routes` through the library route selector when `legion-llm` is available, leaving LLM API ownership with `legion-llm` instead of registering partial fallback routes first.
20
+ - LLM provider health API and CLI output now require native `Legion::LLM::Inventory` data and return a clear unavailable response when inventory is not loaded.
21
+ - Bumped packaged dependency floors to `legion-llm >= 0.9.0` and `legion-data >= 1.8.0` for the coordinated LLM route/schema sweep.
22
+
23
+ ### Fixed
24
+ - Lite and local mode startup now write development mode through the public `Legion::Settings.set_prop` API.
25
+
26
+ ### Removed
27
+ - Removed active `lex-llm-gateway` fallback paths from LLM chat, provider health, extension catalog, role filtering, and README documentation.
28
+
29
+ ## [1.9.20] - 2026-05-06
30
+
31
+ ### Fixed
32
+ - Nested LEX extensions now merge default settings into their nested `extensions` path (for example `lex-foo-bar` -> `extensions.foo.bar`) while underscored flat extensions continue to use the flat key (for example `lex-foo_bar` -> `extensions.foo_bar`).
33
+ - Extension load-time settings checks now use the discovered settings path for nested extensions, keeping `enabled`, `min_version`, `workers`, and `remote_invocable` overrides aligned with where defaults are merged.
34
+
35
+ ## [1.9.19] - 2026-05-05
36
+
37
+ ### Added
38
+ - `UnrecoverableMessageError` for messages that should be dead-lettered immediately (e.g., missing IV header on encrypted messages) instead of retried.
39
+ - Subscription actors now extract `message_id` and `correlation_id` from AMQP metadata into the message hash for downstream tracing.
40
+ - Runner builder auto-includes `Helpers::Lex` into runner modules when available, ensuring all runners have LEX metadata helpers.
41
+
42
+ ### Fixed
43
+ - Encrypted messages (`encrypted/cs`) with a missing `iv` header now raise `UnrecoverableMessageError` and are dead-lettered rather than crashing with a nil argument to `Crypt.decrypt`.
44
+
45
+ ## [1.9.18] - 2026-04-29
46
+
47
+ ### Fixed
48
+ - API-submitted LLM tools now build native `Legion::LLM::Types::ToolDefinition` objects instead of attempting to require RubyLLM at runtime.
49
+ - Provider route coverage now locks LegionIO's `/api/llm/providers` compatibility response ahead of later colliding LLM library route registrations.
50
+
51
+ ## [1.9.17] - 2026-04-29
52
+
53
+ ### Fixed
54
+ - LegionIO now requires `legion-llm >= 0.8.47` and only uses a local sibling `legion-llm` checkout when it satisfies that release floor, preventing stale local worktrees from breaking Bundler resolution.
55
+ - Native LLM provider health API responses now preserve model, type, health, and instance fields when inventory offerings are loaded from string-keyed data.
56
+
57
+ ## [1.9.16] - 2026-04-28
58
+
59
+ ### Fixed
60
+ - LegionIO now requires `legion-llm >= 0.8.44` so packaged installs include the unified identity migration for LLM caller metadata and Broker token audit context.
61
+
62
+ ## [1.9.15] - 2026-04-28
63
+
64
+ ### Fixed
65
+ - The static extension catalog now classifies `lex-llm-gateway` as legacy compatibility, and setup pack tests explicitly prevent it from returning to default LLM or agentic installs.
66
+ - README LLM documentation now calls out `lex-llm-gateway` as legacy-only compatibility glue that is not installed by default.
67
+
68
+ ## [1.9.14] - 2026-04-28
69
+
70
+ ### Fixed
71
+ - LegionIO now requires `legion-tty >= 0.5.4` so packaged installs include the Legion-native LLM probe instead of the legacy direct RubyLLM probe.
72
+
73
+ ## [1.9.13] - 2026-04-28
74
+
75
+ ### Fixed
76
+ - LegionIO now requires `legion-llm >= 0.8.43` so packaged installs get the optional RubyLLM compatibility layer and native dispatch fallback defaults.
77
+
78
+ ## [1.9.12] - 2026-04-28
79
+
80
+ ### Fixed
81
+ - LegionIO now requires `legion-llm >= 0.8.42` so packaged installs resolve the validated LLM routing uplift release.
82
+
83
+ ## [1.9.11] - 2026-04-28
84
+
85
+ ### Fixed
86
+ - LLM chat API routing now prefers native `Legion::LLM.chat` even when legacy `lex-llm-gateway` compatibility code is loaded.
87
+
88
+ ## [1.9.10] - 2026-04-28
89
+
90
+ ### Fixed
91
+ - LLM provider health endpoints and CLI health checks now use the native `legion-llm` provider inventory before falling back to legacy `lex-llm-gateway` provider stats.
92
+
93
+ ## [1.9.9] - 2026-04-28
94
+
95
+ ### Fixed
96
+ - Registry governance and security scanning now accept nested `lex-*` extension gem names such as `lex-llm-openai` and `lex-llm-azure-foundry`.
97
+
98
+ ## [1.9.8] - 2026-04-28
99
+
100
+ ### Fixed
101
+ - The `agentic` setup pack now installs the Legion-native `lex-llm-*` provider stack without also installing retired legacy LLM provider gems.
102
+ - Role profiles now treat `lex-llm-*` gems as the active AI extension set and exclude legacy LLM providers from default `core`, `dev`, and `cognitive` profile loading.
103
+ - LegionIO now requires `legion-llm >= 0.8.41` so packaged installs get the router dependency cleanup that removes retired legacy provider runtime dependencies.
104
+
105
+ ## [1.9.7] - 2026-04-28
106
+
107
+ ### Fixed
108
+ - Extension discovery now maps `lex-llm-azure-foundry` to `Legion::Extensions::Llm::AzureFoundry` and `legion/extensions/llm/azure_foundry`.
109
+ - LegionIO now requires `legion-llm >= 0.8.40` so packaged installs include the native provider bridge needed by the Legion-native LLM stack.
110
+ - README LLM provider documentation now describes the `lex-llm-*` provider stack instead of the retired legacy provider list.
111
+
112
+ ## [1.9.6] - 2026-04-28
113
+
114
+ ### Fixed
115
+ - LLM API gateway checks now use the `Legion::Extensions::Llm::Gateway` namespace loaded by Legion extension autoloading.
116
+ - LLM inference and skill invocation routes now call the current `Legion::LLM::Inference` request/executor API instead of the retired pipeline constants.
117
+ - `legionio llm ping` now routes through `Legion::LLM.ask_direct` instead of bypassing Legion routing with a raw RubyLLM call.
118
+ - API client tool construction now degrades cleanly when the RubyLLM tool base is unavailable.
119
+
120
+ ## [1.9.5] - 2026-04-28
121
+
122
+ ### Added
123
+ - Extension catalog, setup packs, and local development wiring now include the Legion-native `lex-llm` provider stack, including Bedrock, Azure Foundry, and Vertex hosted provider extensions.
124
+
125
+ ### Fixed
126
+ - Local development Gemfile wiring now includes guarded `lex-llm-ledger` resolution so the local bundle matches the LLM setup pack.
127
+ - Local development Gemfile wiring now points `lex-llm-gateway` at the workspace extension path actually used by LegionIO checkouts.
128
+ - Default setup packs no longer install legacy `lex-llm-gateway`; the extension catalog now labels it as compatibility glue rather than active LLM routing.
129
+ - `require 'legion/extensions'` now loads its logging dependency directly instead of relying on `require 'legion'` order.
130
+
131
+ ## [1.9.4] - 2026-04-27
132
+
133
+ ### Added
134
+ - Extension boot now runs a dedicated LLM load phase so `lex-llm` loads before any `lex-llm-*` extension gems.
135
+ - `/api/health` now reports `uptime_seconds` and `uptime` for dashboard and monitor consumers. Fixes #168
136
+ - `/api/extensions` now returns a flat loaded-extension summary for dashboard consumers. Fixes #169
137
+
138
+ ### Fixed
139
+ - `legionio doctor` no longer reports extension-loader config keys as missing `lex-*` gems. Fixes #157
140
+ - `/api/metering` now returns dashboard headline totals instead of the routing breakdown shape. Fixes #170
141
+ - Extension autobuild now runs per-extension data migrations when migration files are present, even when an extension does not opt into general data models. Fixes #171
142
+ - `/api/webhooks` now loads its `Legion::Webhooks` runtime dependency before route handlers execute. Fixes #172
143
+ - `/api/tenants` now passes positional response data and uses `json_error` for missing tenants. Fixes #173
144
+
5
145
  ## [1.9.3] - 2026-04-27
6
146
 
7
147
  ### Fixed
data/Gemfile CHANGED
@@ -4,17 +4,56 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ def local_gem_version(path, version_file)
8
+ version_path = File.expand_path(File.join(path, version_file), __dir__)
9
+ return unless File.file?(version_path)
10
+
11
+ version_source = File.read(version_path)
12
+ version_source[/VERSION\s*=\s*['"]([^'"]+)['"]/, 1]
13
+ end
14
+
15
+ def local_gem_satisfies?(path, version_file, requirement)
16
+ version = local_gem_version(path, version_file)
17
+ version && Gem::Requirement.new(requirement).satisfied_by?(Gem::Version.new(version))
18
+ end
19
+
20
+ def local_gem_path(name, default_path, version_file, requirement)
21
+ env_name = "#{name.upcase.tr('-', '_')}_PATH"
22
+ env_path = ENV.fetch(env_name, nil)
23
+ return env_path if env_path && File.exist?(File.expand_path(env_path, __dir__))
24
+
25
+ return unless File.exist?(File.expand_path(default_path, __dir__))
26
+ return unless local_gem_satisfies?(default_path, version_file, requirement)
27
+
28
+ default_path
29
+ end
30
+
31
+ gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
7
32
  gem 'legion-data', path: '../legion-data' if File.exist?(File.expand_path('../legion-data', __dir__))
8
- gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
9
- gem 'legion-llm', path: '../legion-llm' if File.exist?(File.expand_path('../legion-llm', __dir__))
10
33
  gem 'legion-logging', path: '../legion-logging' if File.exist?(File.expand_path('../legion-logging', __dir__))
11
- gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
12
34
  gem 'legion-settings', path: '../legion-settings' if File.exist?(File.expand_path('../legion-settings', __dir__))
35
+ if (legion_tty_path = local_gem_path('legion-tty', '../legion-tty', 'lib/legion/tty/version.rb', '>= 0.5.4'))
36
+ gem 'legion-tty', path: legion_tty_path
37
+ end
13
38
 
14
- gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
15
- gem 'lex-agentic-memory', path: '../extensions-agentic/lex-agentic-memory' if File.exist?(File.expand_path('../extensions-agentic/lex-agentic-memory', __dir__))
16
- gem 'lex-llm-gateway', path: '../extensions-core/lex-llm-gateway' if File.exist?(File.expand_path('../extensions-core/lex-llm-gateway', __dir__))
17
- gem 'lex-microsoft_teams', path: '../extensions/lex-microsoft_teams' if File.exist?(File.expand_path('../extensions/lex-microsoft_teams', __dir__))
39
+ gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
40
+ if (legion_llm_path = local_gem_path('legion-llm', '../legion-llm', 'lib/legion/llm/version.rb', '>= 0.8.47'))
41
+ gem 'legion-llm', path: legion_llm_path
42
+ end
43
+ gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
44
+
45
+ gem 'lex-kerberos'
46
+
47
+ gem 'lex-apollo', path: '../extensions/lex-apollo' if File.exist?(File.expand_path('../extensions/lex-apollo', __dir__))
48
+ gem 'lex-llm', path: '../extensions-ai/lex-llm' if File.exist?(File.expand_path('../extensions-ai/lex-llm', __dir__))
49
+ gem 'lex-llm-ledger', path: '../extensions-ai/lex-llm-ledger' if File.exist?(File.expand_path('../extensions-ai/lex-llm-ledger', __dir__))
50
+
51
+ %w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
52
+ provider_path = "../extensions-ai/lex-llm-#{provider}"
53
+ gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
54
+ end
55
+
56
+ # gem 'lex-microsoft_teams', path: '../extensions/lex-microsoft_teams' if File.exist?(File.expand_path('../extensions/lex-microsoft_teams', __dir__))
18
57
 
19
58
  gem 'pg'
20
59
 
data/README.md CHANGED
@@ -18,7 +18,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
18
18
  [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.4-red.svg)](https://www.ruby-lang.org/)
19
19
  [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
20
20
 
21
- **Ruby >= 3.4** | **v1.8.12** | **Apache-2.0** | [@Esity](https://github.com/Esity)
21
+ **Ruby >= 3.4** | **v1.9.18** | **Apache-2.0** | [@Esity](https://github.com/Esity)
22
22
 
23
23
  ---
24
24
 
@@ -411,9 +411,9 @@ Access Vault secrets inline: `<%= Legion::Crypt.read('pushover/token') %>`
411
411
 
412
412
  Browse: [LegionIO GitHub](https://github.com/LegionIO) | [legionio topic](https://github.com/topics/legionio?l=ruby)
413
413
 
414
- ### Core (16 operational extensions)
414
+ ### Core (14 operational extensions)
415
415
 
416
- `lex-node` `lex-tasker` `lex-conditioner` `lex-transformer` `lex-synapse` `lex-scheduler` `lex-health` `lex-log` `lex-ping` `lex-exec` `lex-lex` `lex-codegen` `lex-metering` `lex-telemetry` `lex-audit` `task_pruner`
416
+ `lex-node` `lex-tasker` `lex-conditioner` `lex-transformer` `lex-scheduler` `lex-health` `lex-log` `lex-ping` `lex-exec` `lex-lex` `lex-codegen` `lex-metering` `lex-telemetry` `lex-task_pruner`
417
417
 
418
418
  ### Agentic (242 cognitive extensions)
419
419
 
@@ -429,11 +429,13 @@ Brain-modeled cognitive architecture. 20 core orchestration extensions plus 222
429
429
 
430
430
  Coordinated by [legion-gaia](https://github.com/LegionIO/legion-gaia), the cognitive coordination layer with tick-cycle scheduling, channel abstraction, and weighted routing across cognitive modules.
431
431
 
432
- ### AI / LLM (7 provider extensions)
432
+ ### AI / LLM
433
433
 
434
- `lex-azure-ai` `lex-bedrock` `lex-claude` `lex-foundry` `lex-gemini` `lex-openai` `lex-xai`
434
+ `legion-llm` `lex-llm` `lex-llm-anthropic` `lex-llm-azure-foundry` `lex-llm-bedrock` `lex-llm-gemini` `lex-llm-ledger` `lex-llm-mlx` `lex-llm-ollama` `lex-llm-openai` `lex-llm-vertex` `lex-llm-vllm`
435
435
 
436
- Powered by [legion-llm](https://github.com/LegionIO/legion-llm) with three-tier routing (local Ollama, fleet GPU servers, cloud APIs), intent-based dispatch, health tracking, and automatic model discovery.
436
+ Powered by [legion-llm](https://github.com/LegionIO/legion-llm) with provider-neutral model offerings, local and fleet routing, hosted cloud providers, health tracking, metering, and automatic model discovery.
437
+
438
+ LLM API routes are mounted from `legion-llm` when available; LegionIO only hosts those route modules and does not provide a provider gateway fallback.
437
439
 
438
440
  ### Service Integrations (8 common + 15 additional)
439
441
 
@@ -465,7 +467,7 @@ Control which extensions load at startup via `settings/legion.json`:
465
467
  | `core` | 14 core operational extensions only |
466
468
  | `cognitive` | core + all agentic extensions |
467
469
  | `service` | core + service + other integrations |
468
- | `dev` | core + AI + essential agentic (~20 extensions) |
470
+ | `dev` | core + native LLM providers + essential agentic (~20 extensions) |
469
471
  | `custom` | only what's listed in `role.extensions` |
470
472
 
471
473
  Faster boot and lower memory footprint for dedicated worker roles.
data/legionio.gemspec CHANGED
@@ -54,7 +54,7 @@ Gem::Specification.new do |spec|
54
54
 
55
55
  spec.add_dependency 'legion-cache', '>= 1.3.22'
56
56
  spec.add_dependency 'legion-crypt', '>= 1.5.1'
57
- spec.add_dependency 'legion-data', '>= 1.6.19'
57
+ spec.add_dependency 'legion-data', '>= 1.8.0'
58
58
  spec.add_dependency 'legion-json', '>= 1.2.1'
59
59
  spec.add_dependency 'legion-logging', '>= 1.5.0'
60
60
  spec.add_dependency 'legion-settings', '>= 1.3.25'
@@ -62,7 +62,7 @@ Gem::Specification.new do |spec|
62
62
 
63
63
  spec.add_dependency 'legion-apollo', '>= 0.4.0'
64
64
  spec.add_dependency 'legion-gaia', '>= 0.9.26'
65
- spec.add_dependency 'legion-llm', '>= 0.5.8'
66
- spec.add_dependency 'legion-tty', '>= 0.4.35'
65
+ spec.add_dependency 'legion-llm', '>= 0.9.1'
66
+ spec.add_dependency 'legion-tty', '>= 0.5.4'
67
67
  spec.add_dependency 'lex-node'
68
68
  end
@@ -5,6 +5,7 @@ module Legion
5
5
  module Routes
6
6
  module Extensions
7
7
  def self.registered(app)
8
+ register_loaded_summary_route(app)
8
9
  register_available_route(app)
9
10
  register_extension_routes(app)
10
11
  register_runner_routes(app)
@@ -12,6 +13,22 @@ module Legion
12
13
  register_invoke_route(app)
13
14
  end
14
15
 
16
+ def self.register_loaded_summary_route(app)
17
+ app.get '/api/extensions' do
18
+ items = Legion::Extensions.loaded_extension_modules.map do |mod|
19
+ version = mod.const_get(:VERSION, false).to_s if mod.const_defined?(:VERSION, false)
20
+ name = if mod.respond_to?(:lex_name)
21
+ mod.lex_name
22
+ else
23
+ mod.name.to_s.split('::').last.to_s.downcase
24
+ end
25
+ { name: name, module: mod.name, version: version }.compact
26
+ end
27
+
28
+ json_response(items)
29
+ end
30
+ end
31
+
15
32
  def self.register_available_route(app)
16
33
  app.get '/api/extension_catalog/available' do
17
34
  entries = Legion::Extensions::Catalog::Available.all
@@ -188,7 +205,7 @@ module Legion
188
205
  started_at: entry[:started_at]&.iso8601 }
189
206
  end
190
207
 
191
- private :register_available_route, :register_extension_routes,
208
+ private :register_loaded_summary_route, :register_available_route, :register_extension_routes,
192
209
  :register_runner_routes, :register_function_routes, :register_invoke_route
193
210
  end
194
211
  end
@@ -1,10 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
- require 'open3'
5
- require 'resolv'
6
- require 'ipaddr'
7
- require 'uri'
8
4
 
9
5
  module Legion
10
6
  class API < Sinatra::Base
@@ -28,90 +24,84 @@ module Legion
28
24
  Legion::Cache.connected?
29
25
  end
30
26
 
31
- define_method(:gateway_available?) do
32
- defined?(Legion::Extensions::LLM::Gateway::Runners::Inference)
27
+ define_method(:native_provider_stats_available?) do
28
+ defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:providers)
33
29
  end
34
30
 
35
- define_method(:build_client_tool_class) do |tname, tdesc, tschema|
36
- klass = Class.new(RubyLLM::Tool) do
37
- description tdesc
38
- define_method(:name) { tname }
39
- tool_ref = tname
40
- define_method(:execute) do |**kwargs|
41
- case tool_ref
42
- when 'sh'
43
- cmd = kwargs[:command] || kwargs[:cmd] || kwargs.values.first.to_s
44
- output, status = ::Open3.capture2e(cmd, chdir: Dir.pwd)
45
- "exit=#{status.exitstatus}\n#{output}"
46
- when 'file_read'
47
- path = kwargs[:path] || kwargs[:file_path] || kwargs.values.first.to_s
48
- ::File.exist?(path) ? ::File.read(path, encoding: 'utf-8') : "File not found: #{path}"
49
- when 'file_write'
50
- path = kwargs[:path] || kwargs[:file_path]
51
- content = kwargs[:content] || kwargs[:contents]
52
- ::File.write(path, content)
53
- "Written #{content.to_s.bytesize} bytes to #{path}"
54
- when 'file_edit'
55
- path = kwargs[:path] || kwargs[:file_path]
56
- old_text = kwargs[:old_text] || kwargs[:search]
57
- new_text = kwargs[:new_text] || kwargs[:replace]
58
- content = ::File.read(path, encoding: 'utf-8')
59
- content.sub!(old_text, new_text)
60
- ::File.write(path, content)
61
- "Edited #{path}"
62
- when 'list_directory'
63
- path = kwargs[:path] || kwargs[:dir] || Dir.pwd
64
- Dir.entries(path).reject { |e| e.start_with?('.') }.sort.join("\n")
65
- when 'grep'
66
- pattern = kwargs[:pattern] || kwargs[:query] || kwargs.values.first.to_s
67
- path = kwargs[:path] || Dir.pwd
68
- output, = ::Open3.capture2e('grep', '-rn', '--include=*.rb', pattern, path)
69
- output.lines.first(50).join
70
- when 'glob'
71
- pattern = kwargs[:pattern] || kwargs.values.first.to_s
72
- Dir.glob(pattern).first(100).join("\n")
73
- when 'web_fetch'
74
- url = kwargs[:url] || kwargs.values.first.to_s
75
- raw_length = (kwargs[:maxLength] || kwargs[:max_length])&.to_i
76
- max_length = raw_length&.positive? ? raw_length : nil
77
- parsed = begin
78
- URI.parse(url)
79
- rescue StandardError
80
- nil
81
- end
82
- raise 'Invalid or non-HTTP URL' unless parsed.is_a?(URI::HTTP)
83
-
84
- addr = begin
85
- ::Resolv.getaddress(parsed.host)
86
- rescue StandardError
87
- nil
88
- end
89
- if addr
90
- ip = ::IPAddr.new(addr)
91
- raise 'SSRF: private/loopback targets are not permitted' if
92
- ip.loopback? || ip.private? || ip.link_local?
93
- end
94
- require 'legion/cli/chat/web_fetch'
95
- content = Legion::CLI::Chat::WebFetch.fetch(url)
96
- max_length ? content[0, max_length] : content
97
- when 'web_search'
98
- query = kwargs[:query] || kwargs.values.first.to_s
99
- raw_results = (kwargs[:max_results] || kwargs[:maxResults]).to_i
100
- max_results = raw_results.positive? ? [raw_results, 50].min : 5
101
- require 'legion/cli/chat/web_search'
102
- results = Legion::CLI::Chat::WebSearch.search(query, max_results: max_results,
103
- auto_fetch: false)
104
- results[:results].map { |r| "### #{r[:title]}\n#{r[:url]}\n#{r[:snippet]}" }.join("\n\n")
105
- else
106
- "Tool #{tool_ref} is not executable server-side. Use a legion_ prefixed tool instead."
107
- end
108
- rescue StandardError => e
109
- Legion::Logging.log_exception(e, payload_summary: "client tool #{tool_ref} failed", component_type: :api)
110
- "Tool error: #{e.message}"
111
- end
31
+ define_method(:require_llm_chat!) do
32
+ return if defined?(Legion::LLM) && Legion::LLM.respond_to?(:chat)
33
+
34
+ halt 503, json_error('llm_chat_unavailable',
35
+ 'Legion::LLM.chat is not available',
36
+ status_code: 503)
37
+ end
38
+
39
+ define_method(:require_provider_inventory!) do
40
+ return if native_provider_stats_available?
41
+
42
+ halt 503, json_error('providers_unavailable',
43
+ 'LLM provider inventory is not loaded',
44
+ status_code: 503)
45
+ end
46
+
47
+ define_method(:provider_health_report) do
48
+ groups = Legion::LLM::Inventory.providers
49
+ return [] unless groups.respond_to?(:map)
50
+
51
+ groups.map do |provider, offerings|
52
+ provider_offerings = Array(offerings)
53
+ health = provider_offerings.map { |offering| offering_value(offering, :health) }
54
+ .find { |entry| entry.is_a?(Hash) } || {}
55
+ circuit = health[:circuit_state] || health['circuit_state'] || 'unknown'
56
+ {
57
+ provider: provider.to_s,
58
+ circuit: circuit,
59
+ adjustment: health[:adjustment] || health['adjustment'] || 0,
60
+ healthy: circuit.to_s != 'open',
61
+ offerings: provider_offerings.size,
62
+ models: provider_offerings.map { |offering| offering_value(offering, :model) }.compact.uniq,
63
+ types: provider_offerings.map { |offering| offering_value(offering, :type) }.compact.uniq,
64
+ instances: provider_offerings.map do |offering|
65
+ offering_value(offering, :provider_instance) || offering_value(offering, :instance_id)
66
+ end.compact.uniq
67
+ }
112
68
  end
113
- klass.params(tschema) if tschema.is_a?(Hash) && tschema[:properties]
114
- klass
69
+ end
70
+
71
+ define_method(:provider_circuit_summary) do
72
+ report = provider_health_report
73
+ circuits = report.map { |entry| entry[:circuit].to_s }
74
+ {
75
+ total: report.size,
76
+ closed: circuits.count('closed'),
77
+ open: circuits.count('open'),
78
+ half_open: circuits.count('half_open')
79
+ }
80
+ end
81
+
82
+ define_method(:provider_detail) do |provider|
83
+ provider_name = provider.to_s
84
+ entry = provider_health_report.find { |candidate| candidate[:provider] == provider_name }
85
+ halt 404, json_error('provider_not_found', "Provider '#{provider_name}' not found", status_code: 404) unless entry
86
+
87
+ entry
88
+ end
89
+
90
+ define_method(:offering_value) do |offering, key|
91
+ next unless offering.respond_to?(:[])
92
+
93
+ offering[key] || offering[key.to_s]
94
+ end
95
+
96
+ define_method(:build_client_tool_class) do |tname, tdesc, tschema|
97
+ require 'legion/llm/types/tool_definition' unless defined?(Legion::LLM::Types::ToolDefinition)
98
+
99
+ Legion::LLM::Types::ToolDefinition.build(
100
+ name: tname,
101
+ description: tdesc,
102
+ parameters: tschema || {},
103
+ source: { type: :client, executable: true }
104
+ )
115
105
  rescue StandardError => e
116
106
  Legion::Logging.log_exception(e, payload_summary: "build_client_tool_class failed for #{tname}", component_type: :api)
117
107
  nil
@@ -164,55 +154,14 @@ module Legion
164
154
  end
165
155
  end
166
156
 
157
+ require_llm_chat!
158
+
167
159
  request_id = body[:request_id] || SecureRandom.uuid
168
160
  model = body[:model]
169
161
  provider = body[:provider]
170
162
 
171
- # Route through full Legion pipeline when gateway is available
172
- if gateway_available?
173
- ingress_result = Legion::Ingress.run(
174
- payload: { message: message, model: model, provider: provider,
175
- request_id: request_id },
176
- runner_class: 'Legion::Extensions::LLM::Gateway::Runners::Inference',
177
- function: 'chat',
178
- source: 'api'
179
- )
180
-
181
- unless ingress_result[:success]
182
- Legion::Logging.error "[api/llm/chat] ingress failed: #{ingress_result}"
183
- return json_response({ error: ingress_result[:error] || ingress_result[:status] },
184
- status_code: 502)
185
- end
186
-
187
- result = ingress_result[:result]
188
-
189
- if result.nil?
190
- Legion::Logging.warn "[api/llm/chat] runner returned nil (status=#{ingress_result[:status]})"
191
- return json_response({ error: { code: 'empty_result',
192
- message: 'Gateway runner returned no result' } },
193
- status_code: 502)
194
- end
195
-
196
- response_content = if result.respond_to?(:content)
197
- result.content
198
- elsif result.is_a?(Hash) && result[:error]
199
- return json_response({ error: result[:error] }, status_code: 502)
200
- elsif result.is_a?(Hash)
201
- result[:response] || result[:content] || result.to_s
202
- else
203
- result.to_s
204
- end
205
-
206
- meta = { routed_via: 'gateway' }
207
- meta[:model] = result.model.to_s if result.respond_to?(:model)
208
- meta[:tokens_in] = result.input_tokens if result.respond_to?(:input_tokens)
209
- meta[:tokens_out] = result.output_tokens if result.respond_to?(:output_tokens)
210
-
211
- return json_response({ response: response_content, meta: meta }, status_code: 201)
212
- end
213
-
214
163
  # Fallback: direct LLM call (no metering, no task tracking)
215
- if cache_available? && env['HTTP_X_LEGION_SYNC'] != 'true'
164
+ if cache_available? && env['HTTP_X_LEGION_SYNC'] != 'true' && Legion::LLM.respond_to?(:chat_direct)
216
165
  llm = Legion::LLM
217
166
  rc = Legion::LLM::ResponseCache
218
167
  rc.init_request(request_id)
@@ -307,8 +256,8 @@ module Legion
307
256
  streaming = body[:stream] == true && env['HTTP_ACCEPT']&.include?('text/event-stream')
308
257
 
309
258
  # Executor handles all registry tool injection — API only passes client-defined tools
310
- require 'legion/llm/pipeline/request' unless defined?(Legion::LLM::Pipeline::Request)
311
- require 'legion/llm/pipeline/executor' unless defined?(Legion::LLM::Pipeline::Executor)
259
+ require 'legion/llm/inference' unless defined?(Legion::LLM::Inference::Request) &&
260
+ defined?(Legion::LLM::Inference::Executor)
312
261
 
313
262
  principal = defined?(Legion::Identity::Request) && env['legion.principal']
314
263
  caller_ctx = if principal
@@ -318,7 +267,7 @@ module Legion
318
267
  end
319
268
 
320
269
  caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
321
- req = Legion::LLM::Pipeline::Request.build(
270
+ req = Legion::LLM::Inference::Request.build(
322
271
  messages: messages,
323
272
  system: body[:system],
324
273
  routing: { provider: provider, model: model },
@@ -329,7 +278,7 @@ module Legion
329
278
  stream: streaming,
330
279
  cache: { strategy: :default, cacheable: true }
331
280
  )
332
- executor = Legion::LLM::Pipeline::Executor.new(req)
281
+ executor = Legion::LLM::Inference::Executor.new(req)
333
282
 
334
283
  if streaming
335
284
  content_type 'text/event-stream'
@@ -476,26 +425,19 @@ module Legion
476
425
  def self.register_providers(app)
477
426
  app.get '/api/llm/providers' do
478
427
  require_llm!
479
- unless gateway_available? && defined?(Legion::Extensions::LLM::Gateway::Runners::ProviderStats)
480
- halt 503, json_error('gateway_unavailable', 'LLM gateway is not loaded', status_code: 503)
481
- end
428
+ require_provider_inventory!
482
429
 
483
- stats = Legion::Extensions::LLM::Gateway::Runners::ProviderStats
484
430
  json_response({
485
- providers: stats.health_report,
486
- summary: stats.circuit_summary
431
+ providers: provider_health_report,
432
+ summary: provider_circuit_summary
487
433
  })
488
434
  end
489
435
 
490
436
  app.get '/api/llm/providers/:name' do
491
437
  require_llm!
492
- unless gateway_available? && defined?(Legion::Extensions::LLM::Gateway::Runners::ProviderStats)
493
- halt 503, json_error('gateway_unavailable', 'LLM gateway is not loaded', status_code: 503)
494
- end
438
+ require_provider_inventory!
495
439
 
496
- stats = Legion::Extensions::LLM::Gateway::Runners::ProviderStats
497
- detail = stats.provider_detail(provider: params[:name])
498
- json_response(detail)
440
+ json_response(provider_detail(params[:name]))
499
441
  end
500
442
  end
501
443