lex-llm 0.4.18 → 0.5.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/.rubocop.yml +13 -2
- data/B1b-conformance-kit.md +79 -0
- data/CHANGELOG.md +19 -0
- data/lex-llm.gemspec +2 -3
- data/lib/legion/extensions/llm/attachment.rb +1 -1
- data/lib/legion/extensions/llm/canonical/chunk.rb +184 -0
- data/lib/legion/extensions/llm/canonical/content_block.rb +126 -0
- data/lib/legion/extensions/llm/canonical/message.rb +125 -0
- data/lib/legion/extensions/llm/canonical/params.rb +61 -0
- data/lib/legion/extensions/llm/canonical/request.rb +117 -0
- data/lib/legion/extensions/llm/canonical/response.rb +124 -0
- data/lib/legion/extensions/llm/canonical/thinking.rb +81 -0
- data/lib/legion/extensions/llm/canonical/tool_call.rb +134 -0
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +73 -0
- data/lib/legion/extensions/llm/canonical/usage.rb +61 -0
- data/lib/legion/extensions/llm/canonical.rb +49 -0
- data/lib/legion/extensions/llm/chat.rb +3 -5
- data/lib/legion/extensions/llm/connection.rb +5 -1
- data/lib/legion/extensions/llm/error.rb +3 -7
- data/lib/legion/extensions/llm/fleet/envelope_validation.rb +1 -3
- data/lib/legion/extensions/llm/fleet/provider_responder.rb +1 -3
- data/lib/legion/extensions/llm/fleet/token_validator.rb +1 -3
- data/lib/legion/extensions/llm/model/info.rb +4 -6
- data/lib/legion/extensions/llm/models.rb +3 -3
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +7 -3
- data/lib/legion/extensions/llm/routing/lane_key.rb +1 -3
- data/lib/legion/extensions/llm/stream_accumulator.rb +1 -1
- data/lib/legion/extensions/llm/streaming.rb +1 -3
- data/lib/legion/extensions/llm/tool.rb +1 -3
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +118 -35
- data/spec/fixtures/ruby.mp3 +0 -0
- data/spec/fixtures/ruby.mp4 +0 -0
- data/spec/fixtures/ruby.png +0 -0
- data/spec/fixtures/ruby.txt +1 -0
- data/spec/fixtures/ruby.wav +0 -0
- data/spec/fixtures/ruby.xml +1 -0
- data/spec/fixtures/sample.pdf +0 -0
- data/spec/legion/extensions/llm/agent_spec.rb +179 -0
- data/spec/legion/extensions/llm/attachment_spec.rb +25 -0
- data/spec/legion/extensions/llm/auto_registration_spec.rb +38 -0
- data/spec/legion/extensions/llm/canonical/chunk_spec.rb +285 -0
- data/spec/legion/extensions/llm/canonical/content_block_spec.rb +179 -0
- data/spec/legion/extensions/llm/canonical/message_spec.rb +203 -0
- data/spec/legion/extensions/llm/canonical/params_spec.rb +159 -0
- data/spec/legion/extensions/llm/canonical/request_spec.rb +174 -0
- data/spec/legion/extensions/llm/canonical/response_spec.rb +234 -0
- data/spec/legion/extensions/llm/canonical/thinking_spec.rb +151 -0
- data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +191 -0
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +174 -0
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +138 -0
- data/spec/legion/extensions/llm/configuration_spec.rb +38 -0
- data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +269 -0
- data/spec/legion/extensions/llm/conformance/conformance.rb +51 -0
- data/spec/legion/extensions/llm/conformance/echo_translator.rb +56 -0
- data/spec/legion/extensions/llm/conformance/echo_translator_spec.rb +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_empty_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_error_response.json +19 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +81 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_metering_audit_events.json +101 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +21 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_response.json +13 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_stop_reason_matrix.json +36 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_accumulated_response.json +20 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_error_chunks.json +26 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_text_chunks.json +33 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_thinking_chunks.json +42 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_tool_call_chunks.json +41 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +14 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +18 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_response.json +17 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +75 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json +25 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +34 -0
- data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +390 -0
- data/spec/legion/extensions/llm/connection_logging_spec.rb +53 -0
- data/spec/legion/extensions/llm/connection_retry_spec.rb +36 -0
- data/spec/legion/extensions/llm/context_spec.rb +127 -0
- data/spec/legion/extensions/llm/credential_sources_spec.rb +468 -0
- data/spec/legion/extensions/llm/error_middleware_spec.rb +102 -0
- data/spec/legion/extensions/llm/error_spec.rb +87 -0
- data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +120 -0
- data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +163 -0
- data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +128 -0
- data/spec/legion/extensions/llm/fleet_messages_spec.rb +402 -0
- data/spec/legion/extensions/llm/gemspec_spec.rb +25 -0
- data/spec/legion/extensions/llm/message_spec.rb +64 -0
- data/spec/legion/extensions/llm/model/info_spec.rb +222 -0
- data/spec/legion/extensions/llm/models_spec.rb +104 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +203 -0
- data/spec/legion/extensions/llm/provider_contract_spec.rb +60 -0
- data/spec/legion/extensions/llm/provider_settings_spec.rb +76 -0
- data/spec/legion/extensions/llm/provider_spec.rb +592 -0
- data/spec/legion/extensions/llm/registry_event_builder_spec.rb +68 -0
- data/spec/legion/extensions/llm/registry_publisher_spec.rb +22 -0
- data/spec/legion/extensions/llm/responses/response_objects_spec.rb +75 -0
- data/spec/legion/extensions/llm/responses/thinking_extractor_spec.rb +75 -0
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +222 -0
- data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +50 -0
- data/spec/legion/extensions/llm/routing/registry_event_spec.rb +120 -0
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +103 -0
- data/spec/legion/extensions/llm/streaming_spec.rb +108 -0
- data/spec/legion/extensions/llm/tool_spec.rb +94 -0
- data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +60 -0
- data/spec/legion/extensions/llm/utils_spec.rb +113 -0
- data/spec/legion/extensions/llm_base_contract_spec.rb +110 -0
- data/spec/legion/extensions/llm_extension_spec.rb +78 -0
- data/spec/legion/extensions/llm_root_spec.rb +51 -0
- data/spec/spec_helper.rb +24 -0
- data/spec/support/fake_llm_provider.rb +148 -0
- data/spec/support/llm_configuration.rb +21 -0
- data/spec/support/rspec_configuration.rb +19 -0
- data/spec/support/simplecov_configuration.rb +20 -0
- metadata +96 -15
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shared examples for canonical client translator conformance.
|
|
4
|
+
#
|
|
5
|
+
# Every client translator must implement:
|
|
6
|
+
# - parse_request(body, env) → Canonical::Request
|
|
7
|
+
# - format_response(canonical_response) → Hash
|
|
8
|
+
# - format_chunk(canonical_chunk) → Hash | nil
|
|
9
|
+
# - format_error(error, status) → [status, Hash]
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# it_behaves_like 'a canonical client translator', MyClientTranslatorClass
|
|
13
|
+
# rubocop:disable Lint/NonLocalExitFromIterator -- return guard is idiomatic in shared_example blocks
|
|
14
|
+
RSpec.shared_examples 'a canonical client translator' do |translator_class|
|
|
15
|
+
let(:translator) { translator_class.new }
|
|
16
|
+
let(:canonical) { Legion::Extensions::Llm::Canonical }
|
|
17
|
+
let(:conformance) { Canonical::Conformance }
|
|
18
|
+
|
|
19
|
+
describe '#parse_request' do
|
|
20
|
+
context 'with a simple text request' do
|
|
21
|
+
let(:canonical_req) do
|
|
22
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_simple_text_request'))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'returns a Canonical::Request' do
|
|
26
|
+
return unless translator.respond_to?(:format_request)
|
|
27
|
+
|
|
28
|
+
formatted = translator.format_request(canonical_req)
|
|
29
|
+
return unless formatted
|
|
30
|
+
|
|
31
|
+
parsed = translator.parse_request(formatted, {})
|
|
32
|
+
expect(parsed).to be_a(canonical::Request)
|
|
33
|
+
expect(parsed.messages).to be_an(Array)
|
|
34
|
+
expect(parsed.messages.length).to be > 0
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context 'with a system prompt' do
|
|
39
|
+
let(:canonical_req) do
|
|
40
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_system_prompt_request'))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'preserves the system prompt' do
|
|
44
|
+
return unless translator.respond_to?(:format_request)
|
|
45
|
+
|
|
46
|
+
formatted = translator.format_request(canonical_req)
|
|
47
|
+
return unless formatted
|
|
48
|
+
|
|
49
|
+
parsed = translator.parse_request(formatted, {})
|
|
50
|
+
expect(parsed.system).to be_a(String)
|
|
51
|
+
expect(parsed.system).to include('haiku')
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
context 'with tools defined' do
|
|
56
|
+
let(:canonical_req) do
|
|
57
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_tools_request'))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it 'preserves tool definitions' do
|
|
61
|
+
return unless translator.respond_to?(:format_request)
|
|
62
|
+
|
|
63
|
+
formatted = translator.format_request(canonical_req)
|
|
64
|
+
return unless formatted
|
|
65
|
+
|
|
66
|
+
parsed = translator.parse_request(formatted, {})
|
|
67
|
+
expect(parsed.tools).to be_a(Hash)
|
|
68
|
+
expect(parsed.tools.keys).to include(:get_weather)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'with thinking enabled' do
|
|
73
|
+
let(:canonical_req) do
|
|
74
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_thinking_request'))
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'preserves thinking configuration' do
|
|
78
|
+
return unless translator.respond_to?(:format_request)
|
|
79
|
+
|
|
80
|
+
formatted = translator.format_request(canonical_req)
|
|
81
|
+
return unless formatted
|
|
82
|
+
|
|
83
|
+
parsed = translator.parse_request(formatted, {})
|
|
84
|
+
expect(parsed.thinking).to be_a(canonical::Thinking::Config)
|
|
85
|
+
expect(parsed.thinking.enabled?).to be true
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
context 'with parameter mapping' do
|
|
90
|
+
let(:canonical_req) do
|
|
91
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_params_mapping_request'))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'preserves sampling parameters' do
|
|
95
|
+
return unless translator.respond_to?(:format_request)
|
|
96
|
+
|
|
97
|
+
formatted = translator.format_request(canonical_req)
|
|
98
|
+
return unless formatted
|
|
99
|
+
|
|
100
|
+
parsed = translator.parse_request(formatted, {})
|
|
101
|
+
expect(parsed.params).to be_a(canonical::Params)
|
|
102
|
+
expect(parsed.params.max_tokens).to eq(2048)
|
|
103
|
+
expect(parsed.params.temperature).to eq(0.7)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
describe '#format_response' do
|
|
109
|
+
context 'with a simple text response' do
|
|
110
|
+
let(:canonical_resp) do
|
|
111
|
+
canonical::Response.from_hash(conformance.fixture_symbolized('canonical_simple_text_response'))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'formats a valid client response' do
|
|
115
|
+
formatted = translator.format_response(canonical_resp)
|
|
116
|
+
expect(formatted).to be_a(Hash)
|
|
117
|
+
expect(formatted).not_to be_empty
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
it 'includes the text content' do
|
|
121
|
+
formatted = translator.format_response(canonical_resp)
|
|
122
|
+
formatted_str = formatted.to_s
|
|
123
|
+
expect(formatted_str).to include('doing well')
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
context 'with a tool use response' do
|
|
128
|
+
let(:canonical_resp) do
|
|
129
|
+
canonical::Response.from_hash(conformance.fixture_symbolized('canonical_tool_use_response'))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
it 'formats tool calls in client-appropriate format' do
|
|
133
|
+
formatted = translator.format_response(canonical_resp)
|
|
134
|
+
formatted_str = formatted.to_s.downcase
|
|
135
|
+
expect(formatted_str).to include('get_weather')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
it 'includes tool call arguments' do
|
|
139
|
+
formatted = translator.format_response(canonical_resp)
|
|
140
|
+
formatted_str = formatted.to_s
|
|
141
|
+
expect(formatted_str).to include('San Francisco')
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
context 'with a thinking response' do
|
|
146
|
+
let(:canonical_resp) do
|
|
147
|
+
canonical::Response.from_hash(conformance.fixture_symbolized('canonical_thinking_response'))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
it 'includes thinking content in client format' do
|
|
151
|
+
formatted = translator.format_response(canonical_resp)
|
|
152
|
+
formatted_str = formatted.to_s.downcase
|
|
153
|
+
expect(formatted_str).to match(/think|reason|quantum/)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context 'with an error response' do
|
|
158
|
+
let(:canonical_resp) do
|
|
159
|
+
canonical::Response.from_hash(conformance.fixture_symbolized('canonical_error_response'))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
it 'formats error responses without crashing' do
|
|
163
|
+
formatted = translator.format_response(canonical_resp)
|
|
164
|
+
expect(formatted).to be_a(Hash)
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
describe '#format_chunk' do
|
|
170
|
+
context 'with text delta chunks' do
|
|
171
|
+
let(:stream_fixture) { conformance.fixture('canonical_streaming_text_chunks') }
|
|
172
|
+
let(:chunks_data) { stream_fixture['chunks'] }
|
|
173
|
+
|
|
174
|
+
it 'formats text delta chunks' do
|
|
175
|
+
text_chunk_hash = chunks_data.find { |c| c['type'] == 'text_delta' }
|
|
176
|
+
chunk = canonical::Chunk.from_hash(text_chunk_hash)
|
|
177
|
+
formatted = translator.format_chunk(chunk)
|
|
178
|
+
|
|
179
|
+
return unless formatted
|
|
180
|
+
|
|
181
|
+
expect(formatted).to be_a(Hash)
|
|
182
|
+
formatted_str = formatted.to_s
|
|
183
|
+
expect(formatted_str).to include(chunk.delta)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'formats the done chunk' do
|
|
187
|
+
done_chunk_hash = chunks_data.find { |c| c['type'] == 'done' }
|
|
188
|
+
chunk = canonical::Chunk.from_hash(done_chunk_hash)
|
|
189
|
+
formatted = translator.format_chunk(chunk)
|
|
190
|
+
|
|
191
|
+
return unless formatted
|
|
192
|
+
|
|
193
|
+
expect(formatted).to be_a(Hash)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
context 'with thinking delta chunks' do
|
|
198
|
+
let(:stream_fixture) { conformance.fixture('canonical_streaming_thinking_chunks') }
|
|
199
|
+
let(:chunks_data) { stream_fixture['chunks'] }
|
|
200
|
+
|
|
201
|
+
it 'formats thinking delta chunks' do
|
|
202
|
+
thinking_chunk_hash = chunks_data.find { |c| c['type'] == 'thinking_delta' }
|
|
203
|
+
chunk = canonical::Chunk.from_hash(thinking_chunk_hash)
|
|
204
|
+
formatted = translator.format_chunk(chunk)
|
|
205
|
+
|
|
206
|
+
return unless formatted
|
|
207
|
+
|
|
208
|
+
expect(formatted).to be_a(Hash)
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
context 'with tool call delta chunks' do
|
|
213
|
+
let(:stream_fixture) { conformance.fixture('canonical_streaming_tool_call_chunks') }
|
|
214
|
+
let(:chunks_data) { stream_fixture['chunks'] }
|
|
215
|
+
|
|
216
|
+
it 'formats tool call delta chunks' do
|
|
217
|
+
tool_chunk_hash = chunks_data.find { |c| c['type'] == 'tool_call_delta' }
|
|
218
|
+
chunk = canonical::Chunk.from_hash(tool_chunk_hash)
|
|
219
|
+
formatted = translator.format_chunk(chunk)
|
|
220
|
+
|
|
221
|
+
return unless formatted
|
|
222
|
+
|
|
223
|
+
expect(formatted).to be_a(Hash)
|
|
224
|
+
formatted_str = formatted.to_s.downcase
|
|
225
|
+
expect(formatted_str).to include('get_weather')
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
describe '#format_error' do
|
|
231
|
+
it 'formats an error with status code' do
|
|
232
|
+
error = StandardError.new('Test error')
|
|
233
|
+
result = translator.format_error(error, 500)
|
|
234
|
+
expect(result).to be_an(Array)
|
|
235
|
+
expect(result.length).to eq(2)
|
|
236
|
+
expect(result[0]).to eq(500)
|
|
237
|
+
expect(result[1]).to be_a(Hash)
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
describe 'round-trip consistency' do
|
|
242
|
+
context 'with request round-trip' do
|
|
243
|
+
let(:canonical_req) do
|
|
244
|
+
canonical::Request.from_hash(conformance.fixture_symbolized('canonical_simple_text_request'))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
it 'preserves message content through format/parse cycle' do
|
|
248
|
+
return unless translator.respond_to?(:format_request)
|
|
249
|
+
|
|
250
|
+
formatted = translator.format_request(canonical_req)
|
|
251
|
+
parsed = translator.parse_request(formatted, {})
|
|
252
|
+
expect(parsed.messages.length).to eq(canonical_req.messages.length)
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
context 'with response round-trip' do
|
|
257
|
+
let(:canonical_resp) do
|
|
258
|
+
canonical::Response.from_hash(conformance.fixture_symbolized('canonical_simple_text_response'))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
it 'preserves text through format cycle' do
|
|
262
|
+
formatted = translator.format_response(canonical_resp)
|
|
263
|
+
formatted_str = formatted.to_s
|
|
264
|
+
expect(formatted_str).to include(canonical_resp.text)
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
# rubocop:enable Lint/NonLocalExitFromIterator
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Conformance kit: shared RSpec example groups for N×N canonical routing.
|
|
4
|
+
#
|
|
5
|
+
# Ship location: spec/legion/extensions/llm/conformance/
|
|
6
|
+
# Module: Canonical::Conformance
|
|
7
|
+
#
|
|
8
|
+
# Consumer pattern (in provider gem spec_helper):
|
|
9
|
+
# kit = File.join(Gem.loaded_specs['lex-llm'].full_gem_path,
|
|
10
|
+
# 'spec/legion/extensions/llm/conformance')
|
|
11
|
+
# Dir[File.join(kit, '**', '*.rb')].sort.each { |f| require f }
|
|
12
|
+
#
|
|
13
|
+
# Then in specs:
|
|
14
|
+
# it_behaves_like 'a canonical provider translator', described_class
|
|
15
|
+
# it_behaves_like 'a canonical client translator', described_class
|
|
16
|
+
|
|
17
|
+
module Canonical
|
|
18
|
+
module Conformance
|
|
19
|
+
class << self
|
|
20
|
+
def fixtures_path
|
|
21
|
+
@fixtures_path ||= File.expand_path('fixtures', __dir__)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fixture(name)
|
|
25
|
+
path = File.join(fixtures_path, "#{name}.json")
|
|
26
|
+
raise ArgumentError, "Fixture not found: #{name}" unless File.exist?(path)
|
|
27
|
+
|
|
28
|
+
# Explicit encoding: fixtures contain UTF-8; a bare File.read obeys the
|
|
29
|
+
# ambient locale and breaks in shells without LANG set (CI, tool runners).
|
|
30
|
+
::JSON.parse(File.read(path, encoding: 'UTF-8'))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def fixture_symbolized(name)
|
|
34
|
+
deep_symbolize(fixture(name))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def deep_symbolize(obj)
|
|
40
|
+
case obj
|
|
41
|
+
when Hash then obj.transform_keys(&:to_sym).transform_values { |v| deep_symbolize(v) }
|
|
42
|
+
when Array then obj.map { |v| deep_symbolize(v) }
|
|
43
|
+
else obj
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
require_relative 'provider_translator_examples'
|
|
51
|
+
require_relative 'client_translator_examples'
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Trivial echo translator for conformance kit self-testing.
|
|
4
|
+
# Passes canonical types through unchanged, proving the shared example groups work.
|
|
5
|
+
|
|
6
|
+
module Canonical
|
|
7
|
+
module Conformance
|
|
8
|
+
# Echo translator: identity transform for both provider and client sides.
|
|
9
|
+
# Used exclusively as a self-test to verify the conformance kit works.
|
|
10
|
+
class EchoTranslator
|
|
11
|
+
def capabilities
|
|
12
|
+
{ provider: 'echo', thinking: true, streaming: true, tool_calls: true }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Provider translator interface
|
|
16
|
+
def render_request(canonical_request)
|
|
17
|
+
canonical_request.to_h
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse_response(wire_hash)
|
|
21
|
+
canonical::Response.from_hash(wire_hash)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def parse_chunk(raw_chunk)
|
|
25
|
+
canonical::Chunk.from_hash(raw_chunk)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Client translator interface
|
|
29
|
+
def format_request(canonical_request)
|
|
30
|
+
canonical_request.to_h
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def parse_request(body, _env = {})
|
|
34
|
+
canonical::Request.from_hash(body)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def format_response(canonical_response)
|
|
38
|
+
canonical_response.to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def format_chunk(canonical_chunk)
|
|
42
|
+
canonical_chunk.to_h
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_error(error, status)
|
|
46
|
+
[status, { error: error.message, type: error.class.name }]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def canonical
|
|
52
|
+
Legion::Extensions::Llm::Canonical
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
require_relative 'conformance'
|
|
5
|
+
require_relative 'echo_translator'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Canonical::Conformance::EchoTranslator do
|
|
8
|
+
# Self-test: the echo translator passes both conformance groups,
|
|
9
|
+
# proving the shared example groups work correctly.
|
|
10
|
+
|
|
11
|
+
it_behaves_like 'a canonical provider translator', described_class
|
|
12
|
+
it_behaves_like 'a canonical client translator', described_class
|
|
13
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"text": "",
|
|
3
|
+
"thinking": null,
|
|
4
|
+
"tool_calls": [],
|
|
5
|
+
"usage": {
|
|
6
|
+
"input_tokens": 0,
|
|
7
|
+
"output_tokens": 0
|
|
8
|
+
},
|
|
9
|
+
"stop_reason": "error",
|
|
10
|
+
"model": "test-model-1",
|
|
11
|
+
"routing": {},
|
|
12
|
+
"metadata": {
|
|
13
|
+
"error": {
|
|
14
|
+
"type": "invalid_request_error",
|
|
15
|
+
"message": "Model nonexistent-model-xyz-12345 not found",
|
|
16
|
+
"code": 404
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Fleet round-trip field mapping — R6",
|
|
3
|
+
"round_trip_request": {
|
|
4
|
+
"id": "req_fleet_rt_001",
|
|
5
|
+
"messages": [
|
|
6
|
+
{
|
|
7
|
+
"role": "user",
|
|
8
|
+
"content": [{ "type": "text", "text": "Fleet round-trip test message" }]
|
|
9
|
+
}
|
|
10
|
+
],
|
|
11
|
+
"system": "You are a test assistant.",
|
|
12
|
+
"tools": {
|
|
13
|
+
"test_tool": {
|
|
14
|
+
"name": "test_tool",
|
|
15
|
+
"description": "A test tool for round-trip validation",
|
|
16
|
+
"parameters": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": { "input": { "type": "string" } },
|
|
19
|
+
"required": ["input"]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"params": { "max_tokens": 1024, "temperature": 0.5 },
|
|
24
|
+
"thinking": { "effort": "low", "budget": 1024 },
|
|
25
|
+
"stream": false,
|
|
26
|
+
"conversation_id": "conv_fleet_rt_001",
|
|
27
|
+
"caller": { "type": "anthropic_messages", "client_id": "test_client" },
|
|
28
|
+
"routing": { "tier": "primary", "provider": "anthropic", "model": "claude-sonnet-4-6" },
|
|
29
|
+
"metadata": { "trace_id": "trace_abc123", "span_id": "span_def456" }
|
|
30
|
+
},
|
|
31
|
+
"round_trip_response": {
|
|
32
|
+
"text": "This is a fleet round-trip test response.",
|
|
33
|
+
"thinking": { "content": "Processing fleet round-trip test.", "signature": "sig_test_001" },
|
|
34
|
+
"tool_calls": [
|
|
35
|
+
{
|
|
36
|
+
"id": "call_fleet_rt_001",
|
|
37
|
+
"exchange_id": "exch_fleet_rt_001",
|
|
38
|
+
"name": "test_tool",
|
|
39
|
+
"arguments": { "input": "round_trip_test" },
|
|
40
|
+
"source": "client",
|
|
41
|
+
"status": "pending"
|
|
42
|
+
}
|
|
43
|
+
],
|
|
44
|
+
"usage": {
|
|
45
|
+
"input_tokens": 50,
|
|
46
|
+
"output_tokens": 30,
|
|
47
|
+
"cache_read_tokens": 10,
|
|
48
|
+
"cache_write_tokens": 5,
|
|
49
|
+
"thinking_tokens": 20
|
|
50
|
+
},
|
|
51
|
+
"stop_reason": "tool_use",
|
|
52
|
+
"model": "claude-sonnet-4-6",
|
|
53
|
+
"routing": { "tier": "primary", "provider": "anthropic", "instance": "us-east-1" },
|
|
54
|
+
"metadata": { "trace_id": "trace_abc123", "span_id": "span_def456", "latency_ms": 1250 }
|
|
55
|
+
},
|
|
56
|
+
"field_mapping": {
|
|
57
|
+
"request": {
|
|
58
|
+
"id": "string — unique request identifier",
|
|
59
|
+
"messages": "array[Message] — conversation history",
|
|
60
|
+
"system": "string | nil — system prompt",
|
|
61
|
+
"tools": "hash[name -> ToolDefinition] — available tools",
|
|
62
|
+
"params": "Params — sampling parameters",
|
|
63
|
+
"thinking": "Thinking::Config | nil — thinking configuration",
|
|
64
|
+
"stream": "boolean — streaming mode",
|
|
65
|
+
"conversation_id": "string | nil — conversation grouping",
|
|
66
|
+
"caller": "hash — client translator identity",
|
|
67
|
+
"routing": "hash — routing hints",
|
|
68
|
+
"metadata": "hash — passthrough metadata"
|
|
69
|
+
},
|
|
70
|
+
"response": {
|
|
71
|
+
"text": "string — assistant text content",
|
|
72
|
+
"thinking": "Thinking | nil — thinking block with content and signature",
|
|
73
|
+
"tool_calls": "array[ToolCall] — tool call requests",
|
|
74
|
+
"usage": "Usage — token usage breakdown",
|
|
75
|
+
"stop_reason": "symbol — why generation stopped",
|
|
76
|
+
"model": "string — resolved model identifier",
|
|
77
|
+
"routing": "hash — actual routing used",
|
|
78
|
+
"metadata": "hash — provider passthrough quirks"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Metering/audit event schemas — G15e",
|
|
3
|
+
"schemas": {
|
|
4
|
+
"metering_event": {
|
|
5
|
+
"event_type": "metering",
|
|
6
|
+
"required_fields": {
|
|
7
|
+
"exchange_id": "string",
|
|
8
|
+
"request_id": "string",
|
|
9
|
+
"conversation_id": "string | nil",
|
|
10
|
+
"model": "string",
|
|
11
|
+
"provider": "string",
|
|
12
|
+
"usage": {
|
|
13
|
+
"input_tokens": "integer",
|
|
14
|
+
"output_tokens": "integer",
|
|
15
|
+
"cache_read_tokens": "integer | nil",
|
|
16
|
+
"cache_write_tokens": "integer | nil",
|
|
17
|
+
"thinking_tokens": "integer | nil"
|
|
18
|
+
},
|
|
19
|
+
"cost": {
|
|
20
|
+
"input_cost_usd": "float | nil",
|
|
21
|
+
"output_cost_usd": "float | nil",
|
|
22
|
+
"total_cost_usd": "float | nil"
|
|
23
|
+
},
|
|
24
|
+
"latency_ms": "integer",
|
|
25
|
+
"timestamp": "ISO8601 string"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"audit_event": {
|
|
29
|
+
"event_type": "audit",
|
|
30
|
+
"required_fields": {
|
|
31
|
+
"exchange_id": "string",
|
|
32
|
+
"request_id": "string",
|
|
33
|
+
"conversation_id": "string | nil",
|
|
34
|
+
"model": "string",
|
|
35
|
+
"provider": "string",
|
|
36
|
+
"caller": "hash",
|
|
37
|
+
"status": "symbol — :success, :error, :partial",
|
|
38
|
+
"stop_reason": "symbol | nil",
|
|
39
|
+
"timestamp": "ISO8601 string"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"tool_call_audit_event": {
|
|
43
|
+
"event_type": "tool_call_audit",
|
|
44
|
+
"required_fields": {
|
|
45
|
+
"exchange_id": "string",
|
|
46
|
+
"tool_call_id": "string",
|
|
47
|
+
"name": "string",
|
|
48
|
+
"source": "symbol — :client, :registry, :special, :extension, :mcp",
|
|
49
|
+
"status": "symbol — :pending, :running, :success, :error",
|
|
50
|
+
"arguments": "hash",
|
|
51
|
+
"timestamp": "ISO8601 string"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"example_events": {
|
|
56
|
+
"metering_success": {
|
|
57
|
+
"event_type": "metering",
|
|
58
|
+
"exchange_id": "exch_meter_001",
|
|
59
|
+
"request_id": "req_meter_001",
|
|
60
|
+
"conversation_id": "conv_001",
|
|
61
|
+
"model": "claude-sonnet-4-6",
|
|
62
|
+
"provider": "anthropic",
|
|
63
|
+
"usage": { "input_tokens": 150, "output_tokens": 85, "cache_read_tokens": 20 },
|
|
64
|
+
"cost": { "input_cost_usd": 0.0015, "output_cost_usd": 0.00765, "total_cost_usd": 0.00915 },
|
|
65
|
+
"latency_ms": 1250,
|
|
66
|
+
"stop_reason": "end_turn",
|
|
67
|
+
"routing": { "tier": "primary", "provider": "anthropic" },
|
|
68
|
+
"tool_calls_count": 0,
|
|
69
|
+
"timestamp": "2026-06-10T12:00:00Z"
|
|
70
|
+
},
|
|
71
|
+
"audit_success": {
|
|
72
|
+
"event_type": "audit",
|
|
73
|
+
"exchange_id": "exch_audit_001",
|
|
74
|
+
"request_id": "req_audit_001",
|
|
75
|
+
"conversation_id": "conv_001",
|
|
76
|
+
"model": "claude-sonnet-4-6",
|
|
77
|
+
"provider": "anthropic",
|
|
78
|
+
"caller": { "type": "anthropic_messages", "client_id": "test_client" },
|
|
79
|
+
"status": "success",
|
|
80
|
+
"stop_reason": "end_turn",
|
|
81
|
+
"routing": { "tier": "primary", "provider": "anthropic" },
|
|
82
|
+
"usage": { "input_tokens": 150, "output_tokens": 85 },
|
|
83
|
+
"route_attempts": [
|
|
84
|
+
{ "attempt": 1, "provider": "anthropic", "model": "claude-sonnet-4-6", "status": "success" }
|
|
85
|
+
],
|
|
86
|
+
"timestamp": "2026-06-10T12:00:00Z"
|
|
87
|
+
},
|
|
88
|
+
"tool_call_audit": {
|
|
89
|
+
"event_type": "tool_call_audit",
|
|
90
|
+
"exchange_id": "exch_tc_audit_001",
|
|
91
|
+
"tool_call_id": "call_tc_audit_001",
|
|
92
|
+
"name": "get_weather",
|
|
93
|
+
"source": "client",
|
|
94
|
+
"status": "success",
|
|
95
|
+
"arguments": { "location": "San Francisco, CA", "unit": "fahrenheit" },
|
|
96
|
+
"result": { "temperature": 68, "unit": "fahrenheit", "conditions": "partly cloudy" },
|
|
97
|
+
"duration_ms": 250,
|
|
98
|
+
"timestamp": "2026-06-10T12:00:01Z"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "req_params_mapping_001",
|
|
3
|
+
"messages": [
|
|
4
|
+
{
|
|
5
|
+
"role": "user",
|
|
6
|
+
"content": [{ "type": "text", "text": "Generate a creative story." }]
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"params": {
|
|
10
|
+
"max_tokens": 2048,
|
|
11
|
+
"temperature": 0.7,
|
|
12
|
+
"top_p": 0.9,
|
|
13
|
+
"top_k": 50,
|
|
14
|
+
"stop_sequences": ["[END]", "\\n\\n"],
|
|
15
|
+
"seed": 42,
|
|
16
|
+
"frequency_penalty": 0.1,
|
|
17
|
+
"presence_penalty": 0.2,
|
|
18
|
+
"response_format": { "type": "json_object" }
|
|
19
|
+
},
|
|
20
|
+
"stream": false
|
|
21
|
+
}
|