lex-llm 0.5.0 → 0.5.3

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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/lib/legion/extensions/llm/canonical/message.rb +16 -3
  4. data/lib/legion/extensions/llm/canonical/tool_definition.rb +26 -1
  5. data/lib/legion/extensions/llm/canonical/tool_schema.rb +46 -0
  6. data/lib/legion/extensions/llm/canonical/usage.rb +13 -0
  7. data/lib/legion/extensions/llm/canonical.rb +1 -0
  8. data/lib/legion/extensions/llm/capability_policy.rb +107 -0
  9. data/lib/legion/extensions/llm/configuration.rb +4 -0
  10. data/lib/legion/extensions/llm/error.rb +2 -0
  11. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +9 -8
  12. data/lib/legion/extensions/llm/provider.rb +33 -5
  13. data/lib/legion/extensions/llm/provider_contract.rb +10 -1
  14. data/lib/legion/extensions/llm/routing/model_offering.rb +14 -2
  15. data/lib/legion/extensions/llm/stream_accumulator.rb +39 -0
  16. data/lib/legion/extensions/llm/streaming.rb +36 -3
  17. data/lib/legion/extensions/llm/version.rb +1 -1
  18. data/lib/legion/extensions/llm.rb +3 -0
  19. data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +49 -2
  20. data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +83 -0
  21. data/spec/legion/extensions/llm/canonical/usage_spec.rb +40 -0
  22. data/spec/legion/extensions/llm/capability_policy_spec.rb +192 -0
  23. data/spec/legion/extensions/llm/configuration_spec.rb +40 -0
  24. data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +163 -0
  25. data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_continuation_request.json +43 -0
  26. data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_use_response.json +29 -0
  27. data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_server_tool_chunks.json +52 -0
  28. data/spec/legion/extensions/llm/conformance/provider_tool_rendering_examples.rb +77 -0
  29. data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +68 -0
  30. data/spec/legion/extensions/llm/provider_spec.rb +55 -3
  31. data/spec/legion/extensions/llm/routing/model_offering_spec.rb +58 -0
  32. data/spec/legion/extensions/llm/stream_accumulator_spec.rb +52 -0
  33. data/spec/legion/extensions/llm/streaming_spec.rb +9 -0
  34. metadata +10 -1
@@ -0,0 +1,52 @@
1
+ {
2
+ "description": "G24 — streaming sequence for a server-executed LegionIO tool. The server-side exchange streams as tool_call_delta(s) (with the result attached on close) followed by trailing text from round-2. Client translators must turn this into completed (non-actionable) blocks: Claude server_tool_use+server_tool_result blocks, Codex completed function_call items with results visible.",
3
+ "request_id": "req_stream_legion_001",
4
+ "chunks": [
5
+ {
6
+ "request_id": "req_stream_legion_001",
7
+ "index": 0,
8
+ "type": "tool_call_delta",
9
+ "tool_call": {
10
+ "id": "call_legion_stream_001",
11
+ "name": "legion_list_all_tools",
12
+ "arguments": {"filter": "all"},
13
+ "source": "registry",
14
+ "status": "running"
15
+ }
16
+ },
17
+ {
18
+ "request_id": "req_stream_legion_001",
19
+ "index": 0,
20
+ "type": "tool_call_delta",
21
+ "tool_call": {
22
+ "id": "call_legion_stream_001",
23
+ "name": "legion_list_all_tools",
24
+ "arguments": {"filter": "all"},
25
+ "source": "registry",
26
+ "status": "success",
27
+ "result": "tools: legion_list_all_tools, legion_apollo_search"
28
+ }
29
+ },
30
+ {
31
+ "request_id": "req_stream_legion_001",
32
+ "index": 1,
33
+ "type": "text_delta",
34
+ "delta": "I called the "
35
+ },
36
+ {
37
+ "request_id": "req_stream_legion_001",
38
+ "index": 1,
39
+ "type": "text_delta",
40
+ "delta": "legion_list_all_tools tool."
41
+ },
42
+ {
43
+ "request_id": "req_stream_legion_001",
44
+ "type": "done",
45
+ "stop_reason": "end_turn",
46
+ "usage": {
47
+ "input_tokens": 50,
48
+ "output_tokens": 25
49
+ }
50
+ }
51
+ ]
52
+ }
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared examples for provider tool rendering conformance.
4
+ # Include in any provider gem that renders tools to prove it handles
5
+ # canonical ToolDefinition objects, Hashes, and schemas without double-wrap.
6
+ #
7
+ # Usage in provider specs:
8
+ # it_behaves_like 'canonical tool rendering', described_class.new(...)
9
+ #
10
+ RSpec.shared_examples 'canonical tool rendering' do # rubocop:disable RSpec/MultipleMemoizedHelpers
11
+ let(:full_schema) { { type: 'object', properties: { city: { type: 'string' } }, required: ['city'] } }
12
+
13
+ let(:canonical_tool) do
14
+ Legion::Extensions::Llm::Canonical::ToolDefinition.build(
15
+ name: 'get_weather',
16
+ description: 'Weather lookup',
17
+ parameters: full_schema
18
+ )
19
+ end
20
+
21
+ let(:hash_tool_parameters) do
22
+ { name: 'get_weather', description: 'Weather lookup', parameters: full_schema }
23
+ end
24
+
25
+ let(:hash_tool_input_schema) do
26
+ { name: 'get_weather', description: 'Weather lookup', input_schema: full_schema }
27
+ end
28
+
29
+ let(:hash_tool_params_schema) do
30
+ { name: 'get_weather', description: 'Weather lookup', params_schema: full_schema }
31
+ end
32
+
33
+ let(:tools_map) { { 'get_weather' => canonical_tool } }
34
+
35
+ describe 'canonical tool rendering' do # rubocop:disable RSpec/MultipleMemoizedHelpers
36
+ it 'accepts Canonical::ToolDefinition without raising' do
37
+ expect { render_tools(tools_map) }.not_to raise_error
38
+ end
39
+
40
+ it 'renders schema with top-level type: object' do
41
+ rendered = render_tools(tools_map)
42
+ schema = extract_rendered_schema(rendered)
43
+ expect(schema[:type] || schema['type']).to eq('object')
44
+ end
45
+
46
+ it 'renders properties without double-wrap' do
47
+ rendered = render_tools(tools_map)
48
+ schema = extract_rendered_schema(rendered)
49
+ props = schema[:properties] || schema['properties']
50
+ expect(props).not_to have_key(:type)
51
+ expect(props).not_to have_key('type')
52
+ expect(props).to have_key(:city).or have_key('city')
53
+ end
54
+
55
+ it 'renders Hash tools with :parameters identically to canonical' do
56
+ map = { 'get_weather' => hash_tool_parameters }
57
+ rendered = render_tools(map)
58
+ schema = extract_rendered_schema(rendered)
59
+ expect(schema[:type] || schema['type']).to eq('object')
60
+ expect(schema[:properties] || schema['properties']).to have_key(:city).or have_key('city')
61
+ end
62
+
63
+ it 'renders Hash tools with :input_schema identically' do
64
+ map = { 'get_weather' => hash_tool_input_schema }
65
+ rendered = render_tools(map)
66
+ schema = extract_rendered_schema(rendered)
67
+ expect(schema[:type] || schema['type']).to eq('object')
68
+ end
69
+
70
+ it 'renders Hash tools with :params_schema identically' do
71
+ map = { 'get_weather' => hash_tool_params_schema }
72
+ rendered = render_tools(map)
73
+ schema = extract_rendered_schema(rendered)
74
+ expect(schema[:type] || schema['type']).to eq('object')
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/llm/provider/open_ai_compatible'
5
+ require 'legion/extensions/llm/message'
6
+ require 'legion/extensions/llm/tool_call'
7
+
8
+ # Failing-test for the legionio-e2e claude/openai legionio_tool_injection
9
+ # regression. After the LexLLMAdapter#normalize_message_tool_calls fix
10
+ # (legion-llm 4317d56), the adapter now emits Array<ToolCall> on the
11
+ # assistant message — which is the historical canonical shape per
12
+ # canonical/message.rb:75 ("Array is canonical; Hash is legacy lex-llm
13
+ # format (name → ToolCall)"). The OpenAI-compatible HTTP payload renderer
14
+ # missed that update and still calls `.values` on tool_calls, so live
15
+ # requests now blow up with:
16
+ #
17
+ # undefined method 'values' for an instance of Array
18
+ #
19
+ # (captured at
20
+ # legionio-e2e/results/claude/openai_legionio_tool_injection_returns_response_containing_legionio_tool_references_response.json)
21
+ #
22
+ # The bedrock provider (format_invoke_model_assistant) and the
23
+ # lex-llm-openai canonical translator (translator.rb:290) already handle
24
+ # both shapes via `tool_calls.is_a?(Hash) ? .values : Array(...)`. This
25
+ # spec pins the same shape-tolerance for OpenAICompatible.
26
+ RSpec.describe Legion::Extensions::Llm::Provider::OpenAICompatible do
27
+ let(:host_class) do
28
+ Class.new do
29
+ include Legion::Extensions::Llm::Provider::OpenAICompatible
30
+
31
+ # Expose the otherwise-private renderer so we can test it directly
32
+ # without booting a full provider stack.
33
+ public :format_openai_tool_calls
34
+ end
35
+ end
36
+
37
+ let(:host) { host_class.new }
38
+
39
+ let(:tool_call) do
40
+ Legion::Extensions::Llm::ToolCall.new(
41
+ id: 'call_001',
42
+ name: 'legion_list_all_tools',
43
+ arguments: { 'filter' => 'all' }
44
+ )
45
+ end
46
+
47
+ it 'renders an Array<ToolCall> (the post-canonical shape)' do
48
+ rendered = host.format_openai_tool_calls([tool_call])
49
+ expect(rendered).to be_an(Array)
50
+ expect(rendered.size).to eq(1)
51
+ expect(rendered.first[:id]).to eq('call_001')
52
+ expect(rendered.first[:type]).to eq('function')
53
+ expect(rendered.first[:function][:name]).to eq('legion_list_all_tools')
54
+ end
55
+
56
+ it 'renders a Hash<id, ToolCall> (legacy lex-llm shape) without regression' do
57
+ rendered = host.format_openai_tool_calls({ call_id: tool_call })
58
+ expect(rendered).to be_an(Array)
59
+ expect(rendered.size).to eq(1)
60
+ expect(rendered.first[:function][:name]).to eq('legion_list_all_tools')
61
+ end
62
+
63
+ it 'returns nil when tool_calls is nil or empty' do
64
+ expect(host.format_openai_tool_calls(nil)).to be_nil
65
+ expect(host.format_openai_tool_calls([])).to be_nil
66
+ expect(host.format_openai_tool_calls({})).to be_nil
67
+ end
68
+ end
@@ -542,25 +542,46 @@ RSpec.describe Legion::Extensions::Llm::Provider do
542
542
  end
543
543
 
544
544
  describe '#cache_enabled?' do
545
- it 'returns true when llm_cache_enabled is true in config' do
545
+ it 'returns true when llm_cache_enabled is true in config (per-provider explicit)' do
546
546
  Legion::Extensions::Llm.config.llm_cache_enabled = true
547
547
  provider = provider_class.new(Legion::Extensions::Llm.config)
548
548
 
549
549
  expect(provider.cache_enabled?).to be true
550
550
  end
551
551
 
552
- it 'returns false when llm_cache_enabled is false in config' do
552
+ it 'returns false when llm_cache_enabled is false in config (per-provider explicit)' do
553
553
  Legion::Extensions::Llm.config.llm_cache_enabled = false
554
554
  provider = provider_class.new(Legion::Extensions::Llm.config)
555
555
 
556
556
  expect(provider.cache_enabled?).to be false
557
557
  end
558
558
 
559
- it 'returns false when llm_cache_enabled is not set on config' do
559
+ it 'falls through to global prompt_caching.enabled when config has no llm_cache_enabled' do
560
560
  config = { request_timeout: 30, max_retries: 0, retry_interval: 0, retry_backoff_factor: 0,
561
561
  retry_interval_randomness: 0 }
562
562
  provider = provider_class.new(config)
563
563
 
564
+ Legion::Settings.loader.settings[:llm] ||= {}
565
+ Legion::Settings.loader.settings[:llm][:prompt_caching] = { enabled: true }
566
+ expect(provider.cache_enabled?).to be true
567
+ end
568
+
569
+ it 'returns false when global prompt_caching.enabled is false and no per-provider setting' do
570
+ config = { request_timeout: 30, max_retries: 0, retry_interval: 0, retry_backoff_factor: 0,
571
+ retry_interval_randomness: 0 }
572
+ provider = provider_class.new(config)
573
+
574
+ Legion::Settings.loader.settings[:llm] ||= {}
575
+ Legion::Settings.loader.settings[:llm][:prompt_caching] = { enabled: false }
576
+ expect(provider.cache_enabled?).to be false
577
+ end
578
+
579
+ it 'per-provider setting overrides global when both present' do
580
+ Legion::Extensions::Llm.config.llm_cache_enabled = false
581
+ provider = provider_class.new(Legion::Extensions::Llm.config)
582
+
583
+ Legion::Settings.loader.settings[:llm] ||= {}
584
+ Legion::Settings.loader.settings[:llm][:prompt_caching] = { enabled: true }
564
585
  expect(provider.cache_enabled?).to be false
565
586
  end
566
587
  end
@@ -589,4 +610,35 @@ RSpec.describe Legion::Extensions::Llm::Provider do
589
610
  end
590
611
  end
591
612
  end
613
+
614
+ describe '#model_detail_cache_key' do
615
+ let(:provider_class) do
616
+ Class.new(described_class) do
617
+ def api_base = 'https://test.invalid'
618
+
619
+ def self.slug = 'testprov'
620
+ end
621
+ end
622
+
623
+ let(:provider) do
624
+ provider_class.new({ request_timeout: 30, max_retries: 0, retry_interval: 0,
625
+ retry_backoff_factor: 0, retry_interval_randomness: 0 })
626
+ end
627
+
628
+ it 'includes the model-detail cache schema version in the key' do
629
+ key = provider.send(:model_detail_cache_key, 'test-model')
630
+ expect(key).to include("schema#{described_class::MODEL_DETAIL_CACHE_SCHEMA_VERSION}")
631
+ expect(key).to include('schema2')
632
+ end
633
+
634
+ it 'changes the key when the schema version constant changes' do
635
+ key_v2 = provider.send(:model_detail_cache_key, 'test-model')
636
+
637
+ stub_const("#{described_class}::MODEL_DETAIL_CACHE_SCHEMA_VERSION", 3)
638
+
639
+ key_v3 = provider.send(:model_detail_cache_key, 'test-model')
640
+ expect(key_v3).not_to eq(key_v2)
641
+ expect(key_v3).to include('schema3')
642
+ end
643
+ end
592
644
  end
@@ -219,4 +219,62 @@ RSpec.describe Legion::Extensions::Llm::Routing::ModelOffering do
219
219
  limits: { context_window: 32_768, max_output_tokens: 8192 }
220
220
  )
221
221
  end
222
+
223
+ describe 'capability_sources' do
224
+ it 'accepts and exposes capability source metadata' do
225
+ sourced = described_class.new(
226
+ provider_family: :vllm,
227
+ provider_instance: :apollo,
228
+ model: 'gemma-4-12b-it',
229
+ capabilities: %i[streaming tools],
230
+ capability_sources: {
231
+ streaming: { value: true, source: :provider_envelope },
232
+ tools: { value: true, source: :instance_override },
233
+ embeddings: { value: false, source: :default_false }
234
+ }
235
+ )
236
+
237
+ expect(sourced.capabilities).to include(:streaming, :tools)
238
+ expect(sourced.capability_sources[:tools]).to eq(value: true, source: :instance_override)
239
+ expect(sourced.capability_sources[:embeddings]).to eq(value: false, source: :default_false)
240
+ end
241
+
242
+ it 'includes capability_sources in to_h' do
243
+ sourced = described_class.new(
244
+ provider_family: :vllm,
245
+ provider_instance: :apollo,
246
+ model: 'gemma-4-12b-it',
247
+ capabilities: %i[streaming],
248
+ capability_sources: {
249
+ streaming: { value: true, source: :provider_envelope }
250
+ }
251
+ )
252
+
253
+ expect(sourced.to_h[:capability_sources]).to eq(
254
+ streaming: { value: true, source: :provider_envelope }
255
+ )
256
+ end
257
+
258
+ it 'normalizes string-keyed capability sources' do
259
+ sourced = described_class.new(
260
+ provider_family: :vllm,
261
+ model: 'test',
262
+ capability_sources: {
263
+ 'streaming' => { 'value' => true, 'source' => 'provider_envelope' }
264
+ }
265
+ )
266
+
267
+ expect(sourced.capability_sources[:streaming]).to eq(value: true, source: :provider_envelope)
268
+ end
269
+
270
+ it 'defaults to empty hash when not provided' do
271
+ plain = described_class.new(
272
+ provider_family: :ollama,
273
+ model: 'test',
274
+ capabilities: %i[streaming]
275
+ )
276
+
277
+ expect(plain.capability_sources).to eq({})
278
+ end
279
+ end
222
280
  end
@@ -100,4 +100,56 @@ RSpec.describe Legion::Extensions::Llm::StreamAccumulator do
100
100
  expect(message.thinking).to be_nil
101
101
  end
102
102
  end
103
+
104
+ describe '#filtered_chunk' do
105
+ it 'returns a chunk for a plain text delta' do
106
+ accumulator = described_class.new
107
+ chunk = Legion::Extensions::Llm::Chunk.new(role: :assistant, content: 'Hello', model_id: 'm')
108
+
109
+ accumulator.add(chunk)
110
+ filtered = accumulator.filtered_chunk(chunk)
111
+
112
+ expect(filtered).not_to be_nil
113
+ expect(filtered.content.to_s).to eq('Hello')
114
+ end
115
+ end
116
+
117
+ describe '#flush_pending_chunk' do
118
+ it 'returns nil when nothing is buffered' do
119
+ accumulator = described_class.new
120
+ chunk = Legion::Extensions::Llm::Chunk.new(role: :assistant, content: 'Hello', model_id: 'm')
121
+ accumulator.add(chunk)
122
+
123
+ expect(accumulator.flush_pending_chunk).to be_nil
124
+ end
125
+
126
+ it 'releases text held by the untagged-preamble heuristic as a final delta' do
127
+ accumulator = described_class.new
128
+ deltas = []
129
+ ['I can help', ' with that.'].each do |text|
130
+ chunk = Legion::Extensions::Llm::Chunk.new(role: :assistant, content: text, model_id: 'm')
131
+ accumulator.add(chunk)
132
+ filtered = accumulator.filtered_chunk(chunk)
133
+ deltas << filtered.content.to_s if filtered
134
+ end
135
+
136
+ final = accumulator.flush_pending_chunk
137
+ deltas << final.content.to_s if final
138
+ message = accumulator.to_message(nil)
139
+
140
+ expect(deltas.join).to eq('I can help with that.')
141
+ expect(message.content).to eq('I can help with that.')
142
+ end
143
+
144
+ it 'does not double-append flushed text in to_message' do
145
+ accumulator = described_class.new
146
+ chunk = Legion::Extensions::Llm::Chunk.new(role: :assistant, content: 'The answer', model_id: 'm')
147
+ accumulator.add(chunk)
148
+
149
+ accumulator.flush_pending_chunk
150
+ message = accumulator.to_message(nil)
151
+
152
+ expect(message.content).to eq('The answer')
153
+ end
154
+ end
103
155
  end
@@ -98,6 +98,15 @@ RSpec.describe Legion::Extensions::Llm::Streaming do
98
98
  .to raise_error(Legion::Extensions::Llm::ServerError, /Provider error.*The model is currently overloaded/)
99
99
  end
100
100
 
101
+ it 'raises UnauthorizedError for partial 401 streaming responses' do
102
+ buffer = +''
103
+ auth_env = Struct.new(:status).new(401)
104
+ truncated_chunk = '{"error":{"message":"Unauthorized'
105
+
106
+ expect { test_obj.send(:handle_failed_response, truncated_chunk, buffer, auth_env) }
107
+ .to raise_error(Legion::Extensions::Llm::UnauthorizedError, /Unauthorized/)
108
+ end
109
+
101
110
  it 'raises ServerError with generic message when no partial message is extractable and env cannot buffer' do
102
111
  buffer = +''
103
112
  partial_chunk = '{"error":{'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - LegionIO
@@ -270,7 +270,9 @@ files:
270
270
  - lib/legion/extensions/llm/canonical/thinking.rb
271
271
  - lib/legion/extensions/llm/canonical/tool_call.rb
272
272
  - lib/legion/extensions/llm/canonical/tool_definition.rb
273
+ - lib/legion/extensions/llm/canonical/tool_schema.rb
273
274
  - lib/legion/extensions/llm/canonical/usage.rb
275
+ - lib/legion/extensions/llm/capability_policy.rb
274
276
  - lib/legion/extensions/llm/chat.rb
275
277
  - lib/legion/extensions/llm/chunk.rb
276
278
  - lib/legion/extensions/llm/configuration.rb
@@ -353,7 +355,9 @@ files:
353
355
  - spec/legion/extensions/llm/canonical/thinking_spec.rb
354
356
  - spec/legion/extensions/llm/canonical/tool_call_spec.rb
355
357
  - spec/legion/extensions/llm/canonical/tool_definition_spec.rb
358
+ - spec/legion/extensions/llm/canonical/tool_schema_spec.rb
356
359
  - spec/legion/extensions/llm/canonical/usage_spec.rb
360
+ - spec/legion/extensions/llm/capability_policy_spec.rb
357
361
  - spec/legion/extensions/llm/configuration_spec.rb
358
362
  - spec/legion/extensions/llm/conformance/client_translator_examples.rb
359
363
  - spec/legion/extensions/llm/conformance/conformance.rb
@@ -364,11 +368,14 @@ files:
364
368
  - spec/legion/extensions/llm/conformance/fixtures/canonical_fleet_round_trip.json
365
369
  - spec/legion/extensions/llm/conformance/fixtures/canonical_metering_audit_events.json
366
370
  - spec/legion/extensions/llm/conformance/fixtures/canonical_params_mapping_request.json
371
+ - spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_continuation_request.json
372
+ - spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_use_response.json
367
373
  - spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_request.json
368
374
  - spec/legion/extensions/llm/conformance/fixtures/canonical_simple_text_response.json
369
375
  - spec/legion/extensions/llm/conformance/fixtures/canonical_stop_reason_matrix.json
370
376
  - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_accumulated_response.json
371
377
  - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_error_chunks.json
378
+ - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_server_tool_chunks.json
372
379
  - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_text_chunks.json
373
380
  - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_thinking_chunks.json
374
381
  - spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_tool_call_chunks.json
@@ -378,6 +385,7 @@ files:
378
385
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tool_results_continuation_request.json
379
386
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tool_use_response.json
380
387
  - spec/legion/extensions/llm/conformance/fixtures/canonical_tools_request.json
388
+ - spec/legion/extensions/llm/conformance/provider_tool_rendering_examples.rb
381
389
  - spec/legion/extensions/llm/conformance/provider_translator_examples.rb
382
390
  - spec/legion/extensions/llm/connection_logging_spec.rb
383
391
  - spec/legion/extensions/llm/connection_retry_spec.rb
@@ -394,6 +402,7 @@ files:
394
402
  - spec/legion/extensions/llm/model/info_spec.rb
395
403
  - spec/legion/extensions/llm/models_spec.rb
396
404
  - spec/legion/extensions/llm/provider/open_ai_compatible_spec.rb
405
+ - spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb
397
406
  - spec/legion/extensions/llm/provider_contract_spec.rb
398
407
  - spec/legion/extensions/llm/provider_settings_spec.rb
399
408
  - spec/legion/extensions/llm/provider_spec.rb