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.
- checksums.yaml +4 -4
- data/.pi/skills/live-provider-testing/SKILL.md +183 -0
- data/.pi/skills/options-development/SKILL.md +131 -0
- data/CHANGELOG.md +43 -0
- data/README.md +110 -41
- data/Rakefile +1 -0
- data/docs/migration_guide_0.6.0.md +386 -0
- data/lib/llm_gateway/adapters/adapter.rb +8 -44
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
- data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +59 -47
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +336 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
- data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +193 -170
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +106 -275
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
- data/lib/llm_gateway/adapters/stream_mapper.rb +57 -0
- data/lib/llm_gateway/adapters/structs.rb +102 -52
- data/lib/llm_gateway/base_client.rb +2 -4
- data/lib/llm_gateway/client.rb +10 -66
- data/lib/llm_gateway/clients/anthropic.rb +5 -4
- data/lib/llm_gateway/clients/groq.rb +18 -4
- data/lib/llm_gateway/clients/openai.rb +20 -18
- data/lib/llm_gateway/prompt.rb +35 -17
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +5 -29
- metadata +8 -10
- data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
- data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
- data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
- data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
- data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
- data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
- data/scripts/generate_handoff_live_fixture.rb +0 -169
- 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
|
|
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
|
-
|
|
128
|
-
|
|
183
|
+
class AssistantStreamMessageEvent < BaseStruct
|
|
184
|
+
EventType = Types::Coercible::Symbol.enum(:message_start, :message_delta)
|
|
129
185
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
attr_reader :api_key, :model_key, :base_endpoint
|
|
9
|
+
attr_reader :api_key, :base_endpoint
|
|
11
10
|
|
|
12
|
-
def initialize(
|
|
13
|
-
@model_key = model_key
|
|
11
|
+
def initialize(api_key:)
|
|
14
12
|
@api_key = api_key
|
|
15
13
|
end
|
|
16
14
|
|
data/lib/llm_gateway/client.rb
CHANGED
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
|
|
3
4
|
module LlmGateway
|
|
4
5
|
class Client
|
|
5
|
-
def self.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
14
|
+
def initialize(api_key: ENV["ANTHROPIC_API_KEY"])
|
|
14
15
|
@base_endpoint = "https://api.anthropic.com/v1"
|
|
15
|
-
super(
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
120
|
+
model: model,
|
|
119
121
|
instructions: instructions,
|
|
120
122
|
input: messages,
|
|
121
123
|
store: false,
|
data/lib/llm_gateway/prompt.rb
CHANGED
|
@@ -2,7 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
module LlmGateway
|
|
4
4
|
class Prompt
|
|
5
|
-
|
|
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
|
-
@
|
|
33
|
-
@
|
|
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 =
|
|
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
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
data/lib/llm_gateway/version.rb
CHANGED
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
|