llm_providers 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3083a87542682bdd5de9b30dc4823466d607c9f46c1664ebef59af054f069b1
4
- data.tar.gz: 1a76be65cb40caeb91cf0aafdd899df2b5f71fde80a0d02ddca7a13a00eb3c9e
3
+ metadata.gz: 65eb7fcee6973ec4cd31c06f1c9b0a04f82682cab0f2799d711dda78d4d09f0d
4
+ data.tar.gz: 9c5ea371c80193690d08e5b412ae1adb9872b2b6b39cb845ef09364dd9abc272
5
5
  SHA512:
6
- metadata.gz: 168939cac343a5da282742689977d948a1d4b17d88e220034f1625135ae5a1cb866327c70d8690ad91c3b7af83089889a3e9d97245d5c5cbd3cc8e5c6679b56b
7
- data.tar.gz: e71cd2f4822d47ce6425ad7519f4f4d6198bd083c91f0c653afb4447d50a1f61c353a8f98b88c0d124becc9012344251df1e1d23a5d6d0f9940239587cc82043
6
+ metadata.gz: 6c5bd8f0e9eaa26830433d938322eee4cf4292a68a16031a6c088f819b6b311433ebfe5780dd9f95dbbbc7e7ca9eb4dfd08bc43a88f02e8c3552ade590984019
7
+ data.tar.gz: 10fed167a8f5835f60d3a4d475ec46ee81a80eab2e7a774b6fc3a864b016d0a7a823a5101d986ec20dfeab1d49fab985e877a18998d2b52fab237479f481bf9e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1] - 2026-10-11
4
+
5
+ ### Fixed
6
+
7
+ - Gemini tool loops failed on the second round with `Role 'function' is not supported`. Tool results are now sent as a `user` turn of `functionResponse` parts, and results for parallel calls share one turn.
8
+ - When a tool message has no `tool_name`, the function response is named after the call it answers (looked up by `tool_call_id`). It used to fall back to `"function"`.
9
+ - Gemini 3 returns a `thoughtSignature` on function calls and rejects the next request without it. Tool calls now carry it as `thought_signature`, and it goes back on the matching `functionCall` part.
10
+
3
11
  ## [0.4.0] - 2026-10-10
4
12
 
5
13
  ### Added
@@ -65,40 +65,54 @@ module LlmProviders
65
65
  payload
66
66
  end
67
67
 
68
+ # Gemini only accepts user/model roles. Tool results go back as a user turn of
69
+ # functionResponse parts (one turn per batch of calls), named after the call they
70
+ # answer. Gemini 3 also rejects a functionCall replayed without its thoughtSignature.
68
71
  def format_messages(messages)
69
- messages.filter_map do |msg|
72
+ call_names = {}
73
+
74
+ messages.each_with_object([]) do |msg, contents|
70
75
  msg = stringify_keys(msg)
71
76
  next if msg["role"] == "system"
72
77
 
73
- role = msg["role"] == "assistant" ? "model" : msg["role"]
74
-
75
78
  if msg["role"] == "tool"
76
- {
77
- role: "function",
78
- parts: [{
79
- functionResponse: {
80
- name: msg["tool_name"] || "function",
81
- response: { content: msg["content"] }
82
- }
83
- }]
79
+ part = {
80
+ functionResponse: {
81
+ name: msg["tool_name"] || call_names[msg["tool_call_id"]] || "function",
82
+ response: { content: msg["content"] }
83
+ }
84
84
  }
85
+ if contents.last && contents.last[:function_responses]
86
+ contents.last[:parts] << part
87
+ else
88
+ contents << { role: "user", parts: [part], function_responses: true }
89
+ end
85
90
  elsif msg["tool_calls"] && !msg["tool_calls"].empty?
86
91
  parts = []
87
92
  parts << { text: msg["content"] } if msg["content"] && !msg["content"].empty?
88
93
  msg["tool_calls"].each do |tc|
89
94
  tc = stringify_keys(tc)
90
- parts << {
91
- functionCall: {
92
- name: tc["name"],
93
- args: tc["input"]
94
- }
95
- }
95
+ call_names[tc["id"]] = tc["name"] if tc["id"]
96
+ part = { functionCall: { name: tc["name"], args: tc["input"] || {} } }
97
+ part[:thoughtSignature] = tc["thought_signature"] if tc["thought_signature"]
98
+ parts << part
96
99
  end
97
- { role: role, parts: parts }
100
+ contents << { role: "model", parts: parts }
98
101
  else
99
- { role: role, parts: [{ text: msg["content"] }] }
102
+ role = msg["role"] == "assistant" ? "model" : msg["role"]
103
+ contents << { role: role, parts: [{ text: msg["content"] }] }
100
104
  end
101
- end
105
+ end.map { |content| content.except(:function_responses) }
106
+ end
107
+
108
+ def tool_call_from(part)
109
+ call = {
110
+ id: SecureRandom.hex(12),
111
+ name: part.dig("functionCall", "name"),
112
+ input: part.dig("functionCall", "args") || {}
113
+ }
114
+ call[:thought_signature] = part["thoughtSignature"] if part["thoughtSignature"]
115
+ call
102
116
  end
103
117
 
104
118
  def format_tools(tools)
@@ -205,11 +219,7 @@ module LlmProviders
205
219
  yield(content: part["text"]) if part["text"] && !part["thought"]
206
220
  next unless part["functionCall"]
207
221
 
208
- tool_calls << {
209
- id: SecureRandom.hex(12),
210
- name: part.dig("functionCall", "name"),
211
- input: part.dig("functionCall", "args") || {}
212
- }
222
+ tool_calls << tool_call_from(part)
213
223
  end
214
224
  end
215
225
 
@@ -249,15 +259,7 @@ module LlmProviders
249
259
  parts = candidate&.dig("content", "parts") || []
250
260
 
251
261
  content = parts.filter_map { |p| p["text"] unless p["thought"] }.join
252
- tool_calls = parts.filter_map do |p|
253
- next unless p["functionCall"]
254
-
255
- {
256
- id: SecureRandom.hex(12),
257
- name: p.dig("functionCall", "name"),
258
- input: p.dig("functionCall", "args") || {}
259
- }
260
- end
262
+ tool_calls = parts.filter_map { |p| tool_call_from(p) if p["functionCall"] }
261
263
 
262
264
  {
263
265
  content: content,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmProviders
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.1"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_providers
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kaba
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-10 00:00:00.000000000 Z
11
+ date: 2026-10-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday