coderouter-cli 2.7.2__py3-none-any.whl → 2.7.3__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.
@@ -718,6 +718,57 @@ class FallbackChain(BaseModel):
718
718
  ),
719
719
  )
720
720
 
721
+ # --- ⑧ (empty-response): per-request empty-response fallback ------------
722
+ #
723
+ # Some local backends (observed: gemma4:26b on ``no_tool_temptation``
724
+ # prompts) return a 200 with a structurally-valid but *content-empty*
725
+ # Anthropic response — no tool_use and no non-whitespace text — for a
726
+ # fraction of requests. The drift guard's ``empty_response_rate`` is a
727
+ # windowed aggregate (default threshold 0.3, ``min_window_fill`` 6): it
728
+ # promotes/reloads a backend once the *rate* is bad, but cannot rescue a
729
+ # single blank turn in-flight. This knob adds a per-request in-flight
730
+ # fallback that re-dispatches the *same* request to the next provider in
731
+ # the chain the moment an empty response is detected.
732
+ #
733
+ # Design: "empty" is judged on *content*, not usage.output_tokens (which
734
+ # some backends report unreliably). A response is empty when its content
735
+ # list is empty, or every block is either a whitespace-only ``text`` block
736
+ # or a ``thinking`` block — i.e. nothing the client can act on. A single
737
+ # ``tool_use`` block or one non-whitespace ``text`` block makes it non-empty.
738
+ #
739
+ # * ``off`` — no detection, no fallback, no log. Backward-compatible
740
+ # default (byte-for-byte identical to pre-⑧ behavior).
741
+ # * ``warn`` — detect + emit an ``empty-response-detected`` log line
742
+ # only; the empty response is returned unchanged.
743
+ # * ``fallback`` — on an empty response, log ``empty-response-detected``
744
+ # and continue to the next provider (the empty response
745
+ # is *not* recorded as an error). If every provider in
746
+ # the chain returns empty, the last empty response is
747
+ # returned as-is (a 200 blank is a legitimate answer)
748
+ # with ``chain_exhausted=True`` on the log line. On the
749
+ # streaming path, ``fallback`` buffers events until real
750
+ # content is observed, so an empty stream can be swapped
751
+ # to the next provider without the client seeing bytes.
752
+ #
753
+ # Default ``off`` preserves complete backward compatibility; the original
754
+ # request object is never mutated, so a later provider always receives the
755
+ # untouched request.
756
+ empty_response_action: Literal["off", "warn", "fallback"] = Field(
757
+ default="off",
758
+ description=(
759
+ "⑧ (empty-response): action when a provider returns a 200 with "
760
+ "empty content (no tool_use and no non-whitespace text; a "
761
+ "thinking-only response counts as empty). ``off`` (default) does "
762
+ "nothing. ``warn`` emits an ``empty-response-detected`` log only "
763
+ "and returns the empty response. ``fallback`` re-dispatches the "
764
+ "same request to the next provider (empty is not counted as an "
765
+ "error); if the whole chain returns empty, the last empty response "
766
+ "is returned unchanged. Streaming buffers events until real "
767
+ "content appears so empty streams can be swapped provider-side "
768
+ "without the client seeing any bytes."
769
+ ),
770
+ )
771
+
721
772
  # --- S2 (shim): tool_choice capability gate + emulation -----------------
722
773
  #
723
774
  # Anthropic clients (Claude Code, SDKs) can pin the model to a specific
coderouter/logging.py CHANGED
@@ -1372,6 +1372,46 @@ def log_partial_stitch_surfaced(
1372
1372
  )
1373
1373
 
1374
1374
 
1375
+ # ---------------------------------------------------------------------------
1376
+ # ⑧ (empty-response): per-request empty-response fallback log shape
1377
+ #
1378
+ # Fired when a provider returns a 200 with content-empty output (no
1379
+ # tool_use and no non-whitespace text). The single event lane carries the
1380
+ # ``action`` (warn|fallback) that triggered it, whether the empty response
1381
+ # came from the streaming or non-streaming path, and ``chain_exhausted``
1382
+ # — True only when every provider in the chain returned empty and the last
1383
+ # empty response is being returned to the client as-is.
1384
+ # ---------------------------------------------------------------------------
1385
+
1386
+
1387
+ def log_empty_response_detected(
1388
+ logger: logging.Logger,
1389
+ provider: str,
1390
+ *,
1391
+ action: str,
1392
+ stream: bool,
1393
+ chain_exhausted: bool = False,
1394
+ ) -> None:
1395
+ """Emit an ``empty-response-detected`` warn line.
1396
+
1397
+ Fired when ``empty_response_action`` is ``warn`` or ``fallback`` and a
1398
+ provider returns a structurally-valid but content-empty response. The
1399
+ ``action`` field records which knob value produced the line; ``stream``
1400
+ distinguishes the SSE path from the non-streaming path; ``chain_exhausted``
1401
+ is True only when the whole chain returned empty and the last empty
1402
+ response is being returned unchanged.
1403
+ """
1404
+ logger.warning(
1405
+ "empty-response-detected",
1406
+ extra={
1407
+ "provider": provider,
1408
+ "action": action,
1409
+ "stream": stream,
1410
+ "chain_exhausted": chain_exhausted,
1411
+ },
1412
+ )
1413
+
1414
+
1375
1415
  # ---------------------------------------------------------------------------
1376
1416
  # v2.0-I: Continuous probe log shapes
1377
1417
  #
@@ -52,6 +52,7 @@ Event inventory (dispatch table in :meth:`MetricsCollector._dispatch`)
52
52
  ``drift-promoted`` (v2.0-G) → ``drift_promoted_total``
53
53
  ``drift-reload-attempted`` → ``drift_reload_total`` / success
54
54
  ``partial-stitch-surfaced`` → ``partial_stitch_surfaced_total`` (v2.0-H)
55
+ ``empty-response-detected`` → ``empty_responses_total`` + per-provider (⑧)
55
56
  ``probe-completed`` (v2.0-I) → ``probe_total`` / ``probe_success`` / ``probe_failure``
56
57
  + per-provider latency gauge
57
58
  ``probe-round-completed`` → ``probe_rounds_total`` (v2.0-I)
@@ -109,6 +110,7 @@ _KNOWN_EVENTS: Final[frozenset[str]] = frozenset(
109
110
  "drift-promoted",
110
111
  "drift-reload-attempted",
111
112
  "partial-stitch-surfaced",
113
+ "empty-response-detected",
112
114
  "probe-completed",
113
115
  "probe-round-completed",
114
116
  "probe-capabilities-drift",
@@ -272,6 +274,13 @@ class MetricsCollector(logging.Handler):
272
274
  # client instead of a generic error event.
273
275
  self._partial_stitch_surfaced_total: int = 0
274
276
 
277
+ # ⑧ (empty-response): count of per-request empty-response detections
278
+ # (both ``warn`` and ``fallback`` actions). Aggregate + per-provider
279
+ # so a dashboard can spot which backend blanks out. Counts every
280
+ # detection event, including the terminal chain-exhausted one.
281
+ self._empty_responses_total: int = 0
282
+ self._empty_responses_by_provider: Counter[str] = Counter()
283
+
275
284
  # v2.0-I: continuous probe counters. Per-provider probe attempts
276
285
  # and outcomes, plus a round counter for the dashboard's
277
286
  # "probes/min" panel.
@@ -329,6 +338,7 @@ class MetricsCollector(logging.Handler):
329
338
  "drift-promoted": self._on_drift_promoted,
330
339
  "drift-reload-attempted": self._on_drift_reload_attempted,
331
340
  "partial-stitch-surfaced": self._on_partial_stitch_surfaced,
341
+ "empty-response-detected": self._on_empty_response_detected,
332
342
  "probe-completed": self._on_probe_completed,
333
343
  "probe-round-completed": self._on_probe_round_completed,
334
344
  "probe-capabilities-drift": self._on_probe_capabilities_drift,
@@ -670,6 +680,17 @@ class MetricsCollector(logging.Handler):
670
680
  self._partial_stitch_surfaced_total += 1
671
681
  self._push_recent("partial-stitch-surfaced", extras, record)
672
682
 
683
+ def _on_empty_response_detected(
684
+ self, extras: dict[str, Any], record: logging.LogRecord
685
+ ) -> None:
686
+ # ⑧ (empty-response): a content-empty 200 was detected under a
687
+ # ``warn`` / ``fallback`` action.
688
+ self._empty_responses_total += 1
689
+ provider = _str(extras.get("provider"))
690
+ if provider:
691
+ self._empty_responses_by_provider[provider] += 1
692
+ self._push_recent("empty-response-detected", extras, record)
693
+
673
694
  def _on_probe_completed(
674
695
  self, extras: dict[str, Any], record: logging.LogRecord
675
696
  ) -> None:
@@ -888,6 +909,11 @@ class MetricsCollector(logging.Handler):
888
909
  "drift_reload_success_total": self._drift_reload_success_total,
889
910
  # v2.0-H (L6): partial stitch surfaced.
890
911
  "partial_stitch_surfaced_total": self._partial_stitch_surfaced_total,
912
+ # ⑧ (empty-response): per-request empty-response fallback.
913
+ "empty_responses_total": self._empty_responses_total,
914
+ "empty_responses_by_provider": dict(
915
+ self._empty_responses_by_provider
916
+ ),
891
917
  # v2.0-I: continuous probe counters.
892
918
  "probe_total": dict(self._probe_total),
893
919
  "probe_success": dict(self._probe_success),
@@ -1042,6 +1068,9 @@ class MetricsCollector(logging.Handler):
1042
1068
  self._tokens_saved_by_mechanism.clear()
1043
1069
  # v2.0-H (L6)
1044
1070
  self._partial_stitch_surfaced_total = 0
1071
+ # ⑧ (empty-response)
1072
+ self._empty_responses_total = 0
1073
+ self._empty_responses_by_provider.clear()
1045
1074
  # v2.0-I
1046
1075
  self._probe_total.clear()
1047
1076
  self._probe_success.clear()
@@ -76,6 +76,7 @@ from coderouter.logging import (
76
76
  log_chain_memory_pressure_blocked,
77
77
  log_chain_paid_gate_blocked,
78
78
  log_demote_unhealthy_provider,
79
+ log_empty_response_detected,
79
80
  log_memory_pressure_detected,
80
81
  log_skip_budget_exceeded,
81
82
  log_skip_memory_pressure,
@@ -973,6 +974,82 @@ def _warn_if_uniform_auth_failure(errors: list[AdapterError], *, profile: str) -
973
974
  )
974
975
 
975
976
 
977
+ # ---------------------------------------------------------------------------
978
+ # ⑧ (empty-response): content-based emptiness judgement + stream buffering
979
+ # ---------------------------------------------------------------------------
980
+
981
+
982
+ def _block_field(block: Any, key: str) -> Any:
983
+ """Read ``key`` from a content block whether it is a dict or a model.
984
+
985
+ Anthropic content blocks reach the engine as plain dicts (openai_compat
986
+ translation, most test fakes) or as pydantic-ish objects (native
987
+ passthrough). This mirrors the accessor pattern already used by the
988
+ drift fingerprint at the success-path tail.
989
+ """
990
+ if isinstance(block, dict):
991
+ return block.get(key)
992
+ return getattr(block, key, None)
993
+
994
+
995
+ def _anthropic_response_is_empty(resp: AnthropicResponse) -> bool:
996
+ """Return True when ``resp`` carries no client-actionable content.
997
+
998
+ "Empty" is judged on content, never on ``usage.output_tokens`` (some
999
+ backends report that unreliably). A response is empty when:
1000
+ - its ``content`` list is empty / falsy, or
1001
+ - every block is either a whitespace-only ``text`` block or a
1002
+ ``thinking`` block.
1003
+
1004
+ A single ``tool_use`` block, or one ``text`` block with any
1005
+ non-whitespace character, makes the response non-empty. Unknown block
1006
+ types are treated conservatively as *content* (non-empty) so a novel
1007
+ actionable block is never silently swallowed.
1008
+ """
1009
+ content = getattr(resp, "content", None)
1010
+ if not content:
1011
+ return True
1012
+ for block in content:
1013
+ btype = _block_field(block, "type")
1014
+ if btype == "text":
1015
+ text = _block_field(block, "text") or ""
1016
+ if text.strip():
1017
+ return False
1018
+ # whitespace-only text → keep scanning
1019
+ continue
1020
+ if btype == "thinking":
1021
+ # thinking-only carries nothing the client can act on
1022
+ continue
1023
+ # tool_use or any other (unknown → conservatively non-empty) block
1024
+ return False
1025
+ return True
1026
+
1027
+
1028
+ def _stream_event_is_real_content(event: AnthropicStreamEvent) -> bool:
1029
+ """Return True when ``event`` is the first byte of actionable content.
1030
+
1031
+ Real content is either a ``content_block_start`` opening a ``tool_use``
1032
+ block, or a ``content_block_delta`` carrying a non-whitespace text
1033
+ delta. ``message_start`` / ``ping`` / empty-text openers do not count —
1034
+ they are the buffered preamble that an empty stream also emits.
1035
+ """
1036
+ data = getattr(event, "data", None) or {}
1037
+ etype = data.get("type") if isinstance(data, dict) else None
1038
+ if etype == "content_block_start":
1039
+ block = data.get("content_block") or {}
1040
+ return isinstance(block, dict) and block.get("type") == "tool_use"
1041
+ if etype == "content_block_delta":
1042
+ delta = data.get("delta") or {}
1043
+ if isinstance(delta, dict):
1044
+ # text_delta with non-whitespace text is real content;
1045
+ # input_json_delta (tool args) is real content too.
1046
+ if delta.get("type") == "text_delta":
1047
+ return bool((delta.get("text") or "").strip())
1048
+ if delta.get("type") == "input_json_delta":
1049
+ return True
1050
+ return False
1051
+
1052
+
976
1053
  class FallbackEngine:
977
1054
  """Sequential fallback router — the core of CodeRouter.
978
1055
 
@@ -1717,6 +1794,20 @@ class FallbackEngine:
1717
1794
  getattr(profile, "cache_control_action", "off"),
1718
1795
  )
1719
1796
 
1797
+ def _resolve_empty_response_action(self, profile_name: str | None) -> str:
1798
+ """⑧ (empty-response): resolve ``empty_response_action`` for a profile.
1799
+
1800
+ Defaults to ``off`` so a missing / stub profile (e.g. a
1801
+ ``__new__``-constructed test engine) yields the backward-compatible
1802
+ no-op. Resolved once at the top of each Anthropic entry point.
1803
+ """
1804
+ chosen = profile_name or self.config.default_profile
1805
+ try:
1806
+ profile = self.config.profile_by_name(chosen)
1807
+ except (KeyError, ValueError):
1808
+ return "off"
1809
+ return getattr(profile, "empty_response_action", "off")
1810
+
1720
1811
  def _resolve_chain(self, profile_name: str | None) -> list[BaseAdapter]:
1721
1812
  """Return the list of adapters to try, in order, for this profile.
1722
1813
 
@@ -2431,6 +2522,11 @@ class FallbackEngine:
2431
2522
  tool_choice_action, cache_control_action = self._resolve_shim_actions(
2432
2523
  request.profile
2433
2524
  )
2525
+ # ⑧ (empty-response): resolve the per-request empty-response action
2526
+ # once. ``last_empty_resp`` holds the most recent content-empty 200
2527
+ # under ``fallback`` so a fully-empty chain returns it verbatim.
2528
+ empty_response_action = self._resolve_empty_response_action(request.profile)
2529
+ last_empty_resp: AnthropicResponse | None = None
2434
2530
 
2435
2531
  for adapter, will_degrade in chain:
2436
2532
  is_native = isinstance(adapter, AnthropicAdapter)
@@ -2585,6 +2681,26 @@ class FallbackEngine:
2585
2681
  stream=False,
2586
2682
  response_fingerprint=_fp(_fp_text) if _fp_text else None,
2587
2683
  )
2684
+ # ⑧ (empty-response): per-request empty-response handling. Runs
2685
+ # after the drift observation above (which still gets the real
2686
+ # output_tokens=0 signal for the windowed empty_response_rate)
2687
+ # but before the cache-observed / observer fanout, so a
2688
+ # ``fallback`` swap does not emit a "completed" line for a
2689
+ # response the client never sees.
2690
+ if empty_response_action != "off" and _anthropic_response_is_empty(resp):
2691
+ if empty_response_action == "fallback":
2692
+ # Remember this empty response so a fully-empty chain can
2693
+ # return it verbatim, then try the next provider. Not
2694
+ # recorded in ``errors`` — a 200 blank is not a failure.
2695
+ last_empty_resp = resp
2696
+ log_empty_response_detected(
2697
+ logger, adapter.name, action="fallback", stream=False
2698
+ )
2699
+ continue
2700
+ # ``warn``: log only; fall through and return the empty resp.
2701
+ log_empty_response_detected(
2702
+ logger, adapter.name, action="warn", stream=False
2703
+ )
2588
2704
  # v1.9-A: pair every successful Anthropic response with a
2589
2705
  # cache-observed log line. Native Anthropic / LM Studio
2590
2706
  # /v1/messages report cache_read_input_tokens /
@@ -2625,6 +2741,20 @@ class FallbackEngine:
2625
2741
  )
2626
2742
  return resp
2627
2743
 
2744
+ # ⑧ (empty-response): the chain is exhausted. Under ``fallback``,
2745
+ # if every provider that answered returned an empty 200, return the
2746
+ # last empty response verbatim (a 200 blank is a legitimate answer)
2747
+ # rather than raising — errors is empty in that case anyway.
2748
+ if last_empty_resp is not None:
2749
+ log_empty_response_detected(
2750
+ logger,
2751
+ last_empty_resp.coderouter_provider or "unknown",
2752
+ action="fallback",
2753
+ stream=False,
2754
+ chain_exhausted=True,
2755
+ )
2756
+ return last_empty_resp
2757
+
2628
2758
  profile = request.profile or self.config.default_profile
2629
2759
  _warn_if_uniform_auth_failure(errors, profile=profile)
2630
2760
  raise NoProvidersAvailableError(profile=profile, errors=errors)
@@ -2676,6 +2806,15 @@ class FallbackEngine:
2676
2806
  tool_choice_action, cache_control_action = self._resolve_shim_actions(
2677
2807
  request.profile
2678
2808
  )
2809
+ # ⑧ (empty-response): resolve the per-request empty-response action.
2810
+ # Only ``fallback`` changes the streaming path (it buffers events
2811
+ # until real content appears); ``off`` / ``warn`` stream unchanged.
2812
+ # ``last_empty_stream_buffer`` holds the buffered preamble of the
2813
+ # most recent empty stream so a fully-empty chain can flush it and
2814
+ # terminate normally rather than raising.
2815
+ empty_response_action = self._resolve_empty_response_action(request.profile)
2816
+ empty_fallback = empty_response_action == "fallback"
2817
+ last_empty_stream_buffer: list[AnthropicStreamEvent] | None = None
2679
2818
 
2680
2819
  for adapter, will_degrade in chain:
2681
2820
  is_native = isinstance(adapter, AnthropicAdapter)
@@ -2828,15 +2967,59 @@ class FallbackEngine:
2828
2967
  self._observe_provider_success(
2829
2968
  adapter.name, profile=request.profile
2830
2969
  )
2831
- acc.observe(first)
2832
- yield first
2970
+ # ⑧ (empty-response): under ``fallback`` we withhold the opening
2971
+ # events (message_start / empty content_block_start / ping) from
2972
+ # the client until the *first real content* event is observed.
2973
+ # Because no bytes have reached the client, an empty stream can
2974
+ # still be swapped for the next provider. ``off`` / ``warn`` keep
2975
+ # the legacy immediate-yield behavior (byte-for-byte unchanged).
2976
+ #
2977
+ # ``content_started`` flips True the moment real content is seen
2978
+ # (or immediately, when the action is not ``fallback``); from
2979
+ # then on events pass straight through and the mid-stream guard
2980
+ # is live exactly as before.
2981
+ content_started = not empty_fallback
2982
+ buffer: list[AnthropicStreamEvent] = []
2833
2983
  # Mid-stream guard identical to stream() — any error after the
2834
- # first event is terminal.
2984
+ # first *forwarded* event is terminal.
2835
2985
  try:
2836
- async for ev in event_iter:
2986
+ # Seed the loop with the already-fetched ``first`` event,
2987
+ # then drain the rest of the iterator.
2988
+ pending = first
2989
+ iterator = event_iter.__aiter__()
2990
+ while True:
2991
+ ev = pending
2837
2992
  acc.observe(ev)
2838
- yield ev
2993
+ if content_started:
2994
+ yield ev
2995
+ else:
2996
+ buffer.append(ev)
2997
+ if _stream_event_is_real_content(ev):
2998
+ # Real content arrived — flush the withheld
2999
+ # preamble in order, then switch to passthrough.
3000
+ content_started = True
3001
+ for buffered_ev in buffer:
3002
+ yield buffered_ev
3003
+ buffer = []
3004
+ try:
3005
+ pending = await iterator.__anext__()
3006
+ except StopAsyncIteration:
3007
+ break
2839
3008
  except AdapterError as exc:
3009
+ if not content_started:
3010
+ # Empty-stream failure before any real content under
3011
+ # ``fallback``: nothing reached the client, so treat it
3012
+ # like an empty response and try the next provider. Do
3013
+ # NOT raise MidStreamError (that would surface to the
3014
+ # client). Record the error-only observation for adaptive.
3015
+ self._adaptive.record_attempt(
3016
+ adapter.name, latency_ms=None, success=False
3017
+ )
3018
+ log_empty_response_detected(
3019
+ logger, adapter.name, action="fallback", stream=True
3020
+ )
3021
+ last_empty_stream_buffer = buffer
3022
+ continue
2840
3023
  # M2: mid-stream failure — record an error-only
2841
3024
  # observation (no latency; first-event success already
2842
3025
  # recorded) so adaptive error rate reflects the breakage.
@@ -2868,6 +3051,17 @@ class FallbackEngine:
2868
3051
  raise MidStreamError(
2869
3052
  adapter.name, exc, partial_content=acc.partial_content
2870
3053
  ) from exc
3054
+ # ⑧ (empty-response): the stream ended cleanly but no real
3055
+ # content was ever observed under ``fallback`` — the preamble is
3056
+ # still buffered (never sent). Treat as an empty response: keep
3057
+ # the buffer for a possible chain-exhausted flush and try the
3058
+ # next provider.
3059
+ if empty_fallback and not content_started:
3060
+ log_empty_response_detected(
3061
+ logger, adapter.name, action="fallback", stream=True
3062
+ )
3063
+ last_empty_stream_buffer = buffer
3064
+ continue
2871
3065
  # v2.0-G (L4): drift detection observation (stream success).
2872
3066
  # P1-4: compute response fingerprint for goal_progress_stall.
2873
3067
  _stream_fp_text = " ".join(
@@ -2923,6 +3117,23 @@ class FallbackEngine:
2923
3117
  )
2924
3118
  return
2925
3119
 
3120
+ # ⑧ (empty-response): the chain is exhausted under ``fallback`` and
3121
+ # every provider produced an empty stream. Flush the last buffered
3122
+ # (empty) preamble so the client gets a well-formed, terminating SSE
3123
+ # sequence (message_start … message_stop) instead of an error — a
3124
+ # 200 blank is a legitimate answer. errors is empty in that case.
3125
+ if last_empty_stream_buffer is not None:
3126
+ log_empty_response_detected(
3127
+ logger,
3128
+ "unknown",
3129
+ action="fallback",
3130
+ stream=True,
3131
+ chain_exhausted=True,
3132
+ )
3133
+ for buffered_ev in last_empty_stream_buffer:
3134
+ yield buffered_ev
3135
+ return
3136
+
2926
3137
  profile = request.profile or self.config.default_profile
2927
3138
  _warn_if_uniform_auth_failure(errors, profile=profile)
2928
3139
  raise NoProvidersAvailableError(profile=profile, errors=errors)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coderouter-cli
3
- Version: 2.7.2
3
+ Version: 2.7.3
4
4
  Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
5
5
  Project-URL: Homepage, https://github.com/zephel01/CodeRouter
6
6
  Project-URL: Repository, https://github.com/zephel01/CodeRouter
@@ -10,7 +10,7 @@ coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
10
10
  coderouter/gguf_introspect.py,sha256=FZO14STLSp94Rfo5AInGwYUOpfjiXOW6CH5RiczTWDE,9514
11
11
  coderouter/hardware.py,sha256=gn3_9qbVcGRR81yKMn1lJE_8-YDRau0LxIH_M-f7pxE,8356
12
12
  coderouter/language_tax.py,sha256=LTbE3tIfoJuV2O3T0NixRKhzq_dEOTUuPEerJv2q9uk,9360
13
- coderouter/logging.py,sha256=ZHEgkWG1J7VkgL6pNzGJHRXRq_LB_Hcbl0VOG0phke8,54609
13
+ coderouter/logging.py,sha256=91cveN8SCJEMNz9DGi6TrFSlc8cx0wdUkOdSLWLA-z8,56144
14
14
  coderouter/output_filters.py,sha256=0ry_rPiS_kC-FnHgaNVP6v7e6Al2djxzu9vBzZ8kEkE,25314
15
15
  coderouter/token_estimation.py,sha256=iz22vZEEW2P7uKLB2pYvPNpIbZGbgXRO5MtfkS_-9Sk,7531
16
16
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
@@ -23,7 +23,7 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
23
23
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
24
24
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
25
25
  coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
26
- coderouter/config/schemas.py,sha256=JmoUl5gCRucib940I3oFVMp8Om9Cz_C5iQAPeuJ32rc,74490
26
+ coderouter/config/schemas.py,sha256=f46bpmSzZ_zJRhHbo9-inp-mAW86WZAg6hfSxgPaEcc,77741
27
27
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
28
28
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
29
29
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -45,7 +45,7 @@ coderouter/ingress/launcher_routes.py,sha256=1bU245mEauCMpEOsq0LY-8FKKQIJWJhDe3C
45
45
  coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
46
46
  coderouter/ingress/openai_routes.py,sha256=Mjsk3jwot4qP9f5xshrmR1iuHMW-9sQdsfHMN_Sx5hc,9294
47
47
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
48
- coderouter/metrics/collector.py,sha256=_Zp1wUOZhLA9afEdCwlxdV5QP4Z5sCjm1ya8jL0xkhI,59708
48
+ coderouter/metrics/collector.py,sha256=wdGuX5yOBWEHCz1KVIfaUBZP5NXW3U34yyYlLGwgoCE,61212
49
49
  coderouter/metrics/prometheus.py,sha256=THYkvrcpQK3ZNW-BV0h3O-PoT2ppNZ0dapQuu1nyHwE,24410
50
50
  coderouter/plugins/__init__.py,sha256=76hMLe5dV_ilripHXzWn3HSYoIALjzlw4EJVyI-GyIM,1974
51
51
  coderouter/plugins/base.py,sha256=n9hsck2NCSqi6oeHIumKC5zhQ8JGwCXUz7J5AZQCQss,5772
@@ -56,7 +56,7 @@ coderouter/routing/adaptive.py,sha256=G2o377twGSjbUh65wiIFx6klnpFGjsD_nI3oDvcBwh
56
56
  coderouter/routing/auto_router.py,sha256=y4v0c8u5F9f98Vmhx1vRcKPiOgAvpzbFqr6TIh058h0,13341
57
57
  coderouter/routing/budget.py,sha256=PblmVKJGs_BwNa9uDHAA8hmZ4XIVKv38mHAeU0V3OMs,8451
58
58
  coderouter/routing/capability.py,sha256=rRQhzTgQYFTFDNYcSLKvq3xjdw6B1w7T-3jS7PTMI14,27864
59
- coderouter/routing/fallback.py,sha256=iuCCyIT0ARCI8r6iLQPaMYwt27WhwC1bPSiqE-OFCSM,129102
59
+ coderouter/routing/fallback.py,sha256=vjnDv3z88v76ZnF2Sg1xYUOkKfe0jZm14OtoOrQA7Dc,139707
60
60
  coderouter/state/__init__.py,sha256=XoGcPmmBQSiZWML2S0juSveQ78xfhtdeCliNnVyzu7E,1088
61
61
  coderouter/state/audit_log.py,sha256=n7vuDsTfd5iuNUJhlRQPakJ2tMSsT9EfgPCzs1GEaac,10941
62
62
  coderouter/state/replay.py,sha256=Z_YHKroTKZdrL8qObFxcoLOAQWWXZvXFdLfxzvBhEJg,11230
@@ -67,8 +67,8 @@ coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8I
67
67
  coderouter/translation/anthropic.py,sha256=aZkcYH4x82b0x7efJgJb9RWn9Hbyc9pEOthXe4vjUdU,11113
68
68
  coderouter/translation/convert.py,sha256=VV4kpu4dvLmGBmJfQnCLY5ryy_AnQcV2NuwjqRtaR8Q,52633
69
69
  coderouter/translation/tool_repair.py,sha256=DGOKArM53yfVbIfiAveoul1vGq9NEKmjagf43Qh4IkM,57630
70
- coderouter_cli-2.7.2.dist-info/METADATA,sha256=z-Kd6V1JathVCNb3Gg2UzvZmpXbBmCUahQ2XrjmbdZ0,14857
71
- coderouter_cli-2.7.2.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
72
- coderouter_cli-2.7.2.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
73
- coderouter_cli-2.7.2.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
74
- coderouter_cli-2.7.2.dist-info/RECORD,,
70
+ coderouter_cli-2.7.3.dist-info/METADATA,sha256=EqYCtqhIooSQ806Ps89T9XIxVKRhWHiyyTyJsMpu_Lg,14857
71
+ coderouter_cli-2.7.3.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
72
+ coderouter_cli-2.7.3.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
73
+ coderouter_cli-2.7.3.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
74
+ coderouter_cli-2.7.3.dist-info/RECORD,,