llm_gateway 0.3.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 +43 -0
- data/README.md +559 -185
- data/Rakefile +2 -2
- data/docs/migration-guide.md +135 -0
- data/lib/llm_gateway/adapters/adapter.rb +140 -0
- data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +21 -0
- data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +137 -0
- data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
- data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +17 -0
- data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +95 -0
- data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +95 -0
- data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +48 -0
- data/lib/llm_gateway/adapters/groq/input_mapper.rb +32 -6
- data/lib/llm_gateway/adapters/groq/option_mapper.rb +112 -0
- data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
- data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
- data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +20 -0
- data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +25 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +168 -0
- 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 +129 -0
- data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +241 -0
- data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +19 -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/openai/responses/input_mapper.rb +166 -0
- data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +130 -0
- data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +150 -0
- data/lib/llm_gateway/adapters/openai/responses_adapter.rb +19 -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 +33 -0
- data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
- data/lib/llm_gateway/adapters/stream_mapper.rb +50 -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 +18 -158
- 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 +66 -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 +162 -17
- data/scripts/create_anthropic_credentials.rb +106 -0
- data/scripts/create_openai_codex_credentials.rb +116 -0
- metadata +60 -27
- data/lib/llm_gateway/adapters/claude/bidirectional_message_mapper.rb +0 -83
- data/lib/llm_gateway/adapters/claude/client.rb +0 -60
- data/lib/llm_gateway/adapters/claude/input_mapper.rb +0 -57
- data/lib/llm_gateway/adapters/claude/output_mapper.rb +0 -50
- 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/output_mapper.rb +0 -10
- data/lib/llm_gateway/adapters/open_ai/chat_completions/bidirectional_message_mapper.rb +0 -103
- data/lib/llm_gateway/adapters/open_ai/chat_completions/input_mapper.rb +0 -110
- data/lib/llm_gateway/adapters/open_ai/chat_completions/output_mapper.rb +0 -40
- data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
- data/lib/llm_gateway/adapters/open_ai/responses/bidirectional_message_mapper.rb +0 -72
- data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
- data/lib/llm_gateway/adapters/open_ai/responses/output_mapper.rb +0 -47
- 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,106 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "json"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "../lib/llm_gateway"
|
|
8
|
+
|
|
9
|
+
module Scripts
|
|
10
|
+
class CreateAnthropicCredentials
|
|
11
|
+
def initialize(argv)
|
|
12
|
+
@options = {
|
|
13
|
+
client_id: LlmGateway::Clients::ClaudeCode::OAuthFlow::CLIENT_ID,
|
|
14
|
+
scopes: LlmGateway::Clients::ClaudeCode::OAuthFlow::DEFAULT_SCOPES,
|
|
15
|
+
output: File.expand_path(ENV.fetch("LLM_GATEWAY_AUTH_FILE", "~/.config/llm_gateway/auth.json"))
|
|
16
|
+
}
|
|
17
|
+
parse_options(argv)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def run
|
|
21
|
+
flow = LlmGateway::Clients::ClaudeCode::OAuthFlow.new(
|
|
22
|
+
client_id: @options[:client_id],
|
|
23
|
+
scopes: @options[:scopes]
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
auth = flow.start
|
|
27
|
+
|
|
28
|
+
puts "Anthropic OAuth setup"
|
|
29
|
+
puts "Redirect URI: #{flow.redirect_uri}"
|
|
30
|
+
puts ""
|
|
31
|
+
puts "Open this URL in your browser:"
|
|
32
|
+
puts auth[:authorization_url]
|
|
33
|
+
puts ""
|
|
34
|
+
puts "After authenticating, paste either:"
|
|
35
|
+
puts "- the full callback URL"
|
|
36
|
+
puts "- or the legacy code#state value"
|
|
37
|
+
print "> "
|
|
38
|
+
|
|
39
|
+
callback_value = $stdin.gets.to_s.strip
|
|
40
|
+
tokens = flow.exchange_code(callback_value, auth[:code_verifier], state: auth[:state])
|
|
41
|
+
|
|
42
|
+
credentials = {
|
|
43
|
+
client_id: flow.client_id,
|
|
44
|
+
access_token: tokens[:access_token],
|
|
45
|
+
refresh_token: tokens[:refresh_token],
|
|
46
|
+
expires_at: tokens[:expires_at]&.iso8601
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
persist_credentials("anthropic", credentials)
|
|
50
|
+
|
|
51
|
+
puts "Credentials:"
|
|
52
|
+
puts JSON.pretty_generate(credentials)
|
|
53
|
+
puts ""
|
|
54
|
+
puts "Environment exports:"
|
|
55
|
+
puts "export ANTHROPIC_ACCESS_TOKEN=#{shell_escape(tokens[:access_token])}"
|
|
56
|
+
puts "export ANTHROPIC_REFRESH_TOKEN=#{shell_escape(tokens[:refresh_token])}"
|
|
57
|
+
puts "export ANTHROPIC_EXPIRES_AT=#{shell_escape(tokens[:expires_at]&.iso8601.to_s)}"
|
|
58
|
+
rescue Interrupt
|
|
59
|
+
warn "Aborted."
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def parse_options(argv)
|
|
66
|
+
OptionParser.new do |opts|
|
|
67
|
+
opts.banner = "Usage: ruby scripts/create_anthropic_credentials.rb [options]"
|
|
68
|
+
|
|
69
|
+
opts.on("--client-id ID", "Anthropic OAuth client id") do |value|
|
|
70
|
+
@options[:client_id] = value
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
opts.on("--scopes SCOPES", "OAuth scopes") do |value|
|
|
74
|
+
@options[:scopes] = value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
opts.on("--output PATH", "Write credentials JSON to PATH") do |value|
|
|
78
|
+
@options[:output] = value
|
|
79
|
+
end
|
|
80
|
+
end.parse!(argv)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def persist_credentials(provider, credentials)
|
|
84
|
+
output_path = File.expand_path(@options[:output])
|
|
85
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
86
|
+
|
|
87
|
+
existing = if File.exist?(output_path)
|
|
88
|
+
JSON.parse(File.read(output_path))
|
|
89
|
+
else
|
|
90
|
+
{}
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
existing[provider] = credentials
|
|
94
|
+
File.write(output_path, JSON.pretty_generate(existing) + "\n")
|
|
95
|
+
puts "Credentials written to #{output_path}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def shell_escape(value)
|
|
99
|
+
return "''" if value.nil? || value.empty?
|
|
100
|
+
|
|
101
|
+
"'#{value.to_s.gsub("'", %q('\''))}'"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
Scripts::CreateAnthropicCredentials.new(ARGV).run
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "json"
|
|
6
|
+
require "fileutils"
|
|
7
|
+
require_relative "../lib/llm_gateway"
|
|
8
|
+
|
|
9
|
+
module Scripts
|
|
10
|
+
class CreateOpenAiCodexCredentials
|
|
11
|
+
def initialize(argv)
|
|
12
|
+
@options = {
|
|
13
|
+
client_id: LlmGateway::Clients::OpenAi::OAuthFlow::CLIENT_ID,
|
|
14
|
+
redirect_uri: LlmGateway::Clients::OpenAi::OAuthFlow::REDIRECT_URI,
|
|
15
|
+
scope: LlmGateway::Clients::OpenAi::OAuthFlow::SCOPE,
|
|
16
|
+
output: File.expand_path(ENV.fetch("LLM_GATEWAY_AUTH_FILE", "~/.config/llm_gateway/auth.json"))
|
|
17
|
+
}
|
|
18
|
+
parse_options(argv)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run
|
|
22
|
+
flow = LlmGateway::Clients::OpenAi::OAuthFlow.new(
|
|
23
|
+
client_id: @options[:client_id],
|
|
24
|
+
redirect_uri: @options[:redirect_uri],
|
|
25
|
+
scope: @options[:scope]
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
auth = flow.start
|
|
29
|
+
|
|
30
|
+
puts "OpenAI Codex OAuth setup"
|
|
31
|
+
puts "Redirect URI: #{flow.redirect_uri}"
|
|
32
|
+
puts ""
|
|
33
|
+
puts "Open this URL in your browser:"
|
|
34
|
+
puts auth[:authorization_url]
|
|
35
|
+
puts ""
|
|
36
|
+
puts "After authenticating, the browser will redirect to localhost (the page won't load)."
|
|
37
|
+
puts "Paste either:"
|
|
38
|
+
puts " - the full callback URL (http://localhost:1455/auth/callback?code=...)"
|
|
39
|
+
puts " - or just the code"
|
|
40
|
+
print "> "
|
|
41
|
+
|
|
42
|
+
callback_value = $stdin.gets.to_s.strip
|
|
43
|
+
tokens = flow.exchange_code(callback_value, auth[:code_verifier], expected_state: auth[:state])
|
|
44
|
+
|
|
45
|
+
credentials = {
|
|
46
|
+
client_id: flow.client_id,
|
|
47
|
+
account_id: tokens[:account_id],
|
|
48
|
+
access_token: tokens[:access_token],
|
|
49
|
+
refresh_token: tokens[:refresh_token],
|
|
50
|
+
expires_at: tokens[:expires_at]&.iso8601
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
persist_credentials("openai", credentials)
|
|
54
|
+
|
|
55
|
+
puts ""
|
|
56
|
+
puts "Credentials:"
|
|
57
|
+
puts JSON.pretty_generate(credentials)
|
|
58
|
+
puts ""
|
|
59
|
+
puts "Environment exports:"
|
|
60
|
+
puts "export OPENAI_CODEX_ACCOUNT_ID=#{shell_escape(tokens[:account_id].to_s)}"
|
|
61
|
+
puts "export OPENAI_CODEX_ACCESS_TOKEN=#{shell_escape(tokens[:access_token])}"
|
|
62
|
+
puts "export OPENAI_CODEX_REFRESH_TOKEN=#{shell_escape(tokens[:refresh_token])}"
|
|
63
|
+
puts "export OPENAI_CODEX_EXPIRES_AT=#{shell_escape(tokens[:expires_at]&.iso8601.to_s)}"
|
|
64
|
+
rescue Interrupt
|
|
65
|
+
warn "Aborted."
|
|
66
|
+
exit 1
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def parse_options(argv)
|
|
72
|
+
OptionParser.new do |opts|
|
|
73
|
+
opts.banner = "Usage: ruby scripts/create_openai_codex_credentials.rb [options]"
|
|
74
|
+
|
|
75
|
+
opts.on("--client-id ID", "OpenAI OAuth client id") do |value|
|
|
76
|
+
@options[:client_id] = value
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
opts.on("--redirect-uri URI", "OAuth redirect URI") do |value|
|
|
80
|
+
@options[:redirect_uri] = value
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
opts.on("--scope SCOPE", "OAuth scopes (space-separated)") do |value|
|
|
84
|
+
@options[:scope] = value
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
opts.on("--output PATH", "Write credentials JSON to PATH") do |value|
|
|
88
|
+
@options[:output] = value
|
|
89
|
+
end
|
|
90
|
+
end.parse!(argv)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def persist_credentials(provider, credentials)
|
|
94
|
+
output_path = File.expand_path(@options[:output])
|
|
95
|
+
FileUtils.mkdir_p(File.dirname(output_path))
|
|
96
|
+
|
|
97
|
+
existing = if File.exist?(output_path)
|
|
98
|
+
JSON.parse(File.read(output_path))
|
|
99
|
+
else
|
|
100
|
+
{}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
existing[provider] = credentials
|
|
104
|
+
File.write(output_path, JSON.pretty_generate(existing) + "\n")
|
|
105
|
+
puts "Credentials written to #{output_path}"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def shell_escape(value)
|
|
109
|
+
return "''" if value.nil? || value.empty?
|
|
110
|
+
|
|
111
|
+
"'#{value.to_s.gsub("'", %q('\''))}'"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
Scripts::CreateOpenAiCodexCredentials.new(ARGV).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.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:
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2026-05-20 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.
|
|
@@ -19,45 +33,64 @@ executables: []
|
|
|
19
33
|
extensions: []
|
|
20
34
|
extra_rdoc_files: []
|
|
21
35
|
files:
|
|
36
|
+
- ".pi/skills/live-provider-testing/SKILL.md"
|
|
37
|
+
- ".pi/skills/options-development/SKILL.md"
|
|
22
38
|
- ".rubocop.yml"
|
|
23
39
|
- CHANGELOG.md
|
|
24
40
|
- CODE_OF_CONDUCT.md
|
|
25
41
|
- LICENSE.txt
|
|
26
42
|
- README.md
|
|
27
43
|
- Rakefile
|
|
44
|
+
- docs/migration-guide.md
|
|
28
45
|
- 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/
|
|
46
|
+
- lib/llm_gateway/adapters/adapter.rb
|
|
47
|
+
- lib/llm_gateway/adapters/anthropic/acts_like_messages.rb
|
|
48
|
+
- lib/llm_gateway/adapters/anthropic/input_mapper.rb
|
|
49
|
+
- lib/llm_gateway/adapters/anthropic/messages_adapter.rb
|
|
50
|
+
- lib/llm_gateway/adapters/anthropic/output_mapper.rb
|
|
51
|
+
- lib/llm_gateway/adapters/anthropic/stream_mapper.rb
|
|
52
|
+
- lib/llm_gateway/adapters/anthropic_option_mapper.rb
|
|
53
|
+
- lib/llm_gateway/adapters/groq/chat_completions_adapter.rb
|
|
35
54
|
- lib/llm_gateway/adapters/groq/input_mapper.rb
|
|
36
|
-
- lib/llm_gateway/adapters/groq/
|
|
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/
|
|
55
|
+
- lib/llm_gateway/adapters/groq/option_mapper.rb
|
|
56
|
+
- lib/llm_gateway/adapters/input_message_sanitizer.rb
|
|
57
|
+
- lib/llm_gateway/adapters/normalized_stream_accumulator.rb
|
|
58
|
+
- lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb
|
|
59
|
+
- lib/llm_gateway/adapters/openai/acts_like_responses.rb
|
|
60
|
+
- lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb
|
|
61
|
+
- lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb
|
|
62
|
+
- lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb
|
|
63
|
+
- lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb
|
|
64
|
+
- lib/llm_gateway/adapters/openai/chat_completions_adapter.rb
|
|
65
|
+
- lib/llm_gateway/adapters/openai/file_output_mapper.rb
|
|
66
|
+
- lib/llm_gateway/adapters/openai/prompt_cache_option_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/stream_mapper.rb
|
|
70
|
+
- lib/llm_gateway/adapters/openai/responses_adapter.rb
|
|
71
|
+
- lib/llm_gateway/adapters/openai_codex/input_mapper.rb
|
|
72
|
+
- lib/llm_gateway/adapters/openai_codex/option_mapper.rb
|
|
73
|
+
- lib/llm_gateway/adapters/openai_codex/responses_adapter.rb
|
|
74
|
+
- lib/llm_gateway/adapters/option_mapper.rb
|
|
75
|
+
- lib/llm_gateway/adapters/stream_mapper.rb
|
|
76
|
+
- lib/llm_gateway/adapters/structs.rb
|
|
45
77
|
- lib/llm_gateway/base_client.rb
|
|
46
78
|
- lib/llm_gateway/client.rb
|
|
79
|
+
- lib/llm_gateway/clients/anthropic.rb
|
|
80
|
+
- lib/llm_gateway/clients/claude_code/oauth_flow.rb
|
|
81
|
+
- lib/llm_gateway/clients/claude_code/token_manager.rb
|
|
82
|
+
- lib/llm_gateway/clients/groq.rb
|
|
83
|
+
- lib/llm_gateway/clients/openai.rb
|
|
84
|
+
- lib/llm_gateway/clients/openai_codex/oauth_flow.rb
|
|
85
|
+
- lib/llm_gateway/clients/openai_codex/token_manager.rb
|
|
47
86
|
- lib/llm_gateway/errors.rb
|
|
48
87
|
- lib/llm_gateway/prompt.rb
|
|
88
|
+
- lib/llm_gateway/provider_registry.rb
|
|
49
89
|
- lib/llm_gateway/tool.rb
|
|
50
90
|
- lib/llm_gateway/utils.rb
|
|
51
91
|
- lib/llm_gateway/version.rb
|
|
52
|
-
-
|
|
53
|
-
-
|
|
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
|
|
92
|
+
- scripts/create_anthropic_credentials.rb
|
|
93
|
+
- scripts/create_openai_codex_credentials.rb
|
|
61
94
|
- sig/llm_gateway.rbs
|
|
62
95
|
homepage: https://github.com/Hyper-Unearthing/llm_gateway
|
|
63
96
|
licenses:
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmGateway
|
|
4
|
-
module Adapters
|
|
5
|
-
module Claude
|
|
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
|
-
else
|
|
29
|
-
content
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
private
|
|
34
|
-
|
|
35
|
-
def map_text_content(content)
|
|
36
|
-
{
|
|
37
|
-
type: "text",
|
|
38
|
-
text: content[:text]
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def map_file_content(content)
|
|
43
|
-
{
|
|
44
|
-
type: "document",
|
|
45
|
-
source: {
|
|
46
|
-
data: content[:data],
|
|
47
|
-
type: "text",
|
|
48
|
-
media_type: content[:media_type]
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def map_image_content(content)
|
|
54
|
-
{
|
|
55
|
-
type: "image",
|
|
56
|
-
source: {
|
|
57
|
-
data: content[:data],
|
|
58
|
-
type: "base64",
|
|
59
|
-
media_type: content[:media_type]
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def map_tool_use_content(content)
|
|
65
|
-
{
|
|
66
|
-
type: "tool_use",
|
|
67
|
-
id: content[:id],
|
|
68
|
-
name: content[:name],
|
|
69
|
-
input: content[:input]
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def map_tool_result_content(content)
|
|
74
|
-
{
|
|
75
|
-
type: "tool_result",
|
|
76
|
-
tool_use_id: content[:tool_use_id],
|
|
77
|
-
content: content[:content]
|
|
78
|
-
}
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -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,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "bidirectional_message_mapper"
|
|
4
|
-
|
|
5
|
-
module LlmGateway
|
|
6
|
-
module Adapters
|
|
7
|
-
module Claude
|
|
8
|
-
class InputMapper
|
|
9
|
-
def self.map(data)
|
|
10
|
-
{
|
|
11
|
-
messages: map_messages(data[:messages]),
|
|
12
|
-
response_format: data[:response_format],
|
|
13
|
-
tools: data[:tools],
|
|
14
|
-
system: map_system(data[:system])
|
|
15
|
-
}
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def self.map_messages(messages)
|
|
21
|
-
return messages unless messages
|
|
22
|
-
|
|
23
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_IN)
|
|
24
|
-
|
|
25
|
-
messages.map do |msg|
|
|
26
|
-
msg = msg.merge(role: "user") if msg[:role] == "developer"
|
|
27
|
-
|
|
28
|
-
content = if msg[:content].is_a?(Array)
|
|
29
|
-
msg[:content].map do |content|
|
|
30
|
-
message_mapper.map_content(content)
|
|
31
|
-
end
|
|
32
|
-
else
|
|
33
|
-
[ message_mapper.map_content(msg[:content]) ]
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
{
|
|
37
|
-
role: msg[:role],
|
|
38
|
-
content: content
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.map_system(system)
|
|
44
|
-
if !system || system.empty?
|
|
45
|
-
nil
|
|
46
|
-
elsif system.length == 1 && system.first[:role] == "system"
|
|
47
|
-
# If we have a single system message, convert to Claude format
|
|
48
|
-
[ { type: "text", text: system.first[:content] } ]
|
|
49
|
-
else
|
|
50
|
-
# For multiple messages or non-standard format, pass through
|
|
51
|
-
system
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmGateway
|
|
4
|
-
module Adapters
|
|
5
|
-
module Claude
|
|
6
|
-
class FileOutputMapper
|
|
7
|
-
def self.map(data)
|
|
8
|
-
data.delete(:type) # Didnt see much value in this only option is "file"
|
|
9
|
-
data.merge(
|
|
10
|
-
expires_at: nil, # came from open ai api
|
|
11
|
-
purpose: "user_data", # came from open ai api
|
|
12
|
-
)
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
class OutputMapper
|
|
17
|
-
def self.map(data)
|
|
18
|
-
{
|
|
19
|
-
id: data[:id],
|
|
20
|
-
model: data[:model],
|
|
21
|
-
usage: data[:usage],
|
|
22
|
-
choices: map_choices(data)
|
|
23
|
-
}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
private
|
|
27
|
-
|
|
28
|
-
def self.map_choices(data)
|
|
29
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_OUT)
|
|
30
|
-
|
|
31
|
-
content = if data[:content].is_a?(Array)
|
|
32
|
-
data[:content].map do |content|
|
|
33
|
-
message_mapper.map_content(content)
|
|
34
|
-
end
|
|
35
|
-
else
|
|
36
|
-
data[:content] ? [ message_mapper.map_content(data[:content]) ] : []
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Claude returns content directly at root level, not in a choices array
|
|
40
|
-
# We need to construct the choices array from the full response data
|
|
41
|
-
[ {
|
|
42
|
-
content: content, # Use content directly from Claude response
|
|
43
|
-
finish_reason: data[:stop_reason],
|
|
44
|
-
role: "assistant"
|
|
45
|
-
} ]
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
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
|