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
@@ -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