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 +4 -4
- data/CHANGELOG.md +140 -0
- data/Gemfile +46 -7
- data/README.md +9 -7
- data/legionio.gemspec +3 -3
- data/lib/legion/api/extensions.rb +18 -1
- data/lib/legion/api/llm.rb +87 -145
- 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 +63 -10
- 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/actors/subscription.rb +28 -8
- data/lib/legion/extensions/builders/actors.rb +2 -0
- data/lib/legion/extensions/builders/hooks.rb +1 -0
- data/lib/legion/extensions/builders/runners.rb +15 -0
- data/lib/legion/extensions/catalog/available.rb +10 -1
- data/lib/legion/extensions/core.rb +34 -22
- data/lib/legion/extensions/helpers/segments.rb +6 -1
- data/lib/legion/extensions.rb +125 -15
- data/lib/legion/registry/governance.rb +1 -1
- data/lib/legion/registry/security_scanner.rb +3 -2
- data/lib/legion/service.rb +37 -2
- data/lib/legion/version.rb +1 -1
- metadata +7 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e6be612db3e6de32b7c4baf82b6a38aac19a0a0bb53d198f736ae4d2d638829d
|
|
4
|
+
data.tar.gz: bf294cc20dbb5701b7bd4f5fd474dbc27d6eae37d2c084cf7b06ec2cbb99f2dc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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-
|
|
15
|
-
|
|
16
|
-
gem '
|
|
17
|
-
|
|
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
|
[](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
|
+
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 +
|
|
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.
|
|
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.
|
|
66
|
-
spec.add_dependency 'legion-tty', '>= 0.4
|
|
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
|
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
|
|
@@ -28,90 +24,84 @@ module Legion
|
|
|
28
24
|
Legion::Cache.connected?
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
define_method(:
|
|
32
|
-
defined?(Legion::
|
|
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(:
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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/
|
|
311
|
-
|
|
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::
|
|
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::
|
|
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
|
-
|
|
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:
|
|
486
|
-
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
|
-
|
|
493
|
-
halt 503, json_error('gateway_unavailable', 'LLM gateway is not loaded', status_code: 503)
|
|
494
|
-
end
|
|
438
|
+
require_provider_inventory!
|
|
495
439
|
|
|
496
|
-
|
|
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
|
|