turnkit 0.9.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73e85ee51e846b0d62828d964ad53b00fd0f6ce1e11d56611b47b45462d49d9f
4
- data.tar.gz: f4c4591902e4a9a9fc4c2a50c6e768ad6a4191ac242418434ff5d8b22e99143b
3
+ metadata.gz: af637ae31bf82bcdaaf638f4adc5b8aca0273b61a87e79783f257c792c4d7f1d
4
+ data.tar.gz: b25b5adc0626e1eea3643aed4a71ae5829df105c9d6c6296739ad2b7913ade67
5
5
  SHA512:
6
- metadata.gz: cd39ba14aa1e48ce133d8b4cffc76d63939494a90466cfd62a5b70cb612f76f2caf8a5ffafc371272f00644ff1842a9b96768fea0724788721c45c3d36163655
7
- data.tar.gz: a77ae5606a43be2790b54cf2a6c8eb78225b5f8b41d0f7b166b8c67b6568a357e65d1ae8f690b446c55afde370aab33d1d3f21dad30a7aec493eb2eb6007224c
6
+ metadata.gz: 6318b5bdab01bb9c81668a817b91b60e314ef60d64608a7a8cc30a3ef5caeb931aa501231cde114538cd821d45dcf41aaee2f238fe6871f4f4f6cc22e553b70f
7
+ data.tar.gz: f2a801c4cb643e67078b71f18c9814ee38ca9353255b7ca6cbf0294f558a2688790b349daf0015ae48e6777ea6b03b237d089ee98cc77ac92aad1690f11ef0a5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.0 - 2026-09-19
4
+
5
+ - Add `Adapters::OpenRouterJev` for the non-chat Decisions alpha endpoint, reusing
6
+ evaluation receipts, budgets, and fenced accounting. Prefer reported provider
7
+ charges and preserve billable malformed responses without weakening typed answers.
8
+ - Document OpenRouter shadow/assist integration and add an opt-in, synthetic
9
+ one-request smoke test with semantic checks and runtime receipt replay.
10
+
3
11
  ## 0.9.0 - 2026-09-19
4
12
 
5
13
  - Add `Turn#internal_evaluation` and a dependency-free Cloudflare Jev adapter for
data/README.md CHANGED
@@ -19,7 +19,7 @@ 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.
20
20
 
21
21
  For non-generative judgments, use [structured evaluations](docs/structured-evaluations.md).
22
- Cloudflare Jev supports Noul, Choice, and Score through a separate typed API with
22
+ Cloudflare and OpenRouter Jev support Noul, Choice, and Score through a separate typed API with
23
23
  durable receipts, usage/cost accounting, and output-audit integration—not chat messages.
24
24
 
25
25
  ## Installation
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "timeout"
5
+
6
+ module TurnKit
7
+ module Adapters
8
+ # Decisions transport only. Turn owns attempts, accounting, and recovery.
9
+ class OpenRouterJev
10
+ MODEL = "typesafe/jev-1.13"
11
+ ENDPOINT = "https://openrouter.ai/api/alpha/decisions"
12
+
13
+ def initialize(api_key:, billing_identity:)
14
+ raise ConfigError, "OpenRouter API key is required" if api_key.to_s.empty?
15
+ raise ConfigError, "OpenRouter billing identity is required" if billing_identity.to_s.empty?
16
+ @api_key, @billing_identity = api_key, billing_identity
17
+ end
18
+
19
+ def inspect = "#<#{self.class.name}>"
20
+ def identity = { "provider" => "openrouter", "endpoint" => ENDPOINT, "account" => @billing_identity, "schema" => 1 }
21
+ def cost_model(model:) = "openrouter/#{model}"
22
+
23
+ def validate!(model:, state:, questions:)
24
+ raise InputError, "unsupported OpenRouter evaluation model" unless model == MODEL
25
+ input = Evaluation.input!(state: state, questions: questions)
26
+ raise InputError, "OpenRouter state cannot be null" if input["state"].nil?
27
+ input["questions"].each_value do |question|
28
+ valid = !question["instructions"].nil?
29
+ criteria = question["criteria"]
30
+ if question["type"] == "noul" && question.key?("criteria")
31
+ valid &&= criteria.is_a?(Hash) && criteria.keys.sort == %w[false true] && criteria.values.none?(&:nil?)
32
+ elsif question["type"] == "score"
33
+ valid &&= criteria.none?(&:nil?)
34
+ end
35
+ raise InputError, "invalid OpenRouter question" unless valid
36
+ end
37
+ input
38
+ end
39
+
40
+ def evaluate(model:, state:, questions:, timeout:)
41
+ raise ArgumentError, "timeout must be positive and finite" unless timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
42
+ input = validate!(model: model, state: state, questions: questions)
43
+ uri = URI(ENDPOINT)
44
+ request = Net::HTTP::Post.new(uri)
45
+ request["Authorization"] = "Bearer #{@api_key}"
46
+ request["Content-Type"] = "application/json"
47
+ request["Accept"] = "application/json"
48
+ request.body = JSON.generate(input.merge("model" => model))
49
+ response = Timeout.timeout(timeout) do
50
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true, open_timeout: timeout, read_timeout: timeout, write_timeout: timeout) do |http|
51
+ http.max_retries = 0
52
+ http.request(request)
53
+ end
54
+ end
55
+ code = response.code.to_i
56
+ begin
57
+ value = JSON.parse(response.body)
58
+ rescue JSON::ParserError
59
+ value = nil
60
+ end
61
+ observed = observed_usage(value)
62
+ returned_model = value["model"] if value.is_a?(Hash) && value["model"].is_a?(String)
63
+ unless code.between?(200, 299)
64
+ uncertain = code == 408 || code >= 500
65
+ raise EvaluationError.new(uncertain ? :uncertain : :unavailable,
66
+ retryable: uncertain || code == 429, retry_after: retry_delay(response["Retry-After"]),
67
+ http_status: code, usage: observed, model: returned_model)
68
+ end
69
+ unless value.is_a?(Hash) && valid_cost?(value["usage"])
70
+ raise EvaluationError.new(:malformed, usage: observed, model: returned_model)
71
+ end
72
+ # OpenRouter's envelope and optional charge are not part of Jev's closed
73
+ # answer schema. Never synthesize its optional confidence/distributions.
74
+ normalized = value.slice("model", "answers", "usage")
75
+ normalized["usage"] = value["usage"].slice("input_tokens", "output_tokens")
76
+ begin
77
+ result = Evaluation.result!(normalized, questions: input.fetch("questions"))
78
+ rescue EvaluationError
79
+ raise EvaluationError.new(:malformed, usage: observed, model: returned_model), cause: nil
80
+ end
81
+ EvaluationResult.new(model: result.model, answers: result.answers, usage: observed)
82
+ rescue Timeout::Error, IOError, SystemCallError, OpenSSL::SSL::SSLError, SocketError, Net::HTTPBadResponse, Net::ProtocolError
83
+ raise EvaluationError.new(:uncertain, retryable: true), cause: nil
84
+ end
85
+
86
+ private
87
+ def valid_cost?(usage)
88
+ return false unless usage.is_a?(Hash)
89
+ return true unless usage.key?("cost")
90
+ cost = usage["cost"]
91
+ cost.is_a?(Numeric) && cost.finite? && cost >= 0
92
+ end
93
+
94
+ def observed_usage(value)
95
+ usage = value["usage"] if value.is_a?(Hash)
96
+ return unless usage.is_a?(Hash)
97
+ tokens = Evaluation.usage(usage.slice("input_tokens", "output_tokens"))
98
+ cost = usage["cost"] if valid_cost?(usage)
99
+ return unless tokens || cost
100
+ Usage.new(input_tokens: tokens&.input_tokens, output_tokens: tokens&.output_tokens, cost: cost)
101
+ end
102
+
103
+ def retry_delay(value)
104
+ return unless value
105
+ return value.to_f if value.match?(/\A\d+(\.\d+)?\z/)
106
+ [Time.httpdate(value) - Clock.now, 0].max
107
+ rescue ArgumentError
108
+ nil
109
+ end
110
+ end
111
+ end
112
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TurnKit
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
data/lib/turnkit.rb CHANGED
@@ -52,6 +52,7 @@ require_relative "turnkit/turn"
52
52
  require_relative "turnkit/usage"
53
53
  require_relative "turnkit/run"
54
54
  require_relative "turnkit/adapters/cloudflare_jev"
55
+ require_relative "turnkit/adapters/open_router_jev"
55
56
  require_relative "turnkit/adapters/codex"
56
57
  require_relative "turnkit/adapters/ruby_llm"
57
58
  require_relative "turnkit/active_record_store"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turnkit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Couch
@@ -39,6 +39,7 @@ files:
39
39
  - lib/turnkit/active_record_store.rb
40
40
  - lib/turnkit/adapters/cloudflare_jev.rb
41
41
  - lib/turnkit/adapters/codex.rb
42
+ - lib/turnkit/adapters/open_router_jev.rb
42
43
  - lib/turnkit/adapters/ruby_llm.rb
43
44
  - lib/turnkit/agent.rb
44
45
  - lib/turnkit/authorization.rb