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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: af637ae31bf82bcdaaf638f4adc5b8aca0273b61a87e79783f257c792c4d7f1d
4
- data.tar.gz: b25b5adc0626e1eea3643aed4a71ae5829df105c9d6c6296739ad2b7913ade67
3
+ metadata.gz: 74d8be12d139ca9356ce9040853c4e78a1900f57d78428987e9e8ef930dde9c8
4
+ data.tar.gz: 88311f1061514d6afbd6484d497118718cf0ca95cec7757b9552db026efa8d70
5
5
  SHA512:
6
- metadata.gz: 6318b5bdab01bb9c81668a817b91b60e314ef60d64608a7a8cc30a3ef5caeb931aa501231cde114538cd821d45dcf41aaee2f238fe6871f4f4f6cc22e553b70f
7
- data.tar.gz: f2a801c4cb643e67078b71f18c9814ee38ca9353255b7ca6cbf0294f558a2688790b349daf0015ae48e6777ea6b03b237d089ee98cc77ac92aad1690f11ef0a5
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, opt into the pinned RubyLLM 2 release candidate and
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 2.0 (currently 2.0.0.rc2)"
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 || "1024x1024",
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 || "1024x1024" }.merge(params || {}), metadata: metadata)
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
- chat = ::RubyLLM.chat(model: model)
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
- replay = Array(message[:provider_parts] || message["provider_parts"]).find { |part| part["kind"] == kind } if kind
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
- from_hash(::RubyLLM::Cost.new(tokens: tokens, model: model_info).to_h)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurnKit
4
- VERSION = "0.10.0"
4
+ VERSION = "0.10.1"
5
5
  end
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.0
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-09-19 00:00:00.000000000 Z
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,