llm_gateway 0.6.0 → 0.8.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 +20 -0
- data/README.md +255 -1
- data/docs/migration_guide_0.7.0.md +193 -0
- data/lib/llm_gateway/adapters/adapter.rb +30 -12
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +24 -0
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +31 -8
- 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 +48 -16
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +1 -1
- 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 +131 -3
- data/lib/llm_gateway/adapters/structs.rb +45 -10
- 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 +3 -3
- data/lib/llm_gateway/clients/anthropic.rb +5 -5
- data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +2 -2
- data/lib/llm_gateway/clients/openai.rb +2 -2
- data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +4 -4
- data/lib/llm_gateway/prompt.rb +105 -68
- data/lib/llm_gateway/proxy/adapter.rb +48 -0
- data/lib/llm_gateway/proxy/client.rb +85 -0
- data/lib/llm_gateway/proxy/server.rb +65 -0
- data/lib/llm_gateway/utils.rb +116 -13
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +12 -1
- metadata +15 -2
data/lib/llm_gateway/prompt.rb
CHANGED
|
@@ -2,112 +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)
|
|
10
|
+
self.before_execute_callbacks += methods
|
|
11
|
+
self.before_execute_callbacks += [ block ] if block_given?
|
|
12
|
+
end
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
end
|
|
14
|
+
def self.after_execute(*methods, &block)
|
|
15
|
+
self.after_execute_callbacks += methods
|
|
16
|
+
self.after_execute_callbacks += [ block ] if block_given?
|
|
17
|
+
end
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
25
|
+
end
|
|
18
26
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
@model = value
|
|
26
|
-
end
|
|
27
|
-
end
|
|
32
|
+
run_callbacks(:before_execute, input)
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
32
39
|
end
|
|
33
40
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
37
46
|
end
|
|
38
47
|
|
|
39
|
-
def self.
|
|
40
|
-
|
|
48
|
+
def self.tools
|
|
49
|
+
const_defined?(:TOOLS, false) ? self::TOOLS : []
|
|
41
50
|
end
|
|
42
51
|
|
|
43
|
-
def self.
|
|
44
|
-
|
|
52
|
+
def self.find_tool(name)
|
|
53
|
+
tools.find { |tool| tool.tool_name == name }
|
|
45
54
|
end
|
|
46
55
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
subclass.instance_variable_set(:@before_execute_callbacks, before_execute_callbacks.dup)
|
|
50
|
-
subclass.instance_variable_set(:@after_execute_callbacks, after_execute_callbacks.dup)
|
|
51
|
-
subclass.provider = provider
|
|
52
|
-
subclass.model = model
|
|
56
|
+
def tools
|
|
57
|
+
self.class.tools.map(&:definition)
|
|
53
58
|
end
|
|
54
59
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
@model = model || self.class.model
|
|
60
|
+
def system_prompt
|
|
61
|
+
nil
|
|
58
62
|
end
|
|
59
63
|
|
|
60
|
-
|
|
61
|
-
run_callbacks(:before_execute, prompt)
|
|
64
|
+
private
|
|
62
65
|
|
|
63
|
-
|
|
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)
|
|
64
70
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
result = begin
|
|
72
|
+
if tool_class
|
|
73
|
+
execute_tool(tool_class, tool_input)
|
|
74
|
+
else
|
|
75
|
+
"Unknown tool: #{tool_name}"
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
"Error executing tool: #{e.message}"
|
|
69
79
|
end
|
|
80
|
+
ToolResult.new(
|
|
81
|
+
type: "tool_result",
|
|
82
|
+
tool_use_id: tool_request.id,
|
|
83
|
+
content: result,
|
|
84
|
+
)
|
|
85
|
+
end
|
|
70
86
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
87
|
+
def execute_tool(tool_class, tool_input)
|
|
88
|
+
tool_class.new.execute(tool_input)
|
|
89
|
+
end
|
|
90
|
+
|
|
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)
|
|
76
93
|
|
|
77
|
-
|
|
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)
|
|
97
|
+
end
|
|
78
98
|
|
|
79
|
-
|
|
99
|
+
response
|
|
80
100
|
end
|
|
81
101
|
|
|
82
|
-
def
|
|
83
|
-
|
|
84
|
-
stream_model = model || self.model
|
|
85
|
-
options[:model] = stream_model if stream_model
|
|
102
|
+
def tool_requests(response)
|
|
103
|
+
return [] unless response.respond_to?(:content)
|
|
86
104
|
|
|
87
|
-
|
|
105
|
+
response.content.select { |content| content.respond_to?(:type) && content.type == "tool_use" }
|
|
88
106
|
end
|
|
89
107
|
|
|
90
|
-
def
|
|
91
|
-
|
|
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
|
|
92
116
|
end
|
|
93
117
|
|
|
94
|
-
def
|
|
95
|
-
|
|
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
|
|
96
127
|
end
|
|
97
128
|
|
|
98
|
-
def
|
|
99
|
-
|
|
129
|
+
def resolved_provider(provider)
|
|
130
|
+
provider || self.provider
|
|
100
131
|
end
|
|
101
132
|
|
|
102
|
-
|
|
133
|
+
def resolved_model(model)
|
|
134
|
+
model || self.model
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def resolved_reasoning(reasoning)
|
|
138
|
+
reasoning || self.reasoning
|
|
139
|
+
end
|
|
103
140
|
|
|
104
141
|
def run_callbacks(callback_type, *args)
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
142
|
+
self.class.public_send("#{callback_type}_callbacks").each do |callback|
|
|
143
|
+
case callback
|
|
144
|
+
when Proc
|
|
108
145
|
instance_exec(*args, &callback)
|
|
109
|
-
|
|
110
|
-
|
|
146
|
+
when Symbol, String
|
|
147
|
+
public_send(callback, *args)
|
|
111
148
|
end
|
|
112
149
|
end
|
|
113
150
|
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LlmGateway
|
|
4
|
+
module Proxy
|
|
5
|
+
class Adapter
|
|
6
|
+
attr_reader :client
|
|
7
|
+
|
|
8
|
+
def initialize(client)
|
|
9
|
+
@client = client
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def stream(message, tools: nil, system: nil, **options, &block)
|
|
13
|
+
target_adapter = LlmGateway.build_provider(client.target_config.merge(provider: client.target_provider))
|
|
14
|
+
mapper_class = target_adapter.stream_mapper_class
|
|
15
|
+
raise LlmGateway::Errors::MissingMapperForProvider, "No stream_mapper configured" unless mapper_class
|
|
16
|
+
|
|
17
|
+
mapper = mapper_class.new(
|
|
18
|
+
provider: LlmGateway::Client.provider_id_from_client(target_adapter.client),
|
|
19
|
+
api: target_adapter.stream_api_name
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
client.stream(normalize_messages(message), tools: tools, system: normalize_system(system), **options) do |chunk|
|
|
23
|
+
mapper.map(chunk, &block)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
mapper.result
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def normalize_system(system)
|
|
32
|
+
if system.nil?
|
|
33
|
+
[]
|
|
34
|
+
elsif system.is_a?(String)
|
|
35
|
+
[ { role: "system", content: system } ]
|
|
36
|
+
elsif system.is_a?(Array)
|
|
37
|
+
system
|
|
38
|
+
else
|
|
39
|
+
raise ArgumentError, "System parameter must be a string or array, got #{system.class}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def normalize_messages(message)
|
|
44
|
+
message.is_a?(String) ? [ { role: "user", content: message } ] : message
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
|
|
7
|
+
module LlmGateway
|
|
8
|
+
module Proxy
|
|
9
|
+
class Client
|
|
10
|
+
attr_reader :url, :target_provider, :target_config, :path
|
|
11
|
+
|
|
12
|
+
def initialize(url:, target_provider:, target_config: {}, api_key: nil, path: "/agent/llm_proxy", **_options)
|
|
13
|
+
@url = url.to_s.sub(%r{/+\z}, "")
|
|
14
|
+
@target_provider = target_provider.to_s
|
|
15
|
+
@target_config = (target_config || {}).transform_keys(&:to_sym)
|
|
16
|
+
@api_key = api_key
|
|
17
|
+
@path = path.to_s.start_with?("/") ? path.to_s : "/#{path}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def stream(messages, tools: nil, system: nil, **options, &block)
|
|
21
|
+
uri = URI("#{url}#{path}")
|
|
22
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
23
|
+
http.use_ssl = uri.scheme == "https"
|
|
24
|
+
http.read_timeout = 480
|
|
25
|
+
http.open_timeout = 10
|
|
26
|
+
|
|
27
|
+
request = Net::HTTP::Post.new(uri)
|
|
28
|
+
request["content-type"] = "application/json"
|
|
29
|
+
request["accept"] = "text/event-stream"
|
|
30
|
+
request["accept-encoding"] = "identity"
|
|
31
|
+
request["authorization"] = "Bearer #{@api_key}" if @api_key
|
|
32
|
+
request.body = {
|
|
33
|
+
provider: target_provider,
|
|
34
|
+
config: target_config,
|
|
35
|
+
messages: messages,
|
|
36
|
+
system: system,
|
|
37
|
+
tools: tools,
|
|
38
|
+
options: options
|
|
39
|
+
}.to_json
|
|
40
|
+
|
|
41
|
+
http.request(request) do |response|
|
|
42
|
+
unless response.code.to_i == 200
|
|
43
|
+
body = +""
|
|
44
|
+
response.read_body { |chunk| body << chunk }
|
|
45
|
+
raise Errors::APIStatusError.new("Proxy request failed with status #{response.code}: #{body}", nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
parse_sse_stream(response, &block)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def parse_sse_stream(response)
|
|
55
|
+
buffer = +""
|
|
56
|
+
response.read_body do |chunk|
|
|
57
|
+
buffer << chunk
|
|
58
|
+
while (idx = buffer.index("\n\n"))
|
|
59
|
+
raw_event = buffer.slice!(0, idx + 2)
|
|
60
|
+
event_type = nil
|
|
61
|
+
data_lines = []
|
|
62
|
+
|
|
63
|
+
raw_event.each_line do |line|
|
|
64
|
+
line = line.chomp
|
|
65
|
+
event_type = line.sub(/^event:\s*/, "") if line.start_with?("event:")
|
|
66
|
+
data_lines << line.sub(/^data:\s*/, "") if line.start_with?("data:")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
next if data_lines.empty?
|
|
70
|
+
|
|
71
|
+
data_str = data_lines.join("\n")
|
|
72
|
+
next if data_str == "[DONE]"
|
|
73
|
+
|
|
74
|
+
data = begin
|
|
75
|
+
JSON.parse(data_str).deep_symbolize_keys
|
|
76
|
+
rescue JSON::ParserError
|
|
77
|
+
{ raw: data_str }
|
|
78
|
+
end
|
|
79
|
+
yield({ event: event_type, data: data })
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module LlmGateway
|
|
6
|
+
module Proxy
|
|
7
|
+
class Server
|
|
8
|
+
PATH = "/agent/llm_proxy"
|
|
9
|
+
|
|
10
|
+
def call(env)
|
|
11
|
+
return not_found unless env["REQUEST_METHOD"] == "POST" && env["PATH_INFO"] == PATH
|
|
12
|
+
|
|
13
|
+
request = JSON.parse(env["rack.input"].read).deep_symbolize_keys
|
|
14
|
+
options = request[:options] || {}
|
|
15
|
+
options = options.merge(model: request[:model]) if request.key?(:model)
|
|
16
|
+
adapter = build_adapter(request)
|
|
17
|
+
|
|
18
|
+
body = Enumerator.new do |yielder|
|
|
19
|
+
adapter.raw_stream(
|
|
20
|
+
request[:messages],
|
|
21
|
+
system: request[:system],
|
|
22
|
+
tools: request[:tools],
|
|
23
|
+
**options
|
|
24
|
+
) do |chunk|
|
|
25
|
+
yielder << encode_sse(chunk)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
[ 200, { "content-type" => "text/event-stream", "cache-control" => "no-cache" }, body ]
|
|
30
|
+
rescue KeyError, JSON::ParserError, ArgumentError => e
|
|
31
|
+
json_error(400, e.message)
|
|
32
|
+
rescue Errors::UnsupportedProvider => e
|
|
33
|
+
json_error(404, e.message)
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
json_error(500, e.message)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_adapter(request)
|
|
41
|
+
provider = request.fetch(:provider)
|
|
42
|
+
config = (request[:config] || {}).merge(provider: provider)
|
|
43
|
+
|
|
44
|
+
LlmGateway.build_provider(config)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def encode_sse(chunk)
|
|
48
|
+
event = chunk[:event]
|
|
49
|
+
data = chunk[:data]
|
|
50
|
+
out = +""
|
|
51
|
+
out << "event: #{event}\n" if event
|
|
52
|
+
JSON.generate(data).each_line { |line| out << "data: #{line.chomp}\n" }
|
|
53
|
+
out << "\n"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def json_error(status, message)
|
|
57
|
+
[ status, { "content-type" => "application/json" }, [ { error: message }.to_json ] ]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def not_found
|
|
61
|
+
[ 404, { "content-type" => "application/json" }, [ { error: "Not found" }.to_json ] ]
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
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"
|
|
@@ -44,6 +48,9 @@ require_relative "llm_gateway/adapters/groq/chat_completions_adapter"
|
|
|
44
48
|
|
|
45
49
|
# Load provider registry
|
|
46
50
|
require_relative "llm_gateway/provider_registry"
|
|
51
|
+
require_relative "llm_gateway/proxy/client"
|
|
52
|
+
require_relative "llm_gateway/proxy/adapter"
|
|
53
|
+
require_relative "llm_gateway/proxy/server"
|
|
47
54
|
|
|
48
55
|
module LlmGateway
|
|
49
56
|
class Error < StandardError; end
|
|
@@ -106,7 +113,7 @@ module LlmGateway
|
|
|
106
113
|
entry = ProviderRegistry.resolve(provider_name)
|
|
107
114
|
|
|
108
115
|
client = entry[:client].new(**config)
|
|
109
|
-
entry[:adapter].new(client)
|
|
116
|
+
entry[:adapter].new(client, provider_key: provider_name)
|
|
110
117
|
end
|
|
111
118
|
|
|
112
119
|
def self.configure(configs)
|
|
@@ -156,4 +163,8 @@ module LlmGateway
|
|
|
156
163
|
ProviderRegistry.register("openai_codex",
|
|
157
164
|
client: Clients::OpenAI,
|
|
158
165
|
adapter: Adapters::OpenAICodex::ResponsesAdapter)
|
|
166
|
+
|
|
167
|
+
ProviderRegistry.register("proxy",
|
|
168
|
+
client: Proxy::Client,
|
|
169
|
+
adapter: Proxy::Adapter)
|
|
159
170
|
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.8.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-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|
|
@@ -43,6 +43,7 @@ files:
|
|
|
43
43
|
- Rakefile
|
|
44
44
|
- docs/migration-guide.md
|
|
45
45
|
- docs/migration_guide_0.6.0.md
|
|
46
|
+
- docs/migration_guide_0.7.0.md
|
|
46
47
|
- lib/llm_gateway.rb
|
|
47
48
|
- lib/llm_gateway/adapters/adapter.rb
|
|
48
49
|
- lib/llm_gateway/adapters/anthropic/acts_like_messages.rb
|
|
@@ -75,6 +76,15 @@ files:
|
|
|
75
76
|
- lib/llm_gateway/adapters/option_mapper.rb
|
|
76
77
|
- lib/llm_gateway/adapters/stream_mapper.rb
|
|
77
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
|
|
78
88
|
- lib/llm_gateway/base_client.rb
|
|
79
89
|
- lib/llm_gateway/client.rb
|
|
80
90
|
- lib/llm_gateway/clients/anthropic.rb
|
|
@@ -87,6 +97,9 @@ files:
|
|
|
87
97
|
- lib/llm_gateway/errors.rb
|
|
88
98
|
- lib/llm_gateway/prompt.rb
|
|
89
99
|
- lib/llm_gateway/provider_registry.rb
|
|
100
|
+
- lib/llm_gateway/proxy/adapter.rb
|
|
101
|
+
- lib/llm_gateway/proxy/client.rb
|
|
102
|
+
- lib/llm_gateway/proxy/server.rb
|
|
90
103
|
- lib/llm_gateway/tool.rb
|
|
91
104
|
- lib/llm_gateway/utils.rb
|
|
92
105
|
- lib/llm_gateway/version.rb
|