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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/lib/legion/extensions/llm/canonical/message.rb +16 -3
- data/lib/legion/extensions/llm/canonical/tool_definition.rb +26 -1
- data/lib/legion/extensions/llm/canonical/tool_schema.rb +46 -0
- data/lib/legion/extensions/llm/canonical/usage.rb +13 -0
- data/lib/legion/extensions/llm/canonical.rb +1 -0
- data/lib/legion/extensions/llm/capability_policy.rb +107 -0
- data/lib/legion/extensions/llm/configuration.rb +4 -0
- data/lib/legion/extensions/llm/error.rb +2 -0
- data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +9 -8
- data/lib/legion/extensions/llm/provider.rb +33 -5
- data/lib/legion/extensions/llm/provider_contract.rb +10 -1
- data/lib/legion/extensions/llm/routing/model_offering.rb +14 -2
- data/lib/legion/extensions/llm/stream_accumulator.rb +39 -0
- data/lib/legion/extensions/llm/streaming.rb +36 -3
- data/lib/legion/extensions/llm/version.rb +1 -1
- data/lib/legion/extensions/llm.rb +3 -0
- data/spec/legion/extensions/llm/canonical/tool_definition_spec.rb +49 -2
- data/spec/legion/extensions/llm/canonical/tool_schema_spec.rb +83 -0
- data/spec/legion/extensions/llm/canonical/usage_spec.rb +40 -0
- data/spec/legion/extensions/llm/capability_policy_spec.rb +192 -0
- data/spec/legion/extensions/llm/configuration_spec.rb +40 -0
- data/spec/legion/extensions/llm/conformance/client_translator_examples.rb +163 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_continuation_request.json +43 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_server_tool_use_response.json +29 -0
- data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_server_tool_chunks.json +52 -0
- data/spec/legion/extensions/llm/conformance/provider_tool_rendering_examples.rb +77 -0
- data/spec/legion/extensions/llm/provider/open_ai_compatible_tool_calls_array_spec.rb +68 -0
- data/spec/legion/extensions/llm/provider_spec.rb +55 -3
- data/spec/legion/extensions/llm/routing/model_offering_spec.rb +58 -0
- data/spec/legion/extensions/llm/stream_accumulator_spec.rb +52 -0
- data/spec/legion/extensions/llm/streaming_spec.rb +9 -0
- metadata +10 -1
data/spec/legion/extensions/llm/conformance/fixtures/canonical_streaming_server_tool_chunks.json
ADDED
|
@@ -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 '
|
|
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.
|
|
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
|