@oneciel-ai/ciel-runtime 0.2.2 → 0.2.4

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 (136) hide show
  1. package/ciel_runtime.py +2555 -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/config_repository.py +30 -0
  36. package/ciel_runtime_support/configuration_cli.py +38 -0
  37. package/ciel_runtime_support/context_compaction.py +9 -4
  38. package/ciel_runtime_support/credential_management.py +12 -0
  39. package/ciel_runtime_support/credentials.py +12 -0
  40. package/ciel_runtime_support/github_copilot_oauth.py +2 -2
  41. package/ciel_runtime_support/hosted_formula_tools.py +216 -0
  42. package/ciel_runtime_support/kimi_runtime_context.py +208 -0
  43. package/ciel_runtime_support/llm_preset_context.py +338 -0
  44. package/ciel_runtime_support/managed_mcp_config.py +8 -4
  45. package/ciel_runtime_support/mcp_configuration_context.py +291 -0
  46. package/ciel_runtime_support/mcp_http_proxy.py +14 -8
  47. package/ciel_runtime_support/mcp_probe_transport.py +47 -15
  48. package/ciel_runtime_support/mcp_transport.py +14 -1
  49. package/ciel_runtime_support/native_context_recovery.py +72 -0
  50. package/ciel_runtime_support/ollama_catalog_context.py +213 -0
  51. package/ciel_runtime_support/ollama_stream_collection.py +103 -0
  52. package/ciel_runtime_support/ollama_thinking.py +6 -1
  53. package/ciel_runtime_support/ollama_wire_projection.py +157 -0
  54. package/ciel_runtime_support/openai_forwarding.py +32 -10
  55. package/ciel_runtime_support/openai_responses_router.py +12 -0
  56. package/ciel_runtime_support/package_lifecycle.py +39 -0
  57. package/ciel_runtime_support/prelaunch_assembly.py +37 -0
  58. package/ciel_runtime_support/prelaunch_panel_context.py +418 -0
  59. package/ciel_runtime_support/prelaunch_shell_context.py +394 -0
  60. package/ciel_runtime_support/prompt_compaction.py +144 -0
  61. package/ciel_runtime_support/prompt_injection.py +45 -0
  62. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +1 -1
  63. package/ciel_runtime_support/protocols/chat_projection.py +85 -5
  64. package/ciel_runtime_support/protocols/conversation_turn_policy.py +43 -0
  65. package/ciel_runtime_support/protocols/ollama_chat.py +31 -0
  66. package/ciel_runtime_support/protocols/ollama_response.py +57 -5
  67. package/ciel_runtime_support/protocols/openai_reasoning.py +5 -2
  68. package/ciel_runtime_support/protocols/openai_responses.py +61 -15
  69. package/ciel_runtime_support/provider_adapters.py +26 -0
  70. package/ciel_runtime_support/provider_administration_context.py +207 -0
  71. package/ciel_runtime_support/provider_config_mutations.py +3 -0
  72. package/ciel_runtime_support/provider_model_catalog_context.py +137 -0
  73. package/ciel_runtime_support/provider_model_context.py +107 -0
  74. package/ciel_runtime_support/provider_model_metadata_context.py +197 -0
  75. package/ciel_runtime_support/provider_model_selection.py +10 -3
  76. package/ciel_runtime_support/provider_models.py +45 -2
  77. package/ciel_runtime_support/provider_option_cli.py +19 -0
  78. package/ciel_runtime_support/provider_policy.py +1 -1
  79. package/ciel_runtime_support/provider_readiness_context.py +189 -0
  80. package/ciel_runtime_support/provider_request_builder.py +64 -28
  81. package/ciel_runtime_support/provider_responses_passthrough.py +21 -2
  82. package/ciel_runtime_support/provider_timeout_policy.py +54 -0
  83. package/ciel_runtime_support/provider_tool_policy.py +9 -1
  84. package/ciel_runtime_support/providers/__init__.py +6 -0
  85. package/ciel_runtime_support/providers/alibaba.py +634 -0
  86. package/ciel_runtime_support/providers/catalog.py +24 -16
  87. package/ciel_runtime_support/providers/deepseek.py +73 -0
  88. package/ciel_runtime_support/providers/github_copilot_oauth.py +22 -1
  89. package/ciel_runtime_support/providers/kimi.py +69 -9
  90. package/ciel_runtime_support/providers/ollama.py +8 -0
  91. package/ciel_runtime_support/providers/ollama_context.py +21 -2
  92. package/ciel_runtime_support/providers/vllm.py +7 -1
  93. package/ciel_runtime_support/response_collection.py +68 -18
  94. package/ciel_runtime_support/response_collection_context.py +391 -0
  95. package/ciel_runtime_support/response_stream_context.py +555 -0
  96. package/ciel_runtime_support/responses_input_compatibility.py +121 -0
  97. package/ciel_runtime_support/responses_usage_observer.py +83 -0
  98. package/ciel_runtime_support/router_client_lifecycle.py +1 -0
  99. package/ciel_runtime_support/router_http.py +245 -3
  100. package/ciel_runtime_support/router_observability_context.py +251 -0
  101. package/ciel_runtime_support/router_process_context.py +200 -0
  102. package/ciel_runtime_support/router_process_lifecycle.py +2 -0
  103. package/ciel_runtime_support/router_request_assembly.py +399 -0
  104. package/ciel_runtime_support/router_request_context.py +215 -0
  105. package/ciel_runtime_support/router_server_context.py +84 -0
  106. package/ciel_runtime_support/runaway_output_guard.py +488 -0
  107. package/ciel_runtime_support/runtime_asset_assembly.py +147 -0
  108. package/ciel_runtime_support/runtime_asset_context.py +297 -0
  109. package/ciel_runtime_support/runtime_constants.py +16 -1
  110. package/ciel_runtime_support/runtime_launch.py +9 -5
  111. package/ciel_runtime_support/runtime_launch_context.py +130 -0
  112. package/ciel_runtime_support/runtime_maintenance_assembly.py +60 -0
  113. package/ciel_runtime_support/runtime_maintenance_context.py +309 -0
  114. package/ciel_runtime_support/runtime_maintenance_services.py +265 -0
  115. package/ciel_runtime_support/runtime_paths.py +60 -40
  116. package/ciel_runtime_support/runtime_primitives.py +78 -0
  117. package/ciel_runtime_support/speech_http_controller.py +335 -0
  118. package/ciel_runtime_support/sse_stream_collection.py +236 -0
  119. package/ciel_runtime_support/statusline_script.py +57 -8
  120. package/ciel_runtime_support/streaming_anthropic.py +361 -24
  121. package/ciel_runtime_support/tool_schema.py +40 -2
  122. package/ciel_runtime_support/tool_side_effect_dedupe.py +117 -12
  123. package/ciel_runtime_support/upstream_dump.py +68 -0
  124. package/ciel_runtime_support/upstream_retry_context.py +259 -0
  125. package/ciel_runtime_support/web_ui.py +248 -1
  126. package/ciel_runtime_support/workspace_router_selection.py +86 -0
  127. package/docs/COLAB_SPEECH.md +32 -0
  128. package/docs/Configuration.md +50 -0
  129. package/docs/Test-Suite.md +1 -0
  130. package/package.json +4 -1
  131. package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
  132. package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
  133. package/scripts/colab/bootstrap_moss_tts.py +106 -0
  134. package/scripts/colab/bootstrap_qwen_asr.py +106 -0
  135. package/scripts/configure_speech_workers.py +37 -0
  136. package/scripts/deploy_colab_speech.ps1 +47 -0
@@ -91,10 +91,45 @@ def fetch_upstream_model_ids(provider: str, pcfg: dict[str, Any], force_refresh:
91
91
  with_upstream_user_agent = services.http.with_upstream_user_agent
92
92
  write_model_list_cache = services.storage.write_model_list_cache
93
93
  write_model_registry = services.storage.write_model_registry
94
+ catalog_policy = provider_model_catalog_policy(provider, pcfg)
95
+
96
+ def supplement_model_aliases(
97
+ model_ids: list[str], model_info: dict[str, dict[str, Any]] | None = None
98
+ ) -> list[str]:
99
+ """Add provider-declared wire aliases without weakening catalog authority."""
100
+ supplemented = list(model_ids)
101
+ for source_id, alias_id in catalog_policy.supplemental_model_aliases:
102
+ source = normalize_model_id(provider, source_id)
103
+ alias = normalize_model_id(provider, alias_id)
104
+ if not source or not alias or source not in supplemented:
105
+ continue
106
+ if alias not in supplemented:
107
+ supplemented.append(alias)
108
+ if model_info is not None and source in model_info and alias not in model_info:
109
+ model_info[alias] = dict(model_info[source])
110
+ return supplemented
111
+
94
112
  cached = None if force_refresh else read_model_list_cache(provider, pcfg)
95
113
  if cached is not None:
96
- return cached
97
- catalog_policy = provider_model_catalog_policy(provider, pcfg)
114
+ supplemented = sorted_model_ids(
115
+ unique_model_ids(provider, supplement_model_aliases(cached))
116
+ )
117
+ cached = supplemented
118
+ if (
119
+ catalog_policy.authoritative_upstream_catalog
120
+ or not catalog_policy.allow_configured_fallback
121
+ ):
122
+ return cached
123
+ ids = unique_model_ids(provider, [
124
+ *cached,
125
+ *catalog_policy.fallback_models,
126
+ *(pcfg.get("custom_models", []) or []),
127
+ pcfg.get("current_model") or "",
128
+ ])
129
+ sorted_ids = sorted_model_ids(ids)
130
+ if sorted_ids != cached:
131
+ write_model_list_cache(provider, pcfg, sorted_ids)
132
+ return sorted_ids
98
133
  if catalog_policy.kind == "configured":
99
134
  ids = unique_model_ids(provider, [
100
135
  *catalog_policy.fallback_models,
@@ -212,11 +247,18 @@ def fetch_upstream_model_ids(provider: str, pcfg: dict[str, Any], force_refresh:
212
247
  router_log("DEBUG", f"{provider} public model catalog fetch failed: {type(exc).__name__}: {exc}")
213
248
  except Exception:
214
249
  ids = []
250
+ if fetched and ids and catalog_policy.authoritative_upstream_catalog:
251
+ ids = supplement_model_aliases(ids, model_info)
252
+ sorted_ids = sorted_model_ids(unique_model_ids(provider, ids))
253
+ metadata = {"model_info": model_info} if model_info else None
254
+ write_model_list_cache(provider, pcfg, sorted_ids, metadata)
255
+ return sorted_ids
215
256
  if catalog_policy.use_bundled_catalog_fallback and not ids:
216
257
  ids = ollama_catalog_model_ids(provider)
217
258
  fetched = bool(ids)
218
259
  if not fetched and catalog_policy.allow_configured_fallback:
219
260
  ids = unique_model_ids(provider, [
261
+ *catalog_policy.fallback_models,
220
262
  *(pcfg.get("custom_models", []) or []),
221
263
  pcfg.get("current_model") or "",
222
264
  ])
@@ -244,6 +286,7 @@ def fetch_upstream_model_ids(provider: str, pcfg: dict[str, Any], force_refresh:
244
286
  ids.insert(0, cur)
245
287
  if catalog_policy.kind == "nvidia" and cur and cur not in ids:
246
288
  ids.insert(0, cur)
289
+ ids = supplement_model_aliases(ids, model_info)
247
290
  sorted_ids = unique_model_ids(provider, ids)
248
291
  if catalog_policy.kind != "anthropic":
249
292
  sorted_ids = sorted_model_ids(sorted_ids)
@@ -79,6 +79,25 @@ _OLLAMA_EXAMPLES = (
79
79
  " ciel-runtime --ca-ollama-option temperature=0.7 --ca-ollama-num-ctx 65536",
80
80
  )
81
81
 
82
+ DEFAULT_PROVIDER_NOTES: Mapping[str, Sequence[str]] = {
83
+ "opencode": (
84
+ " OpenCode endpoint override: endpoint:<model-id>=messages|chat|responses|gemini",
85
+ " OpenCode ip_family options: auto, ipv4, ipv6, ipv4-preferred, ipv6-preferred",
86
+ ),
87
+ "opencode-go": (
88
+ " OpenCode endpoint override: endpoint:<model-id>=messages|chat|responses|gemini",
89
+ " OpenCode ip_family options: auto, ipv4, ipv6, ipv4-preferred, ipv6-preferred",
90
+ ),
91
+ "fireworks": (
92
+ " Fireworks model list options: account_id=fireworks, model_api_base_url=https://api.fireworks.ai",
93
+ ),
94
+ }
95
+ DEFAULT_UNSUPPORTED_MESSAGE = (
96
+ "Provider options are available for anthropic, ollama, ollama-cloud, "
97
+ "deepseek, opencode, opencode-go, kimi, z.ai, fireworks, vllm, "
98
+ "lm-studio, nvidia-hosted, self-hosted-nim, and openrouter."
99
+ )
100
+
82
101
 
83
102
  class ProviderOptionCliController:
84
103
  def __init__(
@@ -94,7 +94,7 @@ def normalize_provider_request(provider: str, pcfg: dict[str, Any], body: dict[s
94
94
  out = sanitize_assistant_pseudo_tool_text_history(out)
95
95
  out = normalize_anthropic_tool_turns_for_provider(provider, pcfg, out)
96
96
  if profile.get("upstream_format") == "anthropic-messages":
97
- out = normalize_anthropic_system_role_messages(out)
97
+ out = normalize_anthropic_system_role_messages(provider, pcfg, out)
98
98
  return out
99
99
 
100
100
 
@@ -0,0 +1,189 @@
1
+ """Provider configuration status and launch-readiness bounded context.
2
+
3
+ This module owns the application-level coordination between provider adapters,
4
+ status projection, and launch-readiness validation. The compatibility facade
5
+ supplies concrete ports; no support module reaches back into ``ciel_runtime``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Callable, Mapping
12
+
13
+ from .architecture import ProviderAdapter, ProviderConfig
14
+ from .provider_readiness import (
15
+ ProviderReadinessServices,
16
+ launch_readiness_errors as evaluate_provider_readiness,
17
+ )
18
+ from .provider_status import (
19
+ ProviderStatusServices,
20
+ base_url_status_line as project_provider_base_url_status,
21
+ )
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class ProviderDefaultsPorts:
26
+ nvidia_upstream_base_url: Callable[[], str]
27
+ adapter_exists: Callable[[str], bool]
28
+ adapter_default_base_url: Callable[[str], str]
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class ProviderCredentialPorts:
33
+ key_count: Callable[[str, dict[str, Any]], int]
34
+ primary_key: Callable[[str, dict[str, Any]], str]
35
+ mask_secret: Callable[[str], str]
36
+ secret_fingerprint: Callable[[str], str]
37
+
38
+
39
+ @dataclass(frozen=True, slots=True)
40
+ class ProviderConfigurationPorts:
41
+ load: Callable[[], dict[str, Any]]
42
+ current: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
43
+ adapter: Callable[[str, dict[str, Any]], ProviderAdapter]
44
+ contract: Callable[[str, dict[str, Any]], ProviderConfig]
45
+
46
+
47
+ @dataclass(frozen=True, slots=True)
48
+ class ProviderProjectionPorts:
49
+ status_services: Callable[[], ProviderStatusServices]
50
+ readiness_services: Callable[[], ProviderReadinessServices]
51
+ notes: Mapping[str, Mapping[str, list[str]]]
52
+
53
+
54
+ @dataclass(frozen=True, slots=True)
55
+ class ProviderReadinessContext:
56
+ defaults: ProviderDefaultsPorts
57
+ credentials: ProviderCredentialPorts
58
+ configuration: ProviderConfigurationPorts
59
+ projection: ProviderProjectionPorts
60
+
61
+ def default_base_url(self, provider: str) -> str:
62
+ if provider == "nvidia-hosted":
63
+ return self.defaults.nvidia_upstream_base_url()
64
+ if self.defaults.adapter_exists(provider):
65
+ configured = self.defaults.adapter_default_base_url(provider)
66
+ if configured:
67
+ return configured
68
+ return "http://localhost:8000"
69
+
70
+ @staticmethod
71
+ def meaningful_key(value: str | None) -> bool:
72
+ text = str(value or "").strip()
73
+ return bool(text and text.lower() not in {"none", "null"})
74
+
75
+ def api_key_status_line(self, provider: str, pcfg: dict[str, Any]) -> str:
76
+ key_count = self.credentials.key_count(provider, pcfg)
77
+ primary = self.credentials.primary_key(provider, pcfg)
78
+ primary_detail = (
79
+ f"; primary {self.credentials.mask_secret(primary)}; "
80
+ f"fp {self.credentials.secret_fingerprint(primary)}"
81
+ if key_count
82
+ else ""
83
+ )
84
+ adapter = self.configuration.adapter(provider, pcfg)
85
+ return adapter.api_key_status(
86
+ self.configuration.contract(provider, pcfg),
87
+ key_count=key_count,
88
+ primary_detail=primary_detail,
89
+ )
90
+
91
+ def base_url_status_line(self, provider: str, pcfg: dict[str, Any]) -> str:
92
+ adapter = self.configuration.adapter(provider, pcfg)
93
+ status_policy = adapter.status_policy(
94
+ self.configuration.contract(provider, pcfg)
95
+ )
96
+ return project_provider_base_url_status(
97
+ provider,
98
+ pcfg,
99
+ status_policy,
100
+ services=self.projection.status_services(),
101
+ )
102
+
103
+ def preflight_lines(self) -> list[str]:
104
+ cfg = self.configuration.load()
105
+ provider, pcfg = self.configuration.current(cfg)
106
+ lang = str(cfg.get("language") or "en")
107
+ localized = self.projection.notes.get(
108
+ lang, self.projection.notes.get("en", {})
109
+ )
110
+ notes = localized.get(provider, [])
111
+ return [
112
+ self.base_url_status_line(provider, pcfg),
113
+ self.api_key_status_line(provider, pcfg),
114
+ *notes,
115
+ ]
116
+
117
+ def launch_readiness_errors(
118
+ self, cfg: dict[str, Any] | None = None
119
+ ) -> list[str]:
120
+ cfg = cfg or self.configuration.load()
121
+ provider, pcfg = self.configuration.current(cfg)
122
+ adapter = self.configuration.adapter(provider, pcfg)
123
+ contract = self.configuration.contract(provider, pcfg)
124
+ status_policy = adapter.status_policy(contract)
125
+ return evaluate_provider_readiness(
126
+ cfg,
127
+ provider,
128
+ pcfg,
129
+ adapter,
130
+ contract,
131
+ status_policy,
132
+ services=self.projection.readiness_services(),
133
+ )
134
+
135
+ @staticmethod
136
+ def launch_blockers_require_api_key(blockers: list[str]) -> bool:
137
+ return any(
138
+ "requires" in line.lower() and "api key" in line.lower()
139
+ for line in blockers
140
+ )
141
+
142
+ def settings_ready_except_api_key(self) -> bool:
143
+ cfg = self.configuration.load()
144
+ provider, pcfg = self.configuration.current(cfg)
145
+ if provider == "codex":
146
+ return True
147
+ base = pcfg.get("base_url", "")
148
+ model = pcfg.get("current_model", "")
149
+ return bool(provider and base and model and "your-" not in base)
150
+
151
+
152
+ @dataclass(frozen=True, slots=True)
153
+ class ProviderReadinessCompatibilityApi:
154
+ """Late-bound facade API that preserves patchable composition ports."""
155
+
156
+ context: Callable[[], ProviderReadinessContext]
157
+
158
+ def default_base_url(self, provider: str) -> str:
159
+ return self.context().default_base_url(provider)
160
+
161
+ def api_key_status_line(self, provider: str, pcfg: dict[str, Any]) -> str:
162
+ return self.context().api_key_status_line(provider, pcfg)
163
+
164
+ def base_url_status_line(self, provider: str, pcfg: dict[str, Any]) -> str:
165
+ return self.context().base_url_status_line(provider, pcfg)
166
+
167
+ def preflight_lines(self) -> list[str]:
168
+ return self.context().preflight_lines()
169
+
170
+ def launch_readiness_errors(
171
+ self, cfg: dict[str, Any] | None = None
172
+ ) -> list[str]:
173
+ return self.context().launch_readiness_errors(cfg)
174
+
175
+ def launch_blockers_require_api_key(self, blockers: list[str]) -> bool:
176
+ return self.context().launch_blockers_require_api_key(blockers)
177
+
178
+ def settings_ready_except_api_key(self) -> bool:
179
+ return self.context().settings_ready_except_api_key()
180
+
181
+
182
+ __all__ = [
183
+ "ProviderConfigurationPorts",
184
+ "ProviderCredentialPorts",
185
+ "ProviderDefaultsPorts",
186
+ "ProviderProjectionPorts",
187
+ "ProviderReadinessCompatibilityApi",
188
+ "ProviderReadinessContext",
189
+ ]
@@ -25,16 +25,9 @@ class ProviderRequestBudget:
25
25
  class OllamaRequestPorts:
26
26
  messages: Callable[[dict[str, Any]], list[dict[str, Any]]]
27
27
  tools: Callable[[Any], list[dict[str, Any]]]
28
- extra_options: Callable[[dict[str, Any]], dict[str, Any]]
29
28
  context_limit: Callable[[dict[str, Any]], int]
30
- num_ctx: Callable[..., int]
31
- think_value: Callable[
32
- [str, str | None, dict[str, Any], dict[str, Any]], bool | str | None
33
- ]
34
- # Model-card provenance gate for num_predict: None = omit the parameter
35
- # entirely so the server default applies (operator 2026-07-29). The
36
- # default implementation passes the capped value through unchanged.
37
- num_predict: Callable[[dict[str, Any], int | None], int | None] = lambda _config, capped: capped
29
+ num_ctx: Callable[..., int | None]
30
+ apply_optional: Callable[..., dict[str, Any]]
38
31
 
39
32
 
40
33
  @dataclass(frozen=True, slots=True)
@@ -48,6 +41,7 @@ class OpenAIRequestPorts:
48
41
  sampling_allowed: Callable[..., bool]
49
42
  omit_tool_choice: Callable[..., bool]
50
43
  tool_choice: Callable[[Any], Any]
44
+ normalize_request: Callable[[str, dict[str, Any], dict[str, Any]], dict[str, Any]]
51
45
 
52
46
 
53
47
  @dataclass(frozen=True, slots=True)
@@ -180,20 +174,12 @@ class ProviderRequestBuilder:
180
174
  "messages": messages,
181
175
  "stream": stream,
182
176
  }
183
- think = self.ollama.think_value(provider, model, config, body)
184
- if think is not None:
185
- request["think"] = think
186
- if config.get("keep_alive"):
187
- request["keep_alive"] = str(config["keep_alive"])
188
177
  if tools:
189
178
  request["tools"] = tools
190
- options = self.ollama.extra_options(config)
191
- # num_predict is governed by the output-budget policy below. Remove a
192
- # persisted/default copy so a deliberate provider-default decision can
193
- # actually omit it from the wire request.
194
- options.pop("num_predict", None)
195
179
  token_cache: dict[int, int] = {}
196
180
  num_ctx = self.ollama.num_ctx(config, payload, _token_cache=token_cache)
181
+ if not num_ctx:
182
+ num_ctx = self.ollama.context_limit(config)
197
183
  num_predict = self.budget.cap_output(
198
184
  config,
199
185
  body,
@@ -202,14 +188,14 @@ class ProviderRequestBuilder:
202
188
  configured,
203
189
  _token_cache=token_cache,
204
190
  )
205
- num_predict = self.ollama.num_predict(config, num_predict)
206
- if num_predict:
207
- options["num_predict"] = num_predict
208
- if num_ctx:
209
- options.setdefault("num_ctx", num_ctx)
210
- if options:
211
- request["options"] = options
212
- return request
191
+ return self.ollama.apply_optional(
192
+ request,
193
+ provider,
194
+ model,
195
+ config,
196
+ body,
197
+ output_limit=num_predict,
198
+ )
213
199
 
214
200
  def openai_chat(
215
201
  self,
@@ -252,7 +238,57 @@ class ProviderRequestBuilder:
252
238
  request["tool_choice"] = self.openai.tool_choice(body.get("tool_choice"))
253
239
  if configured:
254
240
  request["max_tokens"] = configured
241
+ if isinstance(body.get("response_format"), dict):
242
+ request["response_format"] = dict(body["response_format"])
255
243
  for key in ("temperature", "top_p"):
256
244
  if self.openai.sampling_allowed(provider, config) and config.get(key) is not None:
257
245
  request[key] = config[key]
258
- return request
246
+ return self.openai.normalize_request(provider, config, request)
247
+
248
+
249
+ @dataclass(frozen=True, slots=True)
250
+ class ProviderRequestCompatibilityApi:
251
+ builder: Callable[[], ProviderRequestBuilder]
252
+
253
+ def cap_anthropic_body(
254
+ self, provider: str, config: dict[str, Any], body: dict[str, Any]
255
+ ) -> dict[str, Any]:
256
+ return self.builder().cap_anthropic_body(provider, config, body)
257
+
258
+ def apply_options(
259
+ self, provider: str, config: dict[str, Any], body: dict[str, Any]
260
+ ) -> dict[str, Any]:
261
+ return self.builder().apply_options(provider, config, body)
262
+
263
+ def normalize_anthropic_options(
264
+ self,
265
+ provider: str,
266
+ config: dict[str, Any],
267
+ body: dict[str, Any],
268
+ model_id: str,
269
+ ) -> dict[str, Any]:
270
+ return self.builder().normalize_anthropic_options(provider, body, model_id)
271
+
272
+ def ollama_chat(
273
+ self,
274
+ model: str,
275
+ body: dict[str, Any],
276
+ config: dict[str, Any],
277
+ stream: bool = True,
278
+ provider: str = "ollama",
279
+ ) -> dict[str, Any]:
280
+ return self.builder().ollama_chat(
281
+ model, body, config, stream=stream, provider=provider
282
+ )
283
+
284
+ def openai_chat(
285
+ self,
286
+ provider: str,
287
+ model: str,
288
+ body: dict[str, Any],
289
+ config: dict[str, Any],
290
+ stream: bool = False,
291
+ ) -> dict[str, Any]:
292
+ return self.builder().openai_chat(
293
+ provider, model, body, config, stream=stream
294
+ )
@@ -7,6 +7,10 @@ import urllib.request
7
7
  from dataclasses import dataclass
8
8
  from typing import Any, Callable, Mapping
9
9
 
10
+ from .responses_usage_observer import ResponsesUsageObserver
11
+ from .responses_input_compatibility import repair_replayed_response_items
12
+ from .upstream_dump import dump_upstream_request
13
+
10
14
 
11
15
  @dataclass(frozen=True, slots=True)
12
16
  class ProviderResponsesPassthroughPorts:
@@ -24,6 +28,10 @@ class ProviderResponsesPassthroughPorts:
24
28
  urlopen: Callable[..., Any]
25
29
  timeout_seconds: Callable[[dict[str, Any]], float]
26
30
  copy_response_headers: Callable[[Any, Any], None]
31
+ record_usage: Callable[[str, str, dict[str, int]], None] = (
32
+ lambda _provider, _model, _usage: None
33
+ )
34
+ log: Callable[[str, str], Any] = lambda _level, _message: None
27
35
 
28
36
 
29
37
  class ProviderResponsesPassthrough:
@@ -39,7 +47,7 @@ class ProviderResponsesPassthrough:
39
47
  config: dict[str, Any],
40
48
  body: dict[str, Any],
41
49
  ) -> dict[str, Any]:
42
- upstream_body = dict(body)
50
+ upstream_body = dict(repair_replayed_response_items(body))
43
51
  upstream_body["model"] = self._ports.normalize_model(
44
52
  provider, config, str(body.get("model") or "")
45
53
  )
@@ -54,9 +62,11 @@ class ProviderResponsesPassthrough:
54
62
  self._ports.upstream_base(provider, config),
55
63
  "/v1/responses",
56
64
  )
65
+ data = json.dumps(upstream_body, ensure_ascii=False).encode("utf-8")
66
+ dump_upstream_request(url, data, self._ports.log)
57
67
  request = urllib.request.Request(
58
68
  url,
59
- data=json.dumps(upstream_body, ensure_ascii=False).encode("utf-8"),
69
+ data=data,
60
70
  headers=self._ports.headers(provider, config, handler.headers),
61
71
  method="POST",
62
72
  )
@@ -66,12 +76,21 @@ class ProviderResponsesPassthrough:
66
76
  provider=provider,
67
77
  pcfg=config,
68
78
  ) as response:
79
+ usage = ResponsesUsageObserver()
69
80
  handler.send_response(getattr(response, "status", 200))
70
81
  self._ports.copy_response_headers(handler, response.headers)
71
82
  handler.end_headers()
72
83
  while chunk := response.read(65_536):
84
+ usage.feed(chunk)
73
85
  handler.wfile.write(chunk)
74
86
  handler.wfile.flush()
87
+ observed = usage.finish()
88
+ if observed:
89
+ self._ports.record_usage(
90
+ provider,
91
+ str(upstream_body.get("model") or ""),
92
+ observed,
93
+ )
75
94
  return delivery_body
76
95
 
77
96
 
@@ -182,3 +182,57 @@ class ProviderTimeoutPolicy:
182
182
  f"Auto timeout: {timeout_ms}ms for context {self.ports.format_context(context)}.",
183
183
  f"stream_idle_timeout_ms: {idle_ms}",
184
184
  ]
185
+
186
+
187
+ @dataclass(frozen=True, slots=True)
188
+ class ProviderTimeoutCompatibilityApi:
189
+ policy: Callable[[], ProviderTimeoutPolicy]
190
+
191
+ def configured_context(
192
+ self, provider: str, config: dict[str, Any]
193
+ ) -> int | None:
194
+ return self.policy().configured_context(provider, config)
195
+
196
+ def configured_output(
197
+ self, provider: str, config: dict[str, Any]
198
+ ) -> int | None:
199
+ return self.policy().configured_output(provider, config)
200
+
201
+ def clamp(self, milliseconds: int | float | None) -> int:
202
+ return self.policy().clamp(milliseconds)
203
+
204
+ def calculated(
205
+ self,
206
+ provider: str,
207
+ config: dict[str, Any],
208
+ timeout_candidates: list[int] | None = None,
209
+ ) -> int:
210
+ return self.policy().calculated(provider, config, timeout_candidates)
211
+
212
+ def recommended(
213
+ self,
214
+ provider: str,
215
+ config: dict[str, Any],
216
+ use_context_fallback: bool = True,
217
+ ) -> int:
218
+ return self.policy().recommended(
219
+ provider, config, use_context_fallback=use_context_fallback
220
+ )
221
+
222
+ def apply(
223
+ self,
224
+ provider: str,
225
+ config: dict[str, Any],
226
+ use_context_fallback: bool = True,
227
+ ) -> list[str]:
228
+ return self.policy().apply(
229
+ provider, config, use_context_fallback=use_context_fallback
230
+ )
231
+
232
+
233
+ __all__ = [
234
+ "ProviderTimeoutCompatibilityApi",
235
+ "ProviderTimeoutPolicy",
236
+ "ProviderTimeoutPorts",
237
+ "ProviderTimeoutSettings",
238
+ ]
@@ -44,7 +44,12 @@ class ProviderToolPolicy:
44
44
  }
45
45
  adapter, contract = self._adapter(provider, config)
46
46
  if adapter.capabilities(contract).blocks_default_tools:
47
- return set(self.default_blocked_tools)
47
+ blocked = set(self.default_blocked_tools)
48
+ if adapter.supports_server_web_tools(contract):
49
+ blocked.difference_update(
50
+ {"WebSearch", "web_search", "WebFetch", "web_fetch"}
51
+ )
52
+ return blocked
48
53
  return set()
49
54
 
50
55
  def normalize_anthropic_stream_tool_use(
@@ -66,6 +71,9 @@ class ProviderToolPolicy:
66
71
  )
67
72
  model = self.strip_context_suffix(raw_model).lower()
68
73
  adapter, contract = self._adapter(provider, config)
74
+ request_support = getattr(adapter, "supports_tool_choice_for_request", None)
75
+ if callable(request_support):
76
+ return request_support(contract, model, body)
69
77
  return adapter.supports_tool_choice(contract, model)
70
78
 
71
79
  def tool_choice_status(
@@ -31,6 +31,10 @@ from .anthropic_catalog import (
31
31
  CatalogAnthropicProviderAdapter,
32
32
  )
33
33
  from .cloud import AzureOpenAIProviderAdapter, CodeBuddyCnProviderAdapter
34
+ from .alibaba import (
35
+ AlibabaIndividualTokenPlanProviderAdapter,
36
+ AlibabaTokenPlanProviderAdapter,
37
+ )
34
38
 
35
39
  __all__ = [
36
40
  "ANTHROPIC_COMPATIBLE_PROVIDER_SPECS",
@@ -40,6 +44,8 @@ __all__ = [
40
44
  "CodeBuddyCnProviderAdapter",
41
45
  "HttpBearerProviderAdapter",
42
46
  "AnthropicProviderAdapter",
47
+ "AlibabaTokenPlanProviderAdapter",
48
+ "AlibabaIndividualTokenPlanProviderAdapter",
43
49
  "NoAuthProviderAdapter",
44
50
  "OpenAICompatibleProviderAdapter",
45
51
  "PROVIDER_DEFAULT_BASE_URLS",