@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
@@ -0,0 +1,634 @@
1
+ """Alibaba Model Studio adapter for Qwen's native API capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass, field
7
+ from typing import Any, Mapping
8
+
9
+ from ..architecture import (
10
+ MessageProtocol,
11
+ ProviderCapabilities,
12
+ ProviderConfig,
13
+ ProviderContextPolicy,
14
+ ProviderModelCatalogPolicy,
15
+ ProviderRequestPolicy,
16
+ )
17
+ from ..runtime_constants import DEFAULT_REQUEST_TIMEOUT_MS
18
+ from .base import HttpBearerProviderAdapter, provider_configuration
19
+
20
+
21
+ QWEN38_MAX_MODEL = "qwen3.8-max"
22
+ QWEN38_MAX_PREVIEW_MODEL = "qwen3.8-max-preview"
23
+ QWEN38_CONTEXT_WINDOW = 1_048_576
24
+ QWEN38_MAX_OUTPUT = 131_072
25
+ QWEN38_AUTO_COMPACT = 900_000
26
+ QWEN38_CODEX_CATALOG = {
27
+ "context_window": 983_616,
28
+ "max_context_window": 983_616,
29
+ "effective_context_window_percent": 95,
30
+ "supports_parallel_tool_calls": False,
31
+ "supports_image_detail_original": True,
32
+ "input_modalities": ["text", "image"],
33
+ "shell_type": "default",
34
+ "support_verbosity": False,
35
+ "supports_reasoning_summaries": False,
36
+ "experimental_supported_tools": [],
37
+ "truncation_policy": {"mode": "bytes", "limit": 10_000},
38
+ "supported_reasoning_levels": [
39
+ {"effort": "low", "description": "Fast responses with lighter reasoning"},
40
+ {"effort": "medium", "description": "Greater reasoning depth for complex problems"},
41
+ {"effort": "xhigh", "description": "Extra high reasoning depth for complex problems"},
42
+ ],
43
+ }
44
+ QWEN37_MAX_MODEL = "qwen3.7-max"
45
+ QWEN37_CONTEXT_WINDOW = 1_000_000
46
+ QWEN37_MAX_OUTPUT = 65_536
47
+ ALIBABA_CODING_PLAN_MODELS = (
48
+ "qwen3.7-plus",
49
+ "qwen3.6-plus",
50
+ "kimi-k2.5",
51
+ "glm-5",
52
+ "MiniMax-M2.5",
53
+ "qwen3.5-plus",
54
+ "qwen3-max-2026-01-23",
55
+ "qwen3-coder-next",
56
+ "qwen3-coder-plus",
57
+ "glm-4.7",
58
+ )
59
+ ALIBABA_MODEL_STUDIO_MODELS = (
60
+ QWEN37_MAX_MODEL,
61
+ "qwen3.7-plus",
62
+ "qwen3.6-plus",
63
+ "qwen3.6-flash",
64
+ "qwen3.5-plus",
65
+ "qwen3-coder-plus",
66
+ "qwen3-coder-flash",
67
+ "deepseek-v4-pro",
68
+ "deepseek-v4-flash",
69
+ "glm-5.2",
70
+ "kimi-k2.7-code",
71
+ "MiniMax-M2.5",
72
+ )
73
+ ALIBABA_TOKEN_PLAN_MODELS = (
74
+ QWEN38_MAX_MODEL,
75
+ QWEN37_MAX_MODEL,
76
+ "qwen3.7-plus",
77
+ "qwen3.6-plus",
78
+ "qwen3.6-flash",
79
+ "deepseek-v4-pro",
80
+ "deepseek-v4-flash",
81
+ "deepseek-v3.2",
82
+ "kimi-k2.7-code",
83
+ "kimi-k2.6",
84
+ "kimi-k2.5",
85
+ "glm-5.2",
86
+ "glm-5.1",
87
+ "glm-5",
88
+ "MiniMax-M2.5",
89
+ )
90
+ _RESPONSES_MODEL_PREFIXES = (
91
+ "qwen3.8-max",
92
+ "qwen3.7-max",
93
+ "qwen3.7-plus",
94
+ "qwen3.6-plus",
95
+ "qwen3.6-flash",
96
+ "qwen3.5-plus",
97
+ "qwen3.5-flash",
98
+ "qwen3-coder-plus",
99
+ "qwen3-coder-flash",
100
+ )
101
+ _CHAT_SEARCH_MODEL_PREFIXES = (
102
+ "qwen3.8-max",
103
+ "qwen3.7-plus",
104
+ "qwen3.6-plus",
105
+ "qwen3.6-flash",
106
+ "qwen3.5-plus",
107
+ "qwen3.5-flash",
108
+ )
109
+ _WEB_SEARCH_NAMES = frozenset({"websearch", "web_search"})
110
+ _WEB_FETCH_NAMES = frozenset({"webfetch", "web_fetch"})
111
+ _RESPONSES_TOOL_ALIASES = {
112
+ "web_search_preview": "web_search",
113
+ "t2i_search": "web_search_image",
114
+ "i2i_search": "image_search",
115
+ }
116
+ _EFFORTS = ("none", "minimal", "low", "medium", "high", "xhigh", "max")
117
+
118
+
119
+ @dataclass(frozen=True)
120
+ class AlibabaModelStudioProviderAdapter(HttpBearerProviderAdapter):
121
+ """Preserve Qwen Responses features while supporting Claude via Chat."""
122
+
123
+ name: str = "alims-intl"
124
+ base_url: str = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
125
+ configuration_defaults_value: dict = field(
126
+ default_factory=lambda: provider_configuration(
127
+ QWEN37_MAX_MODEL,
128
+ custom_models=ALIBABA_MODEL_STUDIO_MODELS,
129
+ native_compat=True,
130
+ supports_tool_choice=True,
131
+ context_window=QWEN37_CONTEXT_WINDOW,
132
+ max_model_len=QWEN37_CONTEXT_WINDOW,
133
+ max_output_tokens=QWEN37_MAX_OUTPUT,
134
+ context_reserve_tokens=8192,
135
+ auto_compact_window=QWEN38_AUTO_COMPACT,
136
+ codex_auto_compact_window=QWEN38_AUTO_COMPACT,
137
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
138
+ stream_enabled=True,
139
+ stream_word_chunking=False,
140
+ effort_level="high",
141
+ explicit_cache=True,
142
+ explicit_cache_markers=4,
143
+ haiku_model="qwen3.6-flash",
144
+ opus_model=QWEN37_MAX_MODEL,
145
+ sonnet_model="qwen3.7-plus",
146
+ subagent_model="qwen3.7-plus",
147
+ )
148
+ )
149
+ authorization_header: str = "authorization"
150
+ include_x_api_key: bool = False
151
+ require_api_key: bool = True
152
+ api_key_display_name_value: str = "Alibaba Model Studio International"
153
+ capabilities_value: ProviderCapabilities = field(
154
+ default_factory=lambda: ProviderCapabilities(
155
+ upstream_protocol="openai_responses",
156
+ supports_thinking=True,
157
+ requires_api_key=True,
158
+ )
159
+ )
160
+ request_policy_value: ProviderRequestPolicy = field(
161
+ default_factory=lambda: ProviderRequestPolicy(
162
+ chat_path="/v1/chat/completions",
163
+ models_path="/v1/models",
164
+ )
165
+ )
166
+ model_catalog_policy_value: ProviderModelCatalogPolicy = field(
167
+ default_factory=lambda: ProviderModelCatalogPolicy(
168
+ kind="openai",
169
+ fallback_models=ALIBABA_MODEL_STUDIO_MODELS,
170
+ allow_configured_fallback=True,
171
+ authoritative_upstream_catalog=True,
172
+ )
173
+ )
174
+
175
+ def context_policy(self, config: ProviderConfig) -> ProviderContextPolicy:
176
+ del config
177
+ return ProviderContextPolicy(
178
+ capacity_strategy="configured_first",
179
+ settings_strategy="standard",
180
+ hosted_timeout=True,
181
+ )
182
+
183
+ def supported_protocols(
184
+ self, config: ProviderConfig, model: str | None = None
185
+ ) -> frozenset[MessageProtocol]:
186
+ protocols: set[MessageProtocol] = {"openai_chat"}
187
+ if self._supports_responses(model):
188
+ protocols.add("openai_responses")
189
+ if self.router_native_anthropic_enabled(config, model):
190
+ protocols.add("anthropic_messages")
191
+ return frozenset(protocols)
192
+
193
+ def select_protocol(
194
+ self, operation: MessageProtocol, config: ProviderConfig, model: str | None = None
195
+ ) -> MessageProtocol:
196
+ if operation == "anthropic_messages" and self.router_native_anthropic_enabled(
197
+ config, model
198
+ ):
199
+ return "anthropic_messages"
200
+ return (
201
+ "openai_responses"
202
+ if operation == "openai_responses" and self._supports_responses(model)
203
+ else "openai_chat"
204
+ )
205
+
206
+ def router_native_anthropic_enabled(
207
+ self, config: ProviderConfig, model: str | None = None
208
+ ) -> bool:
209
+ del model
210
+ return bool(config.options.get("native_compat", True))
211
+
212
+ def anthropic_base_url(self, config: ProviderConfig) -> str:
213
+ base = str(config.base_url or self.default_base_url()).rstrip("/")
214
+ suffix = "/compatible-mode/v1"
215
+ if base.endswith(suffix):
216
+ return f"{base[:-len(suffix)]}/apps/anthropic"
217
+ return base
218
+
219
+ def supports_server_web_tools(self, config: ProviderConfig) -> bool:
220
+ return self._supports_chat_search(config.model)
221
+
222
+ def model_configuration_profile(
223
+ self, config: ProviderConfig
224
+ ) -> tuple[Mapping[str, Any], str | None]:
225
+ if not self._is_qwen38(config.model):
226
+ if QWEN37_MAX_MODEL not in self._clean_model(config.model):
227
+ return {}, None
228
+ return (
229
+ {
230
+ "context_window": QWEN37_CONTEXT_WINDOW,
231
+ "max_model_len": QWEN37_CONTEXT_WINDOW,
232
+ "max_output_tokens": QWEN37_MAX_OUTPUT,
233
+ "auto_compact_window": QWEN38_AUTO_COMPACT,
234
+ "codex_auto_compact_window": QWEN38_AUTO_COMPACT,
235
+ "effort_level": "high",
236
+ "model_profile": "qwen3.7-max-1m",
237
+ },
238
+ "Qwen3.7-Max profile applied: 1M context, 65K output, high reasoning, and 900K compaction.",
239
+ )
240
+ return (
241
+ {
242
+ "context_window": QWEN38_CONTEXT_WINDOW,
243
+ "max_model_len": QWEN38_CONTEXT_WINDOW,
244
+ "max_output_tokens": QWEN38_MAX_OUTPUT,
245
+ "auto_compact_window": QWEN38_AUTO_COMPACT,
246
+ "codex_auto_compact_window": QWEN38_AUTO_COMPACT,
247
+ "effort_level": "xhigh",
248
+ "model_profile": "qwen3.8-max-1m",
249
+ "codex_model_catalog": deepcopy(QWEN38_CODEX_CATALOG),
250
+ },
251
+ "Qwen3.8-Max profile applied: 1M context, 131K output, xhigh reasoning, and 900K compaction.",
252
+ )
253
+
254
+ def model_selection_config_updates(
255
+ self, config: ProviderConfig, model_id: str
256
+ ) -> Mapping[str, Any]:
257
+ del config
258
+ return {
259
+ "haiku_model": model_id,
260
+ "opus_model": model_id,
261
+ "sonnet_model": model_id,
262
+ "subagent_model": model_id,
263
+ }
264
+
265
+ def normalize_request_options(
266
+ self, config: ProviderConfig, request: Mapping[str, Any]
267
+ ) -> Mapping[str, Any]:
268
+ normalized = dict(request)
269
+ if "messages" in normalized:
270
+ normalized["messages"] = deepcopy(normalized["messages"])
271
+ if "tools" in normalized:
272
+ normalized["tools"] = deepcopy(normalized["tools"])
273
+ model = str(normalized.get("model") or config.model)
274
+ if (
275
+ "input" in normalized
276
+ and "messages" not in normalized
277
+ and self._supports_responses(model)
278
+ ):
279
+ self._normalize_responses(normalized, model)
280
+ elif "messages" in normalized:
281
+ self._normalize_chat(config, normalized, model)
282
+ return normalized
283
+
284
+ def openai_reasoning_effort(
285
+ self, config: ProviderConfig, model: str, request: Mapping[str, Any]
286
+ ) -> str | None:
287
+ if not self._supports_responses(model):
288
+ return None
289
+ value = str(
290
+ request.get("reasoning_effort")
291
+ or config.options.get("effort_level")
292
+ or "xhigh"
293
+ ).strip().lower()
294
+ if self._is_qwen38(model):
295
+ return self._normalize_qwen38_effort(value)
296
+ return value if value in _EFFORTS else "xhigh"
297
+
298
+ def openai_reasoning_passback_enabled(
299
+ self, config: ProviderConfig, model: str | None = None
300
+ ) -> bool:
301
+ del config
302
+ return self._is_qwen38(model)
303
+
304
+ def allows_sampling_overrides(self, config: ProviderConfig) -> bool:
305
+ del config
306
+ return False
307
+
308
+ @classmethod
309
+ def _normalize_responses(cls, request: dict[str, Any], model: str) -> None:
310
+ reasoning = request.get("reasoning")
311
+ if isinstance(reasoning, Mapping):
312
+ projected = dict(reasoning)
313
+ effort = str(projected.get("effort") or "xhigh").strip().lower()
314
+ projected["effort"] = (
315
+ cls._normalize_qwen38_effort(effort)
316
+ if cls._is_qwen38(model)
317
+ else effort if effort in _EFFORTS else "xhigh"
318
+ )
319
+ request["reasoning"] = projected
320
+ if cls._is_qwen38(model):
321
+ request.pop("thinking_budget", None)
322
+ elif request.get("enable_thinking") is False and cls._is_qwen38(model):
323
+ request["reasoning"] = {"effort": "none"}
324
+ request.pop("thinking_budget", None)
325
+ request.pop("enable_thinking", None)
326
+
327
+ tools = request.get("tools")
328
+ if isinstance(tools, list):
329
+ normalized_tools: list[Any] = []
330
+ seen: set[str] = set()
331
+ for tool in tools:
332
+ if not isinstance(tool, Mapping):
333
+ normalized_tools.append(tool)
334
+ continue
335
+ projected = dict(tool)
336
+ tool_type = str(projected.get("type") or "").strip()
337
+ projected["type"] = _RESPONSES_TOOL_ALIASES.get(tool_type, tool_type)
338
+ identity = str(projected)
339
+ if identity not in seen:
340
+ normalized_tools.append(projected)
341
+ seen.add(identity)
342
+ request["tools"] = normalized_tools
343
+
344
+ @classmethod
345
+ def _normalize_chat(
346
+ cls, config: ProviderConfig, request: dict[str, Any], model: str
347
+ ) -> None:
348
+ tools = request.get("tools")
349
+ has_search = False
350
+ has_fetch = False
351
+ remaining: list[Any] = []
352
+ supports_search = cls._supports_chat_search(model)
353
+ if isinstance(tools, list):
354
+ for tool in tools:
355
+ function = tool.get("function") if isinstance(tool, Mapping) else None
356
+ name = str(function.get("name") or "").strip().lower() if isinstance(function, Mapping) else ""
357
+ if supports_search and name in _WEB_SEARCH_NAMES:
358
+ has_search = True
359
+ elif supports_search and name in _WEB_FETCH_NAMES:
360
+ has_fetch = True
361
+ else:
362
+ remaining.append(tool)
363
+ if remaining:
364
+ request["tools"] = remaining
365
+ else:
366
+ request.pop("tools", None)
367
+ request.pop("tool_choice", None)
368
+ if has_search or has_fetch:
369
+ request["enable_search"] = True
370
+ request["search_options"] = {
371
+ "search_strategy": (
372
+ "max" if cls._is_qwen38(model)
373
+ else "agent_max" if has_fetch else "agent"
374
+ )
375
+ }
376
+ if request.get("tool_choice") not in (None, "none"):
377
+ request["tool_choice"] = "auto"
378
+
379
+ if bool(config.options.get("explicit_cache", True)):
380
+ messages = request.get("messages")
381
+ if isinstance(messages, list):
382
+ cls._apply_explicit_cache_markers(
383
+ messages,
384
+ config.options.get("explicit_cache_markers", 4),
385
+ )
386
+
387
+ if cls._is_qwen38(model):
388
+ effort = request.get("reasoning_effort")
389
+ if effort is not None:
390
+ request["reasoning_effort"] = cls._normalize_qwen38_effort(effort)
391
+ request.pop("thinking_budget", None)
392
+ if "max_tokens" in request and "max_completion_tokens" not in request:
393
+ request["max_completion_tokens"] = request.pop("max_tokens")
394
+
395
+ @staticmethod
396
+ def _normalize_qwen38_effort(value: Any) -> str:
397
+ effort = str(value or "xhigh").strip().lower()
398
+ if effort in {"max", "high", "xhigh"}:
399
+ return "xhigh"
400
+ if effort == "medium":
401
+ return "medium"
402
+ if effort in {"minimal", "low"}:
403
+ return "low"
404
+ if effort == "none":
405
+ return "none"
406
+ return "xhigh"
407
+
408
+ @classmethod
409
+ def _apply_explicit_cache_markers(
410
+ cls, messages: list[Any], configured_limit: Any
411
+ ) -> None:
412
+ try:
413
+ limit = max(1, min(4, int(configured_limit)))
414
+ except (TypeError, ValueError):
415
+ limit = 4
416
+ for message in messages:
417
+ if isinstance(message, dict):
418
+ cls._clear_message_cache_control(message)
419
+ cacheable = [
420
+ index
421
+ for index, message in enumerate(messages)
422
+ if isinstance(message, dict) and cls._cacheable_content(message.get("content"))
423
+ ]
424
+ if not cacheable:
425
+ return
426
+ system = next(
427
+ (
428
+ index
429
+ for index in cacheable
430
+ if str(messages[index].get("role") or "").lower() == "system"
431
+ ),
432
+ None,
433
+ )
434
+ conversation = [index for index in cacheable if index != system]
435
+ selected: list[int] = [system] if system is not None else []
436
+ thresholds = iter((1, 9, 17))
437
+ threshold = next(thresholds, None)
438
+ blocks_from_tail = 0
439
+ for index in reversed(conversation):
440
+ blocks_from_tail += cls._content_block_count(messages[index].get("content"))
441
+ while threshold is not None and blocks_from_tail >= threshold:
442
+ if index not in selected and len(selected) < limit:
443
+ selected.append(index)
444
+ threshold = next(thresholds, None)
445
+ if threshold is None or len(selected) >= limit:
446
+ break
447
+ for index in selected[:limit]:
448
+ cls._mark_message_cache_control(messages[index])
449
+
450
+ @staticmethod
451
+ def _cacheable_content(content: Any) -> bool:
452
+ if isinstance(content, str):
453
+ return bool(content)
454
+ return isinstance(content, list) and bool(content)
455
+
456
+ @staticmethod
457
+ def _content_block_count(content: Any) -> int:
458
+ return max(1, len(content)) if isinstance(content, list) else 1
459
+
460
+ @staticmethod
461
+ def _mark_message_cache_control(message: dict[str, Any]) -> None:
462
+ content = message.get("content")
463
+ if isinstance(content, str):
464
+ message["content"] = [
465
+ {
466
+ "type": "text",
467
+ "text": content,
468
+ "cache_control": {"type": "ephemeral"},
469
+ }
470
+ ]
471
+ return
472
+ if not isinstance(content, list):
473
+ return
474
+ for index in range(len(content) - 1, -1, -1):
475
+ block = content[index]
476
+ if isinstance(block, Mapping):
477
+ projected = dict(block)
478
+ projected["cache_control"] = {"type": "ephemeral"}
479
+ content[index] = projected
480
+ return
481
+ if isinstance(block, str) and block:
482
+ content[index] = {
483
+ "type": "text",
484
+ "text": block,
485
+ "cache_control": {"type": "ephemeral"},
486
+ }
487
+ return
488
+
489
+ @staticmethod
490
+ def _clear_message_cache_control(message: dict[str, Any]) -> None:
491
+ content = message.get("content")
492
+ if not isinstance(content, list):
493
+ return
494
+ for index, block in enumerate(content):
495
+ if not isinstance(block, Mapping) or "cache_control" not in block:
496
+ continue
497
+ projected = dict(block)
498
+ projected.pop("cache_control", None)
499
+ content[index] = projected
500
+
501
+ @classmethod
502
+ def _is_qwen38(cls, model: str) -> bool:
503
+ return cls._clean_model(model) == QWEN38_MAX_MODEL
504
+
505
+ @classmethod
506
+ def _supports_responses(cls, model: str | None) -> bool:
507
+ clean = cls._clean_model(str(model or ""))
508
+ return any(prefix in clean for prefix in _RESPONSES_MODEL_PREFIXES)
509
+
510
+ @classmethod
511
+ def _supports_chat_search(cls, model: str | None) -> bool:
512
+ clean = cls._clean_model(str(model or ""))
513
+ return any(prefix in clean for prefix in _CHAT_SEARCH_MODEL_PREFIXES)
514
+
515
+ def supports_tool_choice_for_request(
516
+ self,
517
+ config: ProviderConfig,
518
+ model: str | None,
519
+ request: Mapping[str, Any],
520
+ ) -> bool:
521
+ if self._thinking_enabled(config, request):
522
+ return False
523
+ return self.supports_tool_choice(config, model)
524
+
525
+ @staticmethod
526
+ def _thinking_enabled(
527
+ config: ProviderConfig, request: Mapping[str, Any]
528
+ ) -> bool:
529
+ thinking = request.get("thinking")
530
+ if isinstance(thinking, Mapping):
531
+ state = str(thinking.get("type") or "").strip().lower()
532
+ if state in {"enabled", "adaptive"}:
533
+ return True
534
+ if state == "disabled":
535
+ return False
536
+ try:
537
+ if int(thinking.get("budget_tokens") or 0) > 0:
538
+ return True
539
+ except (TypeError, ValueError):
540
+ pass
541
+ if request.get("enable_thinking") is True:
542
+ return True
543
+ if request.get("enable_thinking") is False:
544
+ return False
545
+ reasoning = request.get("reasoning")
546
+ if isinstance(reasoning, Mapping):
547
+ effort = str(reasoning.get("effort") or "").strip().lower()
548
+ return effort not in {"", "none", "minimal"}
549
+ effort = str(config.options.get("effort_level") or "").strip().lower()
550
+ return effort not in {"", "none", "minimal"}
551
+
552
+ @staticmethod
553
+ def _clean_model(model: str) -> str:
554
+ value = str(model or "").strip().lower()
555
+ return QWEN38_MAX_MODEL if QWEN38_MAX_MODEL in value else value
556
+
557
+
558
+ @dataclass(frozen=True)
559
+ class AlibabaTokenPlanProviderAdapter(AlibabaModelStudioProviderAdapter):
560
+ """Singapore Token Plan with native Claude and Responses-based Codex routes."""
561
+
562
+ name: str = "alitoken"
563
+ base_url: str = (
564
+ "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1"
565
+ )
566
+ configuration_defaults_value: dict = field(
567
+ default_factory=lambda: provider_configuration(
568
+ QWEN38_MAX_MODEL,
569
+ custom_models=ALIBABA_TOKEN_PLAN_MODELS,
570
+ native_compat=True,
571
+ supports_tool_choice=True,
572
+ context_window=QWEN38_CONTEXT_WINDOW,
573
+ max_model_len=QWEN38_CONTEXT_WINDOW,
574
+ max_output_tokens=QWEN38_MAX_OUTPUT,
575
+ context_reserve_tokens=8192,
576
+ auto_compact_window=QWEN38_AUTO_COMPACT,
577
+ codex_auto_compact_window=QWEN38_AUTO_COMPACT,
578
+ request_timeout_ms=DEFAULT_REQUEST_TIMEOUT_MS,
579
+ stream_enabled=True,
580
+ stream_word_chunking=False,
581
+ effort_level="xhigh",
582
+ explicit_cache=True,
583
+ explicit_cache_markers=4,
584
+ haiku_model="qwen3.6-flash",
585
+ opus_model=QWEN38_MAX_MODEL,
586
+ sonnet_model="qwen3.7-plus",
587
+ subagent_model="qwen3.7-plus",
588
+ region="ap-southeast-1",
589
+ )
590
+ )
591
+ api_key_display_name_value: str = "Alibaba Model Studio Token Plan (Singapore)"
592
+ model_catalog_policy_value: ProviderModelCatalogPolicy = field(
593
+ default_factory=lambda: ProviderModelCatalogPolicy(
594
+ kind="openai",
595
+ fallback_models=ALIBABA_TOKEN_PLAN_MODELS,
596
+ allow_configured_fallback=True,
597
+ authoritative_upstream_catalog=True,
598
+ supplemental_model_aliases=((QWEN38_MAX_MODEL, QWEN38_MAX_PREVIEW_MODEL),),
599
+ )
600
+ )
601
+
602
+ @dataclass(frozen=True)
603
+ class AlibabaIndividualTokenPlanProviderAdapter(AlibabaTokenPlanProviderAdapter):
604
+ """Individual Token Plan using its separately billed coding endpoint."""
605
+
606
+ name: str = "alitoken-individual"
607
+ base_url: str = "https://coding.dashscope.aliyuncs.com/v1"
608
+ api_key_display_name_value: str = "Alibaba Token Plan Individual"
609
+ model_catalog_policy_value: ProviderModelCatalogPolicy = field(
610
+ default_factory=lambda: ProviderModelCatalogPolicy(
611
+ kind="openai",
612
+ fallback_models=ALIBABA_TOKEN_PLAN_MODELS,
613
+ allow_configured_fallback=True,
614
+ authoritative_upstream_catalog=True,
615
+ )
616
+ )
617
+
618
+ def anthropic_base_url(self, config: ProviderConfig) -> str:
619
+ del config
620
+ return "https://coding.dashscope.aliyuncs.com/apps/anthropic"
621
+
622
+
623
+ __all__ = [
624
+ "ALIBABA_CODING_PLAN_MODELS",
625
+ "ALIBABA_MODEL_STUDIO_MODELS",
626
+ "ALIBABA_TOKEN_PLAN_MODELS",
627
+ "AlibabaModelStudioProviderAdapter",
628
+ "AlibabaIndividualTokenPlanProviderAdapter",
629
+ "AlibabaTokenPlanProviderAdapter",
630
+ "QWEN38_AUTO_COMPACT",
631
+ "QWEN38_CONTEXT_WINDOW",
632
+ "QWEN38_MAX_MODEL",
633
+ "QWEN38_MAX_OUTPUT",
634
+ ]