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 +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +1 -1
- data/lib/turnkit/adapters/open_router_jev.rb +112 -0
- data/lib/turnkit/version.rb +1 -1
- data/lib/turnkit.rb +1 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: af637ae31bf82bcdaaf638f4adc5b8aca0273b61a87e79783f257c792c4d7f1d
|
|
4
|
+
data.tar.gz: b25b5adc0626e1eea3643aed4a71ae5829df105c9d6c6296739ad2b7913ade67
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
data/lib/turnkit/version.rb
CHANGED
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.
|
|
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
|