ruby_llm_community 0.0.6 → 1.1.0
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 +4 -4
- data/README.md +20 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +227 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +43 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +137 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +170 -0
- data/lib/ruby_llm/active_record/acts_as.rb +112 -332
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +403 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +130 -11
- data/lib/ruby_llm/aliases.rb +7 -25
- data/lib/ruby_llm/attachment.rb +22 -0
- data/lib/ruby_llm/chat.rb +10 -17
- data/lib/ruby_llm/configuration.rb +11 -12
- data/lib/ruby_llm/connection.rb +4 -4
- data/lib/ruby_llm/connection_multipart.rb +19 -0
- data/lib/ruby_llm/content.rb +5 -2
- data/lib/ruby_llm/embedding.rb +1 -2
- data/lib/ruby_llm/error.rb +0 -8
- data/lib/ruby_llm/image.rb +23 -8
- data/lib/ruby_llm/image_attachment.rb +30 -0
- data/lib/ruby_llm/message.rb +7 -7
- data/lib/ruby_llm/model/info.rb +12 -10
- data/lib/ruby_llm/model/pricing.rb +0 -3
- data/lib/ruby_llm/model/pricing_category.rb +0 -2
- data/lib/ruby_llm/model/pricing_tier.rb +0 -1
- data/lib/ruby_llm/models.json +4705 -2144
- data/lib/ruby_llm/models.rb +56 -35
- data/lib/ruby_llm/provider.rb +14 -12
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -46
- data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/media.rb +1 -2
- data/lib/ruby_llm/providers/anthropic/tools.rb +1 -2
- data/lib/ruby_llm/providers/anthropic.rb +1 -2
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -4
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -1
- data/lib/ruby_llm/providers/bedrock/models.rb +19 -3
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -7
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -12
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -13
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -18
- data/lib/ruby_llm/providers/bedrock.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +1 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +0 -1
- data/lib/ruby_llm/providers/gemini/capabilities.rb +28 -100
- data/lib/ruby_llm/providers/gemini/chat.rb +57 -29
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -2
- data/lib/ruby_llm/providers/gemini/images.rb +1 -2
- data/lib/ruby_llm/providers/gemini/media.rb +1 -2
- data/lib/ruby_llm/providers/gemini/models.rb +1 -2
- data/lib/ruby_llm/providers/gemini/streaming.rb +15 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -5
- data/lib/ruby_llm/providers/gpustack/chat.rb +11 -1
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +44 -9
- data/lib/ruby_llm/providers/gpustack.rb +1 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +2 -10
- data/lib/ruby_llm/providers/mistral/chat.rb +0 -2
- data/lib/ruby_llm/providers/mistral/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/mistral/models.rb +0 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +0 -1
- data/lib/ruby_llm/providers/ollama/media.rb +2 -7
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +1 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -16
- data/lib/ruby_llm/providers/openai/chat.rb +1 -3
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -3
- data/lib/ruby_llm/providers/openai/images.rb +73 -3
- data/lib/ruby_llm/providers/openai/media.rb +4 -5
- data/lib/ruby_llm/providers/openai/response.rb +121 -29
- data/lib/ruby_llm/providers/openai/response_media.rb +3 -3
- data/lib/ruby_llm/providers/openai/streaming.rb +110 -47
- data/lib/ruby_llm/providers/openai/tools.rb +12 -7
- data/lib/ruby_llm/providers/openai.rb +1 -3
- data/lib/ruby_llm/providers/openai_base.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -16
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -1
- data/lib/ruby_llm/providers/perplexity/chat.rb +0 -1
- data/lib/ruby_llm/providers/perplexity.rb +1 -5
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/providers/xai/capabilities.rb +166 -0
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +48 -0
- data/lib/ruby_llm/providers/xai.rb +46 -0
- data/lib/ruby_llm/railtie.rb +20 -4
- data/lib/ruby_llm/stream_accumulator.rb +68 -10
- data/lib/ruby_llm/streaming.rb +16 -25
- data/lib/ruby_llm/tool.rb +2 -19
- data/lib/ruby_llm/tool_call.rb +0 -9
- data/lib/ruby_llm/utils.rb +5 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm_community.rb +8 -5
- data/lib/tasks/models.rake +549 -0
- data/lib/tasks/release.rake +37 -2
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +2 -9
- metadata +44 -6
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
- data/lib/generators/ruby_llm/install_generator.rb +0 -121
- data/lib/tasks/aliases.rake +0 -235
- data/lib/tasks/models_docs.rake +0 -224
- data/lib/tasks/models_update.rake +0 -108
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Models methods for the Vertex AI integration
|
7
|
+
module Models
|
8
|
+
# Gemini and other Google models that aren't returned by the API
|
9
|
+
KNOWN_GOOGLE_MODELS = %w[
|
10
|
+
gemini-2.5-flash-lite
|
11
|
+
gemini-2.5-pro
|
12
|
+
gemini-2.5-flash
|
13
|
+
gemini-2.0-flash-lite-001
|
14
|
+
gemini-2.0-flash-001
|
15
|
+
gemini-2.0-flash
|
16
|
+
gemini-2.0-flash-exp
|
17
|
+
gemini-1.5-pro-002
|
18
|
+
gemini-1.5-pro
|
19
|
+
gemini-1.5-flash-002
|
20
|
+
gemini-1.5-flash
|
21
|
+
gemini-1.5-flash-8b
|
22
|
+
gemini-pro
|
23
|
+
gemini-pro-vision
|
24
|
+
gemini-exp-1206
|
25
|
+
gemini-exp-1121
|
26
|
+
gemini-embedding-001
|
27
|
+
text-embedding-005
|
28
|
+
text-embedding-004
|
29
|
+
text-multilingual-embedding-002
|
30
|
+
].freeze
|
31
|
+
|
32
|
+
def list_models
|
33
|
+
all_models = []
|
34
|
+
page_token = nil
|
35
|
+
|
36
|
+
all_models.concat(build_known_models)
|
37
|
+
|
38
|
+
loop do
|
39
|
+
response = @connection.get('publishers/google/models') do |req|
|
40
|
+
req.headers['x-goog-user-project'] = @config.vertexai_project_id
|
41
|
+
req.params = { pageSize: 100 }
|
42
|
+
req.params[:pageToken] = page_token if page_token
|
43
|
+
end
|
44
|
+
|
45
|
+
publisher_models = response.body['publisherModels'] || []
|
46
|
+
publisher_models.each do |model_data|
|
47
|
+
next if model_data['launchStage'] == 'DEPRECATED'
|
48
|
+
|
49
|
+
model_id = extract_model_id_from_path(model_data['name'])
|
50
|
+
all_models << build_model_from_api_data(model_data, model_id)
|
51
|
+
end
|
52
|
+
|
53
|
+
page_token = response.body['nextPageToken']
|
54
|
+
break unless page_token
|
55
|
+
end
|
56
|
+
|
57
|
+
all_models
|
58
|
+
rescue StandardError => e
|
59
|
+
RubyLLM.logger.debug "Error fetching Vertex AI models: #{e.message}"
|
60
|
+
build_known_models
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def build_known_models
|
66
|
+
KNOWN_GOOGLE_MODELS.map do |model_id|
|
67
|
+
Model::Info.new(
|
68
|
+
id: model_id,
|
69
|
+
name: model_id,
|
70
|
+
provider: slug,
|
71
|
+
family: determine_model_family(model_id),
|
72
|
+
created_at: nil,
|
73
|
+
context_window: nil,
|
74
|
+
max_output_tokens: nil,
|
75
|
+
modalities: nil,
|
76
|
+
capabilities: %w[streaming function_calling],
|
77
|
+
pricing: nil,
|
78
|
+
metadata: {
|
79
|
+
source: 'known_models'
|
80
|
+
}
|
81
|
+
)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def build_model_from_api_data(model_data, model_id)
|
86
|
+
Model::Info.new(
|
87
|
+
id: model_id,
|
88
|
+
name: model_id,
|
89
|
+
provider: slug,
|
90
|
+
family: determine_model_family(model_id),
|
91
|
+
created_at: nil,
|
92
|
+
context_window: nil,
|
93
|
+
max_output_tokens: nil,
|
94
|
+
modalities: nil,
|
95
|
+
capabilities: extract_capabilities(model_data),
|
96
|
+
pricing: nil,
|
97
|
+
metadata: {
|
98
|
+
version_id: model_data['versionId'],
|
99
|
+
open_source_category: model_data['openSourceCategory'],
|
100
|
+
launch_stage: model_data['launchStage'],
|
101
|
+
supported_actions: model_data['supportedActions'],
|
102
|
+
publisher_model_template: model_data['publisherModelTemplate']
|
103
|
+
}
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def extract_model_id_from_path(path)
|
108
|
+
path.split('/').last
|
109
|
+
end
|
110
|
+
|
111
|
+
def determine_model_family(model_id)
|
112
|
+
case model_id
|
113
|
+
when /^gemini-2\.\d+/ then 'gemini-2'
|
114
|
+
when /^gemini-1\.\d+/ then 'gemini-1.5'
|
115
|
+
when /^text-embedding/ then 'text-embedding'
|
116
|
+
when /bison/ then 'palm'
|
117
|
+
else 'gemini'
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def extract_capabilities(model_data)
|
122
|
+
capabilities = ['streaming']
|
123
|
+
model_name = model_data['name']
|
124
|
+
capabilities << 'function_calling' if model_name.include?('gemini')
|
125
|
+
capabilities.uniq
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class VertexAI
|
6
|
+
# Streaming methods for the Vertex AI implementation
|
7
|
+
module Streaming
|
8
|
+
def stream_url
|
9
|
+
"projects/#{@config.vertexai_project_id}/locations/#{@config.vertexai_location}/publishers/google/models/#{@model}:streamGenerateContent?alt=sse" # rubocop:disable Layout/LineLength
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# Google Vertex AI implementation
|
6
|
+
class VertexAI < Gemini
|
7
|
+
include VertexAI::Chat
|
8
|
+
include VertexAI::Streaming
|
9
|
+
include VertexAI::Embeddings
|
10
|
+
include VertexAI::Models
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
super
|
14
|
+
@authorizer = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def api_base
|
18
|
+
"https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
|
19
|
+
end
|
20
|
+
|
21
|
+
def headers
|
22
|
+
{
|
23
|
+
'Authorization' => "Bearer #{access_token}"
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def configuration_requirements
|
29
|
+
%i[vertexai_project_id vertexai_location]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def access_token
|
36
|
+
return 'test-token' if defined?(VCR) && !VCR.current_cassette.recording?
|
37
|
+
|
38
|
+
initialize_authorizer unless @authorizer
|
39
|
+
@authorizer.fetch_access_token!['access_token']
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize_authorizer
|
43
|
+
require 'googleauth'
|
44
|
+
@authorizer = ::Google::Auth.get_application_default(
|
45
|
+
scope: [
|
46
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
47
|
+
'https://www.googleapis.com/auth/generative-language.retriever'
|
48
|
+
]
|
49
|
+
)
|
50
|
+
rescue LoadError
|
51
|
+
raise Error, 'The googleauth gem is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,166 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class XAI
|
6
|
+
# Determines capabilities and pricing for xAI (Grok) models
|
7
|
+
# - https://docs.x.ai/docs/models
|
8
|
+
module Capabilities
|
9
|
+
module_function
|
10
|
+
|
11
|
+
# rubocop:disable Naming/VariableNumber
|
12
|
+
MODEL_PATTERNS = {
|
13
|
+
grok_2: /^grok-2(?!-vision)/,
|
14
|
+
grok_2_vision: /^grok-2-vision/,
|
15
|
+
grok_2_image: /^grok-2-image/,
|
16
|
+
grok_3: /^grok-3(?!-(?:fast|mini))/,
|
17
|
+
grok_3_fast: /^grok-3-fast/,
|
18
|
+
grok_3_mini: /^grok-3-mini(?!-fast)/,
|
19
|
+
grok_3_mini_fast: /^grok-3-mini-fast/,
|
20
|
+
grok_4: /^grok-4/
|
21
|
+
}.freeze
|
22
|
+
# rubocop:enable Naming/VariableNumber
|
23
|
+
|
24
|
+
def context_window_for(model_id)
|
25
|
+
case model_family(model_id)
|
26
|
+
when 'grok_4' then 256_000
|
27
|
+
when 'grok_2_vision' then 32_768
|
28
|
+
else 131_072
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def max_tokens_for(_model_id)
|
33
|
+
4_096
|
34
|
+
end
|
35
|
+
|
36
|
+
def supports_vision?(model_id)
|
37
|
+
case model_family(model_id)
|
38
|
+
when 'grok_2_vision' then true
|
39
|
+
else false
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def supports_functions?(model_id)
|
44
|
+
model_family(model_id) != 'grok_2_image'
|
45
|
+
end
|
46
|
+
|
47
|
+
def supports_structured_output?(model_id)
|
48
|
+
model_family(model_id) != 'grok_2_image'
|
49
|
+
end
|
50
|
+
|
51
|
+
def supports_json_mode?(model_id)
|
52
|
+
supports_structured_output?(model_id)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Pricing from API data (per million tokens)
|
56
|
+
# rubocop:disable Naming/VariableNumber
|
57
|
+
PRICES = {
|
58
|
+
grok_2: { input: 2.0, output: 10.0 },
|
59
|
+
grok_2_vision: { input: 2.0, output: 10.0 },
|
60
|
+
grok_3: { input: 3.0, output: 15.0, cached_input: 0.75 },
|
61
|
+
grok_3_fast: { input: 5.0, output: 25.0, cached_input: 1.25 },
|
62
|
+
grok_3_mini: { input: 0.3, output: 0.5, cached_input: 0.075 },
|
63
|
+
grok_3_mini_fast: { input: 0.6, output: 4.0, cached_input: 0.15 },
|
64
|
+
grok_4: { input: 3.0, output: 15.0, cached_input: 0.75 }
|
65
|
+
}.freeze
|
66
|
+
# rubocop:enable Naming/VariableNumber
|
67
|
+
|
68
|
+
def model_family(model_id)
|
69
|
+
MODEL_PATTERNS.each do |family, pattern|
|
70
|
+
return family.to_s if model_id.match?(pattern)
|
71
|
+
end
|
72
|
+
'other'
|
73
|
+
end
|
74
|
+
|
75
|
+
def input_price_for(model_id)
|
76
|
+
family = model_family(model_id).to_sym
|
77
|
+
prices = PRICES.fetch(family, { input: default_input_price })
|
78
|
+
prices[:input] || default_input_price
|
79
|
+
end
|
80
|
+
|
81
|
+
def cached_input_price_for(model_id)
|
82
|
+
family = model_family(model_id).to_sym
|
83
|
+
prices = PRICES.fetch(family, {})
|
84
|
+
prices[:cached_input]
|
85
|
+
end
|
86
|
+
|
87
|
+
def output_price_for(model_id)
|
88
|
+
family = model_family(model_id).to_sym
|
89
|
+
prices = PRICES.fetch(family, { output: default_output_price })
|
90
|
+
prices[:output] || default_output_price
|
91
|
+
end
|
92
|
+
|
93
|
+
def model_type(model_id)
|
94
|
+
return 'image' if model_family(model_id) == 'grok_2_image'
|
95
|
+
|
96
|
+
'chat'
|
97
|
+
end
|
98
|
+
|
99
|
+
def default_input_price
|
100
|
+
2.0
|
101
|
+
end
|
102
|
+
|
103
|
+
def default_output_price
|
104
|
+
10.0
|
105
|
+
end
|
106
|
+
|
107
|
+
def format_display_name(model_id)
|
108
|
+
model_id.then { |id| humanize(id) }
|
109
|
+
.then { |name| apply_special_formatting(name) }
|
110
|
+
end
|
111
|
+
|
112
|
+
def humanize(id)
|
113
|
+
id.tr('-', ' ')
|
114
|
+
.split
|
115
|
+
.map(&:capitalize)
|
116
|
+
.join(' ')
|
117
|
+
end
|
118
|
+
|
119
|
+
def apply_special_formatting(name)
|
120
|
+
name
|
121
|
+
.gsub(/^Grok /, 'Grok-')
|
122
|
+
.gsub(/(\d{4}) (\d{2}) (\d{2})/, '\1-\2-\3')
|
123
|
+
end
|
124
|
+
|
125
|
+
def modalities_for(model_id)
|
126
|
+
modalities = {
|
127
|
+
input: ['text'],
|
128
|
+
output: []
|
129
|
+
}
|
130
|
+
|
131
|
+
modalities[:output] << 'text' if model_type(model_id) == 'chat'
|
132
|
+
|
133
|
+
# Vision support
|
134
|
+
modalities[:input] << 'image' if supports_vision?(model_id)
|
135
|
+
|
136
|
+
modalities
|
137
|
+
end
|
138
|
+
|
139
|
+
def capabilities_for(model_id)
|
140
|
+
capabilities = []
|
141
|
+
|
142
|
+
# Common capabilities
|
143
|
+
capabilities << 'streaming'
|
144
|
+
capabilities << 'function_calling' if supports_functions?(model_id)
|
145
|
+
capabilities << 'structured_output' if supports_structured_output?(model_id)
|
146
|
+
|
147
|
+
capabilities
|
148
|
+
end
|
149
|
+
|
150
|
+
def pricing_for(model_id)
|
151
|
+
standard_pricing = {
|
152
|
+
input_per_million: input_price_for(model_id),
|
153
|
+
output_per_million: output_price_for(model_id)
|
154
|
+
}
|
155
|
+
|
156
|
+
# Add cached pricing if available
|
157
|
+
cached_price = cached_input_price_for(model_id)
|
158
|
+
standard_pricing[:cached_input_per_million] = cached_price if cached_price
|
159
|
+
|
160
|
+
# Pricing structure
|
161
|
+
{ text_tokens: { standard: standard_pricing } }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class XAI
|
6
|
+
# Chat implementation for xAI
|
7
|
+
# https://docs.x.ai/docs/api-reference#chat-completions
|
8
|
+
module Chat
|
9
|
+
def format_role(role)
|
10
|
+
role.to_s
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
class XAI
|
6
|
+
# Model definitions for xAI API
|
7
|
+
# https://docs.x.ai/docs/api-reference#list-language-models
|
8
|
+
# https://docs.x.ai/docs/api-reference#list-image-generation-models
|
9
|
+
module Models
|
10
|
+
module_function
|
11
|
+
|
12
|
+
# NOTE: We pull models list from two endpoints here as these provide
|
13
|
+
# detailed, modality, capability and cost information for each
|
14
|
+
# model that we can leverage which the generic OpenAI compatible
|
15
|
+
# /models endpoint does not provide.
|
16
|
+
def models_url
|
17
|
+
%w[language-models image-generation-models]
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse_list_models_response(response, slug, capabilities)
|
21
|
+
data = response.body
|
22
|
+
return [] if data.empty?
|
23
|
+
|
24
|
+
data['models']&.map do |model_data|
|
25
|
+
model_id = model_data['id']
|
26
|
+
|
27
|
+
Model::Info.new(
|
28
|
+
id: model_id,
|
29
|
+
name: capabilities.format_display_name(model_id),
|
30
|
+
provider: slug,
|
31
|
+
family: capabilities.model_family(model_id),
|
32
|
+
modalities: {
|
33
|
+
input: model_data['input_modalities'] | capabilities.modalities_for(model_id)[:input],
|
34
|
+
output: model_data['output_modalities'] | capabilities.modalities_for(model_id)[:output]
|
35
|
+
},
|
36
|
+
context_window: capabilities.context_window_for(model_id),
|
37
|
+
capabilities: capabilities.capabilities_for(model_id),
|
38
|
+
pricing: capabilities.pricing_for(model_id),
|
39
|
+
metadata: {
|
40
|
+
aliases: model_data['aliases']
|
41
|
+
}
|
42
|
+
)
|
43
|
+
end || []
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module Providers
|
5
|
+
# xAI API integration
|
6
|
+
class XAI < OpenAIBase
|
7
|
+
include XAI::Capabilities
|
8
|
+
include XAI::Chat
|
9
|
+
include XAI::Models
|
10
|
+
|
11
|
+
def api_base
|
12
|
+
'https://api.x.ai/v1'
|
13
|
+
end
|
14
|
+
|
15
|
+
def headers
|
16
|
+
{
|
17
|
+
'Authorization' => "Bearer #{@config.xai_api_key}",
|
18
|
+
'Content-Type' => 'application/json'
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
# xAI uses a different error format than OpenAI
|
23
|
+
# {"code": "...", "error": "..."}
|
24
|
+
def parse_error(response)
|
25
|
+
return if response.body.empty?
|
26
|
+
|
27
|
+
body = try_parse_json(response.body)
|
28
|
+
case body
|
29
|
+
when Hash then body['error']
|
30
|
+
when Array then body.map { |part| part['error'] }.join('. ')
|
31
|
+
else body
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def capabilities
|
37
|
+
XAI::Capabilities
|
38
|
+
end
|
39
|
+
|
40
|
+
def configuration_requirements
|
41
|
+
%i[xai_api_key]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/ruby_llm/railtie.rb
CHANGED
@@ -3,15 +3,31 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
# Rails integration for RubyLLM
|
5
5
|
class Railtie < Rails::Railtie
|
6
|
+
initializer 'ruby_llm.inflections' do
|
7
|
+
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
8
|
+
inflect.acronym 'LLM'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
6
12
|
initializer 'ruby_llm.active_record' do
|
7
13
|
ActiveSupport.on_load :active_record do
|
8
|
-
|
14
|
+
if RubyLLM.config.use_new_acts_as
|
15
|
+
require 'ruby_llm/active_record/acts_as'
|
16
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAs
|
17
|
+
else
|
18
|
+
require 'ruby_llm/active_record/acts_as_legacy'
|
19
|
+
::ActiveRecord::Base.include RubyLLM::ActiveRecord::ActsAsLegacy
|
20
|
+
|
21
|
+
Rails.logger.warn(
|
22
|
+
"\n!!! RubyLLM's legacy acts_as API is deprecated and will be removed in RubyLLM 2.0.0. " \
|
23
|
+
"Please consult the migration guide at https://rubyllm.com/upgrading-to-1-7/\n"
|
24
|
+
)
|
25
|
+
end
|
9
26
|
end
|
10
27
|
end
|
11
28
|
|
12
|
-
|
13
|
-
|
14
|
-
require 'generators/ruby_llm/install_generator'
|
29
|
+
rake_tasks do
|
30
|
+
load 'tasks/ruby_llm.rake'
|
15
31
|
end
|
16
32
|
end
|
17
33
|
end
|
@@ -2,29 +2,29 @@
|
|
2
2
|
|
3
3
|
module RubyLLM
|
4
4
|
# Assembles streaming responses from LLMs into complete messages.
|
5
|
-
# Handles the complexities of accumulating content and tool calls
|
6
|
-
# from partial chunks while tracking token usage.
|
7
5
|
class StreamAccumulator
|
8
6
|
attr_reader :content, :model_id, :tool_calls
|
9
7
|
|
10
8
|
def initialize
|
11
|
-
@content =
|
9
|
+
@content = nil
|
12
10
|
@tool_calls = {}
|
13
11
|
@input_tokens = 0
|
14
12
|
@output_tokens = 0
|
15
13
|
@cached_tokens = 0
|
16
14
|
@cache_creation_tokens = 0
|
17
15
|
@latest_tool_call_id = nil
|
16
|
+
@reasoning_id = nil
|
18
17
|
end
|
19
18
|
|
20
19
|
def add(chunk)
|
21
20
|
RubyLLM.logger.debug chunk.inspect if RubyLLM.config.log_stream_debug
|
22
21
|
@model_id ||= chunk.model_id
|
22
|
+
@reasoning_id ||= chunk.reasoning_id
|
23
23
|
|
24
24
|
if chunk.tool_call?
|
25
25
|
accumulate_tool_calls chunk.tool_calls
|
26
26
|
else
|
27
|
-
|
27
|
+
accumulate_content(chunk.content)
|
28
28
|
end
|
29
29
|
|
30
30
|
count_tokens chunk
|
@@ -32,27 +32,85 @@ module RubyLLM
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def to_message(response)
|
35
|
+
content = final_content
|
36
|
+
associate_reasoning_with_images(content)
|
37
|
+
|
35
38
|
Message.new(
|
36
39
|
role: :assistant,
|
37
|
-
content: content
|
40
|
+
content: content,
|
38
41
|
model_id: model_id,
|
39
42
|
tool_calls: tool_calls_from_stream,
|
40
|
-
input_tokens: @input_tokens
|
41
|
-
output_tokens: @output_tokens
|
42
|
-
cached_tokens: @cached_tokens
|
43
|
-
cache_creation_tokens: @cache_creation_tokens
|
43
|
+
input_tokens: positive_or_nil(@input_tokens),
|
44
|
+
output_tokens: positive_or_nil(@output_tokens),
|
45
|
+
cached_tokens: positive_or_nil(@cached_tokens),
|
46
|
+
cache_creation_tokens: positive_or_nil(@cache_creation_tokens),
|
44
47
|
raw: response
|
45
48
|
)
|
46
49
|
end
|
47
50
|
|
48
51
|
private
|
49
52
|
|
53
|
+
def associate_reasoning_with_images(content)
|
54
|
+
return unless @reasoning_id && content.is_a?(Content) && content.attachments.any?
|
55
|
+
|
56
|
+
content.attachments.each do |attachment|
|
57
|
+
attachment.instance_variable_set(:@reasoning_id, @reasoning_id) if attachment.is_a?(ImageAttachment)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def positive_or_nil(value)
|
62
|
+
value.positive? ? value : nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def accumulate_content(new_content)
|
66
|
+
return unless new_content
|
67
|
+
|
68
|
+
if @content.nil?
|
69
|
+
@content = new_content.is_a?(String) ? +new_content : new_content
|
70
|
+
else
|
71
|
+
case [@content.class, new_content.class]
|
72
|
+
when [String, String]
|
73
|
+
@content << new_content
|
74
|
+
when [String, Content]
|
75
|
+
@content = Content.new(@content)
|
76
|
+
merge_content(new_content)
|
77
|
+
when [Content, String]
|
78
|
+
@content.instance_variable_set(:@text, (@content.text || '') + new_content)
|
79
|
+
when [Content, Content]
|
80
|
+
merge_content(new_content)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def merge_content(new_content)
|
86
|
+
current_text = @content.text || ''
|
87
|
+
new_text = new_content.text || ''
|
88
|
+
@content.instance_variable_set(:@text, current_text + new_text)
|
89
|
+
|
90
|
+
new_content.attachments.each do |attachment|
|
91
|
+
@content.attach(attachment)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def final_content
|
96
|
+
case @content
|
97
|
+
when nil
|
98
|
+
nil
|
99
|
+
when String
|
100
|
+
@content.empty? ? nil : @content
|
101
|
+
when Content
|
102
|
+
@content.text.nil? && @content.attachments.empty? ? nil : @content
|
103
|
+
else
|
104
|
+
@content
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
50
108
|
def tool_calls_from_stream
|
51
109
|
tool_calls.transform_values do |tc|
|
52
110
|
arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
|
53
111
|
JSON.parse(tc.arguments)
|
54
112
|
elsif tc.arguments.is_a?(String)
|
55
|
-
{}
|
113
|
+
{}
|
56
114
|
else
|
57
115
|
tc.arguments
|
58
116
|
end
|