@oneciel-ai/ciel-runtime 0.1.0 → 0.1.1

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.
@@ -0,0 +1,479 @@
1
+ """Claude runtime HTTP router."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ import urllib.error
9
+ from collections.abc import Mapping
10
+ from typing import Any, Callable
11
+
12
+ from .agent_router import COMMON_RUNTIME_ROUTER_CAPABILITIES, RouterCapability
13
+
14
+
15
+ CLAUDE_RUNTIME_DEPENDENCIES: tuple[str, ...] = (
16
+ "EVENT_BUS",
17
+ "OPENCODE_PROVIDER_NAMES",
18
+ "PROVIDER_LABELS",
19
+ "_rebatch_anthropic_sse_text",
20
+ "_update_tool_schema_registry",
21
+ "append_synthetic_tasklist_to_message",
22
+ "apply_provider_request_options",
23
+ "apply_router_rate_limit",
24
+ "begin_pending_channel_delivery",
25
+ "body_with_channel_tool_result_context",
26
+ "body_with_pending_channel_messages",
27
+ "body_without_ciel_runtime_internal_metadata",
28
+ "cap_anthropic_body_for_provider",
29
+ "commit_pending_channel_delivery_cursors",
30
+ "dump_request_for_trace",
31
+ "estimate_tokens",
32
+ "filter_blocked_tools",
33
+ "forward_ollama_api_chat",
34
+ "forward_openai_compatible_chat",
35
+ "is_client_disconnect_error",
36
+ "join_url",
37
+ "key_from_request_headers",
38
+ "mark_pending_channel_delivery_failed",
39
+ "mark_pending_channel_delivery_success",
40
+ "maybe_handle_advisor_request",
41
+ "maybe_handle_channel_clear_request",
42
+ "maybe_handle_import_session_request",
43
+ "maybe_handle_live_api_keys_request",
44
+ "maybe_handle_live_llm_options_request",
45
+ "maybe_handle_plan_mode_tool_choice",
46
+ "maybe_handle_router_debug_request",
47
+ "maybe_handle_version_request",
48
+ "native_anthropic_base_url",
49
+ "ncp_model_id_for_nvidia_hosted",
50
+ "normalize_anthropic_model_request_options",
51
+ "normalize_anthropic_system_role_messages",
52
+ "normalize_request_for_provider_wire",
53
+ "normalize_response_thinking_for_non_anthropic_provider",
54
+ "normalize_thinking_for_non_anthropic_provider",
55
+ "normalize_tool_choice_for_provider",
56
+ "open_provider_request_with_key_retry",
57
+ "opencode_endpoint_kind",
58
+ "prepend_anthropic_text",
59
+ "preserves_anthropic_thinking_contract",
60
+ "provider_headers",
61
+ "provider_native_compat_enabled",
62
+ "provider_openai_router_enabled",
63
+ "provider_request_timeout_seconds",
64
+ "provider_stream_idle_timeout_seconds",
65
+ "provider_upstream_request_base",
66
+ "rate_limit_notice",
67
+ "register_api_key_cooldown",
68
+ "rehydrate_suppressed_thinking_passback",
69
+ "resolve_requested_model",
70
+ "resolve_tool_model_references",
71
+ "router_event_message_preview",
72
+ "router_log",
73
+ "set_upstream_stream_read_timeout",
74
+ "should_normalize_anthropic_stream_tool_use",
75
+ "strip_autonomous_advisor_server_tools",
76
+ "try_write_json",
77
+ "upstream_messages_query",
78
+ "write_context_usage",
79
+ "write_json",
80
+ "write_router_activity",
81
+ )
82
+
83
+
84
+ def missing_claude_runtime_dependencies(deps: Mapping[str, Any]) -> list[str]:
85
+ return [name for name in CLAUDE_RUNTIME_DEPENDENCIES if name not in deps]
86
+
87
+
88
+ def handle_claude_count_tokens_post(
89
+ deps: Mapping[str, Any],
90
+ handler: Any,
91
+ provider: str,
92
+ pcfg: dict[str, Any],
93
+ body: dict[str, Any],
94
+ ) -> None:
95
+ tokens = deps["estimate_tokens"](body)
96
+ deps["write_context_usage"](provider, pcfg, body, "count_tokens")
97
+ deps["write_json"](handler, {"input_tokens": tokens})
98
+
99
+
100
+ def handle_claude_messages_post(
101
+ deps: Mapping[str, Any],
102
+ handler: Any,
103
+ cfg: dict[str, Any],
104
+ provider: str,
105
+ pcfg: dict[str, Any],
106
+ path: str,
107
+ body: dict[str, Any],
108
+ ) -> None:
109
+ _rebatch_anthropic_sse_text = deps["_rebatch_anthropic_sse_text"]
110
+ _update_tool_schema_registry = deps["_update_tool_schema_registry"]
111
+ append_synthetic_tasklist_to_message = deps["append_synthetic_tasklist_to_message"]
112
+ apply_provider_request_options = deps["apply_provider_request_options"]
113
+ apply_router_rate_limit = deps["apply_router_rate_limit"]
114
+ begin_pending_channel_delivery = deps["begin_pending_channel_delivery"]
115
+ body_with_channel_tool_result_context = deps["body_with_channel_tool_result_context"]
116
+ body_with_pending_channel_messages = deps["body_with_pending_channel_messages"]
117
+ body_without_ciel_runtime_internal_metadata = deps["body_without_ciel_runtime_internal_metadata"]
118
+ cap_anthropic_body_for_provider = deps["cap_anthropic_body_for_provider"]
119
+ commit_pending_channel_delivery_cursors = deps["commit_pending_channel_delivery_cursors"]
120
+ dump_request_for_trace = deps["dump_request_for_trace"]
121
+ event_bus = deps["EVENT_BUS"]
122
+ filter_blocked_tools = deps["filter_blocked_tools"]
123
+ forward_ollama_api_chat = deps["forward_ollama_api_chat"]
124
+ forward_openai_compatible_chat = deps["forward_openai_compatible_chat"]
125
+ is_client_disconnect_error = deps["is_client_disconnect_error"]
126
+ join_url = deps["join_url"]
127
+ key_from_request_headers = deps["key_from_request_headers"]
128
+ mark_pending_channel_delivery_failed = deps["mark_pending_channel_delivery_failed"]
129
+ mark_pending_channel_delivery_success = deps["mark_pending_channel_delivery_success"]
130
+ maybe_handle_advisor_request = deps["maybe_handle_advisor_request"]
131
+ maybe_handle_channel_clear_request = deps["maybe_handle_channel_clear_request"]
132
+ maybe_handle_import_session_request = deps["maybe_handle_import_session_request"]
133
+ maybe_handle_live_api_keys_request = deps["maybe_handle_live_api_keys_request"]
134
+ maybe_handle_live_llm_options_request = deps["maybe_handle_live_llm_options_request"]
135
+ maybe_handle_plan_mode_tool_choice = deps["maybe_handle_plan_mode_tool_choice"]
136
+ maybe_handle_router_debug_request = deps["maybe_handle_router_debug_request"]
137
+ maybe_handle_version_request = deps["maybe_handle_version_request"]
138
+ native_anthropic_base_url = deps["native_anthropic_base_url"]
139
+ ncp_model_id_for_nvidia_hosted = deps["ncp_model_id_for_nvidia_hosted"]
140
+ normalize_anthropic_model_request_options = deps["normalize_anthropic_model_request_options"]
141
+ normalize_anthropic_system_role_messages = deps["normalize_anthropic_system_role_messages"]
142
+ normalize_request_for_provider_wire = deps["normalize_request_for_provider_wire"]
143
+ normalize_response_thinking_for_non_anthropic_provider = deps["normalize_response_thinking_for_non_anthropic_provider"]
144
+ normalize_thinking_for_non_anthropic_provider = deps["normalize_thinking_for_non_anthropic_provider"]
145
+ normalize_tool_choice_for_provider = deps["normalize_tool_choice_for_provider"]
146
+ open_provider_request_with_key_retry = deps["open_provider_request_with_key_retry"]
147
+ opencode_endpoint_kind = deps["opencode_endpoint_kind"]
148
+ opencode_provider_names = deps["OPENCODE_PROVIDER_NAMES"]
149
+ prepend_anthropic_text = deps["prepend_anthropic_text"]
150
+ preserves_anthropic_thinking_contract = deps["preserves_anthropic_thinking_contract"]
151
+ provider_headers = deps["provider_headers"]
152
+ provider_labels = deps["PROVIDER_LABELS"]
153
+ provider_native_compat_enabled = deps["provider_native_compat_enabled"]
154
+ provider_openai_router_enabled = deps["provider_openai_router_enabled"]
155
+ provider_request_timeout_seconds = deps["provider_request_timeout_seconds"]
156
+ provider_stream_idle_timeout_seconds = deps["provider_stream_idle_timeout_seconds"]
157
+ provider_upstream_request_base = deps["provider_upstream_request_base"]
158
+ rate_limit_notice = deps["rate_limit_notice"]
159
+ register_api_key_cooldown = deps["register_api_key_cooldown"]
160
+ rehydrate_suppressed_thinking_passback = deps["rehydrate_suppressed_thinking_passback"]
161
+ resolve_requested_model = deps["resolve_requested_model"]
162
+ resolve_tool_model_references = deps["resolve_tool_model_references"]
163
+ router_event_message_preview = deps["router_event_message_preview"]
164
+ router_log = deps["router_log"]
165
+ set_upstream_stream_read_timeout = deps["set_upstream_stream_read_timeout"]
166
+ should_normalize_anthropic_stream_tool_use = deps["should_normalize_anthropic_stream_tool_use"]
167
+ strip_autonomous_advisor_server_tools = deps["strip_autonomous_advisor_server_tools"]
168
+ try_write_json = deps["try_write_json"]
169
+ upstream_messages_query = deps["upstream_messages_query"]
170
+ write_context_usage = deps["write_context_usage"]
171
+ write_json = deps["write_json"]
172
+ write_router_activity = deps["write_router_activity"]
173
+
174
+ self = handler
175
+ _update_tool_schema_registry(body.get("tools"))
176
+ body = normalize_thinking_for_non_anthropic_provider(provider, pcfg, body)
177
+ request_id = f"{os.getpid()}-{time.time_ns()}"
178
+ event_bus.publish(
179
+ level="info",
180
+ category="router.request",
181
+ message="Anthropic messages request received",
182
+ request_id=request_id,
183
+ provider=provider,
184
+ model=str(body.get("model") or ""),
185
+ data={
186
+ "path": path,
187
+ "messages": len(body.get("messages") or []),
188
+ "tools": len(body.get("tools") or []),
189
+ **router_event_message_preview(body, cfg),
190
+ },
191
+ )
192
+ dump_request_for_trace(provider, path, body)
193
+ if maybe_handle_plan_mode_tool_choice(self, provider, pcfg, body):
194
+ event_bus.publish(level="info", category="plan_mode.short_circuit", message="plan mode tool choice handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
195
+ return
196
+ body = filter_blocked_tools(provider, pcfg, body)
197
+ body = normalize_tool_choice_for_provider(provider, pcfg, body)
198
+ write_context_usage(provider, pcfg, body, "messages")
199
+ if maybe_handle_router_debug_request(self, body):
200
+ event_bus.publish(level="info", category="router_debug.short_circuit", message="router debug request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
201
+ return
202
+ if maybe_handle_version_request(self, body):
203
+ event_bus.publish(level="info", category="version.short_circuit", message="version request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
204
+ return
205
+ if maybe_handle_channel_clear_request(self, body):
206
+ event_bus.publish(level="info", category="channel_clear.short_circuit", message="channel backlog clear request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
207
+ return
208
+ if maybe_handle_import_session_request(self, body, client_runtime="claude"):
209
+ event_bus.publish(level="info", category="import_session.short_circuit", message="ImportSession request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
210
+ return
211
+ if maybe_handle_live_llm_options_request(self, body):
212
+ event_bus.publish(level="info", category="llm_options.short_circuit", message="live LLM options request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
213
+ return
214
+ if maybe_handle_live_api_keys_request(self, body):
215
+ event_bus.publish(level="info", category="api_keys.short_circuit", message="live API key request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
216
+ return
217
+ if maybe_handle_advisor_request(self, provider, pcfg, body):
218
+ event_bus.publish(level="info", category="advisor.short_circuit", message="advisor request handled locally", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
219
+ return
220
+ body = strip_autonomous_advisor_server_tools(provider, body)
221
+ body = body_with_pending_channel_messages(body)
222
+ body = body_with_channel_tool_result_context(body)
223
+ begin_pending_channel_delivery(self, body)
224
+ body = normalize_request_for_provider_wire(provider, pcfg, body)
225
+ router_log("DEBUG", f"POST {path} provider={provider} model={body.get('model')} tools={len(body.get('tools') or [])} msgs={len(body.get('messages') or [])}")
226
+ try:
227
+ if provider in ("ollama", "ollama-cloud"):
228
+ event_bus.publish(level="info", category="upstream.request", message="forwarding to Ollama-compatible provider", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
229
+ forward_ollama_api_chat(self, provider, pcfg, body)
230
+ commit_pending_channel_delivery_cursors(body, self)
231
+ return
232
+ if provider in opencode_provider_names:
233
+ upstream_model = resolve_requested_model(provider, pcfg, body.get("model"))
234
+ endpoint_kind = opencode_endpoint_kind(provider, upstream_model, pcfg)
235
+ provider_label = provider_labels.get(provider, provider)
236
+ if endpoint_kind == "openai-chat":
237
+ event_bus.publish(
238
+ level="info",
239
+ category="upstream.request",
240
+ message=f"forwarding to {provider_label} chat-compatible provider",
241
+ request_id=request_id,
242
+ provider=provider,
243
+ model=upstream_model,
244
+ )
245
+ forward_openai_compatible_chat(self, provider, pcfg, body)
246
+ commit_pending_channel_delivery_cursors(body, self)
247
+ return
248
+ if endpoint_kind not in ("anthropic-messages",):
249
+ write_json(
250
+ self,
251
+ {
252
+ "type": "error",
253
+ "error": {
254
+ "type": "unsupported_model_endpoint",
255
+ "message": (
256
+ f"{provider_label} model {upstream_model!r} uses the {endpoint_kind} endpoint family. "
257
+ f"ciel-runtime currently routes {provider_label} /v1/messages and /v1/chat/completions models."
258
+ ),
259
+ },
260
+ },
261
+ 400,
262
+ )
263
+ return
264
+ if provider_openai_router_enabled(provider, pcfg):
265
+ event_bus.publish(level="info", category="upstream.request", message="forwarding to OpenAI-compatible provider", request_id=request_id, provider=provider, model=str(body.get("model") or ""))
266
+ forward_openai_compatible_chat(self, provider, pcfg, body)
267
+ commit_pending_channel_delivery_cursors(body, self)
268
+ return
269
+ body = normalize_thinking_for_non_anthropic_provider(provider, pcfg, body)
270
+ body = normalize_anthropic_system_role_messages(body)
271
+ body = cap_anthropic_body_for_provider(provider, pcfg, body)
272
+ body = apply_provider_request_options(provider, pcfg, body)
273
+ body = rehydrate_suppressed_thinking_passback(provider, pcfg, body)
274
+ upstream_model = resolve_requested_model(provider, pcfg, body.get("model"))
275
+ if provider == "nvidia-hosted":
276
+ upstream_model = ncp_model_id_for_nvidia_hosted(upstream_model)
277
+ body["model"] = upstream_model
278
+ body = resolve_tool_model_references(provider, pcfg, body)
279
+ body = normalize_anthropic_model_request_options(provider, pcfg, body, upstream_model)
280
+ stream_enabled = bool(pcfg.get("stream_enabled", True))
281
+ word_chunking = bool(pcfg.get("stream_word_chunking", False))
282
+ if not stream_enabled:
283
+ body["stream"] = False
284
+ upstream_body = body_without_ciel_runtime_internal_metadata(body)
285
+ base = native_anthropic_base_url(provider, pcfg) if provider_native_compat_enabled(provider, pcfg) else provider_upstream_request_base(provider, pcfg)
286
+ url = join_url(base, "/v1/messages")
287
+ upstream_query = upstream_messages_query(pcfg, self.path, provider)
288
+ if upstream_query:
289
+ url = f"{url}?{upstream_query}"
290
+ headers = provider_headers(provider, pcfg, self.headers)
291
+ for h in ("anthropic-beta", "anthropic-dangerous-direct-browser-access"):
292
+ if self.headers.get(h):
293
+ headers[h] = self.headers[h]
294
+ waited, rpm_used, rpm_limit = apply_router_rate_limit(provider, pcfg, upstream_model)
295
+ try:
296
+ event_bus.publish(level="info", category="upstream.request", message="forwarding to Anthropic-compatible provider", request_id=request_id, provider=provider, model=upstream_model, data={"url": url, "stream": bool(body.get("stream", stream_enabled))})
297
+ resp = open_provider_request_with_key_retry(
298
+ url,
299
+ upstream_body,
300
+ headers,
301
+ provider_request_timeout_seconds(pcfg),
302
+ provider,
303
+ pcfg,
304
+ upstream_model,
305
+ stream=bool(body.get("stream", stream_enabled)),
306
+ )
307
+ if bool(body.get("stream", stream_enabled)):
308
+ set_upstream_stream_read_timeout(resp, provider_stream_idle_timeout_seconds(pcfg))
309
+ status = getattr(resp, "status", 200)
310
+ ctype = resp.headers.get("content-type", "application/json")
311
+ if stream_enabled and "text/event-stream" in ctype:
312
+ self.send_response(status)
313
+ self.send_header("content-type", ctype)
314
+ self.send_header("cache-control", "no-cache")
315
+ self.send_header("connection", "close")
316
+ self.end_headers()
317
+ _rebatch_anthropic_sse_text(
318
+ self,
319
+ resp,
320
+ upstream_model,
321
+ word_chunking=word_chunking,
322
+ source_body=body,
323
+ preserve_thinking=preserves_anthropic_thinking_contract(provider, pcfg),
324
+ normalize_tool_use=should_normalize_anthropic_stream_tool_use(provider, pcfg),
325
+ provider=provider,
326
+ )
327
+ else:
328
+ self.send_response(status)
329
+ self.send_header("content-type", ctype)
330
+ self.end_headers()
331
+ raw_resp = resp.read()
332
+ notice = rate_limit_notice(waited, rpm_used, rpm_limit, bool(pcfg.get("rate_limit_status", False)))
333
+ if "application/json" in ctype:
334
+ try:
335
+ payload = json.loads(raw_resp.decode("utf-8", errors="replace"))
336
+ if isinstance(payload, dict):
337
+ payload = normalize_response_thinking_for_non_anthropic_provider(provider, pcfg, payload, upstream_model)
338
+ payload = append_synthetic_tasklist_to_message(payload, upstream_model, body, "native_json", provider=provider)
339
+ if notice:
340
+ payload = prepend_anthropic_text(payload, notice)
341
+ raw_resp = json.dumps(payload, ensure_ascii=False).encode("utf-8")
342
+ except Exception:
343
+ pass
344
+ self.wfile.write(raw_resp)
345
+ self.wfile.flush()
346
+ mark_pending_channel_delivery_success(self, "anthropic_json")
347
+ commit_pending_channel_delivery_cursors(body, self)
348
+ except urllib.error.HTTPError as e:
349
+ err = e.read()
350
+ if e.code == 429:
351
+ register_api_key_cooldown(provider, pcfg, key_from_request_headers(headers), e.headers)
352
+ event_bus.publish(level="error", category="upstream.error", message=f"upstream HTTP {e.code}", request_id=request_id, provider=provider, model=upstream_model, data={"status": e.code})
353
+ self.send_response(e.code)
354
+ self.send_header("content-type", e.headers.get("content-type", "application/json"))
355
+ self.end_headers()
356
+ self.wfile.write(err)
357
+ except Exception as exc:
358
+ if is_client_disconnect_error(exc):
359
+ mark_pending_channel_delivery_failed(self, f"client_disconnected:{type(exc).__name__}")
360
+ write_router_activity(
361
+ "cancel",
362
+ provider,
363
+ str(body.get("model") or ""),
364
+ error=type(exc).__name__,
365
+ stream=bool(body.get("stream", True)),
366
+ )
367
+ router_log(
368
+ "WARN",
369
+ f"router_client_disconnected provider={provider} model={body.get('model')} error={type(exc).__name__}: {exc}",
370
+ )
371
+ event_bus.publish(
372
+ level="warning",
373
+ category="router.client_disconnected",
374
+ message=f"client disconnected: {type(exc).__name__}",
375
+ request_id=request_id,
376
+ provider=provider,
377
+ model=str(body.get("model") or ""),
378
+ )
379
+ return
380
+ event_bus.publish(level="error", category="router.error", message=str(exc), request_id=request_id, provider=provider, model=str(body.get("model") or ""), data={"error_type": type(exc).__name__})
381
+ try_write_json(self, {"type": "error", "error": {"type": "api_error", "message": str(exc)}}, 500)
382
+
383
+
384
+ class ClaudeRouter:
385
+ name = "claude"
386
+ runtime = "claude-code"
387
+ protocol = "anthropic_messages"
388
+ request_paths = ("/v1/messages", "/v1/messages/count_tokens")
389
+ capabilities = tuple(
390
+ RouterCapability(name, description)
391
+ for name, description in (
392
+ ("auth_forwarding", "Provider/API-key headers are prepared for Anthropic-compatible upstreams."),
393
+ ("sse_stream_proxy", "Anthropic SSE streams are proxied and normalized for Claude Code."),
394
+ ("channel_context_injection", "Pending external channel messages are injected before upstream calls."),
395
+ ("pending_delivery_ack", "Injected channel cursors are committed after successful delivery."),
396
+ ("request_observability", "Requests are traced and published to the runtime event bus."),
397
+ ("upstream_error_mapping", "Upstream HTTP and client disconnect errors are mapped for the runtime."),
398
+ ("token_count", "Claude Code token-count requests are handled locally."),
399
+ ("tool_choice_short_circuit", "Runtime management tool choices can be handled locally."),
400
+ )
401
+ )
402
+
403
+ def __init__(
404
+ self,
405
+ *,
406
+ runtime_deps: Mapping[str, Any] | None = None,
407
+ handle_count_tokens_post: Callable[[Any, str, dict[str, Any], dict[str, Any]], None] | None = None,
408
+ handle_messages_post: Callable[[Any, dict[str, Any], str, dict[str, Any], str, dict[str, Any]], None] | None = None,
409
+ ) -> None:
410
+ if runtime_deps is not None:
411
+ missing = missing_claude_runtime_dependencies(runtime_deps)
412
+ if missing:
413
+ raise KeyError(f"ClaudeRouter runtime_deps missing: {', '.join(missing)}")
414
+ self._handle_count_tokens_post = (
415
+ lambda handler, provider, pcfg, body: handle_claude_count_tokens_post(
416
+ runtime_deps,
417
+ handler,
418
+ provider,
419
+ pcfg,
420
+ body,
421
+ )
422
+ )
423
+ self._handle_messages_post = (
424
+ lambda handler, cfg, provider, pcfg, path, body: handle_claude_messages_post(
425
+ runtime_deps,
426
+ handler,
427
+ cfg,
428
+ provider,
429
+ pcfg,
430
+ path,
431
+ body,
432
+ )
433
+ )
434
+ return
435
+ if handle_count_tokens_post is None or handle_messages_post is None:
436
+ raise TypeError("ClaudeRouter requires runtime_deps or both post handlers")
437
+ self._handle_count_tokens_post = handle_count_tokens_post
438
+ self._handle_messages_post = handle_messages_post
439
+
440
+ def can_handle_get(self, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
441
+ del path, provider, pcfg
442
+ return False
443
+
444
+ def handle_get(self, handler: Any, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
445
+ del handler, path, provider, pcfg
446
+ return False
447
+
448
+ def can_handle_post(self, path: str, provider: str, pcfg: dict[str, Any]) -> bool:
449
+ del provider, pcfg
450
+ return path in self.request_paths
451
+
452
+ def handle_post(
453
+ self,
454
+ handler: Any,
455
+ cfg: dict[str, Any],
456
+ provider: str,
457
+ pcfg: dict[str, Any],
458
+ path: str,
459
+ body: dict[str, Any],
460
+ ) -> bool:
461
+ if path == "/v1/messages/count_tokens":
462
+ self._handle_count_tokens_post(handler, provider, pcfg, body)
463
+ return True
464
+ if path == "/v1/messages":
465
+ self._handle_messages_post(handler, cfg, provider, pcfg, path, body)
466
+ return True
467
+ return False
468
+
469
+
470
+ assert all(any(capability.name == required for capability in ClaudeRouter.capabilities) for required in COMMON_RUNTIME_ROUTER_CAPABILITIES)
471
+
472
+
473
+ __all__ = [
474
+ "CLAUDE_RUNTIME_DEPENDENCIES",
475
+ "ClaudeRouter",
476
+ "handle_claude_count_tokens_post",
477
+ "handle_claude_messages_post",
478
+ "missing_claude_runtime_dependencies",
479
+ ]