llm.rb 8.1.0 → 10.0.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/CHANGELOG.md +196 -6
- data/README.md +233 -518
- data/data/anthropic.json +278 -258
- data/data/bedrock.json +1288 -1561
- data/data/deepseek.json +38 -38
- data/data/google.json +656 -579
- data/data/openai.json +860 -818
- data/data/xai.json +243 -552
- data/data/zai.json +168 -168
- data/lib/llm/active_record/acts_as_agent.rb +5 -0
- data/lib/llm/active_record/acts_as_llm.rb +7 -8
- data/lib/llm/active_record.rb +1 -6
- data/lib/llm/agent.rb +121 -82
- data/lib/llm/context.rb +79 -74
- data/lib/llm/contract/completion.rb +45 -0
- data/lib/llm/cost.rb +81 -4
- data/lib/llm/error.rb +1 -1
- data/lib/llm/function/array.rb +8 -5
- data/lib/llm/function/call_group.rb +39 -0
- data/lib/llm/function/call_task.rb +46 -0
- data/lib/llm/function/fork/task.rb +6 -0
- data/lib/llm/function/ractor/task.rb +6 -0
- data/lib/llm/function/task.rb +10 -0
- data/lib/llm/function.rb +28 -1
- data/lib/llm/mcp/transport/http.rb +26 -46
- data/lib/llm/mcp/transport/stdio.rb +0 -8
- data/lib/llm/mcp.rb +6 -23
- data/lib/llm/provider.rb +30 -20
- data/lib/llm/providers/anthropic/error_handler.rb +6 -7
- data/lib/llm/providers/anthropic/files.rb +2 -2
- data/lib/llm/providers/anthropic/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +2 -2
- data/lib/llm/providers/anthropic.rb +1 -1
- data/lib/llm/providers/bedrock/error_handler.rb +8 -9
- data/lib/llm/providers/bedrock/models.rb +13 -13
- data/lib/llm/providers/bedrock/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/bedrock/stream_parser.rb +2 -2
- data/lib/llm/providers/bedrock.rb +1 -1
- data/lib/llm/providers/google/error_handler.rb +6 -7
- data/lib/llm/providers/google/files.rb +2 -4
- data/lib/llm/providers/google/images.rb +1 -1
- data/lib/llm/providers/google/models.rb +0 -2
- data/lib/llm/providers/google/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/google/stream_parser.rb +2 -2
- data/lib/llm/providers/google.rb +1 -1
- data/lib/llm/providers/ollama/error_handler.rb +6 -7
- data/lib/llm/providers/ollama/models.rb +0 -2
- data/lib/llm/providers/ollama/response_adapter/completion.rb +30 -0
- data/lib/llm/providers/ollama.rb +1 -1
- data/lib/llm/providers/openai/audio.rb +3 -3
- data/lib/llm/providers/openai/error_handler.rb +6 -7
- data/lib/llm/providers/openai/files.rb +2 -2
- data/lib/llm/providers/openai/images.rb +3 -3
- data/lib/llm/providers/openai/models.rb +1 -1
- data/lib/llm/providers/openai/response_adapter/completion.rb +42 -0
- data/lib/llm/providers/openai/response_adapter/responds.rb +39 -0
- data/lib/llm/providers/openai/responses/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/responses.rb +2 -2
- data/lib/llm/providers/openai/stream_parser.rb +2 -2
- data/lib/llm/providers/openai/vector_stores.rb +1 -1
- data/lib/llm/providers/openai.rb +1 -1
- data/lib/llm/response.rb +10 -8
- data/lib/llm/schema.rb +11 -0
- data/lib/llm/sequel/agent.rb +5 -0
- data/lib/llm/sequel/plugin.rb +8 -14
- data/lib/llm/stream/queue.rb +15 -42
- data/lib/llm/stream.rb +15 -40
- data/lib/llm/tool/param.rb +1 -8
- data/lib/llm/transport/execution.rb +67 -0
- data/lib/llm/transport/http.rb +134 -0
- data/lib/llm/transport/persistent_http.rb +152 -0
- data/lib/llm/transport/response/http.rb +113 -0
- data/lib/llm/transport/response.rb +112 -0
- data/lib/llm/{provider/transport/http → transport}/stream_decoder.rb +8 -4
- data/lib/llm/transport.rb +139 -0
- data/lib/llm/usage.rb +14 -5
- data/lib/llm/utils.rb +24 -14
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +3 -12
- data/llm.gemspec +2 -16
- metadata +13 -20
- data/lib/llm/bot.rb +0 -3
- data/lib/llm/provider/transport/http/execution.rb +0 -115
- data/lib/llm/provider/transport/http/interruptible.rb +0 -114
- data/lib/llm/provider/transport/http.rb +0 -145
data/lib/llm/cost.rb
CHANGED
|
@@ -2,19 +2,96 @@
|
|
|
2
2
|
|
|
3
3
|
##
|
|
4
4
|
# The {LLM::Cost LLM::Cost} class represents an approximate
|
|
5
|
-
# cost breakdown for a provider request. It stores
|
|
6
|
-
#
|
|
5
|
+
# cost breakdown for a provider request. It stores input,
|
|
6
|
+
# output, input audio, output audio, input image, cache read, cache write,
|
|
7
|
+
# and reasoning costs separately and can return the total.
|
|
7
8
|
#
|
|
8
9
|
# @attr [Float] input_costs
|
|
9
10
|
# Returns the input cost
|
|
10
11
|
# @attr [Float] output_costs
|
|
11
12
|
# Returns the output cost
|
|
12
|
-
|
|
13
|
+
# @attr [Float, nil] input_audio_costs
|
|
14
|
+
# Returns the input audio cost, or nil when no input audio tokens
|
|
15
|
+
# were used
|
|
16
|
+
# @attr [Float, nil] output_audio_costs
|
|
17
|
+
# Returns the output audio cost, or nil when no output audio tokens
|
|
18
|
+
# were used
|
|
19
|
+
# @attr [Float, nil] input_image_costs
|
|
20
|
+
# Returns the input image cost, or nil when no input image tokens
|
|
21
|
+
# were used
|
|
22
|
+
# @attr [Float, nil] cache_read_costs
|
|
23
|
+
# Returns the cache read cost, or nil when no cache tokens
|
|
24
|
+
# were used
|
|
25
|
+
# @attr [Float, nil] cache_write_costs
|
|
26
|
+
# Returns the cache write cost, or nil when no cache creation
|
|
27
|
+
# tokens were used
|
|
28
|
+
# @attr [Float, nil] reasoning_costs
|
|
29
|
+
# Returns the reasoning cost, or nil when no reasoning tokens
|
|
30
|
+
# were used
|
|
31
|
+
class LLM::Cost < Struct.new(
|
|
32
|
+
:input_costs, :output_costs,
|
|
33
|
+
:input_audio_costs, :output_audio_costs,
|
|
34
|
+
:cache_read_costs, :cache_write_costs,
|
|
35
|
+
:input_image_costs, :reasoning_costs,
|
|
36
|
+
keyword_init: true
|
|
37
|
+
)
|
|
38
|
+
##
|
|
39
|
+
# Build a cost breakdown from token usage and model pricing.
|
|
40
|
+
# @param [LLM::Context]
|
|
41
|
+
# Context used to resolve provider, model, and token usage
|
|
42
|
+
# @return [LLM::Cost]
|
|
43
|
+
def self.from(ctx)
|
|
44
|
+
pricing = LLM.registry_for(ctx.llm).cost(model: ctx.model)
|
|
45
|
+
new(
|
|
46
|
+
input_costs: price(pricing.input, ctx.usage.input_tokens),
|
|
47
|
+
output_costs: price(pricing.output, ctx.usage.output_tokens),
|
|
48
|
+
input_audio_costs: price(pricing.input_audio, ctx.usage.input_audio_tokens),
|
|
49
|
+
output_audio_costs: price(pricing.output_audio, ctx.usage.output_audio_tokens),
|
|
50
|
+
input_image_costs: price(pricing.input, ctx.usage.input_image_tokens),
|
|
51
|
+
cache_read_costs: price(pricing.cache_read, ctx.usage.cache_read_tokens),
|
|
52
|
+
cache_write_costs: price(pricing.cache_write, ctx.usage.cache_write_tokens),
|
|
53
|
+
reasoning_costs: price(pricing.output, ctx.usage.reasoning_tokens)
|
|
54
|
+
)
|
|
55
|
+
rescue LLM::NoSuchModelError, LLM::NoSuchRegistryError
|
|
56
|
+
new
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# @api private
|
|
61
|
+
def self.price(rate, tokens)
|
|
62
|
+
return if tokens.nil? || tokens.to_i.zero?
|
|
63
|
+
return if rate.nil? || rate.to_f.zero?
|
|
64
|
+
((rate.to_f / 1_000_000.0) * tokens.to_i).round(12)
|
|
65
|
+
end
|
|
66
|
+
private_class_method :price
|
|
67
|
+
|
|
13
68
|
##
|
|
14
69
|
# @return [Float]
|
|
15
70
|
# Returns the total cost
|
|
16
71
|
def total
|
|
17
|
-
|
|
72
|
+
[
|
|
73
|
+
input_costs, output_costs,
|
|
74
|
+
input_audio_costs, output_audio_costs,
|
|
75
|
+
cache_read_costs, cache_write_costs,
|
|
76
|
+
input_image_costs, reasoning_costs
|
|
77
|
+
].compact.sum.round(12)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
##
|
|
81
|
+
# @return [Hash]
|
|
82
|
+
# Returns a hash with the non-nil cost components and the total
|
|
83
|
+
def to_h
|
|
84
|
+
{
|
|
85
|
+
input: input_costs,
|
|
86
|
+
output: output_costs,
|
|
87
|
+
input_audio: input_audio_costs,
|
|
88
|
+
output_audio: output_audio_costs,
|
|
89
|
+
input_image: input_image_costs,
|
|
90
|
+
cache_read: cache_read_costs,
|
|
91
|
+
cache_write: cache_write_costs,
|
|
92
|
+
reasoning: reasoning_costs,
|
|
93
|
+
total: total
|
|
94
|
+
}.compact
|
|
18
95
|
end
|
|
19
96
|
|
|
20
97
|
##
|
data/lib/llm/error.rb
CHANGED
data/lib/llm/function/array.rb
CHANGED
|
@@ -18,21 +18,23 @@ class LLM::Function
|
|
|
18
18
|
|
|
19
19
|
##
|
|
20
20
|
# Calls all functions in a collection concurrently.
|
|
21
|
-
# This method returns an
|
|
22
|
-
#
|
|
23
|
-
# that can be waited on to access the return values.
|
|
21
|
+
# This method returns an execution group that can be
|
|
22
|
+
# waited on to access the return values.
|
|
24
23
|
#
|
|
25
24
|
# @param [Symbol] strategy
|
|
26
25
|
# Controls concurrency strategy:
|
|
26
|
+
# - `:call`: Call functions sequentially without spawning
|
|
27
27
|
# - `:thread`: Use threads
|
|
28
28
|
# - `:task`: Use async tasks (requires async gem)
|
|
29
29
|
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
30
30
|
# - `:fork`: Use forked child processes
|
|
31
31
|
# - `:ractor`: Use Ruby ractors (class-based tools only; MCP tools are not supported)
|
|
32
32
|
#
|
|
33
|
-
# @return [LLM::Function::ThreadGroup, LLM::Function::TaskGroup, LLM::Function::FiberGroup, LLM::Function::Ractor::Group]
|
|
33
|
+
# @return [LLM::Function::CallGroup, LLM::Function::ThreadGroup, LLM::Function::TaskGroup, LLM::Function::FiberGroup, LLM::Function::Ractor::Group]
|
|
34
34
|
def spawn(strategy)
|
|
35
35
|
case strategy
|
|
36
|
+
when :call
|
|
37
|
+
CallGroup.new(self)
|
|
36
38
|
when :task
|
|
37
39
|
TaskGroup.new(map { |fn| fn.spawn(:task) })
|
|
38
40
|
when :thread
|
|
@@ -44,7 +46,7 @@ class LLM::Function
|
|
|
44
46
|
when :ractor
|
|
45
47
|
Ractor::Group.new(map { |fn| fn.spawn(:ractor) })
|
|
46
48
|
else
|
|
47
|
-
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, :fork, or :ractor"
|
|
49
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :call, :thread, :task, :fiber, :fork, or :ractor"
|
|
48
50
|
end
|
|
49
51
|
end
|
|
50
52
|
|
|
@@ -54,6 +56,7 @@ class LLM::Function
|
|
|
54
56
|
#
|
|
55
57
|
# @param [Symbol] strategy
|
|
56
58
|
# Controls concurrency strategy:
|
|
59
|
+
# - `:call`: Call each function sequentially through a call group
|
|
57
60
|
# - `:thread`: Use threads
|
|
58
61
|
# - `:task`: Use async tasks (requires async gem)
|
|
59
62
|
# - `:fiber`: Use scheduler-backed fibers (requires Fiber.scheduler)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Function
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Function::CallGroup} class wraps an array of
|
|
6
|
+
# {LLM::Function} objects for sequential execution.
|
|
7
|
+
#
|
|
8
|
+
# It provides the same basic interface as the concurrent group
|
|
9
|
+
# wrappers so callers can flow through `spawn(strategy).wait`
|
|
10
|
+
# uniformly, even when the selected strategy is direct calls.
|
|
11
|
+
class CallGroup
|
|
12
|
+
##
|
|
13
|
+
# @param [Array<LLM::Function>] functions
|
|
14
|
+
# @return [LLM::Function::CallGroup]
|
|
15
|
+
def initialize(functions)
|
|
16
|
+
@functions = functions
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def alive?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [nil]
|
|
27
|
+
def interrupt!
|
|
28
|
+
nil
|
|
29
|
+
end
|
|
30
|
+
alias_method :cancel!, :interrupt!
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# @return [Array<LLM::Function::Return>]
|
|
34
|
+
def wait
|
|
35
|
+
@functions.map(&:call)
|
|
36
|
+
end
|
|
37
|
+
alias_method :value, :wait
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Function
|
|
4
|
+
##
|
|
5
|
+
# The {LLM::Function::CallTask} class wraps a single direct function call
|
|
6
|
+
# behind the same task-like interface used by spawned concurrency modes.
|
|
7
|
+
class CallTask
|
|
8
|
+
##
|
|
9
|
+
# @return [LLM::Function]
|
|
10
|
+
attr_reader :function
|
|
11
|
+
|
|
12
|
+
##
|
|
13
|
+
# @param [LLM::Function] function
|
|
14
|
+
# @return [LLM::Function::CallTask]
|
|
15
|
+
def initialize(function)
|
|
16
|
+
@function = function
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
##
|
|
20
|
+
# @return [Boolean]
|
|
21
|
+
def alive?
|
|
22
|
+
false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
##
|
|
26
|
+
# @return [nil]
|
|
27
|
+
def interrupt!
|
|
28
|
+
function.interrupt!
|
|
29
|
+
nil
|
|
30
|
+
end
|
|
31
|
+
alias_method :cancel!, :interrupt!
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# @return [LLM::Function::Return]
|
|
35
|
+
def wait
|
|
36
|
+
function.call
|
|
37
|
+
end
|
|
38
|
+
alias_method :value, :wait
|
|
39
|
+
|
|
40
|
+
##
|
|
41
|
+
# @return [Class]
|
|
42
|
+
def group_class
|
|
43
|
+
LLM::Function::TaskGroup
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/llm/function/task.rb
CHANGED
|
@@ -53,6 +53,16 @@ class LLM::Function
|
|
|
53
53
|
end
|
|
54
54
|
alias_method :value, :wait
|
|
55
55
|
|
|
56
|
+
##
|
|
57
|
+
# @return [Class]
|
|
58
|
+
def group_class
|
|
59
|
+
case task
|
|
60
|
+
when Thread then LLM::Function::ThreadGroup
|
|
61
|
+
when Fiber then LLM::Function::FiberGroup
|
|
62
|
+
else LLM::Function::TaskGroup
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
56
66
|
private
|
|
57
67
|
|
|
58
68
|
def scheduler
|
data/lib/llm/function.rb
CHANGED
|
@@ -32,6 +32,8 @@ class LLM::Function
|
|
|
32
32
|
require_relative "function/registry"
|
|
33
33
|
require_relative "function/tracing"
|
|
34
34
|
require_relative "function/array"
|
|
35
|
+
require_relative "function/call_group"
|
|
36
|
+
require_relative "function/call_task"
|
|
35
37
|
require_relative "function/task"
|
|
36
38
|
require_relative "function/thread_group"
|
|
37
39
|
require_relative "function/fiber_group"
|
|
@@ -209,6 +211,7 @@ class LLM::Function
|
|
|
209
211
|
#
|
|
210
212
|
# @param [Symbol] strategy
|
|
211
213
|
# Controls concurrency strategy:
|
|
214
|
+
# - `:call`: Call the function sequentially without spawning
|
|
212
215
|
# - `:thread`: Use threads
|
|
213
216
|
# - `:task`: Use async tasks (requires async gem)
|
|
214
217
|
# - `:fork`: Use a forked child process (requires xchan.rb support)
|
|
@@ -220,6 +223,8 @@ class LLM::Function
|
|
|
220
223
|
# Returns a task whose `#value` is an {LLM::Function::Return}.
|
|
221
224
|
def spawn(strategy)
|
|
222
225
|
task = case strategy
|
|
226
|
+
when :call
|
|
227
|
+
CallTask.new(self)
|
|
223
228
|
when :task
|
|
224
229
|
LLM.require "async" unless defined?(::Async)
|
|
225
230
|
Async { call! }
|
|
@@ -240,7 +245,7 @@ class LLM::Function
|
|
|
240
245
|
span = @tracer&.on_tool_start(id:, name:, arguments:, model:)
|
|
241
246
|
Ractor::Task.new(@runner, id, name, arguments, tracer: @tracer, span:).spawn
|
|
242
247
|
else
|
|
243
|
-
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :thread, :task, :fiber, :fork, or :ractor"
|
|
248
|
+
raise ArgumentError, "Unknown strategy: #{strategy.inspect}. Expected :call, :thread, :task, :fiber, :fork, or :ractor"
|
|
244
249
|
end
|
|
245
250
|
Task.new(task, self)
|
|
246
251
|
ensure
|
|
@@ -294,6 +299,28 @@ class LLM::Function
|
|
|
294
299
|
!@called && !@cancelled
|
|
295
300
|
end
|
|
296
301
|
|
|
302
|
+
##
|
|
303
|
+
# Returns an in-band error for an unresolved function call.
|
|
304
|
+
# @return [LLM::Function::Return]
|
|
305
|
+
def unavailable
|
|
306
|
+
Return.new(id, name, {
|
|
307
|
+
error: true,
|
|
308
|
+
type: LLM::NoSuchToolError.name,
|
|
309
|
+
message: "tool not found"
|
|
310
|
+
})
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
# Returns an in-band error for a tool loop rate limit.
|
|
315
|
+
# @return [LLM::Function::Return]
|
|
316
|
+
def rate_limit
|
|
317
|
+
LLM::Function::Return.new(id, name, {
|
|
318
|
+
error: true,
|
|
319
|
+
type: LLM::ToolLoopError.name,
|
|
320
|
+
message: "tool loop rate limit reached"
|
|
321
|
+
})
|
|
322
|
+
end
|
|
323
|
+
|
|
297
324
|
##
|
|
298
325
|
# @return [Hash]
|
|
299
326
|
def adapt(provider)
|
|
@@ -16,12 +16,13 @@ module LLM::MCP::Transport
|
|
|
16
16
|
# Extra headers to send with requests
|
|
17
17
|
# @param [Integer, nil] timeout
|
|
18
18
|
# The timeout in seconds. Defaults to nil
|
|
19
|
+
# @param [LLM::Transport, Class, nil] transport
|
|
20
|
+
# Optional override with any {LLM::Transport} instance or subclass
|
|
19
21
|
# @return [LLM::MCP::Transport::HTTP]
|
|
20
|
-
def initialize(url:, headers: {}, timeout: nil)
|
|
22
|
+
def initialize(url:, headers: {}, timeout: nil, transport: nil)
|
|
21
23
|
@uri = URI.parse(url)
|
|
22
|
-
@use_ssl = @uri.scheme == "https"
|
|
23
24
|
@headers = headers
|
|
24
|
-
@
|
|
25
|
+
@transport = resolve_transport(transport, timeout:)
|
|
25
26
|
@queue = []
|
|
26
27
|
@monitor = Monitor.new
|
|
27
28
|
@running = false
|
|
@@ -61,21 +62,11 @@ module LLM::MCP::Transport
|
|
|
61
62
|
# @return [void]
|
|
62
63
|
def write(message)
|
|
63
64
|
raise LLM::MCP::Error, "MCP transport is not running" unless running?
|
|
64
|
-
req = Net::HTTP::Post.new(uri.
|
|
65
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers.merge("content-type" => "application/json"))
|
|
65
66
|
req.body = LLM.json.dump(message)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
else
|
|
70
|
-
http = persistent_client
|
|
71
|
-
args = [uri, req]
|
|
72
|
-
end
|
|
73
|
-
http.request(*args) do |res|
|
|
74
|
-
unless Net::HTTPSuccess === res
|
|
75
|
-
raise LLM::MCP::Error, "MCP transport write failed with HTTP #{res.code}"
|
|
76
|
-
end
|
|
77
|
-
read(res)
|
|
78
|
-
end
|
|
67
|
+
res = transport.request(req, owner: self) { consume(_1) }
|
|
68
|
+
res = LLM::Transport::Response.from(res)
|
|
69
|
+
raise LLM::MCP::Error, "MCP transport write failed with HTTP #{res.code}" unless res.success?
|
|
79
70
|
end
|
|
80
71
|
|
|
81
72
|
##
|
|
@@ -100,30 +91,27 @@ module LLM::MCP::Transport
|
|
|
100
91
|
@running
|
|
101
92
|
end
|
|
102
93
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
LLM.lock(:mcp) do
|
|
112
|
-
LLM.require "net/http/persistent" unless defined?(Net::HTTP::Persistent)
|
|
113
|
-
unless LLM::MCP.clients.key?(key)
|
|
114
|
-
http = Net::HTTP::Persistent.new(name: self.class.name)
|
|
115
|
-
http.read_timeout = timeout
|
|
116
|
-
http.open_timeout = timeout
|
|
117
|
-
LLM::MCP.clients[key] ||= http
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
self
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
attr_reader :uri, :headers, :transport
|
|
97
|
+
|
|
98
|
+
def consume(res)
|
|
99
|
+
res = LLM::Transport::Response.from(res)
|
|
100
|
+
read(res)
|
|
101
|
+
res
|
|
121
102
|
end
|
|
122
|
-
alias_method :persistent, :persist!
|
|
123
103
|
|
|
124
|
-
|
|
104
|
+
def resolve_transport(transport, timeout:)
|
|
105
|
+
return default_transport(timeout:) if transport.nil?
|
|
106
|
+
if Class === transport && transport <= LLM::Transport
|
|
107
|
+
return transport.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
108
|
+
end
|
|
109
|
+
transport
|
|
110
|
+
end
|
|
125
111
|
|
|
126
|
-
|
|
112
|
+
def default_transport(timeout:)
|
|
113
|
+
LLM::Transport::HTTP.new(host: uri.host, port: uri.port, timeout:, ssl: uri.scheme == "https")
|
|
114
|
+
end
|
|
127
115
|
|
|
128
116
|
def read(res)
|
|
129
117
|
if res["content-type"].to_s.include?("text/event-stream")
|
|
@@ -142,14 +130,6 @@ module LLM::MCP::Transport
|
|
|
142
130
|
lock { @queue << message }
|
|
143
131
|
end
|
|
144
132
|
|
|
145
|
-
def persistent_client
|
|
146
|
-
LLM::MCP.clients[key]
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def key
|
|
150
|
-
"#{uri.scheme}:#{uri.host}:#{uri.port}:#{timeout}"
|
|
151
|
-
end
|
|
152
|
-
|
|
153
133
|
def lock(&)
|
|
154
134
|
@monitor.synchronize(&)
|
|
155
135
|
end
|
|
@@ -78,14 +78,6 @@ module LLM::MCP::Transport
|
|
|
78
78
|
command.wait
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
-
##
|
|
82
|
-
# This method is a no-op for stdio transports
|
|
83
|
-
# @return [LLM::MCP::Transport::Stdio]
|
|
84
|
-
def persist!
|
|
85
|
-
self
|
|
86
|
-
end
|
|
87
|
-
alias_method :persistent, :persist!
|
|
88
|
-
|
|
89
81
|
private
|
|
90
82
|
|
|
91
83
|
attr_reader :command, :stdin, :stdout, :stderr
|
data/lib/llm/mcp.rb
CHANGED
|
@@ -24,14 +24,6 @@ class LLM::MCP
|
|
|
24
24
|
|
|
25
25
|
include RPC
|
|
26
26
|
|
|
27
|
-
@clients = {}
|
|
28
|
-
|
|
29
|
-
##
|
|
30
|
-
# @api private
|
|
31
|
-
def self.clients
|
|
32
|
-
@clients
|
|
33
|
-
end
|
|
34
|
-
|
|
35
27
|
##
|
|
36
28
|
# Builds an MCP client that uses the stdio transport.
|
|
37
29
|
# @param [LLM::Provider, nil] llm
|
|
@@ -69,6 +61,9 @@ class LLM::MCP
|
|
|
69
61
|
# The URL for the MCP HTTP endpoint
|
|
70
62
|
# @option http [Hash] :headers
|
|
71
63
|
# Extra headers for requests
|
|
64
|
+
# @option http [LLM::Transport, Class] :transport
|
|
65
|
+
# Optional override with any {LLM::Transport} instance or subclass,
|
|
66
|
+
# similar to {LLM::Provider}
|
|
72
67
|
# @param [Integer] timeout
|
|
73
68
|
# The maximum amount of time to wait when reading from an MCP process
|
|
74
69
|
# @return [LLM::MCP] A new MCP instance
|
|
@@ -82,8 +77,9 @@ class LLM::MCP
|
|
|
82
77
|
@transport = Transport::Stdio.new(command:)
|
|
83
78
|
elsif http
|
|
84
79
|
persistent = http.delete(:persistent)
|
|
85
|
-
|
|
86
|
-
|
|
80
|
+
transport = http.delete(:transport)
|
|
81
|
+
transport ||= LLM::Transport::PersistentHTTP if persistent
|
|
82
|
+
@transport = Transport::HTTP.new(**http, timeout:, transport:)
|
|
87
83
|
else
|
|
88
84
|
raise ArgumentError, "stdio or http is required"
|
|
89
85
|
end
|
|
@@ -121,19 +117,6 @@ class LLM::MCP
|
|
|
121
117
|
stop
|
|
122
118
|
end
|
|
123
119
|
|
|
124
|
-
##
|
|
125
|
-
# Configures an HTTP MCP transport to use a persistent connection pool
|
|
126
|
-
# via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
|
|
127
|
-
# @example
|
|
128
|
-
# mcp = LLM::MCP.http(url: "https://example.com/mcp", persistent: true)
|
|
129
|
-
# # do something with 'mcp'
|
|
130
|
-
# @return [LLM::MCP]
|
|
131
|
-
def persist!
|
|
132
|
-
transport.persist!
|
|
133
|
-
self
|
|
134
|
-
end
|
|
135
|
-
alias_method :persistent, :persist!
|
|
136
|
-
|
|
137
120
|
##
|
|
138
121
|
# Returns the tools provided by the MCP process.
|
|
139
122
|
# @return [Array<Class<LLM::Tool>>]
|
data/lib/llm/provider.rb
CHANGED
|
@@ -6,10 +6,7 @@
|
|
|
6
6
|
#
|
|
7
7
|
# @abstract
|
|
8
8
|
class LLM::Provider
|
|
9
|
-
|
|
10
|
-
require_relative "provider/transport/http"
|
|
11
|
-
require_relative "provider/transport/http/execution"
|
|
12
|
-
include Transport::HTTP::Execution
|
|
9
|
+
include LLM::Transport::Execution
|
|
13
10
|
|
|
14
11
|
##
|
|
15
12
|
# @param [String, nil] key
|
|
@@ -27,7 +24,9 @@ class LLM::Provider
|
|
|
27
24
|
# @param [Boolean] persistent
|
|
28
25
|
# Whether to use a persistent connection.
|
|
29
26
|
# Requires the net-http-persistent gem.
|
|
30
|
-
|
|
27
|
+
# @param [LLM::Transport, Class, nil] transport
|
|
28
|
+
# Optional override with any {LLM::Transport} instance or subclass.
|
|
29
|
+
def initialize(key:, host:, port: 443, timeout: 60, ssl: true, base_path: "", persistent: false, transport: nil)
|
|
31
30
|
@key = key
|
|
32
31
|
@host = host
|
|
33
32
|
@port = port
|
|
@@ -36,7 +35,7 @@ class LLM::Provider
|
|
|
36
35
|
@base_path = normalize_base_path(base_path)
|
|
37
36
|
@base_uri = URI("#{ssl ? "https" : "http"}://#{host}:#{port}/")
|
|
38
37
|
@headers = {"User-Agent" => "llm.rb v#{LLM::VERSION}"}
|
|
39
|
-
@transport =
|
|
38
|
+
@transport = resolve_transport(transport, persistent:)
|
|
40
39
|
@monitor = Monitor.new
|
|
41
40
|
end
|
|
42
41
|
|
|
@@ -316,19 +315,6 @@ class LLM::Provider
|
|
|
316
315
|
end
|
|
317
316
|
end
|
|
318
317
|
|
|
319
|
-
##
|
|
320
|
-
# This method configures a provider to use a persistent connection pool
|
|
321
|
-
# via the optional dependency [Net::HTTP::Persistent](https://github.com/drbrain/net-http-persistent)
|
|
322
|
-
# @example
|
|
323
|
-
# llm = LLM.openai(key: ENV["KEY"]).persistent
|
|
324
|
-
# # do something with 'llm'
|
|
325
|
-
# @return [LLM::Provider]
|
|
326
|
-
def persist!
|
|
327
|
-
transport.persist!
|
|
328
|
-
self
|
|
329
|
-
end
|
|
330
|
-
alias_method :persistent, :persist!
|
|
331
|
-
|
|
332
318
|
##
|
|
333
319
|
# Interrupt the active request, if any.
|
|
334
320
|
# @param [Fiber] owner
|
|
@@ -353,6 +339,13 @@ class LLM::Provider
|
|
|
353
339
|
LLM::Stream === stream || stream.respond_to?(:<<)
|
|
354
340
|
end
|
|
355
341
|
|
|
342
|
+
##
|
|
343
|
+
# @return [Boolean]
|
|
344
|
+
# Returns true when an API key is configured
|
|
345
|
+
def key?
|
|
346
|
+
@key != nil && @key.to_s.strip.size > 0
|
|
347
|
+
end
|
|
348
|
+
|
|
356
349
|
private
|
|
357
350
|
|
|
358
351
|
def path(suffix)
|
|
@@ -403,7 +396,7 @@ class LLM::Provider
|
|
|
403
396
|
# @return [Class]
|
|
404
397
|
# Returns the class responsible for decoding streamed response bodies
|
|
405
398
|
def stream_decoder
|
|
406
|
-
LLM::
|
|
399
|
+
LLM::Transport::StreamDecoder
|
|
407
400
|
end
|
|
408
401
|
|
|
409
402
|
##
|
|
@@ -431,6 +424,23 @@ class LLM::Provider
|
|
|
431
424
|
@monitor.synchronize(&)
|
|
432
425
|
end
|
|
433
426
|
|
|
427
|
+
##
|
|
428
|
+
# @api private
|
|
429
|
+
def default_transport(persistent:)
|
|
430
|
+
transport_class = persistent ? LLM::Transport::PersistentHTTP : LLM::Transport::HTTP
|
|
431
|
+
transport_class.new(host:, port:, timeout:, ssl:)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
##
|
|
435
|
+
# @api private
|
|
436
|
+
def resolve_transport(transport, persistent:)
|
|
437
|
+
return default_transport(persistent:) if transport.nil?
|
|
438
|
+
if Class === transport && transport <= LLM::Transport
|
|
439
|
+
return transport.new(host:, port:, timeout:, ssl:)
|
|
440
|
+
end
|
|
441
|
+
transport
|
|
442
|
+
end
|
|
443
|
+
|
|
434
444
|
##
|
|
435
445
|
# @api private
|
|
436
446
|
def thread
|
|
@@ -5,7 +5,7 @@ class LLM::Anthropic
|
|
|
5
5
|
# @private
|
|
6
6
|
class ErrorHandler
|
|
7
7
|
##
|
|
8
|
-
# @return [
|
|
8
|
+
# @return [LLM::Transport::Response]
|
|
9
9
|
# Non-2XX response from the server
|
|
10
10
|
attr_reader :res
|
|
11
11
|
|
|
@@ -19,13 +19,13 @@ class LLM::Anthropic
|
|
|
19
19
|
# The tracer
|
|
20
20
|
# @param [Object, nil] span
|
|
21
21
|
# The span
|
|
22
|
-
# @param [Net::HTTPResponse] res
|
|
22
|
+
# @param [LLM::Transport::Response, Net::HTTPResponse] res
|
|
23
23
|
# The response from the server
|
|
24
24
|
# @return [LLM::Anthropic::ErrorHandler]
|
|
25
25
|
def initialize(tracer, span, res)
|
|
26
26
|
@tracer = tracer
|
|
27
27
|
@span = span
|
|
28
|
-
@res = res
|
|
28
|
+
@res = LLM::Transport::Response.from(res)
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
##
|
|
@@ -43,12 +43,11 @@ class LLM::Anthropic
|
|
|
43
43
|
##
|
|
44
44
|
# @return [LLM::Error]
|
|
45
45
|
def error
|
|
46
|
-
|
|
47
|
-
when Net::HTTPServerError
|
|
46
|
+
if res.server_error?
|
|
48
47
|
LLM::ServerError.new("Server error").tap { _1.response = res }
|
|
49
|
-
|
|
48
|
+
elsif res.unauthorized?
|
|
50
49
|
LLM::UnauthorizedError.new("Authentication error").tap { _1.response = res }
|
|
51
|
-
|
|
50
|
+
elsif res.rate_limited?
|
|
52
51
|
LLM::RateLimitError.new("Too many requests").tap { _1.response = res }
|
|
53
52
|
else
|
|
54
53
|
LLM::Error.new("Unexpected response").tap { _1.response = res }
|
|
@@ -58,7 +58,7 @@ class LLM::Anthropic
|
|
|
58
58
|
multi = LLM::Multipart.new(params.merge!(file: LLM.File(file)))
|
|
59
59
|
req = Net::HTTP::Post.new("/v1/files", headers)
|
|
60
60
|
req["content-type"] = multi.content_type
|
|
61
|
-
set_body_stream(req, multi.body)
|
|
61
|
+
transport.set_body_stream(req, multi.body)
|
|
62
62
|
res, span, tracer = execute(request: req, operation: "request")
|
|
63
63
|
res = ResponseAdapter.adapt(res, type: :file)
|
|
64
64
|
tracer.on_request_finish(operation: "request", res:, span:)
|
|
@@ -159,7 +159,7 @@ class LLM::Anthropic
|
|
|
159
159
|
@provider.instance_variable_get(:@key)
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
[:headers, :execute, :
|
|
162
|
+
[:headers, :execute, :transport].each do |m|
|
|
163
163
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
164
164
|
end
|
|
165
165
|
end
|