turnkit 0.10.0 → 0.10.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 +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +1 -1
- data/lib/turnkit/adapters/ruby_llm.rb +31 -12
- data/lib/turnkit/cost.rb +2 -1
- data/lib/turnkit/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 74d8be12d139ca9356ce9040853c4e78a1900f57d78428987e9e8ef930dde9c8
|
|
4
|
+
data.tar.gz: 88311f1061514d6afbd6484d497118718cf0ca95cec7757b9552db026efa8d70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 595dc6ea8bdea14f84c5844d5b8fb29769e7ec8dd8d14674af4ae81f4c897446fcf3a3afd2528c2dd3af847f708f312c5ca5d314b4229260b411c93ffe8276ba
|
|
7
|
+
data.tar.gz: d8d9f658f19f538297c9670494a71d7c5e02f086d5980c116053de8f4c17bd336d3970a95b1443b878bbaf74b1f2746af4325020d4db76869c2470c42bd879e5
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.1 - 2026-10-13
|
|
4
|
+
|
|
5
|
+
- Validate against stable RubyLLM 2.1.1 while retaining the 1.16 compatibility bundle.
|
|
6
|
+
- Scope opaque provider replay to the originating requested model and matching
|
|
7
|
+
protocol. Older records without model provenance fall back to portable text
|
|
8
|
+
and tool calls; signed continuation state is not reused across model changes.
|
|
9
|
+
- Forward media provider/protocol selection, leave omitted image sizes to the
|
|
10
|
+
provider, and classify SDK configuration errors as TurnKit configuration errors.
|
|
11
|
+
- Preserve unknown registry costs when prices are incomplete, including after
|
|
12
|
+
serialization and aggregation.
|
|
13
|
+
|
|
3
14
|
## 0.10.0 - 2026-09-19
|
|
4
15
|
|
|
5
16
|
- Add `Adapters::OpenRouterJev` for the non-chat Decisions alpha endpoint, reusing
|
data/README.md
CHANGED
|
@@ -13,7 +13,7 @@ plan. `turn.pause!`/`resume!` preserve progress and release background workers;
|
|
|
13
13
|
use `descendants: :cascade` for a research subtree. See
|
|
14
14
|
[interactive research](docs/interactive-research.md) for exact interruption
|
|
15
15
|
boundaries, durable receipts, approval gates, and Rails integration.
|
|
16
|
-
For GPT-6 Astra tools,
|
|
16
|
+
For GPT-6 Astra tools, use stable RubyLLM 2.1.1 and the
|
|
17
17
|
[Responses protocol](docs/interactive-research.md#gpt-6-astra-and-provider-continuation-state).
|
|
18
18
|
The [live validation app](examples/interactive_validation/README.md) exercises
|
|
19
19
|
these controls with Rails 8.1, PostgreSQL, Sidekiq, and actual Astra/xhigh requests.
|
|
@@ -18,7 +18,7 @@ module TurnKit
|
|
|
18
18
|
ensure_ruby_llm!
|
|
19
19
|
raise ModelAccessError, "model is required" if model.to_s.empty?
|
|
20
20
|
if @protocol && !::RubyLLM::Chat.method_defined?(:generate)
|
|
21
|
-
raise ConfigError, "protocol selection requires RubyLLM
|
|
21
|
+
raise ConfigError, "protocol selection requires RubyLLM >= 2.0"
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
configure_from_environment
|
|
@@ -58,7 +58,7 @@ module TurnKit
|
|
|
58
58
|
chat.respond_to?(:with_tool) ? chat.with_tool(ruby_llm_tool(tool)) : chat.with_tools(ruby_llm_tool(tool))
|
|
59
59
|
end
|
|
60
60
|
tool_names = {}
|
|
61
|
-
Array(messages).each { |message| add_message(chat, message, provider: chat.model.provider, tool_names: tool_names) }
|
|
61
|
+
Array(messages).each { |message| add_message(chat, message, provider: chat.model.provider, model: model, tool_names: tool_names) }
|
|
62
62
|
# The runtime supplies durable snapshots in messages. Direct adapter
|
|
63
63
|
# callers must preserve this message themselves on subsequent calls.
|
|
64
64
|
if context_in_history && !dynamic_instructions.to_s.empty?
|
|
@@ -69,6 +69,8 @@ module TurnKit
|
|
|
69
69
|
normalize_response(response, model: model)
|
|
70
70
|
rescue ConfigError
|
|
71
71
|
raise
|
|
72
|
+
rescue ::RubyLLM::ConfigurationError => error
|
|
73
|
+
raise ConfigError, error.message
|
|
72
74
|
rescue ::RubyLLM::Error, ::RubyLLM::ModelNotFoundError => error
|
|
73
75
|
# Provider failures (after RubyLLM's own retries) are task failures,
|
|
74
76
|
# not worker crashes to be replayed by durable reconciliation.
|
|
@@ -83,14 +85,16 @@ module TurnKit
|
|
|
83
85
|
model: model,
|
|
84
86
|
provider: provider,
|
|
85
87
|
assume_model_exists: assume_model_exists || false,
|
|
86
|
-
size: size
|
|
88
|
+
size: size,
|
|
87
89
|
with: input_images,
|
|
88
90
|
mask: mask,
|
|
89
91
|
**{ (::RubyLLM::Chat.method_defined?(:generate) ? :provider_options : :params) => params || {} }
|
|
90
92
|
)
|
|
91
|
-
normalize_image_response(image, model: model, provider: provider, params: { "size" => size
|
|
93
|
+
normalize_image_response(image, model: model, provider: provider, params: { "size" => size }.compact.merge(params || {}), metadata: metadata)
|
|
92
94
|
rescue ConfigError
|
|
93
95
|
raise
|
|
96
|
+
rescue ::RubyLLM::ConfigurationError => error
|
|
97
|
+
raise ConfigError, error.message
|
|
94
98
|
rescue ::RubyLLM::Error, ::RubyLLM::ModelNotFoundError => error
|
|
95
99
|
raise ModelError, "#{error.class}: #{error.message}"
|
|
96
100
|
end
|
|
@@ -100,7 +104,8 @@ module TurnKit
|
|
|
100
104
|
configure_from_environment
|
|
101
105
|
media_input = MediaInput.wrap(media)
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
validate!(model: model) if @protocol
|
|
108
|
+
chat = ::RubyLLM.chat(**{ model: model, provider: provider, protocol: @protocol }.compact)
|
|
104
109
|
chat.with_schema(normalize_schema(output_schema)) if output_schema
|
|
105
110
|
if params && !params.empty?
|
|
106
111
|
if ::RubyLLM::Chat.method_defined?(:generate)
|
|
@@ -122,6 +127,8 @@ module TurnKit
|
|
|
122
127
|
normalize_media_analysis_response(response, media: media_input, model: model, provider: provider, params: params || {}, metadata: metadata)
|
|
123
128
|
rescue ConfigError
|
|
124
129
|
raise
|
|
130
|
+
rescue ::RubyLLM::ConfigurationError => error
|
|
131
|
+
raise ConfigError, error.message
|
|
125
132
|
rescue ::RubyLLM::Error, ::RubyLLM::ModelNotFoundError => error
|
|
126
133
|
raise ModelError, "#{error.class}: #{error.message}"
|
|
127
134
|
end
|
|
@@ -185,11 +192,23 @@ module TurnKit
|
|
|
185
192
|
chat.send(:provider_completion)
|
|
186
193
|
end
|
|
187
194
|
|
|
188
|
-
def add_message(chat, message, provider: nil, tool_names: {})
|
|
195
|
+
def add_message(chat, message, provider: nil, model: nil, tool_names: {})
|
|
189
196
|
role = (message[:role] || message["role"]).to_sym
|
|
190
197
|
content = message[:content] || message["content"] || ""
|
|
191
198
|
kind = { "openai" => "openai_responses", "anthropic" => "anthropic", "gemini" => "gemini" }[provider.to_s]
|
|
192
|
-
|
|
199
|
+
protocol = { "openai" => :responses, "anthropic" => :anthropic, "gemini" => :gemini }[provider.to_s]
|
|
200
|
+
if chat.respond_to?(:provider) && chat.provider.respond_to?(:protocol_for) && protocol
|
|
201
|
+
# Honor SDK configuration and per-model routing as well as our override.
|
|
202
|
+
configured = chat.provider.config.public_send("#{provider}_protocol")
|
|
203
|
+
selected = @protocol || configured
|
|
204
|
+
codec = selected ? chat.provider.protocols[selected.to_sym] : chat.provider.protocol_for(chat.model)
|
|
205
|
+
kind = nil unless codec == chat.provider.protocols[protocol]
|
|
206
|
+
elsif @protocol && @protocol.to_s != protocol.to_s
|
|
207
|
+
kind = nil
|
|
208
|
+
end
|
|
209
|
+
replay = Array(message[:provider_parts] || message["provider_parts"]).find do |part|
|
|
210
|
+
part["kind"] == kind && model && part["model"] == model
|
|
211
|
+
end if kind
|
|
193
212
|
calls = ruby_llm_tool_calls(message[:tool_calls] || message["tool_calls"])
|
|
194
213
|
raw_content = replay&.fetch("data")
|
|
195
214
|
if raw_content && %w[anthropic gemini].include?(kind) && !::RubyLLM::Chat.method_defined?(:generate)
|
|
@@ -292,7 +311,7 @@ module TurnKit
|
|
|
292
311
|
)
|
|
293
312
|
Result.new(
|
|
294
313
|
text: response_text(response),
|
|
295
|
-
parts: response_parts(response, tool_calls: tool_calls),
|
|
314
|
+
parts: response_parts(response, tool_calls: tool_calls, model: model),
|
|
296
315
|
output_data: response_data(response),
|
|
297
316
|
tool_calls: tool_calls,
|
|
298
317
|
usage: usage,
|
|
@@ -300,7 +319,7 @@ module TurnKit
|
|
|
300
319
|
)
|
|
301
320
|
end
|
|
302
321
|
|
|
303
|
-
def response_parts(response, tool_calls:)
|
|
322
|
+
def response_parts(response, tool_calls:, model:)
|
|
304
323
|
content = response.respond_to?(:content) ? response.content : response
|
|
305
324
|
parts = case content
|
|
306
325
|
when Array
|
|
@@ -313,11 +332,11 @@ module TurnKit
|
|
|
313
332
|
end.compact
|
|
314
333
|
raw = response.raw.body if response.respond_to?(:raw) && response.raw.respond_to?(:body)
|
|
315
334
|
if raw.is_a?(Hash) && raw["object"] == "response"
|
|
316
|
-
parts << { "type" => "provider", "kind" => "openai_responses", "data" => raw.fetch("output") }
|
|
335
|
+
parts << { "type" => "provider", "kind" => "openai_responses", "model" => model, "data" => raw.fetch("output") }
|
|
317
336
|
elsif raw.is_a?(Hash) && raw["type"] == "message" && raw["role"] == "assistant"
|
|
318
|
-
parts << { "type" => "provider", "kind" => "anthropic", "data" => raw.fetch("content") }
|
|
337
|
+
parts << { "type" => "provider", "kind" => "anthropic", "model" => model, "data" => raw.fetch("content") }
|
|
319
338
|
elsif raw.is_a?(Hash) && raw.dig("candidates", 0, "content", "parts")
|
|
320
|
-
parts << { "type" => "provider", "kind" => "gemini", "data" => raw.dig("candidates", 0, "content", "parts") }
|
|
339
|
+
parts << { "type" => "provider", "kind" => "gemini", "model" => model, "data" => raw.dig("candidates", 0, "content", "parts") }
|
|
321
340
|
end
|
|
322
341
|
parts + Array(tool_calls).map { |call| { "type" => "tool_call", "id" => call.id, "name" => call.name, "arguments" => call.arguments } }
|
|
323
342
|
end
|
data/lib/turnkit/cost.rb
CHANGED
|
@@ -91,7 +91,8 @@ module TurnKit
|
|
|
91
91
|
::RubyLLM::Tokens.new(input: usage.input_tokens, output: usage.output_tokens,
|
|
92
92
|
cached: usage.cached_tokens, cache_creation: usage.cache_write_tokens, thinking: usage.thinking_tokens)
|
|
93
93
|
end
|
|
94
|
-
|
|
94
|
+
cost = ::RubyLLM::Cost.new(tokens: tokens, model: model_info)
|
|
95
|
+
from_hash(cost.to_h.merge(unknown: cost.total.nil?))
|
|
95
96
|
rescue ::RubyLLM::ModelNotFoundError
|
|
96
97
|
new
|
|
97
98
|
end
|
data/lib/turnkit/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: turnkit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.10.
|
|
4
|
+
version: 0.10.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sam Couch
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: TurnKit is a Ruby/Rails agent runtime for durable AI conversations, application
|
|
14
14
|
runs, orchestrator agents, tool calling, skills, sub-agents, context compaction,
|