llm_gateway 0.7.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 +8 -0
- data/lib/llm_gateway/adapters/adapter.rb +29 -11
- 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/version.rb +1 -1
- data/lib/llm_gateway.rb +8 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5cd3b8b829adb3e5793ac604aa50c40bdd94546a5c2fc0b90125ca9a75efe4c2
|
|
4
|
+
data.tar.gz: 28e7211450893a23c2116d1cf96010fe5dd359c6203ecbde6802dda89f06f153
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f51eb7792fd0886403c9d3fec068dcd8f00d50c35df747e6735b470ccc170e8129bc8fe5ef18b8cd117a763e95b8cf9bc76c54364ba5c8b5f284f3e0b5ef69a5
|
|
7
|
+
data.tar.gz: c0de665f7e70b60814c8ea8185b34205de309736d4c578fee1d48de4990d76b4d437c1c1b74672d7f57c67b115e244e421f4b17882ea34cb875dcd5ed31dec59
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.8.0](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.8.0) (2026-06-06)
|
|
4
|
+
|
|
5
|
+
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.7.0...v0.8.0)
|
|
6
|
+
|
|
7
|
+
**Merged pull requests:**
|
|
8
|
+
|
|
9
|
+
- feat: create server proxy and corresponding client [\#89](https://github.com/Hyper-Unearthing/llm_gateway/pull/89) ([billybonks](https://github.com/billybonks))
|
|
10
|
+
|
|
3
11
|
## [v0.7.0](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.7.0) (2026-06-03)
|
|
4
12
|
|
|
5
13
|
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.6.0...v0.7.0)
|
|
@@ -5,32 +5,38 @@ require_relative "structs"
|
|
|
5
5
|
module LlmGateway
|
|
6
6
|
module Adapters
|
|
7
7
|
class Adapter
|
|
8
|
-
attr_reader :client
|
|
8
|
+
attr_reader :client, :provider_key
|
|
9
9
|
|
|
10
|
-
def initialize(client)
|
|
10
|
+
def initialize(client, provider_key: nil)
|
|
11
11
|
@client = client
|
|
12
|
+
@provider_key = provider_key
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
def
|
|
15
|
-
raise LlmGateway::Errors::MissingMapperForProvider, "No stream_mapper configured" unless stream_mapper
|
|
16
|
-
|
|
15
|
+
def raw_stream(message, tools: nil, system: nil, **options, &block)
|
|
17
16
|
normalized_input = map_input({
|
|
18
17
|
messages: sanitize_messages(normalize_messages(message), target_model: options[:model]),
|
|
19
18
|
tools: tools,
|
|
20
19
|
system: normalize_system(system)
|
|
21
20
|
})
|
|
22
21
|
|
|
22
|
+
perform_stream(
|
|
23
|
+
normalized_input[:messages],
|
|
24
|
+
tools: normalized_input[:tools],
|
|
25
|
+
system: normalized_input[:system],
|
|
26
|
+
**map_options(options),
|
|
27
|
+
&block
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def stream(message, tools: nil, system: nil, **options, &block)
|
|
32
|
+
raise LlmGateway::Errors::MissingMapperForProvider, "No stream_mapper configured" unless stream_mapper
|
|
33
|
+
|
|
23
34
|
mapper = stream_mapper.new(
|
|
24
35
|
provider: LlmGateway::Client.provider_id_from_client(client),
|
|
25
36
|
api: api_name
|
|
26
37
|
)
|
|
27
38
|
|
|
28
|
-
|
|
29
|
-
normalized_input[:messages],
|
|
30
|
-
tools: normalized_input[:tools],
|
|
31
|
-
system: normalized_input[:system],
|
|
32
|
-
**map_options(options)
|
|
33
|
-
) do |chunk|
|
|
39
|
+
raw_stream(message, tools: tools, system: system, **options) do |chunk|
|
|
34
40
|
mapper.map(chunk, &block)
|
|
35
41
|
end
|
|
36
42
|
|
|
@@ -89,6 +95,18 @@ module LlmGateway
|
|
|
89
95
|
client.stream(messages, tools: tools, system: system, **options, &block)
|
|
90
96
|
end
|
|
91
97
|
|
|
98
|
+
public
|
|
99
|
+
|
|
100
|
+
def stream_api_name
|
|
101
|
+
api_name
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def stream_mapper_class
|
|
105
|
+
stream_mapper
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private
|
|
109
|
+
|
|
92
110
|
def api_name
|
|
93
111
|
self.class.name.split("::").last.gsub(/Adapter$/, "").downcase
|
|
94
112
|
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/version.rb
CHANGED
data/lib/llm_gateway.rb
CHANGED
|
@@ -48,6 +48,9 @@ require_relative "llm_gateway/adapters/groq/chat_completions_adapter"
|
|
|
48
48
|
|
|
49
49
|
# Load provider registry
|
|
50
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"
|
|
51
54
|
|
|
52
55
|
module LlmGateway
|
|
53
56
|
class Error < StandardError; end
|
|
@@ -110,7 +113,7 @@ module LlmGateway
|
|
|
110
113
|
entry = ProviderRegistry.resolve(provider_name)
|
|
111
114
|
|
|
112
115
|
client = entry[:client].new(**config)
|
|
113
|
-
entry[:adapter].new(client)
|
|
116
|
+
entry[:adapter].new(client, provider_key: provider_name)
|
|
114
117
|
end
|
|
115
118
|
|
|
116
119
|
def self.configure(configs)
|
|
@@ -160,4 +163,8 @@ module LlmGateway
|
|
|
160
163
|
ProviderRegistry.register("openai_codex",
|
|
161
164
|
client: Clients::OpenAI,
|
|
162
165
|
adapter: Adapters::OpenAICodex::ResponsesAdapter)
|
|
166
|
+
|
|
167
|
+
ProviderRegistry.register("proxy",
|
|
168
|
+
client: Proxy::Client,
|
|
169
|
+
adapter: Proxy::Adapter)
|
|
163
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-06-
|
|
11
|
+
date: 2026-06-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|
|
@@ -97,6 +97,9 @@ files:
|
|
|
97
97
|
- lib/llm_gateway/errors.rb
|
|
98
98
|
- lib/llm_gateway/prompt.rb
|
|
99
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
|
|
100
103
|
- lib/llm_gateway/tool.rb
|
|
101
104
|
- lib/llm_gateway/utils.rb
|
|
102
105
|
- lib/llm_gateway/version.rb
|