@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.
Files changed (287) hide show
  1. package/README.md +132 -0
  2. package/ciel-runtime-menu.py +56 -6
  3. package/ciel_runtime.py +9379 -35909
  4. package/ciel_runtime_support/advisor_client.py +193 -0
  5. package/ciel_runtime_support/advisor_policy.py +320 -0
  6. package/ciel_runtime_support/advisor_refinement.py +160 -0
  7. package/ciel_runtime_support/advisor_request_builder.py +261 -0
  8. package/ciel_runtime_support/agy_installer.py +169 -0
  9. package/ciel_runtime_support/agy_mcp_restore.py +182 -0
  10. package/ciel_runtime_support/anthropic_model_policy.py +186 -0
  11. package/ciel_runtime_support/anthropic_response_writer.py +255 -0
  12. package/ciel_runtime_support/anthropic_tool_turns.py +130 -0
  13. package/ciel_runtime_support/api_key_cooldown.py +159 -0
  14. package/ciel_runtime_support/architecture.py +488 -1
  15. package/ciel_runtime_support/architecture_budget.py +42 -0
  16. package/ciel_runtime_support/channel_backlog.py +90 -0
  17. package/ciel_runtime_support/channel_cli.py +119 -0
  18. package/ciel_runtime_support/channel_compact_injection.py +82 -0
  19. package/ciel_runtime_support/channel_compact_poll.py +67 -0
  20. package/ciel_runtime_support/channel_compact_request_repository.py +113 -0
  21. package/ciel_runtime_support/channel_config_service.py +281 -0
  22. package/ciel_runtime_support/channel_connection_lifecycle.py +180 -0
  23. package/ciel_runtime_support/channel_connection_registry.py +128 -0
  24. package/ciel_runtime_support/channel_connection_worker.py +284 -0
  25. package/ciel_runtime_support/channel_cursor_recovery.py +92 -0
  26. package/ciel_runtime_support/channel_cursor_repository.py +89 -0
  27. package/ciel_runtime_support/channel_cursor_service.py +178 -0
  28. package/ciel_runtime_support/channel_event_identity.py +212 -0
  29. package/ciel_runtime_support/channel_event_projection.py +315 -0
  30. package/ciel_runtime_support/channel_inflight.py +127 -0
  31. package/ciel_runtime_support/channel_injection.py +115 -0
  32. package/ciel_runtime_support/channel_launch_guard_repository.py +58 -0
  33. package/ciel_runtime_support/channel_launch_policy.py +180 -0
  34. package/ciel_runtime_support/channel_llm_context.py +156 -0
  35. package/ciel_runtime_support/channel_mcp_discovery.py +186 -0
  36. package/ciel_runtime_support/channel_mcp_http_controller.py +239 -0
  37. package/ciel_runtime_support/channel_mcp_ownership.py +148 -0
  38. package/ciel_runtime_support/channel_mcp_tools.py +240 -0
  39. package/ciel_runtime_support/channel_mcp_transport.py +394 -0
  40. package/ciel_runtime_support/channel_message_dedupe.py +65 -0
  41. package/ciel_runtime_support/channel_message_policy.py +256 -0
  42. package/ciel_runtime_support/channel_message_prompt.py +305 -0
  43. package/ciel_runtime_support/channel_message_repository.py +234 -0
  44. package/ciel_runtime_support/channel_notification_projection.py +217 -0
  45. package/ciel_runtime_support/channel_panel.py +162 -0
  46. package/ciel_runtime_support/channel_pending_injection.py +209 -0
  47. package/ciel_runtime_support/channel_pending_poll.py +109 -0
  48. package/ciel_runtime_support/channel_probe_cache.py +433 -0
  49. package/ciel_runtime_support/channel_probe_report.py +101 -0
  50. package/ciel_runtime_support/channel_runtime_environment.py +181 -0
  51. package/ciel_runtime_support/channel_session_lifecycle.py +114 -0
  52. package/ciel_runtime_support/channel_session_repository.py +90 -0
  53. package/ciel_runtime_support/channel_terminal_dispatch.py +115 -0
  54. package/ciel_runtime_support/channel_terminal_input.py +277 -0
  55. package/ciel_runtime_support/channel_terminal_proxy.py +447 -0
  56. package/ciel_runtime_support/channel_tool_context.py +166 -0
  57. package/ciel_runtime_support/channel_transcript.py +414 -0
  58. package/ciel_runtime_support/channel_transcript_repository.py +96 -0
  59. package/ciel_runtime_support/channel_wake_claim_repository.py +126 -0
  60. package/ciel_runtime_support/channel_wake_delivery_repository.py +88 -0
  61. package/ciel_runtime_support/chat_files.py +138 -0
  62. package/ciel_runtime_support/chat_http_controller.py +235 -0
  63. package/ciel_runtime_support/claude_environment.py +375 -0
  64. package/ciel_runtime_support/claude_router.py +247 -193
  65. package/ciel_runtime_support/cli_dispatch.py +792 -0
  66. package/ciel_runtime_support/cli_parser.py +165 -0
  67. package/ciel_runtime_support/cli_usage.py +100 -0
  68. package/ciel_runtime_support/codex_app_server.py +20 -5
  69. package/ciel_runtime_support/codex_channel_sse_launch.py +87 -0
  70. package/ciel_runtime_support/codex_cli.py +42 -6
  71. package/ciel_runtime_support/codex_config.py +323 -0
  72. package/ciel_runtime_support/codex_launch_configuration.py +240 -0
  73. package/ciel_runtime_support/codex_launch_policy.py +66 -0
  74. package/ciel_runtime_support/codex_mcp_integration.py +195 -0
  75. package/ciel_runtime_support/codex_mcp_restore.py +304 -0
  76. package/ciel_runtime_support/codex_model_catalog.py +133 -0
  77. package/ciel_runtime_support/codex_process_lifecycle.py +271 -0
  78. package/ciel_runtime_support/codex_router.py +147 -1
  79. package/ciel_runtime_support/codex_session_repository.py +115 -0
  80. package/ciel_runtime_support/codex_session_selection.py +114 -0
  81. package/ciel_runtime_support/command_asset_installer.py +103 -0
  82. package/ciel_runtime_support/compatibility_probe.py +295 -0
  83. package/ciel_runtime_support/compatibility_protocol.py +251 -0
  84. package/ciel_runtime_support/compatibility_runtime.py +166 -0
  85. package/ciel_runtime_support/compatibility_test.py +370 -0
  86. package/ciel_runtime_support/config_migrations.py +307 -0
  87. package/ciel_runtime_support/config_repository.py +175 -0
  88. package/ciel_runtime_support/config_value_codec.py +64 -0
  89. package/ciel_runtime_support/configuration_cli.py +374 -0
  90. package/ciel_runtime_support/context_compaction.py +280 -0
  91. package/ciel_runtime_support/context_setup.py +208 -0
  92. package/ciel_runtime_support/context_summary_policy.py +392 -0
  93. package/ciel_runtime_support/credential_cli.py +104 -0
  94. package/ciel_runtime_support/credential_management.py +261 -0
  95. package/ciel_runtime_support/credentials.py +269 -0
  96. package/ciel_runtime_support/executable_discovery.py +141 -0
  97. package/ciel_runtime_support/github_copilot_oauth.py +335 -0
  98. package/ciel_runtime_support/github_copilot_oauth_runtime.py +213 -0
  99. package/ciel_runtime_support/header_forwarding.py +73 -0
  100. package/ciel_runtime_support/headless_config.py +221 -0
  101. package/ciel_runtime_support/http_response.py +129 -0
  102. package/ciel_runtime_support/install_diagnostics.py +149 -0
  103. package/ciel_runtime_support/kimi_identity.py +123 -0
  104. package/ciel_runtime_support/launch_diagnostics.py +204 -0
  105. package/ciel_runtime_support/launch_state.py +127 -0
  106. package/ciel_runtime_support/live_api_key_controller.py +58 -0
  107. package/ciel_runtime_support/llm_config_http.py +148 -0
  108. package/ciel_runtime_support/llm_option_config.py +259 -0
  109. package/ciel_runtime_support/llm_presentation_data.py +447 -0
  110. package/ciel_runtime_support/llm_presets.py +773 -0
  111. package/ciel_runtime_support/lm_studio_runtime.py +401 -0
  112. package/ciel_runtime_support/managed_mcp_config.py +144 -0
  113. package/ciel_runtime_support/managed_mcp_discovery.py +151 -0
  114. package/ciel_runtime_support/managed_service_cleanup.py +89 -0
  115. package/ciel_runtime_support/mcp_config_reader.py +230 -0
  116. package/ciel_runtime_support/mcp_http_proxy.py +607 -0
  117. package/ciel_runtime_support/mcp_inventory.py +59 -0
  118. package/ciel_runtime_support/mcp_notification_wait_policy.py +159 -0
  119. package/ciel_runtime_support/mcp_probe_codec.py +136 -0
  120. package/ciel_runtime_support/mcp_probe_transport.py +328 -0
  121. package/ciel_runtime_support/mcp_proxy_codec.py +345 -0
  122. package/ciel_runtime_support/mcp_proxy_config.py +107 -0
  123. package/ciel_runtime_support/mcp_proxy_notifications.py +261 -0
  124. package/ciel_runtime_support/mcp_proxy_process.py +560 -0
  125. package/ciel_runtime_support/mcp_split_proxy_http.py +165 -0
  126. package/ciel_runtime_support/mcp_stdio_probe.py +240 -0
  127. package/ciel_runtime_support/mcp_transport.py +146 -0
  128. package/ciel_runtime_support/model_cache_lifecycle.py +78 -0
  129. package/ciel_runtime_support/model_catalog_projection.py +61 -0
  130. package/ciel_runtime_support/model_context_hints.py +109 -0
  131. package/ciel_runtime_support/model_panel.py +147 -0
  132. package/ciel_runtime_support/model_registry_repository.py +231 -0
  133. package/ciel_runtime_support/npm_runtime.py +191 -0
  134. package/ciel_runtime_support/ollama_catalog.py +462 -0
  135. package/ciel_runtime_support/ollama_catalog_cli.py +41 -0
  136. package/ciel_runtime_support/ollama_catalog_repository.py +75 -0
  137. package/ciel_runtime_support/ollama_context_sync.py +87 -0
  138. package/ciel_runtime_support/ollama_forwarding.py +449 -0
  139. package/ciel_runtime_support/openai_chat_passthrough.py +67 -0
  140. package/ciel_runtime_support/openai_chat_router.py +64 -0
  141. package/ciel_runtime_support/openai_forwarding.py +194 -0
  142. package/ciel_runtime_support/openai_responses_router.py +291 -0
  143. package/ciel_runtime_support/openai_responses_stream.py +135 -0
  144. package/ciel_runtime_support/output_budget.py +89 -0
  145. package/ciel_runtime_support/package_lifecycle.py +217 -0
  146. package/ciel_runtime_support/plan_artifact_controller.py +104 -0
  147. package/ciel_runtime_support/prelaunch.py +959 -0
  148. package/ciel_runtime_support/prelaunch_launch_preference.py +75 -0
  149. package/ciel_runtime_support/prelaunch_panel_projection.py +396 -0
  150. package/ciel_runtime_support/prelaunch_terminal.py +764 -0
  151. package/ciel_runtime_support/process_control.py +708 -0
  152. package/ciel_runtime_support/prompt_compaction.py +322 -0
  153. package/ciel_runtime_support/prompt_injection.py +176 -0
  154. package/ciel_runtime_support/protocols/__init__.py +24 -0
  155. package/ciel_runtime_support/protocols/anthropic_content.py +29 -0
  156. package/ciel_runtime_support/protocols/anthropic_thinking_policy.py +336 -0
  157. package/ciel_runtime_support/protocols/chat_projection.py +315 -0
  158. package/ciel_runtime_support/protocols/conversation_policy.py +217 -0
  159. package/ciel_runtime_support/protocols/conversation_turn_policy.py +972 -0
  160. package/ciel_runtime_support/protocols/ollama_chat.py +111 -0
  161. package/ciel_runtime_support/protocols/ollama_response.py +231 -0
  162. package/ciel_runtime_support/protocols/openai_reasoning.py +75 -0
  163. package/ciel_runtime_support/protocols/openai_responses.py +271 -0
  164. package/ciel_runtime_support/protocols/pseudo_tool_history.py +248 -0
  165. package/ciel_runtime_support/protocols/tool_result_projection.py +112 -0
  166. package/ciel_runtime_support/provider_adapters.py +160 -0
  167. package/ciel_runtime_support/provider_catalog_sources.py +316 -0
  168. package/ciel_runtime_support/provider_choice.py +201 -0
  169. package/ciel_runtime_support/provider_compatibility.py +165 -0
  170. package/ciel_runtime_support/provider_config_mutations.py +361 -0
  171. package/ciel_runtime_support/provider_configuration_service.py +162 -0
  172. package/ciel_runtime_support/provider_context.py +308 -0
  173. package/ciel_runtime_support/provider_contract_projection.py +73 -0
  174. package/ciel_runtime_support/provider_descriptor.py +82 -0
  175. package/ciel_runtime_support/provider_endpoint_policy.py +130 -0
  176. package/ciel_runtime_support/provider_endpoint_probe.py +165 -0
  177. package/ciel_runtime_support/provider_launch_endpoint.py +109 -0
  178. package/ciel_runtime_support/provider_limits.py +457 -0
  179. package/ciel_runtime_support/provider_model_identity.py +139 -0
  180. package/ciel_runtime_support/provider_model_selection.py +431 -0
  181. package/ciel_runtime_support/provider_model_specs.py +142 -0
  182. package/ciel_runtime_support/provider_models.py +263 -0
  183. package/ciel_runtime_support/provider_network.py +176 -0
  184. package/ciel_runtime_support/provider_option_cli.py +238 -0
  185. package/ciel_runtime_support/provider_option_panel.py +275 -0
  186. package/ciel_runtime_support/provider_option_status.py +192 -0
  187. package/ciel_runtime_support/provider_policy.py +101 -0
  188. package/ciel_runtime_support/provider_query_policy.py +67 -0
  189. package/ciel_runtime_support/provider_readiness.py +112 -0
  190. package/ciel_runtime_support/provider_request_access.py +131 -0
  191. package/ciel_runtime_support/provider_request_builder.py +250 -0
  192. package/ciel_runtime_support/provider_responses_passthrough.py +81 -0
  193. package/ciel_runtime_support/provider_runtime_info.py +113 -0
  194. package/ciel_runtime_support/provider_runtime_modes.py +150 -0
  195. package/ciel_runtime_support/provider_sampling_policy.py +46 -0
  196. package/ciel_runtime_support/provider_status.py +145 -0
  197. package/ciel_runtime_support/provider_timeout_policy.py +184 -0
  198. package/ciel_runtime_support/provider_tool_policy.py +145 -0
  199. package/ciel_runtime_support/providers/__init__.py +65 -0
  200. package/ciel_runtime_support/providers/anthropic.py +160 -0
  201. package/ciel_runtime_support/providers/anthropic_catalog.py +119 -0
  202. package/ciel_runtime_support/providers/base.py +232 -0
  203. package/ciel_runtime_support/providers/catalog.py +326 -0
  204. package/ciel_runtime_support/providers/cloud.py +194 -0
  205. package/ciel_runtime_support/providers/constants.py +54 -0
  206. package/ciel_runtime_support/providers/deepseek.py +115 -0
  207. package/ciel_runtime_support/providers/fireworks.py +158 -0
  208. package/ciel_runtime_support/providers/github_copilot_oauth.py +199 -0
  209. package/ciel_runtime_support/providers/kimi.py +297 -0
  210. package/ciel_runtime_support/providers/lm_studio.py +76 -0
  211. package/ciel_runtime_support/providers/meta.py +257 -0
  212. package/ciel_runtime_support/providers/native.py +194 -0
  213. package/ciel_runtime_support/providers/nim.py +69 -0
  214. package/ciel_runtime_support/providers/nvidia.py +158 -0
  215. package/ciel_runtime_support/providers/nvidia_runtime.py +285 -0
  216. package/ciel_runtime_support/providers/ollama.py +181 -0
  217. package/ciel_runtime_support/providers/ollama_context.py +195 -0
  218. package/ciel_runtime_support/providers/ollama_runtime.py +213 -0
  219. package/ciel_runtime_support/providers/opencode.py +220 -0
  220. package/ciel_runtime_support/providers/opencode_go.py +37 -0
  221. package/ciel_runtime_support/providers/openrouter.py +57 -0
  222. package/ciel_runtime_support/providers/vllm.py +65 -0
  223. package/ciel_runtime_support/providers/zai.py +119 -0
  224. package/ciel_runtime_support/pseudo_tool_parser.py +115 -0
  225. package/ciel_runtime_support/rate_limit_policy.py +117 -0
  226. package/ciel_runtime_support/rate_limit_repository.py +154 -0
  227. package/ciel_runtime_support/registry.py +46 -0
  228. package/ciel_runtime_support/request_shortcuts.py +253 -0
  229. package/ciel_runtime_support/request_trace.py +323 -0
  230. package/ciel_runtime_support/response_collection.py +209 -0
  231. package/ciel_runtime_support/router_access.py +238 -0
  232. package/ciel_runtime_support/router_client_lifecycle.py +366 -0
  233. package/ciel_runtime_support/router_health_policy.py +101 -0
  234. package/ciel_runtime_support/router_http.py +513 -0
  235. package/ciel_runtime_support/router_process_lifecycle.py +401 -0
  236. package/ciel_runtime_support/router_rate_limit_service.py +285 -0
  237. package/ciel_runtime_support/router_server_runtime.py +103 -0
  238. package/ciel_runtime_support/router_shortcuts.py +201 -0
  239. package/ciel_runtime_support/routing_fallback.py +73 -0
  240. package/ciel_runtime_support/runtime_activity_repository.py +143 -0
  241. package/ciel_runtime_support/runtime_adapters.py +104 -0
  242. package/ciel_runtime_support/runtime_command_factory.py +73 -0
  243. package/ciel_runtime_support/runtime_compatibility.py +50 -0
  244. package/ciel_runtime_support/runtime_constants.py +178 -0
  245. package/ciel_runtime_support/runtime_launch.py +1602 -0
  246. package/ciel_runtime_support/runtime_llm_options.py +312 -0
  247. package/ciel_runtime_support/runtime_logging.py +161 -0
  248. package/ciel_runtime_support/runtime_paths.py +157 -0
  249. package/ciel_runtime_support/runtime_restart.py +84 -0
  250. package/ciel_runtime_support/runtime_upgrade.py +149 -0
  251. package/ciel_runtime_support/secure_json_repository.py +55 -0
  252. package/ciel_runtime_support/session_import.py +356 -0
  253. package/ciel_runtime_support/settings_repository.py +8 -0
  254. package/ciel_runtime_support/slash_command_assets.py +211 -0
  255. package/ciel_runtime_support/sse_stream.py +57 -0
  256. package/ciel_runtime_support/sse_trace.py +225 -0
  257. package/ciel_runtime_support/statusline_script.py +593 -0
  258. package/ciel_runtime_support/statusline_settings.py +53 -0
  259. package/ciel_runtime_support/stream_chunk_policy.py +18 -0
  260. package/ciel_runtime_support/streaming_anthropic.py +1955 -0
  261. package/ciel_runtime_support/synthetic_tool_policy.py +105 -0
  262. package/ciel_runtime_support/terminal_platform_io.py +127 -0
  263. package/ciel_runtime_support/timeout_profile.py +196 -0
  264. package/ciel_runtime_support/tool_dialects.py +85 -0
  265. package/ciel_runtime_support/tool_exposure_policy.py +63 -0
  266. package/ciel_runtime_support/tool_guard_hooks.py +218 -0
  267. package/ciel_runtime_support/tool_request_projection.py +96 -0
  268. package/ciel_runtime_support/tool_schema.py +483 -0
  269. package/ciel_runtime_support/tool_side_effect_dedupe.py +95 -0
  270. package/ciel_runtime_support/ui_text.py +266 -0
  271. package/ciel_runtime_support/upstream_error_policy.py +104 -0
  272. package/ciel_runtime_support/upstream_retry.py +419 -0
  273. package/ciel_runtime_support/upstream_stream_io.py +106 -0
  274. package/ciel_runtime_support/usage_events.py +96 -0
  275. package/ciel_runtime_support/visible_stream_filters.py +130 -0
  276. package/ciel_runtime_support/web_endpoints.py +447 -0
  277. package/ciel_runtime_support/web_ui.py +915 -0
  278. package/ciel_runtime_support/web_ui_controller.py +189 -0
  279. package/ciel_runtime_support/windows_console_input.py +137 -0
  280. package/ciel_runtime_support/windows_console_mode.py +112 -0
  281. package/docs/Architecture.md +54 -0
  282. package/docs/Configuration.md +17 -0
  283. package/docs/Module-Map.md +1093 -26
  284. package/docs/Providers.md +46 -1
  285. package/docs/adr/0001-runtime-bounded-contexts.md +295 -0
  286. package/npm-bin/run-ciel-runtime.js +22 -2
  287. package/package.json +9 -2
@@ -0,0 +1,449 @@
1
+ """Ollama upstream forwarding application service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from http.server import BaseHTTPRequestHandler
7
+ import json
8
+ from typing import Any, Callable
9
+ import urllib.error
10
+ import urllib.request
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class OllamaForwardConstants:
15
+ client_disconnected_error: type[Exception]
16
+ compatibility_test_header: str
17
+ upstream_retry_http_codes: frozenset[int] | set[int] | tuple[int, ...]
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class OllamaForwardRequest:
22
+ normalize_thinking: Callable[..., Any]
23
+ ollama_chat_request: Callable[..., Any]
24
+ provider_endpoint: Callable[..., Any]
25
+ provider_headers: Callable[..., Any]
26
+ provider_urlopen: Callable[..., Any]
27
+ request_timeout_seconds: Callable[..., Any]
28
+ resolve_requested_model: Callable[..., Any]
29
+ set_stream_read_timeout: Callable[..., Any]
30
+ stream_idle_timeout_seconds: Callable[..., Any]
31
+
32
+
33
+ @dataclass(frozen=True, slots=True)
34
+ class OllamaForwardRateLimit:
35
+ apply_router_rate_limit: Callable[..., Any]
36
+ configured_gateway_retries: Callable[..., Any]
37
+ effective_rpm: Callable[..., Any]
38
+ learn_headers: Callable[..., Any]
39
+ notice: Callable[..., Any]
40
+ register_backoff: Callable[..., Any]
41
+ retry_wait_seconds: Callable[..., Any]
42
+ retryable_upstream_exception: Callable[..., Any]
43
+ sleep_until_or_client_disconnect: Callable[..., Any]
44
+
45
+
46
+ @dataclass(frozen=True, slots=True)
47
+ class OllamaForwardStreaming:
48
+ client_connection_closed: Callable[..., Any]
49
+ iter_upstream_lines: Callable[..., Any]
50
+ log: Callable[..., Any]
51
+ stream_to_anthropic_sse: Callable[..., Any]
52
+ write_router_activity: Callable[..., Any]
53
+
54
+
55
+ @dataclass(frozen=True, slots=True)
56
+ class OllamaForwardAdvisor:
57
+ body_with_tool: Callable[..., Any]
58
+ estimate_tokens: Callable[..., Any]
59
+ gate_possible: Callable[..., Any]
60
+ gate_reason: Callable[..., Any]
61
+ model_enabled: Callable[..., Any]
62
+ prepend_text: Callable[..., Any]
63
+ provider_supported: Callable[..., Any]
64
+ refine_message: Callable[..., Any]
65
+
66
+
67
+ @dataclass(frozen=True, slots=True)
68
+ class OllamaForwardResponse:
69
+ context_error_limit: Callable[..., Any]
70
+ context_retry_config: Callable[..., Any]
71
+ mark_pending_delivery_success: Callable[..., Any]
72
+ ollama_chat_to_anthropic: Callable[..., Any]
73
+ remember_injected_tool_uses: Callable[..., Any]
74
+ update_tool_schema_registry: Callable[..., Any]
75
+ upstream_http_error_message: Callable[..., Any]
76
+ write_json: Callable[..., Any]
77
+
78
+
79
+ @dataclass(frozen=True, slots=True)
80
+ class OllamaForwardServices:
81
+ constants: OllamaForwardConstants
82
+ request: OllamaForwardRequest
83
+ rate_limit: OllamaForwardRateLimit
84
+ streaming: OllamaForwardStreaming
85
+ advisor: OllamaForwardAdvisor
86
+ response: OllamaForwardResponse
87
+
88
+
89
+ def forward_ollama_api_chat(
90
+ handler: BaseHTTPRequestHandler,
91
+ provider: str,
92
+ pcfg: dict[str, Any],
93
+ body: dict[str, Any],
94
+ *,
95
+ services: OllamaForwardServices,
96
+ ) -> None:
97
+ constants = services.constants
98
+ request = services.request
99
+ rate_limit = services.rate_limit
100
+ streaming = services.streaming
101
+ advisor = services.advisor
102
+ response = services.response
103
+ COMPATIBILITY_TEST_HEADER = constants.compatibility_test_header
104
+ UPSTREAM_RETRY_HTTP_CODES = constants.upstream_retry_http_codes
105
+ UpstreamClientDisconnected = constants.client_disconnected_error
106
+ normalize_thinking_for_non_anthropic_provider = request.normalize_thinking
107
+ ollama_chat_request = request.ollama_chat_request
108
+ ollama_request_timeout_seconds = request.request_timeout_seconds
109
+ provider_endpoint = request.provider_endpoint
110
+ provider_headers = request.provider_headers
111
+ provider_stream_idle_timeout_seconds = request.stream_idle_timeout_seconds
112
+ provider_urlopen = request.provider_urlopen
113
+ resolve_requested_model = request.resolve_requested_model
114
+ set_upstream_stream_read_timeout = request.set_stream_read_timeout
115
+ apply_router_rate_limit = rate_limit.apply_router_rate_limit
116
+ configured_gateway_retries = rate_limit.configured_gateway_retries
117
+ learn_router_rate_limit_headers = rate_limit.learn_headers
118
+ rate_limit_notice = rate_limit.notice
119
+ register_router_rate_limit_backoff = rate_limit.register_backoff
120
+ retryable_upstream_exception = rate_limit.retryable_upstream_exception
121
+ router_rate_limit_effective_rpm = rate_limit.effective_rpm
122
+ sleep_until_or_client_disconnect = rate_limit.sleep_until_or_client_disconnect
123
+ upstream_retry_wait_seconds = rate_limit.retry_wait_seconds
124
+ _ollama_stream_to_anthropic_sse = streaming.stream_to_anthropic_sse
125
+ iter_upstream_lines_until_client_disconnect = streaming.iter_upstream_lines
126
+ router_client_connection_closed = streaming.client_connection_closed
127
+ router_log = streaming.log
128
+ write_router_activity = streaming.write_router_activity
129
+ advisor_gate_possible_for_body = advisor.gate_possible
130
+ advisor_gate_reason_for_body = advisor.gate_reason
131
+ advisor_model_enabled = advisor.model_enabled
132
+ advisor_provider_supported = advisor.provider_supported
133
+ body_with_advisor_tool = advisor.body_with_tool
134
+ estimate_tokens = advisor.estimate_tokens
135
+ prepend_anthropic_text = advisor.prepend_text
136
+ refine_message_with_advisor = advisor.refine_message
137
+ _update_tool_schema_registry = response.update_tool_schema_registry
138
+ mark_pending_channel_delivery_success = response.mark_pending_delivery_success
139
+ ollama_chat_to_anthropic = response.ollama_chat_to_anthropic
140
+ ollama_context_error_limit = response.context_error_limit
141
+ ollama_context_retry_config = response.context_retry_config
142
+ remember_channel_injected_tool_uses = response.remember_injected_tool_uses
143
+ upstream_http_error_message = response.upstream_http_error_message
144
+ write_json = response.write_json
145
+ _update_tool_schema_registry(body.get("tools"))
146
+ body = normalize_thinking_for_non_anthropic_provider(provider, pcfg, body)
147
+ model = resolve_requested_model(provider, pcfg, body.get("model"))
148
+ compatibility_test = str(handler.headers.get(COMPATIBILITY_TEST_HEADER) or "").strip().lower() in ("1", "true", "yes", "on")
149
+ original_body = body
150
+ upstream_body = body_with_advisor_tool(body, pcfg) if advisor_provider_supported(provider) else body
151
+ stream_requested = body.get("stream", True)
152
+ if not bool(pcfg.get("stream_enabled", True)):
153
+ stream_requested = False
154
+ if stream_requested and advisor_model_enabled(pcfg) and advisor_provider_supported(provider):
155
+ stream_requested = False
156
+ router_log("INFO", "advisor tool enabled; collecting this turn so advisor tool calls can be resolved internally")
157
+ if stream_requested and advisor_gate_possible_for_body(provider, pcfg, body):
158
+ gate_reason = advisor_gate_reason_for_body(provider, pcfg, body)
159
+ stream_requested = False
160
+ router_log("INFO", f"advisor gate enabled reason={gate_reason}; collecting this turn before returning it to Claude Code")
161
+ word_chunking = bool(pcfg.get("stream_word_chunking", False))
162
+ req_body = ollama_chat_request(model, upstream_body, pcfg, stream=stream_requested, provider=provider)
163
+ headers = provider_headers(
164
+ provider, pcfg, handler.headers, "ollama_chat"
165
+ )
166
+ url = provider_endpoint(provider, pcfg, "ollama_chat")
167
+ if compatibility_test:
168
+ waited, rpm_used, rpm_limit = 0.0, 0, router_rate_limit_effective_rpm(provider, pcfg, model)
169
+ else:
170
+ waited, rpm_used, rpm_limit = apply_router_rate_limit(provider, pcfg, model)
171
+ rpm_status = bool(pcfg.get("rate_limit_status", False))
172
+ if stream_requested:
173
+ # Stream Ollama response through as Anthropic SSE
174
+ data_bytes = json.dumps(req_body).encode("utf-8")
175
+ req_tokens = estimate_tokens(req_body)
176
+ req_bytes = len(data_bytes)
177
+ gateway_retries = 0 if compatibility_test else configured_gateway_retries(pcfg)
178
+ max_attempts = max(1, gateway_retries + 1)
179
+ loop_attempts = max_attempts + 1
180
+ context_retry_used = False
181
+ resp = None
182
+ stream_idle_timeout = provider_stream_idle_timeout_seconds(pcfg)
183
+ for attempt in range(loop_attempts):
184
+ req = urllib.request.Request(url, data=data_bytes, headers=headers, method="POST")
185
+ try:
186
+ write_router_activity(
187
+ "request",
188
+ provider,
189
+ model,
190
+ attempt=attempt + 1,
191
+ total=max_attempts,
192
+ tokens=req_tokens,
193
+ bytes=req_bytes,
194
+ timeout=ollama_request_timeout_seconds(pcfg),
195
+ stream=True,
196
+ )
197
+ router_log("INFO", f"ollama_stream_request provider={provider} model={model} attempt={attempt + 1}/{max_attempts} tokens={req_tokens} bytes={req_bytes}")
198
+ resp = provider_urlopen(req, timeout=ollama_request_timeout_seconds(pcfg), provider=provider, pcfg=pcfg)
199
+ set_upstream_stream_read_timeout(resp, stream_idle_timeout)
200
+ learn_router_rate_limit_headers(provider, pcfg, model, resp.headers)
201
+ break
202
+ except urllib.error.HTTPError as exc:
203
+ raw = exc.read().decode("utf-8", errors="ignore")
204
+ learn_router_rate_limit_headers(provider, pcfg, model, exc.headers)
205
+ context_limit = ollama_context_error_limit(raw)
206
+ if exc.code == 400 and context_limit and not context_retry_used:
207
+ context_retry_used = True
208
+ retry_pcfg = ollama_context_retry_config(pcfg, context_limit)
209
+ req_body = ollama_chat_request(model, upstream_body, retry_pcfg, stream=stream_requested, provider=provider)
210
+ data_bytes = json.dumps(req_body).encode("utf-8")
211
+ req_tokens = estimate_tokens(req_body)
212
+ req_bytes = len(data_bytes)
213
+ write_router_activity(
214
+ "retry",
215
+ provider,
216
+ model,
217
+ attempt=attempt + 1,
218
+ total=max_attempts,
219
+ code=exc.code,
220
+ reason="context_compact_retry",
221
+ context_limit=context_limit,
222
+ tokens=req_tokens,
223
+ bytes=req_bytes,
224
+ stream=True,
225
+ )
226
+ router_log(
227
+ "WARN",
228
+ f"ollama_stream_context_retry provider={provider} model={model} n_ctx={context_limit} tokens={req_tokens} bytes={req_bytes}",
229
+ )
230
+ continue
231
+ if exc.code == 429 and attempt + 1 < max_attempts:
232
+ retry_no = attempt + 1
233
+ wait = register_router_rate_limit_backoff(provider, pcfg, model, exc.headers.get("Retry-After"))
234
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, code=exc.code, wait=wait, tokens=req_tokens, bytes=req_bytes, stream=True)
235
+ router_log("WARN", f"ollama_stream_rate_limit_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} wait={wait:.2f}s tokens={req_tokens} bytes={req_bytes}")
236
+ if not sleep_until_or_client_disconnect(handler, wait):
237
+ write_router_activity("cancel", provider, model, stage="rate_limit_retry_wait", tokens=req_tokens, bytes=req_bytes, stream=True)
238
+ router_log("WARN", f"ollama_stream_cancelled_before_rate_limit_retry provider={provider} model={model} tokens={req_tokens} bytes={req_bytes}")
239
+ return
240
+ continue
241
+ if exc.code in UPSTREAM_RETRY_HTTP_CODES and attempt + 1 < max_attempts:
242
+ retry_no = attempt + 1
243
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, code=exc.code, tokens=req_tokens, bytes=req_bytes, stream=True)
244
+ router_log("WARN", f"ollama_stream_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} code={exc.code} tokens={req_tokens} bytes={req_bytes}")
245
+ if not sleep_until_or_client_disconnect(handler, upstream_retry_wait_seconds(retry_no)):
246
+ write_router_activity("cancel", provider, model, stage="http_retry_wait", tokens=req_tokens, bytes=req_bytes, stream=True)
247
+ router_log("WARN", f"ollama_stream_cancelled_before_http_retry provider={provider} model={model} tokens={req_tokens} bytes={req_bytes}")
248
+ return
249
+ continue
250
+ if router_client_connection_closed(handler):
251
+ write_router_activity("cancel", provider, model, stage="http_error", code=exc.code, tokens=req_tokens, bytes=req_bytes, stream=True)
252
+ router_log("WARN", f"ollama_stream_client_gone_before_error_response provider={provider} model={model} code={exc.code}")
253
+ return
254
+ write_router_activity("error", provider, model, code=exc.code, tokens=req_tokens, bytes=req_bytes, stream=True)
255
+ write_json(
256
+ handler,
257
+ {"type": "error", "error": {"type": "upstream_error", "message": upstream_http_error_message(exc, raw)}},
258
+ exc.code,
259
+ )
260
+ return
261
+ except (TimeoutError, urllib.error.URLError, OSError) as exc:
262
+ if retryable_upstream_exception(exc) and attempt + 1 < max_attempts:
263
+ retry_no = attempt + 1
264
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes, stream=True)
265
+ router_log("WARN", f"ollama_stream_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} error={type(exc).__name__} tokens={req_tokens} bytes={req_bytes}")
266
+ if not sleep_until_or_client_disconnect(handler, upstream_retry_wait_seconds(retry_no)):
267
+ write_router_activity("cancel", provider, model, stage="exception_retry_wait", error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes, stream=True)
268
+ router_log("WARN", f"ollama_stream_cancelled_before_exception_retry provider={provider} model={model} error={type(exc).__name__}")
269
+ return
270
+ continue
271
+ if router_client_connection_closed(handler):
272
+ write_router_activity("cancel", provider, model, stage="exception_error", error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes, stream=True)
273
+ router_log("WARN", f"ollama_stream_client_gone_before_exception_response provider={provider} model={model} error={type(exc).__name__}")
274
+ return
275
+ write_router_activity("error", provider, model, error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes, stream=True)
276
+ write_json(
277
+ handler,
278
+ {"type": "error", "error": {"type": "upstream_error", "message": f"{type(exc).__name__}: {exc}"}},
279
+ 504 if retryable_upstream_exception(exc) else 502,
280
+ )
281
+ return
282
+ if resp is None:
283
+ write_router_activity("error", provider, model, tokens=req_tokens, bytes=req_bytes, stream=True)
284
+ write_json(
285
+ handler,
286
+ {"type": "error", "error": {"type": "upstream_error", "message": "upstream stream request failed"}},
287
+ 504,
288
+ )
289
+ return
290
+ # Check if Claude Code requested SSE streaming
291
+ accept = handler.headers.get("accept", "")
292
+ if "text/event-stream" in accept or stream_requested:
293
+ _ollama_stream_to_anthropic_sse(handler, resp, model, word_chunking=word_chunking, provider=provider, source_body=original_body, idle_timeout=stream_idle_timeout)
294
+ else:
295
+ # Non-SSE client but streaming from Ollama: collect full response
296
+ chunks = []
297
+ try:
298
+ for line in iter_upstream_lines_until_client_disconnect(handler, resp, stream_idle_timeout):
299
+ chunks.append(line)
300
+ except UpstreamClientDisconnected as exc:
301
+ write_router_activity("cancel", provider, model, stage="collect_stream", error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes, stream=True)
302
+ router_log("WARN", f"ollama_stream_collect_client_disconnected provider={provider} model={model} error={exc}")
303
+ try:
304
+ resp.close()
305
+ except Exception:
306
+ pass
307
+ return
308
+ resp.close()
309
+ full = b"".join(chunks).decode("utf-8", errors="ignore")
310
+ data = None
311
+ for line in full.splitlines():
312
+ line = line.strip()
313
+ if not line:
314
+ continue
315
+ try:
316
+ chunk = json.loads(line)
317
+ if isinstance(chunk, dict) and chunk.get("done"):
318
+ data = chunk
319
+ except Exception:
320
+ continue
321
+ if data is None:
322
+ data = {"message": {"content": ""}, "done": True, "done_reason": "end_turn"}
323
+ message = ollama_chat_to_anthropic(data, model, source_body=original_body)
324
+ message = refine_message_with_advisor(provider, pcfg, original_body, message, model)
325
+ remember_channel_injected_tool_uses(original_body, message)
326
+ message = prepend_anthropic_text(message, rate_limit_notice(waited, rpm_used, rpm_limit, rpm_status))
327
+ write_json(handler, message)
328
+ mark_pending_channel_delivery_success(handler, "ollama_collected_json")
329
+ return
330
+ # Non-streaming fallback
331
+ data_bytes = json.dumps(req_body).encode("utf-8")
332
+ req_tokens = estimate_tokens(req_body)
333
+ req_bytes = len(data_bytes)
334
+ gateway_retries = 0 if compatibility_test else configured_gateway_retries(pcfg)
335
+ max_attempts = max(1, gateway_retries + 1)
336
+ loop_attempts = max_attempts + 1
337
+ context_retry_used = False
338
+ data = None
339
+ for attempt in range(loop_attempts):
340
+ req = urllib.request.Request(url, data=data_bytes, headers=headers, method="POST")
341
+ try:
342
+ write_router_activity(
343
+ "request",
344
+ provider,
345
+ model,
346
+ attempt=attempt + 1,
347
+ total=max_attempts,
348
+ tokens=req_tokens,
349
+ bytes=req_bytes,
350
+ timeout=ollama_request_timeout_seconds(pcfg),
351
+ )
352
+ router_log("INFO", f"ollama_request provider={provider} model={model} attempt={attempt + 1}/{max_attempts} tokens={req_tokens} bytes={req_bytes}")
353
+ with provider_urlopen(req, timeout=ollama_request_timeout_seconds(pcfg), provider=provider, pcfg=pcfg) as resp:
354
+ learn_router_rate_limit_headers(provider, pcfg, model, resp.headers)
355
+ data = json.loads(resp.read().decode("utf-8"))
356
+ break
357
+ except urllib.error.HTTPError as exc:
358
+ raw = exc.read().decode("utf-8", errors="ignore")
359
+ learn_router_rate_limit_headers(provider, pcfg, model, exc.headers)
360
+ context_limit = ollama_context_error_limit(raw)
361
+ if exc.code == 400 and context_limit and not context_retry_used:
362
+ context_retry_used = True
363
+ retry_pcfg = ollama_context_retry_config(pcfg, context_limit)
364
+ req_body = ollama_chat_request(model, upstream_body, retry_pcfg, stream=stream_requested, provider=provider)
365
+ data_bytes = json.dumps(req_body).encode("utf-8")
366
+ req_tokens = estimate_tokens(req_body)
367
+ req_bytes = len(data_bytes)
368
+ write_router_activity(
369
+ "retry",
370
+ provider,
371
+ model,
372
+ attempt=attempt + 1,
373
+ total=max_attempts,
374
+ code=exc.code,
375
+ reason="context_compact_retry",
376
+ context_limit=context_limit,
377
+ tokens=req_tokens,
378
+ bytes=req_bytes,
379
+ )
380
+ router_log(
381
+ "WARN",
382
+ f"ollama_context_retry provider={provider} model={model} n_ctx={context_limit} tokens={req_tokens} bytes={req_bytes}",
383
+ )
384
+ continue
385
+ if exc.code == 429 and attempt + 1 < max_attempts:
386
+ retry_no = attempt + 1
387
+ wait = register_router_rate_limit_backoff(provider, pcfg, model, exc.headers.get("Retry-After"))
388
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, code=exc.code, wait=wait, tokens=req_tokens, bytes=req_bytes)
389
+ router_log("WARN", f"ollama_rate_limit_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} wait={wait:.2f}s tokens={req_tokens} bytes={req_bytes}")
390
+ if not sleep_until_or_client_disconnect(handler, wait):
391
+ write_router_activity("cancel", provider, model, stage="rate_limit_retry_wait", tokens=req_tokens, bytes=req_bytes)
392
+ router_log("WARN", f"ollama_cancelled_before_rate_limit_retry provider={provider} model={model} tokens={req_tokens} bytes={req_bytes}")
393
+ return
394
+ continue
395
+ if exc.code in UPSTREAM_RETRY_HTTP_CODES and attempt + 1 < max_attempts:
396
+ retry_no = attempt + 1
397
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, code=exc.code, tokens=req_tokens, bytes=req_bytes)
398
+ router_log("WARN", f"ollama_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} code={exc.code} tokens={req_tokens} bytes={req_bytes}")
399
+ if not sleep_until_or_client_disconnect(handler, upstream_retry_wait_seconds(retry_no)):
400
+ write_router_activity("cancel", provider, model, stage="http_retry_wait", tokens=req_tokens, bytes=req_bytes)
401
+ router_log("WARN", f"ollama_cancelled_before_http_retry provider={provider} model={model} tokens={req_tokens} bytes={req_bytes}")
402
+ return
403
+ continue
404
+ if router_client_connection_closed(handler):
405
+ write_router_activity("cancel", provider, model, stage="http_error", code=exc.code, tokens=req_tokens, bytes=req_bytes)
406
+ router_log("WARN", f"ollama_client_gone_before_error_response provider={provider} model={model} code={exc.code}")
407
+ return
408
+ write_router_activity("error", provider, model, code=exc.code, tokens=req_tokens, bytes=req_bytes)
409
+ write_json(
410
+ handler,
411
+ {"type": "error", "error": {"type": "upstream_error", "message": upstream_http_error_message(exc, raw)}},
412
+ exc.code,
413
+ )
414
+ return
415
+ except (TimeoutError, urllib.error.URLError, OSError) as exc:
416
+ if retryable_upstream_exception(exc) and attempt + 1 < max_attempts:
417
+ retry_no = attempt + 1
418
+ write_router_activity("retry", provider, model, attempt=retry_no, total=gateway_retries, error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes)
419
+ router_log("WARN", f"ollama_retry provider={provider} model={model} attempt={retry_no}/{gateway_retries} error={type(exc).__name__} tokens={req_tokens} bytes={req_bytes}")
420
+ if not sleep_until_or_client_disconnect(handler, upstream_retry_wait_seconds(retry_no)):
421
+ write_router_activity("cancel", provider, model, stage="exception_retry_wait", error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes)
422
+ router_log("WARN", f"ollama_cancelled_before_exception_retry provider={provider} model={model} error={type(exc).__name__}")
423
+ return
424
+ continue
425
+ if router_client_connection_closed(handler):
426
+ write_router_activity("cancel", provider, model, stage="exception_error", error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes)
427
+ router_log("WARN", f"ollama_client_gone_before_exception_response provider={provider} model={model} error={type(exc).__name__}")
428
+ return
429
+ write_router_activity("error", provider, model, error=type(exc).__name__, tokens=req_tokens, bytes=req_bytes)
430
+ write_json(
431
+ handler,
432
+ {"type": "error", "error": {"type": "upstream_error", "message": f"{type(exc).__name__}: {exc}"}},
433
+ 504 if retryable_upstream_exception(exc) else 502,
434
+ )
435
+ return
436
+ if data is None:
437
+ write_router_activity("error", provider, model, tokens=req_tokens, bytes=req_bytes)
438
+ write_json(
439
+ handler,
440
+ {"type": "error", "error": {"type": "upstream_error", "message": "upstream request failed"}},
441
+ 504,
442
+ )
443
+ return
444
+ message = ollama_chat_to_anthropic(data, model, source_body=original_body)
445
+ message = refine_message_with_advisor(provider, pcfg, original_body, message, model)
446
+ remember_channel_injected_tool_uses(original_body, message)
447
+ message = prepend_anthropic_text(message, rate_limit_notice(waited, rpm_used, rpm_limit, rpm_status))
448
+ write_json(handler, message)
449
+ mark_pending_channel_delivery_success(handler, "ollama_json")
@@ -0,0 +1,67 @@
1
+ """Native OpenAI Chat Completions passthrough for compatible CLI runtimes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import urllib.request
7
+ from dataclasses import dataclass
8
+ from typing import Any, Callable, Mapping
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class OpenAIChatPassthroughPorts:
13
+ normalize_model: Callable[[str, dict[str, Any], str], str]
14
+ normalize_request: Callable[[str, dict[str, Any], Mapping[str, Any]], Mapping[str, Any]]
15
+ upstream_base: Callable[[str, dict[str, Any]], str]
16
+ join_url: Callable[[str, str], str]
17
+ headers: Callable[[str, dict[str, Any], Any], dict[str, str]]
18
+ urlopen: Callable[..., Any]
19
+ timeout_seconds: Callable[[dict[str, Any]], float]
20
+ copy_response_headers: Callable[[Any, Any], None]
21
+
22
+
23
+ class OpenAIChatPassthrough:
24
+ """Forward Chat Completions while preserving its native wire protocol."""
25
+
26
+ def __init__(self, ports: OpenAIChatPassthroughPorts) -> None:
27
+ self._ports = ports
28
+
29
+ def forward(
30
+ self,
31
+ handler: Any,
32
+ provider: str,
33
+ config: dict[str, Any],
34
+ body: dict[str, Any],
35
+ ) -> None:
36
+ upstream_body = dict(body)
37
+ upstream_body["model"] = self._ports.normalize_model(
38
+ provider, config, str(body.get("model") or "")
39
+ )
40
+ upstream_body = dict(
41
+ self._ports.normalize_request(provider, config, upstream_body)
42
+ )
43
+ url = self._ports.join_url(
44
+ self._ports.upstream_base(provider, config),
45
+ "/v1/chat/completions",
46
+ )
47
+ request = urllib.request.Request(
48
+ url,
49
+ data=json.dumps(upstream_body, ensure_ascii=False).encode("utf-8"),
50
+ headers=self._ports.headers(provider, config, handler.headers),
51
+ method="POST",
52
+ )
53
+ with self._ports.urlopen(
54
+ request,
55
+ timeout=self._ports.timeout_seconds(config),
56
+ provider=provider,
57
+ pcfg=config,
58
+ ) as response:
59
+ handler.send_response(getattr(response, "status", 200))
60
+ self._ports.copy_response_headers(handler, response.headers)
61
+ handler.end_headers()
62
+ while chunk := response.read(65_536):
63
+ handler.wfile.write(chunk)
64
+ handler.wfile.flush()
65
+
66
+
67
+ __all__ = ["OpenAIChatPassthrough", "OpenAIChatPassthroughPorts"]
@@ -0,0 +1,64 @@
1
+ """OpenAI Chat Completions runtime HTTP router."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+ from .agent_router import COMMON_RUNTIME_ROUTER_CAPABILITIES, RouterCapability
8
+
9
+
10
+ class OpenAIChatRouter:
11
+ name = "openai-chat"
12
+ runtime = "openai-compatible-cli"
13
+ protocol = "openai_chat"
14
+ request_paths = ("/v1/chat/completions",)
15
+ capabilities = tuple(
16
+ RouterCapability(name, description)
17
+ for name, description in (
18
+ ("auth_forwarding", "Provider authentication replaces the local CLI placeholder key."),
19
+ ("sse_stream_proxy", "Chat Completions SSE bytes are streamed without protocol conversion."),
20
+ ("channel_context_injection", "Native Chat Completions message context is preserved."),
21
+ ("pending_delivery_ack", "Requests without pending channel delivery require no acknowledgement."),
22
+ ("request_observability", "Requests use the shared runtime HTTP request/error boundary."),
23
+ ("upstream_error_mapping", "Upstream HTTP errors use the shared router error response."),
24
+ )
25
+ )
26
+
27
+ def __init__(self, forward: Callable[..., None]) -> None:
28
+ self._forward = forward
29
+
30
+ def can_handle_get(self, path: str, provider: str, config: dict[str, Any]) -> bool:
31
+ del path, provider, config
32
+ return False
33
+
34
+ def handle_get(self, handler: Any, path: str, provider: str, config: dict[str, Any]) -> bool:
35
+ del handler, path, provider, config
36
+ return False
37
+
38
+ def can_handle_post(self, path: str, provider: str, config: dict[str, Any]) -> bool:
39
+ del provider, config
40
+ return path in self.request_paths
41
+
42
+ def handle_post(
43
+ self,
44
+ handler: Any,
45
+ config_root: dict[str, Any],
46
+ provider: str,
47
+ config: dict[str, Any],
48
+ path: str,
49
+ body: dict[str, Any],
50
+ ) -> bool:
51
+ del config_root
52
+ if path not in self.request_paths:
53
+ return False
54
+ self._forward(handler, provider, config, body)
55
+ return True
56
+
57
+
58
+ assert all(
59
+ any(capability.name == required for capability in OpenAIChatRouter.capabilities)
60
+ for required in COMMON_RUNTIME_ROUTER_CAPABILITIES
61
+ )
62
+
63
+
64
+ __all__ = ["OpenAIChatRouter"]