llm_gateway 0.4.0 → 0.6.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.pi/skills/live-provider-testing/SKILL.md +183 -0
  3. data/.pi/skills/options-development/SKILL.md +131 -0
  4. data/CHANGELOG.md +43 -0
  5. data/README.md +110 -41
  6. data/Rakefile +1 -0
  7. data/docs/migration_guide_0.6.0.md +386 -0
  8. data/lib/llm_gateway/adapters/adapter.rb +8 -44
  9. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
  10. data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
  11. data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
  12. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +59 -47
  13. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
  14. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
  15. data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
  16. data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
  17. data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +336 -0
  18. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
  19. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
  20. data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
  21. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
  22. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +193 -170
  23. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
  24. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
  25. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
  26. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +106 -275
  27. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
  28. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
  29. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
  30. data/lib/llm_gateway/adapters/stream_mapper.rb +57 -0
  31. data/lib/llm_gateway/adapters/structs.rb +102 -52
  32. data/lib/llm_gateway/base_client.rb +2 -4
  33. data/lib/llm_gateway/client.rb +10 -66
  34. data/lib/llm_gateway/clients/anthropic.rb +5 -4
  35. data/lib/llm_gateway/clients/groq.rb +18 -4
  36. data/lib/llm_gateway/clients/openai.rb +20 -18
  37. data/lib/llm_gateway/prompt.rb +35 -17
  38. data/lib/llm_gateway/version.rb +1 -1
  39. data/lib/llm_gateway.rb +5 -29
  40. metadata +8 -10
  41. data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
  42. data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
  43. data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
  44. data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
  45. data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
  46. data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
  47. data/scripts/generate_handoff_live_fixture.rb +0 -169
  48. data/scripts/generate_handoff_media_fixture.rb +0 -167
@@ -9,35 +9,6 @@ class BaseStruct < Dry::Struct
9
9
  transform_keys(&:to_sym)
10
10
  end
11
11
 
12
- class AssistantStreamEvent < BaseStruct
13
- EventType = Types::Coercible::Symbol.enum(:text_start, :text_delta, :text_end, :tool_start, :tool_delta, :tool_end, :reasoning_start, :reasoning_delta, :reasoning_end)
14
-
15
- attribute :type, EventType
16
- attribute :delta, Types::Coercible::String.default { "" }
17
- attribute :content_index, Types::Integer
18
- end
19
-
20
-
21
- class AssistantToolStartEvent < AssistantStreamEvent
22
- attribute :id, Types::String
23
- attribute :name, Types::String
24
- attribute :content_index, Types::Integer
25
- end
26
-
27
-
28
- class AssistantStreamReasoningEvent < AssistantStreamEvent
29
- attribute :signature, Types::Coercible::String.default { "" }
30
- attribute :content_index, Types::Integer
31
- end
32
-
33
- class AssistantStreamMessageEvent < BaseStruct
34
- EventType = Types::Coercible::Symbol.enum(:message_start, :message_delta, :message_end)
35
-
36
- attribute :type, EventType
37
- attribute :delta, Types::Coercible::Hash.default { {} }
38
- attribute :usage_increment, Types::Coercible::Hash.default { {} }
39
- end
40
-
41
12
  class TextContent < BaseStruct
42
13
  attribute :type, Types::String.enum("text")
43
14
  attribute :text, Types::String
@@ -87,12 +58,101 @@ class ToolResult < BaseStruct
87
58
  attribute :content, Types::String
88
59
  end
89
60
 
90
- class AssistantMessage < BaseStruct
61
+ class PartialAssistantMessage < BaseStruct
91
62
  ContentBlock =
92
63
  Types.Instance(TextContent) |
93
64
  Types.Instance(ReasoningContent) |
94
65
  Types.Instance(ToolCall)
95
66
 
67
+ attribute? :id, Types::String.optional
68
+ attribute? :model, Types::String.optional
69
+ attribute? :role, Types::String.enum("assistant").optional
70
+ attribute :timestamp, Types::Integer
71
+ attribute? :stop_reason, Types::String.enum("stop", "length", "tool_use", "toolUse", "error", "aborted").optional
72
+ attribute? :content, Types::Array.of(ContentBlock).optional
73
+
74
+ def self.new(attributes = {})
75
+ attrs = attributes.to_h.transform_keys(&:to_sym)
76
+ attrs[:content] = Array(attrs[:content]).map { |block| build_content_block(block) } if attrs.key?(:content)
77
+ super(attrs)
78
+ end
79
+
80
+ def self.build_content_block(block)
81
+ return block if block.is_a?(TextContent) || block.is_a?(ReasoningContent) || block.is_a?(ToolCall)
82
+
83
+ case block[:type] || block["type"]
84
+ when "text"
85
+ TextContent.new(block)
86
+ when "reasoning"
87
+ ReasoningContent.new(block)
88
+ when "thinking"
89
+ ReasoningContent.new(
90
+ type: "reasoning",
91
+ reasoning: block[:thinking] || block["thinking"] || block[:reasoning] || block["reasoning"],
92
+ signature: block[:signature] || block["signature"]
93
+ )
94
+ when "tool_use"
95
+ ToolCall.new(block)
96
+ else
97
+ raise ArgumentError, "Unsupported content block type: #{block[:type] || block['type']}"
98
+ end
99
+ end
100
+
101
+ private_class_method :build_content_block
102
+ end
103
+
104
+ class AssistantStreamEvent < BaseStruct
105
+ EventType = Types::Coercible::Symbol.enum(:text_start, :text_delta, :text_end, :tool_start, :tool_delta, :tool_end, :reasoning_start, :reasoning_delta, :reasoning_end)
106
+
107
+ attribute :type, EventType
108
+ attribute :delta, Types::Coercible::String.default { "" }
109
+ attribute :content_index, Types::Integer
110
+ attribute :partial, Types.Instance(PartialAssistantMessage)
111
+
112
+ def content
113
+ case type
114
+ when :text_end
115
+ finalized_content_block&.text
116
+ when :reasoning_end
117
+ finalized_content_block&.reasoning
118
+ when :tool_end
119
+ finalized_content_block
120
+ end
121
+ end
122
+
123
+ def text
124
+ content if type == :text_end
125
+ end
126
+
127
+ def reasoning
128
+ content if type == :reasoning_end
129
+ end
130
+
131
+ def tool_call
132
+ finalized_content_block if type == :tool_end
133
+ end
134
+
135
+ alias tool tool_call
136
+
137
+ private
138
+
139
+ def finalized_content_block
140
+ partial.content&.[](content_index)
141
+ end
142
+ end
143
+
144
+ class AssistantToolStartEvent < AssistantStreamEvent
145
+ attribute :id, Types::String
146
+ attribute :name, Types::String
147
+ attribute :content_index, Types::Integer
148
+ end
149
+
150
+ class AssistantStreamReasoningEvent < AssistantStreamEvent
151
+ attribute :signature, Types::Coercible::String.default { "" }
152
+ attribute :content_index, Types::Integer
153
+ end
154
+
155
+ class AssistantMessage < PartialAssistantMessage
96
156
  attribute :id, Types::String
97
157
  attribute :model, Types::String
98
158
  attribute :usage, Types::Hash
@@ -103,12 +163,6 @@ class AssistantMessage < BaseStruct
103
163
  attribute? :error_message, Types::String.optional
104
164
  attribute :content, Types::Array.of(ContentBlock)
105
165
 
106
- def self.new(attributes)
107
- attrs = attributes.to_h.transform_keys(&:to_sym)
108
- attrs[:content] = Array(attrs[:content]).map { |block| build_content_block(block) }
109
- super(attrs)
110
- end
111
-
112
166
  def to_h
113
167
  result = {
114
168
  id: id,
@@ -120,26 +174,22 @@ class AssistantMessage < BaseStruct
120
174
  api: api,
121
175
  content: content.map(&:to_h)
122
176
  }
177
+ result[:timestamp] = timestamp unless timestamp.nil?
123
178
  result[:error_message] = error_message unless error_message.nil?
124
179
  result
125
180
  end
181
+ end
126
182
 
127
- def self.build_content_block(block)
128
- return block if block.is_a?(TextContent) || block.is_a?(ReasoningContent) || block.is_a?(ToolCall)
183
+ class AssistantStreamMessageEvent < BaseStruct
184
+ EventType = Types::Coercible::Symbol.enum(:message_start, :message_delta)
129
185
 
130
- case block[:type] || block["type"]
131
- when "text"
132
- TextContent.new(block)
133
- when "reasoning"
134
- ReasoningContent.new(block)
135
- when "thinking"
136
- ReasoningContent.new(type: "reasoning", reasoning: block[:thinking] || block["thinking"] || block[:reasoning] || block["reasoning"], signature: block[:signature] || block["signature"])
137
- when "tool_use"
138
- ToolCall.new(block)
139
- else
140
- raise ArgumentError, "Unsupported content block type: #{block[:type] || block['type']}"
141
- end
142
- end
186
+ attribute :type, EventType
187
+ attribute :delta, Types::Coercible::Hash.default { {} }
188
+ attribute :usage, Types::Coercible::Hash.default { {} }
189
+ attribute :partial, Types.Instance(PartialAssistantMessage)
190
+ end
143
191
 
144
- private_class_method :build_content_block
192
+ class AssistantStreamMessageEndEvent < BaseStruct
193
+ attribute :type, Types::Coercible::Symbol.enum(:message_end)
194
+ attribute :message, Types.Instance(AssistantMessage)
145
195
  end
@@ -6,11 +6,9 @@ require "json"
6
6
 
7
7
  module LlmGateway
8
8
  class BaseClient
9
- attr_accessor
10
- attr_reader :api_key, :model_key, :base_endpoint
9
+ attr_reader :api_key, :base_endpoint
11
10
 
12
- def initialize(model_key:, api_key:)
13
- @model_key = model_key
11
+ def initialize(api_key:)
14
12
  @api_key = api_key
15
13
  end
16
14
 
@@ -1,24 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+
3
4
  module LlmGateway
4
5
  class Client
5
- def self.chat(model, message, tools: nil, system: nil, api_key: nil, refresh_token: nil, expires_at: nil, **options)
6
- adapter = build_adapter_from_model(model, api_key: api_key, refresh_token: refresh_token, expires_at: expires_at)
7
- adapter.chat(message, tools: tools, system: system, **options)
8
- end
9
-
10
- def self.responses(model, message, tools: nil, system: nil, api_key: nil, **options)
11
- adapter = build_adapter_from_model(model, api_key: api_key, api: "responses")
12
- adapter.chat(message, tools: tools, system: system, **options)
13
- end
14
-
15
- def self.build_client(provider, api_key:, model: "none")
16
- adapter = LlmGateway.build_provider(
17
- provider: provider,
18
- api_key: api_key,
19
- model_key: model
20
- )
21
- adapter.client
6
+ def self.provider_id_from_client(client)
7
+ case client
8
+ when LlmGateway::Clients::Anthropic
9
+ "anthropic"
10
+ when LlmGateway::Clients::OpenAI
11
+ "openai"
12
+ when LlmGateway::Clients::Groq
13
+ "groq"
14
+ end
22
15
  end
23
16
 
24
17
  def self.upload_file(provider, **kwargs)
@@ -38,54 +31,5 @@ module LlmGateway
38
31
  )
39
32
  adapter.download_file(**kwargs)
40
33
  end
41
-
42
- def self.provider_from_model(model)
43
- return "anthropic" if model.start_with?("claude")
44
- return "groq" if model.start_with?("llama")
45
- return "openai" if model.start_with?("gpt") ||
46
- model.start_with?("o4-") ||
47
- model.start_with?("openai")
48
-
49
- raise LlmGateway::Errors::UnsupportedModel, model
50
- end
51
-
52
- def self.provider_id_from_client(client)
53
- case client
54
- when LlmGateway::Clients::Anthropic
55
- "anthropic"
56
- when LlmGateway::Clients::OpenAI
57
- "openai"
58
- when LlmGateway::Clients::Groq
59
- "groq"
60
- else
61
- client.class.name.downcase
62
- end
63
- end
64
-
65
- # --- private helpers ---
66
-
67
- def self.build_adapter_from_model(model, api_key: nil, refresh_token: nil, expires_at: nil, api: nil)
68
- provider = provider_from_model(model)
69
-
70
- if api == "responses"
71
- config = {
72
- provider: "#{provider}_responses",
73
- model_key: model
74
- }
75
- config[:api_key] = api_key if api_key
76
- LlmGateway.build_provider(config)
77
- else
78
- provider_key = case provider
79
- when "anthropic" then "anthropic_messages"
80
- when "openai" then "openai_completions"
81
- when "groq" then "groq_completions"
82
- end
83
- config = { provider: provider_key, model_key: model }
84
- config[:api_key] = api_key if api_key
85
- LlmGateway.build_provider(config)
86
- end
87
- end
88
-
89
- private_class_method :build_adapter_from_model
90
34
  end
91
35
  end
@@ -9,10 +9,11 @@ module LlmGateway
9
9
  module Clients
10
10
  class Anthropic < BaseClient
11
11
  CLAUDE_CODE_VERSION = "2.1.2"
12
+ DEFAULT_MODEL = "claude-3-7-sonnet-20250219"
12
13
 
13
- def initialize(model_key: "claude-3-7-sonnet-20250219", api_key: ENV["ANTHROPIC_API_KEY"])
14
+ def initialize(api_key: ENV["ANTHROPIC_API_KEY"])
14
15
  @base_endpoint = "https://api.anthropic.com/v1"
15
- super(model_key: model_key, api_key: api_key)
16
+ super(api_key: api_key)
16
17
  end
17
18
 
18
19
  def chat(messages, **kwargs)
@@ -44,11 +45,11 @@ module LlmGateway
44
45
 
45
46
  private
46
47
 
47
- def build_body(messages, tools: nil, system: [], cache_retention: nil, **options)
48
+ def build_body(messages, tools: nil, system: [], cache_retention: nil, model: DEFAULT_MODEL, **options)
48
49
  cache_control = anthropic_cache_control_for(cache_retention)
49
50
 
50
51
  body = {
51
- model: model_key,
52
+ model: model,
52
53
  messages: messages
53
54
  }
54
55
 
@@ -5,14 +5,16 @@ require_relative "../base_client"
5
5
  module LlmGateway
6
6
  module Clients
7
7
  class Groq < BaseClient
8
- def initialize(model_key: "openai/gpt-oss-20b", api_key: ENV["GROQ_API_KEY"])
8
+ DEFAULT_MODEL = "openai/gpt-oss-120b"
9
+
10
+ def initialize(api_key: ENV["GROQ_API_KEY"])
9
11
  @base_endpoint = "https://api.groq.com/openai/v1"
10
- super(model_key: model_key, api_key: api_key)
12
+ super(api_key: api_key)
11
13
  end
12
14
 
13
- def chat(messages, tools: nil, system: [], **options)
15
+ def chat(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options)
14
16
  body = {
15
- model: model_key,
17
+ model: model,
16
18
  messages: system + messages,
17
19
  tools: tools
18
20
  }
@@ -21,6 +23,18 @@ module LlmGateway
21
23
  post("chat/completions", body)
22
24
  end
23
25
 
26
+ def stream(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options, &block)
27
+ body = {
28
+ model: model,
29
+ messages: system + messages,
30
+ tools: tools,
31
+ stream_options: { include_usage: true }
32
+ }
33
+ body.merge!(options)
34
+
35
+ post_stream("chat/completions", body, &block)
36
+ end
37
+
24
38
  private
25
39
 
26
40
  def build_headers
@@ -6,18 +6,20 @@ module LlmGateway
6
6
  module Clients
7
7
  class OpenAI < BaseClient
8
8
  CODEX_BASE_ENDPOINT = "https://chatgpt.com/backend-api/codex"
9
+ DEFAULT_MODEL = "gpt-4o"
10
+ DEFAULT_EMBEDDINGS_MODEL = "text-embedding-3-small"
9
11
 
10
12
  attr_reader :account_id
11
13
 
12
- def initialize(model_key: "gpt-4o", api_key: ENV["OPENAI_API_KEY"], account_id: nil)
14
+ def initialize(api_key: ENV["OPENAI_API_KEY"], account_id: nil)
13
15
  @base_endpoint = "https://api.openai.com/v1"
14
16
  @account_id = account_id
15
- super(model_key: model_key, api_key: api_key)
17
+ super(api_key: api_key)
16
18
  end
17
19
 
18
- def chat(messages, tools: nil, system: [], **options)
20
+ def chat(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options)
19
21
  body = {
20
- model: model_key,
22
+ model: model,
21
23
  messages: system + messages
22
24
  }
23
25
  body[:tools] = tools if tools
@@ -26,9 +28,9 @@ module LlmGateway
26
28
  post("chat/completions", body)
27
29
  end
28
30
 
29
- def stream(messages, tools: nil, system: [], **options, &block)
31
+ def stream(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options, &block)
30
32
  body = {
31
- model: model_key,
33
+ model: model,
32
34
  messages: system + messages
33
35
  }
34
36
  body[:tools] = tools if tools
@@ -38,9 +40,9 @@ module LlmGateway
38
40
  post_stream("chat/completions", body, &block)
39
41
  end
40
42
 
41
- def responses(messages, tools: nil, system: [], **options)
43
+ def responses(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options)
42
44
  body = {
43
- model: model_key,
45
+ model: model,
44
46
  input: messages.flatten
45
47
  }
46
48
  body[:instructions] = system[0][:content] if system.any?
@@ -50,9 +52,9 @@ module LlmGateway
50
52
  post("responses", body)
51
53
  end
52
54
 
53
- def stream_responses(messages, tools: nil, system: [], **options, &block)
55
+ def stream_responses(messages, tools: nil, system: [], model: DEFAULT_MODEL, **options, &block)
54
56
  body = {
55
- model: model_key,
57
+ model: model,
56
58
  input: messages.flatten
57
59
  }
58
60
  body[:instructions] = system[0][:content] if system.any?
@@ -74,8 +76,8 @@ module LlmGateway
74
76
  token_manager.access_token
75
77
  end
76
78
 
77
- def chat_codex(messages, tools: nil, system: [], account_id: nil, **options)
78
- body = build_codex_body(messages, system, tools, **options)
79
+ def chat_codex(messages, tools: nil, system: [], account_id: nil, model: DEFAULT_MODEL, **options)
80
+ body = build_codex_body(messages, system, tools, model: model, **options)
79
81
 
80
82
  completed_response = nil
81
83
  post_codex_stream("responses", body, account_id: account_id) do |raw_sse|
@@ -87,8 +89,8 @@ module LlmGateway
87
89
  completed_response
88
90
  end
89
91
 
90
- def stream_codex(messages, tools: nil, system: [], account_id: nil, **options, &block)
91
- body = build_codex_body(messages, system, tools, **options)
92
+ def stream_codex(messages, tools: nil, system: [], account_id: nil, model: DEFAULT_MODEL, **options, &block)
93
+ body = build_codex_body(messages, system, tools, model: model, **options)
92
94
  post_codex_stream("responses", body, account_id: account_id, &block)
93
95
  end
94
96
 
@@ -96,10 +98,10 @@ module LlmGateway
96
98
  get("files/#{file_id}/content")
97
99
  end
98
100
 
99
- def generate_embeddings(input)
101
+ def generate_embeddings(input, model: DEFAULT_EMBEDDINGS_MODEL)
100
102
  body = {
101
103
  input:,
102
- model: model_key
104
+ model: model
103
105
  }
104
106
  post("embeddings", body)
105
107
  end
@@ -110,12 +112,12 @@ module LlmGateway
110
112
 
111
113
  private
112
114
 
113
- def build_codex_body(messages, system, tools, **options)
115
+ def build_codex_body(messages, system, tools, model:, **options)
114
116
  instructions = Array(system).filter_map { |s| s.is_a?(Hash) ? s[:content] : s }.join("\n")
115
117
  instructions = "You are a helpful assistant." if instructions.empty?
116
118
 
117
119
  body = {
118
- model: model_key,
120
+ model: model,
119
121
  instructions: instructions,
120
122
  input: messages,
121
123
  store: false,
@@ -2,7 +2,29 @@
2
2
 
3
3
  module LlmGateway
4
4
  class Prompt
5
- attr_reader :model
5
+ UNSET = Object.new.freeze
6
+
7
+ attr_reader :provider, :model
8
+
9
+ class << self
10
+ def provider(value = UNSET)
11
+ @provider = value unless value.equal?(UNSET)
12
+ @provider
13
+ end
14
+
15
+ def provider=(value)
16
+ @provider = value
17
+ end
18
+
19
+ def model(value = UNSET)
20
+ @model = value unless value.equal?(UNSET)
21
+ @model
22
+ end
23
+
24
+ def model=(value)
25
+ @model = value
26
+ end
27
+ end
6
28
 
7
29
  def self.before_execute(*methods, &block)
8
30
  before_execute_callbacks.concat(methods)
@@ -26,23 +48,19 @@ module LlmGateway
26
48
  super
27
49
  subclass.instance_variable_set(:@before_execute_callbacks, before_execute_callbacks.dup)
28
50
  subclass.instance_variable_set(:@after_execute_callbacks, after_execute_callbacks.dup)
51
+ subclass.provider = provider
52
+ subclass.model = model
29
53
  end
30
54
 
31
- def initialize(model)
32
- @model = model
33
- @connection = if model.is_a?(String)
34
- LlmGateway.configured_clients.values.find do |client|
35
- client.client.model_key == model
36
- end
37
- else
38
- model
39
- end
55
+ def initialize(provider = nil, model = nil)
56
+ @provider = provider || self.class.provider
57
+ @model = model || self.class.model
40
58
  end
41
59
 
42
60
  def run
43
61
  run_callbacks(:before_execute, prompt)
44
62
 
45
- response = post
63
+ response = stream
46
64
 
47
65
  response_content = if respond_to?(:extract_response)
48
66
  extract_response(response)
@@ -61,12 +79,12 @@ module LlmGateway
61
79
  result
62
80
  end
63
81
 
64
- def post
65
- if @connection
66
- @connection.chat(prompt, tools: tools, system: system_prompt)
67
- else
68
- LlmGateway::Client.chat(model, prompt, tools: tools, system: system_prompt)
69
- end
82
+ def stream(provider: nil, model: nil, **options)
83
+ stream_provider = provider || self.provider
84
+ stream_model = model || self.model
85
+ options[:model] = stream_model if stream_model
86
+
87
+ stream_provider.stream(prompt, tools: tools, system: system_prompt, **options)
70
88
  end
71
89
 
72
90
  def tools
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmGateway
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/llm_gateway.rb CHANGED
@@ -21,17 +21,17 @@ require_relative "llm_gateway/clients/groq"
21
21
  require_relative "llm_gateway/adapters/option_mapper"
22
22
  require_relative "llm_gateway/adapters/anthropic_option_mapper"
23
23
  require_relative "llm_gateway/adapters/structs"
24
+ require_relative "llm_gateway/adapters/stream_mapper"
24
25
 
25
26
  require_relative "llm_gateway/adapters/anthropic/input_mapper"
26
27
  require_relative "llm_gateway/adapters/anthropic/output_mapper"
27
28
  require_relative "llm_gateway/adapters/openai/file_output_mapper"
28
29
  require_relative "llm_gateway/adapters/openai/prompt_cache_option_mapper"
29
30
  require_relative "llm_gateway/adapters/openai/chat_completions/input_mapper"
30
- require_relative "llm_gateway/adapters/openai/chat_completions/output_mapper"
31
31
  require_relative "llm_gateway/adapters/openai/chat_completions/option_mapper"
32
+ require_relative "llm_gateway/adapters/openai/chat_completions/stream_mapper"
32
33
  require_relative "llm_gateway/adapters/openai/file_output_mapper"
33
34
  require_relative "llm_gateway/adapters/openai/responses/input_mapper"
34
- require_relative "llm_gateway/adapters/openai/responses/output_mapper"
35
35
  require_relative "llm_gateway/adapters/openai/responses/option_mapper"
36
36
 
37
37
  # Load adapter classes
@@ -48,10 +48,6 @@ require_relative "llm_gateway/provider_registry"
48
48
  module LlmGateway
49
49
  class Error < StandardError; end
50
50
 
51
- # Direction constants for message mappers
52
- DIRECTION_IN = :in
53
- DIRECTION_OUT = :out
54
-
55
51
  # Backward-compatible aliases for renamed clients/adapters
56
52
  module Clients
57
53
  Claude = Anthropic
@@ -63,9 +59,7 @@ module LlmGateway
63
59
  Client = LlmGateway::Clients::Anthropic
64
60
  MessagesAdapter = LlmGateway::Adapters::Anthropic::MessagesAdapter
65
61
  InputMapper = LlmGateway::Adapters::Anthropic::InputMapper
66
- OutputMapper = LlmGateway::Adapters::Anthropic::OutputMapper
67
62
  StreamMapper = LlmGateway::Adapters::Anthropic::StreamMapper
68
- BidirectionalMessageMapper = LlmGateway::Adapters::Anthropic::BidirectionalMessageMapper
69
63
  FileOutputMapper = LlmGateway::Adapters::Anthropic::FileOutputMapper
70
64
  end
71
65
 
@@ -106,6 +100,9 @@ module LlmGateway
106
100
  def self.build_provider(config)
107
101
  config = config.transform_keys(&:to_sym)
108
102
  provider_name = config.delete(:provider)
103
+ if config.key?(:model_key)
104
+ raise ArgumentError, "model_key is no longer a provider option; pass model: to chat/stream instead"
105
+ end
109
106
  entry = ProviderRegistry.resolve(provider_name)
110
107
 
111
108
  client = entry[:client].new(**config)
@@ -159,25 +156,4 @@ module LlmGateway
159
156
  ProviderRegistry.register("openai_codex",
160
157
  client: Clients::OpenAI,
161
158
  adapter: Adapters::OpenAICodex::ResponsesAdapter)
162
-
163
- # Backward-compatible aliases (deprecated)
164
- ProviderRegistry.register("anthropic_apikey_messages",
165
- client: Clients::Anthropic,
166
- adapter: Adapters::Anthropic::MessagesAdapter)
167
-
168
- ProviderRegistry.register("openai_apikey_completions",
169
- client: Clients::OpenAI,
170
- adapter: Adapters::OpenAI::ChatCompletionsAdapter)
171
-
172
- ProviderRegistry.register("openai_apikey_responses",
173
- client: Clients::OpenAI,
174
- adapter: Adapters::OpenAI::ResponsesAdapter)
175
-
176
- ProviderRegistry.register("groq_apikey_completions",
177
- client: Clients::Groq,
178
- adapter: Adapters::Groq::ChatCompletionsAdapter)
179
-
180
- ProviderRegistry.register("openai_oauth_codex",
181
- client: Clients::OpenAI,
182
- adapter: Adapters::OpenAICodex::ResponsesAdapter)
183
159
  end