coderouter-cli 2.7.0__py3-none-any.whl → 2.7.2__py3-none-any.whl

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.
@@ -86,10 +86,14 @@ from coderouter.routing.adaptive import AdaptiveAdjuster
86
86
  from coderouter.routing.budget import BudgetTracker
87
87
  from coderouter.routing.capability import (
88
88
  anthropic_request_has_cache_control,
89
+ anthropic_request_has_forced_tool_choice,
89
90
  anthropic_request_requires_thinking,
91
+ emulate_tool_choice,
90
92
  log_capability_degraded,
91
93
  provider_supports_cache_control,
92
94
  provider_supports_thinking,
95
+ provider_supports_tool_choice,
96
+ strip_cache_control,
93
97
  strip_thinking,
94
98
  )
95
99
  from coderouter.translation import (
@@ -435,6 +439,104 @@ def _apply_context_budget_guard(
435
439
  return request, "warning"
436
440
 
437
441
 
442
+ # ---------------------------------------------------------------------------
443
+ # S2 / S3 (shim): per-provider tool_choice + cache_control shims
444
+ #
445
+ # Applied at the adapter-call site (same position as strip_thinking) so the
446
+ # mutation is per-provider: a request that falls through to a *capable*
447
+ # provider later in the chain still receives its original tool_choice /
448
+ # cache_control markers. All three actions are opt-in (default ``off``) and
449
+ # read from the resolved profile.
450
+ #
451
+ # The shim is a pure function returning the (possibly-copied) request to
452
+ # send to *this* provider. It never mutates the original request object.
453
+ # ---------------------------------------------------------------------------
454
+
455
+
456
+ def _apply_tool_choice_shim(
457
+ request: AnthropicRequest,
458
+ *,
459
+ adapter: BaseAdapter,
460
+ action: str,
461
+ ) -> AnthropicRequest:
462
+ """S2: warn / emulate a forced tool_choice on a non-supporting provider.
463
+
464
+ Returns the request to hand to this provider. Only acts when the
465
+ request carries a forced ``tool_choice`` (``any`` / ``tool``) AND the
466
+ provider does not support it:
467
+
468
+ * ``warn`` → emit a ``capability-degraded`` log; request unchanged.
469
+ * ``emulate`` → return :func:`emulate_tool_choice` (tool_choice
470
+ stripped, system directive injected) + emit a
471
+ ``tool-choice-emulated`` log.
472
+
473
+ Any other case (action ``off``, non-forced choice, or a capable
474
+ provider) returns ``request`` unchanged.
475
+ """
476
+ if action == "off":
477
+ return request
478
+ if not anthropic_request_has_forced_tool_choice(request):
479
+ return request
480
+ if provider_supports_tool_choice(adapter.config):
481
+ return request
482
+
483
+ if action == "warn":
484
+ log_capability_degraded(
485
+ logger,
486
+ provider=adapter.name,
487
+ dropped=["tool_choice"],
488
+ reason="unsupported-backend",
489
+ )
490
+ return request
491
+
492
+ if action == "emulate":
493
+ choice = request.tool_choice if isinstance(request.tool_choice, dict) else {}
494
+ mode = choice.get("type")
495
+ tool_name = choice.get("name") if mode == "tool" else None
496
+ emulated = emulate_tool_choice(request)
497
+ logger.info(
498
+ "tool-choice-emulated",
499
+ extra={"provider": adapter.name, "tool_name": tool_name, "mode": mode},
500
+ )
501
+ return emulated
502
+
503
+ return request
504
+
505
+
506
+ def _apply_cache_control_shim(
507
+ request: AnthropicRequest,
508
+ *,
509
+ adapter: BaseAdapter,
510
+ action: str,
511
+ request_had_cache_control: bool,
512
+ ) -> AnthropicRequest:
513
+ """S3: strip cache_control markers before a non-supporting provider.
514
+
515
+ Returns the request to hand to this provider. Only acts when the
516
+ profile's ``cache_control_action`` is ``strip``, the request carries
517
+ cache_control markers, and the provider does not support them. Emits a
518
+ ``cache-control-stripped`` log with the marker count. Never emits a
519
+ tokens-saved event (this is a wire-compatibility strip, not a savings).
520
+
521
+ When the strip does not apply, returns ``request`` unchanged — the
522
+ existing v0.5-B ``capability-degraded`` observability log (emitted at
523
+ the call site) is preserved as the ``off`` default behavior.
524
+ """
525
+ if action != "strip":
526
+ return request
527
+ if not request_had_cache_control:
528
+ return request
529
+ if provider_supports_cache_control(adapter.config):
530
+ return request
531
+
532
+ stripped, markers_removed = strip_cache_control(request)
533
+ logger.info(
534
+ "cache-control-stripped",
535
+ extra={"provider": adapter.name, "markers_removed": markers_removed},
536
+ )
537
+ return stripped
538
+
539
+
438
540
  def _emit_cache_observed(
439
541
  response: AnthropicResponse,
440
542
  *,
@@ -1596,6 +1698,25 @@ class FallbackEngine:
1596
1698
  append_system_prompt=profile.append_system_prompt,
1597
1699
  )
1598
1700
 
1701
+ def _resolve_shim_actions(self, profile_name: str | None) -> tuple[str, str]:
1702
+ """S2 / S3 (shim): resolve ``(tool_choice_action, cache_control_action)``.
1703
+
1704
+ Both default to ``off``, so a missing / stub profile (e.g. a
1705
+ ``__new__``-constructed test engine) yields the backward-compatible
1706
+ no-op pair. Resolved once at the top of each Anthropic entry point
1707
+ (profiles are immutable per request) and passed to every adapter
1708
+ call so the per-provider shim helpers stay pure.
1709
+ """
1710
+ chosen = profile_name or self.config.default_profile
1711
+ try:
1712
+ profile = self.config.profile_by_name(chosen)
1713
+ except (KeyError, ValueError):
1714
+ return "off", "off"
1715
+ return (
1716
+ getattr(profile, "tool_choice_action", "off"),
1717
+ getattr(profile, "cache_control_action", "off"),
1718
+ )
1719
+
1599
1720
  def _resolve_chain(self, profile_name: str | None) -> list[BaseAdapter]:
1600
1721
  """Return the list of adapters to try, in order, for this profile.
1601
1722
 
@@ -2306,6 +2427,10 @@ class FallbackEngine:
2306
2427
  # ever asked for caching. Compute once; the v0.5-B gate uses the
2307
2428
  # same value below for the capability-degraded log.
2308
2429
  request_had_cache_control = anthropic_request_has_cache_control(request)
2430
+ # S2 / S3 (shim): resolve the opt-in per-provider actions once.
2431
+ tool_choice_action, cache_control_action = self._resolve_shim_actions(
2432
+ request.profile
2433
+ )
2309
2434
 
2310
2435
  for adapter, will_degrade in chain:
2311
2436
  is_native = isinstance(adapter, AnthropicAdapter)
@@ -2337,6 +2462,19 @@ class FallbackEngine:
2337
2462
  dropped=["cache_control"],
2338
2463
  reason="translation-lossy",
2339
2464
  )
2465
+ # S2 (shim): forced tool_choice warn / emulate on non-supporting
2466
+ # providers. S3 (shim): cache_control strip. Both mutate a
2467
+ # per-provider copy; a later capable provider still gets the
2468
+ # original request.
2469
+ effective_request = _apply_tool_choice_shim(
2470
+ effective_request, adapter=adapter, action=tool_choice_action
2471
+ )
2472
+ effective_request = _apply_cache_control_shim(
2473
+ effective_request,
2474
+ adapter=adapter,
2475
+ action=cache_control_action,
2476
+ request_had_cache_control=request_had_cache_control,
2477
+ )
2340
2478
  logger.info(
2341
2479
  "try-provider",
2342
2480
  extra={
@@ -2534,6 +2672,10 @@ class FallbackEngine:
2534
2672
  # v1.9-A: compute once for the v0.5-B capability-degraded gate
2535
2673
  # AND for the cache-observed emission below.
2536
2674
  request_had_cache_control = anthropic_request_has_cache_control(request)
2675
+ # S2 / S3 (shim): resolve the opt-in per-provider actions once.
2676
+ tool_choice_action, cache_control_action = self._resolve_shim_actions(
2677
+ request.profile
2678
+ )
2537
2679
 
2538
2680
  for adapter, will_degrade in chain:
2539
2681
  is_native = isinstance(adapter, AnthropicAdapter)
@@ -2557,6 +2699,16 @@ class FallbackEngine:
2557
2699
  dropped=["cache_control"],
2558
2700
  reason="translation-lossy",
2559
2701
  )
2702
+ # S2 / S3 (shim): mirror of the non-streaming path.
2703
+ effective_request = _apply_tool_choice_shim(
2704
+ effective_request, adapter=adapter, action=tool_choice_action
2705
+ )
2706
+ effective_request = _apply_cache_control_shim(
2707
+ effective_request,
2708
+ adapter=adapter,
2709
+ action=cache_control_action,
2710
+ request_had_cache_control=request_had_cache_control,
2711
+ )
2560
2712
  logger.info(
2561
2713
  "try-provider",
2562
2714
  extra={