@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.
- package/ciel_runtime.py +2555 -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/config_repository.py +30 -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 +245 -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 +84 -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/speech_http_controller.py +335 -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/web_ui.py +248 -1
- package/ciel_runtime_support/workspace_router_selection.py +86 -0
- package/docs/COLAB_SPEECH.md +32 -0
- package/docs/Configuration.md +50 -0
- package/docs/Test-Suite.md +1 -0
- package/package.json +4 -1
- package/scripts/colab/__pycache__/bootstrap_moss_tts.cpython-311.pyc +0 -0
- package/scripts/colab/__pycache__/bootstrap_qwen_asr.cpython-311.pyc +0 -0
- package/scripts/colab/bootstrap_moss_tts.py +106 -0
- package/scripts/colab/bootstrap_qwen_asr.py +106 -0
- package/scripts/configure_speech_workers.py +37 -0
- 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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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=
|
|
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
|
-
|
|
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",
|