dify_llm 1.9.1 → 1.14.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 +4 -4
- data/README.md +27 -8
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +4 -1
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +19 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +50 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +10 -4
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +132 -27
- data/lib/ruby_llm/active_record/chat_methods.rb +132 -28
- data/lib/ruby_llm/active_record/message_methods.rb +58 -8
- data/lib/ruby_llm/active_record/model_methods.rb +1 -1
- data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
- data/lib/ruby_llm/agent.rb +365 -0
- data/lib/ruby_llm/aliases.json +199 -62
- data/lib/ruby_llm/attachment.rb +15 -4
- data/lib/ruby_llm/chat.rb +150 -22
- data/lib/ruby_llm/configuration.rb +65 -65
- data/lib/ruby_llm/connection.rb +11 -7
- data/lib/ruby_llm/content.rb +6 -2
- data/lib/ruby_llm/error.rb +37 -1
- data/lib/ruby_llm/message.rb +43 -15
- data/lib/ruby_llm/model/info.rb +15 -13
- data/lib/ruby_llm/models.json +37560 -14094
- data/lib/ruby_llm/models.rb +321 -38
- data/lib/ruby_llm/models_schema.json +2 -2
- data/lib/ruby_llm/provider.rb +26 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
- data/lib/ruby_llm/providers/anthropic/chat.rb +149 -17
- data/lib/ruby_llm/providers/anthropic/media.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
- data/lib/ruby_llm/providers/anthropic/streaming.rb +25 -1
- data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +29 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +24 -0
- data/lib/ruby_llm/providers/azure/media.rb +45 -0
- data/lib/ruby_llm/providers/azure/models.rb +14 -0
- data/lib/ruby_llm/providers/azure.rb +148 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +122 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +357 -28
- data/lib/ruby_llm/providers/bedrock/media.rb +62 -33
- data/lib/ruby_llm/providers/bedrock/models.rb +107 -62
- data/lib/ruby_llm/providers/bedrock/streaming.rb +309 -8
- data/lib/ruby_llm/providers/bedrock.rb +69 -52
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/dify/chat.rb +82 -7
- data/lib/ruby_llm/providers/dify/media.rb +2 -2
- data/lib/ruby_llm/providers/dify/streaming.rb +26 -4
- data/lib/ruby_llm/providers/dify.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
- data/lib/ruby_llm/providers/gemini/chat.rb +88 -6
- data/lib/ruby_llm/providers/gemini/images.rb +1 -1
- data/lib/ruby_llm/providers/gemini/models.rb +2 -4
- data/lib/ruby_llm/providers/gemini/streaming.rb +34 -2
- data/lib/ruby_llm/providers/gemini/tools.rb +35 -3
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/gpustack/chat.rb +1 -1
- data/lib/ruby_llm/providers/gpustack.rb +8 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +59 -1
- data/lib/ruby_llm/providers/mistral.rb +4 -0
- data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +1 -1
- data/lib/ruby_llm/providers/ollama.rb +11 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +96 -192
- data/lib/ruby_llm/providers/openai/chat.rb +101 -7
- data/lib/ruby_llm/providers/openai/media.rb +5 -2
- data/lib/ruby_llm/providers/openai/models.rb +2 -4
- data/lib/ruby_llm/providers/openai/streaming.rb +11 -3
- data/lib/ruby_llm/providers/openai/temperature.rb +28 -0
- data/lib/ruby_llm/providers/openai/tools.rb +27 -2
- data/lib/ruby_llm/providers/openai.rb +11 -1
- data/lib/ruby_llm/providers/openrouter/chat.rb +168 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +74 -0
- data/lib/ruby_llm/providers/openrouter.rb +37 -1
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +4 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +23 -7
- data/lib/ruby_llm/providers/xai/chat.rb +15 -0
- data/lib/ruby_llm/providers/xai/models.rb +75 -0
- data/lib/ruby_llm/providers/xai.rb +32 -0
- data/lib/ruby_llm/stream_accumulator.rb +120 -18
- data/lib/ruby_llm/streaming.rb +82 -60
- data/lib/ruby_llm/thinking.rb +49 -0
- data/lib/ruby_llm/tokens.rb +47 -0
- data/lib/ruby_llm/tool.rb +49 -4
- data/lib/ruby_llm/tool_call.rb +6 -3
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +14 -8
- data/lib/tasks/models.rake +62 -23
- data/lib/tasks/release.rake +1 -1
- data/lib/tasks/ruby_llm.rake +9 -1
- data/lib/tasks/vcr.rake +33 -1
- metadata +67 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +0 -167
- data/lib/ruby_llm/providers/bedrock/signing.rb +0 -831
- data/lib/ruby_llm/providers/bedrock/streaming/base.rb +0 -51
- data/lib/ruby_llm/providers/bedrock/streaming/content_extraction.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/streaming/message_processing.rb +0 -67
- data/lib/ruby_llm/providers/bedrock/streaming/payload_processing.rb +0 -80
- data/lib/ruby_llm/providers/bedrock/streaming/prelude_handling.rb +0 -78
|
@@ -10,13 +10,22 @@ module RubyLLM
|
|
|
10
10
|
include VertexAI::Models
|
|
11
11
|
include VertexAI::Transcription
|
|
12
12
|
|
|
13
|
+
SCOPES = [
|
|
14
|
+
'https://www.googleapis.com/auth/cloud-platform',
|
|
15
|
+
'https://www.googleapis.com/auth/generative-language.retriever'
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
13
18
|
def initialize(config)
|
|
14
19
|
super
|
|
15
20
|
@authorizer = nil
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def api_base
|
|
19
|
-
|
|
24
|
+
if @config.vertexai_location.to_s == 'global'
|
|
25
|
+
'https://aiplatform.googleapis.com/v1beta1'
|
|
26
|
+
else
|
|
27
|
+
"https://#{@config.vertexai_location}-aiplatform.googleapis.com/v1beta1"
|
|
28
|
+
end
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def headers
|
|
@@ -31,6 +40,10 @@ module RubyLLM
|
|
|
31
40
|
end
|
|
32
41
|
|
|
33
42
|
class << self
|
|
43
|
+
def configuration_options
|
|
44
|
+
%i[vertexai_project_id vertexai_location vertexai_service_account_key]
|
|
45
|
+
end
|
|
46
|
+
|
|
34
47
|
def configuration_requirements
|
|
35
48
|
%i[vertexai_project_id vertexai_location]
|
|
36
49
|
end
|
|
@@ -40,12 +53,15 @@ module RubyLLM
|
|
|
40
53
|
|
|
41
54
|
def initialize_authorizer
|
|
42
55
|
require 'googleauth'
|
|
43
|
-
@authorizer =
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
56
|
+
@authorizer =
|
|
57
|
+
if @config.vertexai_service_account_key
|
|
58
|
+
::Google::Auth::ServiceAccountCredentials.make_creds(
|
|
59
|
+
json_key_io: StringIO.new(@config.vertexai_service_account_key),
|
|
60
|
+
scope: SCOPES
|
|
61
|
+
)
|
|
62
|
+
else
|
|
63
|
+
::Google::Auth.get_application_default(SCOPES)
|
|
64
|
+
end
|
|
49
65
|
rescue LoadError
|
|
50
66
|
raise Error,
|
|
51
67
|
'The googleauth gem ~> 1.15 is required for Vertex AI. Please add it to your Gemfile: gem "googleauth"'
|
|
@@ -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,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
class XAI
|
|
6
|
+
# Models metadata for xAI list models.
|
|
7
|
+
module Models
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
IMAGE_MODELS = %w[grok-2-image-1212].freeze
|
|
11
|
+
VISION_MODELS = %w[
|
|
12
|
+
grok-2-vision-1212
|
|
13
|
+
grok-4-0709
|
|
14
|
+
grok-4-fast-non-reasoning
|
|
15
|
+
grok-4-fast-reasoning
|
|
16
|
+
grok-4-1-fast-non-reasoning
|
|
17
|
+
grok-4-1-fast-reasoning
|
|
18
|
+
].freeze
|
|
19
|
+
REASONING_MODELS = %w[
|
|
20
|
+
grok-3-mini
|
|
21
|
+
grok-4-0709
|
|
22
|
+
grok-4-fast-reasoning
|
|
23
|
+
grok-4-1-fast-reasoning
|
|
24
|
+
grok-code-fast-1
|
|
25
|
+
].freeze
|
|
26
|
+
|
|
27
|
+
def parse_list_models_response(response, slug, _capabilities)
|
|
28
|
+
Array(response.body['data']).map do |model_data|
|
|
29
|
+
model_id = model_data['id']
|
|
30
|
+
|
|
31
|
+
Model::Info.new(
|
|
32
|
+
id: model_id,
|
|
33
|
+
name: format_display_name(model_id),
|
|
34
|
+
provider: slug,
|
|
35
|
+
family: 'grok',
|
|
36
|
+
created_at: model_data['created'] ? Time.at(model_data['created']) : nil,
|
|
37
|
+
context_window: nil,
|
|
38
|
+
max_output_tokens: nil,
|
|
39
|
+
modalities: modalities_for(model_id),
|
|
40
|
+
capabilities: capabilities_for(model_id),
|
|
41
|
+
pricing: {},
|
|
42
|
+
metadata: {
|
|
43
|
+
object: model_data['object'],
|
|
44
|
+
owned_by: model_data['owned_by']
|
|
45
|
+
}.compact
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def modalities_for(model_id)
|
|
51
|
+
if IMAGE_MODELS.include?(model_id)
|
|
52
|
+
{ input: ['text'], output: ['image'] }
|
|
53
|
+
else
|
|
54
|
+
input = ['text']
|
|
55
|
+
input << 'image' if VISION_MODELS.include?(model_id)
|
|
56
|
+
{ input: input, output: ['text'] }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def capabilities_for(model_id)
|
|
61
|
+
return [] if IMAGE_MODELS.include?(model_id)
|
|
62
|
+
|
|
63
|
+
capabilities = %w[streaming function_calling structured_output]
|
|
64
|
+
capabilities << 'reasoning' if REASONING_MODELS.include?(model_id)
|
|
65
|
+
capabilities << 'vision' if VISION_MODELS.include?(model_id)
|
|
66
|
+
capabilities
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def format_display_name(model_id)
|
|
70
|
+
model_id.tr('-', ' ').split.map(&:capitalize).join(' ')
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Providers
|
|
5
|
+
# xAI API integration
|
|
6
|
+
class XAI < OpenAI
|
|
7
|
+
include XAI::Chat
|
|
8
|
+
include XAI::Models
|
|
9
|
+
|
|
10
|
+
def api_base
|
|
11
|
+
'https://api.x.ai/v1'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def headers
|
|
15
|
+
{
|
|
16
|
+
'Authorization' => "Bearer #{@config.xai_api_key}",
|
|
17
|
+
'Content-Type' => 'application/json'
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class << self
|
|
22
|
+
def configuration_options
|
|
23
|
+
%i[xai_api_key]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def configuration_requirements
|
|
27
|
+
%i[xai_api_key]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -7,40 +7,48 @@ module RubyLLM
|
|
|
7
7
|
|
|
8
8
|
def initialize
|
|
9
9
|
@content = +''
|
|
10
|
+
@thinking_text = +''
|
|
11
|
+
@thinking_signature = nil
|
|
10
12
|
@tool_calls = {}
|
|
11
13
|
@input_tokens = nil
|
|
12
14
|
@output_tokens = nil
|
|
13
15
|
@cached_tokens = nil
|
|
14
16
|
@cache_creation_tokens = nil
|
|
17
|
+
@thinking_tokens = nil
|
|
18
|
+
@inside_think_tag = false
|
|
19
|
+
@pending_think_tag = +''
|
|
15
20
|
@latest_tool_call_id = nil
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
def add(chunk)
|
|
19
|
-
RubyLLM.logger.debug chunk.inspect if RubyLLM.config.log_stream_debug
|
|
24
|
+
RubyLLM.logger.debug { chunk.inspect } if RubyLLM.config.log_stream_debug
|
|
20
25
|
@model_id ||= chunk.model_id
|
|
21
26
|
@conversation_id ||= chunk.conversation_id
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
else
|
|
26
|
-
@content << (chunk.content || '')
|
|
27
|
-
end
|
|
28
|
-
|
|
28
|
+
handle_chunk_content(chunk)
|
|
29
|
+
append_thinking_from_chunk(chunk)
|
|
29
30
|
count_tokens chunk
|
|
30
|
-
RubyLLM.logger.debug inspect if RubyLLM.config.log_stream_debug
|
|
31
|
+
RubyLLM.logger.debug { inspect } if RubyLLM.config.log_stream_debug
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def to_message(response)
|
|
34
35
|
Message.new(
|
|
35
36
|
role: :assistant,
|
|
36
37
|
content: content.empty? ? nil : content,
|
|
38
|
+
thinking: Thinking.build(
|
|
39
|
+
text: @thinking_text.empty? ? nil : @thinking_text,
|
|
40
|
+
signature: @thinking_signature
|
|
41
|
+
),
|
|
42
|
+
tokens: Tokens.build(
|
|
43
|
+
input: @input_tokens,
|
|
44
|
+
output: @output_tokens,
|
|
45
|
+
cached: @cached_tokens,
|
|
46
|
+
cache_creation: @cache_creation_tokens,
|
|
47
|
+
thinking: @thinking_tokens
|
|
48
|
+
),
|
|
37
49
|
model_id: model_id,
|
|
38
50
|
conversation_id: conversation_id,
|
|
39
51
|
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,
|
|
44
52
|
raw: response
|
|
45
53
|
)
|
|
46
54
|
end
|
|
@@ -60,26 +68,38 @@ module RubyLLM
|
|
|
60
68
|
ToolCall.new(
|
|
61
69
|
id: tc.id,
|
|
62
70
|
name: tc.name,
|
|
63
|
-
arguments: arguments
|
|
71
|
+
arguments: arguments,
|
|
72
|
+
thought_signature: tc.thought_signature
|
|
64
73
|
)
|
|
65
74
|
end
|
|
66
75
|
end
|
|
67
76
|
|
|
68
|
-
def accumulate_tool_calls(new_tool_calls)
|
|
69
|
-
RubyLLM.logger.debug "Accumulating tool calls: #{new_tool_calls}" if RubyLLM.config.log_stream_debug
|
|
77
|
+
def accumulate_tool_calls(new_tool_calls) # rubocop:disable Metrics/PerceivedComplexity
|
|
78
|
+
RubyLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if RubyLLM.config.log_stream_debug
|
|
70
79
|
new_tool_calls.each_value do |tool_call|
|
|
71
80
|
if tool_call.id
|
|
72
81
|
tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
|
|
73
|
-
tool_call_arguments = tool_call.arguments
|
|
82
|
+
tool_call_arguments = tool_call.arguments
|
|
83
|
+
if tool_call_arguments.nil? || (tool_call_arguments.respond_to?(:empty?) && tool_call_arguments.empty?)
|
|
84
|
+
tool_call_arguments = +''
|
|
85
|
+
end
|
|
74
86
|
@tool_calls[tool_call.id] = ToolCall.new(
|
|
75
87
|
id: tool_call_id,
|
|
76
88
|
name: tool_call.name,
|
|
77
|
-
arguments: tool_call_arguments
|
|
89
|
+
arguments: tool_call_arguments,
|
|
90
|
+
thought_signature: tool_call.thought_signature
|
|
78
91
|
)
|
|
79
92
|
@latest_tool_call_id = tool_call.id
|
|
80
93
|
else
|
|
81
94
|
existing = @tool_calls[@latest_tool_call_id]
|
|
82
|
-
|
|
95
|
+
if existing
|
|
96
|
+
fragment = tool_call.arguments
|
|
97
|
+
fragment = '' if fragment.nil?
|
|
98
|
+
existing.arguments << fragment
|
|
99
|
+
if tool_call.thought_signature && existing.thought_signature.nil?
|
|
100
|
+
existing.thought_signature = tool_call.thought_signature
|
|
101
|
+
end
|
|
102
|
+
end
|
|
83
103
|
end
|
|
84
104
|
end
|
|
85
105
|
end
|
|
@@ -98,6 +118,88 @@ module RubyLLM
|
|
|
98
118
|
@output_tokens = chunk.output_tokens if chunk.output_tokens
|
|
99
119
|
@cached_tokens = chunk.cached_tokens if chunk.cached_tokens
|
|
100
120
|
@cache_creation_tokens = chunk.cache_creation_tokens if chunk.cache_creation_tokens
|
|
121
|
+
@thinking_tokens = chunk.thinking_tokens if chunk.thinking_tokens
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def handle_chunk_content(chunk)
|
|
125
|
+
return accumulate_tool_calls(chunk.tool_calls) if chunk.tool_call?
|
|
126
|
+
|
|
127
|
+
content_text = chunk.content || ''
|
|
128
|
+
if content_text.is_a?(String)
|
|
129
|
+
append_text_with_thinking(content_text)
|
|
130
|
+
else
|
|
131
|
+
@content << content_text.to_s
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def append_text_with_thinking(text)
|
|
136
|
+
content_chunk, thinking_chunk = extract_think_tags(text)
|
|
137
|
+
@content << content_chunk
|
|
138
|
+
@thinking_text << thinking_chunk if thinking_chunk
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def append_thinking_from_chunk(chunk)
|
|
142
|
+
thinking = chunk.thinking
|
|
143
|
+
return unless thinking
|
|
144
|
+
|
|
145
|
+
@thinking_text << thinking.text.to_s if thinking.text
|
|
146
|
+
@thinking_signature ||= thinking.signature # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def extract_think_tags(text)
|
|
150
|
+
start_tag = '<think>'
|
|
151
|
+
end_tag = '</think>'
|
|
152
|
+
remaining = @pending_think_tag + text
|
|
153
|
+
@pending_think_tag = +''
|
|
154
|
+
|
|
155
|
+
output = +''
|
|
156
|
+
thinking = +''
|
|
157
|
+
|
|
158
|
+
until remaining.empty?
|
|
159
|
+
remaining = if @inside_think_tag
|
|
160
|
+
consume_think_content(remaining, end_tag, thinking)
|
|
161
|
+
else
|
|
162
|
+
consume_non_think_content(remaining, start_tag, output)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
[output, thinking.empty? ? nil : thinking]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def consume_think_content(remaining, end_tag, thinking)
|
|
170
|
+
end_index = remaining.index(end_tag)
|
|
171
|
+
if end_index
|
|
172
|
+
thinking << remaining.slice(0, end_index)
|
|
173
|
+
@inside_think_tag = false
|
|
174
|
+
remaining.slice((end_index + end_tag.length)..) || +''
|
|
175
|
+
else
|
|
176
|
+
suffix_len = longest_suffix_prefix(remaining, end_tag)
|
|
177
|
+
thinking << remaining.slice(0, remaining.length - suffix_len)
|
|
178
|
+
@pending_think_tag = remaining.slice(-suffix_len, suffix_len)
|
|
179
|
+
+''
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def consume_non_think_content(remaining, start_tag, output)
|
|
184
|
+
start_index = remaining.index(start_tag)
|
|
185
|
+
if start_index
|
|
186
|
+
output << remaining.slice(0, start_index)
|
|
187
|
+
@inside_think_tag = true
|
|
188
|
+
remaining.slice((start_index + start_tag.length)..) || +''
|
|
189
|
+
else
|
|
190
|
+
suffix_len = longest_suffix_prefix(remaining, start_tag)
|
|
191
|
+
output << remaining.slice(0, remaining.length - suffix_len)
|
|
192
|
+
@pending_think_tag = remaining.slice(-suffix_len, suffix_len)
|
|
193
|
+
+''
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def longest_suffix_prefix(text, tag)
|
|
198
|
+
max = [text.length, tag.length - 1].min
|
|
199
|
+
max.downto(1) do |len|
|
|
200
|
+
return len if text.end_with?(tag[0, len])
|
|
201
|
+
end
|
|
202
|
+
0
|
|
101
203
|
end
|
|
102
204
|
end
|
|
103
205
|
end
|
data/lib/ruby_llm/streaming.rb
CHANGED
|
@@ -24,13 +24,13 @@ module RubyLLM
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
message = accumulator.to_message(response)
|
|
27
|
-
RubyLLM.logger.debug "Stream completed: #{message.content}"
|
|
27
|
+
RubyLLM.logger.debug { "Stream completed: #{message.content}" }
|
|
28
28
|
message
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def handle_stream(&block)
|
|
32
|
-
|
|
33
|
-
block.call(build_chunk(data)) if data
|
|
32
|
+
build_on_data_handler do |data|
|
|
33
|
+
block.call(build_chunk(data)) if data.is_a?(Hash)
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
|
|
@@ -40,74 +40,52 @@ module RubyLLM
|
|
|
40
40
|
Faraday::VERSION.start_with?('1')
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
-
def
|
|
43
|
+
def build_on_data_handler(&handler)
|
|
44
44
|
buffer = +''
|
|
45
45
|
parser = EventStreamParser::Parser.new
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
legacy_stream_processor(parser, &)
|
|
53
|
-
else
|
|
54
|
-
stream_processor(parser, buffer, &)
|
|
55
|
-
end
|
|
47
|
+
FaradayHandlers.build(
|
|
48
|
+
faraday_v1: faraday_1?,
|
|
49
|
+
on_chunk: ->(chunk, env) { process_stream_chunk(chunk, parser, env, &handler) },
|
|
50
|
+
on_failed_response: ->(chunk, env) { handle_failed_response(chunk, buffer, env) }
|
|
51
|
+
)
|
|
56
52
|
end
|
|
57
53
|
|
|
58
54
|
def process_stream_chunk(chunk, parser, env, &)
|
|
59
|
-
RubyLLM.logger.debug "Received chunk: #{chunk}" if RubyLLM.config.log_stream_debug
|
|
55
|
+
RubyLLM.logger.debug { "Received chunk: #{chunk}" } if RubyLLM.config.log_stream_debug
|
|
60
56
|
|
|
61
57
|
if error_chunk?(chunk)
|
|
62
58
|
handle_error_chunk(chunk, env)
|
|
59
|
+
elsif json_error_payload?(chunk)
|
|
60
|
+
handle_json_error_chunk(chunk, env)
|
|
63
61
|
else
|
|
64
62
|
yield handle_sse(chunk, parser, env, &)
|
|
65
63
|
end
|
|
66
64
|
end
|
|
67
65
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
process_stream_chunk(chunk, parser, nil, &block)
|
|
71
|
-
end
|
|
66
|
+
def error_chunk?(chunk)
|
|
67
|
+
chunk.start_with?('event: error')
|
|
72
68
|
end
|
|
73
69
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
if env&.status == 200
|
|
77
|
-
process_stream_chunk(chunk, parser, env, &block)
|
|
78
|
-
else
|
|
79
|
-
handle_failed_response(chunk, buffer, env)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
70
|
+
def json_error_payload?(chunk)
|
|
71
|
+
chunk.lstrip.start_with?('{') && chunk.include?('"error"')
|
|
82
72
|
end
|
|
83
73
|
|
|
84
|
-
def
|
|
85
|
-
chunk
|
|
74
|
+
def handle_json_error_chunk(chunk, env)
|
|
75
|
+
parse_error_from_json(chunk, env, 'Failed to parse JSON error chunk')
|
|
86
76
|
end
|
|
87
77
|
|
|
88
78
|
def handle_error_chunk(chunk, env)
|
|
89
79
|
error_data = chunk.split("\n")[1].delete_prefix('data: ')
|
|
90
|
-
|
|
91
|
-
parsed_data = JSON.parse(error_data)
|
|
92
|
-
|
|
93
|
-
error_response = if faraday_1?
|
|
94
|
-
Struct.new(:body, :status).new(parsed_data, status)
|
|
95
|
-
else
|
|
96
|
-
env.merge(body: parsed_data, status: status)
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
100
|
-
rescue JSON::ParserError => e
|
|
101
|
-
RubyLLM.logger.debug "Failed to parse error chunk: #{e.message}"
|
|
80
|
+
parse_error_from_json(error_data, env, 'Failed to parse error chunk')
|
|
102
81
|
end
|
|
103
82
|
|
|
104
83
|
def handle_failed_response(chunk, buffer, env)
|
|
105
84
|
buffer << chunk
|
|
106
85
|
error_data = JSON.parse(buffer)
|
|
107
|
-
|
|
108
|
-
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
86
|
+
handle_parsed_error(error_data, env)
|
|
109
87
|
rescue JSON::ParserError
|
|
110
|
-
RubyLLM.logger.debug "Accumulating error chunk: #{chunk}"
|
|
88
|
+
RubyLLM.logger.debug { "Accumulating error chunk: #{chunk}" }
|
|
111
89
|
end
|
|
112
90
|
|
|
113
91
|
def handle_sse(chunk, parser, env, &block)
|
|
@@ -116,38 +94,82 @@ module RubyLLM
|
|
|
116
94
|
when :error
|
|
117
95
|
handle_error_event(data, env)
|
|
118
96
|
else
|
|
119
|
-
yield handle_data(data, &block) unless data == '[DONE]'
|
|
97
|
+
yield handle_data(data, env, &block) unless data == '[DONE]'
|
|
120
98
|
end
|
|
121
99
|
end
|
|
122
100
|
end
|
|
123
101
|
|
|
124
|
-
def handle_data(data)
|
|
125
|
-
JSON.parse(data)
|
|
102
|
+
def handle_data(data, env)
|
|
103
|
+
parsed = JSON.parse(data)
|
|
104
|
+
return parsed unless parsed.is_a?(Hash) && parsed.key?('error')
|
|
105
|
+
|
|
106
|
+
handle_parsed_error(parsed, env)
|
|
126
107
|
rescue JSON::ParserError => e
|
|
127
|
-
RubyLLM.logger.debug "Failed to parse data chunk: #{e.message}"
|
|
108
|
+
RubyLLM.logger.debug { "Failed to parse data chunk: #{e.message}" }
|
|
128
109
|
end
|
|
129
110
|
|
|
130
111
|
def handle_error_event(data, env)
|
|
131
|
-
|
|
132
|
-
parsed_data = JSON.parse(data)
|
|
133
|
-
|
|
134
|
-
error_response = if faraday_1?
|
|
135
|
-
Struct.new(:body, :status).new(parsed_data, status)
|
|
136
|
-
else
|
|
137
|
-
env.merge(body: parsed_data, status: status)
|
|
138
|
-
end
|
|
139
|
-
|
|
140
|
-
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
141
|
-
rescue JSON::ParserError => e
|
|
142
|
-
RubyLLM.logger.debug "Failed to parse error event: #{e.message}"
|
|
112
|
+
parse_error_from_json(data, env, 'Failed to parse error event')
|
|
143
113
|
end
|
|
144
114
|
|
|
145
115
|
def parse_streaming_error(data)
|
|
146
116
|
error_data = JSON.parse(data)
|
|
147
117
|
[500, error_data['message'] || 'Unknown streaming error']
|
|
148
118
|
rescue JSON::ParserError => e
|
|
149
|
-
RubyLLM.logger.debug "Failed to parse streaming error: #{e.message}"
|
|
119
|
+
RubyLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
|
|
150
120
|
[500, "Failed to parse error: #{data}"]
|
|
151
121
|
end
|
|
122
|
+
|
|
123
|
+
def handle_parsed_error(parsed_data, env)
|
|
124
|
+
status, _message = parse_streaming_error(parsed_data.to_json)
|
|
125
|
+
error_response = build_stream_error_response(parsed_data, env, status)
|
|
126
|
+
ErrorMiddleware.parse_error(provider: self, response: error_response)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def parse_error_from_json(data, env, error_message)
|
|
130
|
+
parsed_data = JSON.parse(data)
|
|
131
|
+
handle_parsed_error(parsed_data, env)
|
|
132
|
+
rescue JSON::ParserError => e
|
|
133
|
+
RubyLLM.logger.debug { "#{error_message}: #{e.message}" }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_stream_error_response(parsed_data, env, status)
|
|
137
|
+
error_status = status || env&.status || 500
|
|
138
|
+
|
|
139
|
+
if faraday_1?
|
|
140
|
+
Struct.new(:body, :status).new(parsed_data, error_status)
|
|
141
|
+
else
|
|
142
|
+
env.merge(body: parsed_data, status: error_status)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Builds Faraday on_data handlers for different major versions.
|
|
147
|
+
module FaradayHandlers
|
|
148
|
+
module_function
|
|
149
|
+
|
|
150
|
+
def build(faraday_v1:, on_chunk:, on_failed_response:)
|
|
151
|
+
if faraday_v1
|
|
152
|
+
v1_on_data(on_chunk)
|
|
153
|
+
else
|
|
154
|
+
v2_on_data(on_chunk, on_failed_response)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def v1_on_data(on_chunk)
|
|
159
|
+
proc do |chunk, _size|
|
|
160
|
+
on_chunk.call(chunk, nil)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def v2_on_data(on_chunk, on_failed_response)
|
|
165
|
+
proc do |chunk, _bytes, env|
|
|
166
|
+
if env&.status == 200
|
|
167
|
+
on_chunk.call(chunk, env)
|
|
168
|
+
else
|
|
169
|
+
on_failed_response.call(chunk, env)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
152
174
|
end
|
|
153
175
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Represents provider thinking output.
|
|
5
|
+
class Thinking
|
|
6
|
+
attr_reader :text, :signature
|
|
7
|
+
|
|
8
|
+
def initialize(text: nil, signature: nil)
|
|
9
|
+
@text = text
|
|
10
|
+
@signature = signature
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build(text: nil, signature: nil)
|
|
14
|
+
text = nil if text.is_a?(String) && text.empty?
|
|
15
|
+
signature = nil if signature.is_a?(String) && signature.empty?
|
|
16
|
+
|
|
17
|
+
return nil if text.nil? && signature.nil?
|
|
18
|
+
|
|
19
|
+
new(text: text, signature: signature)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def pretty_print(printer)
|
|
23
|
+
printer.object_group(self) do
|
|
24
|
+
printer.breakable
|
|
25
|
+
printer.text 'text='
|
|
26
|
+
printer.pp text
|
|
27
|
+
printer.comma_breakable
|
|
28
|
+
printer.text 'signature='
|
|
29
|
+
printer.pp(signature ? '[REDACTED]' : nil)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
class Thinking
|
|
35
|
+
# Normalized config for thinking across providers.
|
|
36
|
+
class Config
|
|
37
|
+
attr_reader :effort, :budget
|
|
38
|
+
|
|
39
|
+
def initialize(effort: nil, budget: nil)
|
|
40
|
+
@effort = effort.is_a?(Symbol) ? effort.to_s : effort
|
|
41
|
+
@budget = budget
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def enabled?
|
|
45
|
+
!effort.nil? || !budget.nil?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Represents token usage for a response.
|
|
5
|
+
class Tokens
|
|
6
|
+
attr_reader :input, :output, :cached, :cache_creation, :thinking
|
|
7
|
+
|
|
8
|
+
# rubocop:disable Metrics/ParameterLists
|
|
9
|
+
def initialize(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
|
|
10
|
+
@input = input
|
|
11
|
+
@output = output
|
|
12
|
+
@cached = cached
|
|
13
|
+
@cache_creation = cache_creation
|
|
14
|
+
@thinking = thinking || reasoning
|
|
15
|
+
end
|
|
16
|
+
# rubocop:enable Metrics/ParameterLists
|
|
17
|
+
|
|
18
|
+
# rubocop:disable Metrics/ParameterLists
|
|
19
|
+
def self.build(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
|
|
20
|
+
return nil if [input, output, cached, cache_creation, thinking, reasoning].all?(&:nil?)
|
|
21
|
+
|
|
22
|
+
new(
|
|
23
|
+
input: input,
|
|
24
|
+
output: output,
|
|
25
|
+
cached: cached,
|
|
26
|
+
cache_creation: cache_creation,
|
|
27
|
+
thinking: thinking,
|
|
28
|
+
reasoning: reasoning
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
# rubocop:enable Metrics/ParameterLists
|
|
32
|
+
|
|
33
|
+
def to_h
|
|
34
|
+
{
|
|
35
|
+
input_tokens: input,
|
|
36
|
+
output_tokens: output,
|
|
37
|
+
cached_tokens: cached,
|
|
38
|
+
cache_creation_tokens: cache_creation,
|
|
39
|
+
thinking_tokens: thinking
|
|
40
|
+
}.compact
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def reasoning
|
|
44
|
+
thinking
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|