coderouter-cli 2.16.0__tar.gz → 2.18.0__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 (327) hide show
  1. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/CHANGELOG.md +84 -0
  2. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/PKG-INFO +1 -1
  3. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/anthropic_native.py +4 -0
  4. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/base.py +20 -0
  5. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/openai_compat.py +6 -0
  6. coderouter_cli-2.18.0/coderouter/adapters/slot_affinity.py +54 -0
  7. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cli.py +2 -1
  8. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/schemas.py +58 -1
  9. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor_claude_code.py +12 -0
  10. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_errors.py +24 -5
  11. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/anthropic_routes.py +20 -6
  12. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/openai_routes.py +7 -2
  13. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/auto_router.py +46 -2
  14. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/capability.py +4 -1
  15. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback.py +94 -6
  16. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/fallback_trace.py +8 -1
  17. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/anthropic.py +3 -0
  18. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/convert.py +4 -0
  19. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/vscode_init.py +10 -1
  20. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/README.en.md +1 -0
  21. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/README.md +1 -0
  22. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.en.md +21 -0
  23. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/llamacpp-direct.md +21 -0
  24. coderouter_cli-2.18.0/docs/concepts/commit-point.en.md +68 -0
  25. coderouter_cli-2.18.0/docs/concepts/commit-point.md +70 -0
  26. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.en.md +19 -2
  27. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/subagent-routing.md +22 -2
  28. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/pyproject.toml +1 -1
  29. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_anthropic_errors.py +30 -1
  30. coderouter_cli-2.18.0/tests/test_auto_router_hints.py +99 -0
  31. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_context_budget.py +30 -16
  32. coderouter_cli-2.18.0/tests/test_slot_affinity.py +147 -0
  33. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_search.py +1 -0
  34. coderouter_cli-2.18.0/tests/test_ttft_timeout.py +237 -0
  35. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/.gitignore +0 -0
  36. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/LICENSE +0 -0
  37. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/README.en.md +0 -0
  38. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/README.md +0 -0
  39. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/__init__.py +0 -0
  40. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/__main__.py +0 -0
  41. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/__init__.py +0 -0
  42. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/adapters/registry.py +0 -0
  43. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cli_stats.py +0 -0
  44. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/__init__.py +0 -0
  45. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/capability_registry.py +0 -0
  46. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/env_file.py +0 -0
  47. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/config/loader.py +0 -0
  48. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/cost.py +0 -0
  49. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/credentials.py +0 -0
  50. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/data/__init__.py +0 -0
  51. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/data/model-capabilities.yaml +0 -0
  52. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor.py +0 -0
  53. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/doctor_apply.py +0 -0
  54. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/env_security.py +0 -0
  55. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/errors.py +0 -0
  56. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/gguf_introspect.py +0 -0
  57. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/__init__.py +0 -0
  58. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/_fingerprint.py +0 -0
  59. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/backend_health.py +0 -0
  60. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/context_budget.py +0 -0
  61. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/continuous_probe.py +0 -0
  62. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_actions.py +0 -0
  63. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/drift_detection.py +0 -0
  64. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_budget.py +0 -0
  65. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/memory_pressure.py +0 -0
  66. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/self_healing.py +0 -0
  67. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/guards/tool_loop.py +0 -0
  68. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/hardware.py +0 -0
  69. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/__init__.py +0 -0
  70. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/app.py +0 -0
  71. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/dashboard_routes.py +0 -0
  72. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/launcher_routes.py +0 -0
  73. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/metrics_routes.py +0 -0
  74. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/tool_search.py +0 -0
  75. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/ingress/ui_auth.py +0 -0
  76. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/language_tax.py +0 -0
  77. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_devices.py +0 -0
  78. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_speculative.py +0 -0
  79. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/launcher_swap.py +0 -0
  80. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/logging.py +0 -0
  81. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/__init__.py +0 -0
  82. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/collector.py +0 -0
  83. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/metrics/prometheus.py +0 -0
  84. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/output_filters.py +0 -0
  85. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/__init__.py +0 -0
  86. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/base.py +0 -0
  87. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/loader.py +0 -0
  88. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/plugins/registry.py +0 -0
  89. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/py.typed +0 -0
  90. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/rollback.py +0 -0
  91. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/__init__.py +0 -0
  92. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/adaptive.py +0 -0
  93. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/routing/budget.py +0 -0
  94. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/secret_redaction.py +0 -0
  95. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/__init__.py +0 -0
  96. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/audit_log.py +0 -0
  97. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/replay.py +0 -0
  98. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/request_log.py +0 -0
  99. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/store.py +0 -0
  100. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/state/suggest_rules.py +0 -0
  101. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/token_estimation.py +0 -0
  102. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/token_estimation_accurate.py +0 -0
  103. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/__init__.py +0 -0
  104. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/coderouter/translation/tool_repair.py +0 -0
  105. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/assets/dashboard-demo.png +0 -0
  106. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.en.md +0 -0
  107. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/claude-code-llamacpp-vllm.md +0 -0
  108. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.en.md +0 -0
  109. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/external-agents.md +0 -0
  110. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.en.md +0 -0
  111. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/gguf_dl.md +0 -0
  112. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.en.md +0 -0
  113. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/hf-ollama-models.md +0 -0
  114. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.en.md +0 -0
  115. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/install-backends.md +0 -0
  116. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.en.md +0 -0
  117. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher-quickstart.md +0 -0
  118. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher.en.md +0 -0
  119. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/launcher.md +0 -0
  120. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.en.md +0 -0
  121. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/lmstudio-direct.md +0 -0
  122. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/backends/verify-ollama-0.23.1.md +0 -0
  123. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.en.md +0 -0
  124. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/architecture.md +0 -0
  125. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.en.md +0 -0
  126. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/context-budget.md +0 -0
  127. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.en.md +0 -0
  128. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/continuous-probing.md +0 -0
  129. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.en.md +0 -0
  130. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/drift-detection.md +0 -0
  131. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.en.md +0 -0
  132. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/partial-stitch.md +0 -0
  133. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.en.md +0 -0
  134. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/concepts/stream-truncation.md +0 -0
  135. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/agent-cli-plugin-extraction.md +0 -0
  136. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/external-agents-adapter.md +0 -0
  137. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/launcher-model-swap.md +0 -0
  138. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/launcher-multi-build.md +0 -0
  139. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/orchestration-companion.md +0 -0
  140. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.5-dashboard-mockup.html +0 -0
  141. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router-verification.md +0 -0
  142. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/designs/v1.6-auto-router.md +0 -0
  143. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.en.md +0 -0
  144. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/free-tier-guide.md +0 -0
  145. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.en.md +0 -0
  146. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/language-tax.md +0 -0
  147. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.en.md +0 -0
  148. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/remote-access.md +0 -0
  149. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/security.en.md +0 -0
  150. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/security.md +0 -0
  151. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.en.md +0 -0
  152. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/troubleshooting.md +0 -0
  153. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.en.md +0 -0
  154. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/usage-guide.md +0 -0
  155. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/guides/vscode.md +0 -0
  156. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.en.md +0 -0
  157. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/low-memory-integration.md +0 -0
  158. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/CHANGES.md +0 -0
  159. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/README.md +0 -0
  160. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/openrouter-roster/latest.json +0 -0
  161. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.4.md +0 -0
  162. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5-verify.md +0 -0
  163. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.5.md +0 -0
  164. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.6.md +0 -0
  165. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v0.7.md +0 -0
  166. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0-verify.md +0 -0
  167. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/retrospectives/v1.0.md +0 -0
  168. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/quickstart.en.md +0 -0
  169. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/quickstart.md +0 -0
  170. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.en.md +0 -0
  171. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/docs/start/when-do-i-need-coderouter.md +0 -0
  172. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/.env.example +0 -0
  173. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/README.md +0 -0
  174. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers-agent-cli.yaml +0 -0
  175. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers-multiagent.yaml +0 -0
  176. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.cli-session.yaml +0 -0
  177. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.context-budget-test.yaml +0 -0
  178. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.llamacpp-vllm.yaml +0 -0
  179. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.nvidia-nim.yaml +0 -0
  180. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto-custom.yaml +0 -0
  181. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-auto.yaml +0 -0
  182. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.ollama-free-chain.yaml +0 -0
  183. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.opusplan.yaml +0 -0
  184. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.raspberrypi.yaml +0 -0
  185. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.swap.yaml +0 -0
  186. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/examples/providers.yaml +0 -0
  187. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/demo_traffic.sh +0 -0
  188. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/openrouter_roster_diff.py +0 -0
  189. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/smoke_v2_2.sh +0 -0
  190. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify-providers.yaml +0 -0
  191. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_ollama_0_23.py +0 -0
  192. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_v0_5.sh +0 -0
  193. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/scripts/verify_v1_0.sh +0 -0
  194. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/__init__.py +0 -0
  195. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/conftest.py +0 -0
  196. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_adapter_anthropic.py +0 -0
  197. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_agent_cli.py +0 -0
  198. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_audit_log.py +0 -0
  199. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_auto_router.py +0 -0
  200. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_auto_router_cjk.py +0 -0
  201. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_backend_health.py +0 -0
  202. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_budget.py +0 -0
  203. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability.py +0 -0
  204. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_degraded_payload.py +0 -0
  205. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_registry.py +0 -0
  206. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_registry_cache_control.py +0 -0
  207. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_capability_tool_choice.py +0 -0
  208. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_claude_code_suitability.py +0 -0
  209. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli.py +0 -0
  210. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli_bind_warning.py +0 -0
  211. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_cli_stats.py +0 -0
  212. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config.py +0 -0
  213. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config_cwd_warning.py +0 -0
  214. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_config_shim_actions.py +0 -0
  215. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_continuous_probe.py +0 -0
  216. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_credentials.py +0 -0
  217. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_dashboard_endpoint.py +0 -0
  218. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor.py +0 -0
  219. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_apply.py +0 -0
  220. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_cache_probe.py +0 -0
  221. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_doctor_claude_code.py +0 -0
  222. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_actions.py +0 -0
  223. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_detection.py +0 -0
  224. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_drift_detection_integration.py +0 -0
  225. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_env_file.py +0 -0
  226. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_env_security.py +0 -0
  227. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_errors.py +0 -0
  228. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml.py +0 -0
  229. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_examples_yaml_roundtrip.py +0 -0
  230. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback.py +0 -0
  231. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_anthropic.py +0 -0
  232. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_control.py +0 -0
  233. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_observed.py +0 -0
  234. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_cache_strip.py +0 -0
  235. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_empty_response.py +0 -0
  236. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_misconfig_warn.py +0 -0
  237. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_paid_gate.py +0 -0
  238. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_reason.py +0 -0
  239. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_thinking.py +0 -0
  240. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_choice_emulate.py +0 -0
  241. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fallback_tool_integrity.py +0 -0
  242. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h1_prometheus_drift.py +0 -0
  243. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h2_h6_translation.py +0 -0
  244. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h3_client_reuse.py +0 -0
  245. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h4_h5_guards.py +0 -0
  246. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h7_probe_restore.py +0 -0
  247. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_h8_launcher_auth.py +0 -0
  248. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mA_routing.py +0 -0
  249. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mB_adapters.py +0 -0
  250. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mC_convert.py +0 -0
  251. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mD_logging_metrics.py +0 -0
  252. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mE_config_validation.py +0 -0
  253. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_mF_ingress.py +0 -0
  254. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_fix_v213_launcher_hardening.py +0 -0
  255. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_gguf_introspect.py +0 -0
  256. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_guards_tool_loop.py +0 -0
  257. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_hardware.py +0 -0
  258. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_import_hygiene.py +0 -0
  259. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_anthropic.py +0 -0
  260. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_count_tokens.py +0 -0
  261. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ingress_profile.py +0 -0
  262. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_language_tax.py +0 -0
  263. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_language_tax_integration.py +0 -0
  264. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_backend_variants.py +0 -0
  265. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_config_bench.py +0 -0
  266. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices.py +0 -0
  267. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_detect.py +0 -0
  268. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_devices_routes.py +0 -0
  269. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_devices.py +0 -0
  270. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_events.py +0 -0
  271. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_readiness.py +0 -0
  272. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_gui_sweep_close.py +0 -0
  273. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp.py +0 -0
  274. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_mtp_fallback.py +0 -0
  275. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_provider_sync.py +0 -0
  276. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_readiness_restart.py +0 -0
  277. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap.py +0 -0
  278. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_swap_review.py +0 -0
  279. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_config.py +0 -0
  280. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_gui.py +0 -0
  281. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_routes.py +0 -0
  282. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_launcher_variant_sweep.py +0 -0
  283. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_memory_budget.py +0 -0
  284. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_memory_pressure.py +0 -0
  285. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_cache.py +0 -0
  286. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_collector.py +0 -0
  287. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_cost.py +0 -0
  288. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_endpoint.py +0 -0
  289. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_jsonl.py +0 -0
  290. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus.py +0 -0
  291. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_metrics_prometheus_cache.py +0 -0
  292. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_models_passthrough.py +0 -0
  293. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_openai_compat.py +0 -0
  294. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_openrouter_roster_diff.py +0 -0
  295. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_output_filters.py +0 -0
  296. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_output_filters_adapters.py +0 -0
  297. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_packaging.py +0 -0
  298. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_partial_stitch.py +0 -0
  299. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugin_adapter.py +0 -0
  300. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_integration.py +0 -0
  301. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_loader.py +0 -0
  302. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_plugins_registry.py +0 -0
  303. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_reasoning_strip.py +0 -0
  304. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_repair_byte_fallback.py +0 -0
  305. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_request_log.py +0 -0
  306. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_role_normalization.py +0 -0
  307. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_rollback.py +0 -0
  308. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_routing_adaptive.py +0 -0
  309. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_secret_redaction.py +0 -0
  310. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_self_healing.py +0 -0
  311. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_setup_sh.py +0 -0
  312. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_state_store.py +0 -0
  313. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_stop_reason_forward_compat.py +0 -0
  314. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_stream_truncation.py +0 -0
  315. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_token_estimation.py +0 -0
  316. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_token_estimation_accurate.py +0 -0
  317. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tokens_saved_metric.py +0 -0
  318. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair.py +0 -0
  319. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_differential.py +0 -0
  320. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_lenient.py +0 -0
  321. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_r4.py +0 -0
  322. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_tool_repair_scanners.py +0 -0
  323. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_toolrepair_bench.py +0 -0
  324. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_translation_anthropic.py +0 -0
  325. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_translation_reverse.py +0 -0
  326. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_ui_auth.py +0 -0
  327. {coderouter_cli-2.16.0 → coderouter_cli-2.18.0}/tests/test_vscode_init.py +0 -0
@@ -9,6 +9,90 @@ are kept verbatim where the Japanese text itself is the subject).
9
9
 
10
10
  ---
11
11
 
12
+ ## [v2.18.0] — 2026-10-24 (route on Claude Code's own request hints)
13
+
14
+ ### Added
15
+
16
+ - **auto_router matchers on Claude Code's gateway hint headers.** With
17
+ `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1` (Claude Code 2.1.273+), every request
18
+ says what it is. Three new `RuleMatcher` fields read those headers:
19
+ - `request_class` — `x-claude-code-request-class` (`main` / `subagent` /
20
+ `workflow` / `compaction` / `auxiliary`), case-insensitive;
21
+ - `agent_type` — `x-claude-code-agent-type` (e.g. `Explore`),
22
+ case-insensitive;
23
+ - `compaction: true` — `x-claude-code-compaction` is truthy (`false` is
24
+ rejected at load, like the other boolean matchers).
25
+ Typical use: auto-mode classifier / title calls (`auxiliary`) to the
26
+ smallest local model, compaction to a long-context profile, `Explore`
27
+ sub-agents to a fast one. Without the headers these matchers never match,
28
+ so existing rule sets behave exactly as before. `classify()` gained an
29
+ optional `hints` argument; both `/v1/messages` and `/v1/chat/completions`
30
+ pass the headers through. Docs: `docs/guides/subagent-routing.md` / `.en.md`.
31
+ - **`slot_affinity_slots` (provider; llama.cpp only).** For
32
+ `llama-server --parallel N`: requests carrying a conversation key
33
+ (`x-claude-code-session-id`) get `id_slot = sha256(key) % N`, so every
34
+ turn of a conversation reuses the same slot's prompt cache instead of
35
+ re-prefilling on whichever slot is free. Stable across restarts; an
36
+ explicit `id_slot` in `extra_body` wins; off by default. Works on both the
37
+ native (`kind: anthropic`) and translated (`kind: openai_compat`) paths,
38
+ and survives every internal request rebuild (thinking strip for backends
39
+ without thinking, `tool_choice` emulation, `cache_control` strip,
40
+ OpenAI→Anthropic translation). The main conversation and its compaction
41
+ share a key; sub-agents and auxiliary calls get `<session>:<class>:<agent>`
42
+ so they do not queue on — or evict — the main conversation's slot.
43
+ Docs: `docs/backends/llamacpp-direct.md` / `.en.md`.
44
+ - **`vscode-init` also writes `CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`**, and
45
+ `doctor --check-claude-code` reports (as info) when it is missing.
46
+
47
+ ---
48
+
49
+ ## [v2.17.0] — 2026-10-24 (time-to-first-token cut-off, retry signals)
50
+
51
+ ### Added
52
+
53
+ - **`ttft_timeout_s` (provider).** A streaming attempt that has not
54
+ produced its first real output (text / tool_use / thinking delta) within
55
+ this many seconds is abandoned before any byte reached the client, and
56
+ the chain moves on — e.g. a local llama-server still loading a model or
57
+ prefilling a long prompt while a cloud provider could answer now. Never
58
+ applied to the last provider of a chain (nothing to fall back to), nor to
59
+ non-streaming requests (`timeout_s` covers those), nor to the
60
+ `/v1/chat/completions` route. It is also skipped for the tool-turn
61
+ downgrade (`kind: openai_compat` with tools, which calls the upstream
62
+ non-streaming so tool calls can be repaired): there the "first output" is
63
+ the whole answer, and a deadline would discard slow but healthy replies —
64
+ use `kind: anthropic` (native `/v1/messages`) to get the cut-off for a
65
+ local server. It always covers the
66
+ wait for the first event; covering the gap between the opening events and
67
+ the first real output needs `empty_response_action: fallback`, which is
68
+ what withholds those events from the client. Abandoned attempts are
69
+ ordinary retryable failures: new fallback reason `ttft-timeout` (detail
70
+ `no-first-token`) and the self-healing guards learn from them. A cut that
71
+ happens after the opening events is handled like a truncated stream: the
72
+ withheld, unterminated preamble is never flushed to the client if the
73
+ rest of the chain also fails, and the abandoned upstream stream is closed
74
+ immediately. Default
75
+ `None` (off). Docs: `docs/concepts/commit-point.md` / `.en.md`.
76
+ - **`x-should-retry` on `/v1/messages` errors.** Every Anthropic-shaped
77
+ error CodeRouter produces now says whether retrying can help — the header
78
+ Claude Code reads, with `retry-after`, to decide whether to spend its
79
+ retry budget: `false` for 400 / 413 (including tool-search and
80
+ `context_budget_action: reject` refusals), `true` for 429, and for 502
81
+ `true` only when at least one upstream failure was retryable (a chain that
82
+ failed purely on 401/403 fails the same way on every retry).
83
+ - **Docs: the commit point** (`docs/concepts/commit-point.md` / `.en.md`) —
84
+ which failures before the body ships CodeRouter can still move to another
85
+ provider, which it cannot, and the error each chain failure becomes.
86
+ Recommends `empty_response_action: fallback` for Claude Code profiles.
87
+
88
+ ### Known limits
89
+
90
+ - No keep-alive `ping` is sent while opening events are withheld; keep
91
+ `ttft_timeout_s` below the client's idle timeout
92
+ (`CLAUDE_STREAM_IDLE_TIMEOUT_MS` for Claude Code).
93
+
94
+ ---
95
+
12
96
  ## [v2.16.0] — 2026-10-24 (Claude Code 2.1.3xx behind a gateway)
13
97
 
14
98
  Claude Code 2.1.285–2.1.309 changed several defaults for sessions behind a
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderouter-cli
3
- Version: 2.16.0
3
+ Version: 2.18.0
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -46,6 +46,7 @@ from coderouter.adapters.base import (
46
46
  StreamChunk,
47
47
  StreamTruncatedError,
48
48
  )
49
+ from coderouter.adapters.slot_affinity import apply_slot_affinity
49
50
  from coderouter.credentials import resolve_provider_credential
50
51
  from coderouter.logging import (
51
52
  get_logger,
@@ -243,6 +244,9 @@ class AnthropicAdapter(BaseAdapter):
243
244
  body.update(dumped)
244
245
  body["model"] = self.config.model
245
246
  body["stream"] = stream
247
+ apply_slot_affinity(
248
+ body, slots=self.config.slot_affinity_slots, key=req.session_key
249
+ )
246
250
  return body
247
251
 
248
252
  # ------------------------------------------------------------------
@@ -65,6 +65,9 @@ class ChatRequest(BaseModel):
65
65
 
66
66
  # CodeRouter-specific extension (not sent upstream)
67
67
  profile: str | None = Field(default=None, exclude=True)
68
+ # v2.18.0: conversation key for KV-slot affinity (see
69
+ # ``ProviderConfig.slot_affinity_slots``). Never serialized.
70
+ session_key: str | None = Field(default=None, exclude=True)
68
71
 
69
72
 
70
73
  class ChatResponse(BaseModel):
@@ -200,6 +203,23 @@ class StreamTruncatedError(AdapterError):
200
203
  self.tool_call_in_flight = tool_call_in_flight
201
204
 
202
205
 
206
+ # v2.17.0: the provider did not produce its first real streamed output within
207
+ # ``ProviderConfig.ttft_timeout_s``. Raised by the engine (not the adapter),
208
+ # before any byte reached the client, so the chain falls back normally and the
209
+ # self-healing guards learn from it like any other retryable failure.
210
+ class TTFTTimeoutError(AdapterError):
211
+ """No first token within ``ttft_timeout_s``."""
212
+
213
+ def __init__(self, *, provider: str, ttft_timeout_s: float) -> None:
214
+ super().__init__(
215
+ f"timeout: no first token within {ttft_timeout_s:g}s (ttft_timeout_s)",
216
+ provider=provider,
217
+ status_code=None,
218
+ retryable=True,
219
+ )
220
+ self.ttft_timeout_s = ttft_timeout_s
221
+
222
+
203
223
  # v0.6-B: per-call overrides resolved from the active profile. The engine
204
224
  # builds one instance per request (since a profile is invariant across its
205
225
  # chain) and threads it through every adapter call on that chain. Adapters
@@ -30,6 +30,7 @@ from coderouter.adapters.base import (
30
30
  StreamChunk,
31
31
  StreamTruncatedError,
32
32
  )
33
+ from coderouter.adapters.slot_affinity import apply_slot_affinity
33
34
  from coderouter.credentials import resolve_provider_credential
34
35
  from coderouter.logging import (
35
36
  get_logger,
@@ -182,6 +183,11 @@ class OpenAICompatAdapter(BaseAdapter):
182
183
  value = getattr(request, field, None)
183
184
  if value is not None:
184
185
  body[field] = value
186
+ apply_slot_affinity(
187
+ body,
188
+ slots=self.config.slot_affinity_slots,
189
+ key=getattr(request, "session_key", None),
190
+ )
185
191
  if stream:
186
192
  # Request a terminal usage chunk. Providers that honor this
187
193
  # (OpenAI, OpenRouter, Ollama >=0.x) will send one extra chunk
@@ -0,0 +1,54 @@
1
+ """v2.18.0: KV-slot affinity for llama-server (``id_slot``).
2
+
3
+ llama-server started with ``--parallel N`` serves N slots, each holding its
4
+ own prompt cache. Without a hint it picks a free slot, so consecutive turns
5
+ of one conversation can land on different slots and re-prefill the whole
6
+ prompt. Sending ``id_slot`` pins a conversation to one slot.
7
+
8
+ The slot is ``sha256(key) % N`` — stable across processes and restarts
9
+ (Python's ``hash`` is salted per process).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import hashlib
15
+ from collections.abc import Mapping
16
+ from typing import Any
17
+
18
+ # Request classes whose prompt shares the main conversation's prefix: they
19
+ # belong on the main conversation's slot. Everything else (sub-agents,
20
+ # auxiliary calls such as the auto-mode classifier or titles, workflows) has
21
+ # its own prompt and gets its own key, so it neither waits on the main slot
22
+ # nor evicts its cache.
23
+ _MAIN_CLASSES = frozenset({"", "main", "compaction"})
24
+
25
+
26
+ def affinity_key(hints: Mapping[str, str]) -> str | None:
27
+ """Conversation key for slot affinity, from Claude Code's hint headers.
28
+
29
+ ``None`` when there is no session id. ``main`` / ``compaction`` (and a
30
+ missing class) share ``<session>``; other classes get
31
+ ``<session>:<class>:<agent_type>``.
32
+ """
33
+ session = hints.get("session_id")
34
+ if not session:
35
+ return None
36
+ request_class = hints.get("request_class", "").lower()
37
+ if request_class in _MAIN_CLASSES:
38
+ return session
39
+ return f"{session}:{request_class}:{hints.get('agent_type', '').lower()}"
40
+
41
+
42
+ def slot_for(key: str, slots: int) -> int:
43
+ digest = hashlib.sha256(key.encode("utf-8")).digest()
44
+ return int.from_bytes(digest[:8], "big") % slots
45
+
46
+
47
+ def apply_slot_affinity(body: dict[str, Any], *, slots: int | None, key: str | None) -> None:
48
+ """Set ``body["id_slot"]`` when affinity is configured and a key exists.
49
+
50
+ An ``id_slot`` already present (e.g. from ``extra_body``) wins.
51
+ """
52
+ if not slots or not key or "id_slot" in body:
53
+ return
54
+ body["id_slot"] = slot_for(key, slots)
@@ -483,7 +483,8 @@ def _build_parser() -> argparse.ArgumentParser:
483
483
  help=(
484
484
  "v2.16.0: do not write the Claude Code gateway settings "
485
485
  "(ENABLE_TOOL_SEARCH=false, CLAUDE_CODE_AUTO_MODE_SERVER=0, "
486
- "and --max-context-tokens). Use when the chain is "
486
+ "CLAUDE_CODE_GATEWAY_HINT_HEADERS=1 and --max-context-tokens). "
487
+ "Use when the chain is "
487
488
  "api.anthropic.com only."
488
489
  ),
489
490
  )
@@ -550,6 +550,45 @@ class ProviderConfig(BaseModel):
550
550
  description="If true, only used when ALLOW_PAID=true (plan.md §2.3).",
551
551
  )
552
552
  timeout_s: float = Field(default=30.0, ge=1.0, le=86400.0)
553
+ # v2.17.0: time-to-first-token cut-off for streaming requests. When the
554
+ # provider has not produced its first real output (text / tool_use /
555
+ # thinking delta) within this many seconds of the attempt starting, the
556
+ # attempt is abandoned and the chain moves on — before any byte reached
557
+ # the client. Never applied to the last provider of a chain (nothing to
558
+ # move on to), nor to non-streaming requests (``timeout_s`` covers
559
+ # those). Before the first *event* it always applies; between the first
560
+ # event and the first real output it needs
561
+ # ``empty_response_action: fallback`` on the profile, which is what
562
+ # withholds those opening events from the client. ``None`` (default)
563
+ # disables it. Typical use: a local llama-server still loading a model
564
+ # or chewing through a long prompt while a cloud fallback could answer.
565
+ # v2.18.0: KV-slot affinity for llama-server started with
566
+ # ``--parallel N``. When set to N and the request carries a conversation
567
+ # key (Claude Code's ``x-claude-code-session-id`` hint header), the body
568
+ # gets ``id_slot = stable_hash(key) % N`` so every turn of a conversation
569
+ # lands on the same slot and reuses its prompt cache instead of
570
+ # re-prefilling on whichever slot is free. llama.cpp only (other servers
571
+ # ignore or reject ``id_slot``); ``None`` (default) sends nothing.
572
+ slot_affinity_slots: int | None = Field(
573
+ default=None,
574
+ ge=1,
575
+ le=256,
576
+ description=(
577
+ "v2.18.0: llama-server --parallel N. Pins each conversation to "
578
+ "slot hash(session) % N via id_slot so its KV cache is reused. "
579
+ "llama.cpp only. None disables."
580
+ ),
581
+ )
582
+ ttft_timeout_s: float | None = Field(
583
+ default=None,
584
+ gt=0.0,
585
+ le=3600.0,
586
+ description=(
587
+ "v2.17.0: seconds to wait for the first real streamed output "
588
+ "before falling back to the next provider (not applied to the "
589
+ "last provider in a chain). None disables."
590
+ ),
591
+ )
553
592
 
554
593
  # v2.6 language-tax track: path to a LOCAL ``tokenizer.json`` for this
555
594
  # provider's model, used to measure the CJK over-count vs the char/4
@@ -1617,6 +1656,21 @@ class RuleMatcher(BaseModel):
1617
1656
  # declares any tools); ``False`` is rejected by ``_exactly_one``
1618
1657
  # since a "no-tools" rule would shadow the default fall-through.
1619
1658
  has_tools: bool | None = None
1659
+ # v2.18.0: Claude Code gateway hint headers. Claude Code 2.1.273+ sends
1660
+ # these to a custom ANTHROPIC_BASE_URL when
1661
+ # ``CLAUDE_CODE_GATEWAY_HINT_HEADERS=1`` is set (``coderouter
1662
+ # vscode-init`` sets it). They say what a request *is* — something the
1663
+ # body heuristics above can only guess:
1664
+ # * ``request_class`` — ``x-claude-code-request-class``: ``main`` /
1665
+ # ``subagent`` / ``workflow`` / ``compaction`` / ``auxiliary``
1666
+ # (auto-mode classifier, titles, …). Case-insensitive exact match.
1667
+ # * ``agent_type`` — ``x-claude-code-agent-type`` (e.g. ``Explore``),
1668
+ # case-insensitive exact match.
1669
+ # * ``compaction: True`` — ``x-claude-code-compaction`` is truthy.
1670
+ # Without the headers these matchers simply never match.
1671
+ request_class: str | None = None
1672
+ agent_type: str | None = None
1673
+ compaction: bool | None = None
1620
1674
 
1621
1675
  _MATCHER_FIELDS: tuple[str, ...] = (
1622
1676
  "has_image",
@@ -1627,12 +1681,15 @@ class RuleMatcher(BaseModel):
1627
1681
  "content_token_count_min",
1628
1682
  "has_tools",
1629
1683
  "cjk_ratio_min",
1684
+ "request_class",
1685
+ "agent_type",
1686
+ "compaction",
1630
1687
  )
1631
1688
 
1632
1689
  # Boolean matcher fields carry meaning only at ``True`` — the runtime
1633
1690
  # evaluator uses ``is True`` — so a ``False`` value is a dead rule that
1634
1691
  # never matches. ``_exactly_one`` rejects it explicitly (see below).
1635
- _BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools")
1692
+ _BOOL_MATCHER_FIELDS: tuple[str, ...] = ("has_image", "has_tools", "compaction")
1636
1693
 
1637
1694
  @model_validator(mode="after")
1638
1695
  def _exactly_one(self) -> Self:
@@ -125,6 +125,18 @@ def check(version: Version | None, env: Mapping[str, str]) -> list[ClaudeCodeFin
125
125
  )
126
126
  )
127
127
 
128
+ if env.get("CLAUDE_CODE_GATEWAY_HINT_HEADERS", "").lower() in ("1", "true"):
129
+ out.append(ClaudeCodeFinding("ok", "CLAUDE_CODE_GATEWAY_HINT_HEADERS=1"))
130
+ else:
131
+ out.append(
132
+ ClaudeCodeFinding(
133
+ "info",
134
+ "CLAUDE_CODE_GATEWAY_HINT_HEADERS is not set: auto_router rules "
135
+ "on request_class / agent_type / compaction (v2.18.0) never "
136
+ "match without it.",
137
+ )
138
+ )
139
+
128
140
  if env.get("CLAUDE_CODE_AUTO_MODE_SERVER") == "0":
129
141
  out.append(ClaudeCodeFinding("ok", "CLAUDE_CODE_AUTO_MODE_SERVER=0"))
130
142
  else:
@@ -33,6 +33,13 @@ terminal error 429 429 ``rate_limit_error``
33
33
  anything else 502 ``api_error``
34
34
  ==================================== ====== ========================
35
35
 
36
+ v2.17.0: every mapped response also carries ``x-should-retry`` — the header
37
+ Claude Code reads (with ``retry-after``) to decide whether to spend its retry
38
+ budget. ``false`` for the 4xx rows (resending the same request cannot
39
+ help), ``true`` for 429, and for 502 ``true`` only when at least one
40
+ upstream failure was retryable — a chain that failed purely on, say, 401s
41
+ will fail the same way on every retry.
42
+
36
43
  Upstream 401 / 403 deliberately fall into the last row: they describe
37
44
  CodeRouter's *own* upstream credentials, not the client's, and handing a
38
45
  401 to Claude Code would send the user to ``/login`` for nothing.
@@ -164,17 +171,26 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
164
171
  msg = upstream_message(err)
165
172
  if not msg.lower().startswith(PROMPT_TOO_LONG_PREFIX):
166
173
  msg = f"{PROMPT_TOO_LONG_PREFIX}: {msg}"
167
- return AnthropicErrorResponse(400, "invalid_request_error", msg)
174
+ return AnthropicErrorResponse(
175
+ 400, "invalid_request_error", msg, {"x-should-retry": "false"}
176
+ )
168
177
 
169
178
  terminal = errors[-1] if errors else None
170
179
  if terminal is not None and terminal.status_code is not None:
171
180
  status = terminal.status_code
172
181
  if status in (400, 422) and not terminal.retryable:
173
- return AnthropicErrorResponse(400, "invalid_request_error", upstream_message(terminal))
182
+ return AnthropicErrorResponse(
183
+ 400,
184
+ "invalid_request_error",
185
+ upstream_message(terminal),
186
+ {"x-should-retry": "false"},
187
+ )
174
188
  if status == 413:
175
- return AnthropicErrorResponse(413, "request_too_large", upstream_message(terminal))
189
+ return AnthropicErrorResponse(
190
+ 413, "request_too_large", upstream_message(terminal), {"x-should-retry": "false"}
191
+ )
176
192
  if status == 429:
177
- headers: dict[str, str] = {}
193
+ headers: dict[str, str] = {"x-should-retry": "true"}
178
194
  retry_after = getattr(terminal, "retry_after", None)
179
195
  if retry_after:
180
196
  headers["retry-after"] = retry_after
@@ -182,4 +198,7 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
182
198
  429, "rate_limit_error", upstream_message(terminal), headers
183
199
  )
184
200
 
185
- return AnthropicErrorResponse(502, "api_error", redact(str(exc)))
201
+ retry = any(e.retryable for e in errors)
202
+ return AnthropicErrorResponse(
203
+ 502, "api_error", redact(str(exc)), {"x-should-retry": "true" if retry else "false"}
204
+ )
@@ -31,6 +31,7 @@ from fastapi import APIRouter, Header, HTTPException, Request
31
31
  from fastapi.responses import JSONResponse, Response, StreamingResponse
32
32
 
33
33
  from coderouter.adapters.base import StreamTruncatedError
34
+ from coderouter.adapters.slot_affinity import affinity_key
34
35
  from coderouter.guards.context_budget import ContextBudgetRejectError
35
36
  from coderouter.guards.tool_loop import ToolCountExceededError, ToolLoopBreakError
36
37
  from coderouter.ingress.anthropic_errors import (
@@ -49,7 +50,7 @@ from coderouter.routing import (
49
50
  MidStreamError,
50
51
  NoProvidersAvailableError,
51
52
  )
52
- from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
53
+ from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify, extract_gateway_hints
53
54
  from coderouter.routing.fallback_trace import (
54
55
  SSE_FALLBACK_EVENT,
55
56
  current_fallback_trace,
@@ -226,6 +227,11 @@ async def messages(
226
227
  if anthropic_beta:
227
228
  anth_req.anthropic_beta = anthropic_beta
228
229
 
230
+ # v2.18.0: Claude Code gateway hint headers — routing matchers below,
231
+ # and the conversation key for llama-server KV-slot affinity.
232
+ gateway_hints = extract_gateway_hints(request.headers)
233
+ anth_req.session_key = affinity_key(gateway_hints)
234
+
229
235
  # Profile selection — body field wins over header (same policy as OpenAI route).
230
236
  if anth_req.profile is None and x_coderouter_profile:
231
237
  anth_req.profile = x_coderouter_profile
@@ -253,7 +259,7 @@ async def messages(
253
259
  # AnthropicRequest), so both OpenAI and Anthropic ingress use the same
254
260
  # classifier without a shared request shim.
255
261
  if anth_req.profile is None and config.default_profile == RESERVED_PROFILE_NAME:
256
- anth_req.profile = classify(payload, config)
262
+ anth_req.profile = classify(payload, config, gateway_hints)
257
263
 
258
264
  if anth_req.profile is not None:
259
265
  try:
@@ -276,7 +282,9 @@ async def messages(
276
282
  )
277
283
  if ts_action == "reject":
278
284
  return _anthropic_error_response(
279
- AnthropicErrorResponse(400, "invalid_request_error", REJECT_MESSAGE)
285
+ AnthropicErrorResponse(
286
+ 400, "invalid_request_error", REJECT_MESSAGE, {"x-should-retry": "false"}
287
+ )
280
288
  )
281
289
  if ts_action == "expand":
282
290
  anth_req = expand_tool_search(anth_req)
@@ -290,7 +298,9 @@ async def messages(
290
298
  # v2.16.0: ``context_budget_action: reject`` — Anthropic's own
291
299
  # overflow answer, so Claude Code compacts the conversation.
292
300
  return _anthropic_error_response(
293
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
301
+ AnthropicErrorResponse(
302
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
303
+ )
294
304
  )
295
305
 
296
306
  if anth_req.stream:
@@ -362,7 +372,9 @@ async def messages(
362
372
  # swap) — same answer as the pre-dispatch check above.
363
373
  await _aclose_quietly(source)
364
374
  return _anthropic_error_response(
365
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
375
+ AnthropicErrorResponse(
376
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
377
+ )
366
378
  )
367
379
  except (ToolLoopBreakError, ToolCountExceededError, MidStreamError) as exc:
368
380
  # Policy errors and mid-stream failures keep their existing
@@ -415,7 +427,9 @@ async def messages(
415
427
  except ContextBudgetRejectError as exc:
416
428
  # See the streaming branch: the engine can re-run the guard.
417
429
  return _anthropic_error_response(
418
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
430
+ AnthropicErrorResponse(
431
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
432
+ )
419
433
  )
420
434
  except NoProvidersAvailableError as exc:
421
435
  # v2.16.0: Anthropic-shaped error with the upstream's own wording
@@ -32,9 +32,10 @@ from fastapi import APIRouter, Header, HTTPException, Request
32
32
  from fastapi.responses import JSONResponse, StreamingResponse
33
33
 
34
34
  from coderouter.adapters.base import ChatRequest
35
+ from coderouter.adapters.slot_affinity import affinity_key
35
36
  from coderouter.logging import get_logger
36
37
  from coderouter.routing import FallbackEngine, NoProvidersAvailableError
37
- from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify
38
+ from coderouter.routing.auto_router import RESERVED_PROFILE_NAME, classify, extract_gateway_hints
38
39
  from coderouter.routing.fallback_trace import current_fallback_trace
39
40
 
40
41
  router = APIRouter()
@@ -199,6 +200,10 @@ async def chat_completions(
199
200
  except Exception as exc: # pydantic.ValidationError, etc.
200
201
  raise HTTPException(status_code=422, detail=str(exc)) from exc
201
202
 
203
+ # v2.18.0: gateway hint headers (routing matchers + KV-slot affinity key).
204
+ gateway_hints = extract_gateway_hints(request.headers)
205
+ chat_req.session_key = affinity_key(gateway_hints)
206
+
202
207
  # Header-based override (body wins if both are set — see module docstring)
203
208
  if chat_req.profile is None and x_coderouter_profile:
204
209
  chat_req.profile = x_coderouter_profile
@@ -226,7 +231,7 @@ async def chat_completions(
226
231
  # When inactive, the engine still falls through to
227
232
  # ``config.default_profile`` on its own — same semantics as pre-v1.6.
228
233
  if chat_req.profile is None and config.default_profile == RESERVED_PROFILE_NAME:
229
- chat_req.profile = classify(payload, config)
234
+ chat_req.profile = classify(payload, config, gateway_hints)
230
235
 
231
236
  # Validate profile exists before we kick off any upstream call
232
237
  if chat_req.profile is not None:
@@ -36,6 +36,7 @@ from __future__ import annotations
36
36
 
37
37
  import logging
38
38
  import re
39
+ from collections.abc import Mapping
39
40
  from typing import TYPE_CHECKING, Any
40
41
 
41
42
  from coderouter.config.schemas import AutoRouterConfig, AutoRouteRule, RuleMatcher
@@ -169,6 +170,32 @@ def _has_tools_in_body(body: dict[str, Any]) -> bool:
169
170
  return isinstance(functions, list) and len(functions) > 0
170
171
 
171
172
 
173
+ # v2.18.0: Claude Code gateway hint headers → hint keys.
174
+ _HINT_HEADERS: dict[str, str] = {
175
+ "x-claude-code-request-class": "request_class",
176
+ "x-claude-code-agent-type": "agent_type",
177
+ "x-claude-code-compaction": "compaction",
178
+ "x-claude-code-session-id": "session_id",
179
+ "x-claude-code-prompt-id": "prompt_id",
180
+ }
181
+ _TRUTHY = frozenset({"1", "true", "yes", "on"})
182
+
183
+
184
+ def extract_gateway_hints(headers: Mapping[str, str]) -> dict[str, str]:
185
+ """Pull Claude Code's gateway hint headers out of a request.
186
+
187
+ Returns only the hints that are present (lower-cased keys from
188
+ :data:`_HINT_HEADERS`). Header lookup is case-insensitive whenever the
189
+ mapping is (Starlette's ``Headers`` is).
190
+ """
191
+ out: dict[str, str] = {}
192
+ for header, key in _HINT_HEADERS.items():
193
+ value = headers.get(header)
194
+ if value:
195
+ out[key] = value.strip()
196
+ return out
197
+
198
+
172
199
  def _match_rule(
173
200
  rule: AutoRouteRule,
174
201
  message: dict[str, Any] | None,
@@ -176,8 +203,17 @@ def _match_rule(
176
203
  model: str | None,
177
204
  estimated_tokens: int,
178
205
  has_tools: bool,
206
+ hints: Mapping[str, str] | None = None,
179
207
  ) -> bool:
180
208
  m = rule.match
209
+ if m.request_class is not None:
210
+ value = (hints or {}).get("request_class")
211
+ return value is not None and value.lower() == m.request_class.lower()
212
+ if m.agent_type is not None:
213
+ value = (hints or {}).get("agent_type")
214
+ return value is not None and value.lower() == m.agent_type.lower()
215
+ if m.compaction is True:
216
+ return (hints or {}).get("compaction", "").lower() in _TRUTHY
181
217
  if m.has_image is True:
182
218
  return message is not None and _has_image(message)
183
219
  if m.code_fence_ratio_min is not None:
@@ -237,7 +273,11 @@ def _extract_model(body: dict[str, Any]) -> str | None:
237
273
  return None
238
274
 
239
275
 
240
- def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
276
+ def classify(
277
+ body: dict[str, Any],
278
+ config: CodeRouterConfig,
279
+ hints: Mapping[str, str] | None = None,
280
+ ) -> str:
241
281
  """Resolve an incoming request body to a profile name.
242
282
 
243
283
  Rule order (first match wins) is determined by:
@@ -252,6 +292,10 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
252
292
 
253
293
  Emits one of two log events: ``auto-router-resolved`` on match, or
254
294
  ``auto-router-fallthrough`` on default-rule fall.
295
+
296
+ v2.18.0: ``hints`` carries Claude Code's gateway hint headers (see
297
+ :func:`extract_gateway_hints`) for the ``request_class`` /
298
+ ``agent_type`` / ``compaction`` matchers.
255
299
  """
256
300
  user_msg = _latest_user_message(body)
257
301
  text = _extract_text(user_msg) if user_msg is not None else ""
@@ -294,7 +338,7 @@ def classify(body: dict[str, Any], config: CodeRouterConfig) -> str:
294
338
  # present — they short out via ``_match_rule``'s message-None
295
339
  # handling.
296
340
  for rule in rules:
297
- if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools):
341
+ if _match_rule(rule, user_msg, text, model, estimated_tokens, has_tools, hints):
298
342
  _emit_resolved(rule, user_msg, text, model, estimated_tokens, has_tools)
299
343
  return rule.profile
300
344
 
@@ -194,12 +194,13 @@ def strip_thinking(request: AnthropicRequest) -> AnthropicRequest:
194
194
 
195
195
  # model_dump() serializes extras; roundtripping via validate rebuilds a
196
196
  # clean instance without the dropped key. exclude=True fields (profile,
197
- # anthropic_beta) are omitted by model_dump, so we reassign them.
197
+ # anthropic_beta, session_key) are omitted by model_dump, so we reassign them.
198
198
  dumped = request.model_dump()
199
199
  dumped.pop("thinking", None)
200
200
  stripped = AnthropicRequest.model_validate(dumped)
201
201
  stripped.profile = request.profile
202
202
  stripped.anthropic_beta = request.anthropic_beta
203
+ stripped.session_key = request.session_key
203
204
  return stripped
204
205
 
205
206
 
@@ -450,6 +451,7 @@ def emulate_tool_choice(request: AnthropicRequest) -> AnthropicRequest:
450
451
  emulated = AnthropicRequest.model_validate(dumped)
451
452
  emulated.profile = request.profile
452
453
  emulated.anthropic_beta = request.anthropic_beta
454
+ emulated.session_key = request.session_key
453
455
  return emulated
454
456
 
455
457
 
@@ -522,6 +524,7 @@ def strip_cache_control(request: AnthropicRequest) -> tuple[AnthropicRequest, in
522
524
  stripped = AnthropicRequest.model_validate(dumped)
523
525
  stripped.profile = request.profile
524
526
  stripped.anthropic_beta = request.anthropic_beta
527
+ stripped.session_key = request.session_key
525
528
  return stripped, removed
526
529
 
527
530