@crvouga/dev-fleet 0.0.0-stage → 0.1.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 (333) hide show
  1. package/.mise/locks/aws-cdk/2.1146.1/aube-lock.yaml +24 -0
  2. package/.mise/locks/aws-cdk/2.1146.1/package.json +7 -0
  3. package/.mise/locks/azure-cli/2.91.0~a837e014/pyproject.toml +5 -0
  4. package/.mise/locks/azure-cli/2.91.0~a837e014/uv.lock +2794 -0
  5. package/.mise/locks/gemini/0.66.0~84aac8e6/aube-lock.yaml +123 -0
  6. package/.mise/locks/gemini/0.66.0~84aac8e6/package.json +12 -0
  7. package/.mise/locks/npm-devcontainers-cli/0.90.0/aube-lock.yaml +24 -0
  8. package/.mise/locks/npm-devcontainers-cli/0.90.0/package.json +7 -0
  9. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/aube-lock.yaml +1432 -0
  10. package/.mise/locks/npm-modelcontextprotocol-inspector/2.13.0~4c109bea/package.json +7 -0
  11. package/.mise/locks/npm-vercel/65.8.0/aube-lock.yaml +3595 -0
  12. package/.mise/locks/npm-vercel/65.8.0/package.json +7 -0
  13. package/.mise/locks/npm-wrangler/4.156.0/aube-lock.yaml +895 -0
  14. package/.mise/locks/npm-wrangler/4.156.0/package.json +7 -0
  15. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/aube-lock.yaml +24 -0
  16. package/.mise/locks/npm-yarnpkg-cli-dist/4.18.1/package.json +7 -0
  17. package/LICENSE +21 -0
  18. package/README.md +157 -2
  19. package/agents/antigravity-proxy.ts +3 -0
  20. package/agents/audit.md +89 -0
  21. package/agents/claude-proxy +5 -0
  22. package/agents/claude-proxy.ts +3 -0
  23. package/agents/codex-proxy.ts +3 -0
  24. package/agents/grok-proxy.ts +3 -0
  25. package/agents/managed-proxy.ts +55 -0
  26. package/agents/opencode-proxy.ts +3 -0
  27. package/agents/registry.json +209 -0
  28. package/agents/registry.schema.json +149 -0
  29. package/agents/setup.md +128 -0
  30. package/agents/t3.json +40 -0
  31. package/ansible/macos/manager.yml +30 -0
  32. package/ansible/macos/policy.json +17 -0
  33. package/ansible/monitoring/README.md +154 -0
  34. package/ansible/monitoring/agent-darwin.yml +198 -0
  35. package/ansible/monitoring/agent.yml +134 -0
  36. package/ansible/monitoring/beszel-agent.service +42 -0
  37. package/ansible/monitoring/compose.yml +74 -0
  38. package/ansible/monitoring/hub-darwin.yml +102 -0
  39. package/ansible/monitoring/lock.json +11 -0
  40. package/ansible/net/remote-probe.sh +13 -0
  41. package/ansible/ubuntu/authorize-sudo.sh +51 -0
  42. package/ansible/ubuntu/baseline.yml +89 -0
  43. package/ansible/ubuntu/disk-guard.py +104 -0
  44. package/ansible/ubuntu/policy.json +43 -0
  45. package/ansible/ubuntu/recovery.yml +15 -0
  46. package/ansible/ubuntu/t3/policy.json +7 -0
  47. package/ansible/ubuntu/t3-desktop.yml +220 -0
  48. package/ansible/ubuntu/t3-health.sh +33 -0
  49. package/ansible/ubuntu/t3-policy.yml +76 -0
  50. package/ansible/ubuntu/t3-route.yml +28 -0
  51. package/ansible/ubuntu/tailscale.yml +75 -0
  52. package/ansible/ubuntu/tasks/credentials.yml +47 -0
  53. package/ansible/ubuntu/tasks/docker.yml +31 -0
  54. package/ansible/ubuntu/tasks/packages.yml +56 -0
  55. package/ansible/ubuntu/tasks/power.yml +51 -0
  56. package/ansible/ubuntu/tasks/recovery.yml +39 -0
  57. package/ansible/ubuntu/tasks/resources.yml +252 -0
  58. package/ansible/ubuntu/tasks/services.yml +31 -0
  59. package/ansible/ubuntu/tasks/ssh.yml +71 -0
  60. package/ansible/ubuntu/tasks/user.yml +120 -0
  61. package/bin/devfleet.mjs +36 -0
  62. package/bootstrap/bootstrap.ps1 +50 -0
  63. package/bootstrap/bootstrap.sh +73 -0
  64. package/bootstrap/devfleet +7 -0
  65. package/bootstrap/docker-desktop.sh +33 -0
  66. package/bootstrap/doctor.ts +189 -0
  67. package/bootstrap/fleet-manager-run.sh +76 -0
  68. package/bootstrap/fleet-remote-bootstrap.sh +185 -0
  69. package/bootstrap/global-config.ts +113 -0
  70. package/bootstrap/install-launcher.sh +7 -0
  71. package/bootstrap/native-aws.ts +86 -0
  72. package/bootstrap/native-t3.ts +154 -0
  73. package/bootstrap/npm.ts +350 -0
  74. package/bootstrap/setup-status.ts +51 -0
  75. package/bootstrap/setup.ts +88 -0
  76. package/bootstrap/shell-path.ts +105 -0
  77. package/bootstrap/verify-t3-archive.py +49 -0
  78. package/bootstrap/worker-auth.ts +65 -0
  79. package/bootstrap/worker-github.ts +47 -0
  80. package/bootstrap/worker-tools.ts +24 -0
  81. package/cliproxy/AGENTS.md +11 -0
  82. package/cliproxy/README.md +269 -0
  83. package/cliproxy/clients/bootstrap.py +143 -0
  84. package/cliproxy/clients/launcher.py +57 -0
  85. package/cliproxy/compose.yaml +90 -0
  86. package/cliproxy/config.template.yaml +70 -0
  87. package/cliproxy/docs/cloud-adoption.md +16 -0
  88. package/cliproxy/docs/deployment.png +0 -0
  89. package/cliproxy/docs/deployment.svg +109 -0
  90. package/cliproxy/docs/health-workflow.example.yml +185 -0
  91. package/cliproxy/docs/migration.md +131 -0
  92. package/cliproxy/docs/model-health-report.schema.json +364 -0
  93. package/cliproxy/docs/model-health.md +393 -0
  94. package/cliproxy/image.lock.json +5 -0
  95. package/cliproxy/launchd/dev.devfleet.cliproxy-backup-check.plist +38 -0
  96. package/cliproxy/launchd/dev.devfleet.cliproxy-backup.plist +48 -0
  97. package/cliproxy/launchd/dev.devfleet.cliproxy-fleet.plist +32 -0
  98. package/cliproxy/launchd/dev.devfleet.cliproxy-gc.plist +38 -0
  99. package/cliproxy/launchd/dev.devfleet.cliproxy-monitor.plist +29 -0
  100. package/cliproxy/launchd/dev.devfleet.cliproxy-vault-renew.plist +31 -0
  101. package/cliproxy/models.json +1618 -0
  102. package/cliproxy/requirements.lock +78 -0
  103. package/cliproxy/scripts/accounts.py +38 -0
  104. package/cliproxy/scripts/audit.py +123 -0
  105. package/cliproxy/scripts/backup +234 -0
  106. package/cliproxy/scripts/check.py +34 -0
  107. package/cliproxy/scripts/checkpoint.py +226 -0
  108. package/cliproxy/scripts/cli.py +64 -0
  109. package/cliproxy/scripts/cliproxy_host.py +257 -0
  110. package/cliproxy/scripts/cloud_state.py +150 -0
  111. package/cliproxy/scripts/container-health +6 -0
  112. package/cliproxy/scripts/docker +24 -0
  113. package/cliproxy/scripts/external-health.py +79 -0
  114. package/cliproxy/scripts/fleet_config.py +43 -0
  115. package/cliproxy/scripts/fleet_sync.py +274 -0
  116. package/cliproxy/scripts/initialize +84 -0
  117. package/cliproxy/scripts/install-dependencies +48 -0
  118. package/cliproxy/scripts/install.py +406 -0
  119. package/cliproxy/scripts/model_health.py +16 -0
  120. package/cliproxy/scripts/modelhealth/__init__.py +5 -0
  121. package/cliproxy/scripts/modelhealth/checks.py +429 -0
  122. package/cliproxy/scripts/modelhealth/cli.py +117 -0
  123. package/cliproxy/scripts/modelhealth/config.py +186 -0
  124. package/cliproxy/scripts/modelhealth/emulator.py +536 -0
  125. package/cliproxy/scripts/modelhealth/expect.py +88 -0
  126. package/cliproxy/scripts/modelhealth/findings.py +183 -0
  127. package/cliproxy/scripts/modelhealth/protocol.py +330 -0
  128. package/cliproxy/scripts/modelhealth/redact.py +28 -0
  129. package/cliproxy/scripts/modelhealth/report.py +150 -0
  130. package/cliproxy/scripts/modelhealth/runner.py +390 -0
  131. package/cliproxy/scripts/modelhealth/scenario.py +131 -0
  132. package/cliproxy/scripts/modelhealth/schema.py +63 -0
  133. package/cliproxy/scripts/modelhealth/simulate.py +111 -0
  134. package/cliproxy/scripts/modelhealth/transport.py +213 -0
  135. package/cliproxy/scripts/monitor +68 -0
  136. package/cliproxy/scripts/ops +527 -0
  137. package/cliproxy/scripts/public-endpoint +127 -0
  138. package/cliproxy/scripts/recover_node.py +177 -0
  139. package/cliproxy/scripts/restore-verify +60 -0
  140. package/cliproxy/scripts/retention.py +144 -0
  141. package/cliproxy/scripts/service-start +7 -0
  142. package/cliproxy/scripts/validate-config +56 -0
  143. package/cliproxy/scripts/vault-access.py +57 -0
  144. package/cliproxy/scripts/vault-backup-login +55 -0
  145. package/cliproxy/scripts/vault-recovery +56 -0
  146. package/cliproxy/scripts/vault-renew +30 -0
  147. package/cliproxy/systemd/cliproxy-backup-check.service +10 -0
  148. package/cliproxy/systemd/cliproxy-backup-check.timer +8 -0
  149. package/cliproxy/systemd/cliproxy-backup.service +13 -0
  150. package/cliproxy/systemd/cliproxy-backup.timer +8 -0
  151. package/cliproxy/systemd/cliproxy-fleet.service +17 -0
  152. package/cliproxy/systemd/cliproxy-gc.service +13 -0
  153. package/cliproxy/systemd/cliproxy-gc.timer +8 -0
  154. package/cliproxy/systemd/cliproxy-monitor.service +8 -0
  155. package/cliproxy/systemd/cliproxy-monitor.timer +7 -0
  156. package/cliproxy/systemd/cliproxy-vault-renew.service +8 -0
  157. package/cliproxy/systemd/cliproxy-vault-renew.timer +7 -0
  158. package/cliproxy/vendor/LICENSE +22 -0
  159. package/cliproxy/vendor/config.upstream.yaml +1349 -0
  160. package/examples/fleet.json +95 -0
  161. package/examples/roster.json +13 -0
  162. package/mise.lock +1492 -0
  163. package/mise.toml +99 -0
  164. package/package.json +65 -4
  165. package/schema/fleet.schema.json +364 -0
  166. package/schema/roster.schema.json +77 -0
  167. package/src/agents/account-relay.ts +111 -0
  168. package/src/agents/accounts.ts +85 -0
  169. package/src/agents/audit.ts +77 -0
  170. package/src/agents/cli.ts +13 -0
  171. package/src/agents/command.ts +204 -0
  172. package/src/agents/converge.ts +123 -0
  173. package/src/agents/doctor.ts +107 -0
  174. package/src/agents/errors.ts +2 -0
  175. package/src/agents/files.ts +73 -0
  176. package/src/agents/fleet-verify.ts +156 -0
  177. package/src/agents/launch.ts +290 -0
  178. package/src/agents/native-verify.ts +226 -0
  179. package/src/agents/provider-errors.ts +50 -0
  180. package/src/agents/registry.ts +55 -0
  181. package/src/agents/render.ts +110 -0
  182. package/src/agents/round-trip.ts +192 -0
  183. package/src/agents/secrets.ts +13 -0
  184. package/src/agents/toolchain.ts +34 -0
  185. package/src/agents/verify-cache.ts +103 -0
  186. package/src/agents/verify.ts +127 -0
  187. package/src/auth-checks.ts +163 -0
  188. package/src/cli/agent-guide.ts +95 -0
  189. package/src/cli/app.ts +115 -0
  190. package/src/cli/cli.ts +21 -0
  191. package/src/cli/command-index.ts +50 -0
  192. package/src/cli/contracts.ts +25 -0
  193. package/src/cli/fleet-reference.ts +17 -0
  194. package/src/cli/fleet-sync-checkout.ts +91 -0
  195. package/src/cli/fleet-sync-enrollment.ts +144 -0
  196. package/src/cli/fleet-sync-forward.ts +134 -0
  197. package/src/cli/fleet-sync-manager.ts +193 -0
  198. package/src/cli/fleet-sync-monitoring.ts +185 -0
  199. package/src/cli/fleet-sync-output.ts +314 -0
  200. package/src/cli/fleet-sync-privilege.ts +27 -0
  201. package/src/cli/fleet-sync-progress.ts +109 -0
  202. package/src/cli/fleet-sync-remote.ts +236 -0
  203. package/src/cli/fleet-sync-report.ts +26 -0
  204. package/src/cli/fleet-sync-schedule.ts +138 -0
  205. package/src/cli/fleet-sync-vault.ts +166 -0
  206. package/src/cli/fleet-sync.ts +309 -0
  207. package/src/cli/fleet.ts +163 -0
  208. package/src/cli/managed-health.ts +219 -0
  209. package/src/cli/node-deployment.ts +34 -0
  210. package/src/cli/node-sync.ts +114 -0
  211. package/src/cli/node-t3.ts +31 -0
  212. package/src/cli/options.ts +141 -0
  213. package/src/cli/probe-source.ts +7 -0
  214. package/src/cli/program.ts +169 -0
  215. package/src/cli/register-fleet.ts +246 -0
  216. package/src/cli/registry.ts +158 -0
  217. package/src/cli/repair-apply.ts +67 -0
  218. package/src/cli/repair-guard.ts +11 -0
  219. package/src/cli/repair-inspect.ts +150 -0
  220. package/src/cli/repair-script.ts +76 -0
  221. package/src/cli/repair.ts +209 -0
  222. package/src/cli/roster.ts +89 -0
  223. package/src/cli/search.ts +35 -0
  224. package/src/cli/settings.ts +64 -0
  225. package/src/cli/source-operations.ts +78 -0
  226. package/src/cli/t3/command.ts +90 -0
  227. package/src/cli/t3/register.ts +55 -0
  228. package/src/cli/tailnet.ts +127 -0
  229. package/src/cli/ui-actions.ts +133 -0
  230. package/src/cli/ui-controller.ts +113 -0
  231. package/src/cli/ui-data.ts +106 -0
  232. package/src/cli/ui-execute.ts +163 -0
  233. package/src/cli/ui-repairs.ts +34 -0
  234. package/src/cli/ui-reports.ts +211 -0
  235. package/src/cli/ui-targets.ts +100 -0
  236. package/src/cli/ui-terminal.ts +109 -0
  237. package/src/cli/ui-types.ts +49 -0
  238. package/src/cli/ui-wizard.ts +101 -0
  239. package/src/cli/ui.ts +9 -0
  240. package/src/cli/worker-health-output.ts +37 -0
  241. package/src/config.ts +186 -0
  242. package/src/configure.ts +255 -0
  243. package/src/definition.ts +260 -0
  244. package/src/diagnostics.ts +92 -0
  245. package/src/doctor-check.ts +10 -0
  246. package/src/errors.ts +1 -0
  247. package/src/fingerprint.ts +120 -0
  248. package/src/fleet-config.ts +88 -0
  249. package/src/index.ts +6 -0
  250. package/src/input-hashes.ts +73 -0
  251. package/src/machine/local-source.ts +52 -0
  252. package/src/machine/lock-error.ts +2 -0
  253. package/src/machine/platform.ts +44 -0
  254. package/src/machine/source.ts +68 -0
  255. package/src/machine/state.ts +185 -0
  256. package/src/machine-health.ts +95 -0
  257. package/src/managed-source.ts +47 -0
  258. package/src/manager-failure.ts +48 -0
  259. package/src/manager-sync.ts +400 -0
  260. package/src/manager.ts +62 -0
  261. package/src/monitoring/apply.ts +171 -0
  262. package/src/monitoring/beszel.ts +191 -0
  263. package/src/monitoring/enroll.ts +110 -0
  264. package/src/monitoring/hub-access.ts +113 -0
  265. package/src/monitoring/hub-viewer.ts +142 -0
  266. package/src/monitoring/hub.ts +93 -0
  267. package/src/monitoring/manager-agent.ts +73 -0
  268. package/src/monitoring/manager-hub.ts +232 -0
  269. package/src/monitoring/remote.ts +65 -0
  270. package/src/network-definition.ts +83 -0
  271. package/src/node-converge-linux.ts +372 -0
  272. package/src/node-converge-shared.ts +83 -0
  273. package/src/node-converge.ts +40 -0
  274. package/src/onboard.ts +35 -0
  275. package/src/platform/ansible-receipt.ts +128 -0
  276. package/src/platform/ansible.ts +62 -0
  277. package/src/platform/darwin.ts +203 -0
  278. package/src/platform/detect.ts +11 -0
  279. package/src/platform/fallback.ts +110 -0
  280. package/src/platform/index.ts +38 -0
  281. package/src/platform/launchd.ts +98 -0
  282. package/src/platform/linux.ts +139 -0
  283. package/src/platform/ps.ts +45 -0
  284. package/src/platform/resource-verify.ts +128 -0
  285. package/src/platform/systemd-user.ts +95 -0
  286. package/src/platform/t3-policy.ts +137 -0
  287. package/src/platform/t3-route.ts +68 -0
  288. package/src/platform/t3-service.ts +75 -0
  289. package/src/platform/types.ts +123 -0
  290. package/src/platform/ubuntu-audit.ts +63 -0
  291. package/src/platform/ubuntu-verify.ts +105 -0
  292. package/src/platform/windows.ts +148 -0
  293. package/src/power-recovery.ts +55 -0
  294. package/src/probe.ts +51 -0
  295. package/src/protocol.ts +119 -0
  296. package/src/proxy-converge.ts +85 -0
  297. package/src/remote-bootstrap.ts +212 -0
  298. package/src/remote-entry.ts +89 -0
  299. package/src/remote-lock.ts +12 -0
  300. package/src/repair-catalog.ts +206 -0
  301. package/src/repair-plan.ts +78 -0
  302. package/src/repair-runner.ts +84 -0
  303. package/src/report.ts +71 -0
  304. package/src/runner.ts +104 -0
  305. package/src/secrets.ts +169 -0
  306. package/src/service-auth.ts +115 -0
  307. package/src/ssh.ts +135 -0
  308. package/src/sync.ts +104 -0
  309. package/src/t3/accounts.ts +81 -0
  310. package/src/t3/desired.ts +165 -0
  311. package/src/t3/files.ts +81 -0
  312. package/src/t3/names-check.ts +53 -0
  313. package/src/t3/names.ts +155 -0
  314. package/src/t3/plan.ts +82 -0
  315. package/src/t3/providers.ts +74 -0
  316. package/src/t3/reconcile.ts +123 -0
  317. package/src/t3-endpoint.ts +48 -0
  318. package/src/t3-linked-check.ts +101 -0
  319. package/src/tailnet-health.ts +231 -0
  320. package/src/tailnet-matching.ts +37 -0
  321. package/src/tailscale.ts +141 -0
  322. package/src/test-fixtures.ts +36 -0
  323. package/src/toolchain-checks.ts +83 -0
  324. package/src/topology.ts +24 -0
  325. package/src/transport.ts +122 -0
  326. package/src/types.ts +95 -0
  327. package/src/vault-config.ts +38 -0
  328. package/src/worker-access-checks.ts +115 -0
  329. package/src/worker-capacity.ts +34 -0
  330. package/src/worker-host-checks.ts +176 -0
  331. package/src/worker-policy.ts +64 -0
  332. package/src/worker-readiness.ts +64 -0
  333. package/src/workstation-checks.ts +29 -0
@@ -0,0 +1,1618 @@
1
+ {
2
+ "schema": 1,
3
+ "endpoint": {
4
+ "key_env": "CLIPROXY_API_KEY",
5
+ "vault_field": "CLIPROXY_FLEET_CLIENT_API_KEY"
6
+ },
7
+ "runs": {
8
+ "liveness": {
9
+ "description": "Reachability and TLS only. No credentials, no inference.",
10
+ "levels": ["liveness"]
11
+ },
12
+ "readiness": {
13
+ "description": "Liveness plus authentication, model discovery and configured model ids. No inference.",
14
+ "levels": ["liveness", "readiness"]
15
+ },
16
+ "light": {
17
+ "description": "Readiness plus the cheapest real inference per model (frequent monitoring).",
18
+ "levels": ["liveness", "readiness", "e2e"],
19
+ "max_tier": "light",
20
+ "budget": {
21
+ "max_requests": 16,
22
+ "max_requests_per_model": 3,
23
+ "max_output_tokens": 2000,
24
+ "concurrency": 2
25
+ }
26
+ },
27
+ "nightly": {
28
+ "description": "Comprehensive protocol, tool-calling, streaming and agent workflows per model.",
29
+ "levels": ["liveness", "readiness", "e2e"],
30
+ "max_tier": "full",
31
+ "budget": {
32
+ "max_requests": 240,
33
+ "max_requests_per_model": 60,
34
+ "max_output_tokens": 24000,
35
+ "concurrency": 2
36
+ },
37
+ "simulations": true
38
+ },
39
+ "manual": {
40
+ "description": "Everything in nightly, plus wrong-key probes and the emulator fault-injection scenarios.",
41
+ "levels": ["liveness", "readiness", "e2e"],
42
+ "max_tier": "manual",
43
+ "simulations": true,
44
+ "budget": {
45
+ "max_requests": 300,
46
+ "max_requests_per_model": 75,
47
+ "max_output_tokens": 36000,
48
+ "concurrency": 2
49
+ }
50
+ }
51
+ },
52
+ "timeouts": {
53
+ "request_s": 60,
54
+ "stream_idle_s": 45,
55
+ "stream_deadline_s": 120,
56
+ "liveness_s": 15
57
+ },
58
+ "retry": {
59
+ "inference": 0,
60
+ "read_only": 1,
61
+ "backoff_s": 2,
62
+ "note": "Inference is never retried: a retry would multiply cost and hide a persistent failure."
63
+ },
64
+ "health": {
65
+ "abort_model_after_consecutive_failures": 4,
66
+ "unhealthy_if_failed_ratio": 0.5,
67
+ "latency_ms": {
68
+ "ttft_p50_warn": 20000,
69
+ "total_p95_warn": 90000
70
+ },
71
+ "required_model_fields": ["id"],
72
+ "model_consistency": true,
73
+ "rate_limit_backoff_s": 1800
74
+ },
75
+ "protocols": {
76
+ "chat": {
77
+ "path": "/chat/completions",
78
+ "token_limit_field": "max_tokens"
79
+ },
80
+ "responses": {
81
+ "path": "/responses",
82
+ "token_limit_field": "max_output_tokens"
83
+ }
84
+ },
85
+ "capability_profiles": {
86
+ "frontier-agent": {
87
+ "default": "required",
88
+ "optional": [
89
+ "*.developer",
90
+ "*.json_schema",
91
+ "*.parallel_tools",
92
+ "chat.finish_length",
93
+ "*.stream_long"
94
+ ],
95
+ "unsupported": {},
96
+ "extra": {
97
+ "chat": {},
98
+ "responses": {}
99
+ }
100
+ }
101
+ },
102
+ "models": [
103
+ {
104
+ "provider": "anthropic",
105
+ "profile": "frontier-agent",
106
+ "id": "claude-opus-5-5",
107
+ "capabilities": {}
108
+ },
109
+ {
110
+ "provider": "anthropic",
111
+ "profile": "frontier-agent",
112
+ "id": "claude-sonnet-5-5",
113
+ "capabilities": {}
114
+ },
115
+ {
116
+ "provider": "openai",
117
+ "profile": "frontier-agent",
118
+ "extra": {
119
+ "chat": {
120
+ "reasoning_effort": "low"
121
+ },
122
+ "responses": {
123
+ "reasoning": {
124
+ "effort": "low"
125
+ }
126
+ }
127
+ },
128
+ "id": "gpt-6-astra",
129
+ "capabilities": {}
130
+ },
131
+ {
132
+ "provider": "openai",
133
+ "profile": "frontier-agent",
134
+ "extra": {
135
+ "chat": {
136
+ "reasoning_effort": "low"
137
+ },
138
+ "responses": {
139
+ "reasoning": {
140
+ "effort": "low"
141
+ }
142
+ }
143
+ },
144
+ "id": "gpt-6.1-sol",
145
+ "capabilities": {}
146
+ }
147
+ ],
148
+ "simulated_capabilities": {
149
+ "routing.failover": "Request succeeds on another account after the first account's quota is exhausted.",
150
+ "routing.recovery": "A cooled-down account serves requests again after its cooldown elapses.",
151
+ "routing.model_consistency": "Every account behind a model returns the same response shape and model id.",
152
+ "routing.all_unavailable": "When every account is unavailable the client gets a prompt OpenAI-style error.",
153
+ "limits.rate_limit": "429 and Retry-After are classified and never retried by the harness.",
154
+ "limits.timeout": "A stalled upstream is cut off by the harness deadline.",
155
+ "limits.upstream_failure": "5xx and truncated streams are classified as upstream or protocol failures.",
156
+ "routing.affinity": "Requests in one session stay on one account while it is available.",
157
+ "routing.fill_first": "One account takes all traffic until its quota is exhausted; only then does the next account serve."
158
+ },
159
+ "tools": {
160
+ "get_weather": {
161
+ "description": "Get the current weather for a city.",
162
+ "parameters": {
163
+ "type": "object",
164
+ "properties": {
165
+ "city": {
166
+ "type": "string",
167
+ "minLength": 2
168
+ },
169
+ "unit": {
170
+ "type": "string",
171
+ "enum": ["celsius", "fahrenheit"]
172
+ }
173
+ },
174
+ "required": ["city", "unit"],
175
+ "additionalProperties": false
176
+ }
177
+ },
178
+ "echo_token": {
179
+ "description": "Record a verification token exactly as given.",
180
+ "parameters": {
181
+ "type": "object",
182
+ "properties": {
183
+ "token": {
184
+ "type": "string",
185
+ "pattern": "^[A-Z]{3}-[0-9]{4}$"
186
+ }
187
+ },
188
+ "required": ["token"],
189
+ "additionalProperties": false
190
+ }
191
+ },
192
+ "read_file": {
193
+ "description": "Read a UTF-8 text file from the workspace.",
194
+ "parameters": {
195
+ "type": "object",
196
+ "properties": {
197
+ "path": {
198
+ "type": "string",
199
+ "minLength": 1
200
+ }
201
+ },
202
+ "required": ["path"],
203
+ "additionalProperties": false
204
+ }
205
+ },
206
+ "write_file": {
207
+ "description": "Replace the full contents of a workspace file.",
208
+ "parameters": {
209
+ "type": "object",
210
+ "properties": {
211
+ "path": {
212
+ "type": "string",
213
+ "minLength": 1
214
+ },
215
+ "content": {
216
+ "type": "string",
217
+ "minLength": 1
218
+ }
219
+ },
220
+ "required": ["path", "content"],
221
+ "additionalProperties": false
222
+ }
223
+ },
224
+ "run_tests": {
225
+ "description": "Run the test file and report the result.",
226
+ "parameters": {
227
+ "type": "object",
228
+ "properties": {
229
+ "path": {
230
+ "type": "string",
231
+ "minLength": 1
232
+ }
233
+ },
234
+ "required": ["path"],
235
+ "additionalProperties": false
236
+ }
237
+ }
238
+ },
239
+ "checks": [
240
+ {
241
+ "id": "basic",
242
+ "title": "Non-streaming completion",
243
+ "suites": ["core"],
244
+ "protocols": ["chat", "responses"],
245
+ "tier": {
246
+ "chat": "light",
247
+ "responses": "full"
248
+ },
249
+ "severity": "critical",
250
+ "kind": "request",
251
+ "turn": {
252
+ "messages": [
253
+ {
254
+ "role": "user",
255
+ "content": "Reply with the single word OK."
256
+ }
257
+ ],
258
+ "max_tokens": 256
259
+ },
260
+ "expect": {
261
+ "finish_reason": ["stop"],
262
+ "content": {
263
+ "non_empty": true
264
+ },
265
+ "usage": true
266
+ }
267
+ },
268
+ {
269
+ "id": "stream",
270
+ "title": "Streaming completion, SSE framing and termination",
271
+ "suites": ["core", "cursor"],
272
+ "protocols": ["chat", "responses"],
273
+ "tier": {
274
+ "chat": "full",
275
+ "responses": "light"
276
+ },
277
+ "severity": "critical",
278
+ "kind": "request",
279
+ "turn": {
280
+ "messages": [
281
+ {
282
+ "role": "user",
283
+ "content": "Reply with the single word OK."
284
+ }
285
+ ],
286
+ "max_tokens": 256,
287
+ "stream": true
288
+ },
289
+ "expect": {
290
+ "finish_reason": ["stop"],
291
+ "content": {
292
+ "non_empty": true
293
+ },
294
+ "stream": {
295
+ "min_events": 2
296
+ }
297
+ }
298
+ },
299
+ {
300
+ "id": "stream_usage",
301
+ "title": "Streaming usage metadata and finish reason",
302
+ "suites": ["core", "cursor"],
303
+ "protocols": ["chat", "responses"],
304
+ "tier": "full",
305
+ "severity": "major",
306
+ "kind": "request",
307
+ "turn": {
308
+ "messages": [
309
+ {
310
+ "role": "user",
311
+ "content": "Reply with the single word OK."
312
+ }
313
+ ],
314
+ "max_tokens": 256,
315
+ "stream": true,
316
+ "include_usage": true
317
+ },
318
+ "expect": {
319
+ "finish_reason": ["stop"],
320
+ "usage": true,
321
+ "stream": {
322
+ "min_events": 2
323
+ }
324
+ }
325
+ },
326
+ {
327
+ "id": "multi_turn",
328
+ "title": "Multi-turn context preservation",
329
+ "suites": ["core"],
330
+ "protocols": ["chat", "responses"],
331
+ "tier": "full",
332
+ "severity": "major",
333
+ "kind": "request",
334
+ "turn": {
335
+ "messages": [
336
+ {
337
+ "role": "user",
338
+ "content": "Remember this code word: {codeword}. Reply only with READY."
339
+ },
340
+ {
341
+ "role": "assistant",
342
+ "content": "READY"
343
+ },
344
+ {
345
+ "role": "user",
346
+ "content": "What was the code word? Reply with the code word only."
347
+ }
348
+ ],
349
+ "max_tokens": 256
350
+ },
351
+ "expect": {
352
+ "content": {
353
+ "contains": "{codeword}"
354
+ },
355
+ "finish_reason": ["stop"]
356
+ }
357
+ },
358
+ {
359
+ "id": "system",
360
+ "title": "System instruction is honored",
361
+ "suites": ["core"],
362
+ "protocols": ["chat", "responses"],
363
+ "tier": "full",
364
+ "severity": "major",
365
+ "kind": "request",
366
+ "turn": {
367
+ "system": "You are a terse assistant. Always end your reply with the exact marker {marker}.",
368
+ "messages": [
369
+ {
370
+ "role": "user",
371
+ "content": "Say hello."
372
+ }
373
+ ],
374
+ "max_tokens": 256
375
+ },
376
+ "expect": {
377
+ "content": {
378
+ "contains": "{marker}"
379
+ },
380
+ "finish_reason": ["stop"]
381
+ }
382
+ },
383
+ {
384
+ "id": "developer",
385
+ "title": "Developer instruction is honored",
386
+ "suites": ["core"],
387
+ "protocols": ["chat", "responses"],
388
+ "tier": "full",
389
+ "severity": "minor",
390
+ "kind": "request",
391
+ "turn": {
392
+ "developer": "Always end your reply with the exact marker {marker}.",
393
+ "messages": [
394
+ {
395
+ "role": "user",
396
+ "content": "Say hello."
397
+ }
398
+ ],
399
+ "max_tokens": 256
400
+ },
401
+ "expect": {
402
+ "content": {
403
+ "contains": "{marker}"
404
+ },
405
+ "finish_reason": ["stop"]
406
+ }
407
+ },
408
+ {
409
+ "id": "tools",
410
+ "title": "Forced tool call with structured arguments",
411
+ "suites": ["core", "cursor"],
412
+ "protocols": ["chat", "responses"],
413
+ "tier": "full",
414
+ "severity": "critical",
415
+ "kind": "request",
416
+ "turn": {
417
+ "messages": [
418
+ {
419
+ "role": "user",
420
+ "content": "What is the weather in [[Paris]]? Use the tool with celsius."
421
+ }
422
+ ],
423
+ "tools": ["get_weather"],
424
+ "tool_choice": {
425
+ "name": "get_weather"
426
+ },
427
+ "max_tokens": 256
428
+ },
429
+ "expect": {
430
+ "finish_reason": ["tool_calls", "stop"],
431
+ "tool_calls": {
432
+ "min": 1,
433
+ "max": 1,
434
+ "names": ["get_weather"],
435
+ "unique_ids": true,
436
+ "arguments_schema": "from_tools",
437
+ "arguments": {
438
+ "city": "Paris",
439
+ "unit": "celsius"
440
+ }
441
+ }
442
+ }
443
+ },
444
+ {
445
+ "id": "tools_stream",
446
+ "title": "Streaming tool call, incremental argument assembly",
447
+ "suites": ["core", "cursor"],
448
+ "protocols": ["chat", "responses"],
449
+ "tier": "full",
450
+ "severity": "critical",
451
+ "kind": "request",
452
+ "turn": {
453
+ "messages": [
454
+ {
455
+ "role": "user",
456
+ "content": "What is the weather in [[Tokyo]]? Use the tool with fahrenheit."
457
+ }
458
+ ],
459
+ "tools": ["get_weather"],
460
+ "tool_choice": {
461
+ "name": "get_weather"
462
+ },
463
+ "max_tokens": 256,
464
+ "stream": true,
465
+ "include_usage": true
466
+ },
467
+ "expect": {
468
+ "finish_reason": ["tool_calls", "stop"],
469
+ "stream": {
470
+ "min_events": 2
471
+ },
472
+ "tool_calls": {
473
+ "min": 1,
474
+ "max": 1,
475
+ "names": ["get_weather"],
476
+ "unique_ids": true,
477
+ "arguments_schema": "from_tools",
478
+ "arguments": {
479
+ "city": "Tokyo",
480
+ "unit": "fahrenheit"
481
+ }
482
+ }
483
+ }
484
+ },
485
+ {
486
+ "id": "tool_roundtrip",
487
+ "title": "Tool call, result and final answer",
488
+ "suites": ["core", "cursor"],
489
+ "protocols": ["chat", "responses"],
490
+ "tier": "full",
491
+ "severity": "critical",
492
+ "kind": "roundtrip",
493
+ "turn": {
494
+ "messages": [
495
+ {
496
+ "role": "user",
497
+ "content": "What is the weather in [[Berlin]]? Use the tool with celsius, then report the temperature."
498
+ }
499
+ ],
500
+ "tools": ["get_weather"],
501
+ "tool_choice": {
502
+ "name": "get_weather"
503
+ },
504
+ "max_tokens": 256
505
+ },
506
+ "expect": {
507
+ "tool_calls": {
508
+ "min": 1,
509
+ "max": 1,
510
+ "names": ["get_weather"],
511
+ "arguments_schema": "from_tools",
512
+ "arguments": {
513
+ "city": "Berlin"
514
+ }
515
+ }
516
+ },
517
+ "results": {
518
+ "get_weather": "Berlin is {marker} degrees celsius and clear."
519
+ },
520
+ "followup": {
521
+ "tool_choice": "none",
522
+ "max_tokens": 256,
523
+ "expect": {
524
+ "finish_reason": ["stop"],
525
+ "content": {
526
+ "contains": "{marker}"
527
+ }
528
+ }
529
+ }
530
+ },
531
+ {
532
+ "id": "parallel_tools",
533
+ "title": "Multiple tool calls in one turn, with results",
534
+ "suites": ["core", "cursor"],
535
+ "protocols": ["chat", "responses"],
536
+ "tier": "full",
537
+ "severity": "minor",
538
+ "kind": "roundtrip",
539
+ "turn": {
540
+ "messages": [
541
+ {
542
+ "role": "user",
543
+ "content": "Call get_weather once for each city: [[Paris]] and [[Tokyo]]. Use celsius. Make both calls in the same turn."
544
+ }
545
+ ],
546
+ "tools": ["get_weather"],
547
+ "tool_choice": "required",
548
+ "parallel_tool_calls": true,
549
+ "max_tokens": 400
550
+ },
551
+ "expect": {
552
+ "tool_calls": {
553
+ "min": 2,
554
+ "names": ["get_weather"],
555
+ "unique_ids": true,
556
+ "arguments_schema": "from_tools",
557
+ "values": {
558
+ "field": "city",
559
+ "include": ["Paris", "Tokyo"]
560
+ }
561
+ }
562
+ },
563
+ "results": {
564
+ "get_weather": "Reading for the requested city: {marker} degrees."
565
+ },
566
+ "followup": {
567
+ "tool_choice": "none",
568
+ "max_tokens": 400,
569
+ "expect": {
570
+ "finish_reason": ["stop"],
571
+ "content": {
572
+ "non_empty": true
573
+ }
574
+ }
575
+ }
576
+ },
577
+ {
578
+ "id": "json_schema",
579
+ "title": "Structured JSON output",
580
+ "suites": ["core"],
581
+ "protocols": ["chat", "responses"],
582
+ "tier": "full",
583
+ "severity": "minor",
584
+ "kind": "request",
585
+ "turn": {
586
+ "messages": [
587
+ {
588
+ "role": "user",
589
+ "content": "Return a JSON object with city set to Rome and population_millions set to a number."
590
+ }
591
+ ],
592
+ "json_schema": {
593
+ "name": "city_report",
594
+ "schema": {
595
+ "type": "object",
596
+ "properties": {
597
+ "city": {
598
+ "type": "string"
599
+ },
600
+ "population_millions": {
601
+ "type": "number",
602
+ "minimum": 0
603
+ }
604
+ },
605
+ "required": ["city", "population_millions"],
606
+ "additionalProperties": false
607
+ }
608
+ },
609
+ "max_tokens": 300
610
+ },
611
+ "expect": {
612
+ "finish_reason": ["stop"],
613
+ "json": {
614
+ "schema": {
615
+ "type": "object",
616
+ "properties": {
617
+ "city": {
618
+ "type": "string",
619
+ "const": "Rome"
620
+ },
621
+ "population_millions": {
622
+ "type": "number",
623
+ "minimum": 0
624
+ }
625
+ },
626
+ "required": ["city", "population_millions"]
627
+ }
628
+ }
629
+ }
630
+ },
631
+ {
632
+ "id": "finish_length",
633
+ "title": "Output limit reports finish_reason length",
634
+ "suites": ["core"],
635
+ "protocols": ["chat"],
636
+ "tier": "full",
637
+ "severity": "minor",
638
+ "kind": "request",
639
+ "turn": {
640
+ "messages": [
641
+ {
642
+ "role": "user",
643
+ "content": "Write the numbers from 1 to 200 separated by spaces."
644
+ }
645
+ ],
646
+ "max_tokens": 16
647
+ },
648
+ "expect": {
649
+ "finish_reason": ["length"]
650
+ }
651
+ },
652
+ {
653
+ "id": "agent_workflow",
654
+ "title": "Multi-step coding-agent workflow (read, edit, test, report)",
655
+ "suites": ["core", "cursor"],
656
+ "protocols": ["chat", "responses"],
657
+ "tier": "full",
658
+ "severity": "major",
659
+ "kind": "agent",
660
+ "system": "You are a coding agent. Use the provided tools. Make exactly one tool call per turn.",
661
+ "task": "The file calc.py has a bug: add(a, b) must return the sum. Read calc.py, fix it with write_file, run the tests in test_calc.py, then report the outcome in one sentence.",
662
+ "tools": ["read_file", "write_file", "run_tests"],
663
+ "files": {
664
+ "calc.py": "def add(a, b):\n return a - b\n",
665
+ "test_calc.py": "from calc import add\nassert add(1, 2) == 3\n"
666
+ },
667
+ "steps": [
668
+ {
669
+ "force": "read_file",
670
+ "arguments": {
671
+ "path": "calc.py"
672
+ }
673
+ },
674
+ {
675
+ "force": "write_file",
676
+ "arguments": {
677
+ "path": "calc.py",
678
+ "content": {
679
+ "regex": "a\\s*\\+\\s*b"
680
+ }
681
+ }
682
+ },
683
+ {
684
+ "force": "run_tests",
685
+ "arguments": {
686
+ "path": "test_calc.py"
687
+ }
688
+ }
689
+ ],
690
+ "handlers": {
691
+ "read_file": {
692
+ "op": "read"
693
+ },
694
+ "write_file": {
695
+ "op": "write"
696
+ },
697
+ "run_tests": {
698
+ "op": "run",
699
+ "path": "calc.py",
700
+ "needle": "+",
701
+ "pass": "1 passed",
702
+ "fail": "FAILED: add(1, 2) returned -1"
703
+ }
704
+ },
705
+ "final": {
706
+ "tool_choice": "none",
707
+ "max_tokens": 300,
708
+ "expect": {
709
+ "finish_reason": ["stop"],
710
+ "content": {
711
+ "non_empty": true
712
+ }
713
+ }
714
+ },
715
+ "max_tokens": 400
716
+ },
717
+ {
718
+ "id": "stream_long",
719
+ "title": "Long-running stream stays live and terminates",
720
+ "suites": ["core", "cursor"],
721
+ "protocols": ["chat", "responses"],
722
+ "tier": "full",
723
+ "severity": "minor",
724
+ "kind": "request",
725
+ "turn": {
726
+ "messages": [
727
+ {
728
+ "role": "user",
729
+ "content": "Count from 1 to 40, one number per line, with no other text."
730
+ }
731
+ ],
732
+ "max_tokens": 300,
733
+ "stream": true,
734
+ "include_usage": true
735
+ },
736
+ "expect": {
737
+ "finish_reason": ["stop"],
738
+ "content": {
739
+ "non_empty": true
740
+ },
741
+ "stream": {
742
+ "min_events": 10,
743
+ "max_gap_s": 30
744
+ }
745
+ }
746
+ },
747
+ {
748
+ "id": "cancel_recover",
749
+ "title": "Client cancels a stream, then the next request succeeds",
750
+ "suites": ["core", "cursor"],
751
+ "protocols": ["chat", "responses"],
752
+ "tier": "full",
753
+ "severity": "major",
754
+ "kind": "cancel_recover",
755
+ "turn": {
756
+ "messages": [
757
+ {
758
+ "role": "user",
759
+ "content": "Count from 1 to 200, one number per line, with no other text."
760
+ }
761
+ ],
762
+ "max_tokens": 200,
763
+ "stream": true
764
+ },
765
+ "cancel_after_frames": 3,
766
+ "recover": {
767
+ "messages": [
768
+ {
769
+ "role": "user",
770
+ "content": "Reply with the single word OK."
771
+ }
772
+ ],
773
+ "max_tokens": 256
774
+ },
775
+ "expect": {
776
+ "finish_reason": ["stop"],
777
+ "content": {
778
+ "non_empty": true
779
+ }
780
+ }
781
+ },
782
+ {
783
+ "id": "concurrency_isolation",
784
+ "title": "Concurrent requests do not cross-contaminate",
785
+ "suites": ["core"],
786
+ "protocols": ["chat", "responses"],
787
+ "tier": "full",
788
+ "severity": "major",
789
+ "kind": "concurrent",
790
+ "parallel": 3,
791
+ "turn": {
792
+ "messages": [
793
+ {
794
+ "role": "user",
795
+ "content": "Record the verification token [[{nonce}]] with the tool."
796
+ }
797
+ ],
798
+ "tools": ["echo_token"],
799
+ "tool_choice": {
800
+ "name": "echo_token"
801
+ },
802
+ "max_tokens": 128
803
+ },
804
+ "expect": {
805
+ "tool_calls": {
806
+ "min": 1,
807
+ "max": 1,
808
+ "names": ["echo_token"],
809
+ "arguments_schema": "from_tools",
810
+ "arguments": {
811
+ "token": "{nonce}"
812
+ }
813
+ }
814
+ }
815
+ },
816
+ {
817
+ "id": "errors_unknown_model",
818
+ "title": "Unknown model returns an OpenAI-style error",
819
+ "suites": ["core"],
820
+ "protocols": ["chat", "responses"],
821
+ "tier": "full",
822
+ "severity": "major",
823
+ "kind": "error_probe",
824
+ "model": "model-health-nonexistent",
825
+ "turn": {
826
+ "messages": [
827
+ {
828
+ "role": "user",
829
+ "content": "x"
830
+ }
831
+ ],
832
+ "max_tokens": 16
833
+ },
834
+ "expect": {
835
+ "status": [400, 404, 422],
836
+ "error_envelope": true
837
+ },
838
+ "spends_inference": false
839
+ },
840
+ {
841
+ "id": "errors_malformed",
842
+ "title": "Malformed JSON body returns an OpenAI-style 400",
843
+ "suites": ["core"],
844
+ "protocols": ["chat", "responses"],
845
+ "tier": "full",
846
+ "severity": "major",
847
+ "kind": "error_probe",
848
+ "raw_body": "{\"model\": ",
849
+ "expect": {
850
+ "status": [400, 422],
851
+ "error_envelope": true
852
+ },
853
+ "spends_inference": false
854
+ },
855
+ {
856
+ "id": "errors_missing_input",
857
+ "title": "Missing messages/input returns an OpenAI-style 400",
858
+ "suites": ["core"],
859
+ "protocols": ["chat", "responses"],
860
+ "tier": "full",
861
+ "severity": "minor",
862
+ "kind": "error_probe",
863
+ "body": {},
864
+ "expect": {
865
+ "status": [400, 422],
866
+ "error_envelope": true
867
+ },
868
+ "spends_inference": false
869
+ },
870
+ {
871
+ "id": "invalid_key",
872
+ "title": "Wrong API key is rejected",
873
+ "suites": ["core"],
874
+ "protocols": ["chat"],
875
+ "tier": "manual",
876
+ "severity": "major",
877
+ "kind": "error_probe",
878
+ "key": "invalid-model-health-probe-key-0000000000",
879
+ "turn": {
880
+ "messages": [
881
+ {
882
+ "role": "user",
883
+ "content": "x"
884
+ }
885
+ ],
886
+ "max_tokens": 16
887
+ },
888
+ "expect": {
889
+ "status": [401, 403]
890
+ },
891
+ "spends_inference": false
892
+ }
893
+ ],
894
+ "cursor": {
895
+ "note": "Protocol-level compatibility only. Requests follow the public OpenAI Chat Completions and Responses specifications, which Cursor documents for custom OpenAI-compatible endpoints. Cursor's private request shaping is not reproduced and the Cursor application is not driven by this suite.",
896
+ "application_verified": false,
897
+ "documentation": [
898
+ "https://platform.openai.com/docs/api-reference/chat",
899
+ "https://platform.openai.com/docs/api-reference/responses"
900
+ ]
901
+ },
902
+ "diagnostics": {
903
+ "credentials": "No client API key was available. Set CLIPROXY_API_KEY, provide an existing VAULT_TOKEN, or run on the host with an installed client key. Nothing was tested.",
904
+ "auth": "The client key was rejected. Confirm the fleet's proxy-client-key secret matches access.api-keys in the live config; never paste the key into logs.",
905
+ "not_found": "The model id is not served. Check `bun run cliproxy models` on the host and the provider account behind it.",
906
+ "rate_limit": "The provider account is rate limited or out of quota (HTTP 429 model_cooldown means every account for that model is cooling down). Inspect account usage in the dashboard; do not retry in a loop. Add an account or wait for the limit to reset.",
907
+ "timeout": "No response within the deadline. Check the host (`bun run cliproxy status`), the Cloudflare tunnel and provider status.",
908
+ "transport": "The connection failed (DNS, TLS, tunnel or host down). Run the liveness profile first.",
909
+ "upstream": "The proxy returned a 5xx or reported a provider failure. Check `bun run cliproxy logs` for the model's channel and the provider's status page.",
910
+ "protocol": "The response violates the wire contract. Compare the failing check's detail with the protocol validators; this can follow a CLIProxyAPI upgrade.",
911
+ "assertion": "The response was well-formed but did not satisfy the expected invariant. Re-run the single check manually; if it is stable, the model behavior changed.",
912
+ "request_rejected": "The proxy or provider rejected a valid request (HTTP 400/422). If the capability is genuinely unsupported, declare it optional or unsupported in models.json.",
913
+ "budget": "The request or token budget was exhausted before this check ran.",
914
+ "aborted": "Remaining checks were skipped after repeated consecutive failures to bound spend.",
915
+ "harness": "The test harness itself raised an error; this is a bug in the checks, not the endpoint.",
916
+ "model_missing": "The model id is not in /v1/models, so no inference was attempted for it. Compare `bun run cliproxy models` with models.json; the provider account may be unauthenticated or the model retired."
917
+ },
918
+ "simulations": [
919
+ {
920
+ "id": "failover_after_quota",
921
+ "capability": "routing.failover",
922
+ "title": "First account's quota is exhausted; the request fails over to the second",
923
+ "accounts": [
924
+ {
925
+ "id": "acct-a",
926
+ "quota": 0
927
+ },
928
+ {
929
+ "id": "acct-b"
930
+ }
931
+ ],
932
+ "steps": [
933
+ {
934
+ "request": {
935
+ "protocol": "chat",
936
+ "turn": {
937
+ "messages": [
938
+ {
939
+ "role": "user",
940
+ "content": "Reply with the single word OK."
941
+ }
942
+ ],
943
+ "max_tokens": 64
944
+ }
945
+ },
946
+ "count": 1,
947
+ "expect": {
948
+ "status": 200,
949
+ "served_by_all": ["acct-b"]
950
+ }
951
+ },
952
+ {
953
+ "request": {
954
+ "protocol": "chat",
955
+ "turn": {
956
+ "messages": [
957
+ {
958
+ "role": "user",
959
+ "content": "Reply with the single word OK."
960
+ }
961
+ ],
962
+ "max_tokens": 64
963
+ }
964
+ },
965
+ "count": 3,
966
+ "expect": {
967
+ "status": 200,
968
+ "served_by_all": ["acct-b"]
969
+ }
970
+ }
971
+ ]
972
+ },
973
+ {
974
+ "id": "fill_first_drains_one_account_before_the_next",
975
+ "capability": "routing.fill_first",
976
+ "title": "Three accounts with limited quota are used strictly in order: all of the first, then the second, then the third",
977
+ "accounts": [
978
+ {
979
+ "id": "acct-a",
980
+ "quota": 3
981
+ },
982
+ {
983
+ "id": "acct-b",
984
+ "quota": 3
985
+ },
986
+ {
987
+ "id": "acct-c"
988
+ }
989
+ ],
990
+ "steps": [
991
+ {
992
+ "request": {
993
+ "protocol": "chat"
994
+ },
995
+ "count": 8,
996
+ "expect": {
997
+ "status": 200,
998
+ "served_sequence": [
999
+ "acct-a",
1000
+ "acct-a",
1001
+ "acct-a",
1002
+ "acct-b",
1003
+ "acct-b",
1004
+ "acct-b",
1005
+ "acct-c",
1006
+ "acct-c"
1007
+ ]
1008
+ }
1009
+ }
1010
+ ]
1011
+ },
1012
+ {
1013
+ "id": "fill_first_does_not_spread_load",
1014
+ "capability": "routing.fill_first",
1015
+ "title": "With capacity available, every new session goes to the first account and the others stay idle",
1016
+ "accounts": [
1017
+ {
1018
+ "id": "acct-a"
1019
+ },
1020
+ {
1021
+ "id": "acct-b"
1022
+ },
1023
+ {
1024
+ "id": "acct-c"
1025
+ }
1026
+ ],
1027
+ "steps": [
1028
+ {
1029
+ "request": {
1030
+ "protocol": "chat"
1031
+ },
1032
+ "count": 6,
1033
+ "expect": {
1034
+ "status": 200,
1035
+ "served_by_all": ["acct-a"]
1036
+ }
1037
+ }
1038
+ ]
1039
+ },
1040
+ {
1041
+ "id": "fill_first_returns_to_the_first_account_after_it_recovers",
1042
+ "capability": "routing.fill_first",
1043
+ "title": "After the first account's quota returns and its cooldown ends, new sessions fill it again before the second",
1044
+ "accounts": [
1045
+ {
1046
+ "id": "acct-a",
1047
+ "quota": 1
1048
+ },
1049
+ {
1050
+ "id": "acct-b"
1051
+ }
1052
+ ],
1053
+ "steps": [
1054
+ {
1055
+ "request": {
1056
+ "protocol": "chat"
1057
+ },
1058
+ "count": 2,
1059
+ "expect": {
1060
+ "status": 200,
1061
+ "served_sequence": ["acct-a", "acct-b"]
1062
+ }
1063
+ },
1064
+ {
1065
+ "action": "account",
1066
+ "id": "acct-a",
1067
+ "quota": null
1068
+ },
1069
+ {
1070
+ "action": "advance",
1071
+ "seconds": 31
1072
+ },
1073
+ {
1074
+ "request": {
1075
+ "protocol": "chat"
1076
+ },
1077
+ "count": 3,
1078
+ "expect": {
1079
+ "status": 200,
1080
+ "served_by_all": ["acct-a"]
1081
+ }
1082
+ }
1083
+ ]
1084
+ },
1085
+ {
1086
+ "id": "transient_failure_cools_the_account_then_it_rejoins",
1087
+ "capability": "routing.recovery",
1088
+ "title": "A transient upstream error sends the request to the next account, keeps the failed one cooling for 60s, then it serves again",
1089
+ "accounts": [
1090
+ {
1091
+ "id": "acct-a",
1092
+ "fail_next": 1
1093
+ },
1094
+ {
1095
+ "id": "acct-b"
1096
+ }
1097
+ ],
1098
+ "steps": [
1099
+ {
1100
+ "request": {
1101
+ "protocol": "chat"
1102
+ },
1103
+ "count": 2,
1104
+ "expect": {
1105
+ "status": 200,
1106
+ "served_by_all": ["acct-b"]
1107
+ }
1108
+ },
1109
+ {
1110
+ "action": "advance",
1111
+ "seconds": 61
1112
+ },
1113
+ {
1114
+ "request": {
1115
+ "protocol": "chat"
1116
+ },
1117
+ "count": 1,
1118
+ "expect": {
1119
+ "status": 200,
1120
+ "served_by_all": ["acct-a"]
1121
+ }
1122
+ }
1123
+ ]
1124
+ },
1125
+ {
1126
+ "id": "session_binding_survives_recovery_and_rebinds_on_failover",
1127
+ "capability": "routing.affinity",
1128
+ "title": "A session stays on its account, moves when that account is exhausted, and keeps the new binding after the first account recovers",
1129
+ "accounts": [
1130
+ {
1131
+ "id": "acct-a",
1132
+ "quota": 2
1133
+ },
1134
+ {
1135
+ "id": "acct-b"
1136
+ }
1137
+ ],
1138
+ "steps": [
1139
+ {
1140
+ "request": {
1141
+ "protocol": "chat",
1142
+ "session": "same"
1143
+ },
1144
+ "count": 2,
1145
+ "expect": {
1146
+ "status": 200,
1147
+ "served_by_all": ["acct-a"]
1148
+ }
1149
+ },
1150
+ {
1151
+ "request": {
1152
+ "protocol": "chat",
1153
+ "session": "same"
1154
+ },
1155
+ "count": 1,
1156
+ "expect": {
1157
+ "status": 200,
1158
+ "served_by_all": ["acct-b"]
1159
+ }
1160
+ },
1161
+ {
1162
+ "action": "account",
1163
+ "id": "acct-a",
1164
+ "quota": null
1165
+ },
1166
+ {
1167
+ "action": "advance",
1168
+ "seconds": 31
1169
+ },
1170
+ {
1171
+ "request": {
1172
+ "protocol": "chat",
1173
+ "session": "same"
1174
+ },
1175
+ "count": 2,
1176
+ "expect": {
1177
+ "status": 200,
1178
+ "served_by_all": ["acct-b"]
1179
+ }
1180
+ },
1181
+ {
1182
+ "request": {
1183
+ "protocol": "chat"
1184
+ },
1185
+ "count": 1,
1186
+ "expect": {
1187
+ "status": 200,
1188
+ "served_by_all": ["acct-a"]
1189
+ }
1190
+ }
1191
+ ]
1192
+ },
1193
+ {
1194
+ "id": "recovery_after_quota_cooldown",
1195
+ "capability": "routing.recovery",
1196
+ "title": "A cooled-down account rejoins routing once its cooldown elapses and is filled first again",
1197
+ "accounts": [
1198
+ {
1199
+ "id": "acct-a",
1200
+ "quota": 0
1201
+ },
1202
+ {
1203
+ "id": "acct-b"
1204
+ }
1205
+ ],
1206
+ "steps": [
1207
+ {
1208
+ "request": {
1209
+ "protocol": "chat",
1210
+ "turn": {
1211
+ "messages": [
1212
+ {
1213
+ "role": "user",
1214
+ "content": "Reply with the single word OK."
1215
+ }
1216
+ ],
1217
+ "max_tokens": 64
1218
+ }
1219
+ },
1220
+ "count": 1,
1221
+ "expect": {
1222
+ "status": 200,
1223
+ "served_by_all": ["acct-b"]
1224
+ }
1225
+ },
1226
+ {
1227
+ "action": "account",
1228
+ "id": "acct-a",
1229
+ "quota": null
1230
+ },
1231
+ {
1232
+ "action": "advance",
1233
+ "seconds": 31
1234
+ },
1235
+ {
1236
+ "request": {
1237
+ "protocol": "chat"
1238
+ },
1239
+ "count": 2,
1240
+ "expect": {
1241
+ "status": 200,
1242
+ "served_by_all": ["acct-a"]
1243
+ }
1244
+ }
1245
+ ]
1246
+ },
1247
+ {
1248
+ "id": "model_consistent_across_accounts",
1249
+ "capability": "routing.model_consistency",
1250
+ "title": "Both accounts return the same model id and valid shapes on both protocols",
1251
+ "accounts": [
1252
+ {
1253
+ "id": "acct-a",
1254
+ "quota": 2
1255
+ },
1256
+ {
1257
+ "id": "acct-b"
1258
+ }
1259
+ ],
1260
+ "steps": [
1261
+ {
1262
+ "request": {
1263
+ "protocol": "chat"
1264
+ },
1265
+ "count": 4,
1266
+ "expect": {
1267
+ "status": 200,
1268
+ "served_sequence": ["acct-a", "acct-a", "acct-b", "acct-b"],
1269
+ "single_model": true
1270
+ }
1271
+ },
1272
+ {
1273
+ "action": "account",
1274
+ "id": "acct-a",
1275
+ "quota": 2
1276
+ },
1277
+ {
1278
+ "action": "advance",
1279
+ "seconds": 31
1280
+ },
1281
+ {
1282
+ "request": {
1283
+ "protocol": "responses"
1284
+ },
1285
+ "count": 4,
1286
+ "expect": {
1287
+ "status": 200,
1288
+ "served_sequence": ["acct-a", "acct-a", "acct-b", "acct-b"],
1289
+ "single_model": true
1290
+ }
1291
+ }
1292
+ ]
1293
+ },
1294
+ {
1295
+ "id": "all_accounts_exhausted",
1296
+ "capability": "routing.all_unavailable",
1297
+ "title": "Every account is out of quota: a prompt 429 error envelope with Retry-After",
1298
+ "accounts": [
1299
+ {
1300
+ "id": "acct-a",
1301
+ "quota": 0
1302
+ },
1303
+ {
1304
+ "id": "acct-b",
1305
+ "quota": 0
1306
+ }
1307
+ ],
1308
+ "steps": [
1309
+ {
1310
+ "request": {
1311
+ "protocol": "chat",
1312
+ "turn": {
1313
+ "messages": [
1314
+ {
1315
+ "role": "user",
1316
+ "content": "Reply with the single word OK."
1317
+ }
1318
+ ],
1319
+ "max_tokens": 64
1320
+ }
1321
+ },
1322
+ "count": 1,
1323
+ "expect": {
1324
+ "status": 429,
1325
+ "category": "rate_limit",
1326
+ "retry_after": true,
1327
+ "envelope": true,
1328
+ "max_elapsed_s": 3,
1329
+ "no_retry": true
1330
+ }
1331
+ },
1332
+ {
1333
+ "request": {
1334
+ "protocol": "chat",
1335
+ "turn": {
1336
+ "messages": [
1337
+ {
1338
+ "role": "user",
1339
+ "content": "Reply with the single word OK."
1340
+ }
1341
+ ],
1342
+ "max_tokens": 64
1343
+ }
1344
+ },
1345
+ "count": 1,
1346
+ "expect": {
1347
+ "status": 429,
1348
+ "category": "rate_limit",
1349
+ "envelope": true,
1350
+ "max_elapsed_s": 3
1351
+ }
1352
+ }
1353
+ ]
1354
+ },
1355
+ {
1356
+ "id": "all_accounts_failing",
1357
+ "capability": "routing.all_unavailable",
1358
+ "title": "Every account is failing upstream: a prompt 5xx error envelope",
1359
+ "accounts": [
1360
+ {
1361
+ "id": "acct-a",
1362
+ "fail_next": 9
1363
+ },
1364
+ {
1365
+ "id": "acct-b",
1366
+ "fail_next": 9
1367
+ }
1368
+ ],
1369
+ "steps": [
1370
+ {
1371
+ "request": {
1372
+ "protocol": "chat",
1373
+ "turn": {
1374
+ "messages": [
1375
+ {
1376
+ "role": "user",
1377
+ "content": "Reply with the single word OK."
1378
+ }
1379
+ ],
1380
+ "max_tokens": 64
1381
+ }
1382
+ },
1383
+ "count": 1,
1384
+ "expect": {
1385
+ "status": 503,
1386
+ "category": "upstream",
1387
+ "envelope": true,
1388
+ "max_elapsed_s": 3
1389
+ }
1390
+ }
1391
+ ]
1392
+ },
1393
+ {
1394
+ "id": "rate_limit_is_classified_not_retried",
1395
+ "capability": "limits.rate_limit",
1396
+ "title": "A 429 with Retry-After is classified, never retried, and the next request succeeds",
1397
+ "accounts": [
1398
+ {
1399
+ "id": "acct-a"
1400
+ }
1401
+ ],
1402
+ "steps": [
1403
+ {
1404
+ "action": "fault",
1405
+ "faults": {
1406
+ "status": 429,
1407
+ "status_remaining": 1,
1408
+ "retry_after": 7
1409
+ }
1410
+ },
1411
+ {
1412
+ "request": {
1413
+ "protocol": "chat",
1414
+ "turn": {
1415
+ "messages": [
1416
+ {
1417
+ "role": "user",
1418
+ "content": "Reply with the single word OK."
1419
+ }
1420
+ ],
1421
+ "max_tokens": 64
1422
+ }
1423
+ },
1424
+ "count": 1,
1425
+ "expect": {
1426
+ "status": 429,
1427
+ "category": "rate_limit",
1428
+ "retry_after": true,
1429
+ "envelope": true,
1430
+ "no_retry": true
1431
+ }
1432
+ },
1433
+ {
1434
+ "request": {
1435
+ "protocol": "chat",
1436
+ "turn": {
1437
+ "messages": [
1438
+ {
1439
+ "role": "user",
1440
+ "content": "Reply with the single word OK."
1441
+ }
1442
+ ],
1443
+ "max_tokens": 64
1444
+ }
1445
+ },
1446
+ "count": 1,
1447
+ "expect": {
1448
+ "status": 200
1449
+ }
1450
+ }
1451
+ ]
1452
+ },
1453
+ {
1454
+ "id": "stalled_upstream_times_out",
1455
+ "capability": "limits.timeout",
1456
+ "title": "A stalled response is cut off at the harness deadline",
1457
+ "accounts": [
1458
+ {
1459
+ "id": "acct-a"
1460
+ }
1461
+ ],
1462
+ "timeouts": {
1463
+ "request_s": 0.4,
1464
+ "stream_idle_s": 0.4,
1465
+ "stream_deadline_s": 1.0
1466
+ },
1467
+ "steps": [
1468
+ {
1469
+ "action": "fault",
1470
+ "faults": {
1471
+ "stall_s": 1.5
1472
+ }
1473
+ },
1474
+ {
1475
+ "request": {
1476
+ "protocol": "chat",
1477
+ "turn": {
1478
+ "messages": [
1479
+ {
1480
+ "role": "user",
1481
+ "content": "Reply with the single word OK."
1482
+ }
1483
+ ],
1484
+ "max_tokens": 64
1485
+ }
1486
+ },
1487
+ "count": 1,
1488
+ "expect": {
1489
+ "category": "timeout",
1490
+ "max_elapsed_s": 1.2,
1491
+ "no_retry": true
1492
+ }
1493
+ }
1494
+ ]
1495
+ },
1496
+ {
1497
+ "id": "upstream_5xx_is_classified_not_retried",
1498
+ "capability": "limits.upstream_failure",
1499
+ "title": "A 502 is classified as an upstream failure and not retried",
1500
+ "accounts": [
1501
+ {
1502
+ "id": "acct-a"
1503
+ }
1504
+ ],
1505
+ "steps": [
1506
+ {
1507
+ "action": "fault",
1508
+ "faults": {
1509
+ "status": 502,
1510
+ "status_remaining": 1
1511
+ }
1512
+ },
1513
+ {
1514
+ "request": {
1515
+ "protocol": "chat",
1516
+ "turn": {
1517
+ "messages": [
1518
+ {
1519
+ "role": "user",
1520
+ "content": "Reply with the single word OK."
1521
+ }
1522
+ ],
1523
+ "max_tokens": 64
1524
+ }
1525
+ },
1526
+ "count": 1,
1527
+ "expect": {
1528
+ "status": 502,
1529
+ "category": "upstream",
1530
+ "envelope": true,
1531
+ "no_retry": true
1532
+ }
1533
+ }
1534
+ ]
1535
+ },
1536
+ {
1537
+ "id": "truncated_chat_stream_is_detected",
1538
+ "capability": "limits.upstream_failure",
1539
+ "title": "A chat stream cut off mid-response is reported as a protocol failure, not a success",
1540
+ "accounts": [
1541
+ {
1542
+ "id": "acct-a"
1543
+ }
1544
+ ],
1545
+ "steps": [
1546
+ {
1547
+ "action": "fault",
1548
+ "faults": {
1549
+ "truncate_after": 2
1550
+ }
1551
+ },
1552
+ {
1553
+ "request": {
1554
+ "protocol": "chat",
1555
+ "turn": {
1556
+ "messages": [
1557
+ {
1558
+ "role": "user",
1559
+ "content": "Reply with the single word OK."
1560
+ }
1561
+ ],
1562
+ "max_tokens": 64,
1563
+ "stream": true
1564
+ }
1565
+ },
1566
+ "count": 1,
1567
+ "expect": {
1568
+ "violation": "[DONE]",
1569
+ "no_retry": true
1570
+ }
1571
+ }
1572
+ ]
1573
+ },
1574
+ {
1575
+ "id": "truncated_responses_stream_is_detected",
1576
+ "capability": "limits.upstream_failure",
1577
+ "title": "A Responses stream with no terminal event is reported as a protocol failure",
1578
+ "accounts": [
1579
+ {
1580
+ "id": "acct-a"
1581
+ }
1582
+ ],
1583
+ "steps": [
1584
+ {
1585
+ "action": "fault",
1586
+ "faults": {
1587
+ "truncate_after": 3
1588
+ }
1589
+ },
1590
+ {
1591
+ "request": {
1592
+ "protocol": "responses",
1593
+ "turn": {
1594
+ "messages": [
1595
+ {
1596
+ "role": "user",
1597
+ "content": "Reply with the single word OK."
1598
+ }
1599
+ ],
1600
+ "max_tokens": 64,
1601
+ "stream": true
1602
+ }
1603
+ },
1604
+ "count": 1,
1605
+ "expect": {
1606
+ "violation": "terminal response event",
1607
+ "no_retry": true
1608
+ }
1609
+ }
1610
+ ]
1611
+ }
1612
+ ],
1613
+ "routing": {
1614
+ "strategy": "fill-first",
1615
+ "session_affinity": true,
1616
+ "note": "Mirrors routing in config.template.yaml (a test enforces this). Fill-first drains one account before using the next; the live proxy config is applied separately and is not observable from outside."
1617
+ }
1618
+ }