You keep a mental cache of common stack traces: ENOENT, ModuleNotFoundError, exit code 1. The problem isn't understanding them; it's deciding which one matters when a tool emits forty lines and only two are diagnostic.
Why this is worth reading
A free model endpoint and a free server give you a cheap opportunity to move triage ahead of the moment you open a search engine. Instead of pasting the last three traceback lines from memory, you can send the full stdout and stderr of a failed command to a small sidecar that returns four focused fields: a likely cause, the exact evidence lines, a next command to try, and a confidence score.
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode is described by its operator as an open-source project with free model access, an advertised 30 million token allowance, and a free server option. I treat those availability claims as operator-supplied and verify only the workflow below; check the current model name, quota, and server terms against the project's primary documentation before you rely on them.
The sidecar is for your failed commands, not for watching the model
This is not another endpoint-health canary or token-burn alarm. Those tools tell you whether a free model is still up and how much credit remains. This sidecar is about what you do when the service is available but your command is not: it reads the failure you already have, asks for a structured triage, and gives you a narrower place to start. You still read the log; you just stop skimming the whole thing first.
Build the sidecar with only the standard library
The server and client are one Python file. The server accepts a failed command's stdout, stderr, and exit code; the client runs the command and only calls the model on non-zero exit. That keeps successful builds from spending tokens.
Create triage.py:
#!/usr/bin/env python3
'''Shell error triage sidecar.
Usage:
python triage.py serve
python triage.py run -- <command...>
'''
import argparse
import json
import os
import subprocess
import sys
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
TRIAGE_ENDPOINT = os.getenv('TRIAGE_ENDPOINT', 'http://127.0.0.1:8787')
MODEL_BASE_URL = os.getenv('MODEL_BASE_URL', '')
MODEL_ID = os.getenv('MODEL_ID', '')
API_KEY = os.getenv('MONKEY_CODE_API_KEY', '')
def ask_model(messages, max_tokens=250):
'''Call an OpenAI-compatible chat completions route.'''
payload = json.dumps({
'model': MODEL_ID,
'messages': messages,
'temperature': 0,
'max_tokens': max_tokens,
}).encode()
base = MODEL_BASE_URL.rstrip('/')
url = f'{base}/chat/completions'
request = urllib.request.Request(
url,
data=payload,
headers={
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json',
},
)
with urllib.request.urlopen(request, timeout=30) as response:
data = json.loads(response.read())
return data['choices'][0]['message']['content']
def build_prompt(command, stdout, stderr, returncode):
return f'''You are triaging a shell command that just failed.
Command: {command}
Exit code: {returncode}
Return JSON only with these keys: likely_cause, evidence_lines, next_command, confidence.
Use at most 15 evidence lines. Prefer exact traceback or stderr lines over prose.
Keep stdout and stderr truncated to the last 25 diagnostic lines.
STDOUT:
{stdout[-3000:] or '(empty)'}
STDERR:
{stderr[-3000:] or '(empty)'}
'''
class TriageHandler(BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get('Content-Length', 0))
raw = self.rfile.read(length)
try:
body = json.loads(raw)
prompt = build_prompt(
body['command'],
body['stdout'],
body['stderr'],
body['returncode'],
)
reply = ask_model([{'role': 'user', 'content': prompt}])
try:
result = json.loads(reply)
except json.JSONDecodeError:
result = {
'likely_cause': 'Model did not return JSON',
'evidence_lines': [reply],
'next_command': '',
'confidence': 0,
}
except Exception as exc: # noqa: BLE001
result = {
'likely_cause': 'Triage service error',
'evidence_lines': [str(exc)],
'next_command': '',
'confidence': 0,
}
output = json.dumps(result).encode()
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.send_header('Content-Length', str(len(output)))
self.end_headers()
self.wfile.write(output)
def log_message(self, format, *args): # noqa: A002
pass
def run_command(args):
process = subprocess.run(args, capture_output=True, text=True)
if process.returncode == 0:
print('Command succeeded; no triage needed.')
return 0
payload = json.dumps({
'command': ' '.join(args),
'stdout': process.stdout,
'stderr': process.stderr,
'returncode': process.returncode,
}).encode()
endpoint = TRIAGE_ENDPOINT.rstrip('/')
url = f'{endpoint}/triage'
request = urllib.request.Request(
url,
data=payload,
headers={'Content-Type': 'application/json'},
)
with urllib.request.urlopen(request, timeout=60) as response:
result = json.loads(response.read())
print(json.dumps(result, indent=2))
return process.returncode
def main():
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest='mode', required=True)
subparsers.add_parser('serve', help='Run the triage server')
run_parser = subparsers.add_parser('run', help='Run a command and triage on failure')
run_parser.add_argument('args', nargs=argparse.REMAINDER, help='Command and its arguments after --')
args = parser.parse_args()
if args.mode == 'serve':
server = ThreadingHTTPServer(('0.0.0.0', 8787), TriageHandler)
print('Triage server listening on :8787')
server.serve_forever()
else:
if not args.args or args.args == ['--']:
sys.exit('Provide -- followed by a command to run.')
return run_command(args.args)
if __name__ == '__main__':
main()
Run the server on the free server option
Copy the file to the free server option or any host with outbound network access, then export the three model variables and start the server:
# On the free server option:
cat > /tmp/triage.py <<'PY'
# paste the full triage.py content above
PY
export MONKEY_CODE_API_KEY='...'
export MODEL_BASE_URL='https://api.example.com'
export MODEL_ID='your-model-id'
python3 /tmp/triage.py serve
If the API you are testing is not OpenAI-compatible, change the URL construction and request shape inside ask_model only. The rest of the sidecar stays the same.
Point your local machine at the sidecar
On your laptop, set the same model variables plus the triage endpoint that points at the free server:
export TRIAGE_ENDPOINT='http://your-free-server:8787'
export MODEL_BASE_URL='https://api.example.com'
export MODEL_ID='your-model-id'
export MONKEY_CODE_API_KEY='...'
python3 triage.py run -- npm test
If the free server cannot accept inbound connections from your development machine, run the server locally instead and set TRIAGE_ENDPOINT to http://127.0.0.1:8787.
What a triage result looks like
Run a deliberately messy command and let the sidecar compress the noise:
python3 triage.py run -- sh -c "cat /etc/shadow; ls /nope; python3 -c 'import missing_thing'"
The output is not a repaired command. It is a structured starting point:
{
"likely_cause": "Multiple failures: permission denied on /etc/shadow, missing directory /nope, ModuleNotFoundError for missing_thing",
"evidence_lines": [
"cat: /etc/shadow: Permission denied",
"ls: cannot access '/nope': No such file or directory",
"ModuleNotFoundError: No module named 'missing_thing'"
],
"next_command": "python3 -c 'import missing_thing'",
"confidence": 0.91
}
You still read the evidence lines. The model's job is to move the two or three real failure lines to the top of your attention instead of leaving them buried under dependency output.
Test it with a known failure
Before you trust the sidecar on a real build, run a one-shot assertion against a predictable import error:
python3 - <<'PY'
import json, subprocess, sys
proc = subprocess.run(
[sys.executable, 'triage.py', 'run', '--', 'python3', '-c', 'import definitely_missing_42'],
capture_output=True,
text=True,
)
assert proc.returncode != 0
out = json.loads(proc.stdout)
assert 'ModuleNotFoundError' in ' '.join(out['evidence_lines'])
print('triage result:', out)
PY
If the assertion passes, you know the sidecar can extract at least one common Python failure. Add a second fixture for a missing environment variable or a port-binding conflict if those matter more in your stack.
Keep the token budget small
Each failed command sends up to 6 KB of combined output and requests 250 output tokens. That is deliberately small: a day with 100 failed runs would use roughly 600 KB of input context plus 25,000 output tokens, depending on the tokenizer. With a free 30 million token allowance, the sidecar is a low-usage utility, but verify the current quota and model behavior against the provider's documentation rather than assuming the allowance stays fixed.
Limitations and when to skip the sidecar
Do not run the returned next_command without reading it first. The model can misidentify a rare compiler error or suggest a fix that works only in a container it cannot see. The sidecar also truncates long logs to the last 3,000 characters, so a root cause buried earlier in a multi-step pipeline can be missed.
Skip this workflow if you need deterministic log parsing with audit requirements, if you cannot send command output to a third-party endpoint, or if you already have a local grep pattern that catches 90% of your failures in under a second. The model is a useful second pass, not a replacement for a small collection of known-error regexes.
A failed command should earn its model call
The sidecar does not watch the model; it uses the model only after something fails. That distinction keeps successful builds cheap and gives you a concrete, repeatable reason to try MonkeyCode's free model access and free server option without betting your whole workflow on them. If you add this to your own shell, which failure pattern should it learn first: missing environment variables, dependency conflicts, or port-binding errors?
Top comments (0)