@oneciel-ai/ciel-runtime 0.1.1 → 0.2.0
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/README.md +132 -0
- package/ciel-runtime-menu.py +56 -6
- package/ciel_runtime.py +9379 -35909
- package/ciel_runtime_support/advisor_client.py +193 -0
- package/ciel_runtime_support/advisor_policy.py +320 -0
- package/ciel_runtime_support/advisor_refinement.py +160 -0
- package/ciel_runtime_support/advisor_request_builder.py +261 -0
- package/ciel_runtime_support/agy_installer.py +169 -0
- package/ciel_runtime_support/agy_mcp_restore.py +182 -0
- package/ciel_runtime_support/anthropic_model_policy.py +186 -0
- package/ciel_runtime_support/anthropic_response_writer.py +255 -0
- package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
- package/ciel_runtime_support/api_key_cooldown.py +159 -0
- package/ciel_runtime_support/architecture.py +488 -1
- package/ciel_runtime_support/architecture_budget.py +42 -0
- package/ciel_runtime_support/channel_backlog.py +90 -0
- package/ciel_runtime_support/channel_cli.py +119 -0
- package/ciel_runtime_support/channel_compact_injection.py +82 -0
- package/ciel_runtime_support/channel_compact_poll.py +67 -0
- package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
- package/ciel_runtime_support/channel_config_service.py +281 -0
- package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
- package/ciel_runtime_support/channel_connection_registry.py +128 -0
- package/ciel_runtime_support/channel_connection_worker.py +284 -0
- package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
- package/ciel_runtime_support/channel_cursor_repository.py +89 -0
- package/ciel_runtime_support/channel_cursor_service.py +178 -0
- package/ciel_runtime_support/channel_event_identity.py +212 -0
- package/ciel_runtime_support/channel_event_projection.py +315 -0
- package/ciel_runtime_support/channel_inflight.py +127 -0
- package/ciel_runtime_support/channel_injection.py +115 -0
- package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
- package/ciel_runtime_support/channel_launch_policy.py +180 -0
- package/ciel_runtime_support/channel_llm_context.py +156 -0
- package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
- package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
- package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
- package/ciel_runtime_support/channel_mcp_tools.py +240 -0
- package/ciel_runtime_support/channel_mcp_transport.py +394 -0
- package/ciel_runtime_support/channel_message_dedupe.py +65 -0
- package/ciel_runtime_support/channel_message_policy.py +256 -0
- package/ciel_runtime_support/channel_message_prompt.py +305 -0
- package/ciel_runtime_support/channel_message_repository.py +234 -0
- package/ciel_runtime_support/channel_notification_projection.py +217 -0
- package/ciel_runtime_support/channel_panel.py +162 -0
- package/ciel_runtime_support/channel_pending_injection.py +209 -0
- package/ciel_runtime_support/channel_pending_poll.py +109 -0
- package/ciel_runtime_support/channel_probe_cache.py +433 -0
- package/ciel_runtime_support/channel_probe_report.py +101 -0
- package/ciel_runtime_support/channel_runtime_environment.py +181 -0
- package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
- package/ciel_runtime_support/channel_session_repository.py +90 -0
- package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
- package/ciel_runtime_support/channel_terminal_input.py +277 -0
- package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
- package/ciel_runtime_support/channel_tool_context.py +166 -0
- package/ciel_runtime_support/channel_transcript.py +414 -0
- package/ciel_runtime_support/channel_transcript_repository.py +96 -0
- package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
- package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
- package/ciel_runtime_support/chat_files.py +138 -0
- package/ciel_runtime_support/chat_http_controller.py +235 -0
- package/ciel_runtime_support/claude_environment.py +375 -0
- package/ciel_runtime_support/claude_router.py +247 -193
- package/ciel_runtime_support/cli_dispatch.py +792 -0
- package/ciel_runtime_support/cli_parser.py +165 -0
- package/ciel_runtime_support/cli_usage.py +100 -0
- package/ciel_runtime_support/codex_app_server.py +20 -5
- package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
- package/ciel_runtime_support/codex_cli.py +42 -6
- package/ciel_runtime_support/codex_config.py +323 -0
- package/ciel_runtime_support/codex_launch_configuration.py +240 -0
- package/ciel_runtime_support/codex_launch_policy.py +66 -0
- package/ciel_runtime_support/codex_mcp_integration.py +195 -0
- package/ciel_runtime_support/codex_mcp_restore.py +304 -0
- package/ciel_runtime_support/codex_model_catalog.py +133 -0
- package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
- package/ciel_runtime_support/codex_router.py +147 -1
- package/ciel_runtime_support/codex_session_repository.py +115 -0
- package/ciel_runtime_support/codex_session_selection.py +114 -0
- package/ciel_runtime_support/command_asset_installer.py +103 -0
- package/ciel_runtime_support/compatibility_probe.py +295 -0
- package/ciel_runtime_support/compatibility_protocol.py +251 -0
- package/ciel_runtime_support/compatibility_runtime.py +166 -0
- package/ciel_runtime_support/compatibility_test.py +370 -0
- package/ciel_runtime_support/config_migrations.py +307 -0
- package/ciel_runtime_support/config_repository.py +175 -0
- package/ciel_runtime_support/config_value_codec.py +64 -0
- package/ciel_runtime_support/configuration_cli.py +374 -0
- package/ciel_runtime_support/context_compaction.py +280 -0
- package/ciel_runtime_support/context_setup.py +208 -0
- package/ciel_runtime_support/context_summary_policy.py +392 -0
- package/ciel_runtime_support/credential_cli.py +104 -0
- package/ciel_runtime_support/credential_management.py +261 -0
- package/ciel_runtime_support/credentials.py +269 -0
- package/ciel_runtime_support/executable_discovery.py +141 -0
- package/ciel_runtime_support/github_copilot_oauth.py +335 -0
- package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
- package/ciel_runtime_support/header_forwarding.py +73 -0
- package/ciel_runtime_support/headless_config.py +221 -0
- package/ciel_runtime_support/http_response.py +129 -0
- package/ciel_runtime_support/install_diagnostics.py +149 -0
- package/ciel_runtime_support/kimi_identity.py +123 -0
- package/ciel_runtime_support/launch_diagnostics.py +204 -0
- package/ciel_runtime_support/launch_state.py +127 -0
- package/ciel_runtime_support/live_api_key_controller.py +58 -0
- package/ciel_runtime_support/llm_config_http.py +148 -0
- package/ciel_runtime_support/llm_option_config.py +259 -0
- package/ciel_runtime_support/llm_presentation_data.py +447 -0
- package/ciel_runtime_support/llm_presets.py +773 -0
- package/ciel_runtime_support/lm_studio_runtime.py +401 -0
- package/ciel_runtime_support/managed_mcp_config.py +144 -0
- package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
- package/ciel_runtime_support/managed_service_cleanup.py +89 -0
- package/ciel_runtime_support/mcp_config_reader.py +230 -0
- package/ciel_runtime_support/mcp_http_proxy.py +607 -0
- package/ciel_runtime_support/mcp_inventory.py +59 -0
- package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
- package/ciel_runtime_support/mcp_probe_codec.py +136 -0
- package/ciel_runtime_support/mcp_probe_transport.py +328 -0
- package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
- package/ciel_runtime_support/mcp_proxy_config.py +107 -0
- package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
- package/ciel_runtime_support/mcp_proxy_process.py +560 -0
- package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
- package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
- package/ciel_runtime_support/mcp_transport.py +146 -0
- package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
- package/ciel_runtime_support/model_catalog_projection.py +61 -0
- package/ciel_runtime_support/model_context_hints.py +109 -0
- package/ciel_runtime_support/model_panel.py +147 -0
- package/ciel_runtime_support/model_registry_repository.py +231 -0
- package/ciel_runtime_support/npm_runtime.py +191 -0
- package/ciel_runtime_support/ollama_catalog.py +462 -0
- package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
- package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
- package/ciel_runtime_support/ollama_context_sync.py +87 -0
- package/ciel_runtime_support/ollama_forwarding.py +449 -0
- package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
- package/ciel_runtime_support/openai_chat_router.py +64 -0
- package/ciel_runtime_support/openai_forwarding.py +194 -0
- package/ciel_runtime_support/openai_responses_router.py +291 -0
- package/ciel_runtime_support/openai_responses_stream.py +135 -0
- package/ciel_runtime_support/output_budget.py +89 -0
- package/ciel_runtime_support/package_lifecycle.py +217 -0
- package/ciel_runtime_support/plan_artifact_controller.py +104 -0
- package/ciel_runtime_support/prelaunch.py +959 -0
- package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
- package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
- package/ciel_runtime_support/prelaunch_terminal.py +764 -0
- package/ciel_runtime_support/process_control.py +708 -0
- package/ciel_runtime_support/prompt_compaction.py +322 -0
- package/ciel_runtime_support/prompt_injection.py +176 -0
- package/ciel_runtime_support/protocols/__init__.py +24 -0
- package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
- package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
- package/ciel_runtime_support/protocols/chat_projection.py +315 -0
- package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
- package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
- package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
- package/ciel_runtime_support/protocols/ollama_response.py +231 -0
- package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
- package/ciel_runtime_support/protocols/openai_responses.py +271 -0
- package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
- package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
- package/ciel_runtime_support/provider_adapters.py +160 -0
- package/ciel_runtime_support/provider_catalog_sources.py +316 -0
- package/ciel_runtime_support/provider_choice.py +201 -0
- package/ciel_runtime_support/provider_compatibility.py +165 -0
- package/ciel_runtime_support/provider_config_mutations.py +361 -0
- package/ciel_runtime_support/provider_configuration_service.py +162 -0
- package/ciel_runtime_support/provider_context.py +308 -0
- package/ciel_runtime_support/provider_contract_projection.py +73 -0
- package/ciel_runtime_support/provider_descriptor.py +82 -0
- package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
- package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
- package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
- package/ciel_runtime_support/provider_limits.py +457 -0
- package/ciel_runtime_support/provider_model_identity.py +139 -0
- package/ciel_runtime_support/provider_model_selection.py +431 -0
- package/ciel_runtime_support/provider_model_specs.py +142 -0
- package/ciel_runtime_support/provider_models.py +263 -0
- package/ciel_runtime_support/provider_network.py +176 -0
- package/ciel_runtime_support/provider_option_cli.py +238 -0
- package/ciel_runtime_support/provider_option_panel.py +275 -0
- package/ciel_runtime_support/provider_option_status.py +192 -0
- package/ciel_runtime_support/provider_policy.py +101 -0
- package/ciel_runtime_support/provider_query_policy.py +67 -0
- package/ciel_runtime_support/provider_readiness.py +112 -0
- package/ciel_runtime_support/provider_request_access.py +131 -0
- package/ciel_runtime_support/provider_request_builder.py +250 -0
- package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
- package/ciel_runtime_support/provider_runtime_info.py +113 -0
- package/ciel_runtime_support/provider_runtime_modes.py +150 -0
- package/ciel_runtime_support/provider_sampling_policy.py +46 -0
- package/ciel_runtime_support/provider_status.py +145 -0
- package/ciel_runtime_support/provider_timeout_policy.py +184 -0
- package/ciel_runtime_support/provider_tool_policy.py +145 -0
- package/ciel_runtime_support/providers/__init__.py +65 -0
- package/ciel_runtime_support/providers/anthropic.py +160 -0
- package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
- package/ciel_runtime_support/providers/base.py +232 -0
- package/ciel_runtime_support/providers/catalog.py +326 -0
- package/ciel_runtime_support/providers/cloud.py +194 -0
- package/ciel_runtime_support/providers/constants.py +54 -0
- package/ciel_runtime_support/providers/deepseek.py +115 -0
- package/ciel_runtime_support/providers/fireworks.py +158 -0
- package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
- package/ciel_runtime_support/providers/kimi.py +297 -0
- package/ciel_runtime_support/providers/lm_studio.py +76 -0
- package/ciel_runtime_support/providers/meta.py +257 -0
- package/ciel_runtime_support/providers/native.py +194 -0
- package/ciel_runtime_support/providers/nim.py +69 -0
- package/ciel_runtime_support/providers/nvidia.py +158 -0
- package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
- package/ciel_runtime_support/providers/ollama.py +181 -0
- package/ciel_runtime_support/providers/ollama_context.py +195 -0
- package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
- package/ciel_runtime_support/providers/opencode.py +220 -0
- package/ciel_runtime_support/providers/opencode_go.py +37 -0
- package/ciel_runtime_support/providers/openrouter.py +57 -0
- package/ciel_runtime_support/providers/vllm.py +65 -0
- package/ciel_runtime_support/providers/zai.py +119 -0
- package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
- package/ciel_runtime_support/rate_limit_policy.py +117 -0
- package/ciel_runtime_support/rate_limit_repository.py +154 -0
- package/ciel_runtime_support/registry.py +46 -0
- package/ciel_runtime_support/request_shortcuts.py +253 -0
- package/ciel_runtime_support/request_trace.py +323 -0
- package/ciel_runtime_support/response_collection.py +209 -0
- package/ciel_runtime_support/router_access.py +238 -0
- package/ciel_runtime_support/router_client_lifecycle.py +366 -0
- package/ciel_runtime_support/router_health_policy.py +101 -0
- package/ciel_runtime_support/router_http.py +513 -0
- package/ciel_runtime_support/router_process_lifecycle.py +401 -0
- package/ciel_runtime_support/router_rate_limit_service.py +285 -0
- package/ciel_runtime_support/router_server_runtime.py +103 -0
- package/ciel_runtime_support/router_shortcuts.py +201 -0
- package/ciel_runtime_support/routing_fallback.py +73 -0
- package/ciel_runtime_support/runtime_activity_repository.py +143 -0
- package/ciel_runtime_support/runtime_adapters.py +104 -0
- package/ciel_runtime_support/runtime_command_factory.py +73 -0
- package/ciel_runtime_support/runtime_compatibility.py +50 -0
- package/ciel_runtime_support/runtime_constants.py +178 -0
- package/ciel_runtime_support/runtime_launch.py +1602 -0
- package/ciel_runtime_support/runtime_llm_options.py +312 -0
- package/ciel_runtime_support/runtime_logging.py +161 -0
- package/ciel_runtime_support/runtime_paths.py +157 -0
- package/ciel_runtime_support/runtime_restart.py +84 -0
- package/ciel_runtime_support/runtime_upgrade.py +149 -0
- package/ciel_runtime_support/secure_json_repository.py +55 -0
- package/ciel_runtime_support/session_import.py +356 -0
- package/ciel_runtime_support/settings_repository.py +8 -0
- package/ciel_runtime_support/slash_command_assets.py +211 -0
- package/ciel_runtime_support/sse_stream.py +57 -0
- package/ciel_runtime_support/sse_trace.py +225 -0
- package/ciel_runtime_support/statusline_script.py +593 -0
- package/ciel_runtime_support/statusline_settings.py +53 -0
- package/ciel_runtime_support/stream_chunk_policy.py +18 -0
- package/ciel_runtime_support/streaming_anthropic.py +1955 -0
- package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
- package/ciel_runtime_support/terminal_platform_io.py +127 -0
- package/ciel_runtime_support/timeout_profile.py +196 -0
- package/ciel_runtime_support/tool_dialects.py +85 -0
- package/ciel_runtime_support/tool_exposure_policy.py +63 -0
- package/ciel_runtime_support/tool_guard_hooks.py +218 -0
- package/ciel_runtime_support/tool_request_projection.py +96 -0
- package/ciel_runtime_support/tool_schema.py +483 -0
- package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
- package/ciel_runtime_support/ui_text.py +266 -0
- package/ciel_runtime_support/upstream_error_policy.py +104 -0
- package/ciel_runtime_support/upstream_retry.py +419 -0
- package/ciel_runtime_support/upstream_stream_io.py +106 -0
- package/ciel_runtime_support/usage_events.py +96 -0
- package/ciel_runtime_support/visible_stream_filters.py +130 -0
- package/ciel_runtime_support/web_endpoints.py +447 -0
- package/ciel_runtime_support/web_ui.py +915 -0
- package/ciel_runtime_support/web_ui_controller.py +189 -0
- package/ciel_runtime_support/windows_console_input.py +137 -0
- package/ciel_runtime_support/windows_console_mode.py +112 -0
- package/docs/Architecture.md +54 -0
- package/docs/Configuration.md +17 -0
- package/docs/Module-Map.md +1093 -26
- package/docs/Providers.md +46 -1
- package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
- package/npm-bin/run-ciel-runtime.js +22 -2
- package/package.json +9 -2
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""Protocol-aware prompt compaction policies."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class PromptCompactionText:
|
|
11
|
+
content_to_text: Callable[..., str]
|
|
12
|
+
compact_text: Callable[..., str]
|
|
13
|
+
build_summary: Callable[..., str]
|
|
14
|
+
append_system_texts: Callable[..., Any]
|
|
15
|
+
truncate: Callable[..., str]
|
|
16
|
+
chunk_count: Callable[..., int]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class PromptCompactionRuntime:
|
|
21
|
+
estimate_tokens: Callable[..., int]
|
|
22
|
+
llm_compact_messages: Callable[..., list[dict[str, Any]] | None]
|
|
23
|
+
write_activity: Callable[..., Any]
|
|
24
|
+
log: Callable[[str, str], Any]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True, slots=True)
|
|
28
|
+
class PromptCompactionServices:
|
|
29
|
+
text: PromptCompactionText
|
|
30
|
+
runtime: PromptCompactionRuntime
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def compact_chat_messages_for_budget(
|
|
34
|
+
messages: list[dict[str, Any]],
|
|
35
|
+
tools: list[dict[str, Any]],
|
|
36
|
+
budget_tokens: int,
|
|
37
|
+
*,
|
|
38
|
+
provider: str = "",
|
|
39
|
+
model: str = "",
|
|
40
|
+
pcfg: dict[str, Any] | None = None,
|
|
41
|
+
full_compact_request: bool = False,
|
|
42
|
+
wire: str | None = None,
|
|
43
|
+
services: PromptCompactionServices,
|
|
44
|
+
) -> list[dict[str, Any]]:
|
|
45
|
+
if not messages:
|
|
46
|
+
return messages
|
|
47
|
+
text = services.text
|
|
48
|
+
runtime = services.runtime
|
|
49
|
+
budget_tokens = max(8192, budget_tokens)
|
|
50
|
+
initial_tokens = runtime.estimate_tokens({"messages": messages, "tools": tools})
|
|
51
|
+
if initial_tokens <= budget_tokens:
|
|
52
|
+
return messages
|
|
53
|
+
if full_compact_request and pcfg is not None and provider and model:
|
|
54
|
+
compacted_by_llm = runtime.llm_compact_messages(
|
|
55
|
+
provider,
|
|
56
|
+
model,
|
|
57
|
+
pcfg,
|
|
58
|
+
messages,
|
|
59
|
+
budget_tokens,
|
|
60
|
+
wire=wire or "ollama",
|
|
61
|
+
)
|
|
62
|
+
if compacted_by_llm:
|
|
63
|
+
final_tokens = runtime.estimate_tokens({"messages": compacted_by_llm, "tools": []})
|
|
64
|
+
if final_tokens <= budget_tokens:
|
|
65
|
+
return compacted_by_llm
|
|
66
|
+
runtime.log(
|
|
67
|
+
"WARN",
|
|
68
|
+
f"context_compact_map_reduce_oversize provider={provider} model={model} tokens={final_tokens} budget={budget_tokens}; falling back to deterministic compact",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def compact_message(message: dict[str, Any], max_chars: int, prefix: str) -> dict[str, Any]:
|
|
72
|
+
role = str(message.get("role") or "user")
|
|
73
|
+
if role not in ("system", "user", "assistant", "tool"):
|
|
74
|
+
role = "user"
|
|
75
|
+
content = message.get("content")
|
|
76
|
+
content_text = content if isinstance(content, str) else text.content_to_text(content)
|
|
77
|
+
out: dict[str, Any] = {"role": role}
|
|
78
|
+
if message.get("name"):
|
|
79
|
+
out["name"] = str(message.get("name"))
|
|
80
|
+
out["content"] = text.truncate(f"{prefix}\n{content_text}", max(256, max_chars))
|
|
81
|
+
return out
|
|
82
|
+
|
|
83
|
+
def hard_cap(
|
|
84
|
+
system_messages: list[dict[str, Any]],
|
|
85
|
+
latest_message: dict[str, Any] | None,
|
|
86
|
+
omitted_messages: list[dict[str, Any]],
|
|
87
|
+
) -> list[dict[str, Any]]:
|
|
88
|
+
summary_text = text.build_summary(omitted_messages, max(8192, budget_tokens // 2))
|
|
89
|
+
summary_prefix = (
|
|
90
|
+
"[ciel-runtime context guard: older conversation history and oversized retained messages were "
|
|
91
|
+
"compacted because they exceeded the provider context budget.]"
|
|
92
|
+
)
|
|
93
|
+
latest_prefix = "[Latest retained message compacted because the previous payload exceeded the provider context budget.]"
|
|
94
|
+
system_chars = max(512, min(8192, budget_tokens))
|
|
95
|
+
summary_chars = max(512, min(4096, budget_tokens))
|
|
96
|
+
latest_chars = max(1024, budget_tokens * 4)
|
|
97
|
+
last_candidate: list[dict[str, Any]] = []
|
|
98
|
+
for _ in range(10):
|
|
99
|
+
per_system_chars = max(256, system_chars // max(1, len(system_messages)))
|
|
100
|
+
capped_systems = [
|
|
101
|
+
compact_message(message, per_system_chars, "[System message compacted for context budget.]")
|
|
102
|
+
for message in system_messages
|
|
103
|
+
]
|
|
104
|
+
summary_message = {
|
|
105
|
+
"role": "user",
|
|
106
|
+
"content": text.truncate(f"{summary_prefix}\n{summary_text}", summary_chars),
|
|
107
|
+
}
|
|
108
|
+
candidate = [*capped_systems, summary_message]
|
|
109
|
+
base_tokens = runtime.estimate_tokens({"messages": candidate, "tools": tools})
|
|
110
|
+
remaining_chars = max(256, (budget_tokens - base_tokens) * 4 - 1024)
|
|
111
|
+
if latest_message is not None:
|
|
112
|
+
candidate.append(compact_message(latest_message, min(latest_chars, remaining_chars), latest_prefix))
|
|
113
|
+
last_candidate = candidate
|
|
114
|
+
if runtime.estimate_tokens({"messages": candidate, "tools": tools}) <= budget_tokens:
|
|
115
|
+
return candidate
|
|
116
|
+
system_chars = max(256, system_chars // 2)
|
|
117
|
+
summary_chars = max(256, summary_chars // 2)
|
|
118
|
+
latest_chars = max(256, latest_chars // 2)
|
|
119
|
+
return last_candidate
|
|
120
|
+
|
|
121
|
+
system_messages = [message for message in messages if message.get("role") == "system"]
|
|
122
|
+
non_system = [message for message in messages if message.get("role") != "system"]
|
|
123
|
+
first_user = next((message for message in non_system if message.get("role") == "user"), None)
|
|
124
|
+
preserved_tail: list[dict[str, Any]] = []
|
|
125
|
+
omitted_reversed: list[dict[str, Any]] = []
|
|
126
|
+
summary: dict[str, Any] = {
|
|
127
|
+
"role": "user",
|
|
128
|
+
"content": (
|
|
129
|
+
"[ciel-runtime context guard: older conversation messages were omitted because the provider context "
|
|
130
|
+
"budget would be exceeded. Large file contents and prior Write/Edit inputs were truncated. "
|
|
131
|
+
"Use Read on specific files if exact old content is needed.]"
|
|
132
|
+
),
|
|
133
|
+
}
|
|
134
|
+
fixed_prefix = list(system_messages)
|
|
135
|
+
if first_user is not None:
|
|
136
|
+
fixed_prefix.append(first_user)
|
|
137
|
+
fixed_prefix.append(summary)
|
|
138
|
+
for message in reversed(non_system):
|
|
139
|
+
if first_user is not None and message is first_user:
|
|
140
|
+
continue
|
|
141
|
+
candidate = fixed_prefix + list(reversed(preserved_tail + [message]))
|
|
142
|
+
if runtime.estimate_tokens({"messages": candidate, "tools": tools}) <= budget_tokens:
|
|
143
|
+
preserved_tail.append(message)
|
|
144
|
+
else:
|
|
145
|
+
omitted_reversed.append(message)
|
|
146
|
+
if first_user is None:
|
|
147
|
+
fixed_prefix = [*system_messages, summary]
|
|
148
|
+
omitted_messages = list(reversed(omitted_reversed))
|
|
149
|
+
summary["content"] = text.build_summary(omitted_messages, budget_tokens)
|
|
150
|
+
compacted = fixed_prefix + list(reversed(preserved_tail))
|
|
151
|
+
while runtime.estimate_tokens({"messages": compacted, "tools": tools}) > budget_tokens and preserved_tail:
|
|
152
|
+
omitted_messages.append(preserved_tail.pop())
|
|
153
|
+
summary["content"] = text.build_summary(omitted_messages, budget_tokens)
|
|
154
|
+
compacted = fixed_prefix + list(reversed(preserved_tail))
|
|
155
|
+
if runtime.estimate_tokens({"messages": compacted, "tools": tools}) > budget_tokens:
|
|
156
|
+
latest_message = next((message for message in reversed(non_system) if isinstance(message, dict)), None)
|
|
157
|
+
omitted_for_hard_cap = [
|
|
158
|
+
message
|
|
159
|
+
for message in messages
|
|
160
|
+
if message not in system_messages and (latest_message is None or message is not latest_message)
|
|
161
|
+
]
|
|
162
|
+
compacted = hard_cap(system_messages, latest_message, omitted_for_hard_cap)
|
|
163
|
+
final_tokens = runtime.estimate_tokens({"messages": compacted, "tools": tools})
|
|
164
|
+
runtime.log(
|
|
165
|
+
"WARN",
|
|
166
|
+
f"compacted ollama payload messages {len(messages)}->{len(compacted)} tokens {initial_tokens}->{final_tokens} budget={budget_tokens}",
|
|
167
|
+
)
|
|
168
|
+
chunk_count = text.chunk_count(omitted_messages, budget_tokens)
|
|
169
|
+
if chunk_count and (provider or model):
|
|
170
|
+
runtime.write_activity(
|
|
171
|
+
provider or "provider",
|
|
172
|
+
model,
|
|
173
|
+
chunks=chunk_count,
|
|
174
|
+
parallel_sessions=1,
|
|
175
|
+
tokens=initial_tokens,
|
|
176
|
+
final_tokens=final_tokens,
|
|
177
|
+
budget=budget_tokens,
|
|
178
|
+
omitted_messages=len(omitted_messages),
|
|
179
|
+
retained_messages=len(compacted),
|
|
180
|
+
)
|
|
181
|
+
return compacted
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def anthropic_message_has_tool_result(message: dict[str, Any]) -> bool:
|
|
185
|
+
content = message.get("content")
|
|
186
|
+
return isinstance(content, list) and any(
|
|
187
|
+
isinstance(block, dict) and block.get("type") == "tool_result"
|
|
188
|
+
for block in content
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def anthropic_safe_tail_start(message: dict[str, Any]) -> bool:
|
|
193
|
+
return str(message.get("role") or "") == "user" and not anthropic_message_has_tool_result(message)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def compact_anthropic_body_for_budget(
|
|
197
|
+
body: dict[str, Any],
|
|
198
|
+
budget_tokens: int,
|
|
199
|
+
*,
|
|
200
|
+
provider: str = "",
|
|
201
|
+
model: str = "",
|
|
202
|
+
pcfg: dict[str, Any] | None = None,
|
|
203
|
+
full_compact_request: bool = False,
|
|
204
|
+
services: PromptCompactionServices,
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
messages = body.get("messages")
|
|
207
|
+
if not isinstance(messages, list) or not messages:
|
|
208
|
+
return body
|
|
209
|
+
typed_messages = [message for message in messages if isinstance(message, dict)]
|
|
210
|
+
if len(typed_messages) != len(messages):
|
|
211
|
+
return body
|
|
212
|
+
text = services.text
|
|
213
|
+
runtime = services.runtime
|
|
214
|
+
budget_tokens = max(8192, budget_tokens)
|
|
215
|
+
initial_tokens = runtime.estimate_tokens(body)
|
|
216
|
+
if initial_tokens <= budget_tokens:
|
|
217
|
+
return body
|
|
218
|
+
if full_compact_request and pcfg is not None and provider and model:
|
|
219
|
+
compact_messages = runtime.llm_compact_messages(
|
|
220
|
+
provider,
|
|
221
|
+
model,
|
|
222
|
+
pcfg,
|
|
223
|
+
typed_messages,
|
|
224
|
+
budget_tokens,
|
|
225
|
+
wire="anthropic",
|
|
226
|
+
)
|
|
227
|
+
if compact_messages:
|
|
228
|
+
compacted = dict(body)
|
|
229
|
+
compacted["messages"] = compact_messages
|
|
230
|
+
compacted["tools"] = []
|
|
231
|
+
compacted.pop("tool_choice", None)
|
|
232
|
+
compacted.pop("parallel_tool_calls", None)
|
|
233
|
+
final_tokens = runtime.estimate_tokens(compacted)
|
|
234
|
+
if final_tokens <= budget_tokens:
|
|
235
|
+
return compacted
|
|
236
|
+
runtime.log(
|
|
237
|
+
"WARN",
|
|
238
|
+
f"context_compact_map_reduce_oversize provider={provider} model={model} tokens={final_tokens} budget={budget_tokens}; falling back to deterministic compact",
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
summary_budget = max(1024, min(24576, budget_tokens // 10))
|
|
242
|
+
tail_budget = max(8192, budget_tokens - summary_budget)
|
|
243
|
+
tail_start = len(typed_messages)
|
|
244
|
+
base = dict(body)
|
|
245
|
+
for index in range(len(typed_messages) - 1, -1, -1):
|
|
246
|
+
candidate_body = dict(base)
|
|
247
|
+
candidate_body["messages"] = typed_messages[index:]
|
|
248
|
+
if runtime.estimate_tokens(candidate_body) <= tail_budget:
|
|
249
|
+
tail_start = index
|
|
250
|
+
continue
|
|
251
|
+
break
|
|
252
|
+
while tail_start < len(typed_messages) and not anthropic_safe_tail_start(typed_messages[tail_start]):
|
|
253
|
+
tail_start += 1
|
|
254
|
+
if tail_start >= len(typed_messages):
|
|
255
|
+
tail_start = max(0, len(typed_messages) - 1)
|
|
256
|
+
latest = typed_messages[tail_start]
|
|
257
|
+
if anthropic_message_has_tool_result(latest):
|
|
258
|
+
safe_text = text.content_to_text(latest.get("content"))
|
|
259
|
+
tail = [{"role": "user", "content": text.compact_text(safe_text)}]
|
|
260
|
+
else:
|
|
261
|
+
tail = [latest]
|
|
262
|
+
else:
|
|
263
|
+
tail = typed_messages[tail_start:]
|
|
264
|
+
omitted = typed_messages[:tail_start]
|
|
265
|
+
summary_text = text.build_summary(omitted, budget_tokens)
|
|
266
|
+
out = dict(body)
|
|
267
|
+
out["messages"] = tail
|
|
268
|
+
out["system"] = text.append_system_texts(body.get("system"), [summary_text])
|
|
269
|
+
while runtime.estimate_tokens(out) > budget_tokens and len(tail) > 1:
|
|
270
|
+
tail = tail[1:]
|
|
271
|
+
while tail and not anthropic_safe_tail_start(tail[0]):
|
|
272
|
+
tail = tail[1:]
|
|
273
|
+
if not tail:
|
|
274
|
+
tail = [typed_messages[-1]]
|
|
275
|
+
break
|
|
276
|
+
out["messages"] = tail
|
|
277
|
+
omitted = typed_messages[: len(typed_messages) - len(tail)]
|
|
278
|
+
out["system"] = text.append_system_texts(
|
|
279
|
+
body.get("system"),
|
|
280
|
+
[text.build_summary(omitted, budget_tokens)],
|
|
281
|
+
)
|
|
282
|
+
final_tokens = runtime.estimate_tokens(out)
|
|
283
|
+
if final_tokens > budget_tokens:
|
|
284
|
+
compact_summary = text.build_summary(omitted, max(8192, budget_tokens // 2))
|
|
285
|
+
out["system"] = text.append_system_texts(
|
|
286
|
+
body.get("system"),
|
|
287
|
+
[text.truncate(compact_summary, max(4096, budget_tokens * 2))],
|
|
288
|
+
)
|
|
289
|
+
final_tokens = runtime.estimate_tokens(out)
|
|
290
|
+
if final_tokens > budget_tokens and tail:
|
|
291
|
+
base_without_messages = dict(out)
|
|
292
|
+
base_without_messages["messages"] = []
|
|
293
|
+
remaining_chars = max(1024, (budget_tokens - runtime.estimate_tokens(base_without_messages)) * 4 - 1024)
|
|
294
|
+
latest = tail[-1]
|
|
295
|
+
latest_role = str(latest.get("role") or "unknown")
|
|
296
|
+
latest_text = text.content_to_text(latest.get("content"))
|
|
297
|
+
out["messages"] = [{
|
|
298
|
+
"role": "user",
|
|
299
|
+
"content": text.truncate(
|
|
300
|
+
f"[Latest retained message compacted from role={latest_role} because it alone exceeded the provider context budget.]\n{latest_text}",
|
|
301
|
+
remaining_chars,
|
|
302
|
+
),
|
|
303
|
+
}]
|
|
304
|
+
final_tokens = runtime.estimate_tokens(out)
|
|
305
|
+
runtime.log(
|
|
306
|
+
"WARN",
|
|
307
|
+
f"compacted anthropic payload messages {len(typed_messages)}->{len(out.get('messages') or [])} tokens {initial_tokens}->{final_tokens} budget={budget_tokens}",
|
|
308
|
+
)
|
|
309
|
+
chunk_count = text.chunk_count(omitted, budget_tokens)
|
|
310
|
+
if chunk_count and (provider or model):
|
|
311
|
+
runtime.write_activity(
|
|
312
|
+
provider or "provider",
|
|
313
|
+
model or str(body.get("model") or ""),
|
|
314
|
+
chunks=chunk_count,
|
|
315
|
+
parallel_sessions=1,
|
|
316
|
+
tokens=initial_tokens,
|
|
317
|
+
final_tokens=final_tokens,
|
|
318
|
+
budget=budget_tokens,
|
|
319
|
+
omitted_messages=len(omitted),
|
|
320
|
+
retained_messages=len(out.get("messages") or []),
|
|
321
|
+
)
|
|
322
|
+
return out
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Protocol-aware system prompt transformation policies.
|
|
2
|
+
|
|
3
|
+
The router calls one application service while wire-format strategies own the
|
|
4
|
+
placement details. This keeps cross-cutting prompt features independent from
|
|
5
|
+
providers: a provider only declares its protocol and does not need custom
|
|
6
|
+
prompt mutation code.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable, Mapping, Protocol
|
|
13
|
+
|
|
14
|
+
from ciel_runtime_support.architecture import MessageProtocol
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
JsonObject = dict[str, Any]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class SystemPromptInjection:
|
|
22
|
+
"""A provider-neutral request to add trusted system context."""
|
|
23
|
+
|
|
24
|
+
texts: tuple[str, ...]
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_texts(cls, texts: list[str] | tuple[str, ...] | None) -> SystemPromptInjection:
|
|
28
|
+
return cls(tuple(clean for item in (texts or ()) if (clean := str(item or "").strip())))
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def empty(self) -> bool:
|
|
32
|
+
return not self.texts
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class PromptInjectionStrategy(Protocol):
|
|
36
|
+
"""Wire-format strategy used by :class:`PromptInjector`."""
|
|
37
|
+
|
|
38
|
+
def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def append_anthropic_system_texts(system: Any, texts: list[str] | tuple[str, ...] | None) -> Any:
|
|
42
|
+
"""Append Anthropic text blocks without mutating or reordering existing blocks."""
|
|
43
|
+
|
|
44
|
+
injection = SystemPromptInjection.from_texts(texts)
|
|
45
|
+
if injection.empty:
|
|
46
|
+
return system
|
|
47
|
+
blocks: list[Any]
|
|
48
|
+
if isinstance(system, list):
|
|
49
|
+
blocks = [dict(block) if isinstance(block, dict) else block for block in system]
|
|
50
|
+
elif isinstance(system, str):
|
|
51
|
+
blocks = [{"type": "text", "text": system.strip()}] if system.strip() else []
|
|
52
|
+
elif system:
|
|
53
|
+
blocks = [{"type": "text", "text": str(system).strip()}]
|
|
54
|
+
else:
|
|
55
|
+
blocks = []
|
|
56
|
+
blocks.extend({"type": "text", "text": text} for text in injection.texts)
|
|
57
|
+
return blocks
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AnthropicPromptInjectionStrategy:
|
|
61
|
+
def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
|
|
62
|
+
out = dict(body)
|
|
63
|
+
out["system"] = append_anthropic_system_texts(body.get("system"), injection.texts)
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(frozen=True)
|
|
68
|
+
class ChatPromptInjectionStrategy:
|
|
69
|
+
"""Insert context after existing system/developer messages."""
|
|
70
|
+
|
|
71
|
+
role: str = "system"
|
|
72
|
+
|
|
73
|
+
def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
|
|
74
|
+
messages = body.get("messages")
|
|
75
|
+
copied = [dict(item) if isinstance(item, dict) else item for item in messages] if isinstance(messages, list) else []
|
|
76
|
+
insertion = 0
|
|
77
|
+
while insertion < len(copied):
|
|
78
|
+
item = copied[insertion]
|
|
79
|
+
if not isinstance(item, dict) or str(item.get("role") or "") not in {"system", "developer"}:
|
|
80
|
+
break
|
|
81
|
+
insertion += 1
|
|
82
|
+
copied[insertion:insertion] = ({"role": self.role, "content": text} for text in injection.texts)
|
|
83
|
+
out = dict(body)
|
|
84
|
+
out["messages"] = copied
|
|
85
|
+
return out
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class OpenAIResponsesPromptInjectionStrategy:
|
|
89
|
+
def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
|
|
90
|
+
out = dict(body)
|
|
91
|
+
parts = [str(body.get("instructions") or "").strip(), *injection.texts]
|
|
92
|
+
out["instructions"] = "\n\n".join(part for part in parts if part)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class GooglePromptInjectionStrategy:
|
|
97
|
+
"""Append Gemini ``systemInstruction.parts`` while preserving key casing."""
|
|
98
|
+
|
|
99
|
+
def inject(self, body: Mapping[str, Any], injection: SystemPromptInjection) -> JsonObject:
|
|
100
|
+
out = dict(body)
|
|
101
|
+
key = "system_instruction" if "system_instruction" in body else "systemInstruction"
|
|
102
|
+
current = body.get(key)
|
|
103
|
+
if isinstance(current, dict):
|
|
104
|
+
system_instruction = dict(current)
|
|
105
|
+
raw_parts = current.get("parts")
|
|
106
|
+
parts = [dict(item) if isinstance(item, dict) else item for item in raw_parts] if isinstance(raw_parts, list) else []
|
|
107
|
+
elif isinstance(current, str) and current.strip():
|
|
108
|
+
system_instruction = {}
|
|
109
|
+
parts = [{"text": current.strip()}]
|
|
110
|
+
else:
|
|
111
|
+
system_instruction = {}
|
|
112
|
+
parts = []
|
|
113
|
+
parts.extend({"text": text} for text in injection.texts)
|
|
114
|
+
system_instruction["parts"] = parts
|
|
115
|
+
out[key] = system_instruction
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class PromptInjector:
|
|
120
|
+
"""Application service dispatching to protocol transformation strategies."""
|
|
121
|
+
|
|
122
|
+
def __init__(self, strategies: Mapping[MessageProtocol, PromptInjectionStrategy] | None = None) -> None:
|
|
123
|
+
self._strategies: dict[MessageProtocol, PromptInjectionStrategy] = dict(strategies or default_strategies())
|
|
124
|
+
|
|
125
|
+
def inject(
|
|
126
|
+
self,
|
|
127
|
+
body: Mapping[str, Any],
|
|
128
|
+
protocol: MessageProtocol,
|
|
129
|
+
texts: list[str] | tuple[str, ...] | None,
|
|
130
|
+
) -> JsonObject:
|
|
131
|
+
injection = SystemPromptInjection.from_texts(texts)
|
|
132
|
+
if injection.empty:
|
|
133
|
+
return dict(body)
|
|
134
|
+
try:
|
|
135
|
+
strategy = self._strategies[protocol]
|
|
136
|
+
except KeyError as exc: # pragma: no cover - MessageProtocol keeps this defensive
|
|
137
|
+
raise ValueError(f"unsupported prompt injection protocol: {protocol}") from exc
|
|
138
|
+
return strategy.inject(body, injection)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def default_strategies() -> Mapping[MessageProtocol, PromptInjectionStrategy]:
|
|
142
|
+
chat = ChatPromptInjectionStrategy()
|
|
143
|
+
return {
|
|
144
|
+
"anthropic_messages": AnthropicPromptInjectionStrategy(),
|
|
145
|
+
"openai_chat": chat,
|
|
146
|
+
"openai_responses": OpenAIResponsesPromptInjectionStrategy(),
|
|
147
|
+
"ollama_chat": chat,
|
|
148
|
+
"google_generative": GooglePromptInjectionStrategy(),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def normalize_anthropic_system_role_messages(
|
|
153
|
+
body: Mapping[str, Any],
|
|
154
|
+
content_to_text: Callable[[Any], str],
|
|
155
|
+
) -> JsonObject:
|
|
156
|
+
"""Move non-standard Anthropic system-role messages to top-level blocks."""
|
|
157
|
+
|
|
158
|
+
messages = body.get("messages")
|
|
159
|
+
if not isinstance(messages, list):
|
|
160
|
+
return dict(body)
|
|
161
|
+
next_messages: list[Any] = []
|
|
162
|
+
system_texts: list[str] = []
|
|
163
|
+
changed = False
|
|
164
|
+
for message in messages:
|
|
165
|
+
if not isinstance(message, dict) or str(message.get("role") or "").strip() != "system":
|
|
166
|
+
next_messages.append(message)
|
|
167
|
+
continue
|
|
168
|
+
changed = True
|
|
169
|
+
if text := content_to_text(message.get("content")).strip():
|
|
170
|
+
system_texts.append(text)
|
|
171
|
+
if not changed:
|
|
172
|
+
return dict(body)
|
|
173
|
+
out = dict(body)
|
|
174
|
+
out["messages"] = next_messages
|
|
175
|
+
out["system"] = append_anthropic_system_texts(body.get("system"), system_texts)
|
|
176
|
+
return out
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Wire-protocol conversion helpers used by the runtime routers."""
|
|
2
|
+
|
|
3
|
+
from .openai_responses import (
|
|
4
|
+
OpenAIResponsesProtocolAdapter,
|
|
5
|
+
anthropic_message_to_openai_response,
|
|
6
|
+
openai_responses_to_anthropic_messages,
|
|
7
|
+
)
|
|
8
|
+
from ..registry import AdapterRegistry
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PROTOCOL_ADAPTERS: AdapterRegistry[OpenAIResponsesProtocolAdapter] = AdapterRegistry()
|
|
12
|
+
PROTOCOL_ADAPTERS.register(
|
|
13
|
+
"openai_responses",
|
|
14
|
+
lambda **kwargs: OpenAIResponsesProtocolAdapter(**kwargs),
|
|
15
|
+
aliases=("openai-responses",),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"PROTOCOL_ADAPTERS",
|
|
21
|
+
"OpenAIResponsesProtocolAdapter",
|
|
22
|
+
"anthropic_message_to_openai_response",
|
|
23
|
+
"openai_responses_to_anthropic_messages",
|
|
24
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Pure Anthropic content-block text projection."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def content_to_text(content: Any) -> str:
|
|
9
|
+
if isinstance(content, str):
|
|
10
|
+
return content
|
|
11
|
+
if not isinstance(content, list):
|
|
12
|
+
return str(content) if content is not None else ""
|
|
13
|
+
parts: list[str] = []
|
|
14
|
+
for block in content:
|
|
15
|
+
if isinstance(block, str):
|
|
16
|
+
parts.append(block)
|
|
17
|
+
continue
|
|
18
|
+
if not isinstance(block, dict):
|
|
19
|
+
continue
|
|
20
|
+
block_type = block.get("type")
|
|
21
|
+
if block_type == "text":
|
|
22
|
+
parts.append(str(block.get("text", "")))
|
|
23
|
+
elif block_type == "tool_result":
|
|
24
|
+
tool_text = content_to_text(block.get("content", ""))
|
|
25
|
+
parts.append(
|
|
26
|
+
"Tool result for %s:\n%s"
|
|
27
|
+
% (block.get("tool_use_id", "tool"), tool_text)
|
|
28
|
+
)
|
|
29
|
+
return "\n".join(part for part in parts if part)
|