steerable-agent-runtime 0.6.0__tar.gz → 0.6.2__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 (139) hide show
  1. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/PKG-INFO +1 -1
  2. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/pyproject.toml +1 -1
  3. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/compat.py +38 -3
  4. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/openai_compat.py +19 -9
  5. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime.egg-info/PKG-INFO +1 -1
  6. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_llm_wire_helpers.py +28 -0
  7. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_provider_compat.py +39 -0
  8. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/README.md +0 -0
  9. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/setup.cfg +0 -0
  10. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/__init__.py +0 -0
  11. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/antihallucination.py +0 -0
  12. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/approval.py +0 -0
  13. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/approval_policy.py +0 -0
  14. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/ask_user.py +0 -0
  15. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/branch.py +0 -0
  16. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/cache_control.py +0 -0
  17. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/calibration.py +0 -0
  18. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/compaction.py +0 -0
  19. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/config.py +0 -0
  20. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/default.harness.json +0 -0
  21. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/default.harness.yaml +0 -0
  22. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/errors.py +0 -0
  23. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/handoff.py +0 -0
  24. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/harness.py +0 -0
  25. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/harness_spec.py +0 -0
  26. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/history.py +0 -0
  27. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/hooks.py +0 -0
  28. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/__init__.py +0 -0
  29. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/anthropic_native.py +0 -0
  30. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/errors.py +0 -0
  31. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/parts.py +0 -0
  32. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/llm/system_proxy.py +0 -0
  33. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/loop.py +0 -0
  34. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/maintenance.py +0 -0
  35. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/mcp.py +0 -0
  36. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/mcp_server.py +0 -0
  37. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/model_catalog.py +0 -0
  38. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/model_info.py +0 -0
  39. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/model_resolve.py +0 -0
  40. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/observation_aging.py +0 -0
  41. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/orchestration.py +0 -0
  42. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/otel.py +0 -0
  43. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/plugins.py +0 -0
  44. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/pricing.py +0 -0
  45. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/pseudo.py +0 -0
  46. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/recording.py +0 -0
  47. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/reminders.py +0 -0
  48. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/replay.py +0 -0
  49. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/resume.py +0 -0
  50. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/retry.py +0 -0
  51. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/sandboxed.py +0 -0
  52. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/skills.py +0 -0
  53. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/spill.py +0 -0
  54. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/storage/__init__.py +0 -0
  55. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/storage/in_memory.py +0 -0
  56. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/storage/sqlalchemy_store.py +0 -0
  57. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/storage/sqlite_store.py +0 -0
  58. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/storage/write_lease.py +0 -0
  59. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/subagent.py +0 -0
  60. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/tokens.py +0 -0
  61. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/tool_schema.py +0 -0
  62. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/tool_search.py +0 -0
  63. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/tools.py +0 -0
  64. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/tracing.py +0 -0
  65. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/transport/__init__.py +0 -0
  66. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/transport/fastapi_sse.py +0 -0
  67. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/transport/stdio_jsonrpc.py +0 -0
  68. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime/world_state.py +0 -0
  69. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime.egg-info/SOURCES.txt +0 -0
  70. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime.egg-info/dependency_links.txt +0 -0
  71. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime.egg-info/requires.txt +0 -0
  72. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/src/steerable_agent_runtime.egg-info/top_level.txt +0 -0
  73. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_antihallucination.py +0 -0
  74. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_approval.py +0 -0
  75. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_approval_policy.py +0 -0
  76. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_ask_user.py +0 -0
  77. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_branch.py +0 -0
  78. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_cache_control.py +0 -0
  79. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_cache_instrumentation.py +0 -0
  80. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_calibration.py +0 -0
  81. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_compaction.py +0 -0
  82. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_config.py +0 -0
  83. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_content_parts.py +0 -0
  84. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_error_taxonomy.py +0 -0
  85. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_fragment_bounds.py +0 -0
  86. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_golden.py +0 -0
  87. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_handoff.py +0 -0
  88. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_harness.py +0 -0
  89. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_harness_spec.py +0 -0
  90. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_history.py +0 -0
  91. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_history_persistence.py +0 -0
  92. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_hooks.py +0 -0
  93. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_in_memory_storage.py +0 -0
  94. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_long_session.py +0 -0
  95. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_loop.py +0 -0
  96. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_loop_cancellation.py +0 -0
  97. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_loop_replay.py +0 -0
  98. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_loop_sandbox_event.py +0 -0
  99. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_maintenance.py +0 -0
  100. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_mcp.py +0 -0
  101. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_mcp_server.py +0 -0
  102. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_model_catalog.py +0 -0
  103. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_model_equivalence.py +0 -0
  104. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_model_info.py +0 -0
  105. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_model_resolve.py +0 -0
  106. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_observation_aging.py +0 -0
  107. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_orchestration.py +0 -0
  108. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_otel.py +0 -0
  109. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_parallel_tools.py +0 -0
  110. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_plugins.py +0 -0
  111. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_pseudo.py +0 -0
  112. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_recording.py +0 -0
  113. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_reminders.py +0 -0
  114. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_replay_crosslang.py +0 -0
  115. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_resume.py +0 -0
  116. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_retry_hooks.py +0 -0
  117. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_safety_gate.py +0 -0
  118. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_sandboxed.py +0 -0
  119. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_skills.py +0 -0
  120. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_soft_timeout.py +0 -0
  121. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_spill.py +0 -0
  122. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_sqlite_storage.py +0 -0
  123. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_steer.py +0 -0
  124. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_storage_contract.py +0 -0
  125. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_stream_strip.py +0 -0
  126. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_subagent.py +0 -0
  127. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_system_proxy.py +0 -0
  128. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tokens.py +0 -0
  129. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tool_exposure.py +0 -0
  130. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tool_hygiene.py +0 -0
  131. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tool_router.py +0 -0
  132. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tool_schema.py +0 -0
  133. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_tool_timeout.py +0 -0
  134. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_trace_recorder.py +0 -0
  135. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_transport_jsonrpc.py +0 -0
  136. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_transport_sse.py +0 -0
  137. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_usage_attribution.py +0 -0
  138. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_world_state.py +0 -0
  139. {steerable_agent_runtime-0.6.0 → steerable_agent_runtime-0.6.2}/tests/test_write_lease.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "steerable-agent-runtime"
3
- version = "0.6.0"
3
+ version = "0.6.2"
4
4
  description = "Steerable agent runtime: LLM, tool, storage, and transport adapters."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -57,6 +57,18 @@ _FLAG_WIRE_SPEC: tuple[tuple[str, str, str, str], ...] = (
57
57
  "string-list",
58
58
  "Delta keys read as reasoning text, in preference order",
59
59
  ),
60
+ (
61
+ "reasoningEchoField",
62
+ "reasoning_echo_field",
63
+ "string",
64
+ "Message key for echoing plaintext reasoning back to the vendor",
65
+ ),
66
+ (
67
+ "supportsForcedToolChoice",
68
+ "supports_forced_tool_choice",
69
+ "bool",
70
+ "Accept tool_choice=required; disable to downgrade it to auto",
71
+ ),
60
72
  (
61
73
  "cachedTokensFields",
62
74
  "cached_tokens_fields",
@@ -109,11 +121,23 @@ class OpenAICompatFlags:
109
121
  #: Whether ``temperature`` may be sent. Reasoning-only model families
110
122
  #: (o-series class) reject it.
111
123
  supports_temperature: bool = True
124
+ #: Whether ``tool_choice: "required"`` is accepted. Thinking-mode
125
+ #: gateways (DeepSeek live-verified 2026-09-08: "Thinking mode does not
126
+ #: support this tool_choice") 400 the forced value while plain ``auto``
127
+ #: works — flagging off downgrades ``required`` → ``auto`` on the wire.
128
+ supports_forced_tool_choice: bool = True
112
129
 
113
130
  # ── response shape ───────────────────────────────────────────────
114
131
  #: Delta keys read as reasoning text, in preference order. DeepSeek uses
115
132
  #: ``reasoning_content``; OpenRouter's GLM path uses ``reasoning``.
116
133
  reasoning_delta_fields: tuple[str, ...] = ("reasoning_content", "reasoning")
134
+ #: Message key for echoing plaintext reasoning back on the next request.
135
+ #: OpenRouter normalizes to ``reasoning`` (the reference default);
136
+ #: DeepSeek thinking mode *requires* the round-trip under its own
137
+ #: ``reasoning_content`` key — echoing as ``reasoning`` is an HTTP 400
138
+ #: ("The `reasoning_content` in the thinking mode must be passed back to
139
+ #: the API.", live-verified 2026-09-08 with deepseek-v4-flash).
140
+ reasoning_echo_field: str = "reasoning"
117
141
  #: Usage locations read for cached prompt tokens, in preference order.
118
142
  #: Dotted paths resolve nested objects. OpenAI nests under
119
143
  #: ``prompt_tokens_details.cached_tokens``; DeepSeek reports top-level
@@ -142,6 +166,8 @@ class OpenAICompatFlags:
142
166
  value = data[wire]
143
167
  if field in ("reasoning_delta_fields", "cached_tokens_fields"):
144
168
  kwargs[field] = tuple(str(v) for v in value)
169
+ elif field == "reasoning_echo_field":
170
+ kwargs[field] = str(value)
145
171
  elif field == "max_tokens_field":
146
172
  if value not in _MAX_TOKENS_FIELDS:
147
173
  raise ValueError(
@@ -162,13 +188,22 @@ class OpenAICompatFlags:
162
188
  #: as data — adding a vendor must not touch request-building or parsing.
163
189
  PROVIDER_COMPAT_HOSTS: list[tuple[str, OpenAICompatFlags]] = [
164
190
  # DeepSeek: reasoning arrives as ``reasoning_content`` and cache hits as
165
- # top-level ``prompt_cache_hit_tokens``. The tolerant defaults already
166
- # cover both — this entry pins that coverage as data so a future default
167
- # change cannot silently regress it.
191
+ # top-level ``prompt_cache_hit_tokens``. Thinking mode additionally
192
+ # REQUIRES the reasoning round-trip under ``reasoning_content`` — the
193
+ # reference echo key ``reasoning`` is ignored and the follow-up request
194
+ # 400s (live-verified 2026-09-08, deepseek-v4-flash tool-turn: "The
195
+ # `reasoning_content` in the thinking mode must be passed back to the
196
+ # API."). The tolerant delta-field defaults already cover parsing; the
197
+ # echo key is the divergence pinned here.
168
198
  (
169
199
  "api.deepseek.com",
170
200
  OpenAICompatFlags(
171
201
  reasoning_delta_fields=("reasoning_content", "reasoning"),
202
+ reasoning_echo_field="reasoning_content",
203
+ # Thinking mode 400s ``tool_choice="required"`` ("Thinking mode
204
+ # does not support this tool_choice", live-verified 2026-09-08
205
+ # with deepseek-v4-flash); ``auto`` and omission both work.
206
+ supports_forced_tool_choice=False,
172
207
  cached_tokens_fields=(
173
208
  "prompt_cache_hit_tokens",
174
209
  "prompt_tokens_details.cached_tokens",
@@ -312,7 +312,7 @@ class OpenAICompatProvider:
312
312
  compat = self.compat or OpenAICompatFlags()
313
313
  body: dict[str, Any] = {
314
314
  "model": self.model,
315
- "messages": [_encode_message(m) for m in messages],
315
+ "messages": [_encode_message(m, compat=compat) for m in messages],
316
316
  "stream": stream,
317
317
  }
318
318
  if stream and compat.supports_usage_in_streaming:
@@ -331,11 +331,16 @@ class OpenAICompatProvider:
331
331
  if tools_list:
332
332
  body["tools"] = tools_list
333
333
  body.update(extra)
334
- # Z.AI (direct or OpenRouter pin) 400s ``tool_choice=required``.
335
- # Harbor still logs the hook; the wire must send auto or the trial
336
- # dies on round 0 (failed-prev 33335200327).
337
- if body.get("tool_choice") == "required" and _z_ai_tool_choice_auto_only(
338
- self.model, self.base_url
334
+ # ``tool_choice=required`` downgrade paths:
335
+ # - compat flag: vendors whose thinking mode 400s the forced value
336
+ # (DeepSeek: "Thinking mode does not support this tool_choice");
337
+ # - Z.AI (direct or OpenRouter pin) 400s it outright — model-name
338
+ # based, which host flags can't cover. Harbor still logs the hook;
339
+ # the wire must send auto or the trial dies on round 0
340
+ # (failed-prev 33335200327).
341
+ if body.get("tool_choice") == "required" and (
342
+ not compat.supports_forced_tool_choice
343
+ or _z_ai_tool_choice_auto_only(self.model, self.base_url)
339
344
  ):
340
345
  body["tool_choice"] = "auto"
341
346
  # W6-8: clamp the env-requested reasoning effort to a level the model
@@ -476,7 +481,10 @@ def _encode_content(message: LLMMessage) -> str | list[dict[str, Any]]:
476
481
  return out
477
482
 
478
483
 
479
- def _encode_message(message: LLMMessage) -> dict[str, Any]:
484
+ def _encode_message(
485
+ message: LLMMessage, *, compat: OpenAICompatFlags | None = None
486
+ ) -> dict[str, Any]:
487
+ flags = compat or OpenAICompatFlags()
480
488
  out: dict[str, Any] = {"role": message.role, "content": _encode_content(message)}
481
489
  if message.name is not None:
482
490
  out["name"] = message.name
@@ -495,11 +503,13 @@ def _encode_message(message: LLMMessage) -> dict[str, Any]:
495
503
  for tc in message.tool_calls
496
504
  ]
497
505
  # OpenRouter: echo reasoning_details unmodified so GLM continues after
498
- # tools. Prefer the structured block; plaintext is the fallback.
506
+ # tools. Prefer the structured block; plaintext is the fallback. The
507
+ # plaintext echo key is compat data: DeepSeek thinking mode 400s unless
508
+ # the round-trip uses its own ``reasoning_content`` field.
499
509
  if message.reasoning_details:
500
510
  out["reasoning_details"] = message.reasoning_details
501
511
  elif message.reasoning:
502
- out["reasoning"] = message.reasoning
512
+ out[flags.reasoning_echo_field] = message.reasoning
503
513
  return out
504
514
 
505
515
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: steerable-agent-runtime
3
- Version: 0.6.0
3
+ Version: 0.6.2
4
4
  Summary: Steerable agent runtime: LLM, tool, storage, and transport adapters.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -154,6 +154,34 @@ def test_openai_parse_and_encode_reasoning_details() -> None:
154
154
  assert "reasoning_details" not in plaintext
155
155
 
156
156
 
157
+ def test_openai_encode_reasoning_echo_field_from_compat() -> None:
158
+ """DeepSeek thinking mode 400s unless reasoning round-trips under its own
159
+ ``reasoning_content`` key (live-verified 2026-09-08, deepseek-v4-flash);
160
+ the echo field name is compat data, not a hardcoded constant."""
161
+ from steerable_agent_runtime.llm import OpenAICompatFlags, compat_for_base_url
162
+
163
+ msg = LLMMessage.text_of("assistant", "", reasoning="think step")
164
+
165
+ deepseek = compat_for_base_url("https://api.deepseek.com")
166
+ assert deepseek is not None
167
+ encoded = _encode_message(msg, compat=deepseek)
168
+ assert encoded["reasoning_content"] == "think step"
169
+ assert "reasoning" not in encoded
170
+
171
+ # Reference default (and OpenRouter) keep the ``reasoning`` echo key.
172
+ default = _encode_message(msg)
173
+ assert default["reasoning"] == "think step"
174
+ assert "reasoning_content" not in default
175
+
176
+ openrouter = compat_for_base_url("https://openrouter.ai/api/v1")
177
+ assert openrouter is not None
178
+ assert _encode_message(msg, compat=openrouter)["reasoning"] == "think step"
179
+
180
+ # Host-passed camelCase payload builds the same flags.
181
+ from_dict = OpenAICompatFlags.from_dict({"reasoningEchoField": "reasoning_content"})
182
+ assert _encode_message(msg, compat=from_dict)["reasoning_content"] == "think step"
183
+
184
+
157
185
  def test_openai_parse_stream_chunk_usage_only() -> None:
158
186
  chunk = {
159
187
  "choices": [],
@@ -96,6 +96,45 @@ def test_temperature_unsupported_suppresses_field() -> None:
96
96
  assert "temperature" not in body
97
97
 
98
98
 
99
+ def test_forced_tool_choice_downgraded_when_flagged_off() -> None:
100
+ """DeepSeek thinking mode 400s ``tool_choice="required"`` ("Thinking mode
101
+ does not support this tool_choice", live-verified 2026-09-08 with
102
+ deepseek-v4-flash); the flag downgrades it to ``auto`` on the wire."""
103
+ from steerable_agent_runtime.llm import LLMMessage
104
+
105
+ provider = _provider(compat=OpenAICompatFlags(supports_forced_tool_choice=False))
106
+ body = provider._build_body(
107
+ messages=[LLMMessage.text_of("user", "hi")],
108
+ tools=None,
109
+ temperature=None,
110
+ max_tokens=None,
111
+ stream=True,
112
+ extra={"tool_choice": "required"},
113
+ )
114
+ assert body["tool_choice"] == "auto"
115
+
116
+
117
+ def test_forced_tool_choice_kept_by_default() -> None:
118
+ from steerable_agent_runtime.llm import LLMMessage
119
+
120
+ provider = _provider()
121
+ body = provider._build_body(
122
+ messages=[LLMMessage.text_of("user", "hi")],
123
+ tools=None,
124
+ temperature=None,
125
+ max_tokens=None,
126
+ stream=True,
127
+ extra={"tool_choice": "required"},
128
+ )
129
+ assert body["tool_choice"] == "required"
130
+
131
+
132
+ def test_registry_deepseek_disables_forced_tool_choice() -> None:
133
+ entry = compat_for_base_url("https://api.deepseek.com")
134
+ assert entry is not None
135
+ assert entry.supports_forced_tool_choice is False
136
+
137
+
99
138
  # ---------------------------------------------------------------------------
100
139
  # Response-shape flags
101
140
  # ---------------------------------------------------------------------------