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.
Files changed (116) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -2
  3. data/B1b-conformance-kit.md +79 -0
  4. data/CHANGELOG.md +19 -0
  5. data/lex-llm.gemspec +2 -3
  6. data/lib/legion/extensions/llm/attachment.rb +1 -1
  7. data/lib/legion/extensions/llm/canonical/chunk.rb +184 -0
  8. data/lib/legion/extensions/llm/canonical/content_block.rb +126 -0
  9. data/lib/legion/extensions/llm/canonical/message.rb +125 -0
  10. data/lib/legion/extensions/llm/canonical/params.rb +61 -0
  11. data/lib/legion/extensions/llm/canonical/request.rb +117 -0
  12. data/lib/legion/extensions/llm/canonical/response.rb +124 -0
  13. data/lib/legion/extensions/llm/canonical/thinking.rb +81 -0
  14. data/lib/legion/extensions/llm/canonical/tool_call.rb +134 -0
  15. data/lib/legion/extensions/llm/canonical/tool_definition.rb +73 -0
  16. data/lib/legion/extensions/llm/canonical/usage.rb +61 -0
  17. data/lib/legion/extensions/llm/canonical.rb +49 -0
  18. data/lib/legion/extensions/llm/chat.rb +3 -5
  19. data/lib/legion/extensions/llm/connection.rb +5 -1
  20. data/lib/legion/extensions/llm/error.rb +3 -7
  21. data/lib/legion/extensions/llm/fleet/envelope_validation.rb +1 -3
  22. data/lib/legion/extensions/llm/fleet/provider_responder.rb +1 -3
  23. data/lib/legion/extensions/llm/fleet/token_validator.rb +1 -3
  24. data/lib/legion/extensions/llm/model/info.rb +4 -6
  25. data/lib/legion/extensions/llm/models.rb +3 -3
  26. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +7 -3
  27. data/lib/legion/extensions/llm/routing/lane_key.rb +1 -3
  28. data/lib/legion/extensions/llm/stream_accumulator.rb +1 -1
  29. data/lib/legion/extensions/llm/streaming.rb +1 -3
  30. data/lib/legion/extensions/llm/tool.rb +1 -3
  31. data/lib/legion/extensions/llm/version.rb +1 -1
  32. data/lib/legion/extensions/llm.rb +118 -35
  33. data/spec/fixtures/ruby.mp3 +0 -0
  34. data/spec/fixtures/ruby.mp4 +0 -0
  35. data/spec/fixtures/ruby.png +0 -0
  36. data/spec/fixtures/ruby.txt +1 -0
  37. data/spec/fixtures/ruby.wav +0 -0
  38. data/spec/fixtures/ruby.xml +1 -0
  39. data/spec/fixtures/sample.pdf +0 -0
  40. data/spec/legion/extensions/llm/agent_spec.rb +179 -0
  41. data/spec/legion/extensions/llm/attachment_spec.rb +25 -0
  42. data/spec/legion/extensions/llm/auto_registration_spec.rb +38 -0
  43. data/spec/legion/extensions/llm/canonical/chunk_spec.rb +285 -0
  44. data/spec/legion/extensions/llm/canonical/content_block_spec.rb +179 -0
  45. data/spec/legion/extensions/llm/canonical/message_spec.rb +203 -0
  46. data/spec/legion/extensions/llm/canonical/params_spec.rb +159 -0
  47. data/spec/legion/extensions/llm/canonical/request_spec.rb +174 -0
  48. data/spec/legion/extensions/llm/canonical/response_spec.rb +234 -0
  49. data/spec/legion/extensions/llm/canonical/thinking_spec.rb +151 -0
  50. data/spec/legion/extensions/llm/canonical/tool_call_spec.rb +191 -0
  51. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +174 -0
  52. data/spec/legion/extensions/llm/canonical/usage_spec.rb +138 -0
  53. data/spec/legion/extensions/llm/configuration_spec.rb +38 -0
  54. data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +269 -0
  55. data/spec/legion/extensions/llm/conformance/conformance.rb +51 -0
  56. data/spec/legion/extensions/llm/conformance/echo_translator.rb +56 -0
  57. data/spec/legion/extensions/llm/conformance/echo_translator_spec.rb +13 -0
  58. data/spec/legion/extensions/llm/conformance/fixtures/canonical_empty_response.json +13 -0
  59. data/spec/legion/extensions/llm/conformance/fixtures/canonical_error_response.json +19 -0
  60. data/spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json +81 -0
  61. data/spec/legion/extensions/llm/conformance/fixtures/canonical_metering_audit_events.json +101 -0
  62. data/spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json +21 -0
  63. data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json +13 -0
  64. data/spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_response.json +13 -0
  65. data/spec/legion/extensions/llm/conformance/fixtures/canonical_stop_reason_matrix.json +36 -0
  66. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_accumulated_response.json +20 -0
  67. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_error_chunks.json +26 -0
  68. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_text_chunks.json +33 -0
  69. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_thinking_chunks.json +42 -0
  70. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_tool_call_chunks.json +41 -0
  71. data/spec/legion/extensions/llm/conformance/fixtures/canonical_system_prompt_request.json +14 -0
  72. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_request.json +18 -0
  73. data/spec/legion/extensions/llm/conformance/fixtures/canonical_thinking_response.json +17 -0
  74. data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json +75 -0
  75. data/spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json +25 -0
  76. data/spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json +34 -0
  77. data/spec/legion/extensions/llm/conformance/provider_translator_examples.rb +390 -0
  78. data/spec/legion/extensions/llm/connection_logging_spec.rb +53 -0
  79. data/spec/legion/extensions/llm/connection_retry_spec.rb +36 -0
  80. data/spec/legion/extensions/llm/context_spec.rb +127 -0
  81. data/spec/legion/extensions/llm/credential_sources_spec.rb +468 -0
  82. data/spec/legion/extensions/llm/error_middleware_spec.rb +102 -0
  83. data/spec/legion/extensions/llm/error_spec.rb +87 -0
  84. data/spec/legion/extensions/llm/fleet/provider_responder_spec.rb +120 -0
  85. data/spec/legion/extensions/llm/fleet/token_validator_spec.rb +163 -0
  86. data/spec/legion/extensions/llm/fleet/worker_execution_spec.rb +128 -0
  87. data/spec/legion/extensions/llm/fleet_messages_spec.rb +402 -0
  88. data/spec/legion/extensions/llm/gemspec_spec.rb +25 -0
  89. data/spec/legion/extensions/llm/message_spec.rb +64 -0
  90. data/spec/legion/extensions/llm/model/info_spec.rb +222 -0
  91. data/spec/legion/extensions/llm/models_spec.rb +104 -0
  92. data/spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb +203 -0
  93. data/spec/legion/extensions/llm/provider_contract_spec.rb +60 -0
  94. data/spec/legion/extensions/llm/provider_settings_spec.rb +76 -0
  95. data/spec/legion/extensions/llm/provider_spec.rb +592 -0
  96. data/spec/legion/extensions/llm/registry_event_builder_spec.rb +68 -0
  97. data/spec/legion/extensions/llm/registry_publisher_spec.rb +22 -0
  98. data/spec/legion/extensions/llm/responses/response_objects_spec.rb +75 -0
  99. data/spec/legion/extensions/llm/responses/thinking_extractor_spec.rb +75 -0
  100. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +222 -0
  101. data/spec/legion/extensions/llm/routing/offering_registry_spec.rb +50 -0
  102. data/spec/legion/extensions/llm/routing/registry_event_spec.rb +120 -0
  103. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +103 -0
  104. data/spec/legion/extensions/llm/streaming_spec.rb +108 -0
  105. data/spec/legion/extensions/llm/tool_spec.rb +94 -0
  106. data/spec/legion/extensions/llm/transport/fleet_lane_spec.rb +60 -0
  107. data/spec/legion/extensions/llm/utils_spec.rb +113 -0
  108. data/spec/legion/extensions/llm_base_contract_spec.rb +110 -0
  109. data/spec/legion/extensions/llm_extension_spec.rb +78 -0
  110. data/spec/legion/extensions/llm_root_spec.rb +51 -0
  111. data/spec/spec_helper.rb +24 -0
  112. data/spec/support/fake_llm_provider.rb +148 -0
  113. data/spec/support/llm_configuration.rb +21 -0
  114. data/spec/support/rspec_configuration.rb +19 -0
  115. data/spec/support/simplecov_configuration.rb +20 -0
  116. 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,13 @@
1
+ {
2
+ "text": "",
3
+ "thinking": null,
4
+ "tool_calls": [],
5
+ "usage": {
6
+ "input_tokens": 8,
7
+ "output_tokens": 0
8
+ },
9
+ "stop_reason": "end_turn",
10
+ "model": "test-model-1",
11
+ "routing": {},
12
+ "metadata": {}
13
+ }
@@ -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
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "id": "req_simple_text_001",
3
+ "messages": [
4
+ {
5
+ "role": "user",
6
+ "content": [{ "type": "text", "text": "Hello, how are you?" }]
7
+ }
8
+ ],
9
+ "params": {
10
+ "max_tokens": 1024
11
+ },
12
+ "stream": false
13
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "text": "I'm doing well, thank you for asking!",
3
+ "thinking": null,
4
+ "tool_calls": [],
5
+ "usage": {
6
+ "input_tokens": 12,
7
+ "output_tokens": 10
8
+ },
9
+ "stop_reason": "end_turn",
10
+ "model": "test-model-1",
11
+ "routing": {},
12
+ "metadata": {}
13
+ }