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,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
# Custom error class that wraps API errors from different providers
|
|
5
|
+
# into a consistent format with helpful error messages.
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
attr_reader :response
|
|
8
|
+
|
|
9
|
+
def initialize(response = nil, message = nil)
|
|
10
|
+
if response.is_a?(String)
|
|
11
|
+
message = response
|
|
12
|
+
response = nil
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
@response = response
|
|
16
|
+
super(message || response&.body)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Error classes for non-HTTP errors
|
|
21
|
+
class ConfigurationError < StandardError; end
|
|
22
|
+
class PromptNotFoundError < StandardError; end
|
|
23
|
+
class InvalidRoleError < StandardError; end
|
|
24
|
+
class InvalidToolChoiceError < StandardError; end
|
|
25
|
+
class ModelNotFoundError < StandardError; end
|
|
26
|
+
class UnsupportedAttachmentError < StandardError; end
|
|
27
|
+
|
|
28
|
+
# Error classes for different HTTP status codes
|
|
29
|
+
class BadRequestError < Error; end
|
|
30
|
+
class ForbiddenError < Error; end
|
|
31
|
+
class ContextLengthExceededError < Error; end
|
|
32
|
+
class OverloadedError < Error; end
|
|
33
|
+
class PaymentRequiredError < Error; end
|
|
34
|
+
class RateLimitError < Error; end
|
|
35
|
+
class ServerError < Error; end
|
|
36
|
+
class ServiceUnavailableError < Error; end
|
|
37
|
+
class UnauthorizedError < Error; end
|
|
38
|
+
|
|
39
|
+
# Faraday middleware that maps provider-specific API errors to LexLLM errors.
|
|
40
|
+
class ErrorMiddleware < Faraday::Middleware
|
|
41
|
+
def initialize(app, options = {})
|
|
42
|
+
super(app)
|
|
43
|
+
@provider = options[:provider]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call(env)
|
|
47
|
+
@app.call(env).on_complete do |response|
|
|
48
|
+
self.class.parse_error(provider: @provider, response: response)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
CONTEXT_LENGTH_PATTERNS = [
|
|
54
|
+
/context length/i,
|
|
55
|
+
/context window/i,
|
|
56
|
+
/maximum context/i,
|
|
57
|
+
/request too large/i,
|
|
58
|
+
/too many tokens/i,
|
|
59
|
+
/token count exceeds/i,
|
|
60
|
+
/input[_\s-]?token/i,
|
|
61
|
+
/input or output tokens? must be reduced/i,
|
|
62
|
+
/reduce the length of messages/i
|
|
63
|
+
].freeze
|
|
64
|
+
|
|
65
|
+
def parse_error(provider:, response:) # rubocop:disable Metrics/PerceivedComplexity
|
|
66
|
+
message = provider&.parse_error(response)
|
|
67
|
+
|
|
68
|
+
case response.status
|
|
69
|
+
when 200..399
|
|
70
|
+
message
|
|
71
|
+
when 400
|
|
72
|
+
if context_length_exceeded?(message)
|
|
73
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
raise BadRequestError.new(response, message || 'Invalid request - please check your input')
|
|
77
|
+
when 401
|
|
78
|
+
raise UnauthorizedError.new(response, message || 'Invalid API key - check your credentials')
|
|
79
|
+
when 402
|
|
80
|
+
raise PaymentRequiredError.new(response, message || 'Payment required - please top up your account')
|
|
81
|
+
when 403
|
|
82
|
+
raise ForbiddenError.new(response,
|
|
83
|
+
message || 'Forbidden - you do not have permission to access this resource')
|
|
84
|
+
when 429
|
|
85
|
+
if context_length_exceeded?(message)
|
|
86
|
+
raise ContextLengthExceededError.new(response, message || 'Context length exceeded')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise RateLimitError.new(response, message || 'Rate limit exceeded - please wait a moment')
|
|
90
|
+
when 500
|
|
91
|
+
raise ServerError.new(response, message || 'API server error - please try again')
|
|
92
|
+
when 502..504
|
|
93
|
+
raise ServiceUnavailableError.new(response, message || 'API server unavailable - please try again later')
|
|
94
|
+
when 529
|
|
95
|
+
raise OverloadedError.new(response, message || 'Service overloaded - please try again later')
|
|
96
|
+
else
|
|
97
|
+
raise Error.new(response, message || 'An unknown error occurred')
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def context_length_exceeded?(message)
|
|
104
|
+
return false if message.to_s.empty?
|
|
105
|
+
|
|
106
|
+
CONTEXT_LENGTH_PATTERNS.any? { |pattern| message.match?(pattern) }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
Faraday::Middleware.register_middleware(llm_errors: LexLLM::ErrorMiddleware)
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
# Represents a generated image from an AI model.
|
|
5
|
+
class Image
|
|
6
|
+
attr_reader :url, :data, :mime_type, :revised_prompt, :model_id, :usage
|
|
7
|
+
|
|
8
|
+
def initialize(url: nil, data: nil, mime_type: nil, revised_prompt: nil, model_id: nil, usage: {}) # rubocop:disable Metrics/ParameterLists
|
|
9
|
+
@url = url
|
|
10
|
+
@data = data
|
|
11
|
+
@mime_type = mime_type
|
|
12
|
+
@revised_prompt = revised_prompt
|
|
13
|
+
@model_id = model_id
|
|
14
|
+
@usage = usage
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def base64?
|
|
18
|
+
!@data.nil?
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_blob
|
|
22
|
+
if base64?
|
|
23
|
+
Base64.decode64 @data
|
|
24
|
+
else
|
|
25
|
+
response = Connection.basic.get @url
|
|
26
|
+
response.body
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def save(path)
|
|
31
|
+
File.binwrite(File.expand_path(path), to_blob)
|
|
32
|
+
path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.paint(prompt, # rubocop:disable Metrics/ParameterLists
|
|
36
|
+
model: nil,
|
|
37
|
+
provider: nil,
|
|
38
|
+
assume_model_exists: false,
|
|
39
|
+
size: '1024x1024',
|
|
40
|
+
context: nil,
|
|
41
|
+
with: nil,
|
|
42
|
+
mask: nil,
|
|
43
|
+
params: {})
|
|
44
|
+
config = context&.config || LexLLM.config
|
|
45
|
+
model ||= config.default_image_model
|
|
46
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_exists: assume_model_exists,
|
|
47
|
+
config: config)
|
|
48
|
+
model_id = model.id
|
|
49
|
+
|
|
50
|
+
provider_instance.paint(prompt, model: model_id, size:, with:, mask:, params:)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def total_cost
|
|
54
|
+
input_cost + output_cost
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def input_cost
|
|
58
|
+
return flat_input_cost unless detailed_input_usage?
|
|
59
|
+
|
|
60
|
+
text_input_cost + image_input_cost
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def output_cost
|
|
64
|
+
tokens_for('output_tokens') * output_token_price.to_f / 1_000_000
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def model_info
|
|
68
|
+
@model_info ||= LexLLM.models.find(model_id)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def flat_input_cost
|
|
74
|
+
tokens_for('input_tokens') * model_info.input_price_per_million.to_f / 1_000_000
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def text_input_cost
|
|
78
|
+
input_detail('text_tokens') * model_info.input_price_per_million.to_f / 1_000_000
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def image_input_cost
|
|
82
|
+
input_detail('image_tokens') * image_input_token_price.to_f / 1_000_000
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def output_token_price
|
|
86
|
+
model_info.pricing.images.output || model_info.output_price_per_million
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def image_input_token_price
|
|
90
|
+
model_info.pricing.images.input || model_info.input_price_per_million
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def detailed_input_usage?
|
|
94
|
+
usage['input_tokens_details'].is_a?(Hash)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def input_detail(key)
|
|
98
|
+
usage.dig('input_tokens_details', key).to_i
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def tokens_for(key)
|
|
102
|
+
usage[key].to_i
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
# A single message in a chat conversation.
|
|
5
|
+
class Message
|
|
6
|
+
ROLES = %i[system user assistant tool].freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :role, :model_id, :tool_calls, :tool_call_id, :raw, :thinking, :tokens
|
|
9
|
+
attr_writer :content
|
|
10
|
+
|
|
11
|
+
def initialize(options = {})
|
|
12
|
+
@role = options.fetch(:role).to_sym
|
|
13
|
+
@tool_calls = options[:tool_calls]
|
|
14
|
+
@content = normalize_content(options.fetch(:content), role: @role, tool_calls: @tool_calls)
|
|
15
|
+
@model_id = options[:model_id]
|
|
16
|
+
@tool_call_id = options[:tool_call_id]
|
|
17
|
+
@tokens = options[:tokens] || Tokens.build(
|
|
18
|
+
input: options[:input_tokens],
|
|
19
|
+
output: options[:output_tokens],
|
|
20
|
+
cached: options[:cached_tokens],
|
|
21
|
+
cache_creation: options[:cache_creation_tokens],
|
|
22
|
+
thinking: options[:thinking_tokens],
|
|
23
|
+
reasoning: options[:reasoning_tokens]
|
|
24
|
+
)
|
|
25
|
+
@raw = options[:raw]
|
|
26
|
+
@thinking = options[:thinking]
|
|
27
|
+
|
|
28
|
+
ensure_valid_role
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def content
|
|
32
|
+
if @content.is_a?(Content) && @content.text && @content.attachments.empty?
|
|
33
|
+
@content.text
|
|
34
|
+
else
|
|
35
|
+
@content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def tool_call?
|
|
40
|
+
!tool_calls.nil? && !tool_calls.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def tool_result?
|
|
44
|
+
!tool_call_id.nil? && !tool_call_id.empty?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def tool_results
|
|
48
|
+
content if tool_result?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def input_tokens
|
|
52
|
+
tokens&.input
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def output_tokens
|
|
56
|
+
tokens&.output
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def cached_tokens
|
|
60
|
+
tokens&.cached
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def cache_creation_tokens
|
|
64
|
+
tokens&.cache_creation
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def thinking_tokens
|
|
68
|
+
tokens&.thinking
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def reasoning_tokens
|
|
72
|
+
tokens&.thinking
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def to_h
|
|
76
|
+
{
|
|
77
|
+
role: role,
|
|
78
|
+
content: content,
|
|
79
|
+
model_id: model_id,
|
|
80
|
+
tool_calls: tool_calls,
|
|
81
|
+
tool_call_id: tool_call_id,
|
|
82
|
+
thinking: thinking&.text,
|
|
83
|
+
thinking_signature: thinking&.signature
|
|
84
|
+
}.merge(tokens ? tokens.to_h : {}).compact
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def instance_variables
|
|
88
|
+
super - [:@raw]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
private
|
|
92
|
+
|
|
93
|
+
def normalize_content(content, role:, tool_calls:)
|
|
94
|
+
return '' if role == :assistant && content.nil? && tool_calls && !tool_calls.empty?
|
|
95
|
+
|
|
96
|
+
case content
|
|
97
|
+
when String then Content.new(content)
|
|
98
|
+
when Hash then Content.new(content[:text], content)
|
|
99
|
+
else content
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def ensure_valid_role
|
|
104
|
+
raise InvalidRoleError, "Expected role to be one of: #{ROLES.join(', ')}" unless ROLES.include?(role)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'marcel'
|
|
4
|
+
|
|
5
|
+
module LexLLM
|
|
6
|
+
# MimeTypes module provides methods to handle MIME types using Marcel gem
|
|
7
|
+
module MimeType
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def for(...)
|
|
11
|
+
Marcel::MimeType.for(...)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def image?(type)
|
|
15
|
+
type.start_with?('image/')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def video?(type)
|
|
19
|
+
type.start_with?('video/')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def audio?(type)
|
|
23
|
+
type.start_with?('audio/')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def pdf?(type)
|
|
27
|
+
type == 'application/pdf'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def text?(type)
|
|
31
|
+
type.start_with?('text/') ||
|
|
32
|
+
TEXT_SUFFIXES.any? { |suffix| type.end_with?(suffix) } ||
|
|
33
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES.include?(type)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# MIME types that have a text/ prefix but need to be handled differently
|
|
37
|
+
TEXT_SUFFIXES = ['+json', '+xml', '+html', '+yaml', '+csv', '+plain', '+javascript', '+svg'].freeze
|
|
38
|
+
|
|
39
|
+
# MIME types that don't have a text/ prefix but should be treated as text
|
|
40
|
+
NON_TEXT_PREFIX_TEXT_MIME_TYPES = [
|
|
41
|
+
'application/json', # Base type, even if specific ones end with +json
|
|
42
|
+
'application/xml', # Base type, even if specific ones end with +xml
|
|
43
|
+
'application/javascript',
|
|
44
|
+
'application/ecmascript',
|
|
45
|
+
'application/rtf',
|
|
46
|
+
'application/sql',
|
|
47
|
+
'application/x-sh',
|
|
48
|
+
'application/x-csh',
|
|
49
|
+
'application/x-httpd-php',
|
|
50
|
+
'application/sdp',
|
|
51
|
+
'application/sparql-query',
|
|
52
|
+
'application/graphql',
|
|
53
|
+
'application/yang', # Data modeling language, often serialized as XML/JSON but the type itself is distinct
|
|
54
|
+
'application/mbox', # Mailbox format
|
|
55
|
+
'application/x-tex',
|
|
56
|
+
'application/x-latex',
|
|
57
|
+
'application/x-perl',
|
|
58
|
+
'application/x-python',
|
|
59
|
+
'application/x-tcl',
|
|
60
|
+
'application/pgp-signature', # Often ASCII armored
|
|
61
|
+
'application/pgp-keys', # Often ASCII armored
|
|
62
|
+
'application/vnd.coffeescript',
|
|
63
|
+
'application/vnd.dart',
|
|
64
|
+
'application/vnd.oai.openapi', # Base for OpenAPI, often with +json or +yaml suffix
|
|
65
|
+
'application/vnd.zul', # ZK User Interface Language (can be XML-like)
|
|
66
|
+
'application/x-yaml', # Common non-standard for YAML
|
|
67
|
+
'application/yaml', # Standard for YAML
|
|
68
|
+
'application/toml' # TOML configuration files
|
|
69
|
+
].freeze
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Model
|
|
5
|
+
# Information about an AI model's capabilities, pricing, and metadata.
|
|
6
|
+
class Info
|
|
7
|
+
attr_reader :id, :name, :provider, :family, :created_at, :context_window, :max_output_tokens, :knowledge_cutoff,
|
|
8
|
+
:modalities, :capabilities, :pricing, :metadata
|
|
9
|
+
|
|
10
|
+
# Create a default model with assumed capabilities
|
|
11
|
+
def self.default(model_id, provider)
|
|
12
|
+
new(
|
|
13
|
+
id: model_id,
|
|
14
|
+
name: model_id.tr('-', ' ').capitalize,
|
|
15
|
+
provider: provider,
|
|
16
|
+
capabilities: %w[function_calling streaming vision structured_output],
|
|
17
|
+
modalities: { input: %w[text image], output: %w[text] },
|
|
18
|
+
metadata: { warning: 'Assuming model exists, capabilities may not be accurate' }
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(data)
|
|
23
|
+
@id = data[:id]
|
|
24
|
+
@name = data[:name]
|
|
25
|
+
@provider = data[:provider]
|
|
26
|
+
@family = data[:family]
|
|
27
|
+
@created_at = Utils.to_time(data[:created_at])&.utc
|
|
28
|
+
@context_window = data[:context_window]
|
|
29
|
+
@max_output_tokens = data[:max_output_tokens]
|
|
30
|
+
@knowledge_cutoff = Utils.to_date(data[:knowledge_cutoff])
|
|
31
|
+
@modalities = Modalities.new(data[:modalities] || {})
|
|
32
|
+
@capabilities = data[:capabilities] || []
|
|
33
|
+
@pricing = Pricing.new(data[:pricing] || {})
|
|
34
|
+
@metadata = data[:metadata] || {}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def supports?(capability)
|
|
38
|
+
capabilities.include?(capability.to_s)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
%w[function_calling structured_output batch reasoning citations streaming].each do |cap|
|
|
42
|
+
define_method "#{cap}?" do
|
|
43
|
+
supports?(cap)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def display_name
|
|
48
|
+
name
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def label
|
|
52
|
+
provider_name = provider_class&.name || provider
|
|
53
|
+
"#{provider_name} - #{display_name}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def max_tokens
|
|
57
|
+
max_output_tokens
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def supports_vision?
|
|
61
|
+
modalities.input.include?('image')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def supports_video?
|
|
65
|
+
modalities.input.include?('video')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def supports_functions?
|
|
69
|
+
function_calling?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def input_price_per_million
|
|
73
|
+
pricing.text_tokens.input
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def output_price_per_million
|
|
77
|
+
pricing.text_tokens.output
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def provider_class
|
|
81
|
+
LexLLM::Provider.resolve provider
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def type
|
|
85
|
+
output = modalities.output
|
|
86
|
+
return 'embedding' if output.include?('embeddings')
|
|
87
|
+
return 'moderation' if output.include?('moderation')
|
|
88
|
+
return 'image' if output.include?('image')
|
|
89
|
+
return 'audio' if output.include?('audio')
|
|
90
|
+
return 'video' if output.include?('video')
|
|
91
|
+
|
|
92
|
+
'chat'
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def to_h
|
|
96
|
+
{
|
|
97
|
+
id: id,
|
|
98
|
+
name: name,
|
|
99
|
+
provider: provider,
|
|
100
|
+
family: family,
|
|
101
|
+
created_at: created_at,
|
|
102
|
+
context_window: context_window,
|
|
103
|
+
max_output_tokens: max_output_tokens,
|
|
104
|
+
knowledge_cutoff: knowledge_cutoff,
|
|
105
|
+
modalities: modalities.to_h,
|
|
106
|
+
capabilities: capabilities,
|
|
107
|
+
pricing: pricing.to_h,
|
|
108
|
+
metadata: metadata
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Model
|
|
5
|
+
# Holds and manages input and output modalities for a language model
|
|
6
|
+
class Modalities
|
|
7
|
+
attr_reader :input, :output
|
|
8
|
+
|
|
9
|
+
def initialize(data)
|
|
10
|
+
@input = Array(data[:input]).map(&:to_s)
|
|
11
|
+
@output = Array(data[:output]).map(&:to_s)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
{
|
|
16
|
+
input: input,
|
|
17
|
+
output: output
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Model
|
|
5
|
+
# A collection that manages and provides access to different categories of pricing information
|
|
6
|
+
class Pricing
|
|
7
|
+
def initialize(data)
|
|
8
|
+
@data = {}
|
|
9
|
+
|
|
10
|
+
%i[text_tokens images audio_tokens embeddings].each do |category|
|
|
11
|
+
@data[category] = PricingCategory.new(data[category]) if data[category] && !empty_pricing?(data[category])
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def method_missing(method, *args)
|
|
16
|
+
if respond_to_missing?(method)
|
|
17
|
+
@data[method.to_sym] || PricingCategory.new
|
|
18
|
+
else
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def respond_to_missing?(method, include_private = false)
|
|
24
|
+
%i[text_tokens images audio_tokens embeddings].include?(method.to_sym) || super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
@data.transform_values(&:to_h)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def empty_pricing?(data)
|
|
34
|
+
return true unless data
|
|
35
|
+
|
|
36
|
+
%i[standard batch].each do |tier|
|
|
37
|
+
next unless data[tier]
|
|
38
|
+
|
|
39
|
+
data[tier].each_value do |value|
|
|
40
|
+
return false if value && value != 0.0
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
true
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Model
|
|
5
|
+
# Represents pricing tiers for different usage categories (standard and batch)
|
|
6
|
+
class PricingCategory
|
|
7
|
+
attr_reader :standard, :batch
|
|
8
|
+
|
|
9
|
+
def initialize(data = {})
|
|
10
|
+
@standard = PricingTier.new(data[:standard] || {}) unless empty_tier?(data[:standard])
|
|
11
|
+
@batch = PricingTier.new(data[:batch] || {}) unless empty_tier?(data[:batch])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def input
|
|
15
|
+
standard&.input_per_million
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def output
|
|
19
|
+
standard&.output_per_million
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def cached_input
|
|
23
|
+
standard&.cached_input_per_million
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def [](key)
|
|
27
|
+
key == :batch ? batch : standard
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
result = {}
|
|
32
|
+
result[:standard] = standard.to_h if standard
|
|
33
|
+
result[:batch] = batch.to_h if batch
|
|
34
|
+
result
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def empty_tier?(tier_data)
|
|
40
|
+
return true unless tier_data
|
|
41
|
+
|
|
42
|
+
tier_data.values.all? { |v| v.nil? || v == 0.0 }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LexLLM
|
|
4
|
+
module Model
|
|
5
|
+
# A dynamic class for storing non-zero pricing values with flexible attribute access
|
|
6
|
+
class PricingTier
|
|
7
|
+
def initialize(data = {})
|
|
8
|
+
@values = {}
|
|
9
|
+
|
|
10
|
+
data.each do |key, value|
|
|
11
|
+
@values[key.to_sym] = value if value && value != 0.0
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def method_missing(method, *args)
|
|
16
|
+
if method.to_s.end_with?('=')
|
|
17
|
+
key = method.to_s.chomp('=').to_sym
|
|
18
|
+
@values[key] = args.first if args.first && args.first != 0.0
|
|
19
|
+
elsif @values.key?(method)
|
|
20
|
+
@values[method]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def respond_to_missing?(method, include_private = false)
|
|
25
|
+
method.to_s.end_with?('=') || @values.key?(method.to_sym) || super
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_h
|
|
29
|
+
@values
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|