synth-optimizers 0.2.6.dev20260622__tar.gz → 0.2.6.dev20260626__tar.gz

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 (107) hide show
  1. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/PKG-INFO +1 -1
  2. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/pyproject.toml +1 -1
  3. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/30-hosted.md +25 -6
  4. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/gelo.py +209 -26
  5. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/gepa.py +51 -1
  6. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/hosted.py +82 -2
  7. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/tunnels.py +5 -10
  8. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/Cargo.lock +0 -0
  9. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/Cargo.toml +0 -0
  10. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/README.md +0 -0
  11. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/Cargo.toml +0 -0
  12. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/README.md +0 -0
  13. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/openapi/gepa-service-v1.yaml +0 -0
  14. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/codex_app_server.rs +0 -0
  15. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/lib.rs +0 -0
  16. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/machines.rs +0 -0
  17. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/pipeline.rs +0 -0
  18. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/planner.rs +0 -0
  19. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/prompting_best_practices.md +0 -0
  20. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/runtime.rs +0 -0
  21. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_gepa/src/service.rs +0 -0
  22. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/Cargo.toml +0 -0
  23. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/README.md +0 -0
  24. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/README.md +0 -0
  25. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/app_server.rs +0 -0
  26. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/codex_home.rs +0 -0
  27. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/daytona.rs +0 -0
  28. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/docker.rs +0 -0
  29. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/jsonrpc_read_window.rs +0 -0
  30. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/limits.rs +0 -0
  31. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/local.rs +0 -0
  32. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/mod.rs +0 -0
  33. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/role_agent.rs +0 -0
  34. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/session.rs +0 -0
  35. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/substrate.rs +0 -0
  36. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/supervisor.rs +0 -0
  37. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/agent_runtime/usage.rs +0 -0
  38. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/artifact_store.rs +0 -0
  39. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/artifacts.rs +0 -0
  40. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/cache.rs +0 -0
  41. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/candidates.rs +0 -0
  42. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/checkpoints.rs +0 -0
  43. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/config.rs +0 -0
  44. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/configured_limits.rs +0 -0
  45. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/container_contract.rs +0 -0
  46. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/data_models.rs +0 -0
  47. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/disk_budget.rs +0 -0
  48. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/error.rs +0 -0
  49. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/event_visualization.rs +0 -0
  50. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/events.rs +0 -0
  51. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/evidence.rs +0 -0
  52. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/failures.rs +0 -0
  53. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/fsm.rs +0 -0
  54. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/http.rs +0 -0
  55. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/invariants.rs +0 -0
  56. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/jobs.rs +0 -0
  57. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/levers.rs +0 -0
  58. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/lib.rs +0 -0
  59. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/limit_engine.rs +0 -0
  60. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/limits.rs +0 -0
  61. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/observability.rs +0 -0
  62. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/operations.rs +0 -0
  63. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/process.rs +0 -0
  64. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/projections.rs +0 -0
  65. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/prompt_program.rs +0 -0
  66. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/registry.rs +0 -0
  67. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/resources.rs +0 -0
  68. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/rollouts.rs +0 -0
  69. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/runtime_records.rs +0 -0
  70. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/scores.rs +0 -0
  71. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/sensors.rs +0 -0
  72. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/state_machine.rs +0 -0
  73. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/stopper.rs +0 -0
  74. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/storage_maintenance.rs +0 -0
  75. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/usage.rs +0 -0
  76. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizer_platform/src/workspace.rs +0 -0
  77. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizers_py/Cargo.toml +0 -0
  78. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizers_py/README.md +0 -0
  79. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/rust/crates/synth_optimizers_py/src/lib.rs +0 -0
  80. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/README.md +0 -0
  81. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/__init__.py +0 -0
  82. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/_synth_optimizers.pyi +0 -0
  83. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/board_server.py +0 -0
  84. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/cli.py +0 -0
  85. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/discovery.py +0 -0
  86. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/00-index.md +0 -0
  87. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/10-cli.md +0 -0
  88. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/20-sdk.md +0 -0
  89. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/containers/00-overview.md +0 -0
  90. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gelo/containers/20-contract.md +0 -0
  91. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/00-index.md +0 -0
  92. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/10-cli.md +0 -0
  93. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/20-sdk.md +0 -0
  94. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/algorithms/00-overview.md +0 -0
  95. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/algorithms/10-pipeline-modes.md +0 -0
  96. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/algorithms/20-proposer-and-policies.md +0 -0
  97. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/assets/synth-logo.png +0 -0
  98. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/containers/00-overview.md +0 -0
  99. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/containers/10-catalog.md +0 -0
  100. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs/gepa/containers/20-contract.md +0 -0
  101. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/docs_server.py +0 -0
  102. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/eval_stats.py +0 -0
  103. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/hosted_config.py +0 -0
  104. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/o11y.py +0 -0
  105. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/observability.py +0 -0
  106. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/py.typed +0 -0
  107. {synth_optimizers-0.2.6.dev20260622 → synth_optimizers-0.2.6.dev20260626}/src/synth_optimizers/sdk.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: synth-optimizers
3
- Version: 0.2.6.dev20260622
3
+ Version: 0.2.6.dev20260626
4
4
  Requires-Dist: pydantic>=2.0.0
5
5
  Requires-Dist: synth-containers==0.2.0.20260604
6
6
  Requires-Dist: websocket-client>=1.8.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "synth-optimizers"
3
- version = "0.2.6.dev20260622"
3
+ version = "0.2.6.dev20260626"
4
4
  description = "Public Synth optimizer tooling with a Rust GEPA core."
5
5
  authors = [{name = "Synth Laboratories", email = "josh@usesynth.ai"}]
6
6
  readme = "README.md"
@@ -17,13 +17,13 @@ Durable status, events, and artifacts land in backend PG/Redis/S3 (see storage s
17
17
  [container contract](#/containers/contract)). **Env checkpoint blobs for resume stay in your
18
18
  container** — hosted storage does not replace them.
19
19
 
20
- ## Launch promo
20
+ ## 72-hour launch promo
21
21
 
22
- The GELO launch promo gives the first 20 organizations a `$500` hosted Go-Ex proposer
23
- spend grant, valid for 14 days after claim. Hosted `go-ex` submits auto-claim the grant
24
- when slots remain, attach the GELO launch promo grant, and preflight at least
25
- `$1` of `optimizer_go_ex_llm_spend` headroom before the run is queued. In-container policy
26
- LLM calls are still owned by the task container.
22
+ The June 26 GELO launch promo gives eligible organizations a 72-hour hosted Go-Ex
23
+ proposer-spend grant after claim. Hosted `go-ex` submits auto-claim the grant when
24
+ promo capacity remains, attach the GELO launch promo grant, and preflight at least
25
+ `$1` of `optimizer_go_ex_llm_spend` headroom before the run is queued. In-container
26
+ policy LLM calls are still owned by the task container.
27
27
 
28
28
  The promo grant is reused for repeat submits from the same organization. Each organization
29
29
  may have one hosted GELO run in `queued` or `running` status at once while using the promo.
@@ -35,6 +35,9 @@ Promo submits require GPT models for the five paid proposer roles:
35
35
  `aux_consolidate_proposer`, and `aux_consolidate_hill_climb_proposer`.
36
36
  `theme_verifier_agent` and `terminator_agent` are intentionally exempt.
37
37
 
38
+ The active campaign id is `gelo_free_72h_20260626`. Check the status route for the
39
+ exact grant expiry, remaining promo capacity, headroom snapshot, and first GELO run id.
40
+
38
41
  ## Public API routes
39
42
 
40
43
  | Route | GELO use |
@@ -69,6 +72,22 @@ Supported providers are `synth_tunnel`, `cloudflared`, and `ngrok`. Only SynthTu
69
72
 
70
73
  Container must be up and pass `GET /health` before submit.
71
74
 
75
+ ## GameBench Rust presets
76
+
77
+ GameBench Rust task containers can be submitted through the same tunnel path:
78
+
79
+ ```bash
80
+ synth-optimizers gelo submit \
81
+ --preset craftax_gamebench_rust_smoke \
82
+ --tunnel-url http://127.0.0.1:8098 \
83
+ --tunnel-provider synth_tunnel \
84
+ --follow
85
+ ```
86
+
87
+ Use `rogue_gamebench_rust_smoke` for the Rogue prototype container. These presets
88
+ materialize explicit `gamebench/<task>/rust` substrate metadata so blog evidence can
89
+ separate GameBench Rust replications from legacy Craftax, NLE, or Crafter rows.
90
+
72
91
  ## Staging vs prod claims
73
92
 
74
93
  | Claim | Minimum gate |
@@ -50,6 +50,8 @@ class GeloPresetName(StrEnum):
50
50
  CRAFTER = "crafter"
51
51
  CRAFTER_SMOKE = "crafter_smoke"
52
52
  SOKOBAN_SMOKE = "sokoban_smoke"
53
+ CRAFTAX_GAMEBENCH_RUST_SMOKE = "craftax_gamebench_rust_smoke"
54
+ ROGUE_GAMEBENCH_RUST_SMOKE = "rogue_gamebench_rust_smoke"
53
55
  NETHACK_SMOKE = "nethack_smoke"
54
56
  DUNGEONGRID_PLUS_PICO = "dungeongrid_plus_pico"
55
57
 
@@ -284,9 +286,7 @@ _AGENT_ROLES: tuple[GeloProposerRole, ...] = (
284
286
 
285
287
  _ALL_ROLES: tuple[GeloProposerRole, ...] = _PROPOSER_ROLES + _AGENT_ROLES
286
288
 
287
- _GO_EX_FLAT_KEYS: frozenset[str] = frozenset(
288
- field.name for field in fields(GeloEngineSection)
289
- )
289
+ _GO_EX_FLAT_KEYS: frozenset[str] = frozenset(field.name for field in fields(GeloEngineSection))
290
290
 
291
291
  _TASKSET_KEYS: frozenset[str] = frozenset(
292
292
  {
@@ -373,6 +373,19 @@ class GeloPreset:
373
373
  "policy_provider": "groq",
374
374
  "policy_api_key_env": "GROQ_API_KEY",
375
375
  }
376
+ elif preset_name in {
377
+ GeloPresetName.CRAFTAX_GAMEBENCH_RUST_SMOKE,
378
+ GeloPresetName.ROGUE_GAMEBENCH_RUST_SMOKE,
379
+ }:
380
+ defaults = {
381
+ "proposer_rounds": 1,
382
+ "train_seed_count": 4,
383
+ "heldout_seed_count": 2,
384
+ "max_rollouts": 64,
385
+ "policy_model": "openai/gpt-oss-120b",
386
+ "policy_provider": "groq",
387
+ "policy_api_key_env": "GROQ_API_KEY",
388
+ }
376
389
  else:
377
390
  raise GeloMaterializeError(
378
391
  f"preset {preset_name.value!r} is not available in the public package yet"
@@ -392,6 +405,14 @@ class GeloPreset:
392
405
  def sokoban_smoke(cls, **overrides: Any) -> "GeloPreset":
393
406
  return cls.from_name(GeloPresetName.SOKOBAN_SMOKE, **overrides)
394
407
 
408
+ @classmethod
409
+ def craftax_gamebench_rust_smoke(cls, **overrides: Any) -> "GeloPreset":
410
+ return cls.from_name(GeloPresetName.CRAFTAX_GAMEBENCH_RUST_SMOKE, **overrides)
411
+
412
+ @classmethod
413
+ def rogue_gamebench_rust_smoke(cls, **overrides: Any) -> "GeloPreset":
414
+ return cls.from_name(GeloPresetName.ROGUE_GAMEBENCH_RUST_SMOKE, **overrides)
415
+
395
416
  def to_config(
396
417
  self,
397
418
  *,
@@ -418,6 +439,28 @@ class GeloPreset:
418
439
  run = GeloRunSection(run_id=run_id or _default_run_id(self.name))
419
440
  if self.name == GeloPresetName.SOKOBAN_SMOKE:
420
441
  config = _sokoban_config(self, run=run, container=container)
442
+ elif self.name == GeloPresetName.CRAFTAX_GAMEBENCH_RUST_SMOKE:
443
+ config = _gamebench_rust_config(
444
+ self,
445
+ run=run,
446
+ container=container,
447
+ task_family="craftax-singleplayer",
448
+ profile="craftax_singleplayer_rust",
449
+ target_objective_label="midgame_crafting_progress",
450
+ max_steps=300,
451
+ seed_prompt=_CRAFTAX_GAMEBENCH_SEED_PROMPT,
452
+ )
453
+ elif self.name == GeloPresetName.ROGUE_GAMEBENCH_RUST_SMOKE:
454
+ config = _gamebench_rust_config(
455
+ self,
456
+ run=run,
457
+ container=container,
458
+ task_family="rogue-singleplayer",
459
+ profile="rogue_singleplayer_rust",
460
+ target_objective_label="survive_and_explore",
461
+ max_steps=200,
462
+ seed_prompt=_ROGUE_GAMEBENCH_SEED_PROMPT,
463
+ )
421
464
  else:
422
465
  config = GeloHostedConfig(
423
466
  run=run,
@@ -522,9 +565,7 @@ class GeloMaterializer:
522
565
  if container_tunnel is not None:
523
566
  if not hasattr(container_tunnel, "container_config"):
524
567
  raise GeloMaterializeError("container_tunnel must expose container_config()")
525
- container = _clean_config_value(
526
- _container_section_from_tunnel(container_tunnel)
527
- )
568
+ container = _clean_config_value(_container_section_from_tunnel(container_tunnel))
528
569
  config["container"] = _deep_merge(_mapping(config.get("container")), container)
529
570
  if container_url is not None:
530
571
  container = dict(_mapping(config.get("container")))
@@ -633,9 +674,26 @@ _SOKOBAN_SEED_PROMPT = (
633
674
  "move places the box on a target."
634
675
  )
635
676
 
677
+ _CRAFTAX_GAMEBENCH_SEED_PROMPT = (
678
+ "You are playing the GameBench Craftax single-player Rust task. Use valid "
679
+ "container actions only. Prioritize survival, collect wood and stone, craft "
680
+ "basic tools, recover coal and iron, and make measurable achievement progress."
681
+ )
682
+
683
+ _ROGUE_GAMEBENCH_SEED_PROMPT = (
684
+ "You are playing the GameBench Rogue single-player Rust task. Use valid "
685
+ "container actions only. Prioritize survival, map coverage, safe combat, "
686
+ "inventory discipline, and progress toward deeper exploration."
687
+ )
688
+
636
689
  # Mirrors goex_sokoban_gpt_oss_120b_budget.json taskset.context.milestone_ladder.
637
690
  _SOKOBAN_MILESTONE_LADDER: tuple[Mapping[str, Any], ...] = (
638
- {"milestone_id": "first_push", "title": "Make the first useful box push", "reward": 0.2, "region": {}},
691
+ {
692
+ "milestone_id": "first_push",
693
+ "title": "Make the first useful box push",
694
+ "reward": 0.2,
695
+ "region": {},
696
+ },
639
697
  {
640
698
  "milestone_id": "box_on_target",
641
699
  "title": "Move a box onto a target",
@@ -768,6 +826,131 @@ def _sokoban_config(
768
826
  )
769
827
 
770
828
 
829
+ def _gamebench_rust_engine(preset: "GeloPreset") -> GeloEngineSection:
830
+ return GeloEngineSection(
831
+ max_rollouts=preset.max_rollouts,
832
+ proposer_rounds=preset.proposer_rounds,
833
+ base_react_system_prompt=None,
834
+ submission_mode="async",
835
+ execute_live_proposers=True,
836
+ bootstrap_train_rollout_count=1,
837
+ heldout_measurement_rollouts=max(1, preset.heldout_seed_count),
838
+ all_candidate_holdout_seed_count=max(1, preset.heldout_seed_count),
839
+ full_rollout_lane_enabled=True,
840
+ full_rollout_initial_budget=8,
841
+ full_rollout_budget_per_round=4,
842
+ full_rollout_cadence=1,
843
+ fresh_rollouts_per_parent=1,
844
+ resume_rollouts_per_parent=1,
845
+ full_rollout_concurrency=4,
846
+ theme_rollout_concurrency=4,
847
+ closeout_heldout_concurrency=4,
848
+ closeout_heldout_candidate_parallelism=2,
849
+ heldout_measurement_concurrency=4,
850
+ heldout_measurement_candidate_parallelism=2,
851
+ full_rollout_checkpoint_cadence="per_llm_call",
852
+ full_rollout_checkpoint_budget=240,
853
+ data_miner_authority=True,
854
+ data_miner_min_new_checkpoints=1,
855
+ data_miner_rollouts_per_job=2,
856
+ data_miner_cadence="after_full_rollout_phase",
857
+ theme_finalize_min_checkpoints=1,
858
+ max_tentative_themes=4,
859
+ tentative_theme_max_age_rounds=2,
860
+ theme_start_score_band=(0.0, 0.95),
861
+ theme_partials_per_candidate=2,
862
+ theme_saturation_threshold=0.9,
863
+ theme_saturation_min_rollouts=2,
864
+ theme_aux_rounds_per_staircase=2,
865
+ theme_proposal_round_budget=2,
866
+ theme_aux_budget_per_theme=2,
867
+ theme_no_progress_rounds=2,
868
+ terminator_default="agent",
869
+ promotion_min_seeds=1,
870
+ promotion_margin=0.01,
871
+ holdout_consolidate_k=2,
872
+ auto_aux_hill_climb_calls_per_round=2,
873
+ auto_consolidate_min_mature_themes=1,
874
+ auto_consolidate_theme_count=2,
875
+ auto_consolidate_min_score=0.0,
876
+ allow_single_theme_consolidation=True,
877
+ consolidation_budget_per_round=2,
878
+ consolidation_max_themes=3,
879
+ target_new_candidate_count=3,
880
+ candidates_per_proposer=3,
881
+ segment_steps=50,
882
+ resume_segment_steps=25,
883
+ max_llm_turns=20,
884
+ max_actions_per_turn=1,
885
+ request_timeout_seconds=240.0,
886
+ rollout_state_poll_seconds=0.25,
887
+ rollout_terminator_poll_seconds=0.25,
888
+ rollout_stall_timeout_seconds=300.0,
889
+ container_connect_timeout_seconds=30.0,
890
+ allow_resume_fallback_to_fresh=False,
891
+ )
892
+
893
+
894
+ def _gamebench_rust_config(
895
+ preset: "GeloPreset",
896
+ *,
897
+ run: GeloRunSection,
898
+ container: GeloContainerSection,
899
+ task_family: str,
900
+ profile: str,
901
+ target_objective_label: str,
902
+ max_steps: int,
903
+ seed_prompt: str,
904
+ ) -> GeloHostedConfig:
905
+ return GeloHostedConfig(
906
+ run=run,
907
+ container=container,
908
+ taskset=GeloTasksetSection(
909
+ train_seeds=_seed_tuple(101, preset.train_seed_count),
910
+ heldout_seeds=_seed_tuple(501, preset.heldout_seed_count),
911
+ profile=profile,
912
+ backend="gamebench_gold_rust",
913
+ reward_mode=GeloRewardMode.PROGRESS,
914
+ checkpoint_semantics=GeloCheckpointSemantics.TRUE_ENV_SNAPSHOT,
915
+ env_config={
916
+ "gamebench_task": task_family,
917
+ "substrate": "rust",
918
+ "max_steps": max_steps,
919
+ },
920
+ context={
921
+ "task_family": task_family,
922
+ "gamebench_task": task_family,
923
+ "substrate_label": f"gamebench/{task_family}/rust",
924
+ "checkpoint_restore_semantics": "true_environment_snapshot",
925
+ "allow_request_snapshot_resume": False,
926
+ "require_real_rewards": True,
927
+ "required_candidate_kind": "prompt",
928
+ "prompt_only_candidate_authoring": True,
929
+ "forbid_code_policy_candidates": True,
930
+ "target_objective_label": target_objective_label,
931
+ "source_ref": f"gamebench/tasks/{task_family}/gold_rust",
932
+ },
933
+ ),
934
+ policy=GeloPolicySection(
935
+ model=preset.policy_model,
936
+ provider=preset.policy_provider,
937
+ api_key_env=preset.policy_api_key_env,
938
+ inference_url="https://api.groq.com/openai/v1/chat/completions",
939
+ max_tokens=512,
940
+ config={
941
+ "use_lm": True,
942
+ "temperature": 0.0,
943
+ "max_steps": max_steps,
944
+ },
945
+ ),
946
+ go_ex=_gamebench_rust_engine(preset),
947
+ seed_candidate=GeloSeedCandidateSection(react_system_prompt=seed_prompt),
948
+ proposers=_default_crafter_roles(),
949
+ cache=GeloCacheSection(mode=GeloCacheMode.OFF),
950
+ disk_budget=GeloDiskBudgetSection(enabled=True, soft_limit_gb=5.0, hard_limit_gb=10.0),
951
+ )
952
+
953
+
771
954
  def _default_crafter_roles() -> Mapping[GeloProposerRole, GeloProposerSection]:
772
955
  roles: dict[GeloProposerRole, GeloProposerSection] = {}
773
956
  for role in _PROPOSER_ROLES:
@@ -878,11 +1061,7 @@ def _structured_from_launcher(payload: Mapping[str, Any], source: Path | None) -
878
1061
 
879
1062
  def _structured_from_flat(flat: Mapping[str, Any]) -> dict[str, Any]:
880
1063
  result: dict[str, Any] = {}
881
- run = {
882
- key: flat[key]
883
- for key in ("run_id", "output_dir", "seed")
884
- if key in flat
885
- }
1064
+ run = {key: flat[key] for key in ("run_id", "output_dir", "seed") if key in flat}
886
1065
  if run:
887
1066
  result["run"] = run
888
1067
  service_url = flat.get("service_url") or flat.get("container_url")
@@ -947,8 +1126,7 @@ def _role_from_flat(flat: Mapping[str, Any], role: GeloProposerRole) -> dict[str
947
1126
  continue
948
1127
  if source_key == "backend" and str(value).strip() in _LEGACY_BACKEND_ALIASES:
949
1128
  raise GeloMaterializeError(
950
- f"GELO proposer {role.value} backend {value!r} is retired; "
951
- "use 'codex_app_server'"
1129
+ f"GELO proposer {role.value} backend {value!r} is retired; use 'codex_app_server'"
952
1130
  )
953
1131
  config[source_key] = value
954
1132
  return config
@@ -987,7 +1165,9 @@ def _container_payload_from_target(target: Any) -> Mapping[str, Any]:
987
1165
  else:
988
1166
  payload = _clean_config_value(target)
989
1167
  if not isinstance(payload, Mapping):
990
- raise GeloMaterializeError("container_tunnel.container_config() must serialize to an object")
1168
+ raise GeloMaterializeError(
1169
+ "container_tunnel.container_config() must serialize to an object"
1170
+ )
991
1171
  return payload
992
1172
 
993
1173
 
@@ -1065,9 +1245,7 @@ def _validate_container_inputs(
1065
1245
 
1066
1246
 
1067
1247
  _GELO_SUPPORTED_PLUGIN_KINDS = frozenset({GeloPluginKind.SFT.value})
1068
- _GELO_FUTURE_PLUGIN_KINDS = frozenset(
1069
- {GeloPluginKind.RLVR.value, GeloPluginKind.OPSD.value}
1070
- )
1248
+ _GELO_FUTURE_PLUGIN_KINDS = frozenset({GeloPluginKind.RLVR.value, GeloPluginKind.OPSD.value})
1071
1249
  _GELO_PLUGIN_KIND_KEYS = frozenset({"kind", "type", "plugin_kind"})
1072
1250
  _GELO_PLUGIN_COLLECTION_KEYS = frozenset({"lanes", "items"})
1073
1251
  _GELO_PLUGIN_METADATA_KEYS = frozenset({"enabled", "status", "version", "config"})
@@ -1099,9 +1277,7 @@ def _validate_plugin_declaration(value: Any, *, path: str) -> None:
1099
1277
  if key in value:
1100
1278
  _validate_plugin_kind(value[key], path=f"{path}.{key}")
1101
1279
  return
1102
- raise GeloMaterializeError(
1103
- f"GELO plugin declaration at {path} must include kind='sft'"
1104
- )
1280
+ raise GeloMaterializeError(f"GELO plugin declaration at {path} must include kind='sft'")
1105
1281
 
1106
1282
 
1107
1283
  def _validate_plugin_collection(value: Any, *, path: str) -> None:
@@ -1169,10 +1345,13 @@ def _validate_materialized_config(config: Mapping[str, Any]) -> None:
1169
1345
  "GELO config container.auth_refresh.provider must be synth_tunnel"
1170
1346
  )
1171
1347
  if not str(raw_auth_refresh.get("lease_id") or "").strip():
1172
- raise GeloMaterializeError(
1173
- "GELO config container.auth_refresh.lease_id is required"
1174
- )
1348
+ raise GeloMaterializeError("GELO config container.auth_refresh.lease_id is required")
1175
1349
  taskset = _mapping(config.get("taskset"))
1350
+ if "checkpoint_semantics" in taskset:
1351
+ raise GeloMaterializeError(
1352
+ "GELO config taskset.checkpoint_semantics is not accepted by hosted "
1353
+ "go-ex; use taskset.context.checkpoint_restore_semantics instead"
1354
+ )
1176
1355
  if not taskset.get("train_seeds") or not taskset.get("heldout_seeds"):
1177
1356
  raise GeloMaterializeError("GELO config requires taskset.train_seeds and heldout_seeds")
1178
1357
  go_ex = _mapping(config.get("go_ex"))
@@ -1188,8 +1367,7 @@ def _validate_materialized_config(config: Mapping[str, Any]) -> None:
1188
1367
  backend = str(role_config.get("backend") or "").strip()
1189
1368
  if backend in _LEGACY_BACKEND_ALIASES:
1190
1369
  raise GeloMaterializeError(
1191
- f"GELO proposer {role_name} backend {backend!r} is retired; "
1192
- "use 'codex_app_server'"
1370
+ f"GELO proposer {role_name} backend {backend!r} is retired; use 'codex_app_server'"
1193
1371
  )
1194
1372
 
1195
1373
 
@@ -1201,6 +1379,11 @@ def _clean_config_value(value: Any) -> Any:
1201
1379
  if is_dataclass(value) and not isinstance(value, type):
1202
1380
  result: dict[str, Any] = {}
1203
1381
  for dataclass_field in fields(value):
1382
+ if (
1383
+ isinstance(value, GeloTasksetSection)
1384
+ and dataclass_field.name == "checkpoint_semantics"
1385
+ ):
1386
+ continue
1204
1387
  item = getattr(value, dataclass_field.name)
1205
1388
  if item is None:
1206
1389
  continue
@@ -1249,7 +1249,13 @@ class GepaConfig:
1249
1249
  if not self.container_command:
1250
1250
  self._preflight_container_capabilities()
1251
1251
  config_path = self.write_toml()
1252
- return _NativeGepaRun.from_toml(str(config_path)).execute()
1252
+ try:
1253
+ result = _NativeGepaRun.from_toml(str(config_path)).execute()
1254
+ except Exception:
1255
+ self._register_usage_complete(status="failed")
1256
+ raise
1257
+ self._register_usage_complete(status="succeeded")
1258
+ return result
1253
1259
 
1254
1260
  def _register_usage_submit(self) -> None:
1255
1261
  if not self.usage_registration.enabled:
@@ -1268,10 +1274,54 @@ class GepaConfig:
1268
1274
  )
1269
1275
  client.register_usage_submit(
1270
1276
  algorithm=OptimizerAlgorithmSlug.GEPA,
1277
+ models=self._usage_registration_models(),
1278
+ )
1279
+ except HostedOptimizerError:
1280
+ return
1281
+
1282
+ def _register_usage_complete(self, *, status: str) -> None:
1283
+ if not self.usage_registration.enabled:
1284
+ return
1285
+ from .hosted import (
1286
+ HostedOptimizerClient,
1287
+ HostedOptimizerError,
1288
+ OptimizerAlgorithmSlug,
1289
+ )
1290
+
1291
+ try:
1292
+ client = HostedOptimizerClient(
1293
+ api_key="",
1294
+ register_usage=None,
1295
+ require_api_key=False,
1296
+ )
1297
+ client.register_usage_complete(
1298
+ algorithm=OptimizerAlgorithmSlug.GEPA,
1299
+ status=status,
1300
+ models=self._usage_registration_models(),
1271
1301
  )
1272
1302
  except HostedOptimizerError:
1273
1303
  return
1274
1304
 
1305
+ def _usage_registration_models(self) -> list[dict[str, str]]:
1306
+ models: list[dict[str, str]] = []
1307
+ if self.proposer.model:
1308
+ models.append(
1309
+ {
1310
+ "role": "proposer",
1311
+ "provider": self.proposer.provider,
1312
+ "model": self.proposer.model,
1313
+ }
1314
+ )
1315
+ if self.policy is not None:
1316
+ models.append(
1317
+ {
1318
+ "role": "policy",
1319
+ "provider": self.policy.provider,
1320
+ "model": self.policy.model,
1321
+ }
1322
+ )
1323
+ return models
1324
+
1275
1325
  def _preflight_container_capabilities(self) -> None:
1276
1326
  metadata = ContainerMetadataPayload.model_validate(
1277
1327
  _http_json(
@@ -323,12 +323,18 @@ class HostedOptimizerClient:
323
323
  "container_pool and container_tunnel are mutually exclusive"
324
324
  )
325
325
  usage_registration_enabled = _usage_registration_enabled_from_toml(config_toml)
326
+ usage_registration_models: list[dict[str, str]] = []
327
+ try:
328
+ usage_registration_models = _usage_registration_models_from_config(tomllib.loads(config_toml))
329
+ except tomllib.TOMLDecodeError:
330
+ usage_registration_models = []
326
331
  if container_tunnel is not None:
327
332
  try:
328
333
  config_json = tomllib.loads(config_toml)
329
334
  except tomllib.TOMLDecodeError as exc:
330
335
  raise HostedOptimizerError(f"invalid GEPA config_toml: {exc}") from exc
331
336
  usage_registration_enabled = _usage_registration_enabled_from_config(config_json)
337
+ usage_registration_models = _usage_registration_models_from_config(config_json)
332
338
  config_json = _with_tunnel_container(config_json, container_tunnel)
333
339
  payload: dict[str, Any] = {
334
340
  "algorithm": OptimizerAlgorithmSlug.GEPA.value,
@@ -349,7 +355,10 @@ class HostedOptimizerClient:
349
355
  response = self._json_request("POST", "/api/v1/optimizers/runs", payload)
350
356
  submit_response = OptimizerRunSubmitResponse.from_payload(response)
351
357
  if usage_registration_enabled:
352
- self.register_usage_submit(algorithm=OptimizerAlgorithmSlug.GEPA)
358
+ self.register_usage_submit(
359
+ algorithm=OptimizerAlgorithmSlug.GEPA,
360
+ models=usage_registration_models,
361
+ )
353
362
  return submit_response
354
363
 
355
364
  def submit_gepa_tunnel_toml(
@@ -583,6 +592,7 @@ class HostedOptimizerClient:
583
592
  )
584
593
  config_json = _config_to_json(config)
585
594
  usage_registration_enabled = _usage_registration_enabled_from_config(config_json)
595
+ usage_registration_models = _usage_registration_models_from_config(config_json)
586
596
  if container_tunnel is not None:
587
597
  config_json = _with_tunnel_container(config_json, container_tunnel)
588
598
  payload: dict[str, Any] = {
@@ -600,13 +610,14 @@ class HostedOptimizerClient:
600
610
  response = self._json_request("POST", "/api/v1/optimizers/runs", payload)
601
611
  submit_response = OptimizerRunSubmitResponse.from_payload(response)
602
612
  if usage_registration_enabled:
603
- self.register_usage_submit(algorithm=algorithm)
613
+ self.register_usage_submit(algorithm=algorithm, models=usage_registration_models)
604
614
  return submit_response
605
615
 
606
616
  def register_usage_submit(
607
617
  self,
608
618
  *,
609
619
  algorithm: OptimizerAlgorithmSlug,
620
+ models: Sequence[Mapping[str, str]] | None = None,
610
621
  ) -> None:
611
622
  if not self.register_usage:
612
623
  return
@@ -619,6 +630,9 @@ class HostedOptimizerClient:
619
630
  "internal": _usage_registration_internal(),
620
631
  "install_id": _usage_install_id(),
621
632
  }
633
+ usage_models = _sanitize_usage_registration_models(models)
634
+ if usage_models:
635
+ payload["models"] = usage_models
622
636
  try:
623
637
  self._json_request(
624
638
  "POST",
@@ -637,6 +651,7 @@ class HostedOptimizerClient:
637
651
  algorithm: OptimizerAlgorithmSlug,
638
652
  status: str,
639
653
  uplift: float | None = None,
654
+ models: Sequence[Mapping[str, str]] | None = None,
640
655
  ) -> None:
641
656
  """Fire a run-completion ping: terminal status + uplift number (if present).
642
657
  Anonymous and best-effort, like the submit ping; never raises."""
@@ -654,6 +669,9 @@ class HostedOptimizerClient:
654
669
  }
655
670
  if uplift is not None:
656
671
  payload["uplift"] = uplift
672
+ usage_models = _sanitize_usage_registration_models(models)
673
+ if usage_models:
674
+ payload["models"] = usage_models
657
675
  try:
658
676
  self._json_request(
659
677
  "POST",
@@ -919,6 +937,68 @@ def _usage_registration_enabled_from_config(config_json: Mapping[str, Any]) -> b
919
937
  return True
920
938
 
921
939
 
940
+ def _usage_registration_models_from_config(config_json: Mapping[str, Any]) -> list[dict[str, str]]:
941
+ models: list[Mapping[str, str]] = []
942
+ policy = config_json.get("policy")
943
+ if isinstance(policy, Mapping):
944
+ models.append(
945
+ {
946
+ "role": "policy",
947
+ "provider": str(policy.get("provider") or ""),
948
+ "model": str(policy.get("model") or ""),
949
+ }
950
+ )
951
+ proposer = config_json.get("proposer")
952
+ if isinstance(proposer, Mapping):
953
+ models.append(
954
+ {
955
+ "role": "proposer",
956
+ "provider": str(proposer.get("provider") or ""),
957
+ "model": str(proposer.get("model") or ""),
958
+ }
959
+ )
960
+ proposers = config_json.get("proposers")
961
+ if isinstance(proposers, Mapping):
962
+ defaults = proposers.get("defaults")
963
+ default_provider = str(defaults.get("provider") or "") if isinstance(defaults, Mapping) else ""
964
+ default_model = str(defaults.get("model") or "") if isinstance(defaults, Mapping) else ""
965
+ for role, raw in proposers.items():
966
+ if role == "defaults" or not isinstance(raw, Mapping):
967
+ continue
968
+ models.append(
969
+ {
970
+ "role": str(raw.get("role") or role),
971
+ "provider": str(raw.get("provider") or default_provider),
972
+ "model": str(raw.get("model") or default_model),
973
+ }
974
+ )
975
+ return _sanitize_usage_registration_models(models)
976
+
977
+
978
+ def _sanitize_usage_registration_models(
979
+ models: Sequence[Mapping[str, str]] | None,
980
+ ) -> list[dict[str, str]]:
981
+ sanitized: list[dict[str, str]] = []
982
+ seen: set[tuple[str, str, str]] = set()
983
+ for raw in list(models or [])[:16]:
984
+ model = str(raw.get("model") or "").strip()[:160]
985
+ if not model:
986
+ continue
987
+ provider = str(raw.get("provider") or "").strip()[:80]
988
+ role = str(raw.get("role") or "").strip()[:64]
989
+ key = (role, provider, model)
990
+ if key in seen:
991
+ continue
992
+ seen.add(key)
993
+ entry = {"model": model}
994
+ if provider:
995
+ entry["provider"] = provider
996
+ if role:
997
+ entry["role"] = role
998
+ sanitized.append(entry)
999
+ return sanitized
1000
+
1001
+
922
1002
  def _package_version() -> str | None:
923
1003
  try:
924
1004
  return version(_PACKAGE_NAME)
@@ -453,8 +453,7 @@ def _require_managed_localhost_target(
453
453
  ) -> None:
454
454
  if target.host not in {"127.0.0.1", "localhost"}:
455
455
  raise TunnelError(
456
- f"{provider.value} managed leases require a localhost target; "
457
- f"got {target.host!r}"
456
+ f"{provider.value} managed leases require a localhost target; got {target.host!r}"
458
457
  )
459
458
 
460
459
 
@@ -675,8 +674,7 @@ class _SynthTunnelAgent:
675
674
  if self._lease_id not in {str(item) for item in accepted}:
676
675
  rejected = ", ".join(str(item) for item in payload.get("rejected_leases") or [])
677
676
  raise TunnelError(
678
- "SynthTunnel agent attach was rejected"
679
- + (f": {rejected}" if rejected else "")
677
+ "SynthTunnel agent attach was rejected" + (f": {rejected}" if rejected else "")
680
678
  )
681
679
  self._ready.set()
682
680
  return
@@ -924,9 +922,7 @@ def _required_binary(name: str, provider: TunnelProvider) -> str:
924
922
  binary = shutil.which(name)
925
923
  if binary:
926
924
  return binary
927
- raise TunnelError(
928
- f"{provider.value} tunnel provider requires the {name!r} binary in PATH"
929
- )
925
+ raise TunnelError(f"{provider.value} tunnel provider requires the {name!r} binary in PATH")
930
926
 
931
927
 
932
928
  def _start_cloudflared(binary: str, tunnel_token: str) -> subprocess.Popen[str]:
@@ -969,13 +965,12 @@ def _connect_websocket(url: str, *, headers: Mapping[str, str]) -> Any:
969
965
  try:
970
966
  import websocket
971
967
  except ImportError as exc:
972
- raise TunnelError(
973
- "synth_tunnel provider requires the 'websocket-client' package"
974
- ) from exc
968
+ raise TunnelError("synth_tunnel provider requires the 'websocket-client' package") from exc
975
969
  ws = websocket.WebSocket()
976
970
  header_lines = [f"{key}: {value}" for key, value in headers.items()]
977
971
  try:
978
972
  ws.connect(url, header=header_lines, timeout=10)
973
+ ws.settimeout(None)
979
974
  except Exception:
980
975
  try:
981
976
  ws.close()