llm.rb 15.2.2 → 15.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 +76 -0
- data/lib/llm/agent.rb +16 -0
- data/lib/llm/context/deserializer.rb +4 -2
- data/lib/llm/context.rb +54 -22
- data/lib/llm/message.rb +18 -7
- data/lib/llm/provider.rb +58 -16
- data/lib/llm/providers/anthropic.rb +3 -1
- data/lib/llm/providers/bedrock.rb +3 -1
- data/lib/llm/providers/google.rb +3 -3
- data/lib/llm/providers/mistral.rb +3 -1
- data/lib/llm/providers/ollama.rb +3 -1
- data/lib/llm/providers/openai/request_adapter.rb +1 -1
- data/lib/llm/providers/openai/responses.rb +2 -2
- data/lib/llm/providers/openai.rb +6 -4
- data/lib/llm/schema.rb +24 -0
- data/lib/llm/utils.rb +13 -0
- data/lib/llm/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a461691c878ffef8343a8f1ae2dad4298ebb631d379d901110ef8ab8641d9c68
|
|
4
|
+
data.tar.gz: 95bb85e3debb178ebbe6949cd3cafbb3625d6f20a72d617f08f1a600354911ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c9b6e723d1bc16d9cc515a9ae2a3fd34072ce88024804d5b7b0e713cff0b08c26e78e2008641763bbc9a3aabea6f262023fa1f243d9fe155f8d8fdf58215d24f
|
|
7
|
+
data.tar.gz: e125d9936d34b3550fb504d281760b73bc617aa4b5d42f1f4d1e3b9b3edd98a448c4a7509d010c62b67b1f57def5f3a9f1e088786b860e6d8f08c94bf64aa0f8
|
data/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,82 @@
|
|
|
17
17
|
|
|
18
18
|
*No unreleased changes yet. Check back after the next release.*
|
|
19
19
|
|
|
20
|
+
## v15.3.0
|
|
21
|
+
|
|
22
|
+
Changes since `v15.2.2`.
|
|
23
|
+
|
|
24
|
+
This release gives contexts, agents, and messages UUIDv7 ids that survive
|
|
25
|
+
persistence and back their `created_at`, scopes per-request headers through a
|
|
26
|
+
block on `LLM::Provider#with`, and pins an OpenRouter session per context. It
|
|
27
|
+
also fixes the OpenAI tool schema sent through OpenRouter and Azure, which
|
|
28
|
+
mixed the Responses and Chat Completions shapes and left nested objects open.
|
|
29
|
+
|
|
30
|
+
### Core
|
|
31
|
+
|
|
32
|
+
* **context: give every context an id you can sort and trace** <br>
|
|
33
|
+
[`LLM::Context#id`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#id-instance_method)
|
|
34
|
+
returns a UUIDv7 string that encodes its creation time, so contexts and their
|
|
35
|
+
agents sort by creation order and stay identifiable within and across
|
|
36
|
+
sessions. The id is generated on creation and restored with the runtime state
|
|
37
|
+
on load. [`LLM::Agent#id`](https://r.uby.dev/api-docs/llm.rb/LLM/Agent.html#id-instance_method)
|
|
38
|
+
delegates to the context it wraps; a model and its agent keep separate ids.
|
|
39
|
+
|
|
40
|
+
* **context: read the creation time without storing it** <br>
|
|
41
|
+
[`LLM::Context#created_at`](https://r.uby.dev/api-docs/llm.rb/LLM/Context.html#created_at-instance_method)
|
|
42
|
+
returns when the context was created, read from the timestamp its UUIDv7 id
|
|
43
|
+
already carries, so no separate timestamp has to be persisted or kept in sync.
|
|
44
|
+
`LLM::Agent#created_at` delegates to it. The time is recomputed rather than
|
|
45
|
+
stored, so it returns `nil` for a context whose id is not a UUIDv7 string.
|
|
46
|
+
|
|
47
|
+
* **message: give every message an id that survives persistence** <br>
|
|
48
|
+
[`LLM::Message#id`](https://r.uby.dev/api-docs/llm.rb/LLM/Message.html#id-instance_method)
|
|
49
|
+
returns a UUIDv7 string generated once when the message is created and restored
|
|
50
|
+
with the runtime state on load, so a message keeps the same identity across
|
|
51
|
+
saves and loads. `LLM::Message#created_at` is derived from the id rather than
|
|
52
|
+
stored. `LLM::Message#==` ignores the id, so two messages with the same role
|
|
53
|
+
and content are equal even when they were created at different times;
|
|
54
|
+
previously the timestamp took part in the comparison.
|
|
55
|
+
|
|
56
|
+
* **utils: decode a UUIDv7 timestamp once, in one place** <br>
|
|
57
|
+
[`LLM::Utils.timestamp`](https://r.uby.dev/api-docs/llm.rb/LLM/Utils.html#timestamp-instance_method)
|
|
58
|
+
returns the UTC time encoded in a UUIDv7 string, or `nil` when the value is not
|
|
59
|
+
a UUIDv7. It backs `created_at` on messages, contexts, and agents, so the
|
|
60
|
+
decoding logic lives in a single method instead of three.
|
|
61
|
+
|
|
62
|
+
### Provider
|
|
63
|
+
|
|
64
|
+
* **provider: set a per-request header without leaking it** <br>
|
|
65
|
+
[`LLM::Provider#with`](https://r.uby.dev/api-docs/llm.rb/LLM/Provider.html#with-instance_method)
|
|
66
|
+
now takes a block: headers set inside apply to the current fiber only and are
|
|
67
|
+
restored when the block returns, so a header that varies per request can be set
|
|
68
|
+
for one call without leaking into the next. Without a block the headers are
|
|
69
|
+
merged into the provider's defaults as before. The method returns the provider
|
|
70
|
+
without a block and the block's value with one.
|
|
71
|
+
|
|
72
|
+
* **openrouter: keep one session so requests reuse a cached model** <br>
|
|
73
|
+
A context now sends its id as the `x-session-id` header on OpenRouter requests,
|
|
74
|
+
so consecutive requests share a session and OpenRouter can route them to the
|
|
75
|
+
same cached model instead of starting fresh each time. Other providers are
|
|
76
|
+
unaffected.
|
|
77
|
+
|
|
78
|
+
### Fix
|
|
79
|
+
|
|
80
|
+
* **openai: make tools callable through OpenRouter and Azure again** <br>
|
|
81
|
+
Fix a bug where [`LLM::OpenAI`](https://r.uby.dev/api-docs/llm.rb/LLM/OpenAI.html)
|
|
82
|
+
sent a function schema that mixed the Responses and Chat Completions shapes,
|
|
83
|
+
carrying a top-level `name` next to a nested `function` object. OpenRouter and
|
|
84
|
+
OpenAI through Azure read it as a Responses function and could not describe the
|
|
85
|
+
tool correctly. The name, description, parameters, and `strict: false` now sit
|
|
86
|
+
under `function:`, the shape chat completions documents.
|
|
87
|
+
|
|
88
|
+
* **openai: stop Azure rejecting nested schema objects** <br>
|
|
89
|
+
Tool parameters and structured-output schemas now carry
|
|
90
|
+
`additionalProperties: false` on every object they contain, not just the root,
|
|
91
|
+
in the chat completions and Responses paths alike. OpenAI and Azure reject a
|
|
92
|
+
schema that leaves any object open, so nested objects were failing validation.
|
|
93
|
+
The chat completions structured-output schema, which was sent open, is now
|
|
94
|
+
closed too.
|
|
95
|
+
|
|
20
96
|
## v15.2.2
|
|
21
97
|
|
|
22
98
|
Changes since `v15.2.1`.
|
data/lib/llm/agent.rb
CHANGED
|
@@ -510,6 +510,22 @@ module LLM
|
|
|
510
510
|
@ctx.messages
|
|
511
511
|
end
|
|
512
512
|
|
|
513
|
+
##
|
|
514
|
+
# Returns a stable id for this agent. The id is borrowed from
|
|
515
|
+
# the context it wraps, so it survives save and restore.
|
|
516
|
+
# @return [String]
|
|
517
|
+
def id
|
|
518
|
+
@ctx.id
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
##
|
|
522
|
+
# Returns the time this agent was created. It is derived
|
|
523
|
+
# from the context id, so it is not persisted separately.
|
|
524
|
+
# @return [Time, nil]
|
|
525
|
+
def created_at
|
|
526
|
+
@ctx.created_at
|
|
527
|
+
end
|
|
528
|
+
|
|
513
529
|
##
|
|
514
530
|
# @return [Integer]
|
|
515
531
|
def retry_budget
|
|
@@ -25,6 +25,8 @@ class LLM::Context
|
|
|
25
25
|
else
|
|
26
26
|
LLM.json.load(string)
|
|
27
27
|
end
|
|
28
|
+
@id = ctx["id"] || @id
|
|
29
|
+
@created_at = nil
|
|
28
30
|
@messages.concat [*ctx["messages"]].map { deserialize_message(_1) }
|
|
29
31
|
@compacted = !!ctx["compacted"]
|
|
30
32
|
self
|
|
@@ -40,8 +42,8 @@ class LLM::Context
|
|
|
40
42
|
usage = payload["usage"]
|
|
41
43
|
reasoning_content = payload["reasoning_content"]
|
|
42
44
|
compaction = payload["compaction"]
|
|
43
|
-
|
|
44
|
-
extra = {tool_calls:, original_tool_calls:, tools: @params[:tools], usage:, reasoning_content:, compaction:,
|
|
45
|
+
id = payload["id"]
|
|
46
|
+
extra = {tool_calls:, original_tool_calls:, tools: @params[:tools], usage:, reasoning_content:, compaction:, id:}.compact
|
|
45
47
|
content = returns.nil? ? deserialize_content(payload["content"]) : returns
|
|
46
48
|
LLM::Message.new(payload["role"], content, extra)
|
|
47
49
|
end
|
data/lib/llm/context.rb
CHANGED
|
@@ -55,7 +55,7 @@ module LLM
|
|
|
55
55
|
# @api private
|
|
56
56
|
# @return [Array<Symbol>]
|
|
57
57
|
def self.params
|
|
58
|
-
%w[guard retry_budget concurrency transformer compactor record]
|
|
58
|
+
%w[guard retry_budget concurrency transformer compactor record id]
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
##
|
|
@@ -78,6 +78,12 @@ module LLM
|
|
|
78
78
|
# @return [Object, nil]
|
|
79
79
|
attr_reader :record
|
|
80
80
|
|
|
81
|
+
##
|
|
82
|
+
# Returns a stable id for this context. It is generated once
|
|
83
|
+
# on creation and restored with the runtime state on load.
|
|
84
|
+
# @return [String]
|
|
85
|
+
attr_reader :id
|
|
86
|
+
|
|
81
87
|
##
|
|
82
88
|
# @param [LLM::Provider] llm
|
|
83
89
|
# A provider
|
|
@@ -89,6 +95,8 @@ module LLM
|
|
|
89
95
|
# Defaults to `:responses` for OpenAI, otherwise it defaults
|
|
90
96
|
# to `:completions`.
|
|
91
97
|
# @option params [String] :model Defaults to the provider's default model
|
|
98
|
+
# @option params [String] :id
|
|
99
|
+
# A stable id for the context. Defaults to a UUID.
|
|
92
100
|
# @option params [Class<LLM::Compactor>, nil] :compactor
|
|
93
101
|
# A compactor class to use for context compaction. Defaults to
|
|
94
102
|
# {LLM::Compactor::Null}.
|
|
@@ -109,6 +117,7 @@ module LLM
|
|
|
109
117
|
def initialize(llm, params = {})
|
|
110
118
|
params = {}.merge!(params)
|
|
111
119
|
@llm = llm
|
|
120
|
+
@id = params.delete(:id) || SecureRandom.uuid_v7
|
|
112
121
|
@record = params.delete(:record)
|
|
113
122
|
@mode = params.delete(:mode) || (llm.name == :openai ? :responses : :completions)
|
|
114
123
|
tools = [*params.delete(:tools), *load_skills(params.delete(:skills))]
|
|
@@ -488,11 +497,20 @@ module LLM
|
|
|
488
497
|
messages.find(&:assistant?)&.model || @params[:model] || @llm.default_model
|
|
489
498
|
end
|
|
490
499
|
|
|
500
|
+
##
|
|
501
|
+
# Returns the time this context was created, derived from the
|
|
502
|
+
# timestamp embedded in its UUIDv7 id, or nil when it is not.
|
|
503
|
+
# @return [Time, nil]
|
|
504
|
+
def created_at
|
|
505
|
+
@created_at ||= LLM::Utils.timestamp(@id)
|
|
506
|
+
end
|
|
507
|
+
|
|
491
508
|
##
|
|
492
509
|
# @return [Hash]
|
|
493
510
|
def to_h
|
|
494
511
|
{
|
|
495
512
|
schema_version: 1,
|
|
513
|
+
id: @id,
|
|
496
514
|
model:,
|
|
497
515
|
compacted:,
|
|
498
516
|
messages: @messages.map { serialize_message(_1) }
|
|
@@ -597,33 +615,37 @@ module LLM
|
|
|
597
615
|
# Executes a turn through the Responses API.
|
|
598
616
|
# @api private
|
|
599
617
|
def respond(prompt, params)
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
618
|
+
@llm.with(**headers) do
|
|
619
|
+
history = @messages.to_a
|
|
620
|
+
params = @params.merge(params).reject { self.class.params.include?(_1.to_s) }
|
|
621
|
+
extra = params.slice(:model, :tools).merge!(ctx: self, tracer:, guard: @guard[:klass].new(self))
|
|
622
|
+
params[:stream] = LLM::Stream.try(params[:stream], extra:)
|
|
623
|
+
res_id = params[:store] == false ? nil : @messages.find(&:assistant?)&.response&.response_id
|
|
624
|
+
input = res_id ? [] : history
|
|
625
|
+
params[:input] = input
|
|
626
|
+
messages = transform(prompt, params, key: :input)
|
|
627
|
+
@stream = params[:stream]
|
|
628
|
+
new_messages = messages[input.size..]
|
|
629
|
+
params = params.merge(previous_response_id: res_id, input:).compact
|
|
630
|
+
[new_messages, params, @llm.responses.create(messages, params)]
|
|
631
|
+
end
|
|
612
632
|
end
|
|
613
633
|
|
|
614
634
|
##
|
|
615
635
|
# Executes a turn through the chat completions API.
|
|
616
636
|
# @api private
|
|
617
637
|
def complete(prompt, params)
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
638
|
+
@llm.with(**headers) do
|
|
639
|
+
history = @messages.to_a
|
|
640
|
+
params = params.merge(messages: history)
|
|
641
|
+
params = @params.merge(params).reject { self.class.params.include?(_1.to_s) }
|
|
642
|
+
extra = params.slice(:model, :tools).merge!(ctx: self, tracer:, guard: @guard[:klass].new(self))
|
|
643
|
+
params[:stream] = LLM::Stream.try(params[:stream], extra:)
|
|
644
|
+
messages = transform(prompt, params)
|
|
645
|
+
@stream = params[:stream]
|
|
646
|
+
new_messages = messages[history.size..]
|
|
647
|
+
[new_messages, params, @llm.complete(messages, params)]
|
|
648
|
+
end
|
|
627
649
|
end
|
|
628
650
|
|
|
629
651
|
##
|
|
@@ -652,5 +674,15 @@ module LLM
|
|
|
652
674
|
end
|
|
653
675
|
messages << LLM::Message.new(@llm.tool_role, cancelled) unless cancelled.empty?
|
|
654
676
|
end
|
|
677
|
+
|
|
678
|
+
##
|
|
679
|
+
# @return [Hash]
|
|
680
|
+
def headers
|
|
681
|
+
if @llm.name == :openrouter
|
|
682
|
+
{"x-session-id" => @id}
|
|
683
|
+
else
|
|
684
|
+
{}
|
|
685
|
+
end
|
|
686
|
+
end
|
|
655
687
|
end
|
|
656
688
|
end
|
data/lib/llm/message.rb
CHANGED
|
@@ -18,9 +18,10 @@ module LLM
|
|
|
18
18
|
attr_reader :extra
|
|
19
19
|
|
|
20
20
|
##
|
|
21
|
-
# Returns
|
|
22
|
-
#
|
|
23
|
-
|
|
21
|
+
# Returns a stable id for this message. It is generated once
|
|
22
|
+
# on creation and restored with the runtime state on load.
|
|
23
|
+
# @return [String]
|
|
24
|
+
attr_reader :id
|
|
24
25
|
|
|
25
26
|
##
|
|
26
27
|
# Returns a new message
|
|
@@ -32,7 +33,15 @@ module LLM
|
|
|
32
33
|
@role = role.to_s
|
|
33
34
|
@content = content
|
|
34
35
|
@extra = LLM::Object.from(extra)
|
|
35
|
-
@
|
|
36
|
+
@id = extra[:id] || SecureRandom.uuid_v7
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
##
|
|
40
|
+
# Returns the time this message was created, derived from the
|
|
41
|
+
# timestamp embedded in its UUIDv7 id, or nil when it is not.
|
|
42
|
+
# @return [Time, nil]
|
|
43
|
+
def created_at
|
|
44
|
+
@created_at ||= LLM::Utils.timestamp(@id)
|
|
36
45
|
end
|
|
37
46
|
|
|
38
47
|
##
|
|
@@ -40,10 +49,10 @@ module LLM
|
|
|
40
49
|
# @return [Hash]
|
|
41
50
|
def to_h
|
|
42
51
|
{
|
|
52
|
+
id: @id,
|
|
43
53
|
role:,
|
|
44
54
|
content:,
|
|
45
55
|
reasoning_content:,
|
|
46
|
-
created_at: created_at.utc.iso8601,
|
|
47
56
|
compaction: extra.compaction,
|
|
48
57
|
tools: extra.tool_calls&.map { LLM::Object === _1 ? _1.to_h : _1 },
|
|
49
58
|
usage:,
|
|
@@ -58,13 +67,15 @@ module LLM
|
|
|
58
67
|
end
|
|
59
68
|
|
|
60
69
|
##
|
|
61
|
-
# Returns true when two objects have the same role and content
|
|
70
|
+
# Returns true when two objects have the same role and content.
|
|
71
|
+
# The id is ignored, because it identifies a message rather than
|
|
72
|
+
# describing it.
|
|
62
73
|
# @param [Object] other
|
|
63
74
|
# The other object to compare
|
|
64
75
|
# @return [Boolean]
|
|
65
76
|
def ==(other)
|
|
66
77
|
if other.respond_to?(:to_h)
|
|
67
|
-
to_h == other.to_h
|
|
78
|
+
to_h.except(:id) == other.to_h.except(:id)
|
|
68
79
|
else
|
|
69
80
|
false
|
|
70
81
|
end
|
data/lib/llm/provider.rb
CHANGED
|
@@ -263,11 +263,21 @@ class LLM::Provider
|
|
|
263
263
|
end
|
|
264
264
|
|
|
265
265
|
##
|
|
266
|
-
# Add one or more headers to all requests
|
|
267
|
-
#
|
|
266
|
+
# Add one or more headers to all requests, or scope them to a block.
|
|
267
|
+
#
|
|
268
|
+
# Without a block the headers are merged into the provider's defaults
|
|
269
|
+
# permanently. With a block the headers apply only to the current fiber,
|
|
270
|
+
# for the duration of the block, then the previous headers are restored.
|
|
271
|
+
# This is useful for a header that varies per context, such as
|
|
272
|
+
# OpenRouter's `x-session-id`.
|
|
273
|
+
# @example Permanent
|
|
268
274
|
# llm = LLM.openai(key: ENV["KEY"])
|
|
269
275
|
# llm.with("OpenAI-Organization" => ENV["ORG"])
|
|
270
276
|
# llm.with("OpenAI-Project" => ENV["PROJECT"])
|
|
277
|
+
# @example Scoped
|
|
278
|
+
# llm.with("x-session-id" => ctx.id) do
|
|
279
|
+
# llm.complete("hello")
|
|
280
|
+
# end
|
|
271
281
|
# @param [Hash<String,String>] headers
|
|
272
282
|
# One or more headers
|
|
273
283
|
# @note
|
|
@@ -275,12 +285,26 @@ class LLM::Provider
|
|
|
275
285
|
# provided via the `headers:` keyword argument,
|
|
276
286
|
# or provided directly as a Hash without the
|
|
277
287
|
# `headers:` key namespace.
|
|
288
|
+
# @yield
|
|
278
289
|
# @return [LLM::Provider]
|
|
279
|
-
# Returns self
|
|
280
|
-
def with(**headers)
|
|
290
|
+
# Returns self without a block, the block's value with one
|
|
291
|
+
def with(**headers, &block)
|
|
281
292
|
headers = headers.merge(headers.delete(:headers) || {})
|
|
282
|
-
|
|
283
|
-
|
|
293
|
+
if block
|
|
294
|
+
wm = weakmaps.header
|
|
295
|
+
previous = wm[self]
|
|
296
|
+
wm[self] = (previous || {}).merge(headers)
|
|
297
|
+
block.call
|
|
298
|
+
else
|
|
299
|
+
lock { tap { @headers.merge!(headers) } }
|
|
300
|
+
end
|
|
301
|
+
ensure
|
|
302
|
+
if block
|
|
303
|
+
if previous.nil?
|
|
304
|
+
wm.respond_to?(:delete) ? wm.delete(self) : wm[self] = nil
|
|
305
|
+
else
|
|
306
|
+
wm[self] = previous
|
|
307
|
+
end
|
|
284
308
|
end
|
|
285
309
|
end
|
|
286
310
|
|
|
@@ -349,7 +373,7 @@ class LLM::Provider
|
|
|
349
373
|
# @return [LLM::Tracer]
|
|
350
374
|
# Returns the current scoped tracer override or provider default tracer
|
|
351
375
|
def tracer
|
|
352
|
-
|
|
376
|
+
weakmaps.tracer[self] || @tracer || LLM::Tracer::Null.new(self)
|
|
353
377
|
end
|
|
354
378
|
|
|
355
379
|
##
|
|
@@ -378,17 +402,18 @@ class LLM::Provider
|
|
|
378
402
|
# @yield
|
|
379
403
|
# @return [Object]
|
|
380
404
|
def with_tracer(tracer)
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
405
|
+
wm = weakmaps.tracer
|
|
406
|
+
had_override = wm.key?(self)
|
|
407
|
+
previous = wm[self]
|
|
408
|
+
wm[self] = tracer || LLM::Tracer::Null.new(self)
|
|
384
409
|
yield
|
|
385
410
|
ensure
|
|
386
411
|
if had_override
|
|
387
|
-
|
|
388
|
-
elsif
|
|
389
|
-
|
|
412
|
+
wm[self] = previous
|
|
413
|
+
elsif wm.respond_to?(:delete)
|
|
414
|
+
wm.delete(self)
|
|
390
415
|
else
|
|
391
|
-
|
|
416
|
+
wm[self] = nil
|
|
392
417
|
end
|
|
393
418
|
end
|
|
394
419
|
|
|
@@ -504,8 +529,25 @@ class LLM::Provider
|
|
|
504
529
|
end
|
|
505
530
|
|
|
506
531
|
##
|
|
532
|
+
# Returns the temporary headers set by {#with}
|
|
533
|
+
# for the current fiber.
|
|
507
534
|
# @api private
|
|
508
|
-
|
|
509
|
-
|
|
535
|
+
# @return [Hash]
|
|
536
|
+
def temporary_headers
|
|
537
|
+
weakmaps.header[self] || {}
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
##
|
|
541
|
+
# @return [LLM::Object]
|
|
542
|
+
def weakmaps
|
|
543
|
+
if thread["llm.#{name}.weakmaps"]
|
|
544
|
+
thread["llm.#{name}.weakmaps"]
|
|
545
|
+
else
|
|
546
|
+
weakmaps = LLM::Object.from(
|
|
547
|
+
tracer: ObjectSpace::WeakMap.new,
|
|
548
|
+
header: ObjectSpace::WeakMap.new
|
|
549
|
+
)
|
|
550
|
+
thread["llm.#{name}.weakmaps"] = weakmaps
|
|
551
|
+
end
|
|
510
552
|
end
|
|
511
553
|
end
|
data/lib/llm/providers/google.rb
CHANGED
data/lib/llm/providers/ollama.rb
CHANGED
|
@@ -98,7 +98,7 @@ class LLM::OpenAI
|
|
|
98
98
|
def adapt_function(fn)
|
|
99
99
|
{
|
|
100
100
|
type: "function", name: fn.name, description: fn.description,
|
|
101
|
-
parameters: fn.params
|
|
101
|
+
parameters: LLM::Schema::Utils.close(fn.params), strict: false
|
|
102
102
|
}.compact
|
|
103
103
|
end
|
|
104
104
|
|
|
@@ -112,7 +112,7 @@ class LLM::OpenAI
|
|
|
112
112
|
return {} unless params && params[:schema]
|
|
113
113
|
schema = params.delete(:schema)
|
|
114
114
|
schema = schema.respond_to?(:object) ? schema.object : schema
|
|
115
|
-
schema =
|
|
115
|
+
schema = LLM::Schema::Utils.close(schema)
|
|
116
116
|
name = "JSONSchema"
|
|
117
117
|
{text: {format: {type: "json_schema", name:, schema:}}}
|
|
118
118
|
end
|
data/lib/llm/providers/openai.rb
CHANGED
|
@@ -156,10 +156,10 @@ module LLM
|
|
|
156
156
|
# @param [LLM::Function] fn
|
|
157
157
|
# @return [Hash]
|
|
158
158
|
def adapt_function(fn)
|
|
159
|
-
params = fn.params
|
|
159
|
+
params = LLM::Schema::Utils.close(fn.params)
|
|
160
160
|
{
|
|
161
|
-
type: "function",
|
|
162
|
-
function: {name: fn.name, description: fn.description, parameters: params}
|
|
161
|
+
type: "function",
|
|
162
|
+
function: {name: fn.name, description: fn.description, parameters: params, strict: false}
|
|
163
163
|
}.compact
|
|
164
164
|
end
|
|
165
165
|
|
|
@@ -203,7 +203,9 @@ module LLM
|
|
|
203
203
|
|
|
204
204
|
def headers
|
|
205
205
|
lock do
|
|
206
|
-
(@headers || {})
|
|
206
|
+
(@headers || {})
|
|
207
|
+
.merge(temporary_headers)
|
|
208
|
+
.merge(
|
|
207
209
|
"Content-Type" => "application/json",
|
|
208
210
|
"Authorization" => "Bearer #{@key}"
|
|
209
211
|
)
|
data/lib/llm/schema.rb
CHANGED
|
@@ -81,6 +81,30 @@ class LLM::Schema
|
|
|
81
81
|
def fetch(properties, name)
|
|
82
82
|
properties[name] || properties.fetch(name.to_s)
|
|
83
83
|
end
|
|
84
|
+
|
|
85
|
+
##
|
|
86
|
+
# Returns a copy of a JSON schema where every object is
|
|
87
|
+
# closed with "additionalProperties": false. OpenAI and
|
|
88
|
+
# Azure reject tool parameters that leave an object open,
|
|
89
|
+
# and they validate nested objects as well as the root.
|
|
90
|
+
# @param [Object] node
|
|
91
|
+
# A schema, a Hash of properties, or an Array of either
|
|
92
|
+
# @return [Object]
|
|
93
|
+
def close(node)
|
|
94
|
+
case node
|
|
95
|
+
when LLM::Schema::Leaf
|
|
96
|
+
close(node.to_h)
|
|
97
|
+
when LLM::Object
|
|
98
|
+
close(node.to_h)
|
|
99
|
+
when Hash
|
|
100
|
+
node.to_h { |key, value| [key, close(value)] }
|
|
101
|
+
.then { _1[:type] == "object" ? _1.merge(additionalProperties: false) : _1 }
|
|
102
|
+
when Array
|
|
103
|
+
node.map { close(_1) }
|
|
104
|
+
else
|
|
105
|
+
node
|
|
106
|
+
end
|
|
107
|
+
end
|
|
84
108
|
end
|
|
85
109
|
|
|
86
110
|
##
|
data/lib/llm/utils.rb
CHANGED
|
@@ -48,6 +48,19 @@ module LLM
|
|
|
48
48
|
(defined?(::Sequel::Model) and ::Sequel::Model === obj)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
##
|
|
52
|
+
# Returns the UTC time encoded in a UUIDv7, or nil when the
|
|
53
|
+
# given value is not a UUIDv7. The first 48 bits of a UUIDv7
|
|
54
|
+
# are a Unix millisecond timestamp.
|
|
55
|
+
# @param [Object] id
|
|
56
|
+
# @return [Time, nil]
|
|
57
|
+
def timestamp(id)
|
|
58
|
+
hex = id.to_s.delete("-")
|
|
59
|
+
return nil unless hex.match?(/\A\h{32}\z/)
|
|
60
|
+
return nil unless hex[12] == "7"
|
|
61
|
+
Time.at(hex[0, 12].to_i(16) / 1000.0).utc
|
|
62
|
+
end
|
|
63
|
+
|
|
51
64
|
##
|
|
52
65
|
# Normalizes an HTTP API base path.
|
|
53
66
|
#
|
data/lib/llm/version.rb
CHANGED