@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
|
@@ -34,6 +34,95 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
34
34
|
migrations = {}
|
|
35
35
|
cfg["migrations"] = migrations
|
|
36
36
|
|
|
37
|
+
marker = "alibaba_provider_catalogs_20260806"
|
|
38
|
+
if not migrations.get(marker):
|
|
39
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
40
|
+
catalogs = {
|
|
41
|
+
"alicode": (
|
|
42
|
+
"qwen3.7-plus", "qwen3.6-plus", "kimi-k2.5", "glm-5",
|
|
43
|
+
"MiniMax-M2.5", "qwen3.5-plus", "qwen3-max-2026-01-23",
|
|
44
|
+
"qwen3-coder-next", "qwen3-coder-plus", "glm-4.7",
|
|
45
|
+
),
|
|
46
|
+
"alicode-intl": (
|
|
47
|
+
"qwen3.7-plus", "qwen3.6-plus", "kimi-k2.5", "glm-5",
|
|
48
|
+
"MiniMax-M2.5", "qwen3.5-plus", "qwen3-max-2026-01-23",
|
|
49
|
+
"qwen3-coder-next", "qwen3-coder-plus", "glm-4.7",
|
|
50
|
+
),
|
|
51
|
+
"alims-intl": (
|
|
52
|
+
"qwen3.7-max", "qwen3.7-plus", "qwen3.6-plus",
|
|
53
|
+
"qwen3.6-flash", "qwen3.5-plus", "qwen3-coder-plus",
|
|
54
|
+
"qwen3-coder-flash", "deepseek-v4-pro", "deepseek-v4-flash",
|
|
55
|
+
"glm-5.2", "kimi-k2.7-code", "MiniMax-M2.5",
|
|
56
|
+
),
|
|
57
|
+
}
|
|
58
|
+
for provider_name, models in catalogs.items():
|
|
59
|
+
pcfg = providers.get(provider_name)
|
|
60
|
+
if not isinstance(pcfg, dict):
|
|
61
|
+
continue
|
|
62
|
+
custom = pcfg.get("custom_models")
|
|
63
|
+
if not isinstance(custom, list):
|
|
64
|
+
custom = []
|
|
65
|
+
pcfg["custom_models"] = custom
|
|
66
|
+
known = {
|
|
67
|
+
normalize_model_id(provider_name, str(model))
|
|
68
|
+
for model in custom
|
|
69
|
+
if str(model).strip()
|
|
70
|
+
}
|
|
71
|
+
for model in models:
|
|
72
|
+
if normalize_model_id(provider_name, model) not in known:
|
|
73
|
+
custom.append(model)
|
|
74
|
+
migrations[marker] = True
|
|
75
|
+
|
|
76
|
+
marker = "alibaba_token_plan_singapore_20260806"
|
|
77
|
+
if not migrations.get(marker):
|
|
78
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
79
|
+
pcfg = providers.get("alitoken")
|
|
80
|
+
if isinstance(pcfg, dict):
|
|
81
|
+
custom = pcfg.get("custom_models")
|
|
82
|
+
if not isinstance(custom, list):
|
|
83
|
+
custom = []
|
|
84
|
+
pcfg["custom_models"] = custom
|
|
85
|
+
models = (
|
|
86
|
+
"qwen3.8-max", "qwen3.7-max", "qwen3.7-plus",
|
|
87
|
+
"qwen3.6-plus", "qwen3.6-flash", "deepseek-v4-pro",
|
|
88
|
+
"deepseek-v4-flash", "deepseek-v3.2", "kimi-k2.7-code",
|
|
89
|
+
"kimi-k2.6", "kimi-k2.5", "glm-5.2", "glm-5.1", "glm-5",
|
|
90
|
+
"MiniMax-M2.5",
|
|
91
|
+
)
|
|
92
|
+
known = {
|
|
93
|
+
normalize_model_id("alitoken", str(model))
|
|
94
|
+
for model in custom
|
|
95
|
+
if str(model).strip()
|
|
96
|
+
}
|
|
97
|
+
custom.extend(
|
|
98
|
+
model
|
|
99
|
+
for model in models
|
|
100
|
+
if normalize_model_id("alitoken", model) not in known
|
|
101
|
+
)
|
|
102
|
+
pcfg["region"] = "ap-southeast-1"
|
|
103
|
+
migrations[marker] = True
|
|
104
|
+
|
|
105
|
+
marker = "alibaba_native_anthropic_routes_20260806"
|
|
106
|
+
if not migrations.get(marker):
|
|
107
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
108
|
+
for provider_name in (
|
|
109
|
+
"alicode", "alicode-intl", "alims-intl", "alitoken",
|
|
110
|
+
"alitoken-individual",
|
|
111
|
+
):
|
|
112
|
+
pcfg = providers.get(provider_name)
|
|
113
|
+
if isinstance(pcfg, dict):
|
|
114
|
+
pcfg["native_compat"] = True
|
|
115
|
+
migrations[marker] = True
|
|
116
|
+
|
|
117
|
+
marker = "alibaba_token_plan_individual_20260806"
|
|
118
|
+
if not migrations.get(marker):
|
|
119
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
120
|
+
pcfg = providers.get("alitoken-individual")
|
|
121
|
+
if isinstance(pcfg, dict):
|
|
122
|
+
pcfg["region"] = "ap-southeast-1"
|
|
123
|
+
pcfg["native_compat"] = True
|
|
124
|
+
migrations[marker] = True
|
|
125
|
+
|
|
37
126
|
marker = "ollama_cloud_deepseek_v4_flash_0731_20260803"
|
|
38
127
|
if not migrations.get(marker):
|
|
39
128
|
pcfg = cfg.get("providers", {}).get("ollama-cloud", {})
|
|
@@ -329,4 +418,18 @@ def apply_config_migrations(cfg: dict[str, Any], *, policy: ConfigMigrationPolic
|
|
|
329
418
|
pcfg["model_profile"] = "kimi-k3-1m"
|
|
330
419
|
migrations[marker] = True
|
|
331
420
|
|
|
421
|
+
marker = "kimi_k3_official_max_effort_20260806"
|
|
422
|
+
if not migrations.get(marker):
|
|
423
|
+
providers = cfg.get("providers") if isinstance(cfg.get("providers"), dict) else {}
|
|
424
|
+
pcfg = providers.get("kimi")
|
|
425
|
+
if isinstance(pcfg, dict):
|
|
426
|
+
current = normalize_model_id("kimi", str(pcfg.get("current_model") or ""))
|
|
427
|
+
if (
|
|
428
|
+
current in {KIMI_K3_MODEL, f"{KIMI_K3_MODEL}[1m]"}
|
|
429
|
+
and pcfg.get("model_profile") == "kimi-k3-1m"
|
|
430
|
+
and str(pcfg.get("effort_level") or "").lower() == "high"
|
|
431
|
+
):
|
|
432
|
+
pcfg["effort_level"] = "max"
|
|
433
|
+
migrations[marker] = True
|
|
434
|
+
|
|
332
435
|
__all__ = ["ConfigMigrationPolicy", "apply_config_migrations"]
|
|
@@ -364,6 +364,44 @@ class ConfigurationCliController:
|
|
|
364
364
|
return aliases.get(normalized, normalized)
|
|
365
365
|
|
|
366
366
|
|
|
367
|
+
@dataclass(frozen=True, slots=True)
|
|
368
|
+
class ConfigurationCliCompatibilityApi:
|
|
369
|
+
controller: Callable[[], ConfigurationCliController]
|
|
370
|
+
|
|
371
|
+
def provider(self, args: Any) -> None:
|
|
372
|
+
self.controller().provider_command(args.name)
|
|
373
|
+
|
|
374
|
+
def base_url(self, args: Any) -> None:
|
|
375
|
+
self.controller().base_url_command(args.provider, args.url)
|
|
376
|
+
|
|
377
|
+
def model(self, args: Any) -> None:
|
|
378
|
+
self.controller().model_command(args.value)
|
|
379
|
+
|
|
380
|
+
def advisor_model(self, args: Any) -> None:
|
|
381
|
+
self.controller().advisor_model_command(args.value)
|
|
382
|
+
|
|
383
|
+
def models(self, args: Any) -> None:
|
|
384
|
+
self.controller().models_command(args.provider)
|
|
385
|
+
|
|
386
|
+
def log_level(self, args: Any) -> None:
|
|
387
|
+
self.controller().log_level_command(getattr(args, "value", None))
|
|
388
|
+
|
|
389
|
+
def language(self, args: Any) -> None:
|
|
390
|
+
self.controller().language_command(args.value)
|
|
391
|
+
|
|
392
|
+
def web_search(self, args: Any) -> None:
|
|
393
|
+
self.controller().web_search_command(args.value)
|
|
394
|
+
|
|
395
|
+
def web_fetch(self, args: Any) -> None:
|
|
396
|
+
self.controller().web_fetch_command(args.value)
|
|
397
|
+
|
|
398
|
+
def portable_provider_menu(self) -> int:
|
|
399
|
+
return self.controller().portable_provider_menu()
|
|
400
|
+
|
|
401
|
+
def portable_language_menu(self) -> int:
|
|
402
|
+
return self.controller().portable_language_menu()
|
|
403
|
+
|
|
404
|
+
|
|
367
405
|
__all__ = [
|
|
368
406
|
"ConfigurationCliConfigPorts",
|
|
369
407
|
"ConfigurationCliController",
|
|
@@ -38,6 +38,7 @@ class ContextCompactionProjection:
|
|
|
38
38
|
build_fallback_summary: Callable[..., str]
|
|
39
39
|
build_reduce_prompt: Callable[..., str]
|
|
40
40
|
log: Callable[[str, str], None]
|
|
41
|
+
apply_ollama_optional: Callable[..., dict[str, Any]]
|
|
41
42
|
|
|
42
43
|
|
|
43
44
|
@dataclass(frozen=True)
|
|
@@ -69,11 +70,15 @@ def request_context_summary(
|
|
|
69
70
|
{"role": "user", "content": prompt},
|
|
70
71
|
],
|
|
71
72
|
"stream": False,
|
|
72
|
-
"think": False,
|
|
73
|
-
"options": {"num_predict": max_tokens},
|
|
74
73
|
}
|
|
75
|
-
|
|
76
|
-
request
|
|
74
|
+
request = services.projection.apply_ollama_optional(
|
|
75
|
+
request,
|
|
76
|
+
provider,
|
|
77
|
+
model,
|
|
78
|
+
provider_config,
|
|
79
|
+
{},
|
|
80
|
+
output_limit=max_tokens,
|
|
81
|
+
)
|
|
77
82
|
operation = "ollama_chat"
|
|
78
83
|
elif wire == "openai":
|
|
79
84
|
request = {
|
|
@@ -33,6 +33,7 @@ class CredentialPersistencePorts:
|
|
|
33
33
|
clear_requested: Callable[[Any], bool]
|
|
34
34
|
rotation_name: Callable[[str, dict[str, Any]], str]
|
|
35
35
|
error_text: Callable[[Any], bool] = lambda _value: False
|
|
36
|
+
plausible: Callable[[Any], bool] = lambda value: bool(str(value or "").strip())
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
@dataclass(frozen=True, slots=True)
|
|
@@ -125,6 +126,7 @@ class CredentialManagementService:
|
|
|
125
126
|
def store_one(self, provider: str, key: str) -> list[str]:
|
|
126
127
|
if self.persistence.clear_requested(key):
|
|
127
128
|
return self.clear(provider)
|
|
129
|
+
self._reject_invalid_keys([key])
|
|
128
130
|
config = self.persistence.load_config()
|
|
129
131
|
provider_config = config["providers"][provider]
|
|
130
132
|
if self.external.enabled(provider):
|
|
@@ -174,6 +176,7 @@ class CredentialManagementService:
|
|
|
174
176
|
return self.clear(provider)
|
|
175
177
|
if not parsed:
|
|
176
178
|
raise SystemExit("No API keys provided; unchanged.")
|
|
179
|
+
self._reject_invalid_keys(parsed)
|
|
177
180
|
config = self.persistence.load_config()
|
|
178
181
|
provider_config = config["providers"][provider]
|
|
179
182
|
provider_config["api_key"] = parsed[0]
|
|
@@ -215,6 +218,15 @@ class CredentialManagementService:
|
|
|
215
218
|
return self.store_one(provider, keys[0])
|
|
216
219
|
raise SystemExit("No API key provided; unchanged.")
|
|
217
220
|
|
|
221
|
+
def _reject_invalid_keys(self, keys: list[str]) -> None:
|
|
222
|
+
invalid = [key for key in keys if not self.persistence.plausible(key)]
|
|
223
|
+
if invalid:
|
|
224
|
+
raise SystemExit(
|
|
225
|
+
"Refusing to store an API key with an invalid token shape "
|
|
226
|
+
f"({len(invalid)} item(s)). Control characters and whitespace "
|
|
227
|
+
"are not valid inside API keys. Unchanged."
|
|
228
|
+
)
|
|
229
|
+
|
|
218
230
|
@staticmethod
|
|
219
231
|
def _snapshot_other_provider_keys(
|
|
220
232
|
providers: dict[str, Any], target_provider: str
|
|
@@ -58,6 +58,18 @@ def plausible_api_key(value: Any) -> bool:
|
|
|
58
58
|
if not _KEY_PLAUSIBLE_RE.match(text):
|
|
59
59
|
return False
|
|
60
60
|
return not looks_like_error_text(text)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def transportable_api_key(value: Any) -> bool:
|
|
64
|
+
"""Reject control/whitespace corruption without assuming provider key length."""
|
|
65
|
+
|
|
66
|
+
text = str(value or "")
|
|
67
|
+
return bool(
|
|
68
|
+
text
|
|
69
|
+
and text == text.strip()
|
|
70
|
+
and not looks_like_error_text(text)
|
|
71
|
+
and not any(character.isspace() or ord(character) < 32 or ord(character) == 127 for character in text)
|
|
72
|
+
)
|
|
61
73
|
SECRET_TEXT_PATTERNS = (
|
|
62
74
|
re.compile(r"ak_key_[A-Za-z0-9_-]+_secret_[A-Za-z0-9_-]+"),
|
|
63
75
|
re.compile(r"(AINET_API_KEY\s*=\s*)(\S+)", re.IGNORECASE),
|
|
@@ -20,8 +20,8 @@ GITHUB_USER_URL = "https://api.github.com/user"
|
|
|
20
20
|
GITHUB_COPILOT_TOKEN_URL = "https://api.github.com/copilot_internal/v2/token"
|
|
21
21
|
GITHUB_API_VERSION = "2022-11-28"
|
|
22
22
|
COPILOT_API_VERSION = "2025-04-01"
|
|
23
|
-
COPILOT_VSCODE_VERSION = "1.
|
|
24
|
-
COPILOT_CHAT_VERSION = "0.
|
|
23
|
+
COPILOT_VSCODE_VERSION = "1.128.0"
|
|
24
|
+
COPILOT_CHAT_VERSION = "0.43.0"
|
|
25
25
|
COPILOT_USER_AGENT = f"GitHubCopilotChat/{COPILOT_CHAT_VERSION}"
|
|
26
26
|
|
|
27
27
|
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Append-only orchestration for provider-hosted Formula tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import json
|
|
7
|
+
import threading
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any, Callable, Mapping
|
|
12
|
+
|
|
13
|
+
from .architecture import HostedToolPolicy
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True, slots=True)
|
|
17
|
+
class HostedFormulaState:
|
|
18
|
+
formula_by_function: Mapping[str, str]
|
|
19
|
+
headers: Mapping[str, str]
|
|
20
|
+
policy: HostedToolPolicy
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def enabled(self) -> bool:
|
|
24
|
+
return bool(self.formula_by_function)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class HostedFormulaToolService:
|
|
28
|
+
"""Load Formula schemas, execute calls, and append results to chat history."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
policy_for: Callable[[str, dict[str, Any]], HostedToolPolicy],
|
|
33
|
+
log: Callable[[str, str], None],
|
|
34
|
+
*,
|
|
35
|
+
open_url: Callable[..., Any] = urllib.request.urlopen,
|
|
36
|
+
max_rounds: int = 8,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._policy_for = policy_for
|
|
39
|
+
self._log = log
|
|
40
|
+
self._open_url = open_url
|
|
41
|
+
self._max_rounds = max(1, max_rounds)
|
|
42
|
+
self._cache: dict[tuple[str, str], tuple[dict[str, Any], ...]] = {}
|
|
43
|
+
self._unavailable: set[str] = set()
|
|
44
|
+
self._lock = threading.Lock()
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _auth_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
48
|
+
projected = {"content-type": "application/json"}
|
|
49
|
+
authorization = headers.get("authorization") or headers.get("Authorization")
|
|
50
|
+
if authorization:
|
|
51
|
+
projected["authorization"] = str(authorization)
|
|
52
|
+
return projected
|
|
53
|
+
|
|
54
|
+
def _json_request(
|
|
55
|
+
self,
|
|
56
|
+
url: str,
|
|
57
|
+
headers: Mapping[str, str],
|
|
58
|
+
*,
|
|
59
|
+
payload: Mapping[str, Any] | None = None,
|
|
60
|
+
timeout: float,
|
|
61
|
+
) -> dict[str, Any]:
|
|
62
|
+
data = None if payload is None else json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
63
|
+
request = urllib.request.Request(
|
|
64
|
+
url,
|
|
65
|
+
data=data,
|
|
66
|
+
headers=dict(headers),
|
|
67
|
+
method="GET" if data is None else "POST",
|
|
68
|
+
)
|
|
69
|
+
with self._open_url(request, timeout=timeout) as response:
|
|
70
|
+
decoded = json.loads(response.read().decode("utf-8", errors="replace"))
|
|
71
|
+
if not isinstance(decoded, dict):
|
|
72
|
+
raise RuntimeError("hosted Formula API returned non-object JSON")
|
|
73
|
+
return decoded
|
|
74
|
+
|
|
75
|
+
@staticmethod
|
|
76
|
+
def _formula_path(formula: str) -> str:
|
|
77
|
+
return urllib.parse.quote(formula, safe="/:")
|
|
78
|
+
|
|
79
|
+
def _tools_for_formula(
|
|
80
|
+
self,
|
|
81
|
+
policy: HostedToolPolicy,
|
|
82
|
+
formula: str,
|
|
83
|
+
headers: Mapping[str, str],
|
|
84
|
+
timeout: float,
|
|
85
|
+
) -> tuple[dict[str, Any], ...]:
|
|
86
|
+
cache_key = (policy.base_url, formula)
|
|
87
|
+
with self._lock:
|
|
88
|
+
cached = self._cache.get(cache_key)
|
|
89
|
+
if cached is not None:
|
|
90
|
+
return cached
|
|
91
|
+
payload = self._json_request(
|
|
92
|
+
f"{policy.base_url}/formulas/{self._formula_path(formula)}/tools",
|
|
93
|
+
headers,
|
|
94
|
+
timeout=timeout,
|
|
95
|
+
)
|
|
96
|
+
tools = tuple(copy.deepcopy(tool) for tool in payload.get("tools", ()) if isinstance(tool, dict))
|
|
97
|
+
with self._lock:
|
|
98
|
+
self._cache[cache_key] = tools
|
|
99
|
+
return tools
|
|
100
|
+
|
|
101
|
+
def prepare(
|
|
102
|
+
self,
|
|
103
|
+
provider: str,
|
|
104
|
+
config: dict[str, Any],
|
|
105
|
+
request_body: dict[str, Any],
|
|
106
|
+
upstream_headers: Mapping[str, str],
|
|
107
|
+
timeout: float,
|
|
108
|
+
) -> tuple[dict[str, Any], HostedFormulaState]:
|
|
109
|
+
policy = self._policy_for(provider, config)
|
|
110
|
+
auth_headers = self._auth_headers(upstream_headers)
|
|
111
|
+
empty = HostedFormulaState({}, auth_headers, policy)
|
|
112
|
+
if not policy.base_url or not policy.formulas or "authorization" not in auth_headers:
|
|
113
|
+
return request_body, empty
|
|
114
|
+
with self._lock:
|
|
115
|
+
if policy.base_url in self._unavailable:
|
|
116
|
+
return request_body, empty
|
|
117
|
+
timeout = min(max(1.0, timeout), 10.0)
|
|
118
|
+
existing = list(request_body.get("tools") or [])
|
|
119
|
+
used_names = {
|
|
120
|
+
str(tool.get("function", {}).get("name") or "")
|
|
121
|
+
for tool in existing
|
|
122
|
+
if isinstance(tool, dict) and isinstance(tool.get("function"), dict)
|
|
123
|
+
}
|
|
124
|
+
formula_by_function: dict[str, str] = {}
|
|
125
|
+
try:
|
|
126
|
+
for formula in policy.formulas:
|
|
127
|
+
for tool in self._tools_for_formula(policy, formula, auth_headers, timeout):
|
|
128
|
+
function = tool.get("function") if isinstance(tool.get("function"), dict) else {}
|
|
129
|
+
name = str(function.get("name") or "")
|
|
130
|
+
if not name or name in used_names:
|
|
131
|
+
continue
|
|
132
|
+
used_names.add(name)
|
|
133
|
+
formula_by_function[name] = formula
|
|
134
|
+
existing.append(tool)
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
with self._lock:
|
|
137
|
+
self._unavailable.add(policy.base_url)
|
|
138
|
+
self._log("WARN", f"hosted Formula tools unavailable for {provider}: {type(exc).__name__}: {exc}")
|
|
139
|
+
return request_body, empty
|
|
140
|
+
if not formula_by_function:
|
|
141
|
+
return request_body, empty
|
|
142
|
+
prepared = {**request_body, "tools": existing, "stream": False}
|
|
143
|
+
return prepared, HostedFormulaState(formula_by_function, auth_headers, policy)
|
|
144
|
+
|
|
145
|
+
def _execute(
|
|
146
|
+
self,
|
|
147
|
+
state: HostedFormulaState,
|
|
148
|
+
function: Mapping[str, Any],
|
|
149
|
+
timeout: float,
|
|
150
|
+
) -> str:
|
|
151
|
+
name = str(function.get("name") or "")
|
|
152
|
+
formula = state.formula_by_function[name]
|
|
153
|
+
payload = self._json_request(
|
|
154
|
+
f"{state.policy.base_url}/formulas/{self._formula_path(formula)}/fibers",
|
|
155
|
+
state.headers,
|
|
156
|
+
payload={"function": {"name": name, "arguments": str(function.get("arguments") or "{}")}},
|
|
157
|
+
timeout=timeout,
|
|
158
|
+
)
|
|
159
|
+
context = payload.get("context") if isinstance(payload.get("context"), dict) else {}
|
|
160
|
+
output = context.get("output")
|
|
161
|
+
if output is None:
|
|
162
|
+
output = context.get("encrypted_output")
|
|
163
|
+
if isinstance(output, str):
|
|
164
|
+
return output
|
|
165
|
+
return json.dumps(output if output is not None else payload, ensure_ascii=False)
|
|
166
|
+
|
|
167
|
+
def resolve(
|
|
168
|
+
self,
|
|
169
|
+
state: HostedFormulaState,
|
|
170
|
+
request_body: dict[str, Any],
|
|
171
|
+
response: dict[str, Any],
|
|
172
|
+
post_chat: Callable[[dict[str, Any]], dict[str, Any]],
|
|
173
|
+
timeout: float,
|
|
174
|
+
) -> dict[str, Any]:
|
|
175
|
+
if not state.enabled:
|
|
176
|
+
return response
|
|
177
|
+
body = copy.deepcopy(request_body)
|
|
178
|
+
current = response
|
|
179
|
+
for _round in range(self._max_rounds):
|
|
180
|
+
choices = current.get("choices") if isinstance(current, dict) else None
|
|
181
|
+
choice = choices[0] if isinstance(choices, list) and choices and isinstance(choices[0], dict) else {}
|
|
182
|
+
message = choice.get("message") if isinstance(choice.get("message"), dict) else {}
|
|
183
|
+
calls = [call for call in message.get("tool_calls", ()) if isinstance(call, dict)]
|
|
184
|
+
if not calls:
|
|
185
|
+
return current
|
|
186
|
+
functions = [call.get("function") if isinstance(call.get("function"), dict) else {} for call in calls]
|
|
187
|
+
if any(str(function.get("name") or "") not in state.formula_by_function for function in functions):
|
|
188
|
+
return current
|
|
189
|
+
body.setdefault("messages", []).append(copy.deepcopy(message))
|
|
190
|
+
for call, function in zip(calls, functions):
|
|
191
|
+
body["messages"].append(
|
|
192
|
+
{
|
|
193
|
+
"role": "tool",
|
|
194
|
+
"tool_call_id": str(call.get("id") or "hosted_tool"),
|
|
195
|
+
"content": self._execute(state, function, timeout),
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
body["tool_choice"] = "auto"
|
|
199
|
+
current = post_chat(body)
|
|
200
|
+
raise RuntimeError(f"hosted Formula tool loop exceeded {self._max_rounds} rounds")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
__all__ = ["HostedFormulaState", "HostedFormulaToolService"]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
_SHARED_SERVICE: HostedFormulaToolService | None = None
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def shared_service(
|
|
210
|
+
policy_for: Callable[[str, dict[str, Any]], HostedToolPolicy],
|
|
211
|
+
log: Callable[[str, str], None],
|
|
212
|
+
) -> HostedFormulaToolService:
|
|
213
|
+
global _SHARED_SERVICE
|
|
214
|
+
if _SHARED_SERVICE is None:
|
|
215
|
+
_SHARED_SERVICE = HostedFormulaToolService(policy_for, log)
|
|
216
|
+
return _SHARED_SERVICE
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Kimi Code installation, OAuth identity, and launch bounded context."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True, slots=True)
|
|
11
|
+
class KimiIdentityPorts:
|
|
12
|
+
home: Path
|
|
13
|
+
code_home: Callable[[Path], Path]
|
|
14
|
+
token_record: Callable[[Path], dict[str, Any] | None]
|
|
15
|
+
access_token: Callable[[Path], str | None]
|
|
16
|
+
configured: Callable[[Path], bool]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class KimiProcessPorts:
|
|
21
|
+
find_executable: Callable[[str], str | None]
|
|
22
|
+
run: Callable[..., Any]
|
|
23
|
+
call: Callable[..., int]
|
|
24
|
+
print_line: Callable[..., None]
|
|
25
|
+
environment: dict[str, str]
|
|
26
|
+
augment_path: Callable[[dict[str, str]], str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class KimiConfigurationPorts:
|
|
31
|
+
load: Callable[[], dict[str, Any]]
|
|
32
|
+
current_provider: Callable[[dict[str, Any]], tuple[str, dict[str, Any]]]
|
|
33
|
+
provider_has_key: Callable[[str, dict[str, Any]], bool]
|
|
34
|
+
current_alias: Callable[[dict[str, Any]], str]
|
|
35
|
+
positive_int: Callable[[Any], int | None]
|
|
36
|
+
clear_api_key: Callable[[str], list[str]]
|
|
37
|
+
router_base: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True, slots=True)
|
|
41
|
+
class KimiLifecyclePorts:
|
|
42
|
+
install: Callable[[], str]
|
|
43
|
+
oauth_configured: Callable[[], bool]
|
|
44
|
+
oauth_login: Callable[[], int]
|
|
45
|
+
start_router: Callable[[], Any]
|
|
46
|
+
run_with_router: Callable[[Callable[[], int], bool], int]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, slots=True)
|
|
50
|
+
class KimiRuntimeContext:
|
|
51
|
+
identity: KimiIdentityPorts
|
|
52
|
+
process: KimiProcessPorts
|
|
53
|
+
config: KimiConfigurationPorts
|
|
54
|
+
lifecycle: KimiLifecyclePorts
|
|
55
|
+
|
|
56
|
+
def code_home(self) -> Path:
|
|
57
|
+
return self.identity.code_home(self.identity.home)
|
|
58
|
+
|
|
59
|
+
def oauth_token_record(self) -> dict[str, Any] | None:
|
|
60
|
+
return self.identity.token_record(self.identity.home)
|
|
61
|
+
|
|
62
|
+
def oauth_access_token(self) -> str | None:
|
|
63
|
+
return self.identity.access_token(self.identity.home)
|
|
64
|
+
|
|
65
|
+
def oauth_configured(self) -> bool:
|
|
66
|
+
return self.identity.configured(self.identity.home)
|
|
67
|
+
|
|
68
|
+
def install_if_missing(self) -> str:
|
|
69
|
+
executable = self.process.find_executable("kimi")
|
|
70
|
+
if executable:
|
|
71
|
+
return executable
|
|
72
|
+
npm = self.process.find_executable("npm")
|
|
73
|
+
if not npm:
|
|
74
|
+
raise RuntimeError(
|
|
75
|
+
"Kimi Code CLI is missing; install @moonshot-ai/kimi-code "
|
|
76
|
+
"(Node.js 22.19+)."
|
|
77
|
+
)
|
|
78
|
+
self.process.print_line(
|
|
79
|
+
"Installing official Kimi Code CLI (@moonshot-ai/kimi-code)...",
|
|
80
|
+
flush=True,
|
|
81
|
+
)
|
|
82
|
+
result = self.process.run(
|
|
83
|
+
[npm, "install", "-g", "@moonshot-ai/kimi-code"], check=False
|
|
84
|
+
)
|
|
85
|
+
if result.returncode:
|
|
86
|
+
raise RuntimeError(
|
|
87
|
+
f"Kimi Code CLI installation failed (exit {result.returncode})."
|
|
88
|
+
)
|
|
89
|
+
executable = self.process.find_executable("kimi")
|
|
90
|
+
if not executable:
|
|
91
|
+
raise RuntimeError(
|
|
92
|
+
"Kimi Code CLI installed but 'kimi' is not available on PATH."
|
|
93
|
+
)
|
|
94
|
+
return executable
|
|
95
|
+
|
|
96
|
+
def oauth_login(self) -> int:
|
|
97
|
+
return self.process.call([self.lifecycle.install(), "login"])
|
|
98
|
+
|
|
99
|
+
def oauth_action(self, action: str) -> list[str]:
|
|
100
|
+
if action != "login":
|
|
101
|
+
return [f"Unsupported Kimi OAuth action: {action}"]
|
|
102
|
+
try:
|
|
103
|
+
code = self.lifecycle.oauth_login()
|
|
104
|
+
except Exception as exc:
|
|
105
|
+
return [f"Kimi OAuth login failed: {type(exc).__name__}: {exc}"]
|
|
106
|
+
if code:
|
|
107
|
+
return [f"Kimi OAuth login exited with status {code}."]
|
|
108
|
+
if not self.lifecycle.oauth_configured():
|
|
109
|
+
return [
|
|
110
|
+
"Kimi OAuth login exited successfully, but no usable credential "
|
|
111
|
+
"was detected; the existing Kimi API key was not cleared."
|
|
112
|
+
]
|
|
113
|
+
messages = [
|
|
114
|
+
"Kimi OAuth login completed in the official Kimi Code credential store."
|
|
115
|
+
]
|
|
116
|
+
messages.extend(self.config.clear_api_key("kimi"))
|
|
117
|
+
return messages
|
|
118
|
+
|
|
119
|
+
def launch(self, passthrough: list[str]) -> int:
|
|
120
|
+
cfg = self.config.load()
|
|
121
|
+
provider, pcfg = self.config.current_provider(cfg)
|
|
122
|
+
if provider != "kimi":
|
|
123
|
+
self.process.print_line(
|
|
124
|
+
"Launch Kimi Code requires Kimi Native or Kimi Routed provider.",
|
|
125
|
+
flush=True,
|
|
126
|
+
)
|
|
127
|
+
return 2
|
|
128
|
+
executable = self.lifecycle.install()
|
|
129
|
+
routed = bool(pcfg.get("route_through_router"))
|
|
130
|
+
env = self.process.environment.copy()
|
|
131
|
+
env["PATH"] = self.process.augment_path(env)
|
|
132
|
+
if not routed:
|
|
133
|
+
if not self.lifecycle.oauth_configured():
|
|
134
|
+
self.process.print_line(
|
|
135
|
+
"Kimi Code OAuth login is required for first launch.", flush=True
|
|
136
|
+
)
|
|
137
|
+
if self.process.call([executable, "login"], env=env):
|
|
138
|
+
return 1
|
|
139
|
+
return self.process.call([executable, *passthrough], env=env)
|
|
140
|
+
if (
|
|
141
|
+
not self.config.provider_has_key(provider, pcfg)
|
|
142
|
+
and not self.lifecycle.oauth_configured()
|
|
143
|
+
):
|
|
144
|
+
self.process.print_line(
|
|
145
|
+
"Kimi Routed requires Kimi OAuth login or a Kimi API key.",
|
|
146
|
+
flush=True,
|
|
147
|
+
)
|
|
148
|
+
if self.process.call([executable, "login"], env=env):
|
|
149
|
+
return 1
|
|
150
|
+
manage_router = bool(self.lifecycle.start_router())
|
|
151
|
+
env.update(
|
|
152
|
+
{
|
|
153
|
+
"KIMI_MODEL_NAME": self.config.current_alias(cfg)
|
|
154
|
+
or str(pcfg.get("current_model") or "kimi-for-coding"),
|
|
155
|
+
"KIMI_MODEL_API_KEY": "ciel-runtime-router-local-key",
|
|
156
|
+
"KIMI_MODEL_PROVIDER_TYPE": "openai",
|
|
157
|
+
"KIMI_MODEL_BASE_URL": f"{self.config.router_base.rstrip('/')}/v1",
|
|
158
|
+
"KIMI_MODEL_MAX_CONTEXT_SIZE": str(
|
|
159
|
+
self.config.positive_int(pcfg.get("context_window")) or 262144
|
|
160
|
+
),
|
|
161
|
+
"KIMI_MODEL_THINKING_EFFORT": str(
|
|
162
|
+
pcfg.get("effort_level") or "high"
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
return self.lifecycle.run_with_router(
|
|
167
|
+
lambda: self.process.call([executable, *passthrough], env=env),
|
|
168
|
+
manage_router,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@dataclass(frozen=True, slots=True)
|
|
173
|
+
class KimiRuntimeCompatibilityApi:
|
|
174
|
+
context: Callable[[], KimiRuntimeContext]
|
|
175
|
+
|
|
176
|
+
def code_home(self) -> Path:
|
|
177
|
+
return self.context().code_home()
|
|
178
|
+
|
|
179
|
+
def oauth_token_record(self) -> dict[str, Any] | None:
|
|
180
|
+
return self.context().oauth_token_record()
|
|
181
|
+
|
|
182
|
+
def oauth_access_token(self) -> str | None:
|
|
183
|
+
return self.context().oauth_access_token()
|
|
184
|
+
|
|
185
|
+
def oauth_configured(self) -> bool:
|
|
186
|
+
return self.context().oauth_configured()
|
|
187
|
+
|
|
188
|
+
def install_if_missing(self) -> str:
|
|
189
|
+
return self.context().install_if_missing()
|
|
190
|
+
|
|
191
|
+
def oauth_login(self) -> int:
|
|
192
|
+
return self.context().oauth_login()
|
|
193
|
+
|
|
194
|
+
def oauth_action(self, action: str) -> list[str]:
|
|
195
|
+
return self.context().oauth_action(action)
|
|
196
|
+
|
|
197
|
+
def launch(self, passthrough: list[str]) -> int:
|
|
198
|
+
return self.context().launch(passthrough)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
__all__ = [
|
|
202
|
+
"KimiConfigurationPorts",
|
|
203
|
+
"KimiIdentityPorts",
|
|
204
|
+
"KimiLifecyclePorts",
|
|
205
|
+
"KimiProcessPorts",
|
|
206
|
+
"KimiRuntimeCompatibilityApi",
|
|
207
|
+
"KimiRuntimeContext",
|
|
208
|
+
]
|