lex-llm 0.1.1
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 +7 -0
- data/.github/CODEOWNERS +7 -0
- data/.github/dependabot.yml +18 -0
- data/.github/workflows/ci.yml +16 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +42 -0
- data/CHANGELOG.md +15 -0
- data/Gemfile +50 -0
- data/LICENSE +21 -0
- data/README.md +279 -0
- data/lex-llm.gemspec +43 -0
- data/lib/generators/lex_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/lex_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +256 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +38 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +28 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +25 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +7 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +15 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +38 -0
- data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +17 -0
- data/lib/generators/lex_llm/generator_helpers.rb +214 -0
- data/lib/generators/lex_llm/install/install_generator.rb +109 -0
- data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
- data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +3 -0
- data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +7 -0
- data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +19 -0
- data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +39 -0
- data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +21 -0
- data/lib/generators/lex_llm/install/templates/initializer.rb.tt +20 -0
- data/lib/generators/lex_llm/install/templates/message_model.rb.tt +4 -0
- data/lib/generators/lex_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +3 -0
- data/lib/generators/lex_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/lex_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/lex_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/lex_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +122 -0
- data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
- data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
- data/lib/legion/extensions/llm/provider_settings.rb +49 -0
- data/lib/legion/extensions/llm/transport/fleet_lane.rb +70 -0
- data/lib/legion/extensions/llm.rb +50 -0
- data/lib/lex_llm/active_record/acts_as.rb +180 -0
- data/lib/lex_llm/active_record/acts_as_legacy.rb +503 -0
- data/lib/lex_llm/active_record/chat_methods.rb +468 -0
- data/lib/lex_llm/active_record/message_methods.rb +131 -0
- data/lib/lex_llm/active_record/model_methods.rb +76 -0
- data/lib/lex_llm/active_record/payload_helpers.rb +26 -0
- data/lib/lex_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/lex_llm/agent.rb +365 -0
- data/lib/lex_llm/aliases.json +436 -0
- data/lib/lex_llm/aliases.rb +38 -0
- data/lib/lex_llm/attachment.rb +223 -0
- data/lib/lex_llm/chat.rb +351 -0
- data/lib/lex_llm/chunk.rb +6 -0
- data/lib/lex_llm/configuration.rb +81 -0
- data/lib/lex_llm/connection.rb +130 -0
- data/lib/lex_llm/content.rb +77 -0
- data/lib/lex_llm/context.rb +29 -0
- data/lib/lex_llm/embedding.rb +29 -0
- data/lib/lex_llm/error.rb +112 -0
- data/lib/lex_llm/image.rb +105 -0
- data/lib/lex_llm/message.rb +107 -0
- data/lib/lex_llm/mime_type.rb +71 -0
- data/lib/lex_llm/model/info.rb +113 -0
- data/lib/lex_llm/model/modalities.rb +22 -0
- data/lib/lex_llm/model/pricing.rb +48 -0
- data/lib/lex_llm/model/pricing_category.rb +46 -0
- data/lib/lex_llm/model/pricing_tier.rb +33 -0
- data/lib/lex_llm/model.rb +7 -0
- data/lib/lex_llm/models.json +57241 -0
- data/lib/lex_llm/models.rb +506 -0
- data/lib/lex_llm/models_schema.json +168 -0
- data/lib/lex_llm/moderation.rb +56 -0
- data/lib/lex_llm/provider.rb +278 -0
- data/lib/lex_llm/railtie.rb +35 -0
- data/lib/lex_llm/routing/lane_key.rb +51 -0
- data/lib/lex_llm/routing/model_offering.rb +169 -0
- data/lib/lex_llm/routing.rb +7 -0
- data/lib/lex_llm/stream_accumulator.rb +203 -0
- data/lib/lex_llm/streaming.rb +175 -0
- data/lib/lex_llm/thinking.rb +49 -0
- data/lib/lex_llm/tokens.rb +47 -0
- data/lib/lex_llm/tool.rb +254 -0
- data/lib/lex_llm/tool_call.rb +25 -0
- data/lib/lex_llm/transcription.rb +35 -0
- data/lib/lex_llm/utils.rb +91 -0
- data/lib/lex_llm/version.rb +5 -0
- data/lib/lex_llm.rb +95 -0
- data/lib/tasks/lex_llm.rake +23 -0
- metadata +349 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
# Identify potentially harmful content in text.
|
|
5
|
+
# https://platform.openai.com/docs/guides/moderation
|
|
6
|
+
class Moderation
|
|
7
|
+
attr_reader :id, :model, :results
|
|
8
|
+
|
|
9
|
+
def initialize(id:, model:, results:)
|
|
10
|
+
@id = id
|
|
11
|
+
@model = model
|
|
12
|
+
@results = results
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.moderate(input,
|
|
16
|
+
model: nil,
|
|
17
|
+
provider: nil,
|
|
18
|
+
assume_model_exists: false,
|
|
19
|
+
context: nil)
|
|
20
|
+
config = context&.config || LexLLM.config
|
|
21
|
+
model ||= config.default_moderation_model
|
|
22
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
23
|
+
config: config)
|
|
24
|
+
model_id = model.id
|
|
25
|
+
|
|
26
|
+
provider_instance.moderate(input, model: model_id)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Convenience method to get content from moderation result
|
|
30
|
+
def content
|
|
31
|
+
results
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if any content was flagged
|
|
35
|
+
def flagged?
|
|
36
|
+
results.any? { |result| result['flagged'] }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Get all flagged categories across all results
|
|
40
|
+
def flagged_categories
|
|
41
|
+
results.flat_map do |result|
|
|
42
|
+
result['categories']&.select { |_category, flagged| flagged }&.keys || []
|
|
43
|
+
end.uniq
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get category scores for the first result (most common case)
|
|
47
|
+
def category_scores
|
|
48
|
+
results.first&.dig('category_scores') || {}
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Get categories for the first result (most common case)
|
|
52
|
+
def categories
|
|
53
|
+
results.first&.dig('categories') || {}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
# Base class for LLM providers.
|
|
5
|
+
class Provider
|
|
6
|
+
include Streaming
|
|
7
|
+
|
|
8
|
+
attr_reader :config, :connection
|
|
9
|
+
|
|
10
|
+
def initialize(config)
|
|
11
|
+
@config = config
|
|
12
|
+
ensure_configured!
|
|
13
|
+
@connection = Connection.new(self, @config)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def api_base
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def headers
|
|
21
|
+
{}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def slug
|
|
25
|
+
self.class.slug
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def name
|
|
29
|
+
self.class.name
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def capabilities
|
|
33
|
+
self.class.capabilities
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def configuration_requirements
|
|
37
|
+
self.class.configuration_requirements
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# rubocop:disable Metrics/ParameterLists
|
|
41
|
+
def complete(messages, tools:, temperature:, model:, params: {}, headers: {}, schema: nil, thinking: nil,
|
|
42
|
+
tool_prefs: nil, &)
|
|
43
|
+
normalized_temperature = maybe_normalize_temperature(temperature, model)
|
|
44
|
+
|
|
45
|
+
payload = Utils.deep_merge(
|
|
46
|
+
render_payload(
|
|
47
|
+
messages,
|
|
48
|
+
tools: tools,
|
|
49
|
+
tool_prefs: tool_prefs,
|
|
50
|
+
temperature: normalized_temperature,
|
|
51
|
+
model: model,
|
|
52
|
+
stream: block_given?,
|
|
53
|
+
schema: schema,
|
|
54
|
+
thinking: thinking
|
|
55
|
+
),
|
|
56
|
+
params
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
if block_given?
|
|
60
|
+
stream_response @connection, payload, headers, &
|
|
61
|
+
else
|
|
62
|
+
sync_response @connection, payload, headers
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
# rubocop:enable Metrics/ParameterLists
|
|
66
|
+
|
|
67
|
+
def list_models
|
|
68
|
+
response = @connection.get models_url
|
|
69
|
+
parse_list_models_response response, slug, capabilities
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def embed(text, model:, dimensions:)
|
|
73
|
+
payload = render_embedding_payload(text, model:, dimensions:)
|
|
74
|
+
response = @connection.post(embedding_url(model:), payload)
|
|
75
|
+
parse_embedding_response(response, model:, text:)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def moderate(input, model:)
|
|
79
|
+
payload = render_moderation_payload(input, model:)
|
|
80
|
+
response = @connection.post moderation_url, payload
|
|
81
|
+
parse_moderation_response(response, model:)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def paint(prompt, model:, size:, with: nil, mask: nil, params: {}) # rubocop:disable Metrics/ParameterLists
|
|
85
|
+
validate_paint_inputs!(with:, mask:)
|
|
86
|
+
payload = render_image_payload(prompt, model:, size:, with:, mask:, params:)
|
|
87
|
+
response = @connection.post images_url(with:, mask:), payload
|
|
88
|
+
parse_image_response(response, model:)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def transcribe(audio_file, model:, language:, **)
|
|
92
|
+
file_part = build_audio_file_part(audio_file)
|
|
93
|
+
payload = render_transcription_payload(file_part, model:, language:, **)
|
|
94
|
+
response = @connection.post transcription_url, payload
|
|
95
|
+
parse_transcription_response(response, model:)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def configured?
|
|
99
|
+
configuration_requirements.all? { |req| @config.send(req) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def local?
|
|
103
|
+
self.class.local?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def remote?
|
|
107
|
+
self.class.remote?
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def assume_models_exist?
|
|
111
|
+
self.class.assume_models_exist?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_error(response)
|
|
115
|
+
return if response.body.empty?
|
|
116
|
+
|
|
117
|
+
body = try_parse_json(response.body)
|
|
118
|
+
case body
|
|
119
|
+
when Hash
|
|
120
|
+
error = body['error']
|
|
121
|
+
return error if error.is_a?(String)
|
|
122
|
+
|
|
123
|
+
body.dig('error', 'message')
|
|
124
|
+
when Array
|
|
125
|
+
body.map do |part|
|
|
126
|
+
error = part['error']
|
|
127
|
+
error.is_a?(String) ? error : part.dig('error', 'message')
|
|
128
|
+
end.join('. ')
|
|
129
|
+
else
|
|
130
|
+
body
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def format_messages(messages)
|
|
135
|
+
messages.map do |msg|
|
|
136
|
+
{
|
|
137
|
+
role: msg.role.to_s,
|
|
138
|
+
content: msg.content
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def format_tool_calls(_tool_calls)
|
|
144
|
+
nil
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def parse_tool_calls(_tool_calls)
|
|
148
|
+
nil
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
class << self
|
|
152
|
+
def name
|
|
153
|
+
to_s.split('::').last
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def slug
|
|
157
|
+
name.downcase
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def capabilities
|
|
161
|
+
nil
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def configuration_requirements
|
|
165
|
+
[]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def configuration_options
|
|
169
|
+
[]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def local?
|
|
173
|
+
false
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def remote?
|
|
177
|
+
!local?
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def assume_models_exist?
|
|
181
|
+
false
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def resolve_model_id(model_id, config: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
185
|
+
model_id
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def configured?(config)
|
|
189
|
+
configuration_requirements.all? { |req| config.send(req) }
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def register(name, provider_class)
|
|
193
|
+
providers[name.to_sym] = provider_class
|
|
194
|
+
LexLLM::Configuration.register_provider_options(provider_class.configuration_options)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def resolve(name)
|
|
198
|
+
return nil if name.nil?
|
|
199
|
+
|
|
200
|
+
providers[name.to_sym]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def for(model)
|
|
204
|
+
model_info = Models.find(model)
|
|
205
|
+
resolve model_info.provider
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def providers
|
|
209
|
+
@providers ||= {}
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def local_providers
|
|
213
|
+
providers.select { |_slug, provider_class| provider_class.local? }
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def remote_providers
|
|
217
|
+
providers.select { |_slug, provider_class| provider_class.remote? }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def configured_providers(config)
|
|
221
|
+
providers.select do |_slug, provider_class|
|
|
222
|
+
provider_class.configured?(config)
|
|
223
|
+
end.values
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def configured_remote_providers(config)
|
|
227
|
+
providers.select do |_slug, provider_class|
|
|
228
|
+
provider_class.remote? && provider_class.configured?(config)
|
|
229
|
+
end.values
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
private
|
|
234
|
+
|
|
235
|
+
def validate_paint_inputs!(with:, mask:)
|
|
236
|
+
return if with.nil? && mask.nil?
|
|
237
|
+
|
|
238
|
+
raise UnsupportedAttachmentError, "#{name} does not support image references in paint"
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def build_audio_file_part(file_path)
|
|
242
|
+
expanded_path = File.expand_path(file_path)
|
|
243
|
+
mime_type = Marcel::MimeType.for(Pathname.new(expanded_path))
|
|
244
|
+
|
|
245
|
+
Faraday::Multipart::FilePart.new(
|
|
246
|
+
expanded_path,
|
|
247
|
+
mime_type,
|
|
248
|
+
File.basename(expanded_path)
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def try_parse_json(maybe_json)
|
|
253
|
+
return maybe_json unless maybe_json.is_a?(String)
|
|
254
|
+
|
|
255
|
+
Legion::JSON.parse(maybe_json, symbolize_names: false)
|
|
256
|
+
rescue Legion::JSON::ParseError
|
|
257
|
+
maybe_json
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def ensure_configured!
|
|
261
|
+
missing = configuration_requirements.reject { |req| @config.send(req) }
|
|
262
|
+
return if missing.empty?
|
|
263
|
+
|
|
264
|
+
raise ConfigurationError, "Missing configuration for #{name}: #{missing.join(', ')}"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def maybe_normalize_temperature(temperature, _model)
|
|
268
|
+
temperature
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def sync_response(connection, payload, additional_headers = {})
|
|
272
|
+
response = connection.post completion_url, payload do |req|
|
|
273
|
+
req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
|
|
274
|
+
end
|
|
275
|
+
parse_completion_response response
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
if defined?(Rails::Railtie)
|
|
4
|
+
module LexLLM
|
|
5
|
+
# Rails integration for LexLLM
|
|
6
|
+
class Railtie < Rails::Railtie
|
|
7
|
+
initializer 'lex_llm.inflections' do
|
|
8
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
|
9
|
+
inflect.acronym 'LexLLM'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
initializer 'lex_llm.active_record' do
|
|
14
|
+
ActiveSupport.on_load :active_record do
|
|
15
|
+
if LexLLM.config.use_new_acts_as
|
|
16
|
+
require 'lex_llm/active_record/acts_as'
|
|
17
|
+
::ActiveRecord::Base.include LexLLM::ActiveRecord::ActsAs
|
|
18
|
+
else
|
|
19
|
+
require 'lex_llm/active_record/acts_as_legacy'
|
|
20
|
+
::ActiveRecord::Base.include LexLLM::ActiveRecord::ActsAsLegacy
|
|
21
|
+
|
|
22
|
+
Rails.logger.warn(
|
|
23
|
+
"\n!!! LexLLM's legacy acts_as API is deprecated and will be removed in LexLLM 2.0.0. " \
|
|
24
|
+
"Please consult the migration guide at https://github.com/LegionIO/lex-llm\n"
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
rake_tasks do
|
|
31
|
+
load 'tasks/lex_llm.rake'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Routing
|
|
5
|
+
# Builds stable fleet lane keys from provider-neutral model offerings.
|
|
6
|
+
module LaneKey
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def for(offering, prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
|
|
10
|
+
parts = [prefix, lane_kind(offering), model_slug(offering.model)]
|
|
11
|
+
parts << "ctx#{offering.context_window}" if include_context && offering.inference? && offering.context_window
|
|
12
|
+
parts.push('elig', eligibility_fingerprint(offering)) if include_fingerprint
|
|
13
|
+
parts.join('.')
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def lane_kind(offering)
|
|
17
|
+
offering.embedding? ? 'embed' : 'inference'
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def model_slug(model)
|
|
21
|
+
model.to_s.downcase.gsub(/[^a-z0-9]+/, '-').gsub(/\A-+|-+\z/, '')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def eligibility_fingerprint(offering)
|
|
25
|
+
canonical = {
|
|
26
|
+
usage_type: offering.usage_type,
|
|
27
|
+
capabilities: offering.capabilities.sort,
|
|
28
|
+
context_window: offering.context_window,
|
|
29
|
+
max_output_tokens: offering.max_output_tokens,
|
|
30
|
+
policy_tags: offering.policy_tags.sort,
|
|
31
|
+
metadata: fingerprint_metadata(offering.metadata)
|
|
32
|
+
}
|
|
33
|
+
Digest::SHA1.hexdigest(Legion::JSON.generate(canonical))[0, 10]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def fingerprint_metadata(metadata)
|
|
37
|
+
metadata.fetch(:eligibility, {})
|
|
38
|
+
.to_h
|
|
39
|
+
.transform_keys(&:to_sym)
|
|
40
|
+
.reject { |key, _| sensitive_fingerprint_key?(key) }
|
|
41
|
+
.sort
|
|
42
|
+
.to_h
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def sensitive_fingerprint_key?(key)
|
|
46
|
+
%i[credential credentials endpoint endpoint_url identity path prompt reply_to secret secrets token
|
|
47
|
+
url].include?(key)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Routing
|
|
5
|
+
# Describes one concrete model made available by one provider instance.
|
|
6
|
+
class ModelOffering
|
|
7
|
+
attr_reader :provider_family, :instance_id, :transport, :tier, :model, :usage_type, :capabilities, :limits,
|
|
8
|
+
:credentials, :health, :cost, :policy_tags, :metadata
|
|
9
|
+
|
|
10
|
+
def initialize(data)
|
|
11
|
+
@provider_family = normalize_symbol(fetch_value(data, :provider_family, fetch_value(data, :provider)))
|
|
12
|
+
@instance_id = normalize_symbol(fetch_value(data, :instance_id, @provider_family))
|
|
13
|
+
@transport = normalize_symbol(fetch_value(data, :transport, :http))
|
|
14
|
+
@tier = normalize_symbol(fetch_value(data, :tier, default_tier))
|
|
15
|
+
@model = fetch_value(data, :model).to_s
|
|
16
|
+
@usage_type = normalize_usage_type(fetch_value(data, :usage_type,
|
|
17
|
+
fetch_value(data, :type) ||
|
|
18
|
+
fetch_value(data, :kind) ||
|
|
19
|
+
infer_usage_type(data)))
|
|
20
|
+
@capabilities = normalize_array(fetch_value(data, :capabilities))
|
|
21
|
+
@limits = normalize_hash(fetch_value(data, :limits))
|
|
22
|
+
@credentials = fetch_value(data, :credentials)
|
|
23
|
+
@health = normalize_hash(fetch_value(data, :health))
|
|
24
|
+
@cost = normalize_hash(fetch_value(data, :cost))
|
|
25
|
+
@policy_tags = normalize_array(fetch_value(data, :policy_tags)).map(&:to_sym)
|
|
26
|
+
@metadata = normalize_hash(fetch_value(data, :metadata))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def enabled?
|
|
30
|
+
!metadata.key?(:enabled) || metadata[:enabled] != false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def embedding?
|
|
34
|
+
usage_type == :embedding
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def inference?
|
|
38
|
+
%i[chat inference completion].include?(usage_type)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def context_window
|
|
42
|
+
integer_limit(:context_window) || integer_limit(:max_input_tokens)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def max_output_tokens
|
|
46
|
+
integer_limit(:max_output_tokens)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def supports?(capability)
|
|
50
|
+
capabilities.include?(capability.to_sym)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def eligible_for?(usage_type: nil, required_capabilities: [], min_context_window: nil, policy_tags: [])
|
|
54
|
+
return false unless enabled?
|
|
55
|
+
return false unless usage_type_matches?(usage_type)
|
|
56
|
+
return false unless capabilities_match?(required_capabilities)
|
|
57
|
+
return false unless context_window_matches?(min_context_window)
|
|
58
|
+
return false unless policy_tags_match?(policy_tags)
|
|
59
|
+
|
|
60
|
+
true
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def lane_key(prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
|
|
64
|
+
LaneKey.for(self, prefix:, include_context:, include_fingerprint:)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def eligibility_fingerprint
|
|
68
|
+
LaneKey.eligibility_fingerprint(self)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def to_h
|
|
72
|
+
{
|
|
73
|
+
provider_family: provider_family,
|
|
74
|
+
instance_id: instance_id,
|
|
75
|
+
transport: transport,
|
|
76
|
+
tier: tier,
|
|
77
|
+
model: model,
|
|
78
|
+
usage_type: usage_type,
|
|
79
|
+
capabilities: capabilities,
|
|
80
|
+
limits: limits,
|
|
81
|
+
credentials: credentials,
|
|
82
|
+
health: health,
|
|
83
|
+
cost: cost,
|
|
84
|
+
policy_tags: policy_tags,
|
|
85
|
+
metadata: metadata
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def default_tier
|
|
92
|
+
case @transport
|
|
93
|
+
when :local
|
|
94
|
+
:local
|
|
95
|
+
when :rabbitmq
|
|
96
|
+
:fleet
|
|
97
|
+
else
|
|
98
|
+
:private
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def infer_usage_type(data)
|
|
103
|
+
capabilities = normalize_array(fetch_value(data, :capabilities))
|
|
104
|
+
return :embedding if capabilities.include?(:embedding) || capabilities.include?(:embed)
|
|
105
|
+
|
|
106
|
+
:inference
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def normalize_usage_type(value)
|
|
110
|
+
case value.to_sym
|
|
111
|
+
when :embed, :embeddings
|
|
112
|
+
:embedding
|
|
113
|
+
when :completion, :text, :chat
|
|
114
|
+
:inference
|
|
115
|
+
else
|
|
116
|
+
value.to_sym
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def normalize_symbol(value)
|
|
121
|
+
return nil if value.nil?
|
|
122
|
+
|
|
123
|
+
value.to_sym
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def normalize_array(value)
|
|
127
|
+
Array(value).compact.map(&:to_sym)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def normalize_hash(value)
|
|
131
|
+
(value || {}).to_h.transform_keys(&:to_sym)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def fetch_value(hash, key, default = nil)
|
|
135
|
+
return default unless hash.respond_to?(:key?)
|
|
136
|
+
|
|
137
|
+
string_key = key.to_s
|
|
138
|
+
return hash[string_key] if hash.key?(string_key)
|
|
139
|
+
|
|
140
|
+
hash.key?(key) ? hash[key] : default
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def usage_type_matches?(expected)
|
|
144
|
+
expected.nil? || normalize_usage_type(expected) == usage_type
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def capabilities_match?(required)
|
|
148
|
+
Array(required).all? { |capability| supports?(capability) }
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def context_window_matches?(minimum)
|
|
152
|
+
minimum.nil? || (!!context_window && context_window >= minimum.to_i)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def policy_tags_match?(required)
|
|
156
|
+
Array(required).all? { |tag| policy_tags.include?(tag.to_sym) }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def integer_limit(key)
|
|
160
|
+
value = limits[key]
|
|
161
|
+
return nil if value.nil?
|
|
162
|
+
|
|
163
|
+
Integer(value)
|
|
164
|
+
rescue ArgumentError, TypeError
|
|
165
|
+
nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|