legionio 1.9.2 → 1.9.18
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/.gitignore +1 -0
- data/CHANGELOG.md +105 -0
- data/Gemfile +37 -2
- data/README.md +9 -7
- data/legionio.gemspec +2 -2
- data/lib/legion/api/extensions.rb +18 -1
- data/lib/legion/api/llm.rb +86 -102
- data/lib/legion/api/metering.rb +26 -4
- data/lib/legion/api/skills.rb +5 -2
- data/lib/legion/api/tenants.rb +5 -5
- data/lib/legion/api/webhooks.rb +2 -0
- data/lib/legion/api.rb +4 -1
- data/lib/legion/cli/chat/tools/provider_health.rb +75 -8
- data/lib/legion/cli/doctor/extensions_check.rb +10 -1
- data/lib/legion/cli/llm_command.rb +17 -2
- data/lib/legion/cli/setup_command.rb +13 -6
- data/lib/legion/extensions/catalog/available.rb +11 -1
- data/lib/legion/extensions/catalog.rb +2 -0
- data/lib/legion/extensions/core.rb +8 -1
- data/lib/legion/extensions/helpers/segments.rb +6 -1
- data/lib/legion/extensions.rb +84 -3
- data/lib/legion/registry/governance.rb +1 -1
- data/lib/legion/registry/security_scanner.rb +3 -2
- data/lib/legion/service.rb +35 -0
- data/lib/legion/version.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1ddd3ea28ab0a066f4e3bc7d7fa6f7016ac1b395eec931a76ece7bc3722f04f8
|
|
4
|
+
data.tar.gz: 4c97638f1e52579b2351023fa21c36974412ab60c126fe8825c72f2d22466516
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5641aa16fb0c352632286285433596060ac046c1300c9083f0bf851afea6bca5e9626f41984cdd2b5586346a7515dbf6e11ea288555f5a57c543adca5fd1f52c
|
|
7
|
+
data.tar.gz: d3e33ed0f6fb0bb5da8688705ef13bfe95841b74faa4e06103d462fccf9771714f1d9efc28f0017cc10f1ad0310560f66c0468d3912d7d23ab8c34359e2abec0
|
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,111 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.9.18] - 2026-04-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- API-submitted LLM tools now build native `Legion::LLM::Types::ToolDefinition` objects instead of attempting to require RubyLLM at runtime.
|
|
9
|
+
- Provider route coverage now locks LegionIO's `/api/llm/providers` compatibility response ahead of later colliding LLM library route registrations.
|
|
10
|
+
|
|
11
|
+
## [1.9.17] - 2026-04-29
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- 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.
|
|
15
|
+
- Native LLM provider health API responses now preserve model, type, health, and instance fields when inventory offerings are loaded from string-keyed data.
|
|
16
|
+
|
|
17
|
+
## [1.9.16] - 2026-04-28
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- 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.
|
|
21
|
+
|
|
22
|
+
## [1.9.15] - 2026-04-28
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- 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.
|
|
26
|
+
- README LLM documentation now calls out `lex-llm-gateway` as legacy-only compatibility glue that is not installed by default.
|
|
27
|
+
|
|
28
|
+
## [1.9.14] - 2026-04-28
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- LegionIO now requires `legion-tty >= 0.5.4` so packaged installs include the Legion-native LLM probe instead of the legacy direct RubyLLM probe.
|
|
32
|
+
|
|
33
|
+
## [1.9.13] - 2026-04-28
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- LegionIO now requires `legion-llm >= 0.8.43` so packaged installs get the optional RubyLLM compatibility layer and native dispatch fallback defaults.
|
|
37
|
+
|
|
38
|
+
## [1.9.12] - 2026-04-28
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
- LegionIO now requires `legion-llm >= 0.8.42` so packaged installs resolve the validated LLM routing uplift release.
|
|
42
|
+
|
|
43
|
+
## [1.9.11] - 2026-04-28
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- LLM chat API routing now prefers native `Legion::LLM.chat` even when legacy `lex-llm-gateway` compatibility code is loaded.
|
|
47
|
+
|
|
48
|
+
## [1.9.10] - 2026-04-28
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- 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.
|
|
52
|
+
|
|
53
|
+
## [1.9.9] - 2026-04-28
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
- Registry governance and security scanning now accept nested `lex-*` extension gem names such as `lex-llm-openai` and `lex-llm-azure-foundry`.
|
|
57
|
+
|
|
58
|
+
## [1.9.8] - 2026-04-28
|
|
59
|
+
|
|
60
|
+
### Fixed
|
|
61
|
+
- The `agentic` setup pack now installs the Legion-native `lex-llm-*` provider stack without also installing retired legacy LLM provider gems.
|
|
62
|
+
- 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.
|
|
63
|
+
- LegionIO now requires `legion-llm >= 0.8.41` so packaged installs get the router dependency cleanup that removes retired legacy provider runtime dependencies.
|
|
64
|
+
|
|
65
|
+
## [1.9.7] - 2026-04-28
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Extension discovery now maps `lex-llm-azure-foundry` to `Legion::Extensions::Llm::AzureFoundry` and `legion/extensions/llm/azure_foundry`.
|
|
69
|
+
- LegionIO now requires `legion-llm >= 0.8.40` so packaged installs include the native provider bridge needed by the Legion-native LLM stack.
|
|
70
|
+
- README LLM provider documentation now describes the `lex-llm-*` provider stack instead of the retired legacy provider list.
|
|
71
|
+
|
|
72
|
+
## [1.9.6] - 2026-04-28
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
- LLM API gateway checks now use the `Legion::Extensions::Llm::Gateway` namespace loaded by Legion extension autoloading.
|
|
76
|
+
- LLM inference and skill invocation routes now call the current `Legion::LLM::Inference` request/executor API instead of the retired pipeline constants.
|
|
77
|
+
- `legionio llm ping` now routes through `Legion::LLM.ask_direct` instead of bypassing Legion routing with a raw RubyLLM call.
|
|
78
|
+
- API client tool construction now degrades cleanly when the RubyLLM tool base is unavailable.
|
|
79
|
+
|
|
80
|
+
## [1.9.5] - 2026-04-28
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
- 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.
|
|
84
|
+
|
|
85
|
+
### Fixed
|
|
86
|
+
- Local development Gemfile wiring now includes guarded `lex-llm-ledger` resolution so the local bundle matches the LLM setup pack.
|
|
87
|
+
- Local development Gemfile wiring now points `lex-llm-gateway` at the workspace extension path actually used by LegionIO checkouts.
|
|
88
|
+
- Default setup packs no longer install legacy `lex-llm-gateway`; the extension catalog now labels it as compatibility glue rather than active LLM routing.
|
|
89
|
+
- `require 'legion/extensions'` now loads its logging dependency directly instead of relying on `require 'legion'` order.
|
|
90
|
+
|
|
91
|
+
## [1.9.4] - 2026-04-27
|
|
92
|
+
|
|
93
|
+
### Added
|
|
94
|
+
- Extension boot now runs a dedicated LLM load phase so `lex-llm` loads before any `lex-llm-*` extension gems.
|
|
95
|
+
- `/api/health` now reports `uptime_seconds` and `uptime` for dashboard and monitor consumers. Fixes #168
|
|
96
|
+
- `/api/extensions` now returns a flat loaded-extension summary for dashboard consumers. Fixes #169
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- `legionio doctor` no longer reports extension-loader config keys as missing `lex-*` gems. Fixes #157
|
|
100
|
+
- `/api/metering` now returns dashboard headline totals instead of the routing breakdown shape. Fixes #170
|
|
101
|
+
- 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
|
|
102
|
+
- `/api/webhooks` now loads its `Legion::Webhooks` runtime dependency before route handlers execute. Fixes #172
|
|
103
|
+
- `/api/tenants` now passes positional response data and uses `json_error` for missing tenants. Fixes #173
|
|
104
|
+
|
|
105
|
+
## [1.9.3] - 2026-04-27
|
|
106
|
+
|
|
107
|
+
### Fixed
|
|
108
|
+
- Extension catalog persistence now skips no-op startup updates when the stored state already matches, reducing local SQLite write churn. Fixes #176
|
|
109
|
+
|
|
5
110
|
## [1.9.2] - 2026-04-27
|
|
6
111
|
|
|
7
112
|
### Fixed
|
data/Gemfile
CHANGED
|
@@ -4,16 +4,51 @@ 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
|
+
|
|
7
31
|
gem 'legion-data', path: '../legion-data' if File.exist?(File.expand_path('../legion-data', __dir__))
|
|
8
32
|
gem 'legion-gaia', path: '../legion-gaia' if File.exist?(File.expand_path('../legion-gaia', __dir__))
|
|
9
|
-
|
|
33
|
+
if (legion_llm_path = local_gem_path('legion-llm', '../legion-llm', 'lib/legion/llm/version.rb', '>= 0.8.47'))
|
|
34
|
+
gem 'legion-llm', path: legion_llm_path
|
|
35
|
+
end
|
|
36
|
+
if (legion_tty_path = local_gem_path('legion-tty', '../legion-tty', 'lib/legion/tty/version.rb', '>= 0.5.4'))
|
|
37
|
+
gem 'legion-tty', path: legion_tty_path
|
|
38
|
+
end
|
|
10
39
|
gem 'legion-logging', path: '../legion-logging' if File.exist?(File.expand_path('../legion-logging', __dir__))
|
|
11
40
|
gem 'legion-mcp', path: '../legion-mcp' if File.exist?(File.expand_path('../legion-mcp', __dir__))
|
|
12
41
|
gem 'legion-settings', path: '../legion-settings' if File.exist?(File.expand_path('../legion-settings', __dir__))
|
|
13
42
|
|
|
14
43
|
gem 'legion-apollo', path: '../legion-apollo' if File.exist?(File.expand_path('../legion-apollo', __dir__))
|
|
15
44
|
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
|
|
45
|
+
gem 'lex-llm', path: '../extensions-ai/lex-llm' if File.exist?(File.expand_path('../extensions-ai/lex-llm', __dir__))
|
|
46
|
+
gem 'lex-llm-ledger', path: '../extensions-ai/lex-llm-ledger' if File.exist?(File.expand_path('../extensions-ai/lex-llm-ledger', __dir__))
|
|
47
|
+
%w[anthropic azure-foundry bedrock gemini mlx ollama openai vertex vllm].each do |provider|
|
|
48
|
+
provider_path = "../extensions-ai/lex-llm-#{provider}"
|
|
49
|
+
gem "lex-llm-#{provider}", path: provider_path if File.exist?(File.expand_path(provider_path, __dir__))
|
|
50
|
+
end
|
|
51
|
+
gem 'lex-llm-gateway', path: '../extensions/lex-llm-gateway' if File.exist?(File.expand_path('../extensions/lex-llm-gateway', __dir__))
|
|
17
52
|
gem 'lex-microsoft_teams', path: '../extensions/lex-microsoft_teams' if File.exist?(File.expand_path('../extensions/lex-microsoft_teams', __dir__))
|
|
18
53
|
|
|
19
54
|
gem 'pg'
|
data/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Schedule tasks, chain services into dependency graphs, run them concurrently via
|
|
|
18
18
|
[](https://www.ruby-lang.org/)
|
|
19
19
|
[](LICENSE)
|
|
20
20
|
|
|
21
|
-
**Ruby >= 3.4** | **v1.
|
|
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 (
|
|
414
|
+
### Core (14 operational extensions)
|
|
415
415
|
|
|
416
|
-
`lex-node` `lex-tasker` `lex-conditioner` `lex-transformer` `lex-
|
|
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
|
|
432
|
+
### AI / LLM
|
|
433
433
|
|
|
434
|
-
`lex-azure-
|
|
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
|
|
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
|
+
`lex-llm-gateway` remains available only as legacy compatibility glue for older deployments. New `legion setup llm` and `legion setup agentic` installs use the native `legion-llm` / `lex-llm-*` stack and do not install the gateway by default.
|
|
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 +
|
|
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
|
@@ -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.
|
|
66
|
-
spec.add_dependency 'legion-tty', '>= 0.4
|
|
65
|
+
spec.add_dependency 'legion-llm', '>= 0.8.47'
|
|
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
|
data/lib/legion/api/llm.rb
CHANGED
|
@@ -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
|
|
@@ -29,89 +25,86 @@ module Legion
|
|
|
29
25
|
end
|
|
30
26
|
|
|
31
27
|
define_method(:gateway_available?) do
|
|
32
|
-
defined?(Legion::Extensions::
|
|
28
|
+
defined?(Legion::Extensions::Llm::Gateway::Runners::Inference)
|
|
33
29
|
end
|
|
34
30
|
|
|
35
|
-
define_method(:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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}"
|
|
31
|
+
define_method(:native_provider_stats_available?) do
|
|
32
|
+
defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:providers)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
define_method(:provider_health_report) do
|
|
36
|
+
if native_provider_stats_available?
|
|
37
|
+
groups = Legion::LLM::Inventory.providers
|
|
38
|
+
return [] unless groups.respond_to?(:map)
|
|
39
|
+
|
|
40
|
+
groups.map do |provider, offerings|
|
|
41
|
+
provider_offerings = Array(offerings)
|
|
42
|
+
health = provider_offerings.map { |offering| offering_value(offering, :health) }
|
|
43
|
+
.find { |entry| entry.is_a?(Hash) } || {}
|
|
44
|
+
circuit = health[:circuit_state] || health['circuit_state'] || 'unknown'
|
|
45
|
+
{
|
|
46
|
+
provider: provider.to_s,
|
|
47
|
+
circuit: circuit,
|
|
48
|
+
adjustment: health[:adjustment] || health['adjustment'] || 0,
|
|
49
|
+
healthy: circuit.to_s != 'open',
|
|
50
|
+
offerings: provider_offerings.size,
|
|
51
|
+
models: provider_offerings.map { |offering| offering_value(offering, :model) }.compact.uniq,
|
|
52
|
+
types: provider_offerings.map { |offering| offering_value(offering, :type) }.compact.uniq,
|
|
53
|
+
instances: provider_offerings.map do |offering|
|
|
54
|
+
offering_value(offering, :provider_instance) || offering_value(offering, :instance_id)
|
|
55
|
+
end.compact.uniq
|
|
56
|
+
}
|
|
111
57
|
end
|
|
58
|
+
elsif defined?(Legion::Extensions::Llm::Gateway::Runners::ProviderStats)
|
|
59
|
+
Legion::Extensions::Llm::Gateway::Runners::ProviderStats.health_report
|
|
60
|
+
else
|
|
61
|
+
[]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
define_method(:provider_circuit_summary) do
|
|
66
|
+
report = provider_health_report
|
|
67
|
+
return Legion::Extensions::Llm::Gateway::Runners::ProviderStats.circuit_summary if
|
|
68
|
+
report.empty? && defined?(Legion::Extensions::Llm::Gateway::Runners::ProviderStats)
|
|
69
|
+
|
|
70
|
+
circuits = report.map { |entry| entry[:circuit].to_s }
|
|
71
|
+
{
|
|
72
|
+
total: report.size,
|
|
73
|
+
closed: circuits.count('closed'),
|
|
74
|
+
open: circuits.count('open'),
|
|
75
|
+
half_open: circuits.count('half_open')
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
define_method(:provider_detail) do |provider|
|
|
80
|
+
provider_name = provider.to_s
|
|
81
|
+
if native_provider_stats_available?
|
|
82
|
+
entry = provider_health_report.find { |candidate| candidate[:provider] == provider_name }
|
|
83
|
+
halt 404, json_error('provider_not_found', "Provider '#{provider_name}' not found", status_code: 404) unless entry
|
|
84
|
+
|
|
85
|
+
entry
|
|
86
|
+
elsif defined?(Legion::Extensions::Llm::Gateway::Runners::ProviderStats)
|
|
87
|
+
Legion::Extensions::Llm::Gateway::Runners::ProviderStats.provider_detail(provider: provider_name.to_sym)
|
|
88
|
+
else
|
|
89
|
+
halt 503, json_error('providers_unavailable', 'LLM provider inventory is not loaded', status_code: 503)
|
|
112
90
|
end
|
|
113
|
-
|
|
114
|
-
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
define_method(:offering_value) do |offering, key|
|
|
94
|
+
next unless offering.respond_to?(:[])
|
|
95
|
+
|
|
96
|
+
offering[key] || offering[key.to_s]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
define_method(:build_client_tool_class) do |tname, tdesc, tschema|
|
|
100
|
+
require 'legion/llm/types/tool_definition' unless defined?(Legion::LLM::Types::ToolDefinition)
|
|
101
|
+
|
|
102
|
+
Legion::LLM::Types::ToolDefinition.build(
|
|
103
|
+
name: tname,
|
|
104
|
+
description: tdesc,
|
|
105
|
+
parameters: tschema || {},
|
|
106
|
+
source: { type: :client, executable: true }
|
|
107
|
+
)
|
|
115
108
|
rescue StandardError => e
|
|
116
109
|
Legion::Logging.log_exception(e, payload_summary: "build_client_tool_class failed for #{tname}", component_type: :api)
|
|
117
110
|
nil
|
|
@@ -168,12 +161,12 @@ module Legion
|
|
|
168
161
|
model = body[:model]
|
|
169
162
|
provider = body[:provider]
|
|
170
163
|
|
|
171
|
-
#
|
|
172
|
-
if gateway_available?
|
|
164
|
+
# Compatibility fallback for legacy gateway installs. Native legion-llm handles routing first.
|
|
165
|
+
if !Legion::LLM.respond_to?(:chat) && gateway_available?
|
|
173
166
|
ingress_result = Legion::Ingress.run(
|
|
174
167
|
payload: { message: message, model: model, provider: provider,
|
|
175
168
|
request_id: request_id },
|
|
176
|
-
runner_class: 'Legion::Extensions::
|
|
169
|
+
runner_class: 'Legion::Extensions::Llm::Gateway::Runners::Inference',
|
|
177
170
|
function: 'chat',
|
|
178
171
|
source: 'api'
|
|
179
172
|
)
|
|
@@ -307,8 +300,8 @@ module Legion
|
|
|
307
300
|
streaming = body[:stream] == true && env['HTTP_ACCEPT']&.include?('text/event-stream')
|
|
308
301
|
|
|
309
302
|
# Executor handles all registry tool injection — API only passes client-defined tools
|
|
310
|
-
require 'legion/llm/
|
|
311
|
-
|
|
303
|
+
require 'legion/llm/inference' unless defined?(Legion::LLM::Inference::Request) &&
|
|
304
|
+
defined?(Legion::LLM::Inference::Executor)
|
|
312
305
|
|
|
313
306
|
principal = defined?(Legion::Identity::Request) && env['legion.principal']
|
|
314
307
|
caller_ctx = if principal
|
|
@@ -318,7 +311,7 @@ module Legion
|
|
|
318
311
|
end
|
|
319
312
|
|
|
320
313
|
caller_metadata = body[:metadata].is_a?(Hash) ? body[:metadata] : {}
|
|
321
|
-
req = Legion::LLM::
|
|
314
|
+
req = Legion::LLM::Inference::Request.build(
|
|
322
315
|
messages: messages,
|
|
323
316
|
system: body[:system],
|
|
324
317
|
routing: { provider: provider, model: model },
|
|
@@ -329,7 +322,7 @@ module Legion
|
|
|
329
322
|
stream: streaming,
|
|
330
323
|
cache: { strategy: :default, cacheable: true }
|
|
331
324
|
)
|
|
332
|
-
executor = Legion::LLM::
|
|
325
|
+
executor = Legion::LLM::Inference::Executor.new(req)
|
|
333
326
|
|
|
334
327
|
if streaming
|
|
335
328
|
content_type 'text/event-stream'
|
|
@@ -476,26 +469,17 @@ module Legion
|
|
|
476
469
|
def self.register_providers(app)
|
|
477
470
|
app.get '/api/llm/providers' do
|
|
478
471
|
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
|
|
482
472
|
|
|
483
|
-
stats = Legion::Extensions::LLM::Gateway::Runners::ProviderStats
|
|
484
473
|
json_response({
|
|
485
|
-
providers:
|
|
486
|
-
summary:
|
|
474
|
+
providers: provider_health_report,
|
|
475
|
+
summary: provider_circuit_summary
|
|
487
476
|
})
|
|
488
477
|
end
|
|
489
478
|
|
|
490
479
|
app.get '/api/llm/providers/:name' do
|
|
491
480
|
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
|
|
495
481
|
|
|
496
|
-
|
|
497
|
-
detail = stats.provider_detail(provider: params[:name])
|
|
498
|
-
json_response(detail)
|
|
482
|
+
json_response(provider_detail(params[:name]))
|
|
499
483
|
end
|
|
500
484
|
end
|
|
501
485
|
|
data/lib/legion/api/metering.rb
CHANGED
|
@@ -5,6 +5,13 @@ module Legion
|
|
|
5
5
|
module Routes
|
|
6
6
|
module Metering
|
|
7
7
|
def self.registered(app)
|
|
8
|
+
register_helpers(app)
|
|
9
|
+
register_summary_route(app)
|
|
10
|
+
register_rollup_route(app)
|
|
11
|
+
register_by_model_route(app)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.register_helpers(app)
|
|
8
15
|
app.helpers do
|
|
9
16
|
define_method(:require_metering!) do
|
|
10
17
|
return if defined?(Legion::Extensions::Metering::Runners::Metering)
|
|
@@ -17,18 +24,29 @@ module Legion
|
|
|
17
24
|
Legion::Data.connected? && Legion::Data.connection.table_exists?(:metering_records)
|
|
18
25
|
end
|
|
19
26
|
end
|
|
27
|
+
end
|
|
20
28
|
|
|
29
|
+
def self.register_summary_route(app)
|
|
21
30
|
app.get '/api/metering' do
|
|
22
31
|
require_metering!
|
|
23
|
-
|
|
32
|
+
unless metering_table?
|
|
33
|
+
return json_response({ total_cost_usd: 0.0, total_tokens: 0, total_requests: 0,
|
|
34
|
+
note: 'metering_records table not available' })
|
|
35
|
+
end
|
|
24
36
|
|
|
25
|
-
|
|
26
|
-
json_response(
|
|
37
|
+
ds = Legion::Data.connection[:metering_records]
|
|
38
|
+
json_response({
|
|
39
|
+
total_cost_usd: (ds.sum(:cost_usd) || 0).to_f,
|
|
40
|
+
total_tokens: (ds.sum(:total_tokens) || 0).to_i,
|
|
41
|
+
total_requests: ds.count
|
|
42
|
+
})
|
|
27
43
|
rescue StandardError => e
|
|
28
44
|
Legion::Logging.log_exception(e, payload_summary: 'GET /api/metering', component_type: :api)
|
|
29
|
-
json_response({
|
|
45
|
+
json_response({ total_cost_usd: 0.0, total_tokens: 0, total_requests: 0, error: e.message })
|
|
30
46
|
end
|
|
47
|
+
end
|
|
31
48
|
|
|
49
|
+
def self.register_rollup_route(app)
|
|
32
50
|
app.get '/api/metering/rollup' do
|
|
33
51
|
require_metering!
|
|
34
52
|
return json_response({ rollup: [], period: 'hourly', note: 'metering_records table not available' }) unless metering_table?
|
|
@@ -41,7 +59,9 @@ module Legion
|
|
|
41
59
|
Legion::Logging.log_exception(e, payload_summary: 'GET /api/metering/rollup', component_type: :api)
|
|
42
60
|
json_response({ rollup: [], period: 'hourly', error: e.message })
|
|
43
61
|
end
|
|
62
|
+
end
|
|
44
63
|
|
|
64
|
+
def self.register_by_model_route(app)
|
|
45
65
|
app.get '/api/metering/by_model' do
|
|
46
66
|
require_metering!
|
|
47
67
|
return json_response({ models: [], note: 'metering_records table not available' }) unless metering_table?
|
|
@@ -60,6 +80,8 @@ module Legion
|
|
|
60
80
|
json_response({ models: [], error: e.message })
|
|
61
81
|
end
|
|
62
82
|
end
|
|
83
|
+
|
|
84
|
+
private_class_method :register_helpers, :register_summary_route, :register_rollup_route, :register_by_model_route
|
|
63
85
|
end
|
|
64
86
|
end
|
|
65
87
|
end
|
data/lib/legion/api/skills.rb
CHANGED
|
@@ -64,13 +64,16 @@ module Legion
|
|
|
64
64
|
conv_id = body[:conversation_id] || "conv_#{SecureRandom.hex(8)}"
|
|
65
65
|
begin
|
|
66
66
|
Legion::LLM::ConversationStore.set_skill_state(conv_id, skill_key: skill_name, resume_at: 0)
|
|
67
|
-
|
|
67
|
+
require 'legion/llm/inference' unless defined?(Legion::LLM::Inference::Request) &&
|
|
68
|
+
defined?(Legion::LLM::Inference::Executor)
|
|
69
|
+
|
|
70
|
+
req = Legion::LLM::Inference::Request.build(
|
|
68
71
|
messages: [{ role: :user, content: body[:initial_message] || 'start skill' }],
|
|
69
72
|
conversation_id: conv_id,
|
|
70
73
|
metadata: (body[:metadata].is_a?(Hash) ? body[:metadata] : {}).merge(skill_invoke: true),
|
|
71
74
|
stream: false
|
|
72
75
|
)
|
|
73
|
-
result = Legion::LLM::
|
|
76
|
+
result = Legion::LLM::Inference::Executor.new(req).call
|
|
74
77
|
json_response({ conversation_id: conv_id, content: result.message[:content],
|
|
75
78
|
skill_name: skill_name })
|
|
76
79
|
rescue StandardError => e
|
data/lib/legion/api/tenants.rb
CHANGED
|
@@ -9,7 +9,7 @@ module Legion
|
|
|
9
9
|
def self.registered(app)
|
|
10
10
|
app.get '/api/tenants' do
|
|
11
11
|
tenants = Legion::Tenants.list
|
|
12
|
-
json_response(
|
|
12
|
+
json_response(tenants)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
app.post '/api/tenants' do
|
|
@@ -24,13 +24,13 @@ module Legion
|
|
|
24
24
|
|
|
25
25
|
app.get '/api/tenants/:tenant_id' do
|
|
26
26
|
tenant = Legion::Tenants.find(params[:tenant_id])
|
|
27
|
-
halt 404,
|
|
28
|
-
json_response(
|
|
27
|
+
halt 404, json_error('not_found', 'Tenant not found', status_code: 404) unless tenant
|
|
28
|
+
json_response(tenant)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
app.post '/api/tenants/:tenant_id/suspend' do
|
|
32
32
|
result = Legion::Tenants.suspend(tenant_id: params[:tenant_id])
|
|
33
|
-
json_response(
|
|
33
|
+
json_response(result)
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
app.get '/api/tenants/:tenant_id/quota/:resource' do
|
|
@@ -38,7 +38,7 @@ module Legion
|
|
|
38
38
|
tenant_id: params[:tenant_id],
|
|
39
39
|
resource: params[:resource].to_sym
|
|
40
40
|
)
|
|
41
|
-
json_response(
|
|
41
|
+
json_response(result)
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
data/lib/legion/api/webhooks.rb
CHANGED
data/lib/legion/api.rb
CHANGED
|
@@ -67,6 +67,8 @@ require_relative 'api/graphql' if defined?(GraphQL)
|
|
|
67
67
|
|
|
68
68
|
module Legion
|
|
69
69
|
class API < Sinatra::Base
|
|
70
|
+
START_TIME = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
71
|
+
|
|
70
72
|
helpers Legion::API::Helpers
|
|
71
73
|
helpers Legion::API::Validators
|
|
72
74
|
|
|
@@ -105,7 +107,8 @@ module Legion
|
|
|
105
107
|
|
|
106
108
|
# Health and readiness
|
|
107
109
|
get '/api/health' do
|
|
108
|
-
|
|
110
|
+
uptime_seconds = (::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - START_TIME).to_i
|
|
111
|
+
json_response({ status: 'ok', version: Legion::VERSION, uptime_seconds: uptime_seconds, uptime: uptime_seconds })
|
|
109
112
|
end
|
|
110
113
|
|
|
111
114
|
get '/api/ready' do
|
|
@@ -19,7 +19,7 @@ module Legion
|
|
|
19
19
|
param :provider, type: 'string', desc: 'Specific provider to check (optional)', required: false
|
|
20
20
|
|
|
21
21
|
def execute(provider: nil)
|
|
22
|
-
return 'LLM
|
|
22
|
+
return 'LLM provider inventory not available.' unless provider_stats_available?
|
|
23
23
|
|
|
24
24
|
if provider
|
|
25
25
|
format_detail(provider.strip)
|
|
@@ -34,11 +34,11 @@ module Legion
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
36
|
def format_report
|
|
37
|
-
report =
|
|
37
|
+
report = provider_health_report
|
|
38
38
|
return "Router not available: #{report[:error]}" if report.is_a?(Hash) && report[:error]
|
|
39
39
|
return 'No providers configured.' if report.empty?
|
|
40
40
|
|
|
41
|
-
summary =
|
|
41
|
+
summary = provider_circuit_summary(report)
|
|
42
42
|
lines = ["Provider Health Report:\n"]
|
|
43
43
|
lines << format_circuit_summary(summary) if summary.is_a?(Hash) && !summary[:error]
|
|
44
44
|
lines << ''
|
|
@@ -47,8 +47,9 @@ module Legion
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def format_detail(provider)
|
|
50
|
-
entry =
|
|
50
|
+
entry = provider_detail(provider)
|
|
51
51
|
return "Router not available: #{entry[:error]}" if entry[:error]
|
|
52
|
+
return "Provider not found: #{provider}" if entry.empty?
|
|
52
53
|
|
|
53
54
|
lines = ["Provider: #{entry[:provider]}\n"]
|
|
54
55
|
lines << " Circuit: #{entry[:circuit]}"
|
|
@@ -65,17 +66,83 @@ module Legion
|
|
|
65
66
|
|
|
66
67
|
def format_entry(entry)
|
|
67
68
|
icon = entry[:healthy] ? '+' : '!'
|
|
68
|
-
|
|
69
|
+
suffix = +''
|
|
70
|
+
suffix << " offerings=#{entry[:offerings]}" if entry.key?(:offerings)
|
|
71
|
+
suffix << " models=#{entry[:models].length}" if entry[:models].respond_to?(:length)
|
|
72
|
+
format(' [%<icon>s] %<name>-15s circuit=%<circuit>s adj=%<adj>d%<suffix>s',
|
|
69
73
|
icon: icon, name: entry[:provider],
|
|
70
|
-
circuit: entry[:circuit], adj: entry[:adjustment])
|
|
74
|
+
circuit: entry[:circuit], adj: entry[:adjustment], suffix: suffix)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def provider_stats_available?
|
|
78
|
+
native_provider_stats_available? || gateway_stats_available?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def native_provider_stats_available?
|
|
82
|
+
defined?(Legion::LLM::Inventory) && Legion::LLM::Inventory.respond_to?(:providers)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def provider_health_report
|
|
86
|
+
return native_provider_health_report if native_provider_stats_available?
|
|
87
|
+
|
|
88
|
+
stats_module.health_report
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def native_provider_health_report
|
|
92
|
+
groups = Legion::LLM::Inventory.providers
|
|
93
|
+
return [] unless groups.respond_to?(:map)
|
|
94
|
+
|
|
95
|
+
groups.map do |provider, offerings|
|
|
96
|
+
provider_offerings = Array(offerings)
|
|
97
|
+
health = provider_offerings.map { |offering| offering_value(offering, :health) }
|
|
98
|
+
.find { |entry| entry.is_a?(Hash) } || {}
|
|
99
|
+
circuit = health[:circuit_state] || health['circuit_state'] || 'unknown'
|
|
100
|
+
{
|
|
101
|
+
provider: provider.to_s,
|
|
102
|
+
circuit: circuit,
|
|
103
|
+
adjustment: health[:adjustment] || health['adjustment'] || 0,
|
|
104
|
+
healthy: circuit.to_s != 'open',
|
|
105
|
+
offerings: provider_offerings.size,
|
|
106
|
+
models: provider_offerings.map { |offering| offering_value(offering, :model) }.compact.uniq,
|
|
107
|
+
types: provider_offerings.map { |offering| offering_value(offering, :type) }.compact.uniq,
|
|
108
|
+
instances: provider_offerings.map do |offering|
|
|
109
|
+
offering_value(offering, :provider_instance) || offering_value(offering, :instance_id)
|
|
110
|
+
end.compact.uniq
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def provider_circuit_summary(report)
|
|
116
|
+
return stats_module.circuit_summary unless native_provider_stats_available?
|
|
117
|
+
|
|
118
|
+
circuits = report.map { |entry| entry[:circuit].to_s }
|
|
119
|
+
{
|
|
120
|
+
total: report.size,
|
|
121
|
+
closed: circuits.count('closed'),
|
|
122
|
+
open: circuits.count('open'),
|
|
123
|
+
half_open: circuits.count('half_open')
|
|
124
|
+
}
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def provider_detail(provider)
|
|
128
|
+
provider_name = provider.to_s
|
|
129
|
+
return stats_module.provider_detail(provider: provider_name.to_sym) unless native_provider_stats_available?
|
|
130
|
+
|
|
131
|
+
provider_health_report.find { |entry| entry[:provider] == provider_name } || {}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def offering_value(offering, key)
|
|
135
|
+
return unless offering.respond_to?(:[])
|
|
136
|
+
|
|
137
|
+
offering[key] || offering[key.to_s]
|
|
71
138
|
end
|
|
72
139
|
|
|
73
140
|
def gateway_stats_available?
|
|
74
|
-
defined?(Legion::Extensions::
|
|
141
|
+
defined?(Legion::Extensions::Llm::Gateway::Runners::ProviderStats)
|
|
75
142
|
end
|
|
76
143
|
|
|
77
144
|
def stats_module
|
|
78
|
-
Legion::Extensions::
|
|
145
|
+
Legion::Extensions::Llm::Gateway::Runners::ProviderStats
|
|
79
146
|
end
|
|
80
147
|
end
|
|
81
148
|
end
|
|
@@ -4,6 +4,11 @@ module Legion
|
|
|
4
4
|
module CLI
|
|
5
5
|
class Doctor
|
|
6
6
|
class ExtensionsCheck
|
|
7
|
+
LOADER_CONFIG_KEYS = %w[
|
|
8
|
+
agentic ai auto_install blocked categories core gaia identity
|
|
9
|
+
parallel_pool_size reserved_prefixes reserved_words
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
7
12
|
def name
|
|
8
13
|
'Extensions'
|
|
9
14
|
end
|
|
@@ -38,7 +43,11 @@ module Legion
|
|
|
38
43
|
exts = Legion::Settings[:extensions]
|
|
39
44
|
return [] unless exts.is_a?(Hash) || exts.is_a?(Array)
|
|
40
45
|
|
|
41
|
-
exts.is_a?(Array)
|
|
46
|
+
if exts.is_a?(Array)
|
|
47
|
+
exts.map(&:to_s)
|
|
48
|
+
else
|
|
49
|
+
exts.keys.map(&:to_s).reject { |key| LOADER_CONFIG_KEYS.include?(key) }
|
|
50
|
+
end
|
|
42
51
|
rescue StandardError => e
|
|
43
52
|
Legion::Logging.warn("ExtensionsCheck#configured_extensions failed: #{e.message}") if defined?(Legion::Logging)
|
|
44
53
|
[]
|
|
@@ -234,10 +234,15 @@ module Legion
|
|
|
234
234
|
out.header(" Pinging #{name} (#{model})...") unless options[:json]
|
|
235
235
|
t0 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
236
236
|
|
|
237
|
-
response =
|
|
237
|
+
response = Legion::LLM.ask_direct(
|
|
238
|
+
message: 'Respond with only the word: pong',
|
|
239
|
+
model: model,
|
|
240
|
+
provider: name,
|
|
241
|
+
caller: { source: 'cli', command: 'llm ping' }
|
|
242
|
+
)
|
|
238
243
|
elapsed = ((::Process.clock_gettime(::Process::CLOCK_MONOTONIC) - t0) * 1000).round
|
|
239
244
|
|
|
240
|
-
content = response
|
|
245
|
+
content = response_content(response)
|
|
241
246
|
success = content.downcase.include?('pong')
|
|
242
247
|
|
|
243
248
|
if success
|
|
@@ -255,6 +260,16 @@ module Legion
|
|
|
255
260
|
{ provider: name, status: 'error', message: e.message, model: model, latency_ms: elapsed }
|
|
256
261
|
end
|
|
257
262
|
|
|
263
|
+
def response_content(response)
|
|
264
|
+
if response.respond_to?(:content)
|
|
265
|
+
response.content.to_s.strip
|
|
266
|
+
elsif response.is_a?(Hash)
|
|
267
|
+
(response[:content] || response['content'] || response[:response] || response['response']).to_s.strip
|
|
268
|
+
else
|
|
269
|
+
response.to_s.strip
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
258
273
|
def show_status(out, data)
|
|
259
274
|
out.header('LLM Status')
|
|
260
275
|
out.detail({
|
|
@@ -37,20 +37,27 @@ module Legion
|
|
|
37
37
|
lex-agentic-imagination lex-agentic-inference lex-agentic-integration
|
|
38
38
|
lex-agentic-language lex-agentic-learning lex-agentic-memory
|
|
39
39
|
lex-agentic-self lex-agentic-social lex-apollo lex-audit lex-autofix
|
|
40
|
-
lex-
|
|
40
|
+
lex-codegen lex-coldstart
|
|
41
41
|
lex-conditioner lex-cost-scanner lex-dataset lex-detect
|
|
42
|
-
lex-eval lex-exec lex-extinction lex-factory lex-finops
|
|
43
|
-
lex-
|
|
42
|
+
lex-eval lex-exec lex-extinction lex-factory lex-finops
|
|
43
|
+
lex-governance lex-kerberos lex-knowledge lex-llm
|
|
44
|
+
lex-llm-anthropic lex-llm-azure-foundry lex-llm-bedrock
|
|
45
|
+
lex-llm-gemini lex-llm-ledger lex-llm-mlx
|
|
46
|
+
lex-llm-ollama lex-llm-openai lex-llm-vertex lex-llm-vllm
|
|
44
47
|
lex-metering lex-mesh lex-microsoft_teams lex-mind-growth lex-node
|
|
45
|
-
lex-onboard lex-
|
|
48
|
+
lex-onboard lex-pilot-infra-monitor
|
|
46
49
|
lex-pilot-knowledge-assist lex-privatecore lex-prompt lex-react
|
|
47
50
|
lex-swarm lex-swarm-github lex-synapse lex-telemetry lex-tick
|
|
48
|
-
lex-transformer
|
|
51
|
+
lex-transformer
|
|
49
52
|
]
|
|
50
53
|
},
|
|
51
54
|
llm: {
|
|
52
55
|
description: 'LLM routing and provider integration (no cognitive stack)',
|
|
53
|
-
gems: %w[
|
|
56
|
+
gems: %w[
|
|
57
|
+
legion-llm lex-llm lex-llm-anthropic lex-llm-azure-foundry
|
|
58
|
+
lex-llm-bedrock lex-llm-gemini lex-llm-ledger lex-llm-mlx
|
|
59
|
+
lex-llm-ollama lex-llm-openai lex-llm-vertex lex-llm-vllm
|
|
60
|
+
]
|
|
54
61
|
},
|
|
55
62
|
channels: {
|
|
56
63
|
description: 'Channel adapters for chat platforms',
|
|
@@ -14,7 +14,7 @@ module Legion
|
|
|
14
14
|
{ name: 'lex-exec', category: 'core', description: 'Shell command execution' },
|
|
15
15
|
{ name: 'lex-health', category: 'core', description: 'Health monitoring and metrics' },
|
|
16
16
|
{ name: 'lex-lex', category: 'core', description: 'Extension management' },
|
|
17
|
-
{ name: 'lex-llm-gateway', category: '
|
|
17
|
+
{ name: 'lex-llm-gateway', category: 'legacy', description: 'Legacy LLM gateway compatibility' },
|
|
18
18
|
{ name: 'lex-llm-ledger', category: 'core', description: 'LLM cost and usage ledger' },
|
|
19
19
|
{ name: 'lex-log', category: 'core', description: 'Log shipping and aggregation' },
|
|
20
20
|
{ name: 'lex-metering', category: 'core', description: 'Resource metering and accounting' },
|
|
@@ -36,6 +36,16 @@ module Legion
|
|
|
36
36
|
{ name: 'lex-ollama', category: 'ai', description: 'Ollama local LLM provider integration' },
|
|
37
37
|
{ name: 'lex-openai', category: 'ai', description: 'OpenAI provider integration' },
|
|
38
38
|
{ name: 'lex-xai', category: 'ai', description: 'xAI Grok provider integration' },
|
|
39
|
+
{ name: 'lex-llm', category: 'ai', description: 'Common LLM provider base and routing metadata' },
|
|
40
|
+
{ name: 'lex-llm-anthropic', category: 'ai', description: 'Anthropic LLM provider integration' },
|
|
41
|
+
{ name: 'lex-llm-azure-foundry', category: 'ai', description: 'Azure AI Foundry hosted LLM provider integration' },
|
|
42
|
+
{ name: 'lex-llm-bedrock', category: 'ai', description: 'AWS Bedrock hosted LLM provider integration' },
|
|
43
|
+
{ name: 'lex-llm-gemini', category: 'ai', description: 'Google Gemini LLM provider integration' },
|
|
44
|
+
{ name: 'lex-llm-mlx', category: 'ai', description: 'Apple MLX local LLM provider integration' },
|
|
45
|
+
{ name: 'lex-llm-ollama', category: 'ai', description: 'Ollama LLM provider integration' },
|
|
46
|
+
{ name: 'lex-llm-openai', category: 'ai', description: 'OpenAI LLM provider integration' },
|
|
47
|
+
{ name: 'lex-llm-vertex', category: 'ai', description: 'Google Vertex AI hosted LLM provider integration' },
|
|
48
|
+
{ name: 'lex-llm-vllm', category: 'ai', description: 'vLLM OpenAI-compatible provider integration' },
|
|
39
49
|
# agentic
|
|
40
50
|
{ name: 'lex-agentic-affect', category: 'agentic', description: 'Affective state modeling' },
|
|
41
51
|
{ name: 'lex-agentic-attention', category: 'agentic', description: 'Attentional focus and salience' },
|
|
@@ -85,6 +85,8 @@ module Legion
|
|
|
85
85
|
pending.each do |lex_name, new_state|
|
|
86
86
|
existing = model.where(lex_name: lex_name).first
|
|
87
87
|
if existing
|
|
88
|
+
next if existing.respond_to?(:state) && existing.state == new_state.to_s
|
|
89
|
+
|
|
88
90
|
existing.update(state: new_state.to_s, updated_at: now)
|
|
89
91
|
else
|
|
90
92
|
model.insert(lex_name: lex_name, state: new_state.to_s, created_at: now, updated_at: now)
|
|
@@ -72,7 +72,7 @@ module Legion
|
|
|
72
72
|
@messages = {}
|
|
73
73
|
build_settings
|
|
74
74
|
build_transport
|
|
75
|
-
if Legion::Settings[:data][:connected] && data_required?
|
|
75
|
+
if Legion::Settings[:data][:connected] && (data_required? || data_migrations_available?)
|
|
76
76
|
Legion::Logging.debug "[Core] building data for #{name}" if defined?(Legion::Logging)
|
|
77
77
|
build_data
|
|
78
78
|
end
|
|
@@ -91,6 +91,13 @@ module Legion
|
|
|
91
91
|
false
|
|
92
92
|
end
|
|
93
93
|
|
|
94
|
+
def data_migrations_available?
|
|
95
|
+
Dir[File.join(extension_path.to_s, 'data', 'migrations', '*.rb')].any?
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
log.debug "[Core] data migration discovery failed for #{name}: #{e.message}" if defined?(log)
|
|
98
|
+
false
|
|
99
|
+
end
|
|
100
|
+
|
|
94
101
|
def transport_required?
|
|
95
102
|
true
|
|
96
103
|
end
|
|
@@ -6,8 +6,13 @@ module Legion
|
|
|
6
6
|
module Segments
|
|
7
7
|
module_function
|
|
8
8
|
|
|
9
|
+
COMPOUND_SUFFIXES = {
|
|
10
|
+
%w[llm azure foundry] => %w[llm azure_foundry]
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
9
13
|
def derive_segments(gem_name)
|
|
10
|
-
gem_name.delete_prefix('lex-').split('-')
|
|
14
|
+
segments = gem_name.delete_prefix('lex-').split('-')
|
|
15
|
+
COMPOUND_SUFFIXES.fetch(segments, segments)
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
def derive_namespace(gem_name)
|
data/lib/legion/extensions.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'legion/logging'
|
|
3
4
|
require 'legion/extensions/core'
|
|
4
5
|
require 'legion/extensions/catalog'
|
|
5
6
|
require 'legion/extensions/handle_registry'
|
|
@@ -29,11 +30,19 @@ module Legion
|
|
|
29
30
|
find_extensions
|
|
30
31
|
|
|
31
32
|
phases = group_by_phase
|
|
33
|
+
llm_base_entries, llm_extension_entries = extract_llm_extension_entries!(phases)
|
|
34
|
+
llm_phases_loaded = false
|
|
32
35
|
phases.each do |phase_num, entries|
|
|
36
|
+
unless llm_phases_loaded || before_llm_extension_phase?(phase_num)
|
|
37
|
+
load_llm_extension_phases(llm_base_entries, llm_extension_entries)
|
|
38
|
+
llm_phases_loaded = true
|
|
39
|
+
end
|
|
40
|
+
|
|
33
41
|
@pending_actors = Concurrent::Array.new
|
|
34
42
|
load_phase_extensions(phase_num, entries)
|
|
35
43
|
hook_phase_actors(phase_num)
|
|
36
44
|
end
|
|
45
|
+
load_llm_extension_phases(llm_base_entries, llm_extension_entries) unless llm_phases_loaded
|
|
37
46
|
|
|
38
47
|
transition_loaded_extensions(:running)
|
|
39
48
|
Catalog.flush_persisted_transitions
|
|
@@ -350,6 +359,55 @@ module Legion
|
|
|
350
359
|
end.sort_by(&:first)
|
|
351
360
|
end
|
|
352
361
|
|
|
362
|
+
def load_llm_extension_phases(base_entries, extension_entries)
|
|
363
|
+
run_extension_phase(:llm_base, base_entries)
|
|
364
|
+
|
|
365
|
+
Legion::Logging.warn 'lex-llm-* extensions discovered without lex-llm; provider loading may fail' if base_entries.empty? && extension_entries.any?
|
|
366
|
+
|
|
367
|
+
run_extension_phase(:llm_extensions, extension_entries.sort_by { |entry| entry[:gem_name] })
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def before_llm_extension_phase?(phase_num)
|
|
371
|
+
phase_num.is_a?(Numeric) && phase_num < 1
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def run_extension_phase(phase_num, entries)
|
|
375
|
+
return if entries.empty?
|
|
376
|
+
|
|
377
|
+
@pending_actors = Concurrent::Array.new
|
|
378
|
+
load_phase_extensions(phase_num, entries)
|
|
379
|
+
hook_phase_actors(phase_num)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def extract_llm_extension_entries!(phases)
|
|
383
|
+
base_entries = []
|
|
384
|
+
extension_entries = []
|
|
385
|
+
|
|
386
|
+
phases.each do |(_, entries)|
|
|
387
|
+
entries.delete_if do |entry|
|
|
388
|
+
next false unless llm_extension_entry?(entry)
|
|
389
|
+
|
|
390
|
+
if llm_base_extension_entry?(entry)
|
|
391
|
+
base_entries << entry
|
|
392
|
+
else
|
|
393
|
+
extension_entries << entry
|
|
394
|
+
end
|
|
395
|
+
true
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
phases.reject! { |_, entries| entries.empty? }
|
|
399
|
+
|
|
400
|
+
[base_entries, extension_entries]
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def llm_extension_entry?(entry)
|
|
404
|
+
llm_base_extension_entry?(entry) || entry[:gem_name].start_with?('lex-llm-')
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def llm_base_extension_entry?(entry)
|
|
408
|
+
entry[:gem_name] == 'lex-llm'
|
|
409
|
+
end
|
|
410
|
+
|
|
353
411
|
def group_pending_actors
|
|
354
412
|
groups = { once: [], poll: [], every: [], loop: [], subscription: [] }
|
|
355
413
|
@pending_actors.each do |actor|
|
|
@@ -677,12 +735,32 @@ module Legion
|
|
|
677
735
|
end
|
|
678
736
|
|
|
679
737
|
def core_extension_names
|
|
680
|
-
%w[codegen conditioner exec health lex
|
|
738
|
+
%w[codegen conditioner exec health lex log metering node ping scheduler tasker task_pruner telemetry
|
|
681
739
|
transformer].freeze
|
|
682
740
|
end
|
|
683
741
|
|
|
684
742
|
def ai_extension_names
|
|
685
|
-
|
|
743
|
+
native_llm_extension_names
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def native_llm_extension_names
|
|
747
|
+
%w[
|
|
748
|
+
llm
|
|
749
|
+
llm-anthropic
|
|
750
|
+
llm-azure-foundry
|
|
751
|
+
llm-bedrock
|
|
752
|
+
llm-gemini
|
|
753
|
+
llm-ledger
|
|
754
|
+
llm-mlx
|
|
755
|
+
llm-ollama
|
|
756
|
+
llm-openai
|
|
757
|
+
llm-vertex
|
|
758
|
+
llm-vllm
|
|
759
|
+
].freeze
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def legacy_ai_extension_names
|
|
763
|
+
%w[azure-ai bedrock claude foundry gemini llm-gateway ollama openai xai].freeze
|
|
686
764
|
end
|
|
687
765
|
|
|
688
766
|
def service_extension_names
|
|
@@ -700,7 +778,10 @@ module Legion
|
|
|
700
778
|
end
|
|
701
779
|
|
|
702
780
|
def agentic_extension_names
|
|
703
|
-
known_gem_names = (
|
|
781
|
+
known_gem_names = (
|
|
782
|
+
core_extension_names + service_extension_names + other_extension_names +
|
|
783
|
+
ai_extension_names + legacy_ai_extension_names
|
|
784
|
+
).map { |n| "lex-#{n}" }
|
|
704
785
|
Array(@extensions).reject { |entry| known_gem_names.include?(entry[:gem_name]) }.map { |entry| entry[:gem_name] }
|
|
705
786
|
end
|
|
706
787
|
|
|
@@ -7,7 +7,7 @@ module Legion
|
|
|
7
7
|
require_airb_approval: false,
|
|
8
8
|
auto_approve_risk_tiers: %w[low],
|
|
9
9
|
review_required_risk_tiers: %w[medium high critical],
|
|
10
|
-
naming_convention: 'lex-[a-z][a-z0-9_]*',
|
|
10
|
+
naming_convention: 'lex-[a-z][a-z0-9_]*(?:-[a-z][a-z0-9_]*)*',
|
|
11
11
|
deprecation_notice_days: 30
|
|
12
12
|
}.freeze
|
|
13
13
|
|
|
@@ -39,10 +39,11 @@ module Legion
|
|
|
39
39
|
def naming_convention(name:, **_)
|
|
40
40
|
return { check: :naming_convention, status: :skip, details: 'no name' } unless name
|
|
41
41
|
|
|
42
|
-
if name.match?(/\Alex-[a-z][a-z0-9_]*\z/)
|
|
42
|
+
if name.match?(/\Alex-[a-z][a-z0-9_]*(?:-[a-z][a-z0-9_]*)*\z/)
|
|
43
43
|
{ check: :naming_convention, status: :pass, details: name }
|
|
44
44
|
else
|
|
45
|
-
{ check: :naming_convention, status: :fail,
|
|
45
|
+
{ check: :naming_convention, status: :fail,
|
|
46
|
+
details: "#{name} does not match lex-[a-z][a-z0-9_]*(?:-[a-z][a-z0-9_]*)*" }
|
|
46
47
|
end
|
|
47
48
|
end
|
|
48
49
|
|
data/lib/legion/service.rb
CHANGED
|
@@ -451,6 +451,7 @@ module Legion
|
|
|
451
451
|
log.info 'Setting up Legion::LLM'
|
|
452
452
|
require 'legion/llm'
|
|
453
453
|
Legion::Settings.merge_settings('llm', Legion::LLM::Settings.default)
|
|
454
|
+
preload_llm_providers
|
|
454
455
|
Legion::LLM.start
|
|
455
456
|
log.info 'Legion::LLM started'
|
|
456
457
|
rescue LoadError => e
|
|
@@ -460,6 +461,40 @@ module Legion
|
|
|
460
461
|
handle_exception(e, level: :warn, operation: 'service.setup_llm')
|
|
461
462
|
end
|
|
462
463
|
|
|
464
|
+
def preload_llm_providers
|
|
465
|
+
require 'legion/extensions/llm'
|
|
466
|
+
gems = llm_provider_gems
|
|
467
|
+
gems.each do |gem_name, require_path|
|
|
468
|
+
require require_path
|
|
469
|
+
log.debug "[service] loaded #{gem_name}"
|
|
470
|
+
rescue LoadError => e
|
|
471
|
+
log.warn "[service] #{gem_name} failed to load: #{e.message}"
|
|
472
|
+
rescue StandardError => e
|
|
473
|
+
handle_exception(e, level: :warn, operation: "service.preload_llm_provider.#{gem_name}")
|
|
474
|
+
end
|
|
475
|
+
registered = defined?(Legion::LLM::Call::Registry) ? Legion::LLM::Call::Registry.all_instances : []
|
|
476
|
+
log.info "[service] llm providers preloaded gems=#{gems.size} instances=#{registered.size}"
|
|
477
|
+
rescue LoadError => e
|
|
478
|
+
handle_exception(e, level: :warn, operation: 'service.preload_llm_providers', availability: 'lex-llm not installed')
|
|
479
|
+
rescue StandardError => e
|
|
480
|
+
handle_exception(e, level: :warn, operation: 'service.preload_llm_providers')
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def llm_provider_gems
|
|
484
|
+
specs = if defined?(Bundler)
|
|
485
|
+
Bundler.load.specs.map { |s| s.respond_to?(:name) ? s.name : s[:name].to_s }
|
|
486
|
+
else
|
|
487
|
+
Gem::Specification.latest_specs.map(&:name)
|
|
488
|
+
end
|
|
489
|
+
specs.filter_map do |name|
|
|
490
|
+
next unless name.start_with?('lex-llm-') && name != 'lex-llm-ledger'
|
|
491
|
+
|
|
492
|
+
provider_name = name.delete_prefix('lex-llm-').tr('-', '_')
|
|
493
|
+
require_path = "legion/extensions/llm/#{provider_name}"
|
|
494
|
+
[name, require_path]
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
463
498
|
def setup_gaia
|
|
464
499
|
log.info 'Setting up Legion::Gaia'
|
|
465
500
|
require 'legion/gaia'
|
data/lib/legion/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legionio
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.9.
|
|
4
|
+
version: 1.9.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -351,28 +351,28 @@ dependencies:
|
|
|
351
351
|
requirements:
|
|
352
352
|
- - ">="
|
|
353
353
|
- !ruby/object:Gem::Version
|
|
354
|
-
version: 0.
|
|
354
|
+
version: 0.8.47
|
|
355
355
|
type: :runtime
|
|
356
356
|
prerelease: false
|
|
357
357
|
version_requirements: !ruby/object:Gem::Requirement
|
|
358
358
|
requirements:
|
|
359
359
|
- - ">="
|
|
360
360
|
- !ruby/object:Gem::Version
|
|
361
|
-
version: 0.
|
|
361
|
+
version: 0.8.47
|
|
362
362
|
- !ruby/object:Gem::Dependency
|
|
363
363
|
name: legion-tty
|
|
364
364
|
requirement: !ruby/object:Gem::Requirement
|
|
365
365
|
requirements:
|
|
366
366
|
- - ">="
|
|
367
367
|
- !ruby/object:Gem::Version
|
|
368
|
-
version: 0.4
|
|
368
|
+
version: 0.5.4
|
|
369
369
|
type: :runtime
|
|
370
370
|
prerelease: false
|
|
371
371
|
version_requirements: !ruby/object:Gem::Requirement
|
|
372
372
|
requirements:
|
|
373
373
|
- - ">="
|
|
374
374
|
- !ruby/object:Gem::Version
|
|
375
|
-
version: 0.4
|
|
375
|
+
version: 0.5.4
|
|
376
376
|
- !ruby/object:Gem::Dependency
|
|
377
377
|
name: lex-node
|
|
378
378
|
requirement: !ruby/object:Gem::Requirement
|