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.
Files changed (135) hide show
  1. checksums.yaml +7 -0
  2. data/.github/CODEOWNERS +7 -0
  3. data/.github/dependabot.yml +18 -0
  4. data/.github/workflows/ci.yml +16 -0
  5. data/.gitignore +19 -0
  6. data/.rubocop.yml +42 -0
  7. data/CHANGELOG.md +15 -0
  8. data/Gemfile +50 -0
  9. data/LICENSE +21 -0
  10. data/README.md +279 -0
  11. data/lex-llm.gemspec +43 -0
  12. data/lib/generators/lex_llm/agent/agent_generator.rb +36 -0
  13. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +6 -0
  14. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  15. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +256 -0
  16. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +38 -0
  17. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +21 -0
  18. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
  19. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  20. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
  21. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  22. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  23. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  24. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  25. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  26. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  27. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  28. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  29. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  30. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  31. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  32. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  33. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  34. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  35. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  36. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  37. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  38. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  39. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
  40. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
  41. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +28 -0
  42. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
  43. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +25 -0
  44. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  45. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -0
  46. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  47. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
  48. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  49. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  50. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -0
  51. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  52. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +7 -0
  53. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  54. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  55. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +15 -0
  56. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +38 -0
  57. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +17 -0
  58. data/lib/generators/lex_llm/generator_helpers.rb +214 -0
  59. data/lib/generators/lex_llm/install/install_generator.rb +109 -0
  60. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +9 -0
  61. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +3 -0
  62. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +7 -0
  63. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +19 -0
  64. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +39 -0
  65. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +21 -0
  66. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +20 -0
  67. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +4 -0
  68. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +3 -0
  69. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +3 -0
  70. data/lib/generators/lex_llm/schema/schema_generator.rb +26 -0
  71. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +2 -0
  72. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +9 -0
  73. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +13 -0
  74. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +13 -0
  75. data/lib/generators/lex_llm/tool/tool_generator.rb +96 -0
  76. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
  77. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
  78. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  79. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  80. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  81. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +122 -0
  82. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +15 -0
  83. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +49 -0
  84. data/lib/legion/extensions/llm/provider_settings.rb +49 -0
  85. data/lib/legion/extensions/llm/transport/fleet_lane.rb +70 -0
  86. data/lib/legion/extensions/llm.rb +50 -0
  87. data/lib/lex_llm/active_record/acts_as.rb +180 -0
  88. data/lib/lex_llm/active_record/acts_as_legacy.rb +503 -0
  89. data/lib/lex_llm/active_record/chat_methods.rb +468 -0
  90. data/lib/lex_llm/active_record/message_methods.rb +131 -0
  91. data/lib/lex_llm/active_record/model_methods.rb +76 -0
  92. data/lib/lex_llm/active_record/payload_helpers.rb +26 -0
  93. data/lib/lex_llm/active_record/tool_call_methods.rb +15 -0
  94. data/lib/lex_llm/agent.rb +365 -0
  95. data/lib/lex_llm/aliases.json +436 -0
  96. data/lib/lex_llm/aliases.rb +38 -0
  97. data/lib/lex_llm/attachment.rb +223 -0
  98. data/lib/lex_llm/chat.rb +351 -0
  99. data/lib/lex_llm/chunk.rb +6 -0
  100. data/lib/lex_llm/configuration.rb +81 -0
  101. data/lib/lex_llm/connection.rb +130 -0
  102. data/lib/lex_llm/content.rb +77 -0
  103. data/lib/lex_llm/context.rb +29 -0
  104. data/lib/lex_llm/embedding.rb +29 -0
  105. data/lib/lex_llm/error.rb +112 -0
  106. data/lib/lex_llm/image.rb +105 -0
  107. data/lib/lex_llm/message.rb +107 -0
  108. data/lib/lex_llm/mime_type.rb +71 -0
  109. data/lib/lex_llm/model/info.rb +113 -0
  110. data/lib/lex_llm/model/modalities.rb +22 -0
  111. data/lib/lex_llm/model/pricing.rb +48 -0
  112. data/lib/lex_llm/model/pricing_category.rb +46 -0
  113. data/lib/lex_llm/model/pricing_tier.rb +33 -0
  114. data/lib/lex_llm/model.rb +7 -0
  115. data/lib/lex_llm/models.json +57241 -0
  116. data/lib/lex_llm/models.rb +506 -0
  117. data/lib/lex_llm/models_schema.json +168 -0
  118. data/lib/lex_llm/moderation.rb +56 -0
  119. data/lib/lex_llm/provider.rb +278 -0
  120. data/lib/lex_llm/railtie.rb +35 -0
  121. data/lib/lex_llm/routing/lane_key.rb +51 -0
  122. data/lib/lex_llm/routing/model_offering.rb +169 -0
  123. data/lib/lex_llm/routing.rb +7 -0
  124. data/lib/lex_llm/stream_accumulator.rb +203 -0
  125. data/lib/lex_llm/streaming.rb +175 -0
  126. data/lib/lex_llm/thinking.rb +49 -0
  127. data/lib/lex_llm/tokens.rb +47 -0
  128. data/lib/lex_llm/tool.rb +254 -0
  129. data/lib/lex_llm/tool_call.rb +25 -0
  130. data/lib/lex_llm/transcription.rb +35 -0
  131. data/lib/lex_llm/utils.rb +91 -0
  132. data/lib/lex_llm/version.rb +5 -0
  133. data/lib/lex_llm.rb +95 -0
  134. data/lib/tasks/lex_llm.rake +23 -0
  135. 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
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LexLLM
4
+ # Model-related classes for working with LLM models
5
+ module Model
6
+ end
7
+ end