@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.
- package/ciel_runtime.py +2553 -9635
- package/ciel_runtime_support/advisor_request_builder.py +8 -21
- package/ciel_runtime_support/anthropic_tool_turns.py +13 -8
- package/ciel_runtime_support/architecture.py +68 -0
- package/ciel_runtime_support/architecture_budget.py +1 -1
- package/ciel_runtime_support/channel_connection_context.py +233 -0
- package/ciel_runtime_support/channel_delivery_context.py +332 -0
- package/ciel_runtime_support/channel_mcp_context.py +313 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +47 -0
- package/ciel_runtime_support/channel_mcp_transport.py +5 -1
- package/ciel_runtime_support/channel_message_context.py +212 -0
- package/ciel_runtime_support/channel_message_repository.py +14 -3
- package/ciel_runtime_support/channel_pending_injection.py +9 -0
- package/ciel_runtime_support/channel_probe_launch_context.py +213 -0
- package/ciel_runtime_support/channel_replay_policy.py +38 -0
- package/ciel_runtime_support/channel_runtime_environment.py +8 -0
- package/ciel_runtime_support/channel_session_context.py +236 -0
- package/ciel_runtime_support/channel_terminal_context.py +350 -0
- package/ciel_runtime_support/channel_wake_context.py +532 -0
- package/ciel_runtime_support/claude_environment.py +60 -0
- package/ciel_runtime_support/claude_launch_assembly.py +249 -0
- package/ciel_runtime_support/claude_router.py +62 -12
- package/ciel_runtime_support/cli_application_context.py +132 -0
- package/ciel_runtime_support/cli_assembly.py +50 -0
- package/ciel_runtime_support/codex_backend_context.py +363 -0
- package/ciel_runtime_support/codex_config.py +13 -1
- package/ciel_runtime_support/codex_launch_assembly.py +213 -0
- package/ciel_runtime_support/codex_launch_configuration.py +30 -1
- package/ciel_runtime_support/codex_mcp_integration.py +90 -8
- package/ciel_runtime_support/codex_model_catalog.py +4 -1
- package/ciel_runtime_support/codex_reasoning_rejects.py +225 -0
- package/ciel_runtime_support/codex_router.py +38 -8
- package/ciel_runtime_support/codex_turn_recovery.py +154 -0
- package/ciel_runtime_support/config_migrations.py +103 -0
- package/ciel_runtime_support/configuration_cli.py +38 -0
- package/ciel_runtime_support/context_compaction.py +9 -4
- package/ciel_runtime_support/credential_management.py +12 -0
- package/ciel_runtime_support/credentials.py +12 -0
- package/ciel_runtime_support/github_copilot_oauth.py +2 -2
- package/ciel_runtime_support/hosted_formula_tools.py +216 -0
- package/ciel_runtime_support/kimi_runtime_context.py +208 -0
- package/ciel_runtime_support/llm_preset_context.py +338 -0
- package/ciel_runtime_support/managed_mcp_config.py +8 -4
- package/ciel_runtime_support/mcp_configuration_context.py +291 -0
- package/ciel_runtime_support/mcp_http_proxy.py +14 -8
- package/ciel_runtime_support/mcp_probe_transport.py +47 -15
- package/ciel_runtime_support/mcp_transport.py +14 -1
- package/ciel_runtime_support/native_context_recovery.py +72 -0
- package/ciel_runtime_support/ollama_catalog_context.py +213 -0
- package/ciel_runtime_support/ollama_stream_collection.py +103 -0
- package/ciel_runtime_support/ollama_thinking.py +6 -1
- package/ciel_runtime_support/ollama_wire_projection.py +157 -0
- package/ciel_runtime_support/openai_forwarding.py +32 -10
- package/ciel_runtime_support/openai_responses_router.py +12 -0
- package/ciel_runtime_support/package_lifecycle.py +39 -0
- package/ciel_runtime_support/prelaunch_assembly.py +37 -0
- package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
- package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
- package/ciel_runtime_support/prompt_compaction.py +144 -0
- package/ciel_runtime_support/prompt_injection.py +45 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
- package/ciel_runtime_support/protocols/chat_projection.py +85 -5
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
- package/ciel_runtime_support/protocols/ollama_response.py +57 -5
- package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
- package/ciel_runtime_support/protocols/openai_responses.py +61 -15
- package/ciel_runtime_support/provider_adapters.py +26 -0
- package/ciel_runtime_support/provider_administration_context.py +207 -0
- package/ciel_runtime_support/provider_config_mutations.py +3 -0
- package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
- package/ciel_runtime_support/provider_model_context.py +107 -0
- package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
- package/ciel_runtime_support/provider_model_selection.py +10 -3
- package/ciel_runtime_support/provider_models.py +45 -2
- package/ciel_runtime_support/provider_option_cli.py +19 -0
- package/ciel_runtime_support/provider_policy.py +1 -1
- package/ciel_runtime_support/provider_readiness_context.py +189 -0
- package/ciel_runtime_support/provider_request_builder.py +64 -28
- package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
- package/ciel_runtime_support/provider_timeout_policy.py +54 -0
- package/ciel_runtime_support/provider_tool_policy.py +9 -1
- package/ciel_runtime_support/providers/__init__.py +6 -0
- package/ciel_runtime_support/providers/alibaba.py +634 -0
- package/ciel_runtime_support/providers/catalog.py +24 -16
- package/ciel_runtime_support/providers/deepseek.py +73 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
- package/ciel_runtime_support/providers/kimi.py +69 -9
- package/ciel_runtime_support/providers/ollama.py +8 -0
- package/ciel_runtime_support/providers/ollama_context.py +21 -2
- package/ciel_runtime_support/providers/vllm.py +7 -1
- package/ciel_runtime_support/response_collection.py +68 -18
- package/ciel_runtime_support/response_collection_context.py +391 -0
- package/ciel_runtime_support/response_stream_context.py +555 -0
- package/ciel_runtime_support/responses_input_compatibility.py +121 -0
- package/ciel_runtime_support/responses_usage_observer.py +83 -0
- package/ciel_runtime_support/router_client_lifecycle.py +1 -0
- package/ciel_runtime_support/router_http.py +239 -3
- package/ciel_runtime_support/router_observability_context.py +251 -0
- package/ciel_runtime_support/router_process_context.py +200 -0
- package/ciel_runtime_support/router_process_lifecycle.py +2 -0
- package/ciel_runtime_support/router_request_assembly.py +399 -0
- package/ciel_runtime_support/router_request_context.py +215 -0
- package/ciel_runtime_support/router_server_context.py +82 -0
- package/ciel_runtime_support/runaway_output_guard.py +488 -0
- package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
- package/ciel_runtime_support/runtime_asset_context.py +297 -0
- package/ciel_runtime_support/runtime_constants.py +16 -1
- package/ciel_runtime_support/runtime_launch.py +9 -5
- package/ciel_runtime_support/runtime_launch_context.py +130 -0
- package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
- package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
- package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
- package/ciel_runtime_support/runtime_paths.py +60 -40
- package/ciel_runtime_support/runtime_primitives.py +78 -0
- package/ciel_runtime_support/sse_stream_collection.py +236 -0
- package/ciel_runtime_support/statusline_script.py +57 -8
- package/ciel_runtime_support/streaming_anthropic.py +361 -24
- package/ciel_runtime_support/tool_schema.py +40 -2
- package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
- package/ciel_runtime_support/upstream_dump.py +68 -0
- package/ciel_runtime_support/upstream_retry_context.py +259 -0
- package/ciel_runtime_support/workspace_router_selection.py +86 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- 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
|
+
]
|