lex-llm 0.1.2 → 0.1.4

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.
Files changed (165) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +12 -1
  4. data/Gemfile +1 -19
  5. data/README.md +25 -26
  6. data/lex-llm.gemspec +2 -2
  7. data/lib/legion/extensions/llm/agent.rb +366 -0
  8. data/lib/legion/extensions/llm/aliases.rb +42 -0
  9. data/lib/legion/extensions/llm/attachment.rb +229 -0
  10. data/lib/legion/extensions/llm/chat.rb +355 -0
  11. data/lib/legion/extensions/llm/chunk.rb +10 -0
  12. data/lib/legion/extensions/llm/configuration.rb +82 -0
  13. data/lib/legion/extensions/llm/connection.rb +134 -0
  14. data/lib/legion/extensions/llm/content.rb +81 -0
  15. data/lib/legion/extensions/llm/context.rb +33 -0
  16. data/lib/legion/extensions/llm/embedding.rb +33 -0
  17. data/lib/legion/extensions/llm/error.rb +116 -0
  18. data/lib/legion/extensions/llm/image.rb +109 -0
  19. data/lib/legion/extensions/llm/message.rb +111 -0
  20. data/lib/legion/extensions/llm/mime_type.rb +75 -0
  21. data/lib/legion/extensions/llm/model/info.rb +117 -0
  22. data/lib/legion/extensions/llm/model/modalities.rb +26 -0
  23. data/lib/legion/extensions/llm/model/pricing.rb +52 -0
  24. data/lib/legion/extensions/llm/model/pricing_category.rb +50 -0
  25. data/lib/legion/extensions/llm/model/pricing_tier.rb +37 -0
  26. data/lib/legion/extensions/llm/model.rb +11 -0
  27. data/lib/legion/extensions/llm/models.rb +514 -0
  28. data/lib/{lex_llm → legion/extensions/llm}/models_schema.json +1 -1
  29. data/lib/legion/extensions/llm/moderation.rb +60 -0
  30. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +276 -0
  31. data/lib/legion/extensions/llm/provider.rb +337 -0
  32. data/lib/legion/extensions/llm/routing/lane_key.rb +57 -0
  33. data/lib/legion/extensions/llm/routing/model_offering.rb +173 -0
  34. data/lib/legion/extensions/llm/routing.rb +11 -0
  35. data/lib/legion/extensions/llm/stream_accumulator.rb +209 -0
  36. data/lib/legion/extensions/llm/streaming.rb +181 -0
  37. data/lib/legion/extensions/llm/thinking.rb +53 -0
  38. data/lib/legion/extensions/llm/tokens.rb +51 -0
  39. data/lib/legion/extensions/llm/tool.rb +258 -0
  40. data/lib/legion/extensions/llm/tool_call.rb +29 -0
  41. data/lib/legion/extensions/llm/transcription.rb +39 -0
  42. data/lib/legion/extensions/llm/utils.rb +95 -0
  43. data/lib/legion/extensions/llm/version.rb +9 -0
  44. data/lib/legion/extensions/llm.rb +85 -6
  45. metadata +40 -122
  46. data/lib/generators/lex_llm/agent/agent_generator.rb +0 -36
  47. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +0 -6
  48. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  49. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +0 -256
  50. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +0 -38
  51. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +0 -21
  52. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +0 -14
  53. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  54. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +0 -12
  55. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +0 -16
  56. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +0 -31
  57. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +0 -31
  58. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +0 -9
  59. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +0 -27
  60. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +0 -14
  61. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +0 -1
  62. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +0 -13
  63. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +0 -23
  64. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +0 -10
  65. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +0 -2
  66. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +0 -4
  67. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +0 -14
  68. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +0 -13
  69. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +0 -21
  70. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +0 -17
  71. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +0 -40
  72. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +0 -27
  73. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +0 -16
  74. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +0 -29
  75. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +0 -28
  76. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +0 -11
  77. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +0 -25
  78. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +0 -9
  79. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +0 -1
  80. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +0 -8
  81. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +0 -21
  82. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +0 -6
  83. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +0 -2
  84. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +0 -4
  85. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +0 -9
  86. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +0 -7
  87. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +0 -8
  88. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +0 -16
  89. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +0 -15
  90. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +0 -38
  91. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +0 -17
  92. data/lib/generators/lex_llm/generator_helpers.rb +0 -214
  93. data/lib/generators/lex_llm/install/install_generator.rb +0 -109
  94. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  95. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +0 -3
  96. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +0 -7
  97. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +0 -19
  98. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +0 -39
  99. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  100. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +0 -20
  101. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +0 -4
  102. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +0 -3
  103. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +0 -3
  104. data/lib/generators/lex_llm/schema/schema_generator.rb +0 -26
  105. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +0 -2
  106. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +0 -9
  107. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +0 -13
  108. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +0 -13
  109. data/lib/generators/lex_llm/tool/tool_generator.rb +0 -96
  110. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  111. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  112. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  113. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  114. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  115. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  116. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  117. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  118. data/lib/lex_llm/active_record/acts_as.rb +0 -180
  119. data/lib/lex_llm/active_record/acts_as_legacy.rb +0 -503
  120. data/lib/lex_llm/active_record/chat_methods.rb +0 -468
  121. data/lib/lex_llm/active_record/message_methods.rb +0 -131
  122. data/lib/lex_llm/active_record/model_methods.rb +0 -76
  123. data/lib/lex_llm/active_record/payload_helpers.rb +0 -26
  124. data/lib/lex_llm/active_record/tool_call_methods.rb +0 -15
  125. data/lib/lex_llm/agent.rb +0 -365
  126. data/lib/lex_llm/aliases.rb +0 -38
  127. data/lib/lex_llm/attachment.rb +0 -223
  128. data/lib/lex_llm/chat.rb +0 -351
  129. data/lib/lex_llm/chunk.rb +0 -6
  130. data/lib/lex_llm/configuration.rb +0 -81
  131. data/lib/lex_llm/connection.rb +0 -130
  132. data/lib/lex_llm/content.rb +0 -77
  133. data/lib/lex_llm/context.rb +0 -29
  134. data/lib/lex_llm/embedding.rb +0 -29
  135. data/lib/lex_llm/error.rb +0 -112
  136. data/lib/lex_llm/image.rb +0 -105
  137. data/lib/lex_llm/message.rb +0 -107
  138. data/lib/lex_llm/mime_type.rb +0 -71
  139. data/lib/lex_llm/model/info.rb +0 -113
  140. data/lib/lex_llm/model/modalities.rb +0 -22
  141. data/lib/lex_llm/model/pricing.rb +0 -48
  142. data/lib/lex_llm/model/pricing_category.rb +0 -46
  143. data/lib/lex_llm/model/pricing_tier.rb +0 -33
  144. data/lib/lex_llm/model.rb +0 -7
  145. data/lib/lex_llm/models.rb +0 -506
  146. data/lib/lex_llm/moderation.rb +0 -56
  147. data/lib/lex_llm/provider/open_ai_compatible.rb +0 -219
  148. data/lib/lex_llm/provider.rb +0 -278
  149. data/lib/lex_llm/railtie.rb +0 -35
  150. data/lib/lex_llm/routing/lane_key.rb +0 -51
  151. data/lib/lex_llm/routing/model_offering.rb +0 -169
  152. data/lib/lex_llm/routing.rb +0 -7
  153. data/lib/lex_llm/stream_accumulator.rb +0 -203
  154. data/lib/lex_llm/streaming.rb +0 -175
  155. data/lib/lex_llm/thinking.rb +0 -49
  156. data/lib/lex_llm/tokens.rb +0 -47
  157. data/lib/lex_llm/tool.rb +0 -254
  158. data/lib/lex_llm/tool_call.rb +0 -25
  159. data/lib/lex_llm/transcription.rb +0 -35
  160. data/lib/lex_llm/utils.rb +0 -91
  161. data/lib/lex_llm/version.rb +0 -5
  162. data/lib/lex_llm.rb +0 -96
  163. data/lib/tasks/lex_llm.rake +0 -23
  164. /data/lib/{lex_llm → legion/extensions/llm}/aliases.json +0 -0
  165. /data/lib/{lex_llm → legion/extensions/llm}/models.json +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bba21b096146376e92e11aa2a001e7f13cd91e453ecd2907c44cc1c39dcbe3a1
4
- data.tar.gz: ad320998555a6e6f8eda82b0dac1bc376415def07c15228400c60f0638a82b1c
3
+ metadata.gz: acd86ca9d14268a164c9492297186706a93770c93f4e312f45d6e54cec8bd565
4
+ data.tar.gz: b78f52a9e0cebfedb102b7809aaf3affc20440e1fcd68a9f815e00f4a129b440
5
5
  SHA512:
6
- metadata.gz: 7b657bf222fa57ad9bd887d6e964a9bb1664c732b894dc760f33fe5396b44c136a19ae96d859036c63449f307f6eb613ef27862ace25fd634a95dafb9c3b5c96
7
- data.tar.gz: f1799aba3ec971591f68e7cfe6ad144742acd82582af1e44de251d3ada828773e64f2b617be9482f7fc31c15e2f2a22861630d5383e614565465d6ae050e9e5e
6
+ metadata.gz: cb2b53cfc698777af6dbfbd735a8d12c0ed5eb94a3dbea88fb91a4951946ac2c3f42790b316bfa34e9f56f822ce3c1be6a07cdc8298525f8d317895269fb2348
7
+ data.tar.gz: b7392fe404a9bee6f2cde122522016b43f193c4df546a69732f9d238161f61a8a6dcb93b685fc2de5a1f9703ecf2c14c433e1a790039d8489b302afbb8cce2ef
data/.gitignore CHANGED
@@ -1,5 +1,6 @@
1
1
  /.bundle/
2
2
  Gemfile.lock
3
+ *.gem
3
4
  /.yardoc
4
5
  /_yardoc/
5
6
  /coverage/
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4 - 2026-04-28
4
+
5
+ - Add non-live provider readiness metadata for routing without expensive health or model calls by default.
6
+ - Map OpenAI-compatible model listings to normalized capabilities and modalities for routing.
7
+
8
+ ## 0.1.3 - 2026-04-27
9
+
10
+ - Convert the gem to a standard Legion extension runtime under `Legion::Extensions::Llm`.
11
+ - Remove the fork-era compatibility namespace, Rails railtie, generators, rake tasks, dummy app, and ActiveRecord helpers.
12
+ - Move provider-neutral chat, schema, model, routing, streaming, and fleet primitives under `lib/legion/extensions/llm`.
13
+
3
14
  ## 0.1.2 - 2026-04-27
4
15
 
5
16
  - Add a shared OpenAI-compatible provider adapter for `lex-llm-openai`, `lex-llm-vllm`, `lex-llm-mlx`, and other compatible servers.
@@ -13,7 +24,7 @@
13
24
 
14
25
  ## 0.1.0 - 2026-04-26
15
26
 
16
- - Rename the forked base gem to `lex-llm` with `LexLLM` runtime namespaces and `Legion::Extensions::Llm` integration.
27
+ - Rename the forked base gem to `lex-llm` with Legion extension integration.
17
28
  - Add provider-neutral routing metadata for concrete model offerings and shared fleet lane keys.
18
29
  - Use Legion JSON/settings/logging runtime dependencies for shared extension behavior.
19
30
  - Remove the upstream RubyLLM docs site and issue templates from the LegionIO fork.
data/Gemfile CHANGED
@@ -4,21 +4,18 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- group :development do # rubocop:disable Metrics/BlockLength
7
+ group :development do
8
8
  gem 'appraisal'
9
9
  gem 'async', platform: :mri
10
10
  gem 'bundler', '>= 2.0'
11
11
  gem 'colorize'
12
12
  gem 'dotenv'
13
- gem 'ferrum'
14
13
  gem 'flay'
15
- gem 'image_processing', '~> 1.2'
16
14
  gem 'irb'
17
15
  gem 'json-schema'
18
16
  gem 'nokogiri'
19
17
  gem 'overcommit', '>= 0.66'
20
18
  gem 'pry', '>= 0.14'
21
- gem 'rails'
22
19
  gem 'rake', '>= 13.0'
23
20
  gem 'reline'
24
21
  gem 'rspec', '~> 3.12'
@@ -30,21 +27,6 @@ group :development do # rubocop:disable Metrics/BlockLength
30
27
  gem 'simplecov-cobertura'
31
28
  gem 'test-queue'
32
29
 
33
- # database drivers for MRI and JRuby
34
- gem 'activerecord-jdbcsqlite3-adapter', platform: 'jruby'
35
- gem 'jdbc-sqlite3', platform: 'jruby'
36
- gem 'sqlite3', platform: 'mri'
37
-
38
30
  gem 'vcr'
39
31
  gem 'webmock', '~> 3.18'
40
-
41
- # Optional dependency for Vertex AI
42
- gem 'googleauth'
43
-
44
- # Optional dependency for Bedrock
45
- gem 'aws-eventstream'
46
- end
47
-
48
- group :development, :test do
49
- gem 'turbo-rails'
50
32
  end
data/README.md CHANGED
@@ -4,33 +4,28 @@
4
4
 
5
5
  Shared LegionIO framework for LLM provider extensions.
6
6
 
7
- `lex-llm` is the provider-neutral base layer for LegionIO LLM work. It defines the common Ruby API, schema bridge, model metadata, routing structures, request/response helpers, streaming helpers, and Legion extension namespace that concrete provider gems build on.
7
+ `lex-llm` is a standard Legion extension gem. It does not expose a standalone RubyLLM-compatible API, Rails integration, generators, rake tasks, or concrete providers. Its runtime contract is `Legion::Extensions::Llm`, which provider gems extend through nested namespaces such as `Legion::Extensions::Llm::Ollama`.
8
8
 
9
9
  The routing principle is simple: provider is not the routing unit anymore. A concrete model offering is.
10
10
 
11
- That means Legion can reason about:
12
-
13
- - one local Ollama instance with many models
14
- - multiple remote Ollama or vLLM instances
15
- - several Bedrock accounts or regions exposing overlapping Anthropic models
16
- - direct frontier providers such as OpenAI or Anthropic
17
- - fleet workers on MacBooks, GPU servers, or cloud-side proxy nodes
11
+ That lets Legion reason about one local Ollama instance with many models, multiple remote Ollama or vLLM instances, Bedrock accounts in different regions, direct frontier providers, and fleet workers on MacBooks, GPU servers, or cloud-side proxy nodes.
18
12
 
19
13
  ## What This Gem Owns
20
14
 
21
- `lex-llm` provides shared primitives only. Provider-specific behavior belongs in provider gems.
15
+ `lex-llm` provides provider-neutral primitives only. Provider-specific behavior belongs in provider gems.
22
16
 
23
17
  This gem owns:
24
18
 
25
- - `LexLLM`, the shared Ruby API and base provider framework
26
19
  - `Legion::Extensions::Llm`, the Legion extension namespace used by autoloading and settings
27
- - provider-neutral model metadata and capability normalization
28
- - routing structures such as `LexLLM::Routing::ModelOffering`
20
+ - provider-neutral request, response, message, content, token, and tool objects
21
+ - schema bridging through `Legion::Extensions::Llm::Schema`
22
+ - model metadata and capability normalization
23
+ - routing structures such as `Legion::Extensions::Llm::Routing::ModelOffering`
29
24
  - fleet lane key generation for shared RabbitMQ work lanes
30
- - common chat, embedding, tool, streaming, ActiveRecord, and schema helpers
25
+ - shared chat, embedding, moderation, image, transcription, streaming, and OpenAI-compatible adapter helpers
31
26
  - shared runtime dependencies such as `legion-json`, `legion-settings`, and `legion-logging`
32
27
 
33
- Concrete provider gems should depend on this gem and implement the provider-specific transport, authentication, model discovery, request translation, and response translation.
28
+ Concrete provider gems should depend on this gem and implement the provider-specific transport, authentication, model discovery, request translation, response translation, and health checks.
34
29
 
35
30
  Expected provider gems include:
36
31
 
@@ -53,32 +48,34 @@ gem 'lex-llm'
53
48
  Provider extensions should declare `lex-llm` as a gemspec dependency:
54
49
 
55
50
  ```ruby
56
- spec.add_dependency 'lex-llm', '>= 0.1.0'
51
+ spec.add_dependency 'lex-llm', '>= 0.1.4'
57
52
  ```
58
53
 
59
54
  For local development across LegionIO repos, prefer a local path override in the app or test `Gemfile`, not a permanent git dependency in the gemspec.
60
55
 
61
- ## Namespaces
56
+ ## Namespace
62
57
 
63
- This gem exposes two runtime namespaces:
58
+ Load the extension through the Legion namespace:
64
59
 
65
- - `LexLLM` for shared Ruby classes, provider primitives, schemas, and helpers
66
- - `Legion::Extensions::Llm` for LegionIO extension loading and default settings
60
+ ```ruby
61
+ require 'legion/extensions/llm'
62
+ ```
67
63
 
68
64
  Provider gems must use nested Legion extension namespaces so LegionIO autoloading can find them consistently.
69
65
 
70
66
  Example for `lex-llm-ollama`:
71
67
 
72
68
  ```ruby
73
- require 'legion/extensions/llm/ollama'
69
+ require 'legion/extensions/llm'
74
70
 
75
71
  module Legion
76
72
  module Extensions
77
73
  module Llm
78
74
  module Ollama
79
75
  def self.default_settings
80
- Legion::Extensions::Llm.default_settings.merge(
81
- provider_family: :ollama
76
+ Legion::Extensions::Llm.provider_settings(
77
+ family: :ollama,
78
+ instance: { base_url: 'http://localhost:11434' }
82
79
  )
83
80
  end
84
81
  end
@@ -92,7 +89,7 @@ end
92
89
  A model offering describes one concrete model made available by one provider instance. It is the base unit for routing, filtering, fleet lane creation, health, policy, and cost decisions.
93
90
 
94
91
  ```ruby
95
- offering = LexLLM::Routing::ModelOffering.new(
92
+ offering = Legion::Extensions::Llm::Routing::ModelOffering.new(
96
93
  provider_family: :ollama,
97
94
  instance_id: :macbook_m4_max,
98
95
  transport: :local,
@@ -152,7 +149,7 @@ offering.lane_key
152
149
  Embedding lanes omit context size:
153
150
 
154
151
  ```ruby
155
- LexLLM::Routing::ModelOffering.new(
152
+ Legion::Extensions::Llm::Routing::ModelOffering.new(
156
153
  provider_family: :ollama,
157
154
  instance_id: :gpu_embed_01,
158
155
  transport: :rabbitmq,
@@ -214,7 +211,7 @@ Provider gems can build a complete provider settings hash without duplicating me
214
211
  Legion::Extensions::Llm.provider_settings(
215
212
  family: :ollama,
216
213
  instance: {
217
- base_url: "http://localhost:11434",
214
+ base_url: 'http://localhost:11434',
218
215
  fleet: { enabled: true, consumer_priority: 10 }
219
216
  }
220
217
  )
@@ -236,12 +233,14 @@ At minimum, a provider extension should define:
236
233
 
237
234
  Provider extensions should avoid duplicating shared classes, schema logic, fleet lane construction, JSON handling, or common request/response objects.
238
235
 
236
+ All providers inherit `#readiness(live: false)`, which returns configured state, provider locality, API base, endpoint helpers, and non-live health metadata without probing remote services. Providers with a cheap health endpoint can pass `live: true` to include that endpoint response. OpenAI-compatible providers also inherit shared model-list parsing that maps discovered models into normalized capabilities and modalities for Legion routing.
237
+
239
238
  ## Schema Status
240
239
 
241
240
  `lex-llm` still depends on `ruby_llm-schema` because the current schema bridge exposes:
242
241
 
243
242
  ```ruby
244
- LexLLM::Schema
243
+ Legion::Extensions::Llm::Schema
245
244
  ```
246
245
 
247
246
  as:
data/lex-llm.gemspec CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'lib/lex_llm/version'
3
+ require_relative 'lib/legion/extensions/llm/version'
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = 'lex-llm'
7
- spec.version = LexLLM::VERSION
7
+ spec.version = Legion::Extensions::Llm::VERSION
8
8
  spec.authors = ['LegionIO', 'Carmine Paolino']
9
9
  spec.email = ['matthewdiverson@gmail.com']
10
10
 
@@ -0,0 +1,366 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'forwardable'
5
+ require 'pathname'
6
+ require 'ruby_llm/schema'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Llm
11
+ # Base class for simple, class-configured agents.
12
+ class Agent
13
+ extend Forwardable
14
+ include Enumerable
15
+
16
+ class << self
17
+ def inherited(subclass)
18
+ super
19
+ subclass.instance_variable_set(:@chat_kwargs, (@chat_kwargs || {}).dup)
20
+ subclass.instance_variable_set(:@tools, (@tools || []).dup)
21
+ subclass.instance_variable_set(:@instructions, @instructions)
22
+ subclass.instance_variable_set(:@temperature, @temperature)
23
+ subclass.instance_variable_set(:@thinking, @thinking)
24
+ subclass.instance_variable_set(:@params, (@params || {}).dup)
25
+ subclass.instance_variable_set(:@headers, (@headers || {}).dup)
26
+ subclass.instance_variable_set(:@schema, @schema)
27
+ subclass.instance_variable_set(:@context, @context)
28
+ subclass.instance_variable_set(:@chat_model, @chat_model)
29
+ subclass.instance_variable_set(:@input_names, (@input_names || []).dup)
30
+ end
31
+
32
+ def model(model_id = nil, **options)
33
+ options[:model] = model_id unless model_id.nil?
34
+ @chat_kwargs = options
35
+ end
36
+
37
+ def tools(*tools, &block)
38
+ return @tools || [] if tools.empty? && !block_given?
39
+
40
+ @tools = block_given? ? block : tools.flatten
41
+ end
42
+
43
+ def instructions(text = nil, **prompt_locals, &block)
44
+ if text.nil? && prompt_locals.empty? && !block_given?
45
+ @instructions ||= { prompt: 'instructions', locals: {} }
46
+ return @instructions
47
+ end
48
+
49
+ @instructions = block || text || { prompt: 'instructions', locals: prompt_locals }
50
+ end
51
+
52
+ def temperature(value = nil)
53
+ return @temperature if value.nil?
54
+
55
+ @temperature = value
56
+ end
57
+
58
+ def thinking(effort: nil, budget: nil)
59
+ return @thinking if effort.nil? && budget.nil?
60
+
61
+ @thinking = { effort: effort, budget: budget }
62
+ end
63
+
64
+ def params(**params, &block)
65
+ return @params || {} if params.empty? && !block_given?
66
+
67
+ @params = block_given? ? block : params
68
+ end
69
+
70
+ def headers(**headers, &block)
71
+ return @headers || {} if headers.empty? && !block_given?
72
+
73
+ @headers = block_given? ? block : headers
74
+ end
75
+
76
+ def schema(value = nil, &block)
77
+ return @schema if value.nil? && !block_given?
78
+
79
+ @schema = block_given? ? block : value
80
+ end
81
+
82
+ def context(value = nil)
83
+ return @context if value.nil?
84
+
85
+ @context = value
86
+ end
87
+
88
+ def chat_model(value = nil)
89
+ return @chat_model if value.nil?
90
+
91
+ @chat_model = value
92
+ remove_instance_variable(:@resolved_chat_model) if instance_variable_defined?(:@resolved_chat_model)
93
+ end
94
+
95
+ def inputs(*names)
96
+ return @input_names || [] if names.empty?
97
+
98
+ @input_names = names.flatten.map(&:to_sym)
99
+ end
100
+
101
+ def chat_kwargs
102
+ @chat_kwargs || {}
103
+ end
104
+
105
+ def chat(**kwargs)
106
+ input_values, chat_options = partition_inputs(kwargs)
107
+ chat = Legion::Extensions::Llm.chat(**chat_kwargs, **chat_options)
108
+ apply_configuration(chat, input_values:, persist_instructions: true)
109
+ chat
110
+ end
111
+
112
+ def create(**)
113
+ with_chat_record(:create, **)
114
+ end
115
+
116
+ def create!(**)
117
+ with_chat_record(:create!, **)
118
+ end
119
+
120
+ def find(id, **kwargs)
121
+ raise ArgumentError, 'chat_model must be configured to use find' unless resolved_chat_model
122
+
123
+ input_values, = partition_inputs(kwargs)
124
+ record = resolved_chat_model.find(id)
125
+ apply_configuration(record, input_values:, persist_instructions: false)
126
+
127
+ record
128
+ end
129
+
130
+ def sync_instructions!(chat_or_id, **kwargs)
131
+ raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
132
+
133
+ input_values, = partition_inputs(kwargs)
134
+ record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
135
+ apply_assume_model_exists(record)
136
+ runtime = runtime_context(chat: record, inputs: input_values)
137
+ instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
138
+ return record if instructions_value.nil?
139
+
140
+ record.with_instructions(instructions_value)
141
+ record
142
+ end
143
+
144
+ def render_prompt(name, chat:, inputs:, locals:)
145
+ path = prompt_path_for(name)
146
+ unless File.exist?(path)
147
+ raise Legion::Extensions::Llm::PromptNotFoundError,
148
+ "Prompt file not found for #{self}: #{path}. Create the file or use inline instructions."
149
+ end
150
+
151
+ resolved_locals = resolve_prompt_locals(locals, runtime: runtime_context(chat:, inputs:), chat:, inputs:)
152
+ ERB.new(File.read(path)).result_with_hash(resolved_locals)
153
+ end
154
+
155
+ private
156
+
157
+ def with_chat_record(method_name, **kwargs)
158
+ raise ArgumentError, 'chat_model must be configured to use create/create!' unless resolved_chat_model
159
+
160
+ input_values, chat_options = partition_inputs(kwargs)
161
+ record = resolved_chat_model.public_send(method_name, **chat_kwargs, **chat_options)
162
+ apply_configuration(record, input_values:, persist_instructions: true) if record
163
+ record
164
+ end
165
+
166
+ def apply_configuration(chat_object, input_values:, persist_instructions:)
167
+ runtime = runtime_context(chat: chat_object, inputs: input_values)
168
+ llm_chat = llm_chat_for(chat_object)
169
+
170
+ apply_context(llm_chat)
171
+ apply_instructions(chat_object, runtime, inputs: input_values, persist: persist_instructions)
172
+ apply_tools(llm_chat, runtime)
173
+ apply_temperature(llm_chat)
174
+ apply_thinking(llm_chat)
175
+ apply_params(llm_chat, runtime)
176
+ apply_headers(llm_chat, runtime)
177
+ apply_schema(llm_chat, runtime)
178
+ end
179
+
180
+ def apply_context(llm_chat)
181
+ llm_chat.with_context(context) if context
182
+ end
183
+
184
+ def apply_instructions(chat_object, runtime, inputs:, persist:)
185
+ value = resolved_instructions_value(chat_object, runtime, inputs:)
186
+ return if value.nil?
187
+
188
+ target = instruction_target(chat_object, persist:)
189
+ return target.with_runtime_instructions(value) if use_runtime_instructions?(target, persist:)
190
+
191
+ target.with_instructions(value)
192
+ end
193
+
194
+ def apply_tools(llm_chat, runtime)
195
+ tools_to_apply = Array(evaluate(tools, runtime))
196
+ llm_chat.with_tools(*tools_to_apply) unless tools_to_apply.empty?
197
+ end
198
+
199
+ def apply_temperature(llm_chat)
200
+ llm_chat.with_temperature(temperature) unless temperature.nil?
201
+ end
202
+
203
+ def apply_thinking(llm_chat)
204
+ llm_chat.with_thinking(**thinking) if thinking
205
+ end
206
+
207
+ def apply_params(llm_chat, runtime)
208
+ value = evaluate(params, runtime)
209
+ llm_chat.with_params(**value) if value && !value.empty?
210
+ end
211
+
212
+ def apply_headers(llm_chat, runtime)
213
+ value = evaluate(headers, runtime)
214
+ llm_chat.with_headers(**value) if value && !value.empty?
215
+ end
216
+
217
+ def apply_schema(llm_chat, runtime)
218
+ value = resolved_schema_value(runtime)
219
+ llm_chat.with_schema(value) if value
220
+ end
221
+
222
+ def resolved_schema_value(runtime)
223
+ value = schema
224
+ return value unless value.is_a?(Proc)
225
+
226
+ evaluate(value, runtime)
227
+ rescue NoMethodError => e
228
+ raise unless e.receiver.equal?(runtime)
229
+
230
+ Legion::Extensions::Llm::Schema.create(&value)
231
+ end
232
+
233
+ def llm_chat_for(chat_object)
234
+ apply_assume_model_exists(chat_object)
235
+ chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
236
+ end
237
+
238
+ def apply_assume_model_exists(chat_object)
239
+ return unless chat_kwargs.key?(:assume_model_exists) &&
240
+ resolved_chat_model &&
241
+ chat_object.is_a?(resolved_chat_model)
242
+
243
+ chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
244
+ end
245
+
246
+ def evaluate(value, runtime)
247
+ value.is_a?(Proc) ? runtime.instance_exec(&value) : value
248
+ end
249
+
250
+ def resolved_instructions_value(chat_object, runtime, inputs:)
251
+ value = evaluate(@instructions, runtime)
252
+ return value unless prompt_instruction?(value)
253
+
254
+ runtime.prompt(
255
+ value[:prompt],
256
+ **resolve_prompt_locals(value[:locals] || {}, runtime:, chat: chat_object, inputs:)
257
+ )
258
+ end
259
+
260
+ def prompt_instruction?(value)
261
+ value.is_a?(Hash) && value[:prompt]
262
+ end
263
+
264
+ def instruction_target(chat_object, persist:)
265
+ if persist || !chat_object.respond_to?(:to_llm)
266
+ chat_object
267
+ else
268
+ runtime_instruction_target(chat_object)
269
+ end
270
+ end
271
+
272
+ def runtime_instruction_target(chat_object)
273
+ return chat_object if chat_object.respond_to?(:with_runtime_instructions)
274
+
275
+ chat_object.to_llm
276
+ end
277
+
278
+ def use_runtime_instructions?(target, persist:)
279
+ !persist && target.respond_to?(:with_runtime_instructions)
280
+ end
281
+
282
+ def resolve_prompt_locals(locals, runtime:, chat:, inputs:)
283
+ base = { chat: chat }.merge(inputs)
284
+ evaluated = locals.each_with_object({}) do |(key, value), acc|
285
+ acc[key.to_sym] = value.is_a?(Proc) ? runtime.instance_exec(&value) : value
286
+ end
287
+ base.merge(evaluated)
288
+ end
289
+
290
+ def partition_inputs(kwargs)
291
+ input_values = {}
292
+ chat_options = {}
293
+
294
+ kwargs.each do |key, value|
295
+ symbolized_key = key.to_sym
296
+ if inputs.include?(symbolized_key)
297
+ input_values[symbolized_key] = value
298
+ else
299
+ chat_options[symbolized_key] = value
300
+ end
301
+ end
302
+
303
+ [input_values, chat_options]
304
+ end
305
+
306
+ def runtime_context(chat:, inputs:)
307
+ agent_class = self
308
+ Object.new.tap do |runtime|
309
+ runtime.define_singleton_method(:chat) { chat }
310
+ runtime.define_singleton_method(:prompt) do |name, **locals|
311
+ agent_class.render_prompt(name, chat:, inputs:, locals:)
312
+ end
313
+
314
+ inputs.each do |name, value|
315
+ runtime.define_singleton_method(name) { value }
316
+ end
317
+ end
318
+ end
319
+
320
+ def prompt_path_for(name)
321
+ filename = name.to_s
322
+ filename += '.txt.erb' unless filename.end_with?('.txt.erb')
323
+ prompt_root.join(prompt_agent_path, filename)
324
+ end
325
+
326
+ def prompt_agent_path
327
+ class_name = name || 'agent'
328
+ class_name.gsub('::', '/')
329
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
330
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
331
+ .tr('-', '_')
332
+ .downcase
333
+ end
334
+
335
+ def prompt_root
336
+ Pathname.new(Dir.pwd).join('app/prompts')
337
+ end
338
+
339
+ def resolved_chat_model
340
+ return @resolved_chat_model if defined?(@resolved_chat_model)
341
+
342
+ @resolved_chat_model = case @chat_model
343
+ when String then Object.const_get(@chat_model)
344
+ else @chat_model
345
+ end
346
+ end
347
+ end
348
+
349
+ def initialize(chat: nil, inputs: nil, persist_instructions: true, **kwargs)
350
+ input_values, chat_options = self.class.send(:partition_inputs, kwargs)
351
+ @chat = chat || Legion::Extensions::Llm.chat(**self.class.chat_kwargs, **chat_options)
352
+ self.class.send(:apply_configuration, @chat, input_values: input_values.merge(inputs || {}),
353
+ persist_instructions:)
354
+ end
355
+
356
+ attr_reader :chat
357
+
358
+ def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say,
359
+ :with_tool, :with_tools,
360
+ :with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
361
+ :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
362
+ :add_message, :reset_messages!
363
+ end
364
+ end
365
+ end
366
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Llm
6
+ # Manages model aliases for provider-specific versions
7
+ class Aliases
8
+ class << self
9
+ def resolve(model_id, provider = nil)
10
+ return model_id unless aliases[model_id]
11
+
12
+ if provider
13
+ aliases[model_id][provider.to_s] || model_id
14
+ else
15
+ aliases[model_id].values.first || model_id
16
+ end
17
+ end
18
+
19
+ def aliases
20
+ @aliases ||= load_aliases
21
+ end
22
+
23
+ def aliases_file
24
+ File.expand_path('aliases.json', __dir__)
25
+ end
26
+
27
+ def load_aliases
28
+ if File.exist?(aliases_file)
29
+ Legion::JSON.parse(File.read(aliases_file), symbolize_names: false)
30
+ else
31
+ {}
32
+ end
33
+ end
34
+
35
+ def reload!
36
+ @aliases = load_aliases
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end