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.
Files changed (78) 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 +43 -0
  5. data/README.md +559 -185
  6. data/Rakefile +2 -2
  7. data/docs/migration-guide.md +135 -0
  8. data/lib/llm_gateway/adapters/adapter.rb +140 -0
  9. data/lib/llm_gateway/adapters/anthropic/acts_like_messages.rb +21 -0
  10. data/lib/llm_gateway/adapters/anthropic/input_mapper.rb +137 -0
  11. data/lib/llm_gateway/adapters/anthropic/messages_adapter.rb +19 -0
  12. data/lib/llm_gateway/adapters/anthropic/output_mapper.rb +17 -0
  13. data/lib/llm_gateway/adapters/anthropic/stream_mapper.rb +95 -0
  14. data/lib/llm_gateway/adapters/anthropic_option_mapper.rb +95 -0
  15. data/lib/llm_gateway/adapters/groq/chat_completions_adapter.rb +48 -0
  16. data/lib/llm_gateway/adapters/groq/input_mapper.rb +32 -6
  17. data/lib/llm_gateway/adapters/groq/option_mapper.rb +112 -0
  18. data/lib/llm_gateway/adapters/input_message_sanitizer.rb +93 -0
  19. data/lib/llm_gateway/adapters/normalized_stream_accumulator.rb +275 -0
  20. data/lib/llm_gateway/adapters/openai/acts_like_chat_completions.rb +20 -0
  21. data/lib/llm_gateway/adapters/openai/acts_like_responses.rb +25 -0
  22. data/lib/llm_gateway/adapters/openai/chat_completions/input_mapper.rb +168 -0
  23. data/lib/llm_gateway/adapters/openai/chat_completions/input_message_sanitizer.rb +65 -0
  24. data/lib/llm_gateway/adapters/openai/chat_completions/option_mapper.rb +129 -0
  25. data/lib/llm_gateway/adapters/openai/chat_completions/stream_mapper.rb +241 -0
  26. data/lib/llm_gateway/adapters/openai/chat_completions_adapter.rb +19 -0
  27. data/lib/llm_gateway/adapters/{open_ai → openai}/file_output_mapper.rb +1 -1
  28. data/lib/llm_gateway/adapters/openai/prompt_cache_option_mapper.rb +39 -0
  29. data/lib/llm_gateway/adapters/openai/responses/input_mapper.rb +166 -0
  30. data/lib/llm_gateway/adapters/openai/responses/option_mapper.rb +130 -0
  31. data/lib/llm_gateway/adapters/openai/responses/stream_mapper.rb +150 -0
  32. data/lib/llm_gateway/adapters/openai/responses_adapter.rb +19 -0
  33. data/lib/llm_gateway/adapters/openai_codex/input_mapper.rb +206 -0
  34. data/lib/llm_gateway/adapters/openai_codex/option_mapper.rb +28 -0
  35. data/lib/llm_gateway/adapters/openai_codex/responses_adapter.rb +33 -0
  36. data/lib/llm_gateway/adapters/option_mapper.rb +13 -0
  37. data/lib/llm_gateway/adapters/stream_mapper.rb +50 -0
  38. data/lib/llm_gateway/adapters/structs.rb +145 -0
  39. data/lib/llm_gateway/base_client.rb +62 -1
  40. data/lib/llm_gateway/client.rb +18 -158
  41. data/lib/llm_gateway/clients/anthropic.rb +167 -0
  42. data/lib/llm_gateway/clients/claude_code/oauth_flow.rb +162 -0
  43. data/lib/llm_gateway/clients/claude_code/token_manager.rb +112 -0
  44. data/lib/llm_gateway/clients/groq.rb +66 -0
  45. data/lib/llm_gateway/clients/openai.rb +208 -0
  46. data/lib/llm_gateway/clients/openai_codex/oauth_flow.rb +258 -0
  47. data/lib/llm_gateway/clients/openai_codex/token_manager.rb +71 -0
  48. data/lib/llm_gateway/errors.rb +21 -0
  49. data/lib/llm_gateway/prompt.rb +12 -1
  50. data/lib/llm_gateway/provider_registry.rb +37 -0
  51. data/lib/llm_gateway/version.rb +1 -1
  52. data/lib/llm_gateway.rb +162 -17
  53. data/scripts/create_anthropic_credentials.rb +106 -0
  54. data/scripts/create_openai_codex_credentials.rb +116 -0
  55. metadata +60 -27
  56. data/lib/llm_gateway/adapters/claude/bidirectional_message_mapper.rb +0 -83
  57. data/lib/llm_gateway/adapters/claude/client.rb +0 -60
  58. data/lib/llm_gateway/adapters/claude/input_mapper.rb +0 -57
  59. data/lib/llm_gateway/adapters/claude/output_mapper.rb +0 -50
  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/output_mapper.rb +0 -10
  63. data/lib/llm_gateway/adapters/open_ai/chat_completions/bidirectional_message_mapper.rb +0 -103
  64. data/lib/llm_gateway/adapters/open_ai/chat_completions/input_mapper.rb +0 -110
  65. data/lib/llm_gateway/adapters/open_ai/chat_completions/output_mapper.rb +0 -40
  66. data/lib/llm_gateway/adapters/open_ai/client.rb +0 -80
  67. data/lib/llm_gateway/adapters/open_ai/responses/bidirectional_message_mapper.rb +0 -72
  68. data/lib/llm_gateway/adapters/open_ai/responses/input_mapper.rb +0 -62
  69. data/lib/llm_gateway/adapters/open_ai/responses/output_mapper.rb +0 -47
  70. data/sample/claude_code_clone/agent.rb +0 -65
  71. data/sample/claude_code_clone/claude_code_clone.rb +0 -40
  72. data/sample/claude_code_clone/prompt.rb +0 -79
  73. data/sample/claude_code_clone/run.rb +0 -47
  74. data/sample/claude_code_clone/tools/bash_tool.rb +0 -54
  75. data/sample/claude_code_clone/tools/edit_tool.rb +0 -61
  76. data/sample/claude_code_clone/tools/grep_tool.rb +0 -113
  77. data/sample/claude_code_clone/tools/read_tool.rb +0 -61
  78. 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.3.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: 2025-08-19 00:00:00.000000000 Z
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/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
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/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
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
- - 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
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
@@ -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