Repository navigation
Expand file tree
/
Copy pathconfig.default.yaml
More file actions
577 lines (565 loc) · 30.4 KB
/
Copy pathconfig.default.yaml
File metadata and controls
577 lines (565 loc) · 30.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
# harness-dispatch configuration — shipped defaults
#
# This is the config harness-dispatch ships with — not a separate "defaults
# registry" in some other format, just an ordinary config.yaml shape. A
# DIFFERENT file from your own config.yaml on purpose: this one is tracked
# and updated by the package itself, yours is gitignored/untouched by
# updates and may hold secrets/local overrides — keeping them separate means
# neither ever clobbers the other. The entries below are both the built-in
# defaults and the worked examples of the pattern. With no config.yaml of
# your own, the router uses this file directly, filtered by which CLIs are
# actually installed. Do not copy this whole file: start from the short one
# `harness-dispatch configure --print` shows and add entries to it. Same
# schema, same parser, no special cases. Add a new entry and it's a route.
#
# Listing any `clis:` or `endpoints:` in your own config.yaml turns
# auto-detection off — see docs/configuration.md#listing-a-route-turns-detection-off
# (https://github.com/fstubner/harness-dispatch/blob/main/docs/configuration.md).
# Every key is in the reference at the bottom of that page.
#
# Route selection scores each entry by task fit:
# capabilities[task_type] x cli_capability x quota x weight
# Routing is: tier first (lowest number wins), then that score, then fallback
# to the next candidate if the pick fails.
# Billing safety: routes that can cost money (metered api_key, unknown billing)
# stay blocked until you set allow_paid_usage: true on them. Product-login CLIs
# (Claude Code, Codex, Cursor) run by default. They draw on your plan's included
# usage; if paid overage is switched on at the provider (Cursor on-demand,
# Claude usage credits, Codex flexible credits), that is spent too.
#
# ${ENV_VAR} anywhere in a string value is replaced from the environment.
clis:
# ── Claude Code ─────────────────────────────────────────────────────────────
# Best for plan + review (extended thinking). Subscription auth via the CLI's
# own login; add api_key: ${ANTHROPIC_API_KEY} only for API billing.
- name: claude_code_cli
harness: claude_code
command: claude
model_hint: >-
Anthropic model family only. Current model ids:
https://platform.claude.com/docs/en/docs/about-claude/models/overview
(e.g. claude-opus-4-8, claude-sonnet-5) — the installed claude CLI may
lag behind what's newly listed there.
# What hints.modelTier runs on this route: family aliases the claude CLI
# accepts for --model, so each tracks that family's current model.
model_tiers: { cheap: haiku, standard: sonnet, strong: opus }
cli_capability: 1.1
tier: 1
capabilities: { execute: 0.95, plan: 1.0, review: 1.0 }
max_output_tokens: 64000
max_input_tokens: 1000000 # Opus + Sonnet 1M context
provider: anthropic
surface: claude_agent_sdk
auth_source: oauth_session
# protocol: is the full invocation template — args is a literal command
# line; {{tokens}} are substituted at dispatch time (see the reference at
# the bottom of this file).
protocol:
# The prompt goes on STDIN, not in argv.
#
# `claude -p` accepts either; argv was the original choice and it is the
# one that carries every command-line defect this project has fixed. On
# Windows a .CMD/.bat target is re-spawned through cmd.exe, which caps
# the whole command line at 8,191 characters — so a long prompt was
# refused before it reached the harness at all, and the machinery
# deciding where that boundary sits had to replicate cross-spawn's own
# escaping to know. generic-cli.ts skips that check entirely when
# `stdin: true` (the guard is `if (!protocol.stdin)`), because a prompt
# on stdin has no such limit. codex_cli has always worked this way.
#
# Verified live: a 21,670-character prompt — 2.65x the 8,191-character ceiling, and
# measured at 20,064 characters as argv, which the old form refused
# outright — dispatched and answered.
stdin: true
args: ["-p", "--output-format", "json", "{{safety}}", "{{model}}"]
model: { flag: "--model" }
# --allowedTools only ADDS auto-approvals on top of the user's own
# settings, so on its own "workspace_edit: no shell" meant "no shell
# beyond the user's allow rules" — 17 of them start with Bash on the
# maintainer's machine. --tools is the flag that sets which built-in
# tools exist at all, and --strict-mcp-config (with no --mcp-config)
# loads no MCP servers, so the user's servers — harness-dispatch itself
# among them — are not handed to a read_only or workspace_edit delegate.
# --setting-sources user keeps the project's own .claude/settings.json
# (and its hooks) out of those two profiles: the project is what the
# delegate is being asked to read, and a hook there runs shell commands
# whatever --tools says. The user's own settings still load, so login
# and user-level hooks are unchanged. full_auto is left as it was.
# Each variadic list is followed by another option so it cannot swallow
# the next argument. Flags checked against `claude --help`, 2.1.278.
safety:
read_only:
["--tools", "Read,Grep,Glob", "--allowedTools", "Read,Grep,Glob",
"--strict-mcp-config", "--setting-sources", "user", "--permission-mode", "plan"]
workspace_edit:
["--tools", "Read,Grep,Glob,Edit,Write", "--allowedTools", "Read,Grep,Glob,Edit,Write",
"--strict-mcp-config", "--setting-sources", "user", "--permission-mode", "acceptEdits"]
full_auto: ["--allowedTools", "Bash,Read,Edit,Write", "--permission-mode", "acceptEdits"]
# Claude Code reads ANTHROPIC_API_KEY itself to switch from
# subscription/OAuth session auth to API-key billing — without this,
# setting api_key: on the route reclassified billing to metered_api
# (blocking it) while the CLI kept silently using subscription auth.
api_key_env_var: ANTHROPIC_API_KEY
file_list_header: "Files to work with:"
file_list_bullet: " "
output:
mode: json_field
fields: [result, response, text]
# input_extra is SUMMED on top of input, unlike the alternatives in
# `input`. Claude splits a turn's input across three sibling fields and
# `input_tokens` is only the uncached remainder — a real turn reported
# input_tokens=2 against cache_creation=34141 + cache_read=21070, so
# reading the first alone made `usage` totals off by four orders of
# magnitude. No field carries the total; it has to be added up.
usage:
input: [usage.input_tokens]
input_extra: [usage.cache_creation_input_tokens, usage.cache_read_input_tokens]
output: [usage.output_tokens]
# Confirmed live 2026-07-24: a successful response carries a sibling
# "is_error": false (and "api_error_status": null) alongside "result"
# — without this, an API-level failure that still exits 0 would be
# reported as success (success_requires_output: false takes ANY
# exit-0 output as a win, including an error's own JSON text).
# message_fields omitted deliberately: the error text lands in the
# same "result" field as a successful answer, already covered above.
error: { field: is_error }
success_requires_output: false
# ── Codex ───────────────────────────────────────────────────────────────────
# Best for execute — full-auto drives code + tests in a loop. Streams NDJSON
# events mid-run; event_rules below surface tool_use/thinking/usage live.
- name: codex_cli
harness: codex
command: codex
model_hint: >-
OpenAI/Codex model family only. Current model ids:
https://developers.openai.com/api/docs/models (e.g. gpt-5.6-sol,
gpt-5.6-terra) — the installed codex CLI may lag behind what's newly
listed there.
# No cheap entry: a cheap request runs the CLI's own default model.
model_tiers: { standard: gpt-5.6-terra, strong: gpt-5.6-sol }
cli_capability: 1.08
tier: 1
capabilities: { execute: 1.0, plan: 0.83, review: 0.82 }
max_output_tokens: 128000
max_input_tokens: 400000
provider: openai
surface: codex_cli
auth_source: product_login
billing_kind: included_plan_then_flexible_credits
# OpenAI hard-stops at the plan's included usage unless you enabled
# flexible-credit overage on your account — set true here if you did.
paid_usage_possible: false
# Stop a run that has printed nothing for 15 minutes as hung, instead of
# letting it hold a concurrency slot until the 60-minute job ceiling.
# Codex streams an event per step, so silence that long is not work: the
# longest gap between events in 236 real `codex exec` sessions
# (~/.codex/sessions, 2026-07 to 2026-10) was 123 s. Only for a harness
# that streams — claude_code_cli and cursor_cli print their answer only at
# the end, so an idle limit there would stop every long task.
idle_timeout_ms: 900000
protocol:
# "exec" is Codex's required subcommand; trailing "-" = read prompt from
# stdin (stdin: true below).
args: ["exec", "{{native_args}}", "{{safety}}", "{{model}}", "{{working_dir}}", "--json", "-"]
stdin: true
working_dir: { flag: "--cd", extra_args_when_set: ["--skip-git-repo-check"] }
model: { flag: "--model" }
# full_auto was `--full-auto`, which `codex exec --help` (0.146) no longer
# lists; it was still accepted and meant the workspace-write sandbox. It
# is spelled out here so a removal cannot change what runs. Codex's
# full_auto therefore still runs shell inside its sandbox — the safe
# direction; `danger-full-access` would be the unsandboxed reading.
safety:
read_only: ["--sandbox", "read-only"]
workspace_edit: ["--sandbox", "workspace-write"]
full_auto: ["--sandbox", "workspace-write"]
api_key_env_var: OPENAI_API_KEY
file_list_header: "Files to work with:"
file_list_bullet: " "
output:
mode: jsonl_stream
event_rules:
- when: { type: "item.completed", "item.type": "agent_message" }
emit: text
text_field: item.text
- when: { type: "message" }
emit: text
text_field: message.content
- when: { "item.type": "tool_use" }
emit: tool_use
name_field: item.name
input_field: item.input
- when: { type: "thinking" }
emit: thinking
chunk_field: item.text
- when: {} # unconditional — fires on every line carrying usage
emit: usage
input_token_fields: [usage.input_tokens, usage.prompt_tokens]
output_token_fields: [usage.output_tokens, usage.completion_tokens]
# Real shapes confirmed live 2026-07-24 against an actually-exhausted
# account: {"type":"error","message":"You've hit your usage
# limit..."} followed by {"type":"turn.failed","error":{"message":
# "..."}}. Without these, the dispatcher never sees either frame —
# it falls back to stderr text (just "Reading additional input from
# stdin..." noise) and reports a useless, unclassifiable failure.
- when: { type: "error" }
emit: error
message_field: message
- when: { type: "turn.failed" }
emit: error
message_field: error.message
success_requires_output: false
# Used only when a route runs endpoint_mode: harness_native_endpoint
# against a matching endpoint_provider (see codex_ollama example below).
endpoint_native_args:
ollama: ["--oss", "--local-provider", "ollama"]
lmstudio: ["--oss", "--local-provider", "lmstudio"]
# ── Cursor ──────────────────────────────────────────────────────────────────
# Best for execute with editor-aware codebase indexing. Note: Cursor print
# mode has write+shell capability, so these routes run under full_auto —
# they're skipped for read_only/workspace_edit requests.
- name: cursor_cli
harness: cursor
command: cursor-agent
model_hint: >-
Wide multi-vendor catalog. Current model ids and pricing:
https://cursor.com/docs/models — or run `cursor-agent --list-models`
for what's actually available on this install.
cli_capability: 1.05
tier: 1
capabilities: { execute: 1.0, plan: 0.82, review: 0.9 }
max_output_tokens: 64000
max_input_tokens: 1000000
provider: cursor
surface: cursor_agent_cli
auth_source: product_login
billing_kind: included_usage_then_on_demand
# Cursor stops at the plan's included pool unless you have turned on
# on-demand usage in Settings -> Billing -> Spending. Set true here if you
# did — and note the second half, which the earlier wording ("hard-stops")
# missed: once on-demand IS on, overage is billed in ARREARS at the end of
# the cycle, and Cursor's own docs recommend setting a spend limit because
# without one there is no cap. So this route is the one subscription in the
# shipped set whose overage is not prepaid, which is why the `true` here is
# a bigger statement than it looks. Checked against Cursor's 2026 pricing
# documentation on 2026-08-31; NOT live-verified, because doing so means
# exhausting a real included pool.
paid_usage_possible: false
# Capability floor, per requested profile. cursor-agent's capability really
# does differ by mode, so a single value cannot describe it:
#
# --mode plan read-only. Verified 2026-08-17: asked to create one file
# and overwrite another, it did neither, while the identical
# invocation without --mode plan created the file.
# default -p "Has access to all tools, including write and shell"
# (its own --help). Write and shell arrive together, so
# there is no workspace_edit rung on this ladder and a
# workspace_edit request still gets the full_auto floor,
# i.e. it is still skipped unless full_auto is allowed.
#
# --sandbox enabled looks like the missing middle rung but is macOS/Linux
# only ("Sandbox mode is enabled but not available on this system" on
# Windows), so it is not used here.
effective_safety:
read_only: read_only
workspace_edit: full_auto
full_auto: full_auto
protocol:
# -p is Cursor's boolean "print mode" flag; the prompt itself is the
# trailing positional {{prompt}}.
# Prompt on STDIN, for the same reason as claude_code_cli above: a
# prompt in argv is measured against cmd.exe's 8,191-character ceiling
# and escaped to get there, and this route is a `cursor-agent.CMD`
# PowerShell wrapper — the one that failed at ~9k characters and drove
# two releases of escaping work. generic-cli.ts skips the check
# entirely when `stdin: true`.
#
# Verified live before changing: `cursor-agent -p --trust
# --output-format json` with the prompt piped in returns
# {"type":"result","subtype":"success","is_error":false,...,"result":"pong"}.
# --trust is required or it refuses with a workspace-trust prompt.
stdin: true
args: ["-p", "--trust", "{{safety}}", "{{working_dir}}", "--output-format", "json", "{{model}}"]
# Only read_only gets extra flags; the other profiles run print mode as
# before. --trust is unrelated to capability — it just suppresses the
# workspace-trust prompt, which would hang a non-interactive run.
safety:
read_only: ["--mode", "plan"]
# cursor-agent needs SOME --workspace value even with no workingDir —
# fall back to the user's home directory.
working_dir: { flag: "--workspace", fallback: home }
model: { flag: "--model" }
api_key_env_var: CURSOR_API_KEY
file_list_header: "Focus on these files:"
file_list_bullet: " - "
output:
mode: json_field
fields: [result, output, text]
usage: { input: [usage.input_tokens, usage.prompt_tokens], output: [usage.output_tokens, usage.completion_tokens] }
# The success reply verified above carries "is_error": false beside
# "result". Without this, a reply with "is_error": true and exit 0 —
# a usage limit reported in the body — was taken as a success,
# because a parsed "result" is all strict mode asks for: the text was
# returned as the answer and the rate-limit scan never ran. Found in
# an audit. The error text is in "result", read as usual.
error: { field: is_error }
# success_requires_output omitted -> strict: exit 0 with no parseable
# output still counts as failure.
# ── Antigravity ─────────────────────────────────────────────────────────────
# Google's successor to Gemini CLI (shut down 2026-06-18). Best for plan +
# review — 1M+ context holds whole codebases; free_quota billing. Model
# selection lives in the agy CLI's own config, not per route.
#
# OPT-IN. Auto-detection finds `agy` but adds this route switched off
# (`enabled: false` here). Google's Antigravity terms, read 2026-10-08, call
# using "third party software, tools, or services to access the Service" a
# breach and allow suspension, so driving agy from an orchestrator is the
# operator's decision, not a default. Turn it on with
# overrides:
# antigravity_cli: { enabled: true }
# or write a `clis:` entry with `harness: antigravity_cli`, which is already
# that decision and is never switched off by this line. Details:
# docs/configuration.md#vendor-terms
- name: antigravity_cli
harness: antigravity_cli
enabled: false
command: agy
model_hint: >-
Cross-vendor catalog (Gemini plus some Claude/GPT-OSS models). Gemini's
portion: https://ai.google.dev/gemini-api/docs/models; check the agy
CLI's own docs for the rest.
cli_capability: 1.0
tier: 1
capabilities: { execute: 0.87, plan: 0.97, review: 0.95 }
max_output_tokens: 65536
max_input_tokens: 2000000
provider: google
surface: antigravity_cli
auth_source: product_login
billing_kind: free_quota
paid_usage_possible: false
# workspace_edit promises "no arbitrary shell", and the workspace_edit
# flags below auto-approve every tool request — shell included — with
# nothing restricting the terminal: it is --sandbox ("Run in a sandbox
# with terminal restrictions enabled", `agy --help`) that does that for
# read_only, and whether edits still work under it is not verified, nor
# whether it does anything on Windows (Cursor's equivalent does not). So
# this route declares what it can actually guarantee, the same way the
# cursor route does: a workspace_edit request is skipped unless full_auto
# is acceptable.
effective_safety:
read_only: read_only
workspace_edit: full_auto
full_auto: full_auto
# A wall clock of its own: the longest logged successful Antigravity run
# took 706 s (19 successes, dispatch log to 2026-10-01), while runs that
# hung were cancelled by hand after 31-35 minutes. Its own internal limit
# fired only twice. 25 minutes leaves twice the longest success.
timeout_ms: 1500000
# stream-json (below) prints a step event as each step starts and ends,
# so a long silence is a hang, as for codex_cli.
idle_timeout_ms: 900000
protocol:
# --output-format stream-json: one JSON event per step, as it happens.
# In text mode agy printed nothing until it finished, so a slow job and
# a stuck one looked the same and a cancelled one left nothing behind.
args: ["{{model}}", "{{safety}}", "{{working_dir}}", "{{file_dirs}}", "--output-format", "stream-json", "--print", "{{prompt}}"]
# --print is headless: agy cannot prompt for per-tool permissions, so
# every profile needs auto-approval or tools are silently auto-denied
# ("a tool required the read_file permission that headless mode cannot
# prompt for"). read_only stays safe via --mode plan --sandbox (blocks
# writes/shell); the skip-permissions flag just lets reads proceed.
working_dir: { flag: "--add-dir" }
file_dirs: { flag: "--add-dir" }
model: { flag: "--model" }
safety:
read_only: ["--mode", "plan", "--sandbox", "--dangerously-skip-permissions"]
workspace_edit: ["--mode", "accept-edits", "--dangerously-skip-permissions"]
full_auto: ["--dangerously-skip-permissions"]
file_list_header: "Files to work with:"
file_list_bullet: " "
# Event shapes captured from agy 2026-10-01 (`--mode plan --sandbox
# --output-format stream-json --print`):
# {"event":"init","conversation_id":...,"init":{"cwd":...,"tools":[...]}}
# {"event":"step_update","step_update":{"step_index":2,"state":"ACTIVE",
# "step_type":"tool","tool_name":"run_command","tool_info":{"name":...,
# "parameters":{"CommandLine":"dir"}}}}
# {"event":"result","result":{"status":"SUCCESS","response":"...",
# "usage":{"input_tokens":17391,"output_tokens":698,...}}}
# The answer is result.response. That run's response was empty (its one
# tool call was denied), so a non-empty response was not observed; only
# SUCCESS was seen as a status, so no error rule is guessed at.
output:
mode: jsonl_stream
event_rules:
- when: { event: "result" }
emit: text
text_field: result.response
- when: { event: "step_update", "step_update.step_type": "tool", "step_update.state": "ACTIVE" }
emit: tool_use
name_field: step_update.tool_name
input_field: step_update.tool_info.parameters
# From the result only: each step_update carries its own usage too,
# and counting both would double it.
- when: { event: "result" }
emit: usage
input_token_fields: [result.usage.input_tokens]
output_token_fields: [result.usage.output_tokens]
# Strict (success_requires_output omitted): in stream-json an exit 0
# with an empty response would otherwise return the raw event stream as
# the answer. An empty answer is a failure, with the events kept.
# ── More routes from the same harnesses ─────────────────────────────────────
# A route is (harness, model) — declare as many as you want. `harness:` picks
# up everything from the matching entry above (including protocol), so a
# variant is a few lines:
#
# - name: codex_sol
# harness: codex
# model: gpt-5.6-sol
# tier: 1
# weight: 0.9
#
# - name: codex_ollama # Codex harness driving a local Ollama model
# harness: codex
# model: qwen3-coder:latest
# endpoint_mode: harness_native_endpoint
# endpoint_provider: ollama
# billing_kind: local_compute
# tier: 3
# ── A wholly new CLI harness ────────────────────────────────────────────────
# harness: generic + your own protocol: block — no code changes. protocol.args
# is written the way you'd type the command line; reserved {{tokens}}:
#
# {{prompt}} the prompt text (omitted from args if stdin: true)
# {{model}} [model.flag, value] if a model is set, else nothing
# {{safety}} safety[requested profile], zero or more tokens
# {{working_dir}} [working_dir.flag, dir, ...extra_args_when_set] if set
# {{file_dirs}} [file_dirs.flag, dir] per unique file directory
# {{native_args}} endpoint_native_args[endpoint_provider] under
# endpoint_mode: harness_native_endpoint
#
# Everything else in args passes through literally. You can also reuse an
# entry above by name (protocol: cursor) or extend one (protocol: { extends:
# codex, model: { flag: "--llm-model" } }) — safety merges per-profile.
# See docs/configuration.md#adding-a-harness for the full reference.
#
# - name: my_cli
# harness: generic
# command: my-cli
# tier: 3
# protocol:
# args: ["-p", "{{prompt}}", "{{model}}", "--json"]
# model: { flag: "--model" }
# output: { mode: json_field, fields: [result] }
endpoints:
# OpenAI-compatible HTTP endpoints — local or hosted. Same idea: an entry is
# a route. Endpoints are structurally read_only (no agent loop or file
# access), so they serve plan/review calls, never execute.
#
# - name: ollama
# base_url: http://localhost:11434/v1
# model: llama3.2
# billing_kind: local_compute
# tier: 3
#
# - name: lmstudio
# base_url: http://localhost:1234/v1
# model: mistral-7b-instruct
# api_key: lm-studio
# billing_kind: local_compute
# tier: 3
#
# - name: gemini_api # AI Studio direct (free tier)
# base_url: https://generativelanguage.googleapis.com/v1beta/openai
# model: gemini-2.5-flash
# api_key: ${GEMINI_API_KEY}
# billing_kind: free_quota
# tier: 3
#
# - name: openrouter # metered — blocked until you opt in
# base_url: https://openrouter.ai/api/v1
# model: openai/gpt-5.4
# api_key: ${OPENROUTER_API_KEY}
# allow_paid_usage: true
# tier: 1
#
# - name: nvidia_nim # NVIDIA-hosted models (build.nvidia.com)
# base_url: https://integrate.api.nvidia.com/v1
# model: qwen/qwen3-coder-480b-a35b-instruct # any model id from build.nvidia.com
# api_key: ${NVIDIA_API_KEY}
# allow_paid_usage: true # metered API key -> blocked until you opt in
# tier: 2
# # Optional, both surfaced to calling agents via the `usage` tool. `usage`
# # with listModels: nvidia_nim returns this models: list VERBATIM, no
# # network call — declaring it overrides live discovery, e.g. to pin
# # specific ids or because the endpoint's live /models is noisy/untrusted.
# # Omit models: and listModels instead does a live GET {base_url}/models
# # (key stays server-side either way). model_hint points humans/agents at
# # the full catalog for picking new ids to add here.
# models: [qwen/qwen3-coder-480b-a35b-instruct]
# model_hint: "Full catalog: https://build.nvidia.com/models"
#
# - name: anthropic_api # Claude via the direct Anthropic API
# base_url: https://api.anthropic.com/v1
# model: claude-opus-4-6
# api_key: ${ANTHROPIC_API_KEY}
# wire_protocol: anthropic_messages # see below — different from the default
# allow_paid_usage: true # metered API key -> blocked until you opt in
# max_output_tokens: 8192 # required by the Messages API; defaults to 8192 if omitted
# tier: 1
#
# wire_protocol picks which request/response shape this endpoint speaks:
#
# openai_chat_completions (default) POST {base_url}/chat/completions,
# Authorization: Bearer <api_key>, OpenAI's messages[]/choices[] shape.
# Covers Ollama, LM Studio, OpenRouter, NVIDIA NIM, Gemini's OpenAI-
# compatible endpoint, and most other hosted/local model servers.
#
# anthropic_messages POST {base_url}/messages, x-api-key + anthropic-
# version headers (NOT Bearer), system prompt at the top level (not a
# messages[] entry), max_tokens required, content: [{type,text}] +
# usage: {input_tokens,output_tokens} response shape. Use this for
# Anthropic's own API or any third-party host that mirrors its
# Messages API. thinking_level isn't translated to Anthropic's
# extended-thinking parameter yet.
#
# - name: claude_compatible_host # a third party serving the Anthropic shape
# base_url: https://your-host.example.com/v1
# model: claude-opus-4-6
# api_key: ${CLAUDE_COMPATIBLE_API_KEY}
# wire_protocol: anthropic_messages
# allow_paid_usage: true
# tier: 2
# ── Global settings ───────────────────────────────────────────────────────────
# OpenTelemetry traces — OFF by default. Purely local operator observability:
# only useful if you run an OTLP collector (Jaeger, Tempo, ...) on
# localhost:4318 (or point OTEL_EXPORTER_OTLP_ENDPOINT elsewhere). Nothing is
# ever sent anywhere unless you enable this AND run a collector.
# telemetry:
# enabled: true
# How long job artifacts (~/.harness-dispatch/jobs: manifests, stdout/stderr
# logs, workspace snapshots) are kept before being pruned. Default: 7 days.
# The dispatch log (~/.harness-dispatch/logs/dispatches.jsonl) is size-capped
# at ~5MB with one rotation instead of time-based.
# retention:
# jobs_days: 7 # 0 = keep forever (same convention as max_concurrent_runs: 0)
# Instructions every connecting agent is told, after the server's own — this
# machine's routing policy, written once instead of in each client's CLAUDE.md
# or AGENTS.md. A route can carry its own `instructions:` too (on its clis:,
# endpoints: or overrides: entry); those are listed by route id and returned by
# the `usage` tool. 1,000 characters each; read when a session connects. See
# docs/configuration.md#instructions-for-connecting-agents.
# instructions: |
# Pick the cheapest model that can do the task; keep the strongest for hard
# judgment.
# Per-route knobs worth knowing (all optional; every one is in the config
# reference, docs/configuration.md#config-reference): enabled, weight,
# timeout_ms, idle_timeout_ms, safety_profile, effective_safety, workspace_policy (shared |
# shared_locked | copy | git_worktree), instructions, thinking_level,
# escalate_model / escalate_on, resource_weight, allow_paid_usage, billing_kind,
# paid_usage_possible.
#
# disabled: [route_name, ...] stops an auto-detected harness from being added. It
# never affects routes you list under clis:/endpoints: (use `enabled: false` on
# the entry for that), and does nothing at all in a file that lists any routes
# unless it also says `detect: true`.