@oneciel-ai/ciel-runtime 0.2.2 → 0.2.3

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 (126) hide show
  1. package/ciel_runtime.py +2553 -9635
  2. package/ciel_runtime_support/advisor_request_builder.py +8 -21
  3. package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
  4. package/ciel_runtime_support/architecture.py +68 -0
  5. package/ciel_runtime_support/architecture_budget.py +1 -1
  6. package/ciel_runtime_support/channel_connection_context.py +233 -0
  7. package/ciel_runtime_support/channel_delivery_context.py +332 -0
  8. package/ciel_runtime_support/channel_mcp_context.py +313 -0
  9. package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
  10. package/ciel_runtime_support/channel_mcp_transport.py +5 -1
  11. package/ciel_runtime_support/channel_message_context.py +212 -0
  12. package/ciel_runtime_support/channel_message_repository.py +14 -3
  13. package/ciel_runtime_support/channel_pending_injection.py +9 -0
  14. package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
  15. package/ciel_runtime_support/channel_replay_policy.py +38 -0
  16. package/ciel_runtime_support/channel_runtime_environment.py +8 -0
  17. package/ciel_runtime_support/channel_session_context.py +236 -0
  18. package/ciel_runtime_support/channel_terminal_context.py +350 -0
  19. package/ciel_runtime_support/channel_wake_context.py +532 -0
  20. package/ciel_runtime_support/claude_environment.py +60 -0
  21. package/ciel_runtime_support/claude_launch_assembly.py +249 -0
  22. package/ciel_runtime_support/claude_router.py +62 -12
  23. package/ciel_runtime_support/cli_application_context.py +132 -0
  24. package/ciel_runtime_support/cli_assembly.py +50 -0
  25. package/ciel_runtime_support/codex_backend_context.py +363 -0
  26. package/ciel_runtime_support/codex_config.py +13 -1
  27. package/ciel_runtime_support/codex_launch_assembly.py +213 -0
  28. package/ciel_runtime_support/codex_launch_configuration.py +30 -1
  29. package/ciel_runtime_support/codex_mcp_integration.py +90 -8
  30. package/ciel_runtime_support/codex_model_catalog.py +4 -1
  31. package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
  32. package/ciel_runtime_support/codex_router.py +38 -8
  33. package/ciel_runtime_support/codex_turn_recovery.py +154 -0
  34. package/ciel_runtime_support/config_migrations.py +103 -0
  35. package/ciel_runtime_support/configuration_cli.py +38 -0
  36. package/ciel_runtime_support/context_compaction.py +9 -4
  37. package/ciel_runtime_support/credential_management.py +12 -0
  38. package/ciel_runtime_support/credentials.py +12 -0
  39. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  40. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  41. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  42. package/ciel_runtime_support/llm_preset_context.py +338 -0
  43. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  44. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  45. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  46. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  47. package/ciel_runtime_support/mcp_transport.py +14 -1
  48. package/ciel_runtime_support/native_context_recovery.py +72 -0
  49. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  50. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  51. package/ciel_runtime_support/ollama_thinking.py +6 -1
  52. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  53. package/ciel_runtime_support/openai_forwarding.py +32 -10
  54. package/ciel_runtime_support/openai_responses_router.py +12 -0
  55. package/ciel_runtime_support/package_lifecycle.py +39 -0
  56. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  57. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  58. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  59. package/ciel_runtime_support/prompt_compaction.py +144 -0
  60. package/ciel_runtime_support/prompt_injection.py +45 -0
  61. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  62. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  63. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  64. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  65. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  66. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  67. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  68. package/ciel_runtime_support/provider_adapters.py +26 -0
  69. package/ciel_runtime_support/provider_administration_context.py +207 -0
  70. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  71. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  72. package/ciel_runtime_support/provider_model_context.py +107 -0
  73. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  74. package/ciel_runtime_support/provider_model_selection.py +10 -3
  75. package/ciel_runtime_support/provider_models.py +45 -2
  76. package/ciel_runtime_support/provider_option_cli.py +19 -0
  77. package/ciel_runtime_support/provider_policy.py +1 -1
  78. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  79. package/ciel_runtime_support/provider_request_builder.py +64 -28
  80. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  81. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  82. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  83. package/ciel_runtime_support/providers/__init__.py +6 -0
  84. package/ciel_runtime_support/providers/alibaba.py +634 -0
  85. package/ciel_runtime_support/providers/catalog.py +24 -16
  86. package/ciel_runtime_support/providers/deepseek.py +73 -0
  87. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  88. package/ciel_runtime_support/providers/kimi.py +69 -9
  89. package/ciel_runtime_support/providers/ollama.py +8 -0
  90. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  91. package/ciel_runtime_support/providers/vllm.py +7 -1
  92. package/ciel_runtime_support/response_collection.py +68 -18
  93. package/ciel_runtime_support/response_collection_context.py +391 -0
  94. package/ciel_runtime_support/response_stream_context.py +555 -0
  95. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  96. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  97. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  98. package/ciel_runtime_support/router_http.py +239 -3
  99. package/ciel_runtime_support/router_observability_context.py +251 -0
  100. package/ciel_runtime_support/router_process_context.py +200 -0
  101. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  102. package/ciel_runtime_support/router_request_assembly.py +399 -0
  103. package/ciel_runtime_support/router_request_context.py +215 -0
  104. package/ciel_runtime_support/router_server_context.py +82 -0
  105. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  106. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  107. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  108. package/ciel_runtime_support/runtime_constants.py +16 -1
  109. package/ciel_runtime_support/runtime_launch.py +9 -5
  110. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  111. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  112. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  113. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  114. package/ciel_runtime_support/runtime_paths.py +60 -40
  115. package/ciel_runtime_support/runtime_primitives.py +78 -0
  116. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  117. package/ciel_runtime_support/statusline_script.py +57 -8
  118. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  119. package/ciel_runtime_support/tool_schema.py +40 -2
  120. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  121. package/ciel_runtime_support/upstream_dump.py +68 -0
  122. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  123. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  124. package/docs/Configuration.md +50 -0
  125. package/docs/Test-Suite.md +1 -0
  126. package/package.json +1 -1
@@ -16,6 +16,7 @@ from ..architecture import (
16
16
  ProviderRequestPolicy,
17
17
  )
18
18
  from ..runtime_constants import DEFAULT_REQUEST_TIMEOUT_MS
19
+ from .alibaba import ALIBABA_CODING_PLAN_MODELS
19
20
  from .base import OpenAICompatibleProviderAdapter, provider_configuration
20
21
 
21
22
 
@@ -30,8 +31,8 @@ class CompatibleProviderSpec:
30
31
  aliases: tuple[str, ...] = ()
31
32
  chat_path: str = "/v1/chat/completions"
32
33
  models_path: str = "/v1/models"
34
+ anthropic_url: str = ""
33
35
  authorization_header: str = "Authorization"
34
- include_x_api_key: bool = False
35
36
  requires_api_key: bool = True
36
37
 
37
38
 
@@ -52,7 +53,7 @@ class CatalogOpenAIProviderAdapter(OpenAICompatibleProviderAdapter):
52
53
  configuration_defaults_value=provider_configuration(
53
54
  default_model,
54
55
  custom_models=spec.models,
55
- native_compat=False,
56
+ native_compat=bool(spec.anthropic_url),
56
57
  context_window=131072,
57
58
  max_output_tokens=8192,
58
59
  context_reserve_tokens=4096,
@@ -61,7 +62,7 @@ class CatalogOpenAIProviderAdapter(OpenAICompatibleProviderAdapter):
61
62
  stream_word_chunking=False,
62
63
  ),
63
64
  authorization_header=spec.authorization_header,
64
- include_x_api_key=spec.include_x_api_key,
65
+ include_x_api_key=False,
65
66
  require_api_key=spec.requires_api_key,
66
67
  api_key_display_name_value=spec.label,
67
68
  api_key_launch_error_value=(
@@ -97,16 +98,25 @@ class CatalogOpenAIProviderAdapter(OpenAICompatibleProviderAdapter):
97
98
  config: ProviderConfig,
98
99
  model: str | None = None,
99
100
  ) -> bool:
100
- del config, model
101
- return False
101
+ del model
102
+ return bool(self.spec.anthropic_url) and bool(
103
+ config.options.get("native_compat", True)
104
+ )
105
+
106
+ def anthropic_base_url(self, config: ProviderConfig) -> str:
107
+ del config
108
+ return self.spec.anthropic_url or self.default_base_url()
102
109
 
103
110
  def supported_protocols(
104
111
  self,
105
112
  config: ProviderConfig,
106
113
  model: str | None = None,
107
114
  ) -> frozenset[MessageProtocol]:
108
- del config, model
109
- return frozenset({"openai_chat"})
115
+ del model
116
+ protocols: set[MessageProtocol] = {"openai_chat"}
117
+ if self.router_native_anthropic_enabled(config):
118
+ protocols.add("anthropic_messages")
119
+ return frozenset(protocols)
110
120
 
111
121
  def select_protocol(
112
122
  self,
@@ -114,7 +124,9 @@ class CatalogOpenAIProviderAdapter(OpenAICompatibleProviderAdapter):
114
124
  config: ProviderConfig,
115
125
  model: str | None = None,
116
126
  ) -> MessageProtocol:
117
- del operation, config, model
127
+ del model
128
+ if operation == "anthropic_messages" and self.router_native_anthropic_enabled(config):
129
+ return "anthropic_messages"
118
130
  return "openai_chat"
119
131
 
120
132
 
@@ -130,19 +142,15 @@ COMPATIBLE_PROVIDER_SPECS: tuple[CompatibleProviderSpec, ...] = (
130
142
  CompatibleProviderSpec(
131
143
  "alicode-intl", "Alibaba Coding International",
132
144
  "https://coding-intl.dashscope.aliyuncs.com/v1",
133
- ("qwen3.5-plus", "kimi-k2.5", "glm-5", "qwen3-coder-next"),
145
+ ALIBABA_CODING_PLAN_MODELS,
134
146
  ("alibaba-coding-intl",),
147
+ anthropic_url="https://coding-intl.dashscope.aliyuncs.com/apps/anthropic",
135
148
  ),
136
149
  CompatibleProviderSpec(
137
150
  "alicode", "Alibaba Coding", "https://coding.dashscope.aliyuncs.com/v1",
138
- ("qwen3.5-plus", "kimi-k2.5", "glm-5", "qwen3-coder-next"),
151
+ ALIBABA_CODING_PLAN_MODELS,
139
152
  ("alibaba-coding",),
140
- ),
141
- CompatibleProviderSpec(
142
- "alims-intl", "Alibaba Model Studio International",
143
- "https://dashscope-intl.aliyuncs.com/compatible-mode/v1",
144
- ("qwen3.5-plus", "kimi-k2.5", "glm-5", "qwen3-coder-next"),
145
- ("dashscope-intl",),
153
+ anthropic_url="https://coding.dashscope.aliyuncs.com/apps/anthropic",
146
154
  ),
147
155
  CompatibleProviderSpec(
148
156
  "blackbox", "Blackbox AI", "https://api.blackbox.ai/v1",
@@ -1,6 +1,7 @@
1
1
  """DeepSeek provider adapter."""
2
2
 
3
3
  from dataclasses import dataclass, field
4
+ from typing import Any, Mapping
4
5
 
5
6
  from ..architecture import (
6
7
  ProviderCapabilities,
@@ -24,6 +25,12 @@ class DeepSeekProviderAdapter(HttpBearerProviderAdapter):
24
25
  "deepseek-v4-pro[1m]",
25
26
  custom_models=("deepseek-v4-pro[1m]", "deepseek-v4-flash"),
26
27
  native_compat=True,
28
+ claude_code_supported_capabilities=[
29
+ "effort",
30
+ "max_effort",
31
+ "thinking",
32
+ "interleaved_thinking",
33
+ ],
27
34
  context_window=1048576,
28
35
  max_output_tokens=8192,
29
36
  context_reserve_tokens=8192,
@@ -111,5 +118,71 @@ class DeepSeekProviderAdapter(HttpBearerProviderAdapter):
111
118
  not in str(model or config.model or "").split("[", 1)[0].lower()
112
119
  )
113
120
 
121
+ @staticmethod
122
+ def _normalized_effort(value: Any) -> str:
123
+ effort = str(value or "").strip().lower()
124
+ if effort in {"max", "xhigh"}:
125
+ return "max"
126
+ return "high"
127
+
128
+ def normalize_request_options(
129
+ self, config: ProviderConfig, request: Mapping[str, Any]
130
+ ) -> Mapping[str, Any]:
131
+ """Honor DeepSeek's native thinking contract on Anthropic requests."""
132
+
133
+ del config
134
+ normalized = dict(request)
135
+ thinking = request.get("thinking")
136
+ thinking_disabled = isinstance(thinking, Mapping) and str(
137
+ thinking.get("type") or ""
138
+ ).strip().lower() in {"disabled", "none", "off", "false"}
139
+ if not thinking_disabled:
140
+ for key in (
141
+ "temperature",
142
+ "top_p",
143
+ "top_k",
144
+ "presence_penalty",
145
+ "frequency_penalty",
146
+ ):
147
+ normalized.pop(key, None)
148
+ output_config = request.get("output_config")
149
+ if thinking_disabled:
150
+ normalized.pop("output_config", None)
151
+ output_config = None
152
+ elif not isinstance(output_config, Mapping) and isinstance(thinking, Mapping):
153
+ if thinking.get("effort") is not None:
154
+ output_config = {"effort": thinking.get("effort")}
155
+ if isinstance(output_config, Mapping) and output_config.get("effort") is not None:
156
+ normalized["output_config"] = {
157
+ **output_config,
158
+ "effort": self._normalized_effort(output_config.get("effort")),
159
+ }
160
+ if isinstance(thinking, Mapping) and "effort" in thinking:
161
+ normalized["thinking"] = {
162
+ key: value for key, value in thinking.items() if key != "effort"
163
+ }
164
+ return normalized
165
+
166
+ def openai_reasoning_passback_enabled(
167
+ self, config: ProviderConfig, model: str | None = None
168
+ ) -> bool:
169
+ del config
170
+ normalized = str(model or "").split("[", 1)[0].lower()
171
+ return normalized.startswith(("deepseek-v4-", "deepseek-reasoner"))
172
+
173
+ def openai_reasoning_effort(
174
+ self, config: ProviderConfig, model: str, request: Mapping[str, Any]
175
+ ) -> str | None:
176
+ del model
177
+ metadata = request.get("metadata")
178
+ hinted = metadata.get("ciel_runtime_reasoning_effort") if isinstance(metadata, Mapping) else None
179
+ return self._normalized_effort(hinted or config.options.get("effort_level") or "high")
180
+
181
+ def allows_sampling_overrides(self, config: ProviderConfig) -> bool:
182
+ # Thinking is enabled by default. Do not inject configured sampling
183
+ # values that DeepSeek documents as ineffective in that mode.
184
+ del config
185
+ return False
186
+
114
187
 
115
188
  __all__ = ["DeepSeekProviderAdapter"]
@@ -26,12 +26,32 @@ from .base import HttpBearerProviderAdapter, provider_configuration
26
26
 
27
27
  GITHUB_COPILOT_BASE_URL = "https://api.githubcopilot.com"
28
28
  GITHUB_COPILOT_MODELS = (
29
+ "gpt-5.6-sol",
30
+ "gpt-5.6-terra",
31
+ "gpt-5.6-luna",
32
+ "gpt-5.5",
29
33
  "gpt-5.4",
34
+ "gpt-5.4-mini",
35
+ "gpt-5.4-nano",
30
36
  "gpt-5.3-codex",
31
- "gpt-5.2-codex",
37
+ "gpt-5-mini",
38
+ "claude-fable-5",
39
+ "claude-sonnet-5",
40
+ "claude-opus-5",
41
+ "claude-opus-4.8",
42
+ "claude-opus-4.7",
32
43
  "claude-sonnet-4.6",
33
44
  "claude-opus-4.6",
45
+ "claude-sonnet-4.5",
46
+ "claude-opus-4.5",
47
+ "claude-haiku-4.5",
48
+ "gemini-3.6-flash",
49
+ "gemini-3.5-flash",
34
50
  "gemini-3.1-pro-preview",
51
+ "kimi-k2.7-code",
52
+ "mai-code-1-flash",
53
+ "raptor-mini",
54
+ "grok-4.5",
35
55
  )
36
56
 
37
57
 
@@ -78,6 +98,7 @@ class GitHubCopilotOAuthProviderAdapter(HttpBearerProviderAdapter):
78
98
  kind="openai",
79
99
  fallback_models=GITHUB_COPILOT_MODELS,
80
100
  allow_configured_fallback=True,
101
+ authoritative_upstream_catalog=True,
81
102
  )
82
103
  )
83
104
 
@@ -6,6 +6,7 @@ from typing import Any, Mapping
6
6
 
7
7
  from ..architecture import (
8
8
  MessageProtocol,
9
+ HostedToolPolicy,
9
10
  ProviderCapabilities,
10
11
  ProviderConfig,
11
12
  ProviderContextPolicy,
@@ -40,7 +41,9 @@ class KimiProviderAdapter(HttpBearerProviderAdapter):
40
41
  request_timeout_ms=600000,
41
42
  stream_enabled=True,
42
43
  stream_word_chunking=False,
43
- effort_level="high",
44
+ effort_level="max",
45
+ official_tools_enabled=True,
46
+ official_tools=["web-search", "fetch"],
44
47
  haiku_model="kimi-for-coding",
45
48
  subagent_model="kimi-for-coding",
46
49
  )
@@ -155,10 +158,10 @@ class KimiProviderAdapter(HttpBearerProviderAdapter):
155
158
  {
156
159
  "context_window": context,
157
160
  "max_model_len": context,
158
- "effort_level": "high",
161
+ "effort_level": "max",
159
162
  "model_profile": "kimi-k3-1m",
160
163
  },
161
- "Kimi K3 profile applied: 1M context and high reasoning effort. "
164
+ "Kimi K3 profile applied: 1M context and max reasoning effort. "
162
165
  "Start a new session after changing model, context, or reasoning effort.",
163
166
  )
164
167
 
@@ -218,7 +221,15 @@ class KimiProviderAdapter(HttpBearerProviderAdapter):
218
221
  config: ProviderConfig,
219
222
  model: str | None = None,
220
223
  ) -> MessageProtocol:
221
- del config, model
224
+ normalized_model = self.normalize_model_id(
225
+ str(model or config.model or "")
226
+ )
227
+ if (
228
+ operation == "anthropic_messages"
229
+ and normalized_model in {"k3", "k3[1m]"}
230
+ and self.hosted_tool_policy(config).formulas
231
+ ):
232
+ return "openai_chat"
222
233
  return (
223
234
  "openai_chat"
224
235
  if operation in {"openai_chat", "openai_responses"}
@@ -266,20 +277,69 @@ class KimiProviderAdapter(HttpBearerProviderAdapter):
266
277
  requested = config.options.get("effort_level")
267
278
  return self._reasoning_effort(requested)
268
279
 
280
+ def openai_reasoning_passback_enabled(
281
+ self, config: ProviderConfig, model: str | None = None
282
+ ) -> bool:
283
+ del config
284
+ normalized = self.normalize_model_id(str(model or ""))
285
+ return normalized in {
286
+ "k3",
287
+ "k3[1m]",
288
+ "kimi-for-coding",
289
+ "kimi-for-coding-highspeed",
290
+ }
291
+
292
+ def hosted_tool_policy(self, config: ProviderConfig) -> HostedToolPolicy:
293
+ if config.options.get("official_tools_enabled", True) is False:
294
+ return HostedToolPolicy()
295
+ configured = config.options.get("official_tools", ("web-search", "fetch"))
296
+ if configured is False or configured is None:
297
+ return HostedToolPolicy()
298
+ if isinstance(configured, str):
299
+ names = tuple(part.strip() for part in configured.split(",") if part.strip())
300
+ elif isinstance(configured, (list, tuple)):
301
+ names = tuple(str(part).strip() for part in configured if str(part).strip())
302
+ else:
303
+ names = ("web-search", "fetch") if configured is True else ()
304
+ formulas = tuple(
305
+ name if "/" in name else f"moonshot/{name}:latest" for name in names
306
+ )
307
+ base_url = str(
308
+ config.options.get("official_tools_base_url")
309
+ or "https://api.moonshot.ai/v1"
310
+ ).rstrip("/")
311
+ return HostedToolPolicy(base_url=base_url, formulas=formulas)
312
+
313
+ def should_omit_openai_tool_choice(
314
+ self,
315
+ config: ProviderConfig,
316
+ model: str | None,
317
+ request: Mapping[str, Any],
318
+ ) -> bool:
319
+ if not self.openai_reasoning_passback_enabled(config, model):
320
+ return False
321
+ choice = request.get("tool_choice")
322
+ return bool(
323
+ isinstance(choice, Mapping)
324
+ and str(choice.get("type") or "").strip().lower() == "tool"
325
+ )
326
+
269
327
  def allows_sampling_overrides(self, config: ProviderConfig) -> bool:
270
328
  del config
271
329
  return False
272
330
 
273
331
  @staticmethod
274
332
  def _reasoning_effort(value: Any) -> str:
275
- effort = str(value or "high").strip().lower()
276
- if effort == "ultra":
333
+ effort = str(value or "max").strip().lower()
334
+ if effort in {"xhigh", "ultra", "max"}:
277
335
  return "max"
278
336
  if effort in {"minimum", "light"}:
279
337
  return "low"
280
- if effort in {"low", "medium", "high", "xhigh", "max"}:
338
+ if effort == "medium":
339
+ return "high"
340
+ if effort in {"low", "high"}:
281
341
  return effort
282
- return "high"
342
+ return "max"
283
343
 
284
344
  def normalize_tool_choice(
285
345
  self, config: ProviderConfig, model: str, tool_choice: Any
@@ -289,7 +349,7 @@ class KimiProviderAdapter(HttpBearerProviderAdapter):
289
349
  tool_choice, Mapping
290
350
  ):
291
351
  return tool_choice
292
- if str(tool_choice.get("type") or "").strip().lower() in {"any", "tool"}:
352
+ if str(tool_choice.get("type") or "").strip().lower() == "tool":
293
353
  return {"type": "auto"}
294
354
  return tool_choice
295
355
 
@@ -84,6 +84,14 @@ class OllamaProviderAdapter(HttpBearerProviderAdapter):
84
84
  ) -> bool | str | None:
85
85
  return OllamaThinkingPolicy().value(config.options, model, request)
86
86
 
87
+ def reasoning_passback_enabled(
88
+ self, config: ProviderConfig, model: str | None = None
89
+ ) -> bool:
90
+ """Keep native Ollama thinking blocks so multi-turn tool history remains valid."""
91
+
92
+ del config, model
93
+ return True
94
+
87
95
  def option_presentation_policy(
88
96
  self, config: ProviderConfig
89
97
  ) -> ProviderOptionPresentationPolicy:
@@ -179,10 +179,29 @@ class OllamaRequestContextPolicy:
179
179
  )
180
180
  options = dict(self.extra_options(retry_config))
181
181
  configured_num_predict = self.positive_int(options.get("num_predict"))
182
- if configured_num_predict:
182
+ output_is_explicit = bool(config.get("output_tokens_explicit")) or (
183
+ "num_predict"
184
+ in {
185
+ str(item)
186
+ for item in config.get("ollama_explicit_options") or []
187
+ }
188
+ )
189
+ if configured_num_predict and output_is_explicit:
183
190
  options["num_predict"] = min(configured_num_predict, output_cap)
191
+ else:
192
+ options.pop("num_predict", None)
184
193
  retry_config["ollama_options"] = options
185
- retry_config["ollama_transient_options"] = ["num_predict"]
194
+ transient = {
195
+ str(item)
196
+ for item in retry_config.get("ollama_transient_options") or []
197
+ if str(item) != "num_predict"
198
+ }
199
+ if output_is_explicit:
200
+ transient.add("num_predict")
201
+ if transient:
202
+ retry_config["ollama_transient_options"] = sorted(transient)
203
+ else:
204
+ retry_config.pop("ollama_transient_options", None)
186
205
  return retry_config
187
206
 
188
207
  def context_limit_for_budget(self, config: dict[str, Any]) -> int:
@@ -1,6 +1,7 @@
1
1
  """vLLM provider adapter."""
2
2
 
3
3
  from dataclasses import dataclass, field, replace
4
+ from typing import Literal
4
5
 
5
6
  from ..architecture import (
6
7
  ProviderCapabilities,
@@ -39,11 +40,16 @@ class VllmProviderAdapter(OpenAICompatibleProviderAdapter):
39
40
  upstream_protocol="openai_chat", supports_tool_choice=False, local=True
40
41
  )
41
42
  )
42
-
43
43
  def requires_catalog_model_selection(self, config: ProviderConfig) -> bool:
44
44
  del config
45
45
  return True
46
46
 
47
+ def anthropic_system_role_strategy(
48
+ self, config: ProviderConfig
49
+ ) -> Literal["hoist_top_level"]:
50
+ del config
51
+ return "hoist_top_level"
52
+
47
53
  def placeholder_model_ids(self) -> frozenset[str]:
48
54
  return super().placeholder_model_ids() | {"my-model"}
49
55
 
@@ -15,6 +15,10 @@ class ChatCollectionStrategy:
15
15
  request_timeout_seconds: Callable[..., float]
16
16
  normalize_upstream_model: Callable[..., str]
17
17
  skip_rate_limit_during_compatibility_test: bool = False
18
+ # When set, the upstream is read as a stream and assembled here instead of
19
+ # being fetched with one blocking POST. Same result, except a runaway can be
20
+ # cut off while it is still being generated rather than after.
21
+ stream_collect: Callable[..., dict[str, Any]] | None = None
18
22
 
19
23
 
20
24
  @dataclass(frozen=True, slots=True)
@@ -48,6 +52,7 @@ class ResponseCollectionServices:
48
52
  rate_limit: ResponseCollectionRateLimit
49
53
  projection: ResponseCollectionProjection
50
54
  post_json_with_retry: Callable[..., Any]
55
+ hosted_tools: Any
51
56
 
52
57
 
53
58
  @dataclass(frozen=True, slots=True)
@@ -112,25 +117,60 @@ def collect_chat_message_for_responses(
112
117
  model = strategy.normalize_upstream_model(provider, pcfg, model)
113
118
  original_body = body
114
119
  upstream_body = request.body_with_advisor_tool(body, pcfg) if request.advisor_provider_supported(provider) else body
115
- req_body = strategy.build_request(provider, model, upstream_body, pcfg, stream=False)
120
+ streaming = strategy.stream_collect is not None
121
+ req_body = strategy.build_request(provider, model, upstream_body, pcfg, stream=streaming)
116
122
  url = request.provider_endpoint(provider, pcfg, strategy.operation)
123
+ timeout = strategy.request_timeout_seconds(pcfg)
124
+ headers = request.provider_headers(provider, pcfg, handler.headers, strategy.operation)
125
+ req_body, hosted_state = services.hosted_tools.prepare(
126
+ provider, pcfg, req_body, headers, timeout
127
+ )
117
128
  compatibility_test = str(handler.headers.get(services.compatibility_test_header) or "").strip().lower() in ("1", "true", "yes", "on")
118
129
  if compatibility_test and strategy.skip_rate_limit_during_compatibility_test:
119
130
  waited, rpm_used, rpm_limit = 0.0, 0, rate_limit.effective_rpm(provider, pcfg, model)
120
131
  else:
121
132
  waited, rpm_used, rpm_limit = rate_limit.apply(provider, pcfg, model)
122
- data = services.post_json_with_retry(
123
- url,
124
- req_body,
125
- request.provider_headers(
126
- provider, pcfg, handler.headers, strategy.operation
133
+ if streaming:
134
+ data = strategy.stream_collect(
135
+ url,
136
+ req_body,
137
+ headers,
138
+ timeout,
139
+ provider,
140
+ pcfg,
141
+ model,
142
+ retry_rate_limits=not compatibility_test,
143
+ )
144
+ else:
145
+ data = services.post_json_with_retry(
146
+ url,
147
+ req_body,
148
+ headers,
149
+ timeout,
150
+ provider,
151
+ pcfg,
152
+ model,
153
+ None,
154
+ retry_rate_limits=not compatibility_test,
155
+ )
156
+ # Hosted-tool follow-ups are plain request/response, so they keep using the
157
+ # blocking POST regardless of how the first turn was read.
158
+ data = services.hosted_tools.resolve(
159
+ hosted_state,
160
+ {**req_body, "stream": False} if streaming else req_body,
161
+ data,
162
+ lambda next_body: services.post_json_with_retry(
163
+ url,
164
+ next_body,
165
+ headers,
166
+ timeout,
167
+ provider,
168
+ pcfg,
169
+ model,
170
+ None,
171
+ retry_rate_limits=not compatibility_test,
127
172
  ),
128
- strategy.request_timeout_seconds(pcfg),
129
- provider,
130
- pcfg,
131
- model,
132
- None,
133
- retry_rate_limits=not compatibility_test,
173
+ timeout,
134
174
  )
135
175
  message = strategy.decode_response(data, model, source_body=original_body)
136
176
  message = projection.refine_with_advisor(provider, pcfg, original_body, message, model)
@@ -146,14 +186,20 @@ def collect_anthropic_message_for_responses(
146
186
  body: dict[str, Any],
147
187
  *,
148
188
  services: AnthropicCollectionServices,
189
+ stream_collect: Callable[..., dict[str, Any]] | None = None,
149
190
  ) -> dict[str, Any]:
150
- """Collect one native Anthropic message for a Responses API projection."""
191
+ """Collect one native Anthropic message for a Responses API projection.
192
+
193
+ ``stream_collect`` reads the upstream as SSE and assembles the same message
194
+ object, which lets the repetition guard cut a loop off mid-generation
195
+ instead of paying for all of it and trimming afterwards.
196
+ """
151
197
 
152
198
  request = services.request
153
199
  transport = services.transport
154
200
  projection = services.projection
155
201
  body = request.normalize_thinking(provider, pcfg, body)
156
- body = request.normalize_system_roles(body)
202
+ body = request.normalize_system_roles(provider, pcfg, body)
157
203
  body = request.cap_body(provider, pcfg, body)
158
204
  body = request.apply_options(provider, pcfg, body)
159
205
  body = request.rehydrate_thinking(provider, pcfg, body)
@@ -162,7 +208,8 @@ def collect_anthropic_message_for_responses(
162
208
  body["model"] = upstream_model
163
209
  body = request.resolve_tool_models(provider, pcfg, body)
164
210
  body = request.normalize_model_options(provider, pcfg, body, upstream_model)
165
- upstream_body = request.strip_internal_metadata({**body, "stream": False})
211
+ streaming = stream_collect is not None
212
+ upstream_body = request.strip_internal_metadata({**body, "stream": streaming})
166
213
  if transport.native_compat_enabled(provider, pcfg):
167
214
  base = transport.native_base_url(provider, pcfg)
168
215
  else:
@@ -186,11 +233,14 @@ def collect_anthropic_message_for_responses(
186
233
  provider,
187
234
  pcfg,
188
235
  upstream_model,
189
- stream=False,
236
+ stream=streaming,
190
237
  )
191
238
  try:
192
- raw_response = upstream_response.read()
193
- payload = json.loads(raw_response.decode("utf-8", errors="replace"))
239
+ if streaming:
240
+ payload = stream_collect(upstream_response, provider, upstream_model)
241
+ else:
242
+ raw_response = upstream_response.read()
243
+ payload = json.loads(raw_response.decode("utf-8", errors="replace"))
194
244
  if not isinstance(payload, dict):
195
245
  raise RuntimeError("upstream returned non-object JSON")
196
246
  payload = projection.normalize_response_thinking(provider, pcfg, payload, upstream_model)