lex-llm 0.1.2 → 0.1.4

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 (165) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/CHANGELOG.md +12 -1
  4. data/Gemfile +1 -19
  5. data/README.md +25 -26
  6. data/lex-llm.gemspec +2 -2
  7. data/lib/legion/extensions/llm/agent.rb +366 -0
  8. data/lib/legion/extensions/llm/aliases.rb +42 -0
  9. data/lib/legion/extensions/llm/attachment.rb +229 -0
  10. data/lib/legion/extensions/llm/chat.rb +355 -0
  11. data/lib/legion/extensions/llm/chunk.rb +10 -0
  12. data/lib/legion/extensions/llm/configuration.rb +82 -0
  13. data/lib/legion/extensions/llm/connection.rb +134 -0
  14. data/lib/legion/extensions/llm/content.rb +81 -0
  15. data/lib/legion/extensions/llm/context.rb +33 -0
  16. data/lib/legion/extensions/llm/embedding.rb +33 -0
  17. data/lib/legion/extensions/llm/error.rb +116 -0
  18. data/lib/legion/extensions/llm/image.rb +109 -0
  19. data/lib/legion/extensions/llm/message.rb +111 -0
  20. data/lib/legion/extensions/llm/mime_type.rb +75 -0
  21. data/lib/legion/extensions/llm/model/info.rb +117 -0
  22. data/lib/legion/extensions/llm/model/modalities.rb +26 -0
  23. data/lib/legion/extensions/llm/model/pricing.rb +52 -0
  24. data/lib/legion/extensions/llm/model/pricing_category.rb +50 -0
  25. data/lib/legion/extensions/llm/model/pricing_tier.rb +37 -0
  26. data/lib/legion/extensions/llm/model.rb +11 -0
  27. data/lib/legion/extensions/llm/models.rb +514 -0
  28. data/lib/{lex_llm → legion/extensions/llm}/models_schema.json +1 -1
  29. data/lib/legion/extensions/llm/moderation.rb +60 -0
  30. data/lib/legion/extensions/llm/provider/open_ai_compatible.rb +276 -0
  31. data/lib/legion/extensions/llm/provider.rb +337 -0
  32. data/lib/legion/extensions/llm/routing/lane_key.rb +57 -0
  33. data/lib/legion/extensions/llm/routing/model_offering.rb +173 -0
  34. data/lib/legion/extensions/llm/routing.rb +11 -0
  35. data/lib/legion/extensions/llm/stream_accumulator.rb +209 -0
  36. data/lib/legion/extensions/llm/streaming.rb +181 -0
  37. data/lib/legion/extensions/llm/thinking.rb +53 -0
  38. data/lib/legion/extensions/llm/tokens.rb +51 -0
  39. data/lib/legion/extensions/llm/tool.rb +258 -0
  40. data/lib/legion/extensions/llm/tool_call.rb +29 -0
  41. data/lib/legion/extensions/llm/transcription.rb +39 -0
  42. data/lib/legion/extensions/llm/utils.rb +95 -0
  43. data/lib/legion/extensions/llm/version.rb +9 -0
  44. data/lib/legion/extensions/llm.rb +85 -6
  45. metadata +40 -122
  46. data/lib/generators/lex_llm/agent/agent_generator.rb +0 -36
  47. data/lib/generators/lex_llm/agent/templates/agent.rb.tt +0 -6
  48. data/lib/generators/lex_llm/agent/templates/instructions.txt.erb.tt +0 -0
  49. data/lib/generators/lex_llm/chat_ui/chat_ui_generator.rb +0 -256
  50. data/lib/generators/lex_llm/chat_ui/templates/controllers/chats_controller.rb.tt +0 -38
  51. data/lib/generators/lex_llm/chat_ui/templates/controllers/messages_controller.rb.tt +0 -21
  52. data/lib/generators/lex_llm/chat_ui/templates/controllers/models_controller.rb.tt +0 -14
  53. data/lib/generators/lex_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
  54. data/lib/generators/lex_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +0 -12
  55. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +0 -16
  56. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +0 -31
  57. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +0 -31
  58. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +0 -9
  59. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +0 -27
  60. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +0 -14
  61. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +0 -1
  62. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +0 -13
  63. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +0 -23
  64. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +0 -10
  65. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +0 -2
  66. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +0 -4
  67. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +0 -14
  68. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +0 -13
  69. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +0 -21
  70. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +0 -17
  71. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +0 -40
  72. data/lib/generators/lex_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +0 -27
  73. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +0 -16
  74. data/lib/generators/lex_llm/chat_ui/templates/views/chats/_form.html.erb.tt +0 -29
  75. data/lib/generators/lex_llm/chat_ui/templates/views/chats/index.html.erb.tt +0 -28
  76. data/lib/generators/lex_llm/chat_ui/templates/views/chats/new.html.erb.tt +0 -11
  77. data/lib/generators/lex_llm/chat_ui/templates/views/chats/show.html.erb.tt +0 -25
  78. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +0 -9
  79. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_content.html.erb.tt +0 -1
  80. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_error.html.erb.tt +0 -8
  81. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_form.html.erb.tt +0 -21
  82. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_system.html.erb.tt +0 -6
  83. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +0 -2
  84. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +0 -4
  85. data/lib/generators/lex_llm/chat_ui/templates/views/messages/_user.html.erb.tt +0 -9
  86. data/lib/generators/lex_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +0 -7
  87. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +0 -8
  88. data/lib/generators/lex_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +0 -16
  89. data/lib/generators/lex_llm/chat_ui/templates/views/models/_model.html.erb.tt +0 -15
  90. data/lib/generators/lex_llm/chat_ui/templates/views/models/index.html.erb.tt +0 -38
  91. data/lib/generators/lex_llm/chat_ui/templates/views/models/show.html.erb.tt +0 -17
  92. data/lib/generators/lex_llm/generator_helpers.rb +0 -214
  93. data/lib/generators/lex_llm/install/install_generator.rb +0 -109
  94. data/lib/generators/lex_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
  95. data/lib/generators/lex_llm/install/templates/chat_model.rb.tt +0 -3
  96. data/lib/generators/lex_llm/install/templates/create_chats_migration.rb.tt +0 -7
  97. data/lib/generators/lex_llm/install/templates/create_messages_migration.rb.tt +0 -19
  98. data/lib/generators/lex_llm/install/templates/create_models_migration.rb.tt +0 -39
  99. data/lib/generators/lex_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
  100. data/lib/generators/lex_llm/install/templates/initializer.rb.tt +0 -20
  101. data/lib/generators/lex_llm/install/templates/message_model.rb.tt +0 -4
  102. data/lib/generators/lex_llm/install/templates/model_model.rb.tt +0 -3
  103. data/lib/generators/lex_llm/install/templates/tool_call_model.rb.tt +0 -3
  104. data/lib/generators/lex_llm/schema/schema_generator.rb +0 -26
  105. data/lib/generators/lex_llm/schema/templates/schema.rb.tt +0 -2
  106. data/lib/generators/lex_llm/tool/templates/tool.rb.tt +0 -9
  107. data/lib/generators/lex_llm/tool/templates/tool_call.html.erb.tt +0 -13
  108. data/lib/generators/lex_llm/tool/templates/tool_result.html.erb.tt +0 -13
  109. data/lib/generators/lex_llm/tool/tool_generator.rb +0 -96
  110. data/lib/generators/lex_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
  111. data/lib/generators/lex_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
  112. data/lib/generators/lex_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
  113. data/lib/generators/lex_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
  114. data/lib/generators/lex_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
  115. data/lib/generators/lex_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
  116. data/lib/generators/lex_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
  117. data/lib/generators/lex_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
  118. data/lib/lex_llm/active_record/acts_as.rb +0 -180
  119. data/lib/lex_llm/active_record/acts_as_legacy.rb +0 -503
  120. data/lib/lex_llm/active_record/chat_methods.rb +0 -468
  121. data/lib/lex_llm/active_record/message_methods.rb +0 -131
  122. data/lib/lex_llm/active_record/model_methods.rb +0 -76
  123. data/lib/lex_llm/active_record/payload_helpers.rb +0 -26
  124. data/lib/lex_llm/active_record/tool_call_methods.rb +0 -15
  125. data/lib/lex_llm/agent.rb +0 -365
  126. data/lib/lex_llm/aliases.rb +0 -38
  127. data/lib/lex_llm/attachment.rb +0 -223
  128. data/lib/lex_llm/chat.rb +0 -351
  129. data/lib/lex_llm/chunk.rb +0 -6
  130. data/lib/lex_llm/configuration.rb +0 -81
  131. data/lib/lex_llm/connection.rb +0 -130
  132. data/lib/lex_llm/content.rb +0 -77
  133. data/lib/lex_llm/context.rb +0 -29
  134. data/lib/lex_llm/embedding.rb +0 -29
  135. data/lib/lex_llm/error.rb +0 -112
  136. data/lib/lex_llm/image.rb +0 -105
  137. data/lib/lex_llm/message.rb +0 -107
  138. data/lib/lex_llm/mime_type.rb +0 -71
  139. data/lib/lex_llm/model/info.rb +0 -113
  140. data/lib/lex_llm/model/modalities.rb +0 -22
  141. data/lib/lex_llm/model/pricing.rb +0 -48
  142. data/lib/lex_llm/model/pricing_category.rb +0 -46
  143. data/lib/lex_llm/model/pricing_tier.rb +0 -33
  144. data/lib/lex_llm/model.rb +0 -7
  145. data/lib/lex_llm/models.rb +0 -506
  146. data/lib/lex_llm/moderation.rb +0 -56
  147. data/lib/lex_llm/provider/open_ai_compatible.rb +0 -219
  148. data/lib/lex_llm/provider.rb +0 -278
  149. data/lib/lex_llm/railtie.rb +0 -35
  150. data/lib/lex_llm/routing/lane_key.rb +0 -51
  151. data/lib/lex_llm/routing/model_offering.rb +0 -169
  152. data/lib/lex_llm/routing.rb +0 -7
  153. data/lib/lex_llm/stream_accumulator.rb +0 -203
  154. data/lib/lex_llm/streaming.rb +0 -175
  155. data/lib/lex_llm/thinking.rb +0 -49
  156. data/lib/lex_llm/tokens.rb +0 -47
  157. data/lib/lex_llm/tool.rb +0 -254
  158. data/lib/lex_llm/tool_call.rb +0 -25
  159. data/lib/lex_llm/transcription.rb +0 -35
  160. data/lib/lex_llm/utils.rb +0 -91
  161. data/lib/lex_llm/version.rb +0 -5
  162. data/lib/lex_llm.rb +0 -96
  163. data/lib/tasks/lex_llm.rake +0 -23
  164. /data/lib/{lex_llm → legion/extensions/llm}/aliases.json +0 -0
  165. /data/lib/{lex_llm → legion/extensions/llm}/models.json +0 -0
@@ -1,169 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- module Routing
5
- # Describes one concrete model made available by one provider instance.
6
- class ModelOffering
7
- attr_reader :provider_family, :instance_id, :transport, :tier, :model, :usage_type, :capabilities, :limits,
8
- :credentials, :health, :cost, :policy_tags, :metadata
9
-
10
- def initialize(data)
11
- @provider_family = normalize_symbol(fetch_value(data, :provider_family, fetch_value(data, :provider)))
12
- @instance_id = normalize_symbol(fetch_value(data, :instance_id, @provider_family))
13
- @transport = normalize_symbol(fetch_value(data, :transport, :http))
14
- @tier = normalize_symbol(fetch_value(data, :tier, default_tier))
15
- @model = fetch_value(data, :model).to_s
16
- @usage_type = normalize_usage_type(fetch_value(data, :usage_type,
17
- fetch_value(data, :type) ||
18
- fetch_value(data, :kind) ||
19
- infer_usage_type(data)))
20
- @capabilities = normalize_array(fetch_value(data, :capabilities))
21
- @limits = normalize_hash(fetch_value(data, :limits))
22
- @credentials = fetch_value(data, :credentials)
23
- @health = normalize_hash(fetch_value(data, :health))
24
- @cost = normalize_hash(fetch_value(data, :cost))
25
- @policy_tags = normalize_array(fetch_value(data, :policy_tags)).map(&:to_sym)
26
- @metadata = normalize_hash(fetch_value(data, :metadata))
27
- end
28
-
29
- def enabled?
30
- !metadata.key?(:enabled) || metadata[:enabled] != false
31
- end
32
-
33
- def embedding?
34
- usage_type == :embedding
35
- end
36
-
37
- def inference?
38
- %i[chat inference completion].include?(usage_type)
39
- end
40
-
41
- def context_window
42
- integer_limit(:context_window) || integer_limit(:max_input_tokens)
43
- end
44
-
45
- def max_output_tokens
46
- integer_limit(:max_output_tokens)
47
- end
48
-
49
- def supports?(capability)
50
- capabilities.include?(capability.to_sym)
51
- end
52
-
53
- def eligible_for?(usage_type: nil, required_capabilities: [], min_context_window: nil, policy_tags: [])
54
- return false unless enabled?
55
- return false unless usage_type_matches?(usage_type)
56
- return false unless capabilities_match?(required_capabilities)
57
- return false unless context_window_matches?(min_context_window)
58
- return false unless policy_tags_match?(policy_tags)
59
-
60
- true
61
- end
62
-
63
- def lane_key(prefix: 'llm.fleet', include_context: true, include_fingerprint: false)
64
- LaneKey.for(self, prefix:, include_context:, include_fingerprint:)
65
- end
66
-
67
- def eligibility_fingerprint
68
- LaneKey.eligibility_fingerprint(self)
69
- end
70
-
71
- def to_h
72
- {
73
- provider_family: provider_family,
74
- instance_id: instance_id,
75
- transport: transport,
76
- tier: tier,
77
- model: model,
78
- usage_type: usage_type,
79
- capabilities: capabilities,
80
- limits: limits,
81
- credentials: credentials,
82
- health: health,
83
- cost: cost,
84
- policy_tags: policy_tags,
85
- metadata: metadata
86
- }
87
- end
88
-
89
- private
90
-
91
- def default_tier
92
- case @transport
93
- when :local
94
- :local
95
- when :rabbitmq
96
- :fleet
97
- else
98
- :private
99
- end
100
- end
101
-
102
- def infer_usage_type(data)
103
- capabilities = normalize_array(fetch_value(data, :capabilities))
104
- return :embedding if capabilities.include?(:embedding) || capabilities.include?(:embed)
105
-
106
- :inference
107
- end
108
-
109
- def normalize_usage_type(value)
110
- case value.to_sym
111
- when :embed, :embeddings
112
- :embedding
113
- when :completion, :text, :chat
114
- :inference
115
- else
116
- value.to_sym
117
- end
118
- end
119
-
120
- def normalize_symbol(value)
121
- return nil if value.nil?
122
-
123
- value.to_sym
124
- end
125
-
126
- def normalize_array(value)
127
- Array(value).compact.map(&:to_sym)
128
- end
129
-
130
- def normalize_hash(value)
131
- (value || {}).to_h.transform_keys(&:to_sym)
132
- end
133
-
134
- def fetch_value(hash, key, default = nil)
135
- return default unless hash.respond_to?(:key?)
136
-
137
- string_key = key.to_s
138
- return hash[string_key] if hash.key?(string_key)
139
-
140
- hash.key?(key) ? hash[key] : default
141
- end
142
-
143
- def usage_type_matches?(expected)
144
- expected.nil? || normalize_usage_type(expected) == usage_type
145
- end
146
-
147
- def capabilities_match?(required)
148
- Array(required).all? { |capability| supports?(capability) }
149
- end
150
-
151
- def context_window_matches?(minimum)
152
- minimum.nil? || (!!context_window && context_window >= minimum.to_i)
153
- end
154
-
155
- def policy_tags_match?(required)
156
- Array(required).all? { |tag| policy_tags.include?(tag.to_sym) }
157
- end
158
-
159
- def integer_limit(key)
160
- value = limits[key]
161
- return nil if value.nil?
162
-
163
- Integer(value)
164
- rescue ArgumentError, TypeError
165
- nil
166
- end
167
- end
168
- end
169
- end
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Provider-neutral routing metadata used by Legion LLM provider gems.
5
- module Routing
6
- end
7
- end
@@ -1,203 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Assembles streaming responses from LLMs into complete messages.
5
- class StreamAccumulator
6
- attr_reader :content, :model_id, :tool_calls
7
-
8
- def initialize
9
- @content = +''
10
- @thinking_text = +''
11
- @thinking_signature = nil
12
- @tool_calls = {}
13
- @input_tokens = nil
14
- @output_tokens = nil
15
- @cached_tokens = nil
16
- @cache_creation_tokens = nil
17
- @thinking_tokens = nil
18
- @inside_think_tag = false
19
- @pending_think_tag = +''
20
- @latest_tool_call_id = nil
21
- end
22
-
23
- def add(chunk)
24
- LexLLM.logger.debug { chunk.inspect } if LexLLM.config.log_stream_debug
25
- @model_id ||= chunk.model_id
26
-
27
- handle_chunk_content(chunk)
28
- append_thinking_from_chunk(chunk)
29
- count_tokens chunk
30
- LexLLM.logger.debug { inspect } if LexLLM.config.log_stream_debug
31
- end
32
-
33
- def to_message(response)
34
- Message.new(
35
- role: :assistant,
36
- content: content.empty? ? nil : content,
37
- thinking: Thinking.build(
38
- text: @thinking_text.empty? ? nil : @thinking_text,
39
- signature: @thinking_signature
40
- ),
41
- tokens: Tokens.build(
42
- input: @input_tokens,
43
- output: @output_tokens,
44
- cached: @cached_tokens,
45
- cache_creation: @cache_creation_tokens,
46
- thinking: @thinking_tokens
47
- ),
48
- model_id: model_id,
49
- tool_calls: tool_calls_from_stream,
50
- raw: response
51
- )
52
- end
53
-
54
- private
55
-
56
- def tool_calls_from_stream
57
- tool_calls.transform_values do |tc|
58
- arguments = if tc.arguments.is_a?(String) && !tc.arguments.empty?
59
- Legion::JSON.parse(tc.arguments, symbolize_names: false)
60
- elsif tc.arguments.is_a?(String)
61
- {}
62
- else
63
- tc.arguments
64
- end
65
-
66
- ToolCall.new(
67
- id: tc.id,
68
- name: tc.name,
69
- arguments: arguments,
70
- thought_signature: tc.thought_signature
71
- )
72
- end
73
- end
74
-
75
- def accumulate_tool_calls(new_tool_calls) # rubocop:disable Metrics/PerceivedComplexity
76
- LexLLM.logger.debug { "Accumulating tool calls: #{new_tool_calls}" } if LexLLM.config.log_stream_debug
77
- new_tool_calls.each_value do |tool_call|
78
- if tool_call.id
79
- tool_call_id = tool_call.id.empty? ? SecureRandom.uuid : tool_call.id
80
- tool_call_arguments = tool_call.arguments
81
- if tool_call_arguments.nil? || (tool_call_arguments.respond_to?(:empty?) && tool_call_arguments.empty?)
82
- tool_call_arguments = +''
83
- end
84
- @tool_calls[tool_call.id] = ToolCall.new(
85
- id: tool_call_id,
86
- name: tool_call.name,
87
- arguments: tool_call_arguments,
88
- thought_signature: tool_call.thought_signature
89
- )
90
- @latest_tool_call_id = tool_call.id
91
- else
92
- existing = @tool_calls[@latest_tool_call_id]
93
- if existing
94
- fragment = tool_call.arguments
95
- fragment = '' if fragment.nil?
96
- existing.arguments << fragment
97
- if tool_call.thought_signature && existing.thought_signature.nil?
98
- existing.thought_signature = tool_call.thought_signature
99
- end
100
- end
101
- end
102
- end
103
- end
104
-
105
- def find_tool_call(tool_call_id)
106
- if tool_call_id.nil?
107
- @tool_calls[@latest_tool_call]
108
- else
109
- @latest_tool_call_id = tool_call_id
110
- @tool_calls[tool_call_id]
111
- end
112
- end
113
-
114
- def count_tokens(chunk)
115
- @input_tokens = chunk.input_tokens if chunk.input_tokens
116
- @output_tokens = chunk.output_tokens if chunk.output_tokens
117
- @cached_tokens = chunk.cached_tokens if chunk.cached_tokens
118
- @cache_creation_tokens = chunk.cache_creation_tokens if chunk.cache_creation_tokens
119
- @thinking_tokens = chunk.thinking_tokens if chunk.thinking_tokens
120
- end
121
-
122
- def handle_chunk_content(chunk)
123
- return accumulate_tool_calls(chunk.tool_calls) if chunk.tool_call?
124
-
125
- content_text = chunk.content || ''
126
- if content_text.is_a?(String)
127
- append_text_with_thinking(content_text)
128
- else
129
- @content << content_text.to_s
130
- end
131
- end
132
-
133
- def append_text_with_thinking(text)
134
- content_chunk, thinking_chunk = extract_think_tags(text)
135
- @content << content_chunk
136
- @thinking_text << thinking_chunk if thinking_chunk
137
- end
138
-
139
- def append_thinking_from_chunk(chunk)
140
- thinking = chunk.thinking
141
- return unless thinking
142
-
143
- @thinking_text << thinking.text.to_s if thinking.text
144
- @thinking_signature ||= thinking.signature # rubocop:disable Naming/MemoizedInstanceVariableName
145
- end
146
-
147
- def extract_think_tags(text)
148
- start_tag = '<think>'
149
- end_tag = '</think>'
150
- remaining = @pending_think_tag + text
151
- @pending_think_tag = +''
152
-
153
- output = +''
154
- thinking = +''
155
-
156
- until remaining.empty?
157
- remaining = if @inside_think_tag
158
- consume_think_content(remaining, end_tag, thinking)
159
- else
160
- consume_non_think_content(remaining, start_tag, output)
161
- end
162
- end
163
-
164
- [output, thinking.empty? ? nil : thinking]
165
- end
166
-
167
- def consume_think_content(remaining, end_tag, thinking)
168
- end_index = remaining.index(end_tag)
169
- if end_index
170
- thinking << remaining.slice(0, end_index)
171
- @inside_think_tag = false
172
- remaining.slice((end_index + end_tag.length)..) || +''
173
- else
174
- suffix_len = longest_suffix_prefix(remaining, end_tag)
175
- thinking << remaining.slice(0, remaining.length - suffix_len)
176
- @pending_think_tag = remaining.slice(-suffix_len, suffix_len)
177
- +''
178
- end
179
- end
180
-
181
- def consume_non_think_content(remaining, start_tag, output)
182
- start_index = remaining.index(start_tag)
183
- if start_index
184
- output << remaining.slice(0, start_index)
185
- @inside_think_tag = true
186
- remaining.slice((start_index + start_tag.length)..) || +''
187
- else
188
- suffix_len = longest_suffix_prefix(remaining, start_tag)
189
- output << remaining.slice(0, remaining.length - suffix_len)
190
- @pending_think_tag = remaining.slice(-suffix_len, suffix_len)
191
- +''
192
- end
193
- end
194
-
195
- def longest_suffix_prefix(text, tag)
196
- max = [text.length, tag.length - 1].min
197
- max.downto(1) do |len|
198
- return len if text.end_with?(tag[0, len])
199
- end
200
- 0
201
- end
202
- end
203
- end
@@ -1,175 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Handles streaming responses from AI providers.
5
- module Streaming
6
- module_function
7
-
8
- def stream_response(connection, payload, additional_headers = {}, &block)
9
- accumulator = StreamAccumulator.new
10
-
11
- response = connection.post stream_url, payload do |req|
12
- req.headers = additional_headers.merge(req.headers) unless additional_headers.empty?
13
- if faraday_1?
14
- req.options[:on_data] = handle_stream do |chunk|
15
- accumulator.add chunk
16
- block.call chunk
17
- end
18
- else
19
- req.options.on_data = handle_stream do |chunk|
20
- accumulator.add chunk
21
- block.call chunk
22
- end
23
- end
24
- end
25
-
26
- message = accumulator.to_message(response)
27
- LexLLM.logger.debug { "Stream completed: #{message.content}" }
28
- message
29
- end
30
-
31
- def handle_stream(&block)
32
- build_on_data_handler do |data|
33
- block.call(build_chunk(data)) if data.is_a?(Hash)
34
- end
35
- end
36
-
37
- private
38
-
39
- def faraday_1?
40
- Faraday::VERSION.start_with?('1')
41
- end
42
-
43
- def build_on_data_handler(&)
44
- buffer = +''
45
- parser = EventStreamParser::Parser.new
46
-
47
- FaradayHandlers.build(
48
- faraday_v1: faraday_1?,
49
- on_chunk: ->(chunk, env) { process_stream_chunk(chunk, parser, env, &) },
50
- on_failed_response: ->(chunk, env) { handle_failed_response(chunk, buffer, env) }
51
- )
52
- end
53
-
54
- def process_stream_chunk(chunk, parser, env, &)
55
- LexLLM.logger.debug { "Received chunk: #{chunk}" } if LexLLM.config.log_stream_debug
56
-
57
- if error_chunk?(chunk)
58
- handle_error_chunk(chunk, env)
59
- elsif json_error_payload?(chunk)
60
- handle_json_error_chunk(chunk, env)
61
- else
62
- yield handle_sse(chunk, parser, env, &)
63
- end
64
- end
65
-
66
- def error_chunk?(chunk)
67
- chunk.start_with?('event: error')
68
- end
69
-
70
- def json_error_payload?(chunk)
71
- chunk.lstrip.start_with?('{') && chunk.include?('"error"')
72
- end
73
-
74
- def handle_json_error_chunk(chunk, env)
75
- parse_error_from_json(chunk, env, 'Failed to parse JSON error chunk')
76
- end
77
-
78
- def handle_error_chunk(chunk, env)
79
- error_data = chunk.split("\n")[1].delete_prefix('data: ')
80
- parse_error_from_json(error_data, env, 'Failed to parse error chunk')
81
- end
82
-
83
- def handle_failed_response(chunk, buffer, env)
84
- buffer << chunk
85
- error_data = Legion::JSON.parse(buffer, symbolize_names: false)
86
- handle_parsed_error(error_data, env)
87
- rescue Legion::JSON::ParseError
88
- LexLLM.logger.debug { "Accumulating error chunk: #{chunk}" }
89
- end
90
-
91
- def handle_sse(chunk, parser, env, &)
92
- parser.feed(chunk) do |type, data|
93
- case type.to_sym
94
- when :error
95
- handle_error_event(data, env)
96
- else
97
- yield handle_data(data, env, &) unless data == '[DONE]'
98
- end
99
- end
100
- end
101
-
102
- def handle_data(data, env)
103
- parsed = Legion::JSON.parse(data, symbolize_names: false)
104
- return parsed unless parsed.is_a?(Hash) && parsed.key?('error')
105
-
106
- handle_parsed_error(parsed, env)
107
- rescue Legion::JSON::ParseError => e
108
- LexLLM.logger.debug { "Failed to parse data chunk: #{e.message}" }
109
- end
110
-
111
- def handle_error_event(data, env)
112
- parse_error_from_json(data, env, 'Failed to parse error event')
113
- end
114
-
115
- def parse_streaming_error(data)
116
- error_data = Legion::JSON.parse(data, symbolize_names: false)
117
- [500, error_data['message'] || 'Unknown streaming error']
118
- rescue Legion::JSON::ParseError => e
119
- LexLLM.logger.debug { "Failed to parse streaming error: #{e.message}" }
120
- [500, "Failed to parse error: #{data}"]
121
- end
122
-
123
- def handle_parsed_error(parsed_data, env)
124
- status, _message = parse_streaming_error(parsed_data.to_json)
125
- error_response = build_stream_error_response(parsed_data, env, status)
126
- ErrorMiddleware.parse_error(provider: self, response: error_response)
127
- end
128
-
129
- def parse_error_from_json(data, env, error_message)
130
- parsed_data = Legion::JSON.parse(data, symbolize_names: false)
131
- handle_parsed_error(parsed_data, env)
132
- rescue Legion::JSON::ParseError => e
133
- LexLLM.logger.debug { "#{error_message}: #{e.message}" }
134
- end
135
-
136
- def build_stream_error_response(parsed_data, env, status)
137
- error_status = status || env&.status || 500
138
-
139
- if faraday_1?
140
- Struct.new(:body, :status).new(parsed_data, error_status)
141
- else
142
- env.merge(body: parsed_data, status: error_status)
143
- end
144
- end
145
-
146
- # Builds Faraday on_data handlers for different major versions.
147
- module FaradayHandlers
148
- module_function
149
-
150
- def build(faraday_v1:, on_chunk:, on_failed_response:)
151
- if faraday_v1
152
- v1_on_data(on_chunk)
153
- else
154
- v2_on_data(on_chunk, on_failed_response)
155
- end
156
- end
157
-
158
- def v1_on_data(on_chunk)
159
- proc do |chunk, _size|
160
- on_chunk.call(chunk, nil)
161
- end
162
- end
163
-
164
- def v2_on_data(on_chunk, on_failed_response)
165
- proc do |chunk, _bytes, env|
166
- if env&.status == 200
167
- on_chunk.call(chunk, env)
168
- else
169
- on_failed_response.call(chunk, env)
170
- end
171
- end
172
- end
173
- end
174
- end
175
- end
@@ -1,49 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Represents provider thinking output.
5
- class Thinking
6
- attr_reader :text, :signature
7
-
8
- def initialize(text: nil, signature: nil)
9
- @text = text
10
- @signature = signature
11
- end
12
-
13
- def self.build(text: nil, signature: nil)
14
- text = nil if text.is_a?(String) && text.empty?
15
- signature = nil if signature.is_a?(String) && signature.empty?
16
-
17
- return nil if text.nil? && signature.nil?
18
-
19
- new(text: text, signature: signature)
20
- end
21
-
22
- def pretty_print(printer)
23
- printer.object_group(self) do
24
- printer.breakable
25
- printer.text 'text='
26
- printer.pp text
27
- printer.comma_breakable
28
- printer.text 'signature='
29
- printer.pp(signature ? '[REDACTED]' : nil)
30
- end
31
- end
32
- end
33
-
34
- class Thinking
35
- # Normalized config for thinking across providers.
36
- class Config
37
- attr_reader :effort, :budget
38
-
39
- def initialize(effort: nil, budget: nil)
40
- @effort = effort.is_a?(Symbol) ? effort.to_s : effort
41
- @budget = budget
42
- end
43
-
44
- def enabled?
45
- !effort.nil? || !budget.nil?
46
- end
47
- end
48
- end
49
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LexLLM
4
- # Represents token usage for a response.
5
- class Tokens
6
- attr_reader :input, :output, :cached, :cache_creation, :thinking
7
-
8
- # rubocop:disable Metrics/ParameterLists
9
- def initialize(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
10
- @input = input
11
- @output = output
12
- @cached = cached
13
- @cache_creation = cache_creation
14
- @thinking = thinking || reasoning
15
- end
16
- # rubocop:enable Metrics/ParameterLists
17
-
18
- # rubocop:disable Metrics/ParameterLists
19
- def self.build(input: nil, output: nil, cached: nil, cache_creation: nil, thinking: nil, reasoning: nil)
20
- return nil if [input, output, cached, cache_creation, thinking, reasoning].all?(&:nil?)
21
-
22
- new(
23
- input: input,
24
- output: output,
25
- cached: cached,
26
- cache_creation: cache_creation,
27
- thinking: thinking,
28
- reasoning: reasoning
29
- )
30
- end
31
- # rubocop:enable Metrics/ParameterLists
32
-
33
- def to_h
34
- {
35
- input_tokens: input,
36
- output_tokens: output,
37
- cached_tokens: cached,
38
- cache_creation_tokens: cache_creation,
39
- thinking_tokens: thinking
40
- }.compact
41
- end
42
-
43
- def reasoning
44
- thinking
45
- end
46
- end
47
- end