brute 0.1.8 → 0.2.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/lib/brute/agent_stream.rb +4 -1
- data/lib/brute/middleware/message_tracking.rb +15 -1
- data/lib/brute/orchestrator.rb +20 -6
- data/lib/brute/prompts/autonomy.rb +21 -0
- data/lib/brute/prompts/base.rb +23 -0
- data/lib/brute/prompts/build_switch.rb +19 -0
- data/lib/brute/prompts/code_references.rb +21 -0
- data/lib/brute/prompts/code_style.rb +16 -0
- data/lib/brute/prompts/conventions.rb +20 -0
- data/lib/brute/prompts/doing_tasks.rb +11 -0
- data/lib/brute/prompts/editing_approach.rb +20 -0
- data/lib/brute/prompts/editing_constraints.rb +24 -0
- data/lib/brute/prompts/environment.rb +25 -0
- data/lib/brute/prompts/frontend_tasks.rb +21 -0
- data/lib/brute/prompts/git_safety.rb +19 -0
- data/lib/brute/prompts/identity.rb +11 -0
- data/lib/brute/prompts/instructions.rb +18 -0
- data/lib/brute/prompts/max_steps.rb +30 -0
- data/lib/brute/prompts/objectivity.rb +16 -0
- data/lib/brute/prompts/plan_reminder.rb +40 -0
- data/lib/brute/prompts/proactiveness.rb +19 -0
- data/lib/brute/prompts/security_and_safety.rb +17 -0
- data/lib/brute/prompts/skills.rb +22 -0
- data/lib/brute/prompts/task_management.rb +59 -0
- data/lib/brute/prompts/text/agents/compaction.txt +15 -0
- data/lib/brute/prompts/text/agents/explore.txt +17 -0
- data/lib/brute/prompts/text/agents/summary.txt +11 -0
- data/lib/brute/prompts/text/agents/title.txt +40 -0
- data/lib/brute/prompts/text/doing_tasks/anthropic.txt +11 -0
- data/lib/brute/prompts/text/doing_tasks/default.txt +6 -0
- data/lib/brute/prompts/text/doing_tasks/google.txt +9 -0
- data/lib/brute/prompts/text/identity/anthropic.txt +5 -0
- data/lib/brute/prompts/text/identity/default.txt +3 -0
- data/lib/brute/prompts/text/identity/google.txt +1 -0
- data/lib/brute/prompts/text/identity/openai.txt +3 -0
- data/lib/brute/prompts/text/tone_and_style/anthropic.txt +5 -0
- data/lib/brute/prompts/text/tone_and_style/default.txt +9 -0
- data/lib/brute/prompts/text/tone_and_style/google.txt +6 -0
- data/lib/brute/prompts/text/tone_and_style/openai.txt +17 -0
- data/lib/brute/prompts/text/tool_usage/anthropic.txt +16 -0
- data/lib/brute/prompts/text/tool_usage/default.txt +4 -0
- data/lib/brute/prompts/text/tool_usage/google.txt +4 -0
- data/lib/brute/prompts/tone_and_style.rb +11 -0
- data/lib/brute/prompts/tool_usage.rb +11 -0
- data/lib/brute/providers/models_dev.rb +111 -0
- data/lib/brute/providers/opencode_go.rb +38 -0
- data/lib/brute/providers/opencode_zen.rb +82 -0
- data/lib/brute/providers/shell.rb +108 -0
- data/lib/brute/providers/shell_response.rb +100 -0
- data/lib/brute/skill.rb +118 -0
- data/lib/brute/system_prompt.rb +141 -63
- data/lib/brute/tools/delegate.rb +25 -1
- data/lib/brute/tools/question.rb +59 -0
- data/lib/brute/version.rb +1 -1
- data/lib/brute.rb +83 -3
- metadata +49 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
2
|
+
|
|
3
|
+
Your strengths:
|
|
4
|
+
- Rapidly finding files using glob patterns
|
|
5
|
+
- Searching code and text with powerful regex patterns
|
|
6
|
+
- Reading and analyzing file contents
|
|
7
|
+
|
|
8
|
+
Guidelines:
|
|
9
|
+
- Use fs_search for broad file pattern matching and searching file contents with regex
|
|
10
|
+
- Use read when you know the specific file path you need to read
|
|
11
|
+
- Use shell for file operations like listing directory contents
|
|
12
|
+
- Adapt your search approach based on the thoroughness level specified by the caller
|
|
13
|
+
- Return file paths as absolute paths in your final response
|
|
14
|
+
- For clear communication, avoid using emojis
|
|
15
|
+
- Do not create any files, or run shell commands that modify the user's system state in any way
|
|
16
|
+
|
|
17
|
+
Complete the user's search request efficiently and report your findings clearly.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Summarize what was done in this conversation. Write like a pull request description.
|
|
2
|
+
|
|
3
|
+
Rules:
|
|
4
|
+
- 2-3 sentences max
|
|
5
|
+
- Describe the changes made, not the process
|
|
6
|
+
- Do not mention running tests, builds, or other validation steps
|
|
7
|
+
- Do not explain what the user asked for
|
|
8
|
+
- Write in first person (I added..., I fixed...)
|
|
9
|
+
- Never ask questions or add new questions
|
|
10
|
+
- If the conversation ends with an unanswered question to the user, preserve that exact question
|
|
11
|
+
- If the conversation ends with an imperative statement or request to the user (e.g. "Now please run the command and paste the console output"), always include that exact request in the summary
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
You are a title generator. You output ONLY a thread title. Nothing else.
|
|
2
|
+
|
|
3
|
+
<task>
|
|
4
|
+
Generate a brief title that would help the user find this conversation later.
|
|
5
|
+
|
|
6
|
+
Follow all rules in <rules>
|
|
7
|
+
Use the <examples> so you know what a good title looks like.
|
|
8
|
+
Your output must be:
|
|
9
|
+
- A single line
|
|
10
|
+
- <=50 characters
|
|
11
|
+
- No explanations
|
|
12
|
+
</task>
|
|
13
|
+
|
|
14
|
+
<rules>
|
|
15
|
+
- you MUST use the same language as the user message you are summarizing
|
|
16
|
+
- Title must be grammatically correct and read naturally - no word salad
|
|
17
|
+
- Never include tool names in the title (e.g. "read tool", "shell tool", "patch tool")
|
|
18
|
+
- Focus on the main topic or question the user needs to retrieve
|
|
19
|
+
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
|
|
20
|
+
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
|
|
21
|
+
- Keep exact: technical terms, numbers, filenames, HTTP codes
|
|
22
|
+
- Remove: the, this, my, a, an
|
|
23
|
+
- Never assume tech stack
|
|
24
|
+
- Never use tools
|
|
25
|
+
- NEVER respond to questions, just generate a title for the conversation
|
|
26
|
+
- The title should NEVER include "summarizing" or "generating" when generating a title
|
|
27
|
+
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
|
|
28
|
+
- Always output something meaningful, even if the input is minimal.
|
|
29
|
+
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
|
|
30
|
+
-> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
|
|
31
|
+
</rules>
|
|
32
|
+
|
|
33
|
+
<examples>
|
|
34
|
+
"debug 500 errors in production" -> Debugging production 500 errors
|
|
35
|
+
"refactor user service" -> Refactoring user service
|
|
36
|
+
"why is app.js failing" -> app.js failure investigation
|
|
37
|
+
"implement rate limiting" -> Rate limiting implementation
|
|
38
|
+
"how do I connect postgres to my API" -> Postgres API connection
|
|
39
|
+
"best practices for React hooks" -> React hooks best practices
|
|
40
|
+
</examples>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Doing tasks
|
|
2
|
+
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
|
|
3
|
+
- Use the todo_write tool to plan the task if required
|
|
4
|
+
- Always read before editing: Use `read` to examine a file before using `patch` or `write` to modify it.
|
|
5
|
+
- Verify your changes: After editing, re-read the file or run tests to confirm correctness.
|
|
6
|
+
- Use fs_search to find code: Don't guess file locations — search first.
|
|
7
|
+
- Use shell for git, tests, builds: Run `git diff`, `git status`, test suites, etc.
|
|
8
|
+
- Be precise with patch: The `old_string` must match the file content exactly, including whitespace.
|
|
9
|
+
- Prefer patch over write: For existing files, use `patch` to change specific sections rather than rewriting the entire file.
|
|
10
|
+
- Use undo to recover: If a write or patch goes wrong, use `undo` to restore the previous version.
|
|
11
|
+
- Delegate research: Use `delegate` for complex analysis that needs focused investigation.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Doing tasks
|
|
2
|
+
The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended:
|
|
3
|
+
- Use the available search tools to understand the codebase and the user's query. Use search tools extensively both in parallel and sequentially.
|
|
4
|
+
- Implement the solution using all tools available to you
|
|
5
|
+
- Verify the solution if possible with tests. NEVER assume specific test framework or test script. Check the README or search codebase to determine the testing approach.
|
|
6
|
+
- NEVER commit changes unless the user explicitly asks you to.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Primary Workflows
|
|
2
|
+
|
|
3
|
+
## Software Engineering Tasks
|
|
4
|
+
When requested to perform tasks like fixing bugs, adding features, refactoring, or explaining code, follow this sequence:
|
|
5
|
+
1. **Understand:** Use fs_search and read tools extensively (in parallel if independent) to understand file structures, existing code patterns, and conventions.
|
|
6
|
+
2. **Plan:** Build a coherent plan based on your understanding. Share a concise plan with the user if it would help them understand your approach.
|
|
7
|
+
3. **Implement:** Use the available tools (patch, write, shell, etc.) to act on the plan, strictly adhering to the project's established conventions.
|
|
8
|
+
4. **Verify (Tests):** If applicable and feasible, verify the changes using the project's testing procedures. NEVER assume standard test commands.
|
|
9
|
+
5. **Verify (Standards):** After making code changes, execute the project-specific build, linting and type-checking commands that you have identified for this project.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
You are Brute, an expert software engineering agent.
|
|
2
|
+
|
|
3
|
+
You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
4
|
+
|
|
5
|
+
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
You are Brute, an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
|
|
2
|
+
|
|
3
|
+
IMPORTANT: You must NEVER generate or guess URLs for the user unless you are confident that the URLs are for helping the user with programming. You may use URLs provided by the user in their messages or local files.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
You are Brute, an interactive CLI agent specializing in software engineering tasks. Your primary goal is to help users safely and efficiently, adhering strictly to the following instructions and utilizing your available tools.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
You are Brute. You and the user share the same workspace and collaborate to achieve the user's goals.
|
|
2
|
+
|
|
3
|
+
You are a deeply pragmatic, effective software engineer. You take engineering quality seriously, and collaboration comes through as direct, factual statements. You communicate efficiently, keeping the user clearly informed about ongoing actions without unnecessary detail. You build context by examining the codebase first without making assumptions or jumping to conclusions. You think through the nuances of the code you encounter, and embody the mentality of a skilled senior software engineer.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Tone and style
|
|
2
|
+
- Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
|
3
|
+
- Your output will be displayed on a command line interface. Your responses should be short and concise. You can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
|
|
4
|
+
- Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
|
|
5
|
+
- NEVER create files unless they're absolutely necessary for achieving your goal. ALWAYS prefer editing an existing file to creating a new one. This includes markdown files.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Tone and style
|
|
2
|
+
You should be concise, direct, and to the point. When you run a non-trivial shell command, you should explain what the command does and why you are running it, to make sure the user understands what you are doing.
|
|
3
|
+
Remember that your output will be displayed on a command line interface. Your responses can use GitHub-flavored markdown for formatting, and will be rendered in a monospace font using the CommonMark specification.
|
|
4
|
+
Output text to communicate with the user; all text you output outside of tool use is displayed to the user. Only use tools to complete tasks. Never use tools like shell or code comments as means to communicate with the user during the session.
|
|
5
|
+
If you cannot or will not help the user with something, please do not say why or what it could lead to. Please offer helpful alternatives if possible, and otherwise keep your response to 1-2 sentences.
|
|
6
|
+
Only use emojis if the user explicitly requests it. Avoid using emojis in all communication unless asked.
|
|
7
|
+
IMPORTANT: You should minimize output tokens as much as possible while maintaining helpfulness, quality, and accuracy. Only address the specific query or task at hand.
|
|
8
|
+
IMPORTANT: You should NOT answer with unnecessary preamble or postamble (such as explaining your code or summarizing your action), unless the user asks you to.
|
|
9
|
+
IMPORTANT: Keep your responses short. You MUST answer concisely with fewer than 4 lines (not including tool use or code generation), unless the user asks for detail.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Tone and Style
|
|
2
|
+
- **Concise & Direct:** Adopt a professional, direct, and concise tone suitable for a CLI environment.
|
|
3
|
+
- **Minimal Output:** Aim for fewer than 3 lines of text output (excluding tool use/code generation) per response whenever practical.
|
|
4
|
+
- **No Chitchat:** Avoid conversational filler, preambles ("Okay, I will now..."), or postambles ("I have finished the changes..."). Get straight to the action or answer.
|
|
5
|
+
- **Formatting:** Use GitHub-flavored Markdown. Responses will be rendered in monospace.
|
|
6
|
+
- **Tools vs. Text:** Use tools for actions, text output *only* for communication.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Working with the user
|
|
2
|
+
|
|
3
|
+
Do not begin responses with conversational interjections or meta commentary. Avoid openers such as acknowledgements ("Done —", "Got it", "Great question") or framing phrases.
|
|
4
|
+
|
|
5
|
+
Balance conciseness to not overwhelm the user with appropriate detail for the request. Do not narrate abstractly; explain what you are doing and why.
|
|
6
|
+
|
|
7
|
+
## Formatting rules
|
|
8
|
+
|
|
9
|
+
Your responses are rendered as GitHub-flavored Markdown.
|
|
10
|
+
|
|
11
|
+
Never use nested bullets. Keep lists flat (single level). If you need hierarchy, split into separate lists or sections.
|
|
12
|
+
|
|
13
|
+
Use inline code blocks for commands, paths, environment variables, function names, inline examples, keywords.
|
|
14
|
+
|
|
15
|
+
Code samples or multi-line snippets should be wrapped in fenced code blocks. Include a language tag when possible.
|
|
16
|
+
|
|
17
|
+
Don't use emojis or em dashes unless explicitly instructed.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Tool usage policy
|
|
2
|
+
- When doing file search, prefer to use the delegate tool in order to reduce context usage.
|
|
3
|
+
- You should proactively use the delegate tool with specialized agents when the task at hand matches the agent's description.
|
|
4
|
+
- You can call multiple tools in a single response. If you intend to call multiple tools and there are no dependencies between them, make all independent tool calls in parallel. Maximize use of parallel tool calls where possible to increase efficiency. However, if some tool calls depend on previous calls to inform dependent values, do NOT call these tools in parallel and instead call them sequentially. For instance, if one operation must complete before another starts, run these operations sequentially instead. Never use placeholders or guess missing parameters in tool calls.
|
|
5
|
+
- Use specialized tools instead of shell commands when possible, as this provides a better user experience. For file operations, use dedicated tools: read for reading files instead of cat/head/tail, patch for editing instead of sed/awk, and write for creating files instead of cat with heredoc or echo redirection. Reserve the shell tool exclusively for actual system commands and terminal operations that require shell execution. NEVER use shell echo or other command-line tools to communicate thoughts, explanations, or instructions to the user. Output all communication directly in your response text instead.
|
|
6
|
+
- VERY IMPORTANT: When exploring the codebase to gather context or to answer a question that is not a needle query for a specific file/class/function, it is CRITICAL that you use the delegate tool instead of running search commands directly.
|
|
7
|
+
<example>
|
|
8
|
+
user: Where are errors from the client handled?
|
|
9
|
+
assistant: [Uses the delegate tool to find the files that handle client errors instead of using fs_search directly]
|
|
10
|
+
</example>
|
|
11
|
+
<example>
|
|
12
|
+
user: What is the codebase structure?
|
|
13
|
+
assistant: [Uses the delegate tool]
|
|
14
|
+
</example>
|
|
15
|
+
|
|
16
|
+
IMPORTANT: Always use the todo_write tool to plan and track tasks throughout the conversation.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Tool usage policy
|
|
2
|
+
- When doing file search, prefer to use the delegate tool in order to reduce context usage.
|
|
3
|
+
- You can call multiple tools in a single response. When multiple independent pieces of information are requested, batch your tool calls together for optimal performance.
|
|
4
|
+
- Use specialized tools (read, patch, write, fs_search) instead of shell commands for file operations.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# Tool Usage
|
|
2
|
+
- **Parallelism:** Execute multiple independent tool calls in parallel when feasible.
|
|
3
|
+
- **Specialized tools:** Use read, patch, write, and fs_search instead of shell commands for file operations.
|
|
4
|
+
- **Interactive Commands:** Avoid shell commands that require user interaction (e.g. `git rebase -i`). Use non-interactive versions of commands.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Brute
|
|
7
|
+
module Providers
|
|
8
|
+
# Fetches and caches model metadata from the models.dev catalog.
|
|
9
|
+
#
|
|
10
|
+
# Quacks like llm.rb's provider.models so that the REPL's model
|
|
11
|
+
# picker can call:
|
|
12
|
+
#
|
|
13
|
+
# provider.models.all.select(&:chat?)
|
|
14
|
+
#
|
|
15
|
+
# Models are fetched from https://models.dev/api.json and cached
|
|
16
|
+
# in-memory for the lifetime of the process (with a TTL).
|
|
17
|
+
#
|
|
18
|
+
class ModelsDev
|
|
19
|
+
CATALOG_URL = "https://models.dev/api.json"
|
|
20
|
+
CACHE_TTL = 3600 # 1 hour
|
|
21
|
+
|
|
22
|
+
ModelEntry = Struct.new(:id, :name, :chat?, :cost, :limit, :reasoning, :tool_call, keyword_init: true)
|
|
23
|
+
|
|
24
|
+
# @param provider [LLM::Provider] the provider instance (for delegating execute/headers)
|
|
25
|
+
# @param provider_id [String] the provider key in models.dev (e.g., "opencode", "opencode-go")
|
|
26
|
+
def initialize(provider:, provider_id: "opencode")
|
|
27
|
+
@provider = provider
|
|
28
|
+
@provider_id = provider_id
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns all models for this provider from the models.dev catalog.
|
|
32
|
+
# @return [Array<ModelEntry>]
|
|
33
|
+
def all
|
|
34
|
+
entries = fetch_provider_models
|
|
35
|
+
entries.map do |id, model|
|
|
36
|
+
ModelEntry.new(
|
|
37
|
+
id: id,
|
|
38
|
+
name: model["name"] || id,
|
|
39
|
+
chat?: true,
|
|
40
|
+
cost: model["cost"],
|
|
41
|
+
limit: model["limit"],
|
|
42
|
+
reasoning: model["reasoning"] || false,
|
|
43
|
+
tool_call: model["tool_call"] || false
|
|
44
|
+
)
|
|
45
|
+
end.sort_by(&:id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def fetch_provider_models
|
|
51
|
+
catalog = self.class.fetch_catalog
|
|
52
|
+
provider_data = catalog[@provider_id]
|
|
53
|
+
return {} unless provider_data
|
|
54
|
+
|
|
55
|
+
provider_data["models"] || {}
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
# Fetch the models.dev catalog, with in-memory caching.
|
|
60
|
+
# Thread-safe via a simple mutex.
|
|
61
|
+
def fetch_catalog
|
|
62
|
+
@mutex ||= Mutex.new
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
if @catalog && @fetched_at && (Time.now - @fetched_at < CACHE_TTL)
|
|
65
|
+
return @catalog
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@catalog = download_catalog
|
|
69
|
+
@fetched_at = Time.now
|
|
70
|
+
@catalog
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Force a cache refresh on next access.
|
|
75
|
+
def invalidate_cache!
|
|
76
|
+
@mutex&.synchronize do
|
|
77
|
+
@catalog = nil
|
|
78
|
+
@fetched_at = nil
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def download_catalog
|
|
85
|
+
uri = URI.parse(CATALOG_URL)
|
|
86
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
87
|
+
http.use_ssl = true
|
|
88
|
+
http.open_timeout = 10
|
|
89
|
+
http.read_timeout = 30
|
|
90
|
+
|
|
91
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
92
|
+
request["User-Agent"] = "brute/#{Brute::VERSION}"
|
|
93
|
+
request["Accept"] = "application/json"
|
|
94
|
+
|
|
95
|
+
response = http.request(request)
|
|
96
|
+
|
|
97
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
98
|
+
raise "Failed to fetch models.dev catalog: HTTP #{response.code}"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
JSON.parse(response.body)
|
|
102
|
+
rescue => e
|
|
103
|
+
# Return empty catalog on failure so the provider still works
|
|
104
|
+
# with default_model, just without a model list.
|
|
105
|
+
warn "[brute] Warning: Could not fetch models.dev catalog: #{e.message}"
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
##
|
|
5
|
+
# OpenAI-compatible provider for the OpenCode Go API gateway.
|
|
6
|
+
#
|
|
7
|
+
# OpenCode Go is the low-cost subscription plan with a restricted
|
|
8
|
+
# (lite) model list. Same gateway as Zen, different endpoint path.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# llm = LLM::OpencodeGo.new(key: ENV["OPENCODE_API_KEY"])
|
|
12
|
+
# ctx = LLM::Context.new(llm)
|
|
13
|
+
# ctx.talk "Hello from brute"
|
|
14
|
+
#
|
|
15
|
+
class OpencodeGo < OpencodeZen
|
|
16
|
+
##
|
|
17
|
+
# @return [Symbol]
|
|
18
|
+
def name
|
|
19
|
+
:opencode_go
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Returns models from the models.dev catalog.
|
|
24
|
+
# Note: The Go gateway only accepts lite-tier models, but models.dev
|
|
25
|
+
# doesn't distinguish between Zen and Go tiers. We show the full
|
|
26
|
+
# catalog; the gateway returns an error for unsupported models.
|
|
27
|
+
# @return [Brute::Providers::ModelsDev]
|
|
28
|
+
def models
|
|
29
|
+
Brute::Providers::ModelsDev.new(provider: self, provider_id: "opencode")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def completions_path
|
|
35
|
+
"/zen/go/v1/chat/completions"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ensure the OpenAI provider is loaded (llm.rb lazy-loads providers).
|
|
4
|
+
unless defined?(LLM::OpenAI)
|
|
5
|
+
require "llm/providers/openai"
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
module LLM
|
|
9
|
+
##
|
|
10
|
+
# OpenAI-compatible provider for the OpenCode Zen API gateway.
|
|
11
|
+
#
|
|
12
|
+
# OpenCode Zen is a curated model gateway at opencode.ai that proxies
|
|
13
|
+
# requests to upstream LLM providers (Anthropic, OpenAI, Google, etc.).
|
|
14
|
+
# All models are accessed via the OpenAI-compatible chat completions
|
|
15
|
+
# endpoint; the gateway handles format conversion internally.
|
|
16
|
+
#
|
|
17
|
+
# @example
|
|
18
|
+
# llm = LLM::OpencodeZen.new(key: ENV["OPENCODE_API_KEY"])
|
|
19
|
+
# ctx = LLM::Context.new(llm)
|
|
20
|
+
# ctx.talk "Hello from brute"
|
|
21
|
+
#
|
|
22
|
+
# @example Anonymous access (free models only)
|
|
23
|
+
# llm = LLM::OpencodeZen.new(key: "public")
|
|
24
|
+
# ctx = LLM::Context.new(llm)
|
|
25
|
+
# ctx.talk "Hello"
|
|
26
|
+
#
|
|
27
|
+
class OpencodeZen < OpenAI
|
|
28
|
+
HOST = "opencode.ai"
|
|
29
|
+
|
|
30
|
+
##
|
|
31
|
+
# @param key [String] OpenCode API key, or "public" for anonymous access
|
|
32
|
+
# @param (see LLM::Provider#initialize)
|
|
33
|
+
def initialize(key: "public", **)
|
|
34
|
+
super(host: HOST, key: key, **)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# @return [Symbol]
|
|
39
|
+
def name
|
|
40
|
+
:opencode_zen
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Returns the default model (Claude Sonnet 4, the most common Zen model).
|
|
45
|
+
# @return [String]
|
|
46
|
+
def default_model
|
|
47
|
+
"claude-sonnet-4-20250514"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
##
|
|
51
|
+
# Returns models from the models.dev catalog for the opencode provider.
|
|
52
|
+
# @return [Brute::Providers::ModelsDev]
|
|
53
|
+
def models
|
|
54
|
+
Brute::Providers::ModelsDev.new(provider: self, provider_id: "opencode")
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -- Unsupported sub-APIs --
|
|
58
|
+
|
|
59
|
+
def responses = raise(NotImplementedError, "Use chat completions via the Zen gateway")
|
|
60
|
+
def images = raise(NotImplementedError, "Not supported via Zen gateway")
|
|
61
|
+
def audio = raise(NotImplementedError, "Not supported via Zen gateway")
|
|
62
|
+
def files = raise(NotImplementedError, "Not supported via Zen gateway")
|
|
63
|
+
def moderations = raise(NotImplementedError, "Not supported via Zen gateway")
|
|
64
|
+
def vector_stores = raise(NotImplementedError, "Not supported via Zen gateway")
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def completions_path
|
|
69
|
+
"/zen/v1/chat/completions"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def headers
|
|
73
|
+
lock do
|
|
74
|
+
(@headers || {}).merge(
|
|
75
|
+
"Content-Type" => "application/json",
|
|
76
|
+
"Authorization" => "Bearer #{@key}",
|
|
77
|
+
"x-opencode-client" => "brute"
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "shellwords"
|
|
4
|
+
|
|
5
|
+
module Brute
|
|
6
|
+
module Providers
|
|
7
|
+
# A pseudo-LLM provider that executes user input as code via the
|
|
8
|
+
# existing Brute::Tools::Shell tool.
|
|
9
|
+
#
|
|
10
|
+
# Models correspond to interpreters:
|
|
11
|
+
#
|
|
12
|
+
# bash - pass-through (default)
|
|
13
|
+
# ruby - ruby -e '...'
|
|
14
|
+
# python - python3 -c '...'
|
|
15
|
+
# nix - nix eval --expr '...'
|
|
16
|
+
#
|
|
17
|
+
# The provider's #complete method returns a synthetic response
|
|
18
|
+
# containing a single "shell" tool call. The orchestrator executes
|
|
19
|
+
# it through the normal pipeline — all middleware (message tracking,
|
|
20
|
+
# session persistence, token tracking, etc.) fires as usual.
|
|
21
|
+
#
|
|
22
|
+
class Shell
|
|
23
|
+
MODELS = %w[bash ruby python nix].freeze
|
|
24
|
+
|
|
25
|
+
INTERPRETERS = {
|
|
26
|
+
"bash" => ->(cmd) { cmd },
|
|
27
|
+
"ruby" => ->(cmd) { "ruby -e #{Shellwords.escape(cmd)}" },
|
|
28
|
+
"python" => ->(cmd) { "python3 -c #{Shellwords.escape(cmd)}" },
|
|
29
|
+
"nix" => ->(cmd) { "nix eval --expr #{Shellwords.escape(cmd)}" },
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# ── LLM::Provider duck-type interface ──────────────────────────
|
|
33
|
+
|
|
34
|
+
def name = :shell
|
|
35
|
+
def default_model = "bash"
|
|
36
|
+
def user_role = :user
|
|
37
|
+
def tool_role = :tool
|
|
38
|
+
def assistant_role = :assistant
|
|
39
|
+
def system_role = :system
|
|
40
|
+
def tracer = LLM::Tracer::Null.new(self)
|
|
41
|
+
|
|
42
|
+
def complete(prompt, params = {})
|
|
43
|
+
model = params[:model]&.to_s || default_model
|
|
44
|
+
text = extract_text(prompt)
|
|
45
|
+
tools = params[:tools] || []
|
|
46
|
+
|
|
47
|
+
# nil text means we received tool results (second call) —
|
|
48
|
+
# return an empty assistant response so the orchestrator exits.
|
|
49
|
+
return ShellResponse.new(model: model, tools: tools) if text.nil?
|
|
50
|
+
|
|
51
|
+
wrap = INTERPRETERS.fetch(model, INTERPRETERS["bash"])
|
|
52
|
+
command = wrap.call(text)
|
|
53
|
+
|
|
54
|
+
ShellResponse.new(command: command, model: model, tools: tools)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# For the REPL model picker: provider.models.all.select(&:chat?)
|
|
58
|
+
def models
|
|
59
|
+
ModelList.new(MODELS)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# ── Internals ──────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
# Extract the user's text from whatever prompt format ctx.talk sends.
|
|
67
|
+
# Returns nil when the prompt contains tool results (the second
|
|
68
|
+
# round-trip) so #complete knows to return an empty response.
|
|
69
|
+
def extract_text(prompt)
|
|
70
|
+
case prompt
|
|
71
|
+
when String
|
|
72
|
+
prompt
|
|
73
|
+
when ::Array
|
|
74
|
+
return nil if prompt.any? { |p| LLM::Function::Return === p }
|
|
75
|
+
|
|
76
|
+
user_msg = prompt.reverse_each.find { |m| m.respond_to?(:role) && m.role.to_s == "user" }
|
|
77
|
+
user_msg&.content.to_s
|
|
78
|
+
else
|
|
79
|
+
if prompt.respond_to?(:to_a)
|
|
80
|
+
msgs = prompt.to_a
|
|
81
|
+
return nil if msgs.any? { |m| m.respond_to?(:content) && LLM::Function::Return === m.content }
|
|
82
|
+
|
|
83
|
+
user_msg = msgs.reverse_each.find { |m| m.respond_to?(:role) && m.role.to_s == "user" }
|
|
84
|
+
user_msg&.content.to_s
|
|
85
|
+
else
|
|
86
|
+
prompt.to_s
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ── ModelList ──────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
# Minimal object that quacks like provider.models so the REPL's
|
|
94
|
+
# fetch_models can call provider.models.all.select(&:chat?).
|
|
95
|
+
class ModelList
|
|
96
|
+
ModelEntry = Struct.new(:id, :chat?, keyword_init: true)
|
|
97
|
+
|
|
98
|
+
def initialize(names)
|
|
99
|
+
@entries = names.map { |n| ModelEntry.new(id: n, chat?: true) }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def all
|
|
103
|
+
@entries
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Brute
|
|
6
|
+
module Providers
|
|
7
|
+
# Synthetic completion response returned by Brute::Providers::Shell.
|
|
8
|
+
#
|
|
9
|
+
# When +command+ is present, the response contains a single assistant
|
|
10
|
+
# message with a "shell" tool call. The orchestrator picks it up and
|
|
11
|
+
# executes Brute::Tools::Shell through the normal pipeline.
|
|
12
|
+
#
|
|
13
|
+
# When +command+ is nil (tool results round-trip), the response
|
|
14
|
+
# contains an empty assistant message with no tool calls, causing
|
|
15
|
+
# the orchestrator loop to exit.
|
|
16
|
+
#
|
|
17
|
+
class ShellResponse
|
|
18
|
+
def initialize(command: nil, model: "bash", tools: [])
|
|
19
|
+
@command = command
|
|
20
|
+
@model_name = model
|
|
21
|
+
@tools = tools || []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def messages
|
|
25
|
+
return [empty_assistant] if @command.nil?
|
|
26
|
+
|
|
27
|
+
call_id = "shell_#{SecureRandom.hex(8)}"
|
|
28
|
+
tool_call = LLM::Object.from(
|
|
29
|
+
id: call_id,
|
|
30
|
+
name: "shell",
|
|
31
|
+
arguments: { "command" => @command },
|
|
32
|
+
)
|
|
33
|
+
original = [{
|
|
34
|
+
"type" => "tool_use",
|
|
35
|
+
"id" => call_id,
|
|
36
|
+
"name" => "shell",
|
|
37
|
+
"input" => { "command" => @command },
|
|
38
|
+
}]
|
|
39
|
+
|
|
40
|
+
[LLM::Message.new(:assistant, "", {
|
|
41
|
+
tool_calls: [tool_call],
|
|
42
|
+
original_tool_calls: original,
|
|
43
|
+
tools: @tools,
|
|
44
|
+
})]
|
|
45
|
+
end
|
|
46
|
+
alias_method :choices, :messages
|
|
47
|
+
|
|
48
|
+
def model
|
|
49
|
+
@model_name
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def input_tokens
|
|
53
|
+
0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def output_tokens
|
|
57
|
+
0
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def reasoning_tokens
|
|
61
|
+
0
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def total_tokens
|
|
65
|
+
0
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def content
|
|
69
|
+
messages.find(&:assistant?)&.content
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def content!
|
|
73
|
+
LLM.json.load(content)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def reasoning_content
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def usage
|
|
81
|
+
LLM::Usage.new(
|
|
82
|
+
input_tokens: 0,
|
|
83
|
+
output_tokens: 0,
|
|
84
|
+
reasoning_tokens: 0,
|
|
85
|
+
total_tokens: 0,
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Contract must be included AFTER method definitions —
|
|
90
|
+
# LLM::Contract checks that all required methods exist at include time.
|
|
91
|
+
include LLM::Contract::Completion
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def empty_assistant
|
|
96
|
+
LLM::Message.new(:assistant, "")
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|