@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,111 @@
|
|
|
1
|
+
"""Pure Anthropic-to-Ollama chat wire projections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ciel_runtime_support.protocols.anthropic_content import content_to_text
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, slots=True)
|
|
12
|
+
class DecodedOllamaChatResponse:
|
|
13
|
+
"""Provider-neutral values decoded from one Ollama `/api/chat` response."""
|
|
14
|
+
|
|
15
|
+
text: str
|
|
16
|
+
tool_calls: tuple[dict[str, Any], ...]
|
|
17
|
+
done_reason: str
|
|
18
|
+
input_tokens: int
|
|
19
|
+
output_tokens: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def decode_ollama_chat_response(data: dict[str, Any]) -> DecodedOllamaChatResponse:
|
|
23
|
+
"""Decode the Ollama envelope without applying runtime/tool policies."""
|
|
24
|
+
|
|
25
|
+
message = data.get("message") if isinstance(data.get("message"), dict) else {}
|
|
26
|
+
calls = message.get("tool_calls") if isinstance(message, dict) else None
|
|
27
|
+
return DecodedOllamaChatResponse(
|
|
28
|
+
text=str(message.get("content") or ""),
|
|
29
|
+
tool_calls=tuple(call for call in (calls or []) if isinstance(call, dict)),
|
|
30
|
+
done_reason=str(data.get("done_reason") or ""),
|
|
31
|
+
input_tokens=max(0, int(data.get("prompt_eval_count") or 0)),
|
|
32
|
+
output_tokens=max(0, int(data.get("eval_count") or 0)),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def encode_anthropic_message(
|
|
37
|
+
*,
|
|
38
|
+
message_id: str,
|
|
39
|
+
model: str,
|
|
40
|
+
content: list[dict[str, Any]],
|
|
41
|
+
done_reason: str,
|
|
42
|
+
input_tokens: int,
|
|
43
|
+
output_tokens: int,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Encode finalized content blocks as an Anthropic Messages response."""
|
|
46
|
+
|
|
47
|
+
has_tool_use = any(block.get("type") == "tool_use" for block in content)
|
|
48
|
+
stop_reason = "tool_use" if has_tool_use else "end_turn"
|
|
49
|
+
if done_reason == "length":
|
|
50
|
+
stop_reason = "max_tokens"
|
|
51
|
+
return {
|
|
52
|
+
"id": message_id,
|
|
53
|
+
"type": "message",
|
|
54
|
+
"role": "assistant",
|
|
55
|
+
"model": model,
|
|
56
|
+
"content": content or [{"type": "text", "text": ""}],
|
|
57
|
+
"stop_reason": stop_reason,
|
|
58
|
+
"stop_sequence": None,
|
|
59
|
+
"usage": {
|
|
60
|
+
"input_tokens": input_tokens,
|
|
61
|
+
"output_tokens": output_tokens,
|
|
62
|
+
},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def anthropic_system_to_ollama_messages(system: Any) -> list[dict[str, Any]]:
|
|
67
|
+
if not system:
|
|
68
|
+
return []
|
|
69
|
+
text = content_to_text(system)
|
|
70
|
+
return [{"role": "system", "content": text}] if text else []
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def ollama_claude_code_reminder() -> dict[str, str]:
|
|
74
|
+
return {
|
|
75
|
+
"role": "system",
|
|
76
|
+
"content": (
|
|
77
|
+
"Claude Code execution reminder: when the user asks to create, edit, or run code, "
|
|
78
|
+
"use the available tools such as Write, Edit, Read, and Bash. Do not stop after saying "
|
|
79
|
+
"you will run something. Do not present a code block as a substitute for creating the file "
|
|
80
|
+
"unless the user explicitly asks for code only. Exception: while Claude Code is in Plan Mode, "
|
|
81
|
+
"do not implement normal project files; explore as needed, write or update the plan file, "
|
|
82
|
+
"then call ExitPlanMode when the plan is ready for approval."
|
|
83
|
+
),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def anthropic_tools_to_ollama(tools: Any) -> list[dict[str, Any]]:
|
|
88
|
+
if not isinstance(tools, list):
|
|
89
|
+
return []
|
|
90
|
+
return [
|
|
91
|
+
{
|
|
92
|
+
"type": "function",
|
|
93
|
+
"function": {
|
|
94
|
+
"name": tool["name"],
|
|
95
|
+
"description": tool.get("description", ""),
|
|
96
|
+
"parameters": tool.get("input_schema") or {"type": "object", "properties": {}},
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
for tool in tools
|
|
100
|
+
if isinstance(tool, dict) and tool.get("name")
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
__all__ = [
|
|
105
|
+
"DecodedOllamaChatResponse",
|
|
106
|
+
"anthropic_system_to_ollama_messages",
|
|
107
|
+
"anthropic_tools_to_ollama",
|
|
108
|
+
"decode_ollama_chat_response",
|
|
109
|
+
"encode_anthropic_message",
|
|
110
|
+
"ollama_claude_code_reminder",
|
|
111
|
+
]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Ollama response projection into Anthropic Messages content."""
|
|
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 OllamaResponseText:
|
|
11
|
+
decode: Callable[[dict[str, Any]], Any]
|
|
12
|
+
strip_thinking: Callable[[str], str]
|
|
13
|
+
parse_pseudo_tools: Callable[..., tuple[str, list[dict[str, Any]]]]
|
|
14
|
+
log: Callable[[str, str], Any]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True, slots=True)
|
|
18
|
+
class OllamaResponseTools:
|
|
19
|
+
resolve_name: Callable[..., str]
|
|
20
|
+
normalize_arguments: Callable[..., dict[str, Any]]
|
|
21
|
+
validate_input: Callable[..., dict[str, Any]]
|
|
22
|
+
plan_mode_name: Callable[..., tuple[str | None, dict[str, Any]]]
|
|
23
|
+
cap_notification_wait: Callable[..., dict[str, Any]]
|
|
24
|
+
should_drop: Callable[..., bool]
|
|
25
|
+
append_log: Callable[..., Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class OllamaResponseRecovery:
|
|
30
|
+
auto_enter_plan: Callable[..., bool]
|
|
31
|
+
recover_empty_with_tasklist: Callable[..., bool]
|
|
32
|
+
keep_alive_with_tasklist: Callable[..., bool]
|
|
33
|
+
auto_continue_choice: Callable[..., bool]
|
|
34
|
+
empty_notice: Callable[[dict[str, Any]], str]
|
|
35
|
+
latest_tool_result_names: Callable[[dict[str, Any]], list[str]]
|
|
36
|
+
synthetic_tool_response: Callable[..., dict[str, Any]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class OllamaResponseOutput:
|
|
41
|
+
encode_message: Callable[..., dict[str, Any]]
|
|
42
|
+
estimate_tokens: Callable[[dict[str, Any]], int]
|
|
43
|
+
timestamp_ms: Callable[[], int]
|
|
44
|
+
process_id: Callable[[], int]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True, slots=True)
|
|
48
|
+
class OllamaResponseServices:
|
|
49
|
+
text: OllamaResponseText
|
|
50
|
+
tools: OllamaResponseTools
|
|
51
|
+
recovery: OllamaResponseRecovery
|
|
52
|
+
output: OllamaResponseOutput
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def project_ollama_response(
|
|
56
|
+
data: dict[str, Any],
|
|
57
|
+
model: str,
|
|
58
|
+
source_body: dict[str, Any] | None,
|
|
59
|
+
services: OllamaResponseServices,
|
|
60
|
+
) -> dict[str, Any]:
|
|
61
|
+
decoded = services.text.decode(data)
|
|
62
|
+
content: list[dict[str, Any]] = []
|
|
63
|
+
raw_text = decoded.text
|
|
64
|
+
text = services.text.strip_thinking(raw_text)
|
|
65
|
+
if text != raw_text:
|
|
66
|
+
services.text.log(
|
|
67
|
+
"WARN",
|
|
68
|
+
f"suppressed visible Ollama thinking markup model={model} removed_chars={len(raw_text) - len(text)}",
|
|
69
|
+
)
|
|
70
|
+
text, pseudo_tool_calls = services.text.parse_pseudo_tools(text, source_body)
|
|
71
|
+
if text:
|
|
72
|
+
content.append({"type": "text", "text": text})
|
|
73
|
+
tool_id_prefix = f"toolu_ollama_{services.output.timestamp_ms()}_{services.output.process_id()}"
|
|
74
|
+
for index, call in enumerate(list(decoded.tool_calls) + pseudo_tool_calls):
|
|
75
|
+
tool_block = _project_tool_call(call, index, tool_id_prefix, model, source_body, services)
|
|
76
|
+
if tool_block is not None:
|
|
77
|
+
content.append(tool_block)
|
|
78
|
+
|
|
79
|
+
emitted = [block for block in content if block.get("type") == "tool_use"]
|
|
80
|
+
recovered = _recover_response(model, source_body, text, emitted, content, tool_id_prefix, services)
|
|
81
|
+
if recovered is not None:
|
|
82
|
+
return recovered
|
|
83
|
+
if source_body is not None and not text.strip() and not emitted:
|
|
84
|
+
text = services.recovery.empty_notice(source_body)
|
|
85
|
+
names = ",".join(services.recovery.latest_tool_result_names(source_body)) or "-"
|
|
86
|
+
services.text.log("WARN", f"ollama_empty_end_turn_notice model={model} latest_tool_results={names}")
|
|
87
|
+
content.append({"type": "text", "text": text})
|
|
88
|
+
|
|
89
|
+
input_tokens = decoded.input_tokens
|
|
90
|
+
if input_tokens <= 0 and isinstance(source_body, dict):
|
|
91
|
+
input_tokens = services.output.estimate_tokens(source_body)
|
|
92
|
+
output_tokens = decoded.output_tokens or max(1, len(text) // 4)
|
|
93
|
+
return services.output.encode_message(
|
|
94
|
+
message_id=f"msg_ollama_{services.output.timestamp_ms()}",
|
|
95
|
+
model=model,
|
|
96
|
+
content=content,
|
|
97
|
+
done_reason=decoded.done_reason,
|
|
98
|
+
input_tokens=input_tokens,
|
|
99
|
+
output_tokens=output_tokens,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def project_openai_chat_response(
|
|
104
|
+
data: dict[str, Any],
|
|
105
|
+
model: str,
|
|
106
|
+
source_body: dict[str, Any] | None,
|
|
107
|
+
*,
|
|
108
|
+
services: OllamaResponseServices,
|
|
109
|
+
positive_int: Callable[[Any], int | None],
|
|
110
|
+
reasoning_to_block: Callable[[Any], dict[str, Any] | None],
|
|
111
|
+
content_to_text: Callable[[Any], str],
|
|
112
|
+
) -> dict[str, Any]:
|
|
113
|
+
"""Project an OpenAI-compatible chat response through the shared codec."""
|
|
114
|
+
choices = data.get("choices")
|
|
115
|
+
choice = (
|
|
116
|
+
choices[0]
|
|
117
|
+
if isinstance(choices, list)
|
|
118
|
+
and choices
|
|
119
|
+
and isinstance(choices[0], dict)
|
|
120
|
+
else {}
|
|
121
|
+
)
|
|
122
|
+
raw_message = choice.get("message")
|
|
123
|
+
message = raw_message if isinstance(raw_message, dict) else {}
|
|
124
|
+
usage_value = data.get("usage")
|
|
125
|
+
usage = usage_value if isinstance(usage_value, dict) else {}
|
|
126
|
+
wrapped = {
|
|
127
|
+
"message": {
|
|
128
|
+
"content": message.get("content") or "",
|
|
129
|
+
"tool_calls": message.get("tool_calls") or [],
|
|
130
|
+
},
|
|
131
|
+
"done_reason": (
|
|
132
|
+
"length"
|
|
133
|
+
if choice.get("finish_reason") == "length"
|
|
134
|
+
else "stop"
|
|
135
|
+
),
|
|
136
|
+
"prompt_eval_count": positive_int(usage.get("prompt_tokens"))
|
|
137
|
+
or (
|
|
138
|
+
services.output.estimate_tokens(source_body)
|
|
139
|
+
if isinstance(source_body, dict)
|
|
140
|
+
else 0
|
|
141
|
+
),
|
|
142
|
+
"eval_count": positive_int(usage.get("completion_tokens")) or 0,
|
|
143
|
+
}
|
|
144
|
+
output = project_ollama_response(
|
|
145
|
+
wrapped,
|
|
146
|
+
model,
|
|
147
|
+
source_body,
|
|
148
|
+
services,
|
|
149
|
+
)
|
|
150
|
+
thinking_block = reasoning_to_block(message.get("reasoning_content"))
|
|
151
|
+
if thinking_block is None:
|
|
152
|
+
return output
|
|
153
|
+
content = output.get("content")
|
|
154
|
+
if not isinstance(content, list):
|
|
155
|
+
content = [{"type": "text", "text": content_to_text(content)}]
|
|
156
|
+
output = dict(output)
|
|
157
|
+
output["content"] = [thinking_block, *content]
|
|
158
|
+
return output
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _project_tool_call(
|
|
162
|
+
call: dict[str, Any],
|
|
163
|
+
index: int,
|
|
164
|
+
id_prefix: str,
|
|
165
|
+
model: str,
|
|
166
|
+
source_body: dict[str, Any] | None,
|
|
167
|
+
services: OllamaResponseServices,
|
|
168
|
+
) -> dict[str, Any] | None:
|
|
169
|
+
function = call.get("function") if isinstance(call, dict) else {}
|
|
170
|
+
if not isinstance(function, dict) or not function.get("name"):
|
|
171
|
+
return None
|
|
172
|
+
raw_name = str(function["name"])
|
|
173
|
+
name = services.tools.resolve_name(raw_name, source_body)
|
|
174
|
+
raw_arguments = function.get("arguments")
|
|
175
|
+
normalized = services.tools.normalize_arguments(name, raw_arguments)
|
|
176
|
+
tool_input = services.tools.validate_input(name, normalized)
|
|
177
|
+
if source_body is not None:
|
|
178
|
+
name, tool_input = services.tools.plan_mode_name(source_body, name, tool_input)
|
|
179
|
+
if name is None:
|
|
180
|
+
return None
|
|
181
|
+
tool_input = services.tools.cap_notification_wait(name, tool_input)
|
|
182
|
+
if services.tools.should_drop(name, tool_input, raw_name, source_body):
|
|
183
|
+
return None
|
|
184
|
+
services.tools.append_log(
|
|
185
|
+
"ollama_nonstream_tool_call",
|
|
186
|
+
{
|
|
187
|
+
"model": model,
|
|
188
|
+
"raw_name": raw_name,
|
|
189
|
+
"matched_name": name,
|
|
190
|
+
"raw_arguments": raw_arguments,
|
|
191
|
+
"normalized_arguments": normalized,
|
|
192
|
+
"emitted_input": tool_input,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
return {"type": "tool_use", "id": f"{id_prefix}_{index}", "name": name, "input": tool_input}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _recover_response(
|
|
199
|
+
model: str,
|
|
200
|
+
source_body: dict[str, Any] | None,
|
|
201
|
+
text: str,
|
|
202
|
+
emitted: list[dict[str, Any]],
|
|
203
|
+
content: list[dict[str, Any]],
|
|
204
|
+
id_prefix: str,
|
|
205
|
+
services: OllamaResponseServices,
|
|
206
|
+
) -> dict[str, Any] | None:
|
|
207
|
+
if source_body is None:
|
|
208
|
+
return None
|
|
209
|
+
recovery = services.recovery
|
|
210
|
+
if recovery.auto_enter_plan(source_body, text, emitted):
|
|
211
|
+
services.text.log("WARN", "auto-synthesized EnterPlanMode from short/empty upstream response")
|
|
212
|
+
return recovery.synthetic_tool_response(model, "EnterPlanMode")
|
|
213
|
+
if recovery.recover_empty_with_tasklist(source_body, text, emitted):
|
|
214
|
+
services.text.log("WARN", "auto-synthesized TaskList from empty upstream end_turn")
|
|
215
|
+
return recovery.synthetic_tool_response(model, "TaskList")
|
|
216
|
+
if recovery.keep_alive_with_tasklist(source_body, text, emitted):
|
|
217
|
+
services.text.log("WARN", "auto-synthesized TaskList to keep work moving after tool result")
|
|
218
|
+
block = {"type": "tool_use", "id": f"{id_prefix}_keepalive", "name": "TaskList", "input": {}}
|
|
219
|
+
content.append(block)
|
|
220
|
+
emitted.append(block)
|
|
221
|
+
if recovery.auto_continue_choice(source_body, text, emitted):
|
|
222
|
+
services.text.log("WARN", "auto-synthesized TaskList after clarification question")
|
|
223
|
+
block = {
|
|
224
|
+
"type": "tool_use",
|
|
225
|
+
"id": f"toolu_ollama_choice_{services.output.timestamp_ms()}",
|
|
226
|
+
"name": "TaskList",
|
|
227
|
+
"input": {},
|
|
228
|
+
}
|
|
229
|
+
content.append(block)
|
|
230
|
+
emitted.append(block)
|
|
231
|
+
return None
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""OpenAI reasoning/tool-choice projection and provider policy orchestration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
|
|
9
|
+
from ..architecture import ProviderAdapter, ProviderConfig
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def anthropic_tool_choice_to_openai(tool_choice: Any) -> Any:
|
|
13
|
+
if not isinstance(tool_choice, dict):
|
|
14
|
+
return tool_choice
|
|
15
|
+
choice_type = tool_choice.get("type")
|
|
16
|
+
if choice_type == "tool" and tool_choice.get("name"):
|
|
17
|
+
return {"type": "function", "function": {"name": str(tool_choice["name"])}}
|
|
18
|
+
if choice_type == "any":
|
|
19
|
+
return "required"
|
|
20
|
+
if choice_type == "auto":
|
|
21
|
+
return "auto"
|
|
22
|
+
return tool_choice
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def openai_reasoning_to_anthropic_thinking_block(
|
|
26
|
+
reasoning_content: Any,
|
|
27
|
+
) -> dict[str, Any] | None:
|
|
28
|
+
reasoning = str(reasoning_content or "")
|
|
29
|
+
if not reasoning:
|
|
30
|
+
return None
|
|
31
|
+
digest = hashlib.sha256(reasoning.encode("utf-8", errors="replace")).hexdigest()[:24]
|
|
32
|
+
return {
|
|
33
|
+
"type": "thinking",
|
|
34
|
+
"thinking": reasoning,
|
|
35
|
+
"signature": f"ciel-runtime-openai-reasoning-{digest}",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class OpenAiReasoningPolicy:
|
|
41
|
+
"""Delegate provider-specific reasoning support to its registered adapter."""
|
|
42
|
+
|
|
43
|
+
adapter_for: Callable[[str, dict[str, Any]], ProviderAdapter]
|
|
44
|
+
config_for: Callable[[str, dict[str, Any]], ProviderConfig]
|
|
45
|
+
|
|
46
|
+
def passback_enabled(
|
|
47
|
+
self, provider: str, model: str | None, config: dict[str, Any]
|
|
48
|
+
) -> bool:
|
|
49
|
+
adapter = self.adapter_for(provider, config)
|
|
50
|
+
return adapter.openai_reasoning_passback_enabled(
|
|
51
|
+
self.config_for(provider, config), model
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def passback_enabled_for_body(
|
|
55
|
+
self, provider: str, config: dict[str, Any], body: dict[str, Any]
|
|
56
|
+
) -> bool:
|
|
57
|
+
return self.passback_enabled(provider, str(body.get("model") or ""), config)
|
|
58
|
+
|
|
59
|
+
def should_omit_tool_choice(
|
|
60
|
+
self,
|
|
61
|
+
provider: str,
|
|
62
|
+
model: str,
|
|
63
|
+
body: dict[str, Any],
|
|
64
|
+
config: dict[str, Any],
|
|
65
|
+
) -> bool:
|
|
66
|
+
if body.get("tool_choice") is None:
|
|
67
|
+
return False
|
|
68
|
+
return self.passback_enabled(provider, model, config)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"OpenAiReasoningPolicy",
|
|
73
|
+
"anthropic_tool_choice_to_openai",
|
|
74
|
+
"openai_reasoning_to_anthropic_thinking_block",
|
|
75
|
+
]
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Pure OpenAI Responses <-> Anthropic Messages conversions.
|
|
2
|
+
|
|
3
|
+
This module intentionally has no router, configuration, or network dependencies.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import time
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ..architecture import MessageProtocolAdapter
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _positive_int(value: Any) -> int | None:
|
|
17
|
+
try:
|
|
18
|
+
parsed = int(value)
|
|
19
|
+
except (TypeError, ValueError):
|
|
20
|
+
return None
|
|
21
|
+
return parsed if parsed > 0 else None
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _json_object(value: Any) -> dict[str, Any]:
|
|
25
|
+
if isinstance(value, dict):
|
|
26
|
+
return value
|
|
27
|
+
if isinstance(value, str):
|
|
28
|
+
try:
|
|
29
|
+
parsed = json.loads(value)
|
|
30
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
31
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
32
|
+
return {}
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _content_text(content: Any) -> str:
|
|
37
|
+
if isinstance(content, str):
|
|
38
|
+
return content
|
|
39
|
+
if isinstance(content, dict):
|
|
40
|
+
content_type = str(content.get("type") or "")
|
|
41
|
+
if content_type in ("input_text", "output_text", "text"):
|
|
42
|
+
return str(content.get("text") or "")
|
|
43
|
+
if content_type == "refusal":
|
|
44
|
+
return str(content.get("refusal") or "")
|
|
45
|
+
return str(content.get("text") or content.get("output") or "")
|
|
46
|
+
if isinstance(content, list):
|
|
47
|
+
parts = [_content_text(item) for item in content]
|
|
48
|
+
return "\n".join(part for part in parts if part)
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _content_blocks(content: Any) -> list[dict[str, Any]]:
|
|
53
|
+
text = _content_text(content)
|
|
54
|
+
return [{"type": "text", "text": text}] if text else []
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _tools_to_anthropic(tools: Any) -> list[dict[str, Any]]:
|
|
58
|
+
out: list[dict[str, Any]] = []
|
|
59
|
+
if not isinstance(tools, list):
|
|
60
|
+
return out
|
|
61
|
+
for tool in tools:
|
|
62
|
+
if not isinstance(tool, dict):
|
|
63
|
+
continue
|
|
64
|
+
name = tool.get("name")
|
|
65
|
+
description = tool.get("description", "")
|
|
66
|
+
parameters = tool.get("parameters")
|
|
67
|
+
if not name and isinstance(tool.get("function"), dict):
|
|
68
|
+
function = tool["function"]
|
|
69
|
+
name = function.get("name")
|
|
70
|
+
description = function.get("description", description)
|
|
71
|
+
parameters = function.get("parameters", parameters)
|
|
72
|
+
if tool.get("type") not in (None, "function") and not name:
|
|
73
|
+
continue
|
|
74
|
+
if not name:
|
|
75
|
+
continue
|
|
76
|
+
out.append(
|
|
77
|
+
{
|
|
78
|
+
"name": str(name),
|
|
79
|
+
"description": str(description or ""),
|
|
80
|
+
"input_schema": parameters if isinstance(parameters, dict) else {"type": "object", "properties": {}},
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _tool_choice_to_anthropic(tool_choice: Any) -> Any:
|
|
87
|
+
if tool_choice is None:
|
|
88
|
+
return None
|
|
89
|
+
if isinstance(tool_choice, str):
|
|
90
|
+
lowered = tool_choice.strip().lower()
|
|
91
|
+
if lowered == "required":
|
|
92
|
+
return {"type": "any"}
|
|
93
|
+
if lowered in ("auto", "none"):
|
|
94
|
+
return {"type": "auto"} if lowered == "auto" else None
|
|
95
|
+
return tool_choice
|
|
96
|
+
if not isinstance(tool_choice, dict):
|
|
97
|
+
return tool_choice
|
|
98
|
+
if tool_choice.get("type") == "function":
|
|
99
|
+
name = tool_choice.get("name")
|
|
100
|
+
if not name and isinstance(tool_choice.get("function"), dict):
|
|
101
|
+
name = tool_choice["function"].get("name")
|
|
102
|
+
if name:
|
|
103
|
+
return {"type": "tool", "name": str(name)}
|
|
104
|
+
return tool_choice
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def openai_responses_to_anthropic_messages(body: dict[str, Any], fallback_model: str) -> dict[str, Any]:
|
|
108
|
+
system_parts: list[str] = []
|
|
109
|
+
instructions = str(body.get("instructions") or "").strip()
|
|
110
|
+
if instructions:
|
|
111
|
+
system_parts.append(instructions)
|
|
112
|
+
messages: list[dict[str, Any]] = []
|
|
113
|
+
raw_input = body.get("input", [])
|
|
114
|
+
if isinstance(raw_input, str):
|
|
115
|
+
raw_input = [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": raw_input}]}]
|
|
116
|
+
if isinstance(raw_input, dict):
|
|
117
|
+
raw_input = [raw_input]
|
|
118
|
+
if not isinstance(raw_input, list):
|
|
119
|
+
raw_input = []
|
|
120
|
+
for item in raw_input:
|
|
121
|
+
if not isinstance(item, dict):
|
|
122
|
+
continue
|
|
123
|
+
item_type = str(item.get("type") or "message")
|
|
124
|
+
if item_type == "function_call":
|
|
125
|
+
call_id = str(item.get("call_id") or item.get("id") or f"call_{len(messages) + 1}")
|
|
126
|
+
messages.append(
|
|
127
|
+
{
|
|
128
|
+
"role": "assistant",
|
|
129
|
+
"content": [
|
|
130
|
+
{
|
|
131
|
+
"type": "tool_use",
|
|
132
|
+
"id": call_id,
|
|
133
|
+
"name": str(item.get("name") or "tool"),
|
|
134
|
+
"input": _json_object(item.get("arguments")),
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
continue
|
|
140
|
+
if item_type == "function_call_output":
|
|
141
|
+
call_id = str(item.get("call_id") or item.get("id") or "call_tool")
|
|
142
|
+
messages.append(
|
|
143
|
+
{
|
|
144
|
+
"role": "user",
|
|
145
|
+
"content": [
|
|
146
|
+
{
|
|
147
|
+
"type": "tool_result",
|
|
148
|
+
"tool_use_id": call_id,
|
|
149
|
+
"content": _content_text(item.get("output")),
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
continue
|
|
155
|
+
role = str(item.get("role") or "user").strip().lower()
|
|
156
|
+
blocks = _content_blocks(item.get("content", item.get("text", "")))
|
|
157
|
+
if not blocks:
|
|
158
|
+
continue
|
|
159
|
+
if role in ("system", "developer"):
|
|
160
|
+
system_parts.append(_content_text(blocks))
|
|
161
|
+
continue
|
|
162
|
+
if role not in ("user", "assistant"):
|
|
163
|
+
role = "user"
|
|
164
|
+
messages.append({"role": role, "content": blocks})
|
|
165
|
+
if not messages:
|
|
166
|
+
messages.append({"role": "user", "content": [{"type": "text", "text": ""}]})
|
|
167
|
+
out: dict[str, Any] = {
|
|
168
|
+
"model": str(body.get("model") or fallback_model or "model"),
|
|
169
|
+
"messages": messages,
|
|
170
|
+
"stream": bool(body.get("stream", True)),
|
|
171
|
+
}
|
|
172
|
+
tools = _tools_to_anthropic(body.get("tools"))
|
|
173
|
+
if tools:
|
|
174
|
+
out["tools"] = tools
|
|
175
|
+
tool_choice = _tool_choice_to_anthropic(body.get("tool_choice"))
|
|
176
|
+
if tool_choice is not None:
|
|
177
|
+
out["tool_choice"] = tool_choice
|
|
178
|
+
max_tokens = _positive_int(body.get("max_output_tokens")) or _positive_int(body.get("max_tokens"))
|
|
179
|
+
if max_tokens:
|
|
180
|
+
out["max_tokens"] = max_tokens
|
|
181
|
+
reasoning = body.get("reasoning")
|
|
182
|
+
if isinstance(reasoning, dict) and reasoning.get("effort") is not None:
|
|
183
|
+
out["thinking"] = {
|
|
184
|
+
"type": "enabled",
|
|
185
|
+
"effort": str(reasoning["effort"]),
|
|
186
|
+
}
|
|
187
|
+
if system_parts:
|
|
188
|
+
out["system"] = [{"type": "text", "text": part} for part in system_parts if part]
|
|
189
|
+
return out
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _usage_from_anthropic(message: dict[str, Any]) -> dict[str, int]:
|
|
193
|
+
usage = message.get("usage") if isinstance(message.get("usage"), dict) else {}
|
|
194
|
+
input_tokens = _positive_int(usage.get("input_tokens")) or 0
|
|
195
|
+
output_tokens = _positive_int(usage.get("output_tokens")) or 0
|
|
196
|
+
return {
|
|
197
|
+
"input_tokens": input_tokens,
|
|
198
|
+
"output_tokens": output_tokens,
|
|
199
|
+
"total_tokens": input_tokens + output_tokens,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def anthropic_message_to_openai_response(
|
|
204
|
+
message: dict[str, Any], source_body: dict[str, Any] | None = None
|
|
205
|
+
) -> dict[str, Any]:
|
|
206
|
+
response_id = f"resp_{uuid.uuid4().hex}"
|
|
207
|
+
created_at = int(time.time())
|
|
208
|
+
model = str(message.get("model") or (source_body or {}).get("model") or "")
|
|
209
|
+
output: list[dict[str, Any]] = []
|
|
210
|
+
for index, block in enumerate(message.get("content") or []):
|
|
211
|
+
if not isinstance(block, dict):
|
|
212
|
+
continue
|
|
213
|
+
block_type = block.get("type")
|
|
214
|
+
if block_type == "text":
|
|
215
|
+
text = str(block.get("text") or "")
|
|
216
|
+
output.append(
|
|
217
|
+
{
|
|
218
|
+
"id": f"msg_{response_id[5:13]}_{index}",
|
|
219
|
+
"type": "message",
|
|
220
|
+
"status": "completed",
|
|
221
|
+
"role": "assistant",
|
|
222
|
+
"content": [{"type": "output_text", "text": text, "annotations": []}],
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
elif block_type == "tool_use":
|
|
226
|
+
call_id = str(block.get("id") or f"call_{index + 1}")
|
|
227
|
+
output.append(
|
|
228
|
+
{
|
|
229
|
+
"id": f"fc_{response_id[5:13]}_{index}",
|
|
230
|
+
"type": "function_call",
|
|
231
|
+
"status": "completed",
|
|
232
|
+
"call_id": call_id,
|
|
233
|
+
"name": str(block.get("name") or "tool"),
|
|
234
|
+
"arguments": json.dumps(block.get("input") or {}, ensure_ascii=False),
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
return {
|
|
238
|
+
"id": response_id,
|
|
239
|
+
"object": "response",
|
|
240
|
+
"created_at": created_at,
|
|
241
|
+
"status": "completed",
|
|
242
|
+
"model": model,
|
|
243
|
+
"output": output,
|
|
244
|
+
"parallel_tool_calls": bool((source_body or {}).get("parallel_tool_calls", True)),
|
|
245
|
+
"tool_choice": (source_body or {}).get("tool_choice", "auto"),
|
|
246
|
+
"tools": (source_body or {}).get("tools", []),
|
|
247
|
+
"usage": _usage_from_anthropic(message),
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class OpenAIResponsesProtocolAdapter(MessageProtocolAdapter):
|
|
252
|
+
"""Concrete adapter between OpenAI Responses and Anthropic Messages."""
|
|
253
|
+
|
|
254
|
+
name = "openai_responses"
|
|
255
|
+
|
|
256
|
+
def __init__(self, *, fallback_model: str = "model", source_body: dict[str, Any] | None = None) -> None:
|
|
257
|
+
self._fallback_model = fallback_model
|
|
258
|
+
self._source_body = source_body
|
|
259
|
+
|
|
260
|
+
def normalize_request(self, request: Any) -> dict[str, Any]:
|
|
261
|
+
return openai_responses_to_anthropic_messages(dict(request), self._fallback_model)
|
|
262
|
+
|
|
263
|
+
def normalize_response(self, response: Any) -> dict[str, Any]:
|
|
264
|
+
return anthropic_message_to_openai_response(dict(response), self._source_body)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
__all__ = [
|
|
268
|
+
"OpenAIResponsesProtocolAdapter",
|
|
269
|
+
"anthropic_message_to_openai_response",
|
|
270
|
+
"openai_responses_to_anthropic_messages",
|
|
271
|
+
]
|