llm_gateway 0.4.0 → 0.5.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 +17 -0
- data/README.md +16 -0
- data/Rakefile +1 -0
- data/lib/llm_gateway/adapters/adapter.rb +2 -35
- 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 +31 -46
- 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 +275 -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 +169 -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 +81 -271
- 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 +50 -0
- data/lib/llm_gateway/client.rb +10 -66
- data/lib/llm_gateway/clients/groq.rb +13 -1
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +2 -8
- metadata +7 -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
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
|
|
@@ -5,7 +5,7 @@ require_relative "../base_client"
|
|
|
5
5
|
module LlmGateway
|
|
6
6
|
module Clients
|
|
7
7
|
class Groq < BaseClient
|
|
8
|
-
def initialize(model_key: "openai/gpt-oss-
|
|
8
|
+
def initialize(model_key: "openai/gpt-oss-120b", api_key: ENV["GROQ_API_KEY"])
|
|
9
9
|
@base_endpoint = "https://api.groq.com/openai/v1"
|
|
10
10
|
super(model_key: model_key, api_key: api_key)
|
|
11
11
|
end
|
|
@@ -21,6 +21,18 @@ module LlmGateway
|
|
|
21
21
|
post("chat/completions", body)
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
+
def stream(messages, tools: nil, system: [], **options, &block)
|
|
25
|
+
body = {
|
|
26
|
+
model: model_key,
|
|
27
|
+
messages: system + messages,
|
|
28
|
+
tools: tools,
|
|
29
|
+
stream_options: { include_usage: true }
|
|
30
|
+
}
|
|
31
|
+
body.merge!(options)
|
|
32
|
+
|
|
33
|
+
post_stream("chat/completions", body, &block)
|
|
34
|
+
end
|
|
35
|
+
|
|
24
36
|
private
|
|
25
37
|
|
|
26
38
|
def build_headers
|
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
|
|
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.5.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-05-
|
|
11
|
+
date: 2026-05-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: dry-struct
|
|
@@ -33,6 +33,8 @@ executables: []
|
|
|
33
33
|
extensions: []
|
|
34
34
|
extra_rdoc_files: []
|
|
35
35
|
files:
|
|
36
|
+
- ".pi/skills/live-provider-testing/SKILL.md"
|
|
37
|
+
- ".pi/skills/options-development/SKILL.md"
|
|
36
38
|
- ".rubocop.yml"
|
|
37
39
|
- CHANGELOG.md
|
|
38
40
|
- CODE_OF_CONDUCT.md
|
|
@@ -43,37 +45,34 @@ files:
|
|
|
43
45
|
- lib/llm_gateway.rb
|
|
44
46
|
- lib/llm_gateway/adapters/adapter.rb
|
|
45
47
|
- lib/llm_gateway/adapters/anthropic/acts_like_messages.rb
|
|
46
|
-
- lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb
|
|
47
48
|
- lib/llm_gateway/adapters/anthropic/input_mapper.rb
|
|
48
49
|
- lib/llm_gateway/adapters/anthropic/messages_adapter.rb
|
|
49
50
|
- lib/llm_gateway/adapters/anthropic/output_mapper.rb
|
|
50
51
|
- lib/llm_gateway/adapters/anthropic/stream_mapper.rb
|
|
51
52
|
- lib/llm_gateway/adapters/anthropic_option_mapper.rb
|
|
52
53
|
- lib/llm_gateway/adapters/groq/chat_completions_adapter.rb
|
|
54
|
+
- lib/llm_gateway/adapters/groq/input_mapper.rb
|
|
53
55
|
- lib/llm_gateway/adapters/groq/option_mapper.rb
|
|
54
56
|
- lib/llm_gateway/adapters/input_message_sanitizer.rb
|
|
57
|
+
- lib/llm_gateway/adapters/normalized_stream_accumulator.rb
|
|
55
58
|
- lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb
|
|
56
59
|
- lib/llm_gateway/adapters/openai/acts_like_responses.rb
|
|
57
|
-
- lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb
|
|
58
60
|
- lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb
|
|
59
61
|
- lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb
|
|
60
62
|
- lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb
|
|
61
|
-
- lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb
|
|
62
63
|
- lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb
|
|
63
64
|
- lib/llm_gateway/adapters/openai/chat_completions_adapter.rb
|
|
64
65
|
- lib/llm_gateway/adapters/openai/file_output_mapper.rb
|
|
65
66
|
- lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb
|
|
66
|
-
- lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb
|
|
67
67
|
- lib/llm_gateway/adapters/openai/responses/input_mapper.rb
|
|
68
68
|
- lib/llm_gateway/adapters/openai/responses/option_mapper.rb
|
|
69
|
-
- lib/llm_gateway/adapters/openai/responses/output_mapper.rb
|
|
70
69
|
- lib/llm_gateway/adapters/openai/responses/stream_mapper.rb
|
|
71
70
|
- lib/llm_gateway/adapters/openai/responses_adapter.rb
|
|
72
71
|
- lib/llm_gateway/adapters/openai_codex/input_mapper.rb
|
|
73
72
|
- lib/llm_gateway/adapters/openai_codex/option_mapper.rb
|
|
74
73
|
- lib/llm_gateway/adapters/openai_codex/responses_adapter.rb
|
|
75
74
|
- lib/llm_gateway/adapters/option_mapper.rb
|
|
76
|
-
- lib/llm_gateway/adapters/
|
|
75
|
+
- lib/llm_gateway/adapters/stream_mapper.rb
|
|
77
76
|
- lib/llm_gateway/adapters/structs.rb
|
|
78
77
|
- lib/llm_gateway/base_client.rb
|
|
79
78
|
- lib/llm_gateway/client.rb
|
|
@@ -92,8 +91,6 @@ files:
|
|
|
92
91
|
- lib/llm_gateway/version.rb
|
|
93
92
|
- scripts/create_anthropic_credentials.rb
|
|
94
93
|
- scripts/create_openai_codex_credentials.rb
|
|
95
|
-
- scripts/generate_handoff_live_fixture.rb
|
|
96
|
-
- scripts/generate_handoff_media_fixture.rb
|
|
97
94
|
- sig/llm_gateway.rbs
|
|
98
95
|
homepage: https://github.com/Hyper-Unearthing/llm_gateway
|
|
99
96
|
licenses:
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmGateway
|
|
4
|
-
module Adapters
|
|
5
|
-
module Anthropic
|
|
6
|
-
class BidirectionalMessageMapper
|
|
7
|
-
attr_reader :direction
|
|
8
|
-
|
|
9
|
-
def initialize(direction)
|
|
10
|
-
@direction = direction
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def map_content(content)
|
|
14
|
-
# Convert string content to text format
|
|
15
|
-
content = { type: "text", text: content } unless content.is_a?(Hash)
|
|
16
|
-
|
|
17
|
-
case content[:type]
|
|
18
|
-
when "text"
|
|
19
|
-
map_text_content(content)
|
|
20
|
-
when "file"
|
|
21
|
-
map_file_content(content)
|
|
22
|
-
when "image"
|
|
23
|
-
map_image_content(content)
|
|
24
|
-
when "tool_use"
|
|
25
|
-
map_tool_use_content(content)
|
|
26
|
-
when "tool_result"
|
|
27
|
-
map_tool_result_content(content)
|
|
28
|
-
when "thinking", "reasoning"
|
|
29
|
-
map_reasoning_content(content)
|
|
30
|
-
else
|
|
31
|
-
content
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
private
|
|
36
|
-
|
|
37
|
-
def map_text_content(content)
|
|
38
|
-
result = {
|
|
39
|
-
type: "text",
|
|
40
|
-
text: content[:text]
|
|
41
|
-
}
|
|
42
|
-
result[:cache_control] = content[:cache_control] if content[:cache_control]
|
|
43
|
-
result
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def map_file_content(content)
|
|
47
|
-
{
|
|
48
|
-
type: "document",
|
|
49
|
-
source: {
|
|
50
|
-
data: content[:data],
|
|
51
|
-
type: "text",
|
|
52
|
-
media_type: content[:media_type]
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def map_image_content(content)
|
|
58
|
-
{
|
|
59
|
-
type: "image",
|
|
60
|
-
source: {
|
|
61
|
-
data: content[:data],
|
|
62
|
-
type: "base64",
|
|
63
|
-
media_type: content[:media_type]
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def map_tool_use_content(content)
|
|
69
|
-
{
|
|
70
|
-
type: "tool_use",
|
|
71
|
-
id: content[:id],
|
|
72
|
-
name: content[:name],
|
|
73
|
-
input: content[:input]
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def map_tool_result_content(content)
|
|
78
|
-
mapped_content = content[:content]
|
|
79
|
-
if mapped_content.is_a?(Array)
|
|
80
|
-
mapped_content = mapped_content.map do |item|
|
|
81
|
-
item.is_a?(Hash) ? map_content(item.transform_keys(&:to_sym)) : item
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
{
|
|
86
|
-
type: "tool_result",
|
|
87
|
-
tool_use_id: content[:tool_use_id],
|
|
88
|
-
content: mapped_content
|
|
89
|
-
}
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def map_reasoning_content(content)
|
|
93
|
-
if direction == LlmGateway::DIRECTION_IN
|
|
94
|
-
result = {
|
|
95
|
-
type: "thinking",
|
|
96
|
-
thinking: content[:reasoning]
|
|
97
|
-
}
|
|
98
|
-
result[:signature] = content[:signature] unless content[:signature].nil?
|
|
99
|
-
result
|
|
100
|
-
else
|
|
101
|
-
{
|
|
102
|
-
type: "reasoning",
|
|
103
|
-
reasoning: content[:thinking] || content[:reasoning],
|
|
104
|
-
signature: content[:signature]
|
|
105
|
-
}
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
end
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
|
|
5
|
-
module LlmGateway
|
|
6
|
-
module Adapters
|
|
7
|
-
module OpenAI
|
|
8
|
-
module ChatCompletions
|
|
9
|
-
class BidirectionalMessageMapper
|
|
10
|
-
attr_reader :direction
|
|
11
|
-
|
|
12
|
-
def initialize(direction)
|
|
13
|
-
@direction = direction
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def map_content(content)
|
|
17
|
-
# Convert string content to text format
|
|
18
|
-
content = { type: "text", text: content } unless content.is_a?(Hash)
|
|
19
|
-
case content[:type]
|
|
20
|
-
when "text"
|
|
21
|
-
map_text_content(content)
|
|
22
|
-
when "file"
|
|
23
|
-
map_file_content(content)
|
|
24
|
-
when "image"
|
|
25
|
-
map_image_content(content)
|
|
26
|
-
when "tool_use"
|
|
27
|
-
map_tool_use_content(content)
|
|
28
|
-
when "function"
|
|
29
|
-
map_tool_use_content(content)
|
|
30
|
-
when "tool_result"
|
|
31
|
-
map_tool_result_content(content)
|
|
32
|
-
else
|
|
33
|
-
content
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def parse_tool_arguments(arguments)
|
|
40
|
-
return arguments unless arguments.is_a?(String)
|
|
41
|
-
JSON.parse(arguments, symbolize_names: true)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def map_text_content(content)
|
|
45
|
-
{
|
|
46
|
-
type: "text",
|
|
47
|
-
text: content[:text]
|
|
48
|
-
}
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def map_file_content(content)
|
|
52
|
-
# Map text/plain to application/pdf for OpenAI
|
|
53
|
-
media_type = content[:media_type] == "text/plain" ? "application/pdf" : content[:media_type]
|
|
54
|
-
{
|
|
55
|
-
type: "file",
|
|
56
|
-
file: {
|
|
57
|
-
filename: content[:name],
|
|
58
|
-
file_data: "data:#{media_type};base64,#{Base64.encode64(content[:data])}"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def map_image_content(content)
|
|
64
|
-
{
|
|
65
|
-
type: "image_url",
|
|
66
|
-
image_url: {
|
|
67
|
-
url: "data:#{content[:media_type]};base64,#{content[:data]}"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def map_tool_use_content(content)
|
|
73
|
-
if direction == LlmGateway::DIRECTION_IN
|
|
74
|
-
{
|
|
75
|
-
id: content[:id],
|
|
76
|
-
type: "function",
|
|
77
|
-
function: {
|
|
78
|
-
name: content[:name],
|
|
79
|
-
arguments: content[:input].to_json
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else
|
|
83
|
-
{
|
|
84
|
-
id: content[:id],
|
|
85
|
-
type: "tool_use",
|
|
86
|
-
name: content[:function][:name],
|
|
87
|
-
input: parse_tool_arguments(content[:function][:arguments])
|
|
88
|
-
}
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def map_tool_result_content(content)
|
|
93
|
-
mapped_content = content[:content]
|
|
94
|
-
if mapped_content.is_a?(Array)
|
|
95
|
-
mapped_content = mapped_content.map do |item|
|
|
96
|
-
item.is_a?(Hash) ? map_content(item.transform_keys(&:to_sym)) : item
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
{
|
|
101
|
-
role: "tool",
|
|
102
|
-
tool_call_id: content[:tool_use_id],
|
|
103
|
-
content: mapped_content
|
|
104
|
-
}
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmGateway
|
|
4
|
-
module Adapters
|
|
5
|
-
module OpenAI
|
|
6
|
-
module ChatCompletions
|
|
7
|
-
class OutputMapper
|
|
8
|
-
def self.map(data)
|
|
9
|
-
{
|
|
10
|
-
id: data[:id],
|
|
11
|
-
model: data[:model],
|
|
12
|
-
usage: data[:usage],
|
|
13
|
-
choices: map_choices(data[:choices])
|
|
14
|
-
}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def self.map_choices(choices)
|
|
20
|
-
return [] unless choices
|
|
21
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_OUT)
|
|
22
|
-
|
|
23
|
-
choices.map do |choice|
|
|
24
|
-
message = choice[:message] || {}
|
|
25
|
-
content_item = message_mapper.map_content(message[:content])
|
|
26
|
-
tool_calls = message[:tool_calls] ? message[:tool_calls].map { |tool_call| message_mapper.map_content(tool_call) } : []
|
|
27
|
-
|
|
28
|
-
# Only include content_item if it has actual text content
|
|
29
|
-
content_array = []
|
|
30
|
-
content_array << content_item if LlmGateway::Utils.present?(content_item[:text])
|
|
31
|
-
content_array += tool_calls
|
|
32
|
-
|
|
33
|
-
{ role: message[:role], content: content_array }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
|
|
5
|
-
module LlmGateway
|
|
6
|
-
module Adapters
|
|
7
|
-
module OpenAI
|
|
8
|
-
module Responses
|
|
9
|
-
class BidirectionalMessageMapper < OpenAI::ChatCompletions::BidirectionalMessageMapper
|
|
10
|
-
def map_content(content)
|
|
11
|
-
# Convert string content to text format
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
content = { type: "text", text: content } unless content.is_a?(Hash)
|
|
15
|
-
case content[:type]
|
|
16
|
-
when "text"
|
|
17
|
-
map_text_content(content)
|
|
18
|
-
when "image"
|
|
19
|
-
map_image_content(content)
|
|
20
|
-
when "message"
|
|
21
|
-
map_messages(content)
|
|
22
|
-
when "output_text"
|
|
23
|
-
map_output_text_content(content)
|
|
24
|
-
when "tool_use"
|
|
25
|
-
map_tool_use_content(content)
|
|
26
|
-
when "function_call"
|
|
27
|
-
map_tool_use_content(content)
|
|
28
|
-
when "tool_result"
|
|
29
|
-
map_tool_result_content(content)
|
|
30
|
-
when "reasoning"
|
|
31
|
-
map_reasoning_content(content)
|
|
32
|
-
else
|
|
33
|
-
content
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def map_messages(message)
|
|
40
|
-
message[:content].map { |content| map_content(content) }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def map_tool_result_content(content)
|
|
44
|
-
output = content[:content]
|
|
45
|
-
if output.is_a?(Array)
|
|
46
|
-
output = output.map do |item|
|
|
47
|
-
if item.is_a?(Hash)
|
|
48
|
-
map_content(item.transform_keys(&:to_sym))
|
|
49
|
-
else
|
|
50
|
-
item
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
{
|
|
56
|
-
"type": "function_call_output",
|
|
57
|
-
"call_id": content[:tool_use_id],
|
|
58
|
-
"output": output
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def map_tool_use_content(content)
|
|
63
|
-
if direction == LlmGateway::DIRECTION_OUT
|
|
64
|
-
{ id: content[:call_id], type: "tool_use", name: content[:name], input: parse_tool_arguments(content[:arguments]) }
|
|
65
|
-
else
|
|
66
|
-
{ id: content[:id] }
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def map_output_text_content(content)
|
|
71
|
-
{
|
|
72
|
-
type: direction == LlmGateway::DIRECTION_IN ? "input_text" : "text",
|
|
73
|
-
text: content[:text]
|
|
74
|
-
}
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def map_reasoning_content(content)
|
|
78
|
-
if direction == LlmGateway::DIRECTION_IN
|
|
79
|
-
return { id: content[:id] } if content[:id]
|
|
80
|
-
|
|
81
|
-
content
|
|
82
|
-
else
|
|
83
|
-
{
|
|
84
|
-
type: "reasoning",
|
|
85
|
-
reasoning: normalize_reasoning_text(content[:summary]),
|
|
86
|
-
signature: content[:signature]
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def map_image_content(content)
|
|
92
|
-
{
|
|
93
|
-
type: "input_image",
|
|
94
|
-
image_url: "data:#{content[:media_type]};base64,#{content[:data]}"
|
|
95
|
-
}
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def map_text_content(content)
|
|
99
|
-
{
|
|
100
|
-
type: "input_text",
|
|
101
|
-
text: content[:text]
|
|
102
|
-
}
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def normalize_reasoning_text(summary)
|
|
106
|
-
return summary if summary.is_a?(String)
|
|
107
|
-
return nil unless summary.is_a?(Array)
|
|
108
|
-
return nil if summary.empty?
|
|
109
|
-
|
|
110
|
-
summary.filter_map do |item|
|
|
111
|
-
next item if item.is_a?(String)
|
|
112
|
-
|
|
113
|
-
item[:text] || item[:summary_text] || item[:reasoning]
|
|
114
|
-
end.join("\n")
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
require_relative "bidirectional_message_mapper"
|
|
5
|
-
|
|
6
|
-
module LlmGateway
|
|
7
|
-
module Adapters
|
|
8
|
-
module OpenAI
|
|
9
|
-
module Responses
|
|
10
|
-
class OutputMapper
|
|
11
|
-
def self.map(data)
|
|
12
|
-
{
|
|
13
|
-
id: data[:id],
|
|
14
|
-
model: data[:model],
|
|
15
|
-
usage: data[:usage],
|
|
16
|
-
choices: map_choices(data[:output])
|
|
17
|
-
}
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def self.map_choices(choices)
|
|
23
|
-
return [] unless choices
|
|
24
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_OUT)
|
|
25
|
-
choices.map do |choice|
|
|
26
|
-
content = if choice[:id].start_with?("fc_")
|
|
27
|
-
{
|
|
28
|
-
id: choice[:id],
|
|
29
|
-
role: choice[:role] || "assistant", # tool call doesnt have a role apparently
|
|
30
|
-
content: [ message_mapper.map_content(choice) ].flatten
|
|
31
|
-
}
|
|
32
|
-
else
|
|
33
|
-
content = message_mapper.map_content(choice)
|
|
34
|
-
id = content.delete(:id)
|
|
35
|
-
{
|
|
36
|
-
id: choice[:id] || id,
|
|
37
|
-
role: choice[:role],
|
|
38
|
-
content: [ content ].flatten
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|