llm_gateway 0.1.6 → 0.2.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/.rubocop.yml +29 -0
- data/CHANGELOG.md +16 -0
- data/README.md +77 -2
- data/lib/llm_gateway/adapters/claude/input_mapper.rb +34 -12
- data/lib/llm_gateway/adapters/claude/output_mapper.rb +13 -7
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +80 -53
- data/lib/llm_gateway/adapters/groq/output_mapper.rb +40 -24
- data/lib/llm_gateway/adapters/open_ai/input_mapper.rb +47 -3
- data/lib/llm_gateway/client.rb +5 -5
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +0 -1
- metadata +2 -3
- data/lib/llm_gateway/fluent_mapper.rb +0 -144
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 829e306ff0af794ce1a301b1eac5ab52edfa9a21ce1eec479955de3a328be443
|
4
|
+
data.tar.gz: 6935b650d14237e48a82cdbb0614e5201c6960abf71d6dc23bd51e76d6b37ee3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 89572a5c0d05806fdc5fa7a06a7336f5b8b738daaff37f834595b96415a601adc50ca13e33c79cfa29abcb82766eeb6ad3cbe14c099b030e340af7d8dd943f2f
|
7
|
+
data.tar.gz: 0001f5413fe4b3c3b17b2bca197a37e00c0503b09da7e79b90b52a45c65a4247dfd8be60fa86cc99505eb408c4c69c3d2cd835eb700c8018faa1f64da6e0f6ab
|
data/.rubocop.yml
CHANGED
@@ -3,3 +3,32 @@ inherit_gem:
|
|
3
3
|
|
4
4
|
AllCops:
|
5
5
|
TargetRubyVersion: 3.1
|
6
|
+
|
7
|
+
Layout/EndAlignment:
|
8
|
+
EnforcedStyleAlignWith: start_of_line
|
9
|
+
|
10
|
+
Layout/FirstHashElementLineBreak:
|
11
|
+
Enabled: true
|
12
|
+
|
13
|
+
Layout/FirstHashElementIndentation:
|
14
|
+
Enabled: true
|
15
|
+
EnforcedStyle: consistent
|
16
|
+
|
17
|
+
Layout/MultilineHashKeyLineBreaks:
|
18
|
+
Enabled: true
|
19
|
+
|
20
|
+
Layout/HashAlignment:
|
21
|
+
Enabled: true
|
22
|
+
EnforcedHashRocketStyle: key
|
23
|
+
EnforcedColonStyle: key
|
24
|
+
|
25
|
+
Layout/IndentationConsistency:
|
26
|
+
Enabled: true
|
27
|
+
EnforcedStyle: normal
|
28
|
+
|
29
|
+
Layout/IndentationWidth:
|
30
|
+
Width: 2
|
31
|
+
|
32
|
+
Layout/MultilineHashBraceLayout:
|
33
|
+
Enabled: true
|
34
|
+
EnforcedStyle: new_line
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,21 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## [v0.2.0](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.2.0) (2025-08-08)
|
4
|
+
|
5
|
+
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.6...v0.2.0)
|
6
|
+
|
7
|
+
**Merged pull requests:**
|
8
|
+
|
9
|
+
- feat: improve read me [\#21](https://github.com/Hyper-Unearthing/llm_gateway/pull/21) ([billybonks](https://github.com/billybonks))
|
10
|
+
- refactor: remove fluent mapper from the lib [\#20](https://github.com/Hyper-Unearthing/llm_gateway/pull/20) ([billybonks](https://github.com/billybonks))
|
11
|
+
- test: dont fail vcr if key order changes [\#19](https://github.com/Hyper-Unearthing/llm_gateway/pull/19) ([billybonks](https://github.com/billybonks))
|
12
|
+
- burn: fluent mapper from input mappers [\#18](https://github.com/Hyper-Unearthing/llm_gateway/pull/18) ([billybonks](https://github.com/billybonks))
|
13
|
+
- test: open ai mapper [\#17](https://github.com/Hyper-Unearthing/llm_gateway/pull/17) ([billybonks](https://github.com/billybonks))
|
14
|
+
- feat: handle basic text document handling [\#16](https://github.com/Hyper-Unearthing/llm_gateway/pull/16) ([billybonks](https://github.com/billybonks))
|
15
|
+
- test: improve issues [\#15](https://github.com/Hyper-Unearthing/llm_gateway/pull/15) ([billybonks](https://github.com/billybonks))
|
16
|
+
- style: format aligment of content automatically to my preference [\#14](https://github.com/Hyper-Unearthing/llm_gateway/pull/14) ([billybonks](https://github.com/billybonks))
|
17
|
+
- fix: tool calling open ai [\#13](https://github.com/Hyper-Unearthing/llm_gateway/pull/13) ([billybonks](https://github.com/billybonks))
|
18
|
+
|
3
19
|
## [v0.1.6](https://github.com/Hyper-Unearthing/llm_gateway/tree/v0.1.6) (2025-08-05)
|
4
20
|
|
5
21
|
[Full Changelog](https://github.com/Hyper-Unearthing/llm_gateway/compare/v0.1.5...v0.1.6)
|
data/README.md
CHANGED
@@ -30,19 +30,94 @@ gem install llm_gateway
|
|
30
30
|
require 'llm_gateway'
|
31
31
|
|
32
32
|
# Simple text completion
|
33
|
-
|
33
|
+
LlmGateway::Client.chat(
|
34
34
|
'claude-sonnet-4-20250514',
|
35
35
|
'What is the capital of France?'
|
36
36
|
)
|
37
37
|
|
38
38
|
# With system message
|
39
|
-
|
39
|
+
LlmGateway::Client.chat(
|
40
40
|
'gpt-4',
|
41
41
|
'What is the capital of France?',
|
42
42
|
system: 'You are a helpful geography teacher.'
|
43
43
|
)
|
44
|
+
|
45
|
+
# With inline file
|
46
|
+
LlmGateway::Client.chat(
|
47
|
+
"claude-sonnet-4-20250514",
|
48
|
+
[
|
49
|
+
{
|
50
|
+
role: "user", content: [
|
51
|
+
{ type: "text", text: "return the content of the document exactly" },
|
52
|
+
{ type: "file", data: "abc\n", media_type: "text/plain", name: "small.txt" }
|
53
|
+
]
|
54
|
+
},
|
55
|
+
]
|
56
|
+
)
|
57
|
+
|
58
|
+
# Transcript
|
59
|
+
LlmGateway::Client.chat('llama-3.3-70b-versatile',[
|
60
|
+
{ role: "user", content: "Tell Me a joke" },
|
61
|
+
{ role: "assistant", content: "what kind of content"},
|
62
|
+
{ role: "user", content: "About Sparkling water" },
|
63
|
+
]
|
64
|
+
)
|
65
|
+
|
66
|
+
|
67
|
+
# Tool usage
|
68
|
+
LlmGateway::Client.chat('gpt-5',[
|
69
|
+
{ role: "user", content: "What's the weather in Singapore? reply in 10 words and no special characters" },
|
70
|
+
{ role: "assistant",
|
71
|
+
content: [
|
72
|
+
{ id: "call_gpXfy9l9QNmShNEbNI1FyuUZ", type: "tool_use", name: "get_weather", input: { location: "Singapore" } }
|
73
|
+
]
|
74
|
+
},
|
75
|
+
{ role: "developer",
|
76
|
+
content: [
|
77
|
+
{ content: "-15 celcius", type: "tool_result", tool_use_id: "call_gpXfy9l9QNmShNEbNI1FyuUZ" }
|
78
|
+
]
|
79
|
+
}
|
80
|
+
],
|
81
|
+
tools: [ { name: "get_weather", description: "Get current weather for a location", input_schema: { type: "object", properties: { location: { type: "string", description: "City name" } }, required: [ "location" ] } } ]
|
82
|
+
)
|
44
83
|
```
|
45
84
|
|
85
|
+
### Supported Roles
|
86
|
+
|
87
|
+
- user
|
88
|
+
- developer
|
89
|
+
- assistant
|
90
|
+
|
91
|
+
#### Examples
|
92
|
+
```ruby
|
93
|
+
# tool call
|
94
|
+
{ role: "developer",
|
95
|
+
content: [
|
96
|
+
{ content: "-15 celcius", type: "tool_result", tool_use_id: "call_gpXfy9l9QNmShNEbNI1FyuUZ" }
|
97
|
+
]
|
98
|
+
}
|
99
|
+
# plain message
|
100
|
+
{ role: "user", content: "What's the weather in Singapore? reply in 10 words and no special characters" }
|
101
|
+
|
102
|
+
# plain response
|
103
|
+
{ role: "assistant", content: "what kind of content"},
|
104
|
+
|
105
|
+
# tool call response
|
106
|
+
{ role: "assistant",
|
107
|
+
content: [
|
108
|
+
{ id: "call_gpXfy9l9QNmShNEbNI1FyuUZ", type: "tool_use", name: "get_weather", input: { location: "Singapore" } }
|
109
|
+
]
|
110
|
+
},
|
111
|
+
```
|
112
|
+
|
113
|
+
developer is an open ai role, but i thought it was usefull for tracing if message sent from server or user so i added
|
114
|
+
it to the list of roles, when it is not supported it will be mapped to user instead.
|
115
|
+
|
116
|
+
you can assume developer and user to be interchangeable
|
117
|
+
|
118
|
+
|
119
|
+
|
120
|
+
|
46
121
|
### Sample Application
|
47
122
|
|
48
123
|
See the [file search bot example](sample/claude_code_clone/) for a complete working application that demonstrates:
|
@@ -4,30 +4,52 @@ module LlmGateway
|
|
4
4
|
module Adapters
|
5
5
|
module Claude
|
6
6
|
class InputMapper
|
7
|
-
|
7
|
+
def self.map(data)
|
8
|
+
{
|
9
|
+
messages: map_messages(data[:messages]),
|
10
|
+
response_format: data[:response_format],
|
11
|
+
tools: data[:tools],
|
12
|
+
system: map_system(data[:system])
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def self.map_messages(messages)
|
19
|
+
return messages unless messages
|
8
20
|
|
9
|
-
|
10
|
-
value.map do |msg|
|
21
|
+
messages.map do |msg|
|
11
22
|
msg = msg.merge(role: "user") if msg[:role] == "developer"
|
12
23
|
msg.slice(:role, :content)
|
24
|
+
content = if msg[:content].is_a?(Array)
|
25
|
+
msg[:content].map do |content|
|
26
|
+
if content[:type] == "file"
|
27
|
+
{ type: "document", source: { data: content[:data], type: "text", media_type: content[:media_type] } }
|
28
|
+
else
|
29
|
+
content
|
30
|
+
end
|
31
|
+
end
|
32
|
+
else
|
33
|
+
msg[:content]
|
34
|
+
end
|
35
|
+
{
|
36
|
+
role: msg[:role],
|
37
|
+
content: content
|
38
|
+
}
|
13
39
|
end
|
14
40
|
end
|
15
41
|
|
16
|
-
|
17
|
-
if
|
42
|
+
def self.map_system(system)
|
43
|
+
if !system || system.empty?
|
18
44
|
nil
|
19
|
-
elsif
|
45
|
+
elsif system.length == 1 && system.first[:role] == "system"
|
20
46
|
# If we have a single system message, convert to Claude format
|
21
|
-
[ { type: "text", text:
|
47
|
+
[ { type: "text", text: system.first[:content] } ]
|
22
48
|
else
|
23
49
|
# For multiple messages or non-standard format, pass through
|
24
|
-
|
50
|
+
system
|
25
51
|
end
|
26
52
|
end
|
27
|
-
|
28
|
-
map :tools do |_, value|
|
29
|
-
value
|
30
|
-
end
|
31
53
|
end
|
32
54
|
end
|
33
55
|
end
|
@@ -4,17 +4,23 @@ module LlmGateway
|
|
4
4
|
module Adapters
|
5
5
|
module Claude
|
6
6
|
class OutputMapper
|
7
|
-
|
7
|
+
def self.map(data)
|
8
|
+
{
|
9
|
+
id: data[:id],
|
10
|
+
model: data[:model],
|
11
|
+
usage: data[:usage],
|
12
|
+
choices: map_choices(data)
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
8
17
|
|
9
|
-
|
10
|
-
map :model
|
11
|
-
map :usage
|
12
|
-
map :choices do |_, _|
|
18
|
+
def self.map_choices(data)
|
13
19
|
# Claude returns content directly at root level, not in a choices array
|
14
20
|
# We need to construct the choices array from the full response data
|
15
21
|
[ {
|
16
|
-
content:
|
17
|
-
finish_reason:
|
22
|
+
content: data[:content] || [], # Use content directly from Claude response
|
23
|
+
finish_reason: data[:stop_reason],
|
18
24
|
role: "assistant"
|
19
25
|
} ]
|
20
26
|
end
|
@@ -4,74 +4,101 @@ module LlmGateway
|
|
4
4
|
module Adapters
|
5
5
|
module Groq
|
6
6
|
class InputMapper
|
7
|
-
|
7
|
+
def self.map(data)
|
8
|
+
{
|
9
|
+
messages: map_messages(data[:messages]),
|
10
|
+
response_format: map_response_format(data[:response_format]),
|
11
|
+
tools: map_tools(data[:tools]),
|
12
|
+
system: map_system(data[:system])
|
13
|
+
}
|
14
|
+
end
|
8
15
|
|
9
|
-
|
10
|
-
map :response_format
|
16
|
+
private
|
11
17
|
|
12
|
-
|
13
|
-
|
14
|
-
map :content do
|
15
|
-
nil
|
16
|
-
end
|
17
|
-
map :tool_calls, from: :content do |_, value|
|
18
|
-
value.map do |content|
|
19
|
-
{
|
20
|
-
'id': content[:id],
|
21
|
-
'type': "function",
|
22
|
-
'function': {
|
23
|
-
'name': content[:name],
|
24
|
-
'arguments': content[:input].to_json
|
25
|
-
}
|
26
|
-
}
|
27
|
-
end
|
28
|
-
end
|
18
|
+
def self.map_system(system)
|
19
|
+
system
|
29
20
|
end
|
30
21
|
|
31
|
-
|
32
|
-
|
33
|
-
map :tool_call_id, from: "tool_use_id"
|
34
|
-
map :content
|
22
|
+
def self.map_response_format(response_format)
|
23
|
+
response_format
|
35
24
|
end
|
36
25
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
26
|
+
def self.map_messages(messages)
|
27
|
+
return messages unless messages
|
28
|
+
|
29
|
+
messages.flat_map do |msg|
|
30
|
+
if msg[:content].is_a?(Array)
|
31
|
+
# Handle array content with tool calls and tool results
|
32
|
+
tool_calls = []
|
33
|
+
regular_content = []
|
34
|
+
tool_messages = []
|
35
|
+
|
36
|
+
msg[:content].each do |content|
|
37
|
+
case content[:type]
|
38
|
+
when "tool_result"
|
39
|
+
tool_messages << map_tool_result_message(content)
|
40
|
+
when "tool_use"
|
41
|
+
tool_calls << map_tool_usage(content)
|
42
|
+
else
|
43
|
+
regular_content << content
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
result = []
|
48
|
+
|
49
|
+
# Add the main message with tool calls if any
|
50
|
+
if tool_calls.any? || regular_content.any?
|
51
|
+
main_msg = msg.dup
|
52
|
+
main_msg[:role] = "assistant" if !main_msg[:role]
|
53
|
+
main_msg[:tool_calls] = tool_calls if tool_calls.any?
|
54
|
+
main_msg[:content] = regular_content.any? ? regular_content : nil
|
55
|
+
result << main_msg
|
50
56
|
end
|
51
57
|
|
52
|
-
|
58
|
+
# Add separate tool result messages
|
59
|
+
result += tool_messages
|
60
|
+
|
61
|
+
result
|
53
62
|
else
|
54
|
-
|
63
|
+
# Regular message, return as-is
|
64
|
+
[ msg ]
|
55
65
|
end
|
56
|
-
end
|
66
|
+
end
|
57
67
|
end
|
58
68
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
+
def self.map_tools(tools)
|
70
|
+
return tools unless tools
|
71
|
+
|
72
|
+
tools.map do |tool|
|
73
|
+
{
|
74
|
+
type: "function",
|
75
|
+
function: {
|
76
|
+
name: tool[:name],
|
77
|
+
description: tool[:description],
|
78
|
+
parameters: tool[:input_schema]
|
69
79
|
}
|
70
|
-
|
71
|
-
else
|
72
|
-
value
|
80
|
+
}
|
73
81
|
end
|
74
82
|
end
|
83
|
+
|
84
|
+
def self.map_tool_usage(content)
|
85
|
+
{
|
86
|
+
'id': content[:id],
|
87
|
+
'type': "function",
|
88
|
+
'function': {
|
89
|
+
'name': content[:name],
|
90
|
+
'arguments': content[:input].to_json
|
91
|
+
}
|
92
|
+
}
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.map_tool_result_message(content)
|
96
|
+
{
|
97
|
+
role: "tool",
|
98
|
+
tool_call_id: content[:tool_use_id],
|
99
|
+
content: content[:content]
|
100
|
+
}
|
101
|
+
end
|
75
102
|
end
|
76
103
|
end
|
77
104
|
end
|
@@ -4,42 +4,58 @@ module LlmGateway
|
|
4
4
|
module Adapters
|
5
5
|
module Groq
|
6
6
|
class OutputMapper
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
map :name, from: "function.name"
|
15
|
-
map :input, from: "function.arguments" do |_, value|
|
16
|
-
parsed = value.is_a?(String) ? JSON.parse(value) : value
|
17
|
-
parsed
|
18
|
-
end
|
7
|
+
def self.map(data)
|
8
|
+
{
|
9
|
+
id: data[:id],
|
10
|
+
model: data[:model],
|
11
|
+
usage: data[:usage],
|
12
|
+
choices: map_choices(data[:choices])
|
13
|
+
}
|
19
14
|
end
|
20
15
|
|
21
|
-
|
22
|
-
map :text, from: "content"
|
23
|
-
map :type, default: "text"
|
24
|
-
end
|
16
|
+
private
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
value.map do |choice|
|
18
|
+
def self.map_choices(choices)
|
19
|
+
return [] unless choices
|
20
|
+
|
21
|
+
choices.map do |choice|
|
31
22
|
message = choice[:message] || {}
|
32
|
-
content_item =
|
33
|
-
tool_calls =
|
23
|
+
content_item = map_content_item(message)
|
24
|
+
tool_calls = map_tool_calls(message[:tool_calls])
|
34
25
|
|
35
26
|
# Only include content_item if it has actual text content
|
36
27
|
content_array = []
|
37
|
-
content_array << content_item if LlmGateway::Utils.present?(content_item[
|
28
|
+
content_array << content_item if LlmGateway::Utils.present?(content_item[:text])
|
38
29
|
content_array += tool_calls
|
39
30
|
|
40
31
|
{ content: content_array }
|
41
32
|
end
|
42
33
|
end
|
34
|
+
|
35
|
+
def self.map_content_item(message)
|
36
|
+
{
|
37
|
+
text: message[:content],
|
38
|
+
type: "text"
|
39
|
+
}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.map_tool_calls(tool_calls)
|
43
|
+
return [] unless tool_calls
|
44
|
+
|
45
|
+
tool_calls.map do |tool_call|
|
46
|
+
{
|
47
|
+
id: tool_call[:id],
|
48
|
+
type: "tool_use",
|
49
|
+
name: tool_call.dig(:function, :name),
|
50
|
+
input: parse_tool_arguments(tool_call.dig(:function, :arguments))
|
51
|
+
}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.parse_tool_arguments(arguments)
|
56
|
+
return arguments unless arguments.is_a?(String)
|
57
|
+
JSON.parse(arguments, symbolize_names: true)
|
58
|
+
end
|
43
59
|
end
|
44
60
|
end
|
45
61
|
end
|
@@ -1,14 +1,58 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "base64"
|
4
|
+
|
3
5
|
module LlmGateway
|
4
6
|
module Adapters
|
5
7
|
module OpenAi
|
6
8
|
class InputMapper < LlmGateway::Adapters::Groq::InputMapper
|
7
|
-
map
|
8
|
-
|
9
|
+
def self.map(data)
|
10
|
+
{
|
11
|
+
messages: map_messages(data[:messages]),
|
12
|
+
response_format: map_response_format(data[:response_format]),
|
13
|
+
tools: map_tools(data[:tools]),
|
14
|
+
system: map_system(data[:system])
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self.map_messages(messages)
|
21
|
+
return messages unless messages
|
22
|
+
|
23
|
+
# First, handle file transformations
|
24
|
+
messages_with_files = messages.map do |msg|
|
25
|
+
if msg[:content].is_a?(Array)
|
26
|
+
content = msg[:content].map do |content|
|
27
|
+
if content[:type] == "file"
|
28
|
+
# Map text/plain to application/pdf for OpenAI
|
29
|
+
media_type = content[:media_type] == "text/plain" ? "application/pdf" : content[:media_type]
|
30
|
+
{
|
31
|
+
type: "file",
|
32
|
+
file: {
|
33
|
+
filename: content[:name],
|
34
|
+
file_data: "data:#{media_type};base64,#{Base64.encode64(content[:data])}"
|
35
|
+
}
|
36
|
+
}
|
37
|
+
else
|
38
|
+
content
|
39
|
+
end
|
40
|
+
end
|
41
|
+
msg.merge(content: content)
|
42
|
+
else
|
43
|
+
msg
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Then apply parent's tool transformation logic
|
48
|
+
super(messages_with_files)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.map_system(system)
|
52
|
+
if !system || system.empty?
|
9
53
|
[]
|
10
54
|
else
|
11
|
-
|
55
|
+
system.map do |msg|
|
12
56
|
msg[:role] == "system" ? msg.merge(role: "developer") : msg
|
13
57
|
end
|
14
58
|
end
|
data/lib/llm_gateway/client.rb
CHANGED
@@ -10,11 +10,11 @@ module LlmGateway
|
|
10
10
|
|
11
11
|
input_mapper = input_mapper_for_client(client)
|
12
12
|
normalized_input = input_mapper.map({
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
13
|
+
messages: normalize_messages(message),
|
14
|
+
response_format: normalize_response_format(response_format),
|
15
|
+
tools: tools,
|
16
|
+
system: normalize_system(system)
|
17
|
+
})
|
18
18
|
result = client.chat(
|
19
19
|
normalized_input[:messages],
|
20
20
|
response_format: normalized_input[:response_format],
|
data/lib/llm_gateway/version.rb
CHANGED
data/lib/llm_gateway.rb
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
require_relative "llm_gateway/utils"
|
4
4
|
require_relative "llm_gateway/version"
|
5
5
|
require_relative "llm_gateway/errors"
|
6
|
-
require_relative "llm_gateway/fluent_mapper"
|
7
6
|
require_relative "llm_gateway/base_client"
|
8
7
|
require_relative "llm_gateway/client"
|
9
8
|
require_relative "llm_gateway/prompt"
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- billybonks
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-08-
|
11
|
+
date: 2025-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: LlmGateway provides a consistent Ruby interface for multiple LLM providers
|
14
14
|
including Claude, OpenAI, and Groq. Features include unified response formatting,
|
@@ -38,7 +38,6 @@ files:
|
|
38
38
|
- lib/llm_gateway/base_client.rb
|
39
39
|
- lib/llm_gateway/client.rb
|
40
40
|
- lib/llm_gateway/errors.rb
|
41
|
-
- lib/llm_gateway/fluent_mapper.rb
|
42
41
|
- lib/llm_gateway/prompt.rb
|
43
42
|
- lib/llm_gateway/tool.rb
|
44
43
|
- lib/llm_gateway/utils.rb
|
@@ -1,144 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module LlmGateway
|
4
|
-
module FluentMapper
|
5
|
-
def self.extended(base)
|
6
|
-
base.instance_variable_set(:@mappers, {})
|
7
|
-
base.instance_variable_set(:@mappings, [])
|
8
|
-
end
|
9
|
-
|
10
|
-
def inherited(subclass)
|
11
|
-
super
|
12
|
-
# Copy parent's mappers and mappings to the subclass
|
13
|
-
subclass.instance_variable_set(:@mappers, @mappers.dup)
|
14
|
-
subclass.instance_variable_set(:@mappings, @mappings.dup)
|
15
|
-
end
|
16
|
-
|
17
|
-
def mapper(name, &block)
|
18
|
-
@mappers[name] = block
|
19
|
-
end
|
20
|
-
|
21
|
-
def map(field_or_data, options = {}, &block)
|
22
|
-
# If called with a single argument and no block, it's the class method usage
|
23
|
-
if block.nil? && options.empty? && !field_or_data.is_a?(Symbol) && !field_or_data.is_a?(String)
|
24
|
-
return new(field_or_data).call
|
25
|
-
end
|
26
|
-
|
27
|
-
# Otherwise it's the field mapping usage
|
28
|
-
@mappings << { field: field_or_data, options: options, block: block }
|
29
|
-
end
|
30
|
-
|
31
|
-
def new(data)
|
32
|
-
MapperInstance.new(data, @mappers, @mappings)
|
33
|
-
end
|
34
|
-
|
35
|
-
class MapperInstance
|
36
|
-
def initialize(data, mappers, mappings)
|
37
|
-
@data = data.respond_to?(:with_indifferent_access) ? data.with_indifferent_access : data
|
38
|
-
@mappers = mappers
|
39
|
-
@mappings = mappings
|
40
|
-
@mapper_definitions = {}
|
41
|
-
|
42
|
-
# Execute mapper definitions
|
43
|
-
mappers.each do |name, block|
|
44
|
-
@mapper_definitions[name] = MapperDefinition.new
|
45
|
-
@mapper_definitions[name].instance_eval(&block)
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
def call
|
50
|
-
result = {}
|
51
|
-
|
52
|
-
@mappings.each do |mapping|
|
53
|
-
field = mapping[:field]
|
54
|
-
options = mapping[:options]
|
55
|
-
block = mapping[:block]
|
56
|
-
|
57
|
-
from_path = options[:from] || field.to_s
|
58
|
-
default_value = options[:default]
|
59
|
-
|
60
|
-
value = get_nested_value(@data, from_path)
|
61
|
-
value = default_value if value.nil? && !default_value.nil?
|
62
|
-
|
63
|
-
value = instance_exec(field, value, &block) if block
|
64
|
-
|
65
|
-
result[field] = value
|
66
|
-
end
|
67
|
-
|
68
|
-
LlmGateway::Utils.deep_symbolize_keys(result)
|
69
|
-
end
|
70
|
-
|
71
|
-
def map_single(data, options = {})
|
72
|
-
mapper_name = options[:with]
|
73
|
-
default_value = options[:default]
|
74
|
-
return default_value if data.nil? && !default_value.nil?
|
75
|
-
return data unless mapper_name && @mapper_definitions[mapper_name]
|
76
|
-
|
77
|
-
# Apply with_indifferent_access to data
|
78
|
-
data = data.respond_to?(:with_indifferent_access) ? data.with_indifferent_access : data
|
79
|
-
|
80
|
-
mapper_def = @mapper_definitions[mapper_name]
|
81
|
-
result = {}
|
82
|
-
|
83
|
-
mapper_def.mappings.each do |mapping|
|
84
|
-
field = mapping[:field]
|
85
|
-
map_options = mapping[:options]
|
86
|
-
block = mapping[:block]
|
87
|
-
|
88
|
-
from_path = map_options[:from] || field.to_s
|
89
|
-
field_default_value = map_options[:default]
|
90
|
-
|
91
|
-
value = get_nested_value(data, from_path)
|
92
|
-
value = field_default_value if value.nil? && !field_default_value.nil?
|
93
|
-
|
94
|
-
value = instance_exec(field, value, &block) if block
|
95
|
-
|
96
|
-
result[field.to_s] = value
|
97
|
-
end
|
98
|
-
|
99
|
-
result
|
100
|
-
end
|
101
|
-
|
102
|
-
def map_collection(collection, options = {})
|
103
|
-
default_value = options[:default]
|
104
|
-
return default_value if collection.nil? && !default_value.nil?
|
105
|
-
return [] if collection.nil?
|
106
|
-
|
107
|
-
collection.map { |item| map_single(item, options) }
|
108
|
-
end
|
109
|
-
|
110
|
-
private
|
111
|
-
|
112
|
-
def get_nested_value(data, path)
|
113
|
-
return data[path] if data.respond_to?(:[]) && data.key?(path)
|
114
|
-
return data[path.to_sym] if data.respond_to?(:[]) && data.key?(path.to_sym)
|
115
|
-
return data[path.to_s] if data.respond_to?(:[]) && data.key?(path.to_s)
|
116
|
-
|
117
|
-
keys = path.split(".")
|
118
|
-
current = data
|
119
|
-
|
120
|
-
keys.each do |key|
|
121
|
-
return nil unless current.respond_to?(:[])
|
122
|
-
|
123
|
-
current = current[key] || current[key.to_sym] || current[key.to_s]
|
124
|
-
|
125
|
-
return nil if current.nil?
|
126
|
-
end
|
127
|
-
|
128
|
-
current
|
129
|
-
end
|
130
|
-
end
|
131
|
-
|
132
|
-
class MapperDefinition
|
133
|
-
attr_reader :mappings
|
134
|
-
|
135
|
-
def initialize
|
136
|
-
@mappings = []
|
137
|
-
end
|
138
|
-
|
139
|
-
def map(field, options = {}, &block)
|
140
|
-
@mappings << { field: field, options: options, block: block }
|
141
|
-
end
|
142
|
-
end
|
143
|
-
end
|
144
|
-
end
|