llm_gateway 0.5.0 → 0.7.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/CHANGELOG.md +38 -0
- data/README.md +350 -43
- data/docs/migration_guide_0.6.0.md +386 -0
- data/docs/migration_guide_0.7.0.md +193 -0
- data/lib/llm_gateway/adapters/adapter.rb +8 -11
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +24 -0
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +61 -11
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/input_message_sanitizer.rb +98 -7
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +132 -39
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +40 -16
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +47 -31
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +173 -24
- data/lib/llm_gateway/adapters/stream_mapper.rb +9 -2
- data/lib/llm_gateway/adapters/structs.rb +140 -55
- data/lib/llm_gateway/agents/event.rb +105 -0
- data/lib/llm_gateway/agents/file_session_manager.rb +100 -0
- data/lib/llm_gateway/agents/harness.rb +176 -0
- data/lib/llm_gateway/agents/in_memory_session_manager.rb +222 -0
- data/lib/llm_gateway/agents/tools/bash_tool.rb +132 -0
- data/lib/llm_gateway/agents/tools/edit_tool.rb +215 -0
- data/lib/llm_gateway/agents/tools/read_tool.rb +143 -0
- data/lib/llm_gateway/agents/tools/tool_utils.rb +164 -0
- data/lib/llm_gateway/agents/tools/write_tool.rb +34 -0
- data/lib/llm_gateway/base_client.rb +5 -7
- data/lib/llm_gateway/clients/anthropic.rb +10 -9
- data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +2 -2
- data/lib/llm_gateway/clients/groq.rb +8 -6
- data/lib/llm_gateway/clients/openai.rb +22 -20
- data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +4 -4
- data/lib/llm_gateway/prompt.rb +107 -52
- data/lib/llm_gateway/utils.rb +116 -13
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +7 -21
- metadata +13 -2
data/lib/llm_gateway/prompt.rb
CHANGED
|
@@ -2,94 +2,149 @@
|
|
|
2
2
|
|
|
3
3
|
module LlmGateway
|
|
4
4
|
class Prompt
|
|
5
|
-
|
|
5
|
+
class_attribute :provider, :model, :reasoning
|
|
6
|
+
class_attribute :before_execute_callbacks, :after_execute_callbacks, instance_accessor: false, default: []
|
|
7
|
+
attr_accessor :cache_key, :cache_retention
|
|
6
8
|
|
|
7
9
|
def self.before_execute(*methods, &block)
|
|
8
|
-
before_execute_callbacks
|
|
9
|
-
before_execute_callbacks
|
|
10
|
+
self.before_execute_callbacks += methods
|
|
11
|
+
self.before_execute_callbacks += [ block ] if block_given?
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def self.after_execute(*methods, &block)
|
|
13
|
-
after_execute_callbacks
|
|
14
|
-
after_execute_callbacks
|
|
15
|
+
self.after_execute_callbacks += methods
|
|
16
|
+
self.after_execute_callbacks += [ block ] if block_given?
|
|
15
17
|
end
|
|
16
18
|
|
|
17
|
-
def
|
|
18
|
-
@
|
|
19
|
+
def initialize(provider: nil, model: nil, reasoning: nil, cache_key: nil, cache_retention: nil)
|
|
20
|
+
@provider = provider || self.class.provider
|
|
21
|
+
@model = model || self.class.model
|
|
22
|
+
@reasoning = reasoning || self.class.reasoning
|
|
23
|
+
@cache_key = cache_key
|
|
24
|
+
@cache_retention = cache_retention
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
def
|
|
22
|
-
|
|
27
|
+
def run(provider: nil, model: nil, reasoning: nil, **options, &block)
|
|
28
|
+
# Resolve the prompt once so dynamic or expensive prompt builders are not
|
|
29
|
+
# evaluated multiple times during a single run.
|
|
30
|
+
input = prompt
|
|
31
|
+
|
|
32
|
+
run_callbacks(:before_execute, input)
|
|
33
|
+
|
|
34
|
+
response = run_tool_loop(input, provider: resolved_provider(provider), model: model, reasoning: reasoning, **options, &block)
|
|
35
|
+
|
|
36
|
+
run_callbacks(:after_execute, response)
|
|
37
|
+
|
|
38
|
+
response
|
|
23
39
|
end
|
|
24
40
|
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
41
|
+
def stream(input = prompt, provider: nil, model: nil, reasoning: nil, **options, &block)
|
|
42
|
+
stream_provider = resolved_provider(provider)
|
|
43
|
+
stream_options = default_stream_options(model: model, reasoning: reasoning).merge(options)
|
|
44
|
+
|
|
45
|
+
stream_provider.stream(input, **stream_options, &block)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.tools
|
|
49
|
+
const_defined?(:TOOLS, false) ? self::TOOLS : []
|
|
29
50
|
end
|
|
30
51
|
|
|
31
|
-
def
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
def self.find_tool(name)
|
|
53
|
+
tools.find { |tool| tool.tool_name == name }
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tools
|
|
57
|
+
self.class.tools.map(&:definition)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def system_prompt
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def find_and_execute_tool(tool_request)
|
|
67
|
+
tool_name = tool_request.name
|
|
68
|
+
tool_input = tool_request.input
|
|
69
|
+
tool_class = self.class.find_tool(tool_name)
|
|
70
|
+
|
|
71
|
+
result = begin
|
|
72
|
+
if tool_class
|
|
73
|
+
execute_tool(tool_class, tool_input)
|
|
74
|
+
else
|
|
75
|
+
"Unknown tool: #{tool_name}"
|
|
36
76
|
end
|
|
37
|
-
|
|
38
|
-
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
"Error executing tool: #{e.message}"
|
|
39
79
|
end
|
|
80
|
+
ToolResult.new(
|
|
81
|
+
type: "tool_result",
|
|
82
|
+
tool_use_id: tool_request.id,
|
|
83
|
+
content: result,
|
|
84
|
+
)
|
|
40
85
|
end
|
|
41
86
|
|
|
42
|
-
def
|
|
43
|
-
|
|
87
|
+
def execute_tool(tool_class, tool_input)
|
|
88
|
+
tool_class.new.execute(tool_input)
|
|
89
|
+
end
|
|
44
90
|
|
|
45
|
-
|
|
91
|
+
def run_tool_loop(input, provider: nil, model: nil, reasoning: nil, **options, &block)
|
|
92
|
+
response = stream(input, provider: provider, model: model, reasoning: reasoning, **options, &block)
|
|
46
93
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
response[:choices][0][:content]
|
|
94
|
+
while tool_requests(response).any?
|
|
95
|
+
input = prompt_with_tool_results(input, response, tool_requests(response))
|
|
96
|
+
response = stream(input, provider: provider, model: model, reasoning: reasoning, **options, &block)
|
|
51
97
|
end
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
else
|
|
56
|
-
response_content
|
|
57
|
-
end
|
|
99
|
+
response
|
|
100
|
+
end
|
|
58
101
|
|
|
59
|
-
|
|
102
|
+
def tool_requests(response)
|
|
103
|
+
return [] unless response.respond_to?(:content)
|
|
60
104
|
|
|
61
|
-
|
|
105
|
+
response.content.select { |content| content.respond_to?(:type) && content.type == "tool_use" }
|
|
62
106
|
end
|
|
63
107
|
|
|
64
|
-
def
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
108
|
+
def prompt_with_tool_results(input, response, requests)
|
|
109
|
+
messages = input.is_a?(Array) ? input.dup : [ { role: "user", content: input } ]
|
|
110
|
+
messages << response.to_h
|
|
111
|
+
messages << {
|
|
112
|
+
role: "user",
|
|
113
|
+
content: requests.map { |request| find_and_execute_tool(request).to_h }
|
|
114
|
+
}
|
|
115
|
+
messages
|
|
70
116
|
end
|
|
71
117
|
|
|
72
|
-
def
|
|
73
|
-
|
|
118
|
+
def default_stream_options(model: nil, reasoning: nil)
|
|
119
|
+
{
|
|
120
|
+
tools: tools,
|
|
121
|
+
system: system_prompt,
|
|
122
|
+
model: resolved_model(model),
|
|
123
|
+
reasoning: resolved_reasoning(reasoning),
|
|
124
|
+
cache_key: cache_key,
|
|
125
|
+
cache_retention: cache_retention
|
|
126
|
+
}.compact
|
|
74
127
|
end
|
|
75
128
|
|
|
76
|
-
def
|
|
77
|
-
|
|
129
|
+
def resolved_provider(provider)
|
|
130
|
+
provider || self.provider
|
|
78
131
|
end
|
|
79
132
|
|
|
80
|
-
def
|
|
81
|
-
|
|
133
|
+
def resolved_model(model)
|
|
134
|
+
model || self.model
|
|
82
135
|
end
|
|
83
136
|
|
|
84
|
-
|
|
137
|
+
def resolved_reasoning(reasoning)
|
|
138
|
+
reasoning || self.reasoning
|
|
139
|
+
end
|
|
85
140
|
|
|
86
141
|
def run_callbacks(callback_type, *args)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
142
|
+
self.class.public_send("#{callback_type}_callbacks").each do |callback|
|
|
143
|
+
case callback
|
|
144
|
+
when Proc
|
|
90
145
|
instance_exec(*args, &callback)
|
|
91
|
-
|
|
92
|
-
|
|
146
|
+
when Symbol, String
|
|
147
|
+
public_send(callback, *args)
|
|
93
148
|
end
|
|
94
149
|
end
|
|
95
150
|
end
|
data/lib/llm_gateway/utils.rb
CHANGED
|
@@ -4,16 +4,20 @@ module LlmGateway
|
|
|
4
4
|
module Utils
|
|
5
5
|
module_function
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
|
|
7
|
+
def symbolize_keys(hash)
|
|
8
|
+
hash.to_h.transform_keys { |key| key.respond_to?(:to_sym) ? key.to_sym : key }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def deep_symbolize_keys(value)
|
|
12
|
+
case value
|
|
9
13
|
when Hash
|
|
10
|
-
|
|
11
|
-
result[key
|
|
14
|
+
value.each_with_object({}) do |(key, nested_value), result|
|
|
15
|
+
result[symbolize_key(key)] = deep_symbolize_keys(nested_value)
|
|
12
16
|
end
|
|
13
17
|
when Array
|
|
14
|
-
|
|
18
|
+
value.map { |item| deep_symbolize_keys(item) }
|
|
15
19
|
else
|
|
16
|
-
|
|
20
|
+
value
|
|
17
21
|
end
|
|
18
22
|
end
|
|
19
23
|
|
|
@@ -21,19 +25,118 @@ module LlmGateway
|
|
|
21
25
|
!blank?(value)
|
|
22
26
|
end
|
|
23
27
|
|
|
28
|
+
def presence(value)
|
|
29
|
+
present?(value) ? value : nil
|
|
30
|
+
end
|
|
31
|
+
|
|
24
32
|
def blank?(value)
|
|
25
33
|
case value
|
|
26
|
-
when nil
|
|
34
|
+
when nil, false
|
|
27
35
|
true
|
|
28
|
-
when
|
|
29
|
-
value.strip.empty?
|
|
30
|
-
when Array, Hash
|
|
31
|
-
value.empty?
|
|
32
|
-
when Numeric
|
|
36
|
+
when true, Numeric
|
|
33
37
|
false
|
|
38
|
+
when String
|
|
39
|
+
value.match?(/\A[[:space:]]*\z/)
|
|
34
40
|
else
|
|
35
|
-
value.respond_to?(:empty?) ? value.empty? : false
|
|
41
|
+
value.respond_to?(:empty?) ? !!value.empty? : false
|
|
36
42
|
end
|
|
37
43
|
end
|
|
44
|
+
|
|
45
|
+
def symbolize_key(key)
|
|
46
|
+
key.respond_to?(:to_sym) ? key.to_sym : key
|
|
47
|
+
rescue StandardError
|
|
48
|
+
key
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
unless Class.method_defined?(:class_attribute)
|
|
54
|
+
class Class
|
|
55
|
+
def class_attribute(*names, instance_accessor: true, instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
|
|
56
|
+
names.each do |name|
|
|
57
|
+
ivar = :"@#{name}"
|
|
58
|
+
instance_variable_set(ivar, default)
|
|
59
|
+
|
|
60
|
+
unset = Object.new
|
|
61
|
+
|
|
62
|
+
define_singleton_method(name) do |value = unset|
|
|
63
|
+
unless value.equal?(unset)
|
|
64
|
+
instance_variable_set(ivar, value)
|
|
65
|
+
next value
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if instance_variable_defined?(ivar)
|
|
69
|
+
instance_variable_get(ivar)
|
|
70
|
+
elsif superclass.respond_to?(name)
|
|
71
|
+
superclass.public_send(name)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
define_singleton_method("#{name}=") do |value|
|
|
76
|
+
instance_variable_set(ivar, value)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
|
|
80
|
+
|
|
81
|
+
if instance_reader
|
|
82
|
+
define_method(name) do
|
|
83
|
+
if instance_variable_defined?(ivar)
|
|
84
|
+
instance_variable_get(ivar)
|
|
85
|
+
else
|
|
86
|
+
self.class.public_send(name)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
define_method("#{name}=") { |value| instance_variable_set(ivar, value) } if instance_writer
|
|
92
|
+
|
|
93
|
+
define_method("#{name}?") { !!public_send(name) } if instance_predicate
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class Object
|
|
100
|
+
def blank?
|
|
101
|
+
LlmGateway::Utils.blank?(self)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def present?
|
|
105
|
+
LlmGateway::Utils.present?(self)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def presence
|
|
109
|
+
LlmGateway::Utils.presence(self)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
class Hash
|
|
114
|
+
def symbolize_keys
|
|
115
|
+
transform_keys { |key| LlmGateway::Utils.symbolize_key(key) }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def symbolize_keys!
|
|
119
|
+
replace(symbolize_keys)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def deep_symbolize_keys
|
|
123
|
+
LlmGateway::Utils.deep_symbolize_keys(self)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def deep_symbolize_keys!
|
|
127
|
+
replace(deep_symbolize_keys)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
unless method_defined?(:except)
|
|
131
|
+
def except(*keys)
|
|
132
|
+
reject { |key, _| keys.include?(key) }
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
unless method_defined?(:except!)
|
|
137
|
+
def except!(*keys)
|
|
138
|
+
keys.each { |key| delete(key) }
|
|
139
|
+
self
|
|
140
|
+
end
|
|
38
141
|
end
|
|
39
142
|
end
|
data/lib/llm_gateway/version.rb
CHANGED
data/lib/llm_gateway.rb
CHANGED
|
@@ -7,6 +7,10 @@ require_relative "llm_gateway/base_client"
|
|
|
7
7
|
require_relative "llm_gateway/client"
|
|
8
8
|
require_relative "llm_gateway/prompt"
|
|
9
9
|
require_relative "llm_gateway/tool"
|
|
10
|
+
require_relative "llm_gateway/agents/event"
|
|
11
|
+
require_relative "llm_gateway/agents/in_memory_session_manager"
|
|
12
|
+
require_relative "llm_gateway/agents/file_session_manager"
|
|
13
|
+
require_relative "llm_gateway/agents/harness"
|
|
10
14
|
|
|
11
15
|
# Load clients - order matters for inheritance
|
|
12
16
|
require_relative "llm_gateway/clients/anthropic"
|
|
@@ -100,6 +104,9 @@ module LlmGateway
|
|
|
100
104
|
def self.build_provider(config)
|
|
101
105
|
config = config.transform_keys(&:to_sym)
|
|
102
106
|
provider_name = config.delete(:provider)
|
|
107
|
+
if config.key?(:model_key)
|
|
108
|
+
raise ArgumentError, "model_key is no longer a provider option; pass model: to chat/stream instead"
|
|
109
|
+
end
|
|
103
110
|
entry = ProviderRegistry.resolve(provider_name)
|
|
104
111
|
|
|
105
112
|
client = entry[:client].new(**config)
|
|
@@ -153,25 +160,4 @@ module LlmGateway
|
|
|
153
160
|
ProviderRegistry.register("openai_codex",
|
|
154
161
|
client: Clients::OpenAI,
|
|
155
162
|
adapter: Adapters::OpenAICodex::ResponsesAdapter)
|
|
156
|
-
|
|
157
|
-
# Backward-compatible aliases (deprecated)
|
|
158
|
-
ProviderRegistry.register("anthropic_apikey_messages",
|
|
159
|
-
client: Clients::Anthropic,
|
|
160
|
-
adapter: Adapters::Anthropic::MessagesAdapter)
|
|
161
|
-
|
|
162
|
-
ProviderRegistry.register("openai_apikey_completions",
|
|
163
|
-
client: Clients::OpenAI,
|
|
164
|
-
adapter: Adapters::OpenAI::ChatCompletionsAdapter)
|
|
165
|
-
|
|
166
|
-
ProviderRegistry.register("openai_apikey_responses",
|
|
167
|
-
client: Clients::OpenAI,
|
|
168
|
-
adapter: Adapters::OpenAI::ResponsesAdapter)
|
|
169
|
-
|
|
170
|
-
ProviderRegistry.register("groq_apikey_completions",
|
|
171
|
-
client: Clients::Groq,
|
|
172
|
-
adapter: Adapters::Groq::ChatCompletionsAdapter)
|
|
173
|
-
|
|
174
|
-
ProviderRegistry.register("openai_oauth_codex",
|
|
175
|
-
client: Clients::OpenAI,
|
|
176
|
-
adapter: Adapters::OpenAICodex::ResponsesAdapter)
|
|
177
163
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: llm_gateway
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- billybonks
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|
|
@@ -42,6 +42,8 @@ files:
|
|
|
42
42
|
- README.md
|
|
43
43
|
- Rakefile
|
|
44
44
|
- docs/migration-guide.md
|
|
45
|
+
- docs/migration_guide_0.6.0.md
|
|
46
|
+
- docs/migration_guide_0.7.0.md
|
|
45
47
|
- lib/llm_gateway.rb
|
|
46
48
|
- lib/llm_gateway/adapters/adapter.rb
|
|
47
49
|
- lib/llm_gateway/adapters/anthropic/acts_like_messages.rb
|
|
@@ -74,6 +76,15 @@ files:
|
|
|
74
76
|
- lib/llm_gateway/adapters/option_mapper.rb
|
|
75
77
|
- lib/llm_gateway/adapters/stream_mapper.rb
|
|
76
78
|
- lib/llm_gateway/adapters/structs.rb
|
|
79
|
+
- lib/llm_gateway/agents/event.rb
|
|
80
|
+
- lib/llm_gateway/agents/file_session_manager.rb
|
|
81
|
+
- lib/llm_gateway/agents/harness.rb
|
|
82
|
+
- lib/llm_gateway/agents/in_memory_session_manager.rb
|
|
83
|
+
- lib/llm_gateway/agents/tools/bash_tool.rb
|
|
84
|
+
- lib/llm_gateway/agents/tools/edit_tool.rb
|
|
85
|
+
- lib/llm_gateway/agents/tools/read_tool.rb
|
|
86
|
+
- lib/llm_gateway/agents/tools/tool_utils.rb
|
|
87
|
+
- lib/llm_gateway/agents/tools/write_tool.rb
|
|
77
88
|
- lib/llm_gateway/base_client.rb
|
|
78
89
|
- lib/llm_gateway/client.rb
|
|
79
90
|
- lib/llm_gateway/clients/anthropic.rb
|