llm.rb 13.1.0 → 14.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 +320 -0
- data/README.md +340 -31
- data/bin/llm.rb +36 -12
- data/data/anthropic.json +206 -263
- data/data/bedrock.json +2138 -1860
- data/data/deepinfra.json +1003 -624
- data/data/deepseek.json +38 -34
- data/data/google.json +1079 -371
- data/data/mistral.json +448 -368
- data/data/moonshot.json +384 -0
- data/data/openai.json +974 -1343
- data/data/xai.json +154 -126
- data/data/zai.json +191 -191
- data/lib/llm/agent.rb +47 -14
- data/lib/llm/context.rb +71 -88
- data/lib/llm/cost.rb +23 -17
- data/lib/llm/error.rb +0 -8
- data/lib/llm/function/async/task.rb +2 -0
- data/lib/llm/function/fiber/task.rb +2 -0
- data/lib/llm/function/fork/task.rb +2 -0
- data/lib/llm/function/ractor/task.rb +2 -0
- data/lib/llm/function/sequential/group.rb +4 -1
- data/lib/llm/function/sequential/task.rb +1 -1
- data/lib/llm/function/task.rb +4 -0
- data/lib/llm/function/thread/task.rb +2 -0
- data/lib/llm/function.rb +32 -4
- data/lib/llm/guard/loop.rb +89 -0
- data/lib/llm/guard/null.rb +19 -0
- data/lib/llm/guard.rb +61 -0
- data/lib/llm/provider.rb +36 -0
- data/lib/llm/providers/anthropic/stream_parser.rb +1 -0
- data/lib/llm/providers/anthropic.rb +1 -8
- data/lib/llm/providers/bedrock/stream_parser.rb +1 -0
- data/lib/llm/providers/bedrock.rb +1 -8
- data/lib/llm/providers/google/stream_parser.rb +1 -0
- data/lib/llm/providers/google.rb +1 -8
- data/lib/llm/providers/moonshot.rb +76 -0
- data/lib/llm/providers/ollama.rb +1 -8
- data/lib/llm/providers/openai/responses/stream_parser.rb +1 -0
- data/lib/llm/providers/openai/responses.rb +6 -8
- data/lib/llm/providers/openai/stream_parser.rb +1 -0
- data/lib/llm/providers/openai.rb +3 -10
- data/lib/llm/repl/bar.rb +4 -3
- data/lib/llm/repl/buffer.rb +42 -15
- data/lib/llm/repl/color.rb +78 -0
- data/lib/llm/repl/input/char.rb +46 -0
- data/lib/llm/repl/input/row.rb +39 -0
- data/lib/llm/repl/input.rb +251 -66
- data/lib/llm/repl/markdown/table.rb +6 -2
- data/lib/llm/repl/markdown.rb +31 -5
- data/lib/llm/repl/status.rb +38 -3
- data/lib/llm/repl/stream.rb +16 -4
- data/lib/llm/repl/walker.rb +3 -2
- data/lib/llm/repl/window.rb +25 -5
- data/lib/llm/repl.rb +29 -13
- data/lib/llm/stream.rb +8 -7
- data/lib/llm/tool.rb +29 -0
- data/lib/llm/transformer/null.rb +21 -0
- data/lib/llm/transformer.rb +55 -0
- data/lib/llm/version.rb +1 -1
- data/lib/llm.rb +12 -2
- data/llm.gemspec +1 -0
- data/resources/deepdive/advanced/cancellation.md +74 -0
- data/resources/deepdive/advanced/compaction.md +83 -0
- data/resources/deepdive/advanced/context.md +267 -0
- data/resources/deepdive/advanced/guard.md +371 -0
- data/resources/deepdive/advanced/tracer.md +180 -0
- data/resources/deepdive/advanced/transformer.md +67 -0
- data/resources/deepdive/advanced/transports.md +45 -0
- data/resources/deepdive/everything_else/audio.md +122 -0
- data/resources/deepdive/everything_else/cost.md +99 -0
- data/resources/deepdive/everything_else/images.md +89 -0
- data/resources/deepdive/everything_else/object.md +108 -0
- data/resources/deepdive/everything_else/ocr.md +48 -0
- data/resources/deepdive/fundamentals/agents.md +202 -0
- data/resources/deepdive/fundamentals/builtin_tools.md +191 -0
- data/resources/deepdive/fundamentals/concurrency.md +104 -0
- data/resources/deepdive/fundamentals/database.md +449 -0
- data/resources/deepdive/fundamentals/embeddings.md +157 -0
- data/resources/deepdive/fundamentals/repl.md +87 -0
- data/resources/deepdive/fundamentals/schema.md +61 -0
- data/resources/deepdive/fundamentals/skills.md +106 -0
- data/resources/deepdive/fundamentals/stream.md +110 -0
- data/resources/deepdive/fundamentals/tools.md +265 -0
- data/resources/deepdive/protocols/a2a.md +106 -0
- data/resources/deepdive/protocols/mcp.md +111 -0
- data/resources/deepdive.md +7 -1
- metadata +36 -3
- data/lib/llm/loop_guard.rb +0 -107
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Guard
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Guard::Loop LLM::Guard::Loop} is the built-in loop-detection
|
|
6
|
+
# guard. It detects when a context is repeating the same tool-call
|
|
7
|
+
# pattern instead of making progress, and returns an in-band
|
|
8
|
+
# {LLM::Function::Return LLM::Function::Return} so {LLM::Context
|
|
9
|
+
# LLM::Context} can block the pending tool work.
|
|
10
|
+
class Loop < self
|
|
11
|
+
##
|
|
12
|
+
# The default number of repeated tool-call patterns required before
|
|
13
|
+
# the guard intervenes.
|
|
14
|
+
# @return [Integer]
|
|
15
|
+
DEFAULT_THRESHOLD = 3
|
|
16
|
+
|
|
17
|
+
##
|
|
18
|
+
# Checks the current context for repeated tool-call patterns.
|
|
19
|
+
#
|
|
20
|
+
# This method inspects assistant tool calls only. It reduces each call
|
|
21
|
+
# to a `[tool_name, arguments]` signature and checks whether the tail
|
|
22
|
+
# of the sequence is repeating.
|
|
23
|
+
#
|
|
24
|
+
# @param [LLM::Function] function
|
|
25
|
+
# The pending function being decided about. Only its id and name are
|
|
26
|
+
# used to build the return.
|
|
27
|
+
# @param [Integer] threshold
|
|
28
|
+
# How many repeated tool-call patterns must appear at the tail of the
|
|
29
|
+
# sequence before the guard returns a warning.
|
|
30
|
+
# @param [Hash] opts
|
|
31
|
+
# Additional per-call options (ignored).
|
|
32
|
+
# @return [LLM::Function::Return, nil]
|
|
33
|
+
# Returns a tool return that blocks this function's pending call, or
|
|
34
|
+
# nil when execution should continue.
|
|
35
|
+
def call(function:, threshold: DEFAULT_THRESHOLD, **opts)
|
|
36
|
+
repetitions = detect(messages.to_a, threshold)
|
|
37
|
+
return unless repetitions
|
|
38
|
+
function.return(error: true, type: "guard_error", message: warning(repetitions))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def detect(messages, threshold)
|
|
44
|
+
signatures = extract_signatures(messages)
|
|
45
|
+
return if signatures.size < threshold
|
|
46
|
+
check_repeating_pattern(signatures, threshold)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def warning(repetitions)
|
|
50
|
+
<<~MSG
|
|
51
|
+
SYSTEM NOTICE: Repeated tool-call pattern detected - the same pattern has repeated #{repetitions} times.
|
|
52
|
+
You are stuck in a loop and not making progress. Stop and try a fundamentally different approach:
|
|
53
|
+
- Re-read the relevant context before retrying
|
|
54
|
+
- Try a different tool or strategy
|
|
55
|
+
- Break the problem into smaller steps
|
|
56
|
+
- If a tool keeps failing, investigate why before retrying
|
|
57
|
+
MSG
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def extract_signatures(messages)
|
|
61
|
+
messages
|
|
62
|
+
.select(&:assistant?)
|
|
63
|
+
.flat_map { |message| message.functions.map { [_1.name.to_s, _1.arguments.to_h] } }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def check_repeating_pattern(sequence, threshold)
|
|
67
|
+
max_pattern_len = sequence.size / threshold
|
|
68
|
+
(1..max_pattern_len).each do |pattern_len|
|
|
69
|
+
count = count_tail_repetitions(sequence, pattern_len)
|
|
70
|
+
return count if count >= threshold
|
|
71
|
+
end
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def count_tail_repetitions(sequence, length)
|
|
76
|
+
return 0 if sequence.size < length
|
|
77
|
+
pattern = sequence.last(length)
|
|
78
|
+
count = 1
|
|
79
|
+
pos = sequence.size - length
|
|
80
|
+
while pos >= length
|
|
81
|
+
candidate = sequence[(pos - length)...pos]
|
|
82
|
+
break unless candidate == pattern
|
|
83
|
+
count += 1
|
|
84
|
+
pos -= length
|
|
85
|
+
end
|
|
86
|
+
count
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class LLM::Guard
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Guard::Null LLM::Guard::Null} is a no-op guard. It never blocks
|
|
6
|
+
# pending tool work and is the default guard used by
|
|
7
|
+
# {LLM::Context LLM::Context}.
|
|
8
|
+
class Null < self
|
|
9
|
+
##
|
|
10
|
+
# @param [LLM::Function] function
|
|
11
|
+
# The pending function (ignored)
|
|
12
|
+
# @param [Hash] opts
|
|
13
|
+
# Per-call options (ignored)
|
|
14
|
+
# @return [nil]
|
|
15
|
+
def call(function:, **opts)
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/llm/guard.rb
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# {LLM::Guard LLM::Guard} is the superclass for context-level
|
|
6
|
+
# supervisors in llm.rb.
|
|
7
|
+
#
|
|
8
|
+
# A guard is bound to a context and decides whether a tool call should
|
|
9
|
+
# be blocked before it runs. Each subclass implements {#call} and returns
|
|
10
|
+
# an {LLM::Function::Return LLM::Function::Return} that closes the
|
|
11
|
+
# pending tool call, or `nil` when execution should continue. The guard
|
|
12
|
+
# is stamped onto the functions the context binds, so it runs whenever a
|
|
13
|
+
# task is spawned — including tool calls queued from a stream. The
|
|
14
|
+
# built-in implementation is {LLM::Guard::Loop LLM::Guard::Loop}, which
|
|
15
|
+
# detects repeated tool-call patterns. {LLM::Guard::Null LLM::Guard::Null}
|
|
16
|
+
# is a no-op and the default.
|
|
17
|
+
#
|
|
18
|
+
# {LLM::Agent LLM::Agent} enables {LLM::Guard::Loop LLM::Guard::Loop}
|
|
19
|
+
# by default through its wrapped context.
|
|
20
|
+
class Guard
|
|
21
|
+
require_relative "guard/null"
|
|
22
|
+
require_relative "guard/loop"
|
|
23
|
+
|
|
24
|
+
##
|
|
25
|
+
# @return [LLM::Context]
|
|
26
|
+
attr_reader :ctx
|
|
27
|
+
|
|
28
|
+
##
|
|
29
|
+
# @param ctx [LLM::Context, LLM::Agent]
|
|
30
|
+
# @return [LLM::Guard]
|
|
31
|
+
def initialize(ctx)
|
|
32
|
+
@ctx = LLM::Agent === ctx ? ctx.instance_variable_get(:@ctx) : ctx
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# @abstract
|
|
37
|
+
# @param [LLM::Function] function
|
|
38
|
+
# The pending function the guard is deciding about.
|
|
39
|
+
# @param opts [Hash] Per-call options
|
|
40
|
+
# @return [LLM::Function::Return, nil]
|
|
41
|
+
# A tool return that closes this function's pending call when it
|
|
42
|
+
# should be blocked, or nil when execution should continue.
|
|
43
|
+
def call(function:, **opts)
|
|
44
|
+
raise NotImplementedError
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
##
|
|
50
|
+
# @return [LLM::Stream]
|
|
51
|
+
def stream
|
|
52
|
+
@ctx.params[:stream]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
##
|
|
56
|
+
# @return [LLM::Buffer]
|
|
57
|
+
def messages
|
|
58
|
+
@ctx.messages
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -40,6 +40,42 @@ class LLM::Provider
|
|
|
40
40
|
@monitor = Monitor.new
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
##
|
|
44
|
+
# Builds the outgoing message array for a turn. Normalizes the
|
|
45
|
+
# prompt into one or more {LLM::Message} objects and prepends
|
|
46
|
+
# the existing history.
|
|
47
|
+
#
|
|
48
|
+
# The method is idempotent. If the prompt is already an
|
|
49
|
+
# {LLM::Message} or an array of {LLM::Message}s (ie it was built
|
|
50
|
+
# by a previous call and possibly transformed), it is returned
|
|
51
|
+
# as-is without rebuilding.
|
|
52
|
+
#
|
|
53
|
+
# @param [String, Array, LLM::Message, LLM::Prompt] prompt
|
|
54
|
+
# @param [Hash] params
|
|
55
|
+
# Turn params. The history is taken from `params[:messages]`.
|
|
56
|
+
# @param [Symbol] role
|
|
57
|
+
# The role to assign to a raw prompt
|
|
58
|
+
# @param [Symbol] key
|
|
59
|
+
# The params key that holds the history (`:messages` for chat
|
|
60
|
+
# completions, `:input` for the responses API).
|
|
61
|
+
# @return [Array<LLM::Message>]
|
|
62
|
+
def build_messages(prompt, params, role, key: :messages)
|
|
63
|
+
case prompt
|
|
64
|
+
when LLM::Message
|
|
65
|
+
[prompt]
|
|
66
|
+
when Array
|
|
67
|
+
if prompt.all? { LLM::Message === _1 }
|
|
68
|
+
prompt
|
|
69
|
+
else
|
|
70
|
+
[*(params.delete(key) || []), LLM::Message.new(role, prompt)]
|
|
71
|
+
end
|
|
72
|
+
when LLM::Prompt
|
|
73
|
+
[*(params.delete(key) || []), *prompt.to_a]
|
|
74
|
+
else
|
|
75
|
+
[*(params.delete(key) || []), LLM::Message.new(role, prompt)]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
43
79
|
##
|
|
44
80
|
# Returns an inspection of the provider object
|
|
45
81
|
# @return [String]
|
|
@@ -102,6 +102,7 @@ class LLM::Anthropic
|
|
|
102
102
|
fn.arguments = LLM::Anthropic.parse_tool_input(tool["input"])
|
|
103
103
|
fn.tracer = @stream.extra[:tracer]
|
|
104
104
|
fn.model = @stream.extra[:model]
|
|
105
|
+
fn.guard = @stream.extra[:guard]
|
|
105
106
|
end
|
|
106
107
|
[fn, (registered ? nil : fn.unavailable)]
|
|
107
108
|
end
|
|
@@ -167,7 +167,7 @@ module LLM
|
|
|
167
167
|
end
|
|
168
168
|
|
|
169
169
|
def build_complete_request(prompt, params, role)
|
|
170
|
-
messages =
|
|
170
|
+
messages = build_messages(prompt, params, role)
|
|
171
171
|
payload = adapt(messages)
|
|
172
172
|
body = LLM.json.dump(payload.merge!(params))
|
|
173
173
|
req = LLM::Transport::Request.post("/v1/messages", headers)
|
|
@@ -175,12 +175,5 @@ module LLM
|
|
|
175
175
|
req
|
|
176
176
|
end
|
|
177
177
|
|
|
178
|
-
def build_complete_messages(prompt, params, role)
|
|
179
|
-
if LLM::Prompt === prompt
|
|
180
|
-
[*(params.delete(:messages) || []), *prompt.to_a]
|
|
181
|
-
else
|
|
182
|
-
[*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
178
|
end
|
|
186
179
|
end
|
|
@@ -210,7 +210,7 @@ module LLM
|
|
|
210
210
|
end
|
|
211
211
|
|
|
212
212
|
def build_complete_request(prompt, params, role, stream: nil)
|
|
213
|
-
messages =
|
|
213
|
+
messages = build_messages(prompt, params, role)
|
|
214
214
|
model_id = params.delete(:model) || default_model
|
|
215
215
|
payload = build_converse_payload(messages, params)
|
|
216
216
|
body = LLM.json.dump(payload)
|
|
@@ -221,13 +221,6 @@ module LLM
|
|
|
221
221
|
[req, messages, body]
|
|
222
222
|
end
|
|
223
223
|
|
|
224
|
-
def build_complete_messages(prompt, params, role)
|
|
225
|
-
if LLM::Prompt === prompt
|
|
226
|
-
[*(params.delete(:messages) || []), *prompt]
|
|
227
|
-
else
|
|
228
|
-
[*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
229
|
-
end
|
|
230
|
-
end
|
|
231
224
|
|
|
232
225
|
def build_converse_payload(messages, params)
|
|
233
226
|
adapted = adapt(messages)
|
data/lib/llm/providers/google.rb
CHANGED
|
@@ -217,18 +217,11 @@ module LLM
|
|
|
217
217
|
model.respond_to?(:id) ? model.id : model
|
|
218
218
|
path = ["/v1beta/models/#{model}", action].join(":")
|
|
219
219
|
req = LLM::Transport::Request.post(path, headers)
|
|
220
|
-
messages =
|
|
220
|
+
messages = build_messages(prompt, params, role)
|
|
221
221
|
body = LLM.json.dump({contents: adapt(messages)}.merge!(params))
|
|
222
222
|
transport.set_body_stream(req, StringIO.new(body))
|
|
223
223
|
req
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
-
def build_complete_messages(prompt, params, role)
|
|
227
|
-
if LLM::Prompt === prompt
|
|
228
|
-
[*(params.delete(:messages) || []), *prompt.to_a]
|
|
229
|
-
else
|
|
230
|
-
[*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
231
|
-
end
|
|
232
|
-
end
|
|
233
226
|
end
|
|
234
227
|
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "openai" unless defined?(LLM::OpenAI)
|
|
4
|
+
|
|
5
|
+
module LLM
|
|
6
|
+
##
|
|
7
|
+
# The Moonshot class implements a provider for
|
|
8
|
+
# [Moonshot AI](https://platform.moonshot.ai) through its
|
|
9
|
+
# OpenAI-compatible Kimi API.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# #!/usr/bin/env ruby
|
|
13
|
+
# require "llm"
|
|
14
|
+
#
|
|
15
|
+
# llm = LLM.moonshot(key: ENV["KEY"])
|
|
16
|
+
# ctx = LLM::Context.new(llm)
|
|
17
|
+
# ctx.talk "Hello"
|
|
18
|
+
class Moonshot < OpenAI
|
|
19
|
+
HOST = "api.moonshot.ai"
|
|
20
|
+
BASE_PATH = "/v1"
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# @param key (see LLM::Provider#initialize)
|
|
24
|
+
# @param host (see LLM::Provider#initialize)
|
|
25
|
+
# @param base_path (see LLM::Provider#initialize)
|
|
26
|
+
# @return [LLM::Moonshot]
|
|
27
|
+
def initialize(host: HOST, base_path: BASE_PATH, **)
|
|
28
|
+
super
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# @return [Symbol]
|
|
33
|
+
# Returns the provider's name
|
|
34
|
+
def name
|
|
35
|
+
:moonshot
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# @raise [NotImplementedError]
|
|
40
|
+
def images
|
|
41
|
+
raise NotImplementedError
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# @raise [NotImplementedError]
|
|
46
|
+
def audio
|
|
47
|
+
raise NotImplementedError
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# @raise [NotImplementedError]
|
|
52
|
+
def moderations
|
|
53
|
+
raise NotImplementedError
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# @raise [NotImplementedError]
|
|
58
|
+
def responses
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
##
|
|
63
|
+
# @raise [NotImplementedError]
|
|
64
|
+
def vector_stores
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
##
|
|
69
|
+
# Returns the default model for chat completions
|
|
70
|
+
# @see https://platform.moonshot.ai/docs/api/models-overview Kimi models
|
|
71
|
+
# @return [String]
|
|
72
|
+
def default_model
|
|
73
|
+
"kimi-k3"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -137,19 +137,12 @@ module LLM
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def build_complete_request(prompt, params, role)
|
|
140
|
-
messages =
|
|
140
|
+
messages = build_messages(prompt, params, role)
|
|
141
141
|
body = LLM.json.dump({messages: [adapt(messages)].flatten}.merge!(params))
|
|
142
142
|
req = LLM::Transport::Request.post("/api/chat", headers)
|
|
143
143
|
transport.set_body_stream(req, StringIO.new(body))
|
|
144
144
|
req
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
def build_complete_messages(prompt, params, role)
|
|
148
|
-
if LLM::Prompt === prompt
|
|
149
|
-
[*(params.delete(:messages) || []), *prompt.to_a]
|
|
150
|
-
else
|
|
151
|
-
[*(params.delete(:messages) || []), LLM::Message.new(role, prompt)]
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
147
|
end
|
|
155
148
|
end
|
|
@@ -44,8 +44,13 @@ class LLM::OpenAI
|
|
|
44
44
|
adapt_tools(tools)
|
|
45
45
|
].inject({}, &:merge!).compact
|
|
46
46
|
req = LLM::Transport::Request.post(path("/responses"), headers)
|
|
47
|
-
messages =
|
|
47
|
+
messages = @provider.build_messages(prompt, params, role, key: :input)
|
|
48
48
|
@provider.tracer.set_request_metadata(user_input: extract_user_input(messages, fallback: prompt))
|
|
49
|
+
##
|
|
50
|
+
# The input is derived from `prompt`/`messages` above; a
|
|
51
|
+
# stale `:input` passed in params (eg by LLM::Context) must
|
|
52
|
+
# not overwrite it in the request body.
|
|
53
|
+
params.delete(:input)
|
|
49
54
|
body = LLM.json.dump({input: [adapt(messages, mode: :response)].flatten}.merge!(params))
|
|
50
55
|
transport.set_body_stream(req, StringIO.new(body))
|
|
51
56
|
res, span, tracer = execute(request: req, stream:, stream_parser:, operation: "chat", model: params[:model])
|
|
@@ -102,13 +107,6 @@ class LLM::OpenAI
|
|
|
102
107
|
define_method(m) { |*args, **kwargs, &b| @provider.send(m, *args, **kwargs, &b) }
|
|
103
108
|
end
|
|
104
109
|
|
|
105
|
-
def build_complete_messages(prompt, params, role)
|
|
106
|
-
if LLM::Prompt === prompt
|
|
107
|
-
[*(params.delete(:input) || []), *prompt]
|
|
108
|
-
else
|
|
109
|
-
[*(params.delete(:input) || []), LLM::Message.new(role, prompt)]
|
|
110
|
-
end
|
|
111
|
-
end
|
|
112
110
|
|
|
113
111
|
def adapt_schema(params)
|
|
114
112
|
return {} unless params && params[:schema]
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -146,10 +146,10 @@ module LLM
|
|
|
146
146
|
|
|
147
147
|
##
|
|
148
148
|
# Returns the default model for chat completions
|
|
149
|
-
# @see https://platform.openai.com/docs/models/gpt-5.
|
|
149
|
+
# @see https://platform.openai.com/docs/models/gpt-5.6-luna gpt-5.6-luna
|
|
150
150
|
# @return [String]
|
|
151
151
|
def default_model
|
|
152
|
-
"gpt-5.
|
|
152
|
+
"gpt-5.6-luna"
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
##
|
|
@@ -230,20 +230,13 @@ module LLM
|
|
|
230
230
|
end
|
|
231
231
|
|
|
232
232
|
def build_complete_request(prompt, params, role)
|
|
233
|
-
messages =
|
|
233
|
+
messages = build_messages(prompt, params, role)
|
|
234
234
|
body = LLM.json.dump({messages: adapt(messages, mode: :complete).flatten}.merge!(params))
|
|
235
235
|
req = LLM::Transport::Request.post(completions_path, headers)
|
|
236
236
|
transport.set_body_stream(req, StringIO.new(body))
|
|
237
237
|
[req, messages]
|
|
238
238
|
end
|
|
239
239
|
|
|
240
|
-
def build_complete_messages(prompt, params, role)
|
|
241
|
-
if LLM::Prompt === prompt
|
|
242
|
-
[*(params.delete(:messages) || []), *prompt]
|
|
243
|
-
else
|
|
244
|
-
[*(params.delete(:messages) || []), Message.new(role, prompt)]
|
|
245
|
-
end
|
|
246
|
-
end
|
|
247
240
|
|
|
248
241
|
def extract_user_input(messages, fallback:)
|
|
249
242
|
message = messages.reverse.find(&:user?) || messages.last
|
data/lib/llm/repl/bar.rb
CHANGED
|
@@ -17,7 +17,8 @@ class LLM::Repl
|
|
|
17
17
|
FREE = " "
|
|
18
18
|
|
|
19
19
|
##
|
|
20
|
-
# @param [Integer] used
|
|
20
|
+
# @param [Integer, nil] used
|
|
21
|
+
# The used context, or nil when it is unknown (eg after compaction).
|
|
21
22
|
# @param [Integer] total
|
|
22
23
|
# @param [Integer] width
|
|
23
24
|
# @return [LLM::Repl::Bar]
|
|
@@ -36,11 +37,11 @@ class LLM::Repl
|
|
|
36
37
|
private
|
|
37
38
|
|
|
38
39
|
##
|
|
39
|
-
# @param [Integer] used
|
|
40
|
+
# @param [Integer, nil] used
|
|
40
41
|
# @param [Integer] total
|
|
41
42
|
# @return [[String, Integer]]
|
|
42
43
|
def remainder(used, total)
|
|
43
|
-
return ["???", width] if total <= 0
|
|
44
|
+
return ["???", width] if used.nil? || total <= 0
|
|
44
45
|
diff = total - used
|
|
45
46
|
return ["0%", 0] if diff <= 0
|
|
46
47
|
remaining = (diff.fdiv(total) * 100).round(2)
|
data/lib/llm/repl/buffer.rb
CHANGED
|
@@ -20,7 +20,8 @@ class LLM::Repl
|
|
|
20
20
|
# An instance of {LLM::Repl LLM::Repl}.
|
|
21
21
|
# @return [LLM::Repl::Buffer]
|
|
22
22
|
def initialize(repl)
|
|
23
|
-
@repl
|
|
23
|
+
@repl = repl
|
|
24
|
+
@window = repl.window
|
|
24
25
|
@rows = [[]]
|
|
25
26
|
@cursor = nil
|
|
26
27
|
@snapshot = nil
|
|
@@ -46,12 +47,12 @@ class LLM::Repl
|
|
|
46
47
|
# @param [Symbol] method
|
|
47
48
|
# @return [void]
|
|
48
49
|
def write_message(user, content, method: :append)
|
|
49
|
-
chunks = [Node.new("#{user}
|
|
50
|
+
chunks = [Node.new("#{user}:\n", Curses::A_BOLD | Color.blue)]
|
|
50
51
|
case content
|
|
51
52
|
when Array then chunks.concat(content)
|
|
52
53
|
else chunks.push(Node.new(content))
|
|
53
54
|
end
|
|
54
|
-
chunks.push(Node.new("\n"))
|
|
55
|
+
chunks.push(Node.new("\n\n"))
|
|
55
56
|
write(chunks, method:)
|
|
56
57
|
end
|
|
57
58
|
|
|
@@ -90,6 +91,15 @@ class LLM::Repl
|
|
|
90
91
|
@offset = 0
|
|
91
92
|
end
|
|
92
93
|
|
|
94
|
+
##
|
|
95
|
+
# The width of the centered content area -
|
|
96
|
+
# with 20% on each side. The rest makes up
|
|
97
|
+
# the width of the buffer.
|
|
98
|
+
# @return [Integer]
|
|
99
|
+
def width
|
|
100
|
+
(Curses.cols * 0.6).floor
|
|
101
|
+
end
|
|
102
|
+
|
|
93
103
|
##
|
|
94
104
|
# @param [Integer] height
|
|
95
105
|
# @return [Array<String>]
|
|
@@ -103,8 +113,8 @@ class LLM::Repl
|
|
|
103
113
|
private
|
|
104
114
|
|
|
105
115
|
##
|
|
106
|
-
# @
|
|
107
|
-
attr_reader :repl
|
|
116
|
+
# @api private
|
|
117
|
+
attr_reader :repl, :window
|
|
108
118
|
|
|
109
119
|
##
|
|
110
120
|
# Appends a new row
|
|
@@ -126,20 +136,37 @@ class LLM::Repl
|
|
|
126
136
|
end
|
|
127
137
|
|
|
128
138
|
##
|
|
129
|
-
# Given a chunk this method wraps text
|
|
130
|
-
#
|
|
131
|
-
# the current
|
|
132
|
-
#
|
|
133
|
-
#
|
|
139
|
+
# Given a chunk this method wraps text to the buffer's
|
|
140
|
+
# width. The text is split into words; a word that does
|
|
141
|
+
# not fit on the current row moves to the next. Only a
|
|
142
|
+
# single word longer than the whole width is hard-broken,
|
|
143
|
+
# so text is never clipped by the window.
|
|
134
144
|
def wrap(chunk, rows)
|
|
135
145
|
attrs = chunk[:attrs]
|
|
136
|
-
chunk[:text].to_s.
|
|
137
|
-
if
|
|
138
|
-
rows << []
|
|
139
|
-
elsif char == " " and sum(rows.last) >= repl.width
|
|
146
|
+
chunk[:text].to_s.scan(/\n|[^\s]+|\s+/) do |token|
|
|
147
|
+
if token == "\n"
|
|
140
148
|
rows << []
|
|
149
|
+
elsif token.match?(/\s/)
|
|
150
|
+
if sum(rows.last) + token.length <= width
|
|
151
|
+
rows.last << Node.new(token, attrs)
|
|
152
|
+
end
|
|
153
|
+
elsif sum(rows.last) + token.length > width
|
|
154
|
+
if sum(rows.last) == 0
|
|
155
|
+
##
|
|
156
|
+
# A single word wider than the row: break it
|
|
157
|
+
# every width characters.
|
|
158
|
+
token.scan(/.{1,#{width}}/).each do |piece|
|
|
159
|
+
rows.last << Node.new(piece, attrs)
|
|
160
|
+
if sum(rows.last) >= width
|
|
161
|
+
rows << []
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
else
|
|
165
|
+
rows << []
|
|
166
|
+
rows.last << Node.new(token, attrs)
|
|
167
|
+
end
|
|
141
168
|
else
|
|
142
|
-
rows.last << Node.new(
|
|
169
|
+
rows.last << Node.new(token, attrs)
|
|
143
170
|
end
|
|
144
171
|
end
|
|
145
172
|
end
|