llm_gateway 0.3.0 → 0.4.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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +544 -186
  4. data/Rakefile +1 -2
  5. data/docs/migration-guide.md +135 -0
  6. data/lib/llm_gateway/adapters/adapter.rb +173 -0
  7. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
  8. data/lib/llm_gateway/adapters/{claude → anthropic}/bidirectional_message_mapper.rb +31 -3
  9. data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +4 -3
  10. data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
  11. data/lib/llm_gateway/adapters/{claude → anthropic}/output_mapper.rb +1 -1
  12. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
  13. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
  14. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
  15. data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
  16. data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
  17. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
  18. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
  19. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/bidirectional_message_mapper.rb +9 -2
  20. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/input_mapper.rb +1 -6
  21. data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
  22. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
  23. data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/output_mapper.rb +1 -1
  24. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
  25. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
  26. data/lib/llm_gateway/adapters/{open_ai → openai}/file_output_mapper.rb +1 -1
  27. data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
  28. data/lib/llm_gateway/adapters/{open_ai → openai}/responses/bidirectional_message_mapper.rb +52 -4
  29. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
  30. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
  31. data/lib/llm_gateway/adapters/{open_ai → openai}/responses/output_mapper.rb +1 -1
  32. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
  33. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
  34. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
  35. data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
  36. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
  37. data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
  38. data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
  39. data/lib/llm_gateway/adapters/structs.rb +145 -0
  40. data/lib/llm_gateway/base_client.rb +62 -1
  41. data/lib/llm_gateway/client.rb +45 -129
  42. data/lib/llm_gateway/clients/anthropic.rb +167 -0
  43. data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
  44. data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
  45. data/lib/llm_gateway/clients/groq.rb +54 -0
  46. data/lib/llm_gateway/clients/openai.rb +208 -0
  47. data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
  48. data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
  49. data/lib/llm_gateway/errors.rb +21 -0
  50. data/lib/llm_gateway/prompt.rb +12 -1
  51. data/lib/llm_gateway/provider_registry.rb +37 -0
  52. data/lib/llm_gateway/version.rb +1 -1
  53. data/lib/llm_gateway.rb +165 -14
  54. data/scripts/create_anthropic_credentials.rb +106 -0
  55. data/scripts/create_openai_codex_credentials.rb +116 -0
  56. data/scripts/generate_handoff_live_fixture.rb +169 -0
  57. data/scripts/generate_handoff_media_fixture.rb +167 -0
  58. metadata +64 -28
  59. data/lib/llm_gateway/adapters/claude/client.rb +0 -60
  60. data/lib/llm_gateway/adapters/groq/bidirectional_message_mapper.rb +0 -18
  61. data/lib/llm_gateway/adapters/groq/client.rb +0 -58
  62. data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -18
  63. data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -10
  64. data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
  65. data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
  66. data/sample/claude_code_clone/agent.rb +0 -65
  67. data/sample/claude_code_clone/claude_code_clone.rb +0 -40
  68. data/sample/claude_code_clone/prompt.rb +0 -79
  69. data/sample/claude_code_clone/run.rb +0 -47
  70. data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
  71. data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
  72. data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
  73. data/sample/claude_code_clone/tools/read_tool.rb +0 -61
  74. data/sample/claude_code_clone/tools/todowrite_tool.rb +0 -98
@@ -0,0 +1,167 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require "fileutils"
6
+ require_relative "../lib/llm_gateway"
7
+ require_relative "../test/utils/readfile_tool_helper"
8
+
9
+ class HandoffMediaLiveFixtureGenerator
10
+ include ReadfileToolHelper
11
+
12
+ PAIRS = [
13
+ { provider: "openai_apikey_completions", model: "gpt-5.1" },
14
+ { provider: "openai_apikey_responses", model: "gpt-5.4" },
15
+ { provider: "openai_oauth_codex", model: "gpt-5.4" },
16
+ { provider: "anthropic_apikey_messages", model: "claude-sonnet-4-20250514" }
17
+ ].freeze
18
+
19
+ FIXTURE_PATH = File.expand_path("../test/fixtures/handoff_media_live_fixture.json", __dir__)
20
+
21
+ def run
22
+ flat_transcript = []
23
+ skipped = []
24
+
25
+ PAIRS.each do |pair|
26
+ begin
27
+ adapter = load_provider(provider: pair[:provider], model: pair[:model])
28
+ mini_context = generate_mini_context(adapter)
29
+ flat_transcript.concat(mini_context.map(&:to_h))
30
+ puts "[ok] #{pair[:provider]} / #{pair[:model]}"
31
+ rescue StandardError => e
32
+ skipped << pair.merge(error: e.message)
33
+ puts "[skip] #{pair[:provider]} / #{pair[:model]} -> #{e.message}"
34
+ ensure
35
+ LlmGateway.reset_configuration!
36
+ end
37
+ end
38
+
39
+ FileUtils.mkdir_p(File.dirname(FIXTURE_PATH))
40
+ File.write(FIXTURE_PATH, JSON.pretty_generate(flat_transcript) + "\n")
41
+
42
+ puts "\nWrote fixture: #{FIXTURE_PATH}"
43
+ puts "Messages: #{flat_transcript.length}"
44
+ puts "Skipped pairs: #{skipped.length}"
45
+ end
46
+
47
+ private
48
+
49
+ def generate_mini_context(adapter)
50
+ transcript = [
51
+ {
52
+ role: "user",
53
+ content: "Use the readfile tool with path test/fixtures/red-circle.png and tell me what is in the image."
54
+ }
55
+ ]
56
+
57
+ first = adapter.stream(transcript, tools: [ readfile_tool ], reasoning: "high")
58
+ raise first.error_message if first.stop_reason == "error"
59
+
60
+ transcript << first
61
+
62
+ tool_call = first.content.find { |block| block.type == "tool_use" && block.name == "readfile" }
63
+ raise "model did not call readfile" unless tool_call
64
+
65
+ transcript << {
66
+ role: "developer",
67
+ content: [
68
+ {
69
+ type: "tool_result",
70
+ tool_use_id: tool_call.id,
71
+ content: evaluate_readfile(tool_call.input)
72
+ }
73
+ ]
74
+ }
75
+
76
+ second = adapter.stream(
77
+ transcript.map { |m| m.respond_to?(:to_h) ? m.to_h : m },
78
+ tools: [ readfile_tool ],
79
+ reasoning: "high"
80
+ )
81
+ raise second.error_message if second.stop_reason == "error"
82
+
83
+ transcript << second
84
+ transcript
85
+ end
86
+
87
+ def load_provider(provider:, model:)
88
+ config = {
89
+ "provider" => provider,
90
+ "model_key" => model
91
+ }
92
+
93
+ case provider
94
+ when "openai_apikey_completions", "openai_apikey_responses"
95
+ api_key = ENV["OPENAI_API_KEY"].to_s
96
+ raise "missing OPENAI_API_KEY" if api_key.empty?
97
+
98
+ config["api_key"] = api_key
99
+ when "anthropic_apikey_messages"
100
+ api_key = ENV["ANTHROPIC_API_KEY"].to_s
101
+ raise "missing ANTHROPIC_API_KEY" if api_key.empty?
102
+
103
+ config["api_key"] = api_key
104
+ when "openai_oauth_codex"
105
+ creds = load_auth_credentials("openai")
106
+ config["api_key"] = oauth_access_token_for("openai")
107
+ config["account_id"] = creds["account_id"] if creds["account_id"]
108
+ else
109
+ raise ArgumentError, "Unsupported provider: #{provider}"
110
+ end
111
+
112
+ LlmGateway.build_provider(config)
113
+ end
114
+
115
+ def auth_file_path
116
+ File.expand_path(ENV.fetch("LLM_GATEWAY_AUTH_FILE", "~/.config/llm_gateway/auth.json"))
117
+ end
118
+
119
+ def load_auth_credentials(provider)
120
+ path = auth_file_path
121
+ raise "missing auth file at #{path}" unless File.exist?(path)
122
+
123
+ auth = JSON.parse(File.read(path))
124
+ creds = auth[provider]
125
+ raise "missing #{provider} credentials in #{path}" unless creds
126
+
127
+ creds
128
+ end
129
+
130
+ def persist_auth_credentials(provider, attributes)
131
+ path = auth_file_path
132
+ FileUtils.mkdir_p(File.dirname(path))
133
+
134
+ auth = File.exist?(path) ? JSON.parse(File.read(path)) : {}
135
+ auth[provider] ||= {}
136
+ auth[provider].merge!(attributes)
137
+
138
+ File.write(path, JSON.pretty_generate(auth) + "\n")
139
+ end
140
+
141
+ def oauth_access_token_for(provider)
142
+ creds = load_auth_credentials(provider)
143
+
144
+ case provider
145
+ when "openai"
146
+ token = LlmGateway::Clients::OpenAi.new.get_oauth_access_token(
147
+ access_token: creds["access_token"],
148
+ refresh_token: creds["refresh_token"],
149
+ expires_at: creds["expires_at"],
150
+ account_id: creds["account_id"]
151
+ ) do |access_token, refresh_token, expires_at|
152
+ persist_auth_credentials("openai", {
153
+ "access_token" => access_token,
154
+ "refresh_token" => refresh_token,
155
+ "expires_at" => expires_at&.iso8601
156
+ })
157
+ end
158
+
159
+ persist_auth_credentials("openai", { "access_token" => token }) if token != creds["access_token"]
160
+ token
161
+ else
162
+ raise ArgumentError, "Unsupported OAuth provider: #{provider}"
163
+ end
164
+ end
165
+ end
166
+
167
+ HandoffMediaLiveFixtureGenerator.new.run
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm_gateway
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.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-19 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2026-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: dry-struct
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  description: LlmGateway provides a consistent Ruby interface for multiple LLM providers
14
28
  including Claude, OpenAI, and Groq. Features include unified response formatting,
15
29
  error handling, and fluent data mapping.
@@ -25,39 +39,61 @@ files:
25
39
  - LICENSE.txt
26
40
  - README.md
27
41
  - Rakefile
42
+ - docs/migration-guide.md
28
43
  - lib/llm_gateway.rb
29
- - lib/llm_gateway/adapters/claude/bidirectional_message_mapper.rb
30
- - lib/llm_gateway/adapters/claude/client.rb
31
- - lib/llm_gateway/adapters/claude/input_mapper.rb
32
- - lib/llm_gateway/adapters/claude/output_mapper.rb
33
- - lib/llm_gateway/adapters/groq/bidirectional_message_mapper.rb
34
- - lib/llm_gateway/adapters/groq/client.rb
35
- - lib/llm_gateway/adapters/groq/input_mapper.rb
36
- - lib/llm_gateway/adapters/groq/output_mapper.rb
37
- - lib/llm_gateway/adapters/open_ai/chat_completions/bidirectional_message_mapper.rb
38
- - lib/llm_gateway/adapters/open_ai/chat_completions/input_mapper.rb
39
- - lib/llm_gateway/adapters/open_ai/chat_completions/output_mapper.rb
40
- - lib/llm_gateway/adapters/open_ai/client.rb
41
- - lib/llm_gateway/adapters/open_ai/file_output_mapper.rb
42
- - lib/llm_gateway/adapters/open_ai/responses/bidirectional_message_mapper.rb
43
- - lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb
44
- - lib/llm_gateway/adapters/open_ai/responses/output_mapper.rb
44
+ - lib/llm_gateway/adapters/adapter.rb
45
+ - lib/llm_gateway/adapters/anthropic/acts_like_messages.rb
46
+ - lib/llm_gateway/adapters/anthropic/bidirectional_message_mapper.rb
47
+ - lib/llm_gateway/adapters/anthropic/input_mapper.rb
48
+ - lib/llm_gateway/adapters/anthropic/messages_adapter.rb
49
+ - lib/llm_gateway/adapters/anthropic/output_mapper.rb
50
+ - lib/llm_gateway/adapters/anthropic/stream_mapper.rb
51
+ - lib/llm_gateway/adapters/anthropic_option_mapper.rb
52
+ - lib/llm_gateway/adapters/groq/chat_completions_adapter.rb
53
+ - lib/llm_gateway/adapters/groq/option_mapper.rb
54
+ - lib/llm_gateway/adapters/input_message_sanitizer.rb
55
+ - lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb
56
+ - lib/llm_gateway/adapters/openai/acts_like_responses.rb
57
+ - lib/llm_gateway/adapters/openai/chat_completions/bidirectional_message_mapper.rb
58
+ - lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb
59
+ - lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb
60
+ - lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb
61
+ - lib/llm_gateway/adapters/openai/chat_completions/output_mapper.rb
62
+ - lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb
63
+ - lib/llm_gateway/adapters/openai/chat_completions_adapter.rb
64
+ - lib/llm_gateway/adapters/openai/file_output_mapper.rb
65
+ - lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb
66
+ - lib/llm_gateway/adapters/openai/responses/bidirectional_message_mapper.rb
67
+ - lib/llm_gateway/adapters/openai/responses/input_mapper.rb
68
+ - lib/llm_gateway/adapters/openai/responses/option_mapper.rb
69
+ - lib/llm_gateway/adapters/openai/responses/output_mapper.rb
70
+ - lib/llm_gateway/adapters/openai/responses/stream_mapper.rb
71
+ - lib/llm_gateway/adapters/openai/responses_adapter.rb
72
+ - lib/llm_gateway/adapters/openai_codex/input_mapper.rb
73
+ - lib/llm_gateway/adapters/openai_codex/option_mapper.rb
74
+ - lib/llm_gateway/adapters/openai_codex/responses_adapter.rb
75
+ - lib/llm_gateway/adapters/option_mapper.rb
76
+ - lib/llm_gateway/adapters/stream_accumulator.rb
77
+ - lib/llm_gateway/adapters/structs.rb
45
78
  - lib/llm_gateway/base_client.rb
46
79
  - lib/llm_gateway/client.rb
80
+ - lib/llm_gateway/clients/anthropic.rb
81
+ - lib/llm_gateway/clients/claude_code/oauth_flow.rb
82
+ - lib/llm_gateway/clients/claude_code/token_manager.rb
83
+ - lib/llm_gateway/clients/groq.rb
84
+ - lib/llm_gateway/clients/openai.rb
85
+ - lib/llm_gateway/clients/openai_codex/oauth_flow.rb
86
+ - lib/llm_gateway/clients/openai_codex/token_manager.rb
47
87
  - lib/llm_gateway/errors.rb
48
88
  - lib/llm_gateway/prompt.rb
89
+ - lib/llm_gateway/provider_registry.rb
49
90
  - lib/llm_gateway/tool.rb
50
91
  - lib/llm_gateway/utils.rb
51
92
  - lib/llm_gateway/version.rb
52
- - sample/claude_code_clone/agent.rb
53
- - sample/claude_code_clone/claude_code_clone.rb
54
- - sample/claude_code_clone/prompt.rb
55
- - sample/claude_code_clone/run.rb
56
- - sample/claude_code_clone/tools/bash_tool.rb
57
- - sample/claude_code_clone/tools/edit_tool.rb
58
- - sample/claude_code_clone/tools/grep_tool.rb
59
- - sample/claude_code_clone/tools/read_tool.rb
60
- - sample/claude_code_clone/tools/todowrite_tool.rb
93
+ - scripts/create_anthropic_credentials.rb
94
+ - scripts/create_openai_codex_credentials.rb
95
+ - scripts/generate_handoff_live_fixture.rb
96
+ - scripts/generate_handoff_media_fixture.rb
61
97
  - sig/llm_gateway.rbs
62
98
  homepage: https://github.com/Hyper-Unearthing/llm_gateway
63
99
  licenses:
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module Claude
8
- class Client < BaseClient
9
- def initialize(model_key: "claude-3-7-sonnet-20250219", api_key: ENV["ANTHROPIC_API_KEY"])
10
- @base_endpoint = "https://api.anthropic.com/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- max_tokens: max_completion_tokens,
18
- messages: messages
19
- }
20
-
21
- body.merge!(tools: tools) if LlmGateway::Utils.present?(tools)
22
- body.merge!(system: system) if LlmGateway::Utils.present?(system)
23
-
24
- post("messages", body)
25
- end
26
-
27
- def download_file(file_id)
28
- get("files/#{file_id}/content")
29
- end
30
-
31
- def upload_file(filename, content, mime_type = "application/octet-stream")
32
- post_file("files", content, filename, mime_type: mime_type)
33
- end
34
-
35
- private
36
-
37
- def build_headers
38
- {
39
- "anthropic-version" => "2023-06-01",
40
- "content-type" => "application/json",
41
- "x-api-key" => api_key,
42
- "anthropic-beta" => "code-execution-2025-05-22,files-api-2025-04-14"
43
- }
44
- end
45
-
46
- def handle_client_specific_errors(response, error)
47
- case response.code.to_i
48
- when 400
49
- if error["message"]&.start_with?("prompt is too long")
50
- raise Errors::PromptTooLong.new(error["message"], error["type"])
51
- end
52
- end
53
-
54
- # If we get here, we didn't handle it specifically
55
- raise Errors::APIStatusError.new(error["message"], error["type"])
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../open_ai/chat_completions/bidirectional_message_mapper"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module Groq
8
- class BidirectionalMessageMapper < OpenAi::ChatCompletions::BidirectionalMessageMapper
9
- private
10
-
11
- def map_file_content(content)
12
- # Groq doesn't support files, return as text
13
- content[:text] || "[File: #{content[:name]}]"
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,58 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module Groq
8
- class Client < BaseClient
9
- def initialize(model_key: "gemma2-9b-it", api_key: ENV["GROQ_API_KEY"])
10
- @base_endpoint = "https://api.groq.com/openai/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- messages: system + messages,
18
- temperature: 0,
19
- max_completion_tokens: max_completion_tokens,
20
- response_format: response_format,
21
- tools: tools
22
- }
23
-
24
- post("chat/completions", body)
25
- end
26
-
27
- private
28
-
29
- def build_headers
30
- {
31
- "content-type" => "application/json",
32
- "Authorization" => "Bearer #{api_key}"
33
- }
34
- end
35
-
36
- def handle_client_specific_errors(response, error)
37
- # Groq likely uses 'code' like OpenAI since it's OpenAI-compatible
38
- error_code = error["code"]
39
-
40
- case response.code.to_i
41
-
42
- when 413
43
- if error["message"]&.start_with?("Request too large")
44
- raise Errors::PromptTooLong.new(error["message"], error["type"])
45
- end
46
- when 429
47
- raise Errors::RateLimitError.new(error["type"], error_code) if error_code == "rate_limit_exceeded"
48
-
49
- raise Errors::OverloadError.new(error["message"], error_code)
50
- end
51
-
52
- # If we get here, we didn't handle it specifically
53
- raise Errors::APIStatusError.new(error["message"], error_code)
54
- end
55
- end
56
- end
57
- end
58
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "bidirectional_message_mapper"
4
- require_relative "../open_ai/chat_completions/input_mapper"
5
-
6
- module LlmGateway
7
- module Adapters
8
- module Groq
9
- class InputMapper < OpenAi::ChatCompletions::InputMapper
10
- private
11
-
12
- def self.map_system(system)
13
- system
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LlmGateway
4
- module Adapters
5
- module Groq
6
- class OutputMapper < LlmGateway::Adapters::OpenAi::ChatCompletions::OutputMapper
7
- end
8
- end
9
- end
10
- end
@@ -1,80 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../../base_client"
4
-
5
- module LlmGateway
6
- module Adapters
7
- module OpenAi
8
- class Client < BaseClient
9
- def initialize(model_key: "gpt-4o", api_key: ENV["OPENAI_API_KEY"])
10
- @base_endpoint = "https://api.openai.com/v1"
11
- super(model_key: model_key, api_key: api_key)
12
- end
13
-
14
- def chat(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
15
- body = {
16
- model: model_key,
17
- messages: system + messages,
18
- max_completion_tokens: max_completion_tokens
19
- }
20
- body[:tools] = tools if tools
21
-
22
- post("chat/completions", body)
23
- end
24
-
25
- def responses(messages, response_format: { type: "text" }, tools: nil, system: [], max_completion_tokens: 4096)
26
- body = {
27
- model: model_key,
28
- max_output_tokens: max_completion_tokens,
29
- input: messages.flatten
30
- }
31
- body[:instructions] = system[0][:content] if system.any?
32
- body[:tools] = tools if tools
33
- result = post("responses", body)
34
- result
35
- end
36
-
37
-
38
- def download_file(file_id)
39
- get("files/#{file_id}/content")
40
- end
41
-
42
- def generate_embeddings(input)
43
- body = {
44
- input:,
45
- model: model_key
46
- }
47
- post("embeddings", body)
48
- end
49
-
50
- def upload_file(filename, content, mime_type = "application/octet-stream", purpose: "user_data")
51
- post_file("files", content, filename, purpose: purpose, mime_type: mime_type)
52
- end
53
-
54
- private
55
-
56
- def build_headers
57
- {
58
- "content-type" => "application/json",
59
- "Authorization" => "Bearer #{api_key}"
60
- }
61
- end
62
-
63
- def handle_client_specific_errors(response, error)
64
- # OpenAI uses 'code' instead of 'type' for error codes
65
- error_code = error["code"]
66
-
67
- case response.code.to_i
68
- when 429
69
- raise Errors::RateLimitError.new(error["message"], error_code)
70
- when 503
71
- raise Errors::OverloadError.new(error["message"], error_code)
72
- end
73
-
74
- # If we get here, we didn't handle it specifically
75
- raise Errors::APIStatusError.new(error["message"], error_code)
76
- end
77
- end
78
- end
79
- end
80
- end
@@ -1,62 +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 InputMapper < OpenAi::ChatCompletions::InputMapper
11
- def self.message_mapper
12
- BidirectionalMessageMapper.new(LlmGateway::DIRECTION_IN)
13
- end
14
-
15
- def self.map_tools(tools)
16
- return tools unless tools
17
-
18
- tools.map do |tool|
19
- {
20
- type: "function",
21
- name: tool[:name],
22
- description: tool[:description],
23
- parameters: tool[:input_schema]
24
- }
25
- end
26
- end
27
-
28
- def self.map_messages(messages)
29
- return messages unless messages
30
- mapper = message_mapper
31
-
32
- # First map messages like Claude
33
- messages.map do |msg|
34
- if msg[:id]
35
- msg = msg.merge(role: "assistant")
36
- msg.slice(:id)
37
- else
38
- content = if msg[:content].is_a?(Array)
39
- msg[:content].map do |content|
40
- mapper.map_content(content)
41
- end
42
- elsif msg[:id]
43
- mapper.map_content(msg)
44
- else
45
- [ mapper.map_content(msg[:content]) ]
46
- end
47
- if msg.dig(:content).is_a?(Array) && msg.dig(:content, 0, :type) == "tool_result"
48
- content
49
- else
50
- {
51
- role: msg[:role],
52
- content: content
53
- }
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
61
- end
62
- end
@@ -1,65 +0,0 @@
1
- class Agent
2
- def initialize(prompt_class, model, api_key)
3
- @prompt_class = prompt_class
4
- @model = model
5
- @api_key = api_key
6
- @transcript = []
7
- end
8
-
9
- def run(user_input, &block)
10
- @transcript << { role: 'user', content: [ { type: 'text', text: user_input } ] }
11
-
12
- begin
13
- prompt = @prompt_class.new(@model, @transcript, @api_key)
14
- result = prompt.post
15
- process_response(result[:choices][0][:content], &block)
16
- rescue => e
17
- yield({ type: 'error', message: e.message }) if block_given?
18
- raise e
19
- end
20
- end
21
-
22
- private
23
-
24
- def process_response(response, &block)
25
- @transcript << { role: 'assistant', content: response }
26
-
27
- response.each do |message|
28
- yield(message) if block_given?
29
-
30
- if message[:type] == 'text'
31
- # Text response processed
32
- elsif message[:type] == 'tool_use'
33
- result = handle_tool_use(message)
34
-
35
- tool_result = {
36
- type: 'tool_result',
37
- tool_use_id: message[:id],
38
- content: result
39
- }
40
- @transcript << { role: 'user', content: [ tool_result ] }
41
-
42
- yield(tool_result) if block_given?
43
-
44
- follow_up_prompt = @prompt_class.new(@model, @transcript, @api_key)
45
- follow_up = follow_up_prompt.post
46
-
47
- process_response(follow_up[:choices][0][:content], &block) if follow_up[:choices][0][:content]
48
- end
49
- end
50
-
51
- response
52
- end
53
-
54
- def handle_tool_use(message)
55
- tool_class = @prompt_class.find_tool(message[:name])
56
- if tool_class
57
- tool = tool_class.new
58
- tool.execute(message[:input])
59
- else
60
- "Unknown tool: #{message[:name]}"
61
- end
62
- rescue StandardError => e
63
- "Error executing tool: #{e.message}"
64
- end
65
- end