llm.rb 12.1.0 → 12.3.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 +248 -0
- data/README.md +77 -27
- data/data/anthropic.json +3 -2
- data/data/bedrock.json +47 -0
- data/data/deepinfra.json +2 -2
- data/data/google.json +32 -0
- data/data/mistral.json +968 -0
- data/data/openai.json +422 -0
- data/data/xai.json +60 -1
- data/lib/llm/active_record/acts_as_agent.rb +11 -0
- data/lib/llm/agent.rb +27 -3
- data/lib/llm/compactor.rb +2 -2
- data/lib/llm/context.rb +18 -26
- data/lib/llm/function.rb +6 -0
- data/lib/llm/object.rb +13 -0
- data/lib/llm/provider.rb +12 -9
- data/lib/llm/providers/anthropic.rb +4 -5
- data/lib/llm/providers/bedrock.rb +4 -5
- data/lib/llm/providers/google.rb +7 -3
- data/lib/llm/providers/mistral/request_adapter/completion.rb +122 -0
- data/lib/llm/providers/mistral/request_adapter.rb +20 -0
- data/lib/llm/providers/mistral.rb +145 -0
- data/lib/llm/providers/ollama/response_adapter/completion.rb +39 -0
- data/lib/llm/providers/ollama.rb +2 -3
- data/lib/llm/providers/openai/responses.rb +6 -3
- data/lib/llm/providers/openai.rb +2 -3
- data/lib/llm/repl/bar.rb +52 -0
- data/lib/llm/repl/input.rb +143 -30
- data/lib/llm/repl/markdown.rb +85 -0
- data/lib/llm/repl/status.rb +16 -5
- data/lib/llm/repl/stream.rb +15 -5
- data/lib/llm/repl/transcript.rb +107 -18
- data/lib/llm/repl/window.rb +43 -17
- data/lib/llm/repl.rb +99 -18
- data/lib/llm/sequel/agent.rb +11 -0
- data/lib/llm/skill.rb +1 -1
- data/lib/llm/stream/disabled.rb +23 -0
- data/lib/llm/stream/io.rb +43 -0
- data/lib/llm/stream.rb +34 -0
- data/lib/llm/tools/git.rb +2 -3
- data/lib/llm/tools/pwd.rb +0 -1
- data/lib/llm/tools/rg.rb +2 -1
- data/lib/llm/tools/swap_text.rb +6 -0
- data/lib/llm/transport/execution.rb +1 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +19 -0
- data/llm.gemspec +8 -4
- data/resources/deepdive.md +170 -50
- metadata +15 -4
data/lib/llm/context.rb
CHANGED
|
@@ -92,6 +92,8 @@ module LLM
|
|
|
92
92
|
@params[:tools] = tools unless tools.empty?
|
|
93
93
|
@params[:store] ||= false if @mode == :responses
|
|
94
94
|
@messages = LLM::Buffer.new(llm)
|
|
95
|
+
extra = @params.slice(:model, :tools).merge!(ctx: self, tracer:)
|
|
96
|
+
@params[:stream] = LLM::Stream.try(@params[:stream], extra:)
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
##
|
|
@@ -310,10 +312,7 @@ module LLM
|
|
|
310
312
|
# A list of functions to exclude from the wait
|
|
311
313
|
# @return [Array<LLM::Function::Return>]
|
|
312
314
|
def wait(strategy, except: [])
|
|
313
|
-
if
|
|
314
|
-
@queue = stream.queue
|
|
315
|
-
@queue.wait
|
|
316
|
-
else
|
|
315
|
+
if stream.queue.empty?
|
|
317
316
|
tools = except.empty? ? functions : functions - except
|
|
318
317
|
guards = guarded_returns(tools:)
|
|
319
318
|
return guards if guards
|
|
@@ -321,6 +320,9 @@ module LLM
|
|
|
321
320
|
returns = @queue.wait
|
|
322
321
|
emit_tool_returns(tools, returns)
|
|
323
322
|
returns
|
|
323
|
+
else
|
|
324
|
+
@queue = stream.queue
|
|
325
|
+
@queue.wait
|
|
324
326
|
end
|
|
325
327
|
ensure
|
|
326
328
|
@queue = nil
|
|
@@ -426,7 +428,7 @@ module LLM
|
|
|
426
428
|
# A tracer, or nil.
|
|
427
429
|
# @return [void]
|
|
428
430
|
def tracer=(other)
|
|
429
|
-
@llm.tracer =
|
|
431
|
+
@llm.tracer = other || LLM::Tracer::Null.new(@llm)
|
|
430
432
|
end
|
|
431
433
|
|
|
432
434
|
##
|
|
@@ -502,25 +504,11 @@ module LLM
|
|
|
502
504
|
|
|
503
505
|
private
|
|
504
506
|
|
|
505
|
-
##
|
|
506
|
-
# Binds runtime metadata onto an active stream.
|
|
507
|
-
# @api private
|
|
508
|
-
def bind!(stream, model, tools)
|
|
509
|
-
return unless LLM::Stream === stream
|
|
510
|
-
@stream = stream
|
|
511
|
-
stream.extra[:ctx] = self
|
|
512
|
-
stream.extra[:tracer] = tracer
|
|
513
|
-
stream.extra[:model] = model
|
|
514
|
-
stream.extra[:tools] = tools
|
|
515
|
-
end
|
|
516
|
-
|
|
517
507
|
##
|
|
518
508
|
# Returns the bound stream queue, if available.
|
|
519
509
|
# @api private
|
|
520
510
|
def queue
|
|
521
|
-
[@queue, stream
|
|
522
|
-
rescue NoMethodError
|
|
523
|
-
nil
|
|
511
|
+
[@queue, stream.queue].compact.first
|
|
524
512
|
end
|
|
525
513
|
|
|
526
514
|
##
|
|
@@ -543,12 +531,13 @@ module LLM
|
|
|
543
531
|
# Rewrites a prompt and params through the configured transformer.
|
|
544
532
|
# @api private
|
|
545
533
|
def transform(prompt, params)
|
|
534
|
+
transformer = self.transformer
|
|
546
535
|
return [prompt, params] unless transformer
|
|
547
536
|
stream = params[:stream]
|
|
548
|
-
stream.on_transform(self, transformer)
|
|
537
|
+
stream.on_transform(self, transformer)
|
|
549
538
|
transformer.call(self, prompt, params)
|
|
550
539
|
ensure
|
|
551
|
-
stream.on_transform_finish(self, transformer) if
|
|
540
|
+
stream.on_transform_finish(self, transformer) if transformer
|
|
552
541
|
end
|
|
553
542
|
|
|
554
543
|
##
|
|
@@ -556,8 +545,10 @@ module LLM
|
|
|
556
545
|
# @api private
|
|
557
546
|
def respond(prompt, params)
|
|
558
547
|
params = @params.merge(params)
|
|
548
|
+
extra = params.slice(:model, :tools).merge!(ctx: self, tracer:)
|
|
549
|
+
params[:stream] = LLM::Stream.try(params[:stream], extra:)
|
|
559
550
|
prompt, params = transform(prompt, params)
|
|
560
|
-
|
|
551
|
+
@stream = params[:stream]
|
|
561
552
|
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
562
553
|
input = res_id ? [] : @messages.to_a
|
|
563
554
|
params = params.merge(previous_response_id: res_id, input:).compact
|
|
@@ -570,8 +561,10 @@ module LLM
|
|
|
570
561
|
def complete(prompt, params)
|
|
571
562
|
params = params.merge(messages: @messages.to_a)
|
|
572
563
|
params = @params.merge(params)
|
|
564
|
+
extra = params.slice(:model, :tools).merge!(ctx: self, tracer:)
|
|
565
|
+
params[:stream] = LLM::Stream.try(params[:stream], extra:)
|
|
573
566
|
prompt, params = transform(prompt, params)
|
|
574
|
-
|
|
567
|
+
@stream = params[:stream]
|
|
575
568
|
[prompt, params, @llm.complete(prompt, params)]
|
|
576
569
|
end
|
|
577
570
|
|
|
@@ -590,7 +583,6 @@ module LLM
|
|
|
590
583
|
# Emits tool return callbacks for directly waited function work.
|
|
591
584
|
# @api private
|
|
592
585
|
def emit_tool_returns(tools, returns)
|
|
593
|
-
return unless LLM::Stream === stream
|
|
594
586
|
returns.each_with_index { |result, index| stream.on_tool_return(tools[index], result) }
|
|
595
587
|
end
|
|
596
588
|
|
|
@@ -604,7 +596,7 @@ module LLM
|
|
|
604
596
|
def repair!(messages, prompt)
|
|
605
597
|
message = messages.last
|
|
606
598
|
return unless message&.tool_call?
|
|
607
|
-
returns = self.returns
|
|
599
|
+
returns = [self.returns, prompt].flatten.grep(LLM::Function::Return)
|
|
608
600
|
cancelled = []
|
|
609
601
|
[*message.extra.tool_calls].each do |tool|
|
|
610
602
|
next if returns.any? { _1.id == tool[:id] }
|
data/lib/llm/function.rb
CHANGED
|
@@ -390,6 +390,12 @@ class LLM::Function
|
|
|
390
390
|
type: "function", name: @name, description: @description,
|
|
391
391
|
parameters: (@params || {type: "object", properties: {}}).to_h.merge(additionalProperties: false), strict: false
|
|
392
392
|
}.compact
|
|
393
|
+
when "LLM::Mistral"
|
|
394
|
+
params = @params || {type: "object", properties: {}}
|
|
395
|
+
{
|
|
396
|
+
type: "function",
|
|
397
|
+
function: {name: @name, description: @description, parameters: params}
|
|
398
|
+
}.compact
|
|
393
399
|
else
|
|
394
400
|
params = @params || {type: "object", properties: {}}
|
|
395
401
|
{
|
data/lib/llm/object.rb
CHANGED
|
@@ -148,6 +148,19 @@ class LLM::Object < BasicObject
|
|
|
148
148
|
SINGLETON.from @h.merge(other)
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
+
##
|
|
152
|
+
# @param [Hash, #to_h] other
|
|
153
|
+
# The hash to merge in place
|
|
154
|
+
# @return [LLM::Object]
|
|
155
|
+
# Returns self
|
|
156
|
+
def merge!(other = UNDEFINED)
|
|
157
|
+
return SINGLETON.get(@h, :merge!) if other.equal?(UNDEFINED)
|
|
158
|
+
other = ::Hash.try_convert(other)
|
|
159
|
+
raise TypeError, "#{other} cannot be coerced into a Hash" unless other
|
|
160
|
+
@h.merge!(other)
|
|
161
|
+
self
|
|
162
|
+
end
|
|
163
|
+
|
|
151
164
|
##
|
|
152
165
|
# @param [#to_s, #to_sym] k
|
|
153
166
|
# The key name
|
data/lib/llm/provider.rb
CHANGED
|
@@ -26,7 +26,7 @@ class LLM::Provider
|
|
|
26
26
|
# Requires the net-http-persistent gem.
|
|
27
27
|
# @param [LLM::Transport, Class, nil] transport
|
|
28
28
|
# Optional override with any {LLM::Transport} instance or subclass.
|
|
29
|
-
def initialize(key:, host:, port: 443, timeout:
|
|
29
|
+
def initialize(key:, host:, port: 443, timeout: 180, ssl: true, base_path: "", persistent: false, transport: nil)
|
|
30
30
|
@key = key
|
|
31
31
|
@host = host
|
|
32
32
|
@port = port
|
|
@@ -71,6 +71,16 @@ class LLM::Provider
|
|
|
71
71
|
raise NotImplementedError
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
##
|
|
75
|
+
# @note
|
|
76
|
+
# This feature is not implemented by all providers,
|
|
77
|
+
# and it will raise NotImplementedError for providers
|
|
78
|
+
# that do not support it.
|
|
79
|
+
# @return [LLM::Response]
|
|
80
|
+
def ocr(...)
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
end
|
|
83
|
+
|
|
74
84
|
##
|
|
75
85
|
# Provides an interface to the chat completions API
|
|
76
86
|
# @example
|
|
@@ -303,7 +313,7 @@ class LLM::Provider
|
|
|
303
313
|
def with_tracer(tracer)
|
|
304
314
|
had_override = weakmap.key?(self)
|
|
305
315
|
previous = weakmap[self]
|
|
306
|
-
weakmap[self] = tracer
|
|
316
|
+
weakmap[self] = tracer || LLM::Tracer::Null.new(self)
|
|
307
317
|
yield
|
|
308
318
|
ensure
|
|
309
319
|
if had_override
|
|
@@ -332,13 +342,6 @@ class LLM::Provider
|
|
|
332
342
|
transport.request_owner
|
|
333
343
|
end
|
|
334
344
|
|
|
335
|
-
##
|
|
336
|
-
# @param [Object] stream
|
|
337
|
-
# @return [Boolean]
|
|
338
|
-
def streamable?(stream)
|
|
339
|
-
LLM::Stream === stream || stream.respond_to?(:<<)
|
|
340
|
-
end
|
|
341
|
-
|
|
342
345
|
##
|
|
343
346
|
# @return [Boolean]
|
|
344
347
|
# Returns true when an API key is configured
|
|
@@ -92,10 +92,10 @@ module LLM
|
|
|
92
92
|
|
|
93
93
|
##
|
|
94
94
|
# Returns the default model for chat completions
|
|
95
|
-
# @see https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table claude-
|
|
95
|
+
# @see https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table claude-opus-4-8
|
|
96
96
|
# @return [String]
|
|
97
97
|
def default_model
|
|
98
|
-
"claude-
|
|
98
|
+
"claude-opus-4-8"
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
##
|
|
@@ -151,9 +151,8 @@ module LLM
|
|
|
151
151
|
params = {role: :user, model: default_model, max_tokens: 1024}.merge!(params)
|
|
152
152
|
tools = resolve_tools(params.delete(:tools))
|
|
153
153
|
params = [params, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
154
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
155
|
-
params
|
|
156
|
-
[params, stream, tools, role]
|
|
154
|
+
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
155
|
+
[params.merge!(stream: stream.enabled?), stream, tools, role]
|
|
157
156
|
end
|
|
158
157
|
|
|
159
158
|
def build_complete_request(prompt, params, role)
|
|
@@ -205,9 +205,8 @@ module LLM
|
|
|
205
205
|
params = {role: :user, model: default_model, max_tokens: 2048}.merge!(params)
|
|
206
206
|
tools = resolve_tools(params.delete(:tools))
|
|
207
207
|
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
208
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
209
|
-
params
|
|
210
|
-
[params, stream, tools, role]
|
|
208
|
+
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
209
|
+
[params.merge!(stream: stream.enabled?), stream, tools, role]
|
|
211
210
|
end
|
|
212
211
|
|
|
213
212
|
def build_complete_request(prompt, params, role, stream: nil)
|
|
@@ -215,8 +214,8 @@ module LLM
|
|
|
215
214
|
model_id = params.delete(:model) || default_model
|
|
216
215
|
payload = build_converse_payload(messages, params)
|
|
217
216
|
body = LLM.json.dump(payload)
|
|
218
|
-
path = stream ? "/model/#{model_id}/converse-stream" \
|
|
219
|
-
|
|
217
|
+
path = stream&.enabled? ? "/model/#{model_id}/converse-stream" \
|
|
218
|
+
: "/model/#{model_id}/converse"
|
|
220
219
|
req = LLM::Transport::Request.post(path, headers)
|
|
221
220
|
transport.set_body_stream(req, StringIO.new(body))
|
|
222
221
|
[req, messages, body]
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -194,11 +194,15 @@ module LLM
|
|
|
194
194
|
end
|
|
195
195
|
|
|
196
196
|
def normalize_complete_params(params)
|
|
197
|
+
except = %i[role model messages stream]
|
|
197
198
|
params = {role: :user, model: default_model}.merge!(params)
|
|
198
199
|
tools = resolve_tools(params.delete(:tools))
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
200
|
+
config = adapt_generation_config(params.except(*except))
|
|
201
|
+
params = [params.except(:schema), config, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
202
|
+
role, model, stream = params.delete(:role),
|
|
203
|
+
params.delete(:model),
|
|
204
|
+
LLM::Stream.try(params.delete(:stream))
|
|
205
|
+
[params.merge!(stream: stream.enabled?), stream, tools, role, model]
|
|
202
206
|
end
|
|
203
207
|
|
|
204
208
|
def build_complete_request(prompt, params, role, model, stream)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM::Mistral::RequestAdapter
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
class Completion
|
|
7
|
+
##
|
|
8
|
+
# @param [LLM::Message, Hash] message
|
|
9
|
+
# The message to format
|
|
10
|
+
def initialize(message)
|
|
11
|
+
@message = message
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Adapts the message for the Mistral chat completions API
|
|
16
|
+
# @return [Hash]
|
|
17
|
+
def adapt
|
|
18
|
+
catch(:abort) do
|
|
19
|
+
if Hash === message
|
|
20
|
+
{role: message[:role], content: adapt_content(message[:content])}
|
|
21
|
+
elsif message.tool_call?
|
|
22
|
+
{role: message.role, content: nil, tool_calls: message.extra[:original_tool_calls]}
|
|
23
|
+
else
|
|
24
|
+
adapt_message
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def adapt_message
|
|
32
|
+
case content
|
|
33
|
+
when Array
|
|
34
|
+
adapt_array
|
|
35
|
+
else
|
|
36
|
+
{role: message.role, content: adapt_content(content)}
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def adapt_array
|
|
41
|
+
if content.empty?
|
|
42
|
+
nil
|
|
43
|
+
elsif returns.any?
|
|
44
|
+
returns.map do
|
|
45
|
+
{
|
|
46
|
+
role: "tool",
|
|
47
|
+
name: _1.name,
|
|
48
|
+
tool_call_id: _1.id,
|
|
49
|
+
content: LLM.json.dump(_1.value)
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
else
|
|
53
|
+
{role: message.role, content: content.flat_map { adapt_content(_1) }}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def adapt_content(content)
|
|
58
|
+
case content
|
|
59
|
+
when LLM::Object
|
|
60
|
+
adapt_object(content)
|
|
61
|
+
when String
|
|
62
|
+
[{type: :text, text: content.to_s}]
|
|
63
|
+
when LLM::Response
|
|
64
|
+
adapt_remote_file(content)
|
|
65
|
+
when LLM::Message
|
|
66
|
+
adapt_content(content.content)
|
|
67
|
+
when LLM::Function::Return
|
|
68
|
+
throw(:abort, {
|
|
69
|
+
role: "tool",
|
|
70
|
+
name: content.name,
|
|
71
|
+
tool_call_id: content.id,
|
|
72
|
+
content: LLM.json.dump(content.value)
|
|
73
|
+
})
|
|
74
|
+
else
|
|
75
|
+
prompt_error!(content)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def adapt_object(object)
|
|
80
|
+
case object.kind
|
|
81
|
+
when :image_url
|
|
82
|
+
[{type: :image_url, image_url: {url: object.value}}]
|
|
83
|
+
when :local_file
|
|
84
|
+
adapt_local_file(object.value)
|
|
85
|
+
when :remote_file
|
|
86
|
+
adapt_remote_file(object.value)
|
|
87
|
+
else
|
|
88
|
+
prompt_error!(object)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def adapt_local_file(file)
|
|
93
|
+
if file.image?
|
|
94
|
+
[{type: :image_url, image_url: {url: file.to_data_uri}}]
|
|
95
|
+
else
|
|
96
|
+
[{type: :file, file: {filename: file.basename, file_data: file.to_data_uri}}]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def adapt_remote_file(file)
|
|
101
|
+
if file.file?
|
|
102
|
+
[{type: :file, file: {file_id: file.id}}]
|
|
103
|
+
else
|
|
104
|
+
prompt_error!(file)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def prompt_error!(content)
|
|
109
|
+
if LLM::Object === content
|
|
110
|
+
raise LLM::PromptError, "The given LLM::Object with kind '#{content.kind}' is not " \
|
|
111
|
+
"supported by the Mistral chat completions API."
|
|
112
|
+
else
|
|
113
|
+
raise LLM::PromptError, "The given object (an instance of #{content.class}) " \
|
|
114
|
+
"is not supported by the Mistral chat completions API."
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def message = @message
|
|
119
|
+
def content = message.content
|
|
120
|
+
def returns = content.grep(LLM::Function::Return)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Mistral
|
|
4
|
+
##
|
|
5
|
+
# @private
|
|
6
|
+
module RequestAdapter
|
|
7
|
+
require_relative "request_adapter/completion"
|
|
8
|
+
include LLM::OpenAI::RequestAdapter
|
|
9
|
+
|
|
10
|
+
##
|
|
11
|
+
# @param [Array<LLM::Message>] messages
|
|
12
|
+
# The messages to adapt
|
|
13
|
+
# @return [Array<Hash>]
|
|
14
|
+
def adapt(messages, mode: nil)
|
|
15
|
+
messages.filter_map do |message|
|
|
16
|
+
Completion.new(message).adapt
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai" unless defined?(LLM::OpenAI)
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
##
|
|
7
|
+
# The Mistral class implements a provider for
|
|
8
|
+
# [Mistral](https://mistral.ai) through its
|
|
9
|
+
# OpenAI-compatible API.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# #!/usr/bin/env ruby
|
|
13
|
+
# require "llm"
|
|
14
|
+
#
|
|
15
|
+
# llm = LLM.mistral(key: ENV["KEY"])
|
|
16
|
+
# ctx = LLM::Context.new(llm)
|
|
17
|
+
# ctx.talk "Hello"
|
|
18
|
+
# ctx.messages.select(&:assistant?).each { print "[#{_1.role}]", _1.content, "\n" }
|
|
19
|
+
class Mistral < OpenAI
|
|
20
|
+
require_relative "mistral/request_adapter"
|
|
21
|
+
include Mistral::RequestAdapter
|
|
22
|
+
|
|
23
|
+
HOST = "api.mistral.ai"
|
|
24
|
+
BASE_PATH = "/v1/"
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# @param key (see LLM::Provider#initialize)
|
|
28
|
+
# @param host (see LLM::Provider#initialize)
|
|
29
|
+
# @param base_path (see LLM::Provider#initialize)
|
|
30
|
+
# @return [LLM::Mistral]
|
|
31
|
+
def initialize(host: HOST, base_path: BASE_PATH, **)
|
|
32
|
+
super
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @return [Symbol]
|
|
37
|
+
# Returns the provider's name
|
|
38
|
+
def name
|
|
39
|
+
:mistral
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# @return [NotImplementedError]
|
|
44
|
+
def images
|
|
45
|
+
raise NotImplementedError
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Provides an embedding.
|
|
50
|
+
# @param input (see LLM::Provider#embed)
|
|
51
|
+
# @param model (see LLM::Provider#embed)
|
|
52
|
+
# @param params (see LLM::Provider#embed)
|
|
53
|
+
# @raise (see LLM::Provider#request)
|
|
54
|
+
# @return (see LLM::Provider#embed)
|
|
55
|
+
def embed(input, model: "mistral-embed", **params)
|
|
56
|
+
super
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
##
|
|
60
|
+
# Runs OCR on a remote image or document URL.
|
|
61
|
+
# @see https://docs.mistral.ai/api/endpoint/ocr#operation-ocr_v1_ocr_post Mistral OCR docs
|
|
62
|
+
# @param [String, nil] image_url
|
|
63
|
+
# A remote HTTP(S) URL to the image
|
|
64
|
+
# @param [String, nil] document_url
|
|
65
|
+
# A remote HTTP(S) URL to the document
|
|
66
|
+
# @param [String] model
|
|
67
|
+
# The OCR model to use
|
|
68
|
+
# @param [Hash] params
|
|
69
|
+
# Additional OCR parameters
|
|
70
|
+
# @raise (see LLM::Provider#request)
|
|
71
|
+
# @return [LLM::Response]
|
|
72
|
+
def ocr(image_url: nil, document_url: nil, model: "mistral-ocr-latest", **params)
|
|
73
|
+
if [image_url, document_url].all?(&:nil?)
|
|
74
|
+
raise ArgumentError, "must provide one of: image_url, document_url"
|
|
75
|
+
elsif [image_url, document_url].compact.size > 1
|
|
76
|
+
raise ArgumentError, "must provide one of: image_url, document_url"
|
|
77
|
+
end
|
|
78
|
+
document = parse_document(image_url, document_url)
|
|
79
|
+
req = LLM::Transport::Request.post("/v1/ocr", headers)
|
|
80
|
+
req.body = LLM.json.dump({model:, document:}.merge!(params))
|
|
81
|
+
res, = execute(request: req, operation: "ocr", model:)
|
|
82
|
+
LLM::Response.new(res)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# @raise [NotImplementedError]
|
|
87
|
+
def responses
|
|
88
|
+
raise NotImplementedError
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
##
|
|
92
|
+
# @return [LLM::Mistral::Audio]
|
|
93
|
+
def audio
|
|
94
|
+
raise NotImplementedError
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
##
|
|
98
|
+
# @raise [NotImplementedError]
|
|
99
|
+
def files
|
|
100
|
+
raise NotImplementedError
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
##
|
|
104
|
+
# @raise [NotImplementedError]
|
|
105
|
+
def moderations
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
##
|
|
110
|
+
# @raise [NotImplementedError]
|
|
111
|
+
def vector_stores
|
|
112
|
+
raise NotImplementedError
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
##
|
|
116
|
+
# Returns the default model for chat completions
|
|
117
|
+
# @return [String]
|
|
118
|
+
def default_model
|
|
119
|
+
"mistral-large-latest"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
##
|
|
125
|
+
# @api private
|
|
126
|
+
def headers
|
|
127
|
+
lock do
|
|
128
|
+
(@headers || {}).merge(
|
|
129
|
+
"Authorization" => "Bearer #{@key}",
|
|
130
|
+
"Content-Type" => "application/json"
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# @api private
|
|
137
|
+
def parse_document(image_url, document_url)
|
|
138
|
+
if image_url
|
|
139
|
+
{type: "image_url", image_url:}
|
|
140
|
+
elsif document_url
|
|
141
|
+
{type: "document_url", document_url:}
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -9,6 +9,16 @@ module LLM::Ollama::ResponseAdapter
|
|
|
9
9
|
end
|
|
10
10
|
alias_method :choices, :messages
|
|
11
11
|
|
|
12
|
+
##
|
|
13
|
+
# Returns the response body, parsing NDJSON when the
|
|
14
|
+
# transport returned a raw string (non-streaming path).
|
|
15
|
+
# @return [LLM::Object]
|
|
16
|
+
def body
|
|
17
|
+
raw = super
|
|
18
|
+
return raw unless String === raw
|
|
19
|
+
parse_ndjson(raw)
|
|
20
|
+
end
|
|
21
|
+
|
|
12
22
|
##
|
|
13
23
|
# (see LLM::Contract::Completion#input_tokens)
|
|
14
24
|
def input_tokens
|
|
@@ -110,6 +120,35 @@ module LLM::Ollama::ResponseAdapter
|
|
|
110
120
|
end
|
|
111
121
|
end
|
|
112
122
|
|
|
123
|
+
##
|
|
124
|
+
# Parses a raw NDJSON string into an LLM::Object.
|
|
125
|
+
# Ollama's streaming API returns application/x-ndjson,
|
|
126
|
+
# but the transport only parses application/json. When
|
|
127
|
+
# the response body arrives as a raw string, we merge
|
|
128
|
+
# the NDJSON lines here.
|
|
129
|
+
# @param [String] raw
|
|
130
|
+
# @return [LLM::Object]
|
|
131
|
+
def parse_ndjson(raw)
|
|
132
|
+
lines = raw.split("\n").reject(&:empty?)
|
|
133
|
+
merged = lines.each_with_object({}) do |line, hash|
|
|
134
|
+
parsed = LLM.json.load(line)
|
|
135
|
+
next unless parsed.is_a?(Hash)
|
|
136
|
+
parsed.each do |key, value|
|
|
137
|
+
if key == "message" && value.is_a?(Hash)
|
|
138
|
+
if hash.key?("message")
|
|
139
|
+
hash["message"]["content"] << value["content"].to_s
|
|
140
|
+
else
|
|
141
|
+
hash["message"] = {"role" => value["role"], "content" => value["content"].to_s}
|
|
142
|
+
hash["message"]["tool_calls"] = value["tool_calls"] if value.key?("tool_calls")
|
|
143
|
+
end
|
|
144
|
+
else
|
|
145
|
+
hash[key] = value
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
LLM::Object.from(merged)
|
|
150
|
+
end
|
|
151
|
+
|
|
113
152
|
include LLM::Contract::Completion
|
|
114
153
|
end
|
|
115
154
|
end
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -121,9 +121,8 @@ module LLM
|
|
|
121
121
|
params = {role: :user, model: default_model, stream: true}.merge!(params)
|
|
122
122
|
tools = resolve_tools(params.delete(:tools))
|
|
123
123
|
params = [params, {format: params[:schema]}, adapt_tools(tools)].inject({}, &:merge!).compact
|
|
124
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
125
|
-
params
|
|
126
|
-
[params, stream, tools, role]
|
|
124
|
+
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
125
|
+
[params.merge!(stream: stream.enabled?), stream, tools, role]
|
|
127
126
|
end
|
|
128
127
|
|
|
129
128
|
def build_complete_request(prompt, params, role)
|
|
@@ -36,10 +36,13 @@ class LLM::OpenAI
|
|
|
36
36
|
# @return [LLM::Response]
|
|
37
37
|
def create(prompt, params = {})
|
|
38
38
|
params = {role: :user, model: @provider.default_model}.merge!(params)
|
|
39
|
+
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
39
40
|
tools = resolve_tools(params.delete(:tools))
|
|
40
|
-
params = [
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
params = [
|
|
42
|
+
params.merge!(stream: stream.enabled?),
|
|
43
|
+
adapt_schema(params),
|
|
44
|
+
adapt_tools(tools)
|
|
45
|
+
].inject({}, &:merge!).compact
|
|
43
46
|
req = LLM::Transport::Request.post(path("/responses"), headers)
|
|
44
47
|
messages = build_complete_messages(prompt, params, role)
|
|
45
48
|
@provider.tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -211,12 +211,11 @@ module LLM
|
|
|
211
211
|
params = {role: :user, model: default_model}.merge!(params)
|
|
212
212
|
tools = resolve_tools(params.delete(:tools))
|
|
213
213
|
params = [params, adapt_schema(params), adapt_tools(tools)].inject({}, &:merge!).compact
|
|
214
|
-
role, stream = params.delete(:role), params.delete(:stream)
|
|
215
|
-
params[:stream] = true if streamable?(stream) || stream == true
|
|
214
|
+
role, stream = params.delete(:role), LLM::Stream.try(params.delete(:stream))
|
|
216
215
|
if params[:stream]
|
|
217
216
|
params[:stream_options] = {include_usage: true}.merge!(params[:stream_options] || {})
|
|
218
217
|
end
|
|
219
|
-
[params, stream, tools, role]
|
|
218
|
+
[params.merge!(stream: stream.enabled?), stream, tools, role]
|
|
220
219
|
end
|
|
221
220
|
|
|
222
221
|
def build_complete_request(prompt, params, role)
|