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
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
|
|
5
|
-
module LlmGateway
|
|
6
|
-
module Adapters
|
|
7
|
-
module OpenAi
|
|
8
|
-
module ChatCompletions
|
|
9
|
-
class BidirectionalMessageMapper
|
|
10
|
-
attr_reader :direction
|
|
11
|
-
|
|
12
|
-
def initialize(direction)
|
|
13
|
-
@direction = direction
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def map_content(content)
|
|
17
|
-
# Convert string content to text format
|
|
18
|
-
content = { type: "text", text: content } unless content.is_a?(Hash)
|
|
19
|
-
case content[:type]
|
|
20
|
-
when "text"
|
|
21
|
-
map_text_content(content)
|
|
22
|
-
when "file"
|
|
23
|
-
map_file_content(content)
|
|
24
|
-
when "image"
|
|
25
|
-
map_image_content(content)
|
|
26
|
-
when "tool_use"
|
|
27
|
-
map_tool_use_content(content)
|
|
28
|
-
when "function"
|
|
29
|
-
map_tool_use_content(content)
|
|
30
|
-
when "tool_result"
|
|
31
|
-
map_tool_result_content(content)
|
|
32
|
-
else
|
|
33
|
-
content
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def parse_tool_arguments(arguments)
|
|
40
|
-
return arguments unless arguments.is_a?(String)
|
|
41
|
-
JSON.parse(arguments, symbolize_names: true)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def map_text_content(content)
|
|
45
|
-
{
|
|
46
|
-
type: "text",
|
|
47
|
-
text: content[:text]
|
|
48
|
-
}
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
def map_file_content(content)
|
|
52
|
-
# Map text/plain to application/pdf for OpenAI
|
|
53
|
-
media_type = content[:media_type] == "text/plain" ? "application/pdf" : content[:media_type]
|
|
54
|
-
{
|
|
55
|
-
type: "file",
|
|
56
|
-
file: {
|
|
57
|
-
filename: content[:name],
|
|
58
|
-
file_data: "data:#{media_type};base64,#{Base64.encode64(content[:data])}"
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def map_image_content(content)
|
|
64
|
-
{
|
|
65
|
-
type: "image_url",
|
|
66
|
-
image_url: {
|
|
67
|
-
url: "data:#{content[:media_type]};base64,#{content[:data]}"
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def map_tool_use_content(content)
|
|
73
|
-
if direction == LlmGateway::DIRECTION_IN
|
|
74
|
-
{
|
|
75
|
-
id: content[:id],
|
|
76
|
-
type: "function",
|
|
77
|
-
function: {
|
|
78
|
-
name: content[:name],
|
|
79
|
-
arguments: content[:input].to_json
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else
|
|
83
|
-
{
|
|
84
|
-
id: content[:id],
|
|
85
|
-
type: "tool_use",
|
|
86
|
-
name: content[:function][:name],
|
|
87
|
-
input: parse_tool_arguments(content[:function][:arguments])
|
|
88
|
-
}
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
def map_tool_result_content(content)
|
|
93
|
-
{
|
|
94
|
-
role: "tool",
|
|
95
|
-
tool_call_id: content[:tool_use_id],
|
|
96
|
-
content: content[:content]
|
|
97
|
-
}
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
@@ -1,110 +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 ChatCompletions
|
|
10
|
-
class InputMapper
|
|
11
|
-
def self.map(data)
|
|
12
|
-
{
|
|
13
|
-
messages: map_messages(data[:messages]),
|
|
14
|
-
response_format: map_response_format(data[:response_format]),
|
|
15
|
-
tools: map_tools(data[:tools]),
|
|
16
|
-
system: map_system(data[:system])
|
|
17
|
-
}
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def self.map_response_format(response_format)
|
|
23
|
-
response_format
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.map_messages(messages)
|
|
27
|
-
return messages unless messages
|
|
28
|
-
|
|
29
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_IN)
|
|
30
|
-
|
|
31
|
-
# First map messages like Claude
|
|
32
|
-
mapped_messages = messages.map do |msg|
|
|
33
|
-
msg = msg.merge(role: "user") if msg[:role] == "developer"
|
|
34
|
-
|
|
35
|
-
content = if msg[:content].is_a?(Array)
|
|
36
|
-
msg[:content].map do |content|
|
|
37
|
-
message_mapper.map_content(content)
|
|
38
|
-
end
|
|
39
|
-
else
|
|
40
|
-
[ message_mapper.map_content(msg[:content]) ]
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
{
|
|
44
|
-
role: msg[:role],
|
|
45
|
-
content: content
|
|
46
|
-
}
|
|
47
|
-
end
|
|
48
|
-
# Then transform to OpenAI format
|
|
49
|
-
mapped_messages.flat_map do |msg|
|
|
50
|
-
# Handle array content with tool calls and tool results
|
|
51
|
-
tool_calls = []
|
|
52
|
-
regular_content = []
|
|
53
|
-
tool_messages = []
|
|
54
|
-
msg[:content].each do |content|
|
|
55
|
-
case content[:type] || content[:role]
|
|
56
|
-
when "tool"
|
|
57
|
-
tool_messages << content
|
|
58
|
-
when "function"
|
|
59
|
-
tool_calls << content
|
|
60
|
-
else
|
|
61
|
-
regular_content << content
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
result = []
|
|
65
|
-
|
|
66
|
-
# Add the main message with tool calls if any
|
|
67
|
-
if tool_calls.any? || regular_content.any?
|
|
68
|
-
main_msg = msg.dup
|
|
69
|
-
main_msg[:role] = "assistant" if !main_msg[:role]
|
|
70
|
-
main_msg[:tool_calls] = tool_calls if tool_calls.any?
|
|
71
|
-
main_msg[:content] = regular_content.any? ? regular_content : nil
|
|
72
|
-
result << main_msg
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Add separate tool result messages
|
|
76
|
-
result += tool_messages
|
|
77
|
-
|
|
78
|
-
result
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def self.map_tools(tools)
|
|
83
|
-
return tools unless tools
|
|
84
|
-
|
|
85
|
-
tools.map do |tool|
|
|
86
|
-
{
|
|
87
|
-
type: "function",
|
|
88
|
-
function: {
|
|
89
|
-
name: tool[:name],
|
|
90
|
-
description: tool[:description],
|
|
91
|
-
parameters: tool[:input_schema]
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def self.map_system(system)
|
|
98
|
-
if !system || system.empty?
|
|
99
|
-
[]
|
|
100
|
-
else
|
|
101
|
-
system.map do |msg|
|
|
102
|
-
msg[:role] == "system" ? msg.merge(role: "developer") : msg
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
end
|
|
109
|
-
end
|
|
110
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LlmGateway
|
|
4
|
-
module Adapters
|
|
5
|
-
module OpenAi
|
|
6
|
-
module ChatCompletions
|
|
7
|
-
class OutputMapper
|
|
8
|
-
def self.map(data)
|
|
9
|
-
{
|
|
10
|
-
id: data[:id],
|
|
11
|
-
model: data[:model],
|
|
12
|
-
usage: data[:usage],
|
|
13
|
-
choices: map_choices(data[:choices])
|
|
14
|
-
}
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
def self.map_choices(choices)
|
|
20
|
-
return [] unless choices
|
|
21
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_OUT)
|
|
22
|
-
|
|
23
|
-
choices.map do |choice|
|
|
24
|
-
message = choice[:message] || {}
|
|
25
|
-
content_item = message_mapper.map_content(message[:content])
|
|
26
|
-
tool_calls = message[:tool_calls] ? message[:tool_calls].map { |tool_call| message_mapper.map_content(tool_call) } : []
|
|
27
|
-
|
|
28
|
-
# Only include content_item if it has actual text content
|
|
29
|
-
content_array = []
|
|
30
|
-
content_array << content_item if LlmGateway::Utils.present?(content_item[:text])
|
|
31
|
-
content_array += tool_calls
|
|
32
|
-
|
|
33
|
-
{ role: message[:role], content: content_array }
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,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,72 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
|
|
5
|
-
module LlmGateway
|
|
6
|
-
module Adapters
|
|
7
|
-
module OpenAi
|
|
8
|
-
module Responses
|
|
9
|
-
class BidirectionalMessageMapper < OpenAi::ChatCompletions::BidirectionalMessageMapper
|
|
10
|
-
def map_content(content)
|
|
11
|
-
# Convert string content to text format
|
|
12
|
-
#
|
|
13
|
-
|
|
14
|
-
content = { type: "text", text: content } unless content.is_a?(Hash)
|
|
15
|
-
case content[:type]
|
|
16
|
-
when "text"
|
|
17
|
-
map_text_content(content)
|
|
18
|
-
when "message"
|
|
19
|
-
map_messages(content)
|
|
20
|
-
when "output_text"
|
|
21
|
-
map_output_text_content(content)
|
|
22
|
-
when "tool_use"
|
|
23
|
-
map_tool_use_content(content)
|
|
24
|
-
when "function_call"
|
|
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_messages(message)
|
|
36
|
-
message[:content].map { |content| map_content(content) }
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def map_tool_result_content(content)
|
|
40
|
-
{
|
|
41
|
-
"type": "function_call_output",
|
|
42
|
-
"call_id": content[:tool_use_id],
|
|
43
|
-
"output": content[:content]
|
|
44
|
-
}
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def map_tool_use_content(content)
|
|
48
|
-
if direction == LlmGateway::DIRECTION_OUT
|
|
49
|
-
{ id: content[:call_id], type: "tool_use", name: content[:name], input: parse_tool_arguments(content[:arguments]) }
|
|
50
|
-
else
|
|
51
|
-
{ id: content[:id] }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def map_output_text_content(content)
|
|
56
|
-
{
|
|
57
|
-
type: "text",
|
|
58
|
-
text: content[:text]
|
|
59
|
-
}
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def map_text_content(content)
|
|
63
|
-
{
|
|
64
|
-
type: "input_text",
|
|
65
|
-
text: content[:text]
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
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,47 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "base64"
|
|
4
|
-
require_relative "bidirectional_message_mapper"
|
|
5
|
-
|
|
6
|
-
module LlmGateway
|
|
7
|
-
module Adapters
|
|
8
|
-
module OpenAi
|
|
9
|
-
module Responses
|
|
10
|
-
class OutputMapper
|
|
11
|
-
def self.map(data)
|
|
12
|
-
{
|
|
13
|
-
id: data[:id],
|
|
14
|
-
model: data[:model],
|
|
15
|
-
usage: data[:usage],
|
|
16
|
-
choices: map_choices(data[:output])
|
|
17
|
-
}
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def self.map_choices(choices)
|
|
23
|
-
return [] unless choices
|
|
24
|
-
message_mapper = BidirectionalMessageMapper.new(LlmGateway::DIRECTION_OUT)
|
|
25
|
-
choices.map do |choice|
|
|
26
|
-
content = if choice[:id].start_with?("fc_")
|
|
27
|
-
{
|
|
28
|
-
id: choice[:id],
|
|
29
|
-
role: choice[:role] || "assistant", # tool call doesnt have a role apparently
|
|
30
|
-
content: [ message_mapper.map_content(choice) ].flatten
|
|
31
|
-
}
|
|
32
|
-
else
|
|
33
|
-
content = message_mapper.map_content(choice)
|
|
34
|
-
id = content.delete(:id)
|
|
35
|
-
{
|
|
36
|
-
id: choice[:id] || id,
|
|
37
|
-
role: choice[:role],
|
|
38
|
-
content: [ content ].flatten
|
|
39
|
-
}
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
@@ -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
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
require_relative 'prompt'
|
|
2
|
-
require_relative 'agent'
|
|
3
|
-
require 'debug'
|
|
4
|
-
|
|
5
|
-
# Bash File Search Assistant using LlmGateway architecture
|
|
6
|
-
|
|
7
|
-
class ClaudeCloneClone
|
|
8
|
-
def initialize(model, api_key)
|
|
9
|
-
@agent = Agent.new(Prompt, model, api_key)
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def query(input)
|
|
13
|
-
begin
|
|
14
|
-
@agent.run(input) do |message|
|
|
15
|
-
case message[:type]
|
|
16
|
-
when 'text'
|
|
17
|
-
puts "\n\e[32m•\e[0m #{message[:text]}"
|
|
18
|
-
when 'tool_use'
|
|
19
|
-
puts "\n\e[33m•\e[0m \e[36m#{message[:name]}\e[0m"
|
|
20
|
-
if message[:input] && !message[:input].empty?
|
|
21
|
-
puts " \e[90m#{message[:input]}\e[0m"
|
|
22
|
-
end
|
|
23
|
-
when 'tool_result'
|
|
24
|
-
if message[:content] && !message[:content].empty?
|
|
25
|
-
content_preview = message[:content].to_s.split("\n").first(3).join("\n")
|
|
26
|
-
if content_preview.length > 100
|
|
27
|
-
content_preview = content_preview[0..97] + "..."
|
|
28
|
-
end
|
|
29
|
-
puts " \e[90m#{content_preview}\e[0m"
|
|
30
|
-
end
|
|
31
|
-
when 'error'
|
|
32
|
-
puts "\n\e[31m•\e[0m \e[91mError: #{message[:message]}\e[0m"
|
|
33
|
-
end
|
|
34
|
-
end
|
|
35
|
-
rescue => e
|
|
36
|
-
puts "\n\e[31m•\e[0m \e[91mError: #{e.message}\e[0m"
|
|
37
|
-
puts "\e[90m #{e.backtrace.first}\e[0m" if e.backtrace&.first
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
require_relative 'tools/edit_tool'
|
|
2
|
-
require_relative 'tools/read_tool'
|
|
3
|
-
require_relative 'tools/todowrite_tool'
|
|
4
|
-
require_relative 'tools/bash_tool'
|
|
5
|
-
require_relative 'tools/grep_tool'
|
|
6
|
-
|
|
7
|
-
class Prompt < LlmGateway::Prompt
|
|
8
|
-
def initialize(model, transcript, api_key)
|
|
9
|
-
super(model)
|
|
10
|
-
@transcript = transcript
|
|
11
|
-
@api_key = api_key
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def prompt
|
|
15
|
-
@transcript
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def system_prompt
|
|
19
|
-
<<~SYSTEM
|
|
20
|
-
You are Claude Code Clone, an interactive CLI tool that assists with software engineering tasks.
|
|
21
|
-
|
|
22
|
-
# Core Capabilities
|
|
23
|
-
|
|
24
|
-
I provide assistance with:
|
|
25
|
-
- Code analysis and debugging
|
|
26
|
-
- Feature implementation
|
|
27
|
-
- File editing and creation
|
|
28
|
-
- Running tests and builds
|
|
29
|
-
- Git operations
|
|
30
|
-
- Web browsing and research
|
|
31
|
-
- Task planning and management
|
|
32
|
-
|
|
33
|
-
## Available Tools
|
|
34
|
-
|
|
35
|
-
You have access to these specialized tools:
|
|
36
|
-
- `Edit` - Modify existing files by replacing specific text strings
|
|
37
|
-
- `Read` - Read file contents with optional pagination
|
|
38
|
-
- `TodoWrite` - Create and manage structured task lists
|
|
39
|
-
- `Bash` - Execute shell commands with timeout support
|
|
40
|
-
- `Grep` - Search for patterns in files using regex
|
|
41
|
-
|
|
42
|
-
## Core Instructions
|
|
43
|
-
|
|
44
|
-
I am designed to:
|
|
45
|
-
- Be concise and direct (minimize output tokens)
|
|
46
|
-
- Follow existing code conventions and patterns
|
|
47
|
-
- Use defensive security practices only
|
|
48
|
-
- Plan tasks with the TodoWrite tool for complex work
|
|
49
|
-
- Run linting/typechecking after making changes
|
|
50
|
-
- Never commit unless explicitly asked
|
|
51
|
-
|
|
52
|
-
## Process
|
|
53
|
-
|
|
54
|
-
1. **Understand the Request**: Parse what the user needs accomplished
|
|
55
|
-
2. **Plan if Complex**: Use TodoWrite for multi-step tasks
|
|
56
|
-
3. **Execute Tools**: Use appropriate tools to complete the work
|
|
57
|
-
4. **Validate**: Run tests/linting when applicable
|
|
58
|
-
5. **Report**: Provide concise status updates
|
|
59
|
-
|
|
60
|
-
Always use the available tools to perform actions rather than just suggesting commands.
|
|
61
|
-
|
|
62
|
-
Before starting any task, build a todo list of what you need to do, ensuring each item is actionable and prioritized. Then, execute the tasks one by one, using the TodoWrite tool to track progress and completion.
|
|
63
|
-
|
|
64
|
-
After completing each task, update the TodoWrite list to reflect the status and any necessary follow-up actions.
|
|
65
|
-
SYSTEM
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def self.tools
|
|
69
|
-
[ EditTool, ReadTool, TodoWriteTool, BashTool, GrepTool ]
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
def tools
|
|
73
|
-
self.class.tools.map(&:definition)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def post
|
|
77
|
-
LlmGateway::Client.chat(model, prompt, tools: tools, system: system_prompt, api_key: @api_key)
|
|
78
|
-
end
|
|
79
|
-
end
|