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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.pi/skills/live-provider-testing/SKILL.md +183 -0
  3. data/.pi/skills/options-development/SKILL.md +131 -0
  4. data/CHANGELOG.md +17 -0
  5. data/README.md +16 -0
  6. data/Rakefile +1 -0
  7. data/lib/llm_gateway/adapters/adapter.rb +2 -35
  8. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +0 -2
  9. data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +106 -27
  10. data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +0 -33
  11. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +31 -46
  12. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +48 -6
  13. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +3 -2
  14. data/lib/llm_gateway/adapters/groq/input_mapper.rb +44 -0
  15. data/lib/llm_gateway/adapters/groq/option_mapper.rb +89 -4
  16. data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
  17. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +0 -2
  18. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +0 -6
  19. data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +135 -72
  20. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +100 -10
  21. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +169 -170
  22. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +0 -1
  23. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +128 -68
  24. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +99 -10
  25. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +81 -271
  26. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +0 -1
  27. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +3 -3
  28. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +0 -5
  29. data/lib/llm_gateway/adapters/stream_mapper.rb +50 -0
  30. data/lib/llm_gateway/client.rb +10 -66
  31. data/lib/llm_gateway/clients/groq.rb +13 -1
  32. data/lib/llm_gateway/version.rb +1 -1
  33. data/lib/llm_gateway.rb +2 -8
  34. metadata +7 -10
  35. data/lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb +0 -111
  36. data/lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb +0 -110
  37. data/lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb +0 -40
  38. data/lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb +0 -120
  39. data/lib/llm_gateway/adapters/openai/responses/output_mapper.rb +0 -47
  40. data/lib/llm_gateway/adapters/stream_accumulator.rb +0 -91
  41. data/scripts/generate_handoff_live_fixture.rb +0 -169
  42. data/scripts/generate_handoff_media_fixture.rb +0 -167
@@ -1,24 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+
3
4
  module LlmGateway
4
5
  class Client
5
- def self.chat(model, message, tools: nil, system: nil, api_key: nil, refresh_token: nil, expires_at: nil, **options)
6
- adapter = build_adapter_from_model(model, api_key: api_key, refresh_token: refresh_token, expires_at: expires_at)
7
- adapter.chat(message, tools: tools, system: system, **options)
8
- end
9
-
10
- def self.responses(model, message, tools: nil, system: nil, api_key: nil, **options)
11
- adapter = build_adapter_from_model(model, api_key: api_key, api: "responses")
12
- adapter.chat(message, tools: tools, system: system, **options)
13
- end
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-20b", api_key: ENV["GROQ_API_KEY"])
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LlmGateway
4
- VERSION = "0.4.0"
4
+ VERSION = "0.5.0"
5
5
  end
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.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-17 00:00:00.000000000 Z
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/stream_accumulator.rb
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