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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/README.md +544 -186
- data/Rakefile +1 -2
- data/docs/migration-guide.md +135 -0
- data/lib/llm_gateway/adapters/adapter.rb +173 -0
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +23 -0
- data/lib/llm_gateway/adapters/{claude → anthropic}/bidirectional_message_mapper.rb +31 -3
- data/lib/llm_gateway/adapters/{claude → anthropic}/input_mapper.rb +4 -3
- data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
- data/lib/llm_gateway/adapters/{claude → anthropic}/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +110 -0
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +53 -0
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +47 -0
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +27 -0
- data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +22 -0
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +31 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/bidirectional_message_mapper.rb +9 -2
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/input_mapper.rb +1 -6
- data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/chat_completions/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +242 -0
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/file_output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/responses/bidirectional_message_mapper.rb +52 -4
- data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +106 -0
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +41 -0
- data/lib/llm_gateway/adapters/{open_ai → openai}/responses/output_mapper.rb +1 -1
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +340 -0
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +20 -0
- data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
- data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
- data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +38 -0
- data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
- data/lib/llm_gateway/adapters/stream_accumulator.rb +91 -0
- data/lib/llm_gateway/adapters/structs.rb +145 -0
- data/lib/llm_gateway/base_client.rb +62 -1
- data/lib/llm_gateway/client.rb +45 -129
- data/lib/llm_gateway/clients/anthropic.rb +167 -0
- data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
- data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
- data/lib/llm_gateway/clients/groq.rb +54 -0
- data/lib/llm_gateway/clients/openai.rb +208 -0
- data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
- data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
- data/lib/llm_gateway/errors.rb +21 -0
- data/lib/llm_gateway/prompt.rb +12 -1
- data/lib/llm_gateway/provider_registry.rb +37 -0
- data/lib/llm_gateway/version.rb +1 -1
- data/lib/llm_gateway.rb +165 -14
- data/scripts/create_anthropic_credentials.rb +106 -0
- data/scripts/create_openai_codex_credentials.rb +116 -0
- data/scripts/generate_handoff_live_fixture.rb +169 -0
- data/scripts/generate_handoff_media_fixture.rb +167 -0
- metadata +64 -28
- data/lib/llm_gateway/adapters/claude/client.rb +0 -60
- data/lib/llm_gateway/adapters/groq/bidirectional_message_mapper.rb +0 -18
- data/lib/llm_gateway/adapters/groq/client.rb +0 -58
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +0 -18
- data/lib/llm_gateway/adapters/groq/output_mapper.rb +0 -10
- data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
- data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
- data/sample/claude_code_clone/agent.rb +0 -65
- data/sample/claude_code_clone/claude_code_clone.rb +0 -40
- data/sample/claude_code_clone/prompt.rb +0 -79
- data/sample/claude_code_clone/run.rb +0 -47
- data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
- data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
- data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
- data/sample/claude_code_clone/tools/read_tool.rb +0 -61
- 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.
|
|
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:
|
|
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/
|
|
30
|
-
- lib/llm_gateway/adapters/
|
|
31
|
-
- lib/llm_gateway/adapters/
|
|
32
|
-
- lib/llm_gateway/adapters/
|
|
33
|
-
- lib/llm_gateway/adapters/
|
|
34
|
-
- lib/llm_gateway/adapters/
|
|
35
|
-
- lib/llm_gateway/adapters/
|
|
36
|
-
- lib/llm_gateway/adapters/
|
|
37
|
-
- lib/llm_gateway/adapters/
|
|
38
|
-
- lib/llm_gateway/adapters/
|
|
39
|
-
- lib/llm_gateway/adapters/
|
|
40
|
-
- lib/llm_gateway/adapters/
|
|
41
|
-
- lib/llm_gateway/adapters/
|
|
42
|
-
- lib/llm_gateway/adapters/
|
|
43
|
-
- lib/llm_gateway/adapters/
|
|
44
|
-
- lib/llm_gateway/adapters/
|
|
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
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
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,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
|