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
@@ -1,219 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- class Provider
5
- # Shared OpenAI-compatible HTTP payload and response adapter.
6
- module OpenAICompatible
7
- def completion_url = '/v1/chat/completions'
8
- def stream_url = completion_url
9
- def models_url = '/v1/models'
10
- def moderation_url = '/v1/moderations'
11
- def embedding_url(**) = '/v1/embeddings'
12
- def transcription_url = '/v1/audio/transcriptions'
13
-
14
- def images_url(with:, mask:)
15
- with || mask ? '/v1/images/edits' : '/v1/images/generations'
16
- end
17
-
18
- private
19
-
20
- def render_payload(messages, tools:, temperature:, model:, stream:, schema:, thinking:, tool_prefs:) # rubocop:disable Metrics/ParameterLists
21
- {
22
- model: model.id,
23
- messages: format_openai_messages(messages),
24
- temperature: temperature,
25
- stream: stream,
26
- tools: format_openai_tools(tools),
27
- tool_choice: openai_tool_choice(tool_prefs),
28
- response_format: openai_response_format(schema),
29
- reasoning_effort: openai_reasoning_effort(thinking)
30
- }.compact
31
- end
32
-
33
- def format_openai_messages(messages)
34
- messages.map do |message|
35
- {
36
- role: message.role.to_s,
37
- content: openai_content(message.content),
38
- tool_call_id: message.tool_call_id,
39
- tool_calls: format_openai_tool_calls(message.tool_calls)
40
- }.compact
41
- end
42
- end
43
-
44
- def openai_content(content)
45
- return content.format if content.is_a?(LexLLM::Content::Raw)
46
- return content unless content.respond_to?(:attachments)
47
- return content.text.to_s if content.attachments.empty?
48
-
49
- openai_content_parts(content)
50
- end
51
-
52
- def openai_content_parts(content)
53
- parts = []
54
- parts << { type: 'text', text: content.text.to_s } if content.text
55
- content.attachments.each do |attachment|
56
- parts << { type: 'image_url', image_url: { url: attachment.for_llm } } if attachment.image?
57
- end
58
- parts
59
- end
60
-
61
- def format_openai_tool_calls(tool_calls)
62
- return nil unless tool_calls&.any?
63
-
64
- tool_calls.values.map do |tool_call|
65
- {
66
- id: tool_call.id,
67
- type: 'function',
68
- function: {
69
- name: tool_call.name,
70
- arguments: Legion::JSON.generate(tool_call.arguments || {})
71
- }
72
- }
73
- end
74
- end
75
-
76
- def format_openai_tools(tools)
77
- return nil if tools.empty?
78
-
79
- tools.values.map do |tool|
80
- {
81
- type: 'function',
82
- function: {
83
- name: tool.name,
84
- description: tool.description,
85
- parameters: tool.params_schema || { type: 'object', properties: {} }
86
- }
87
- }
88
- end
89
- end
90
-
91
- def openai_tool_choice(tool_prefs)
92
- choice = tool_prefs && (tool_prefs[:choice] || tool_prefs['choice'])
93
- return nil unless choice
94
- return choice.to_s if %i[auto none required].include?(choice.to_sym)
95
-
96
- { type: 'function', function: { name: choice.to_s } }
97
- end
98
-
99
- def openai_response_format(schema)
100
- return nil unless schema
101
-
102
- schema_hash = schema.respond_to?(:to_h) ? schema.to_h : schema
103
- { type: 'json_schema', json_schema: schema_hash }
104
- end
105
-
106
- def openai_reasoning_effort(thinking)
107
- return nil unless thinking.is_a?(Hash)
108
-
109
- thinking[:effort] || thinking['effort']
110
- end
111
-
112
- def parse_completion_response(response)
113
- body = response.body
114
- choice = Array(body['choices']).first || {}
115
- message = choice['message'] || {}
116
- usage = body['usage'] || {}
117
-
118
- LexLLM::Message.new(
119
- role: :assistant,
120
- content: message['content'],
121
- model_id: body['model'],
122
- tool_calls: parse_tool_calls(message['tool_calls']),
123
- input_tokens: usage['prompt_tokens'],
124
- output_tokens: usage['completion_tokens'],
125
- reasoning_tokens: usage.dig('completion_tokens_details', 'reasoning_tokens'),
126
- raw: body
127
- )
128
- end
129
-
130
- def build_chunk(data)
131
- choice = Array(data['choices']).first || {}
132
- delta = choice['delta'] || {}
133
- usage = data['usage'] || {}
134
-
135
- LexLLM::Chunk.new(
136
- role: :assistant,
137
- content: delta['content'],
138
- model_id: data['model'],
139
- tool_calls: parse_tool_calls(delta['tool_calls']),
140
- input_tokens: usage['prompt_tokens'],
141
- output_tokens: usage['completion_tokens'],
142
- raw: data
143
- )
144
- end
145
-
146
- def parse_tool_calls(tool_calls)
147
- return nil unless tool_calls&.any?
148
-
149
- tool_calls.to_h do |call|
150
- function = call.fetch('function', {})
151
- name = function.fetch('name')
152
- [name.to_sym, LexLLM::ToolCall.new(id: call['id'] || name, name: name,
153
- arguments: parse_tool_arguments(function['arguments']))]
154
- end
155
- end
156
-
157
- def parse_tool_arguments(arguments)
158
- return {} if arguments.nil? || arguments == ''
159
- return arguments if arguments.is_a?(Hash)
160
-
161
- Legion::JSON.parse(arguments, symbolize_names: false)
162
- rescue Legion::JSON::ParseError
163
- {}
164
- end
165
-
166
- def parse_list_models_response(response, provider, _capabilities)
167
- response.body.fetch('data', []).map do |model|
168
- LexLLM::Model::Info.new(
169
- id: model.fetch('id'),
170
- name: model['id'],
171
- provider: provider,
172
- created_at: model['created'],
173
- metadata: model
174
- )
175
- end
176
- end
177
-
178
- def render_embedding_payload(text, model:, dimensions:)
179
- { model: model, input: text, dimensions: dimensions }.compact
180
- end
181
-
182
- def parse_embedding_response(response, model:, text:)
183
- vectors = response.body.fetch('data', []).map { |item| item['embedding'] }
184
- vectors = vectors.first unless text.is_a?(Array)
185
- usage = response.body['usage'] || {}
186
-
187
- LexLLM::Embedding.new(vectors: vectors, model: model, input_tokens: usage['prompt_tokens'].to_i)
188
- end
189
-
190
- def render_moderation_payload(input, model:)
191
- { model: model, input: input }.compact
192
- end
193
-
194
- def parse_moderation_response(response, model:)
195
- LexLLM::Moderation.new(id: response.body['id'], model: response.body['model'] || model,
196
- results: response.body.fetch('results', []))
197
- end
198
-
199
- def render_image_payload(prompt, model:, size:, with:, mask:, params:) # rubocop:disable Metrics/ParameterLists
200
- { model: model, prompt: prompt, size: size, image: with, mask: mask }.merge(params).compact
201
- end
202
-
203
- def parse_image_response(response, model:)
204
- image = response.body.fetch('data', []).first || {}
205
- LexLLM::Image.new(url: image['url'], data: image['b64_json'], revised_prompt: image['revised_prompt'],
206
- model_id: model, usage: response.body['usage'] || {})
207
- end
208
-
209
- def render_transcription_payload(file_part, model:, language:, **options)
210
- { model: model, file: file_part, language: language }.merge(options).compact
211
- end
212
-
213
- def parse_transcription_response(response, model:)
214
- LexLLM::Transcription.new(text: response.body['text'], model: model, language: response.body['language'],
215
- duration: response.body['duration'], segments: response.body['segments'])
216
- end
217
- end
218
- end
219
- end
@@ -1,278 +0,0 @@
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
@@ -1,35 +0,0 @@
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
@@ -1,51 +0,0 @@
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