coderouter-cli 2.16.0__py3-none-any.whl → 2.17.0__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.
@@ -200,6 +200,23 @@ class StreamTruncatedError(AdapterError):
200
200
  self.tool_call_in_flight = tool_call_in_flight
201
201
 
202
202
 
203
+ # v2.17.0: the provider did not produce its first real streamed output within
204
+ # ``ProviderConfig.ttft_timeout_s``. Raised by the engine (not the adapter),
205
+ # before any byte reached the client, so the chain falls back normally and the
206
+ # self-healing guards learn from it like any other retryable failure.
207
+ class TTFTTimeoutError(AdapterError):
208
+ """No first token within ``ttft_timeout_s``."""
209
+
210
+ def __init__(self, *, provider: str, ttft_timeout_s: float) -> None:
211
+ super().__init__(
212
+ f"timeout: no first token within {ttft_timeout_s:g}s (ttft_timeout_s)",
213
+ provider=provider,
214
+ status_code=None,
215
+ retryable=True,
216
+ )
217
+ self.ttft_timeout_s = ttft_timeout_s
218
+
219
+
203
220
  # v0.6-B: per-call overrides resolved from the active profile. The engine
204
221
  # builds one instance per request (since a profile is invariant across its
205
222
  # chain) and threads it through every adapter call on that chain. Adapters
@@ -550,6 +550,28 @@ class ProviderConfig(BaseModel):
550
550
  description="If true, only used when ALLOW_PAID=true (plan.md §2.3).",
551
551
  )
552
552
  timeout_s: float = Field(default=30.0, ge=1.0, le=86400.0)
553
+ # v2.17.0: time-to-first-token cut-off for streaming requests. When the
554
+ # provider has not produced its first real output (text / tool_use /
555
+ # thinking delta) within this many seconds of the attempt starting, the
556
+ # attempt is abandoned and the chain moves on — before any byte reached
557
+ # the client. Never applied to the last provider of a chain (nothing to
558
+ # move on to), nor to non-streaming requests (``timeout_s`` covers
559
+ # those). Before the first *event* it always applies; between the first
560
+ # event and the first real output it needs
561
+ # ``empty_response_action: fallback`` on the profile, which is what
562
+ # withholds those opening events from the client. ``None`` (default)
563
+ # disables it. Typical use: a local llama-server still loading a model
564
+ # or chewing through a long prompt while a cloud fallback could answer.
565
+ ttft_timeout_s: float | None = Field(
566
+ default=None,
567
+ gt=0.0,
568
+ le=3600.0,
569
+ description=(
570
+ "v2.17.0: seconds to wait for the first real streamed output "
571
+ "before falling back to the next provider (not applied to the "
572
+ "last provider in a chain). None disables."
573
+ ),
574
+ )
553
575
 
554
576
  # v2.6 language-tax track: path to a LOCAL ``tokenizer.json`` for this
555
577
  # provider's model, used to measure the CJK over-count vs the char/4
@@ -33,6 +33,13 @@ terminal error 429 429 ``rate_limit_error``
33
33
  anything else 502 ``api_error``
34
34
  ==================================== ====== ========================
35
35
 
36
+ v2.17.0: every mapped response also carries ``x-should-retry`` — the header
37
+ Claude Code reads (with ``retry-after``) to decide whether to spend its retry
38
+ budget. ``false`` for the 4xx rows (resending the same request cannot
39
+ help), ``true`` for 429, and for 502 ``true`` only when at least one
40
+ upstream failure was retryable — a chain that failed purely on, say, 401s
41
+ will fail the same way on every retry.
42
+
36
43
  Upstream 401 / 403 deliberately fall into the last row: they describe
37
44
  CodeRouter's *own* upstream credentials, not the client's, and handing a
38
45
  401 to Claude Code would send the user to ``/login`` for nothing.
@@ -164,17 +171,26 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
164
171
  msg = upstream_message(err)
165
172
  if not msg.lower().startswith(PROMPT_TOO_LONG_PREFIX):
166
173
  msg = f"{PROMPT_TOO_LONG_PREFIX}: {msg}"
167
- return AnthropicErrorResponse(400, "invalid_request_error", msg)
174
+ return AnthropicErrorResponse(
175
+ 400, "invalid_request_error", msg, {"x-should-retry": "false"}
176
+ )
168
177
 
169
178
  terminal = errors[-1] if errors else None
170
179
  if terminal is not None and terminal.status_code is not None:
171
180
  status = terminal.status_code
172
181
  if status in (400, 422) and not terminal.retryable:
173
- return AnthropicErrorResponse(400, "invalid_request_error", upstream_message(terminal))
182
+ return AnthropicErrorResponse(
183
+ 400,
184
+ "invalid_request_error",
185
+ upstream_message(terminal),
186
+ {"x-should-retry": "false"},
187
+ )
174
188
  if status == 413:
175
- return AnthropicErrorResponse(413, "request_too_large", upstream_message(terminal))
189
+ return AnthropicErrorResponse(
190
+ 413, "request_too_large", upstream_message(terminal), {"x-should-retry": "false"}
191
+ )
176
192
  if status == 429:
177
- headers: dict[str, str] = {}
193
+ headers: dict[str, str] = {"x-should-retry": "true"}
178
194
  retry_after = getattr(terminal, "retry_after", None)
179
195
  if retry_after:
180
196
  headers["retry-after"] = retry_after
@@ -182,4 +198,7 @@ def map_no_providers_error(exc: NoProvidersAvailableError) -> AnthropicErrorResp
182
198
  429, "rate_limit_error", upstream_message(terminal), headers
183
199
  )
184
200
 
185
- return AnthropicErrorResponse(502, "api_error", redact(str(exc)))
201
+ retry = any(e.retryable for e in errors)
202
+ return AnthropicErrorResponse(
203
+ 502, "api_error", redact(str(exc)), {"x-should-retry": "true" if retry else "false"}
204
+ )
@@ -276,7 +276,9 @@ async def messages(
276
276
  )
277
277
  if ts_action == "reject":
278
278
  return _anthropic_error_response(
279
- AnthropicErrorResponse(400, "invalid_request_error", REJECT_MESSAGE)
279
+ AnthropicErrorResponse(
280
+ 400, "invalid_request_error", REJECT_MESSAGE, {"x-should-retry": "false"}
281
+ )
280
282
  )
281
283
  if ts_action == "expand":
282
284
  anth_req = expand_tool_search(anth_req)
@@ -290,7 +292,9 @@ async def messages(
290
292
  # v2.16.0: ``context_budget_action: reject`` — Anthropic's own
291
293
  # overflow answer, so Claude Code compacts the conversation.
292
294
  return _anthropic_error_response(
293
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
295
+ AnthropicErrorResponse(
296
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
297
+ )
294
298
  )
295
299
 
296
300
  if anth_req.stream:
@@ -362,7 +366,9 @@ async def messages(
362
366
  # swap) — same answer as the pre-dispatch check above.
363
367
  await _aclose_quietly(source)
364
368
  return _anthropic_error_response(
365
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
369
+ AnthropicErrorResponse(
370
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
371
+ )
366
372
  )
367
373
  except (ToolLoopBreakError, ToolCountExceededError, MidStreamError) as exc:
368
374
  # Policy errors and mid-stream failures keep their existing
@@ -415,7 +421,9 @@ async def messages(
415
421
  except ContextBudgetRejectError as exc:
416
422
  # See the streaming branch: the engine can re-run the guard.
417
423
  return _anthropic_error_response(
418
- AnthropicErrorResponse(400, "invalid_request_error", str(exc))
424
+ AnthropicErrorResponse(
425
+ 400, "invalid_request_error", str(exc), {"x-should-retry": "false"}
426
+ )
419
427
  )
420
428
  except NoProvidersAvailableError as exc:
421
429
  # v2.16.0: Anthropic-shaped error with the upstream's own wording
@@ -47,6 +47,7 @@ from coderouter.adapters.base import (
47
47
  ProviderCallOverrides,
48
48
  StreamChunk,
49
49
  StreamTruncatedError,
50
+ TTFTTimeoutError,
50
51
  )
51
52
  from coderouter.adapters.registry import build_adapter
52
53
  from coderouter.config.schemas import CodeRouterConfig, ProviderConfig
@@ -111,6 +112,7 @@ from coderouter.routing.fallback_trace import (
111
112
  REASON_PAID_GATE,
112
113
  REASON_SELF_HEALING_EXCLUDED,
113
114
  REASON_STREAM_TRUNCATED,
115
+ REASON_TTFT_TIMEOUT,
114
116
  REASON_UNKNOWN_PROVIDER,
115
117
  FallbackTrace,
116
118
  begin_fallback_trace,
@@ -898,6 +900,38 @@ def _emit_cache_observed_streaming(
898
900
  )
899
901
 
900
902
 
903
+ async def _await_within_ttft(
904
+ awaitable: Any,
905
+ *,
906
+ deadline: float | None,
907
+ provider: str,
908
+ ttft_timeout_s: float | None,
909
+ ) -> Any:
910
+ """v2.17.0: await ``awaitable`` but give up at the TTFT ``deadline``.
911
+
912
+ ``deadline`` is a ``time.monotonic()`` instant (``None`` = no limit). On
913
+ expiry raises :class:`TTFTTimeoutError` — a retryable ``AdapterError``, so
914
+ every existing pre-content fallback branch handles it unchanged. The
915
+ cancelled await unwinds the adapter's generator, which releases its
916
+ upstream connection.
917
+ """
918
+ if deadline is None:
919
+ return await awaitable
920
+ remaining = deadline - time.monotonic()
921
+ if remaining <= 0:
922
+ close = getattr(awaitable, "close", None)
923
+ if close is not None:
924
+ close()
925
+ raise TTFTTimeoutError(provider=provider, ttft_timeout_s=ttft_timeout_s or 0.0)
926
+ try:
927
+ async with asyncio.timeout(remaining):
928
+ return await awaitable
929
+ except TimeoutError as exc:
930
+ raise TTFTTimeoutError(
931
+ provider=provider, ttft_timeout_s=ttft_timeout_s or 0.0
932
+ ) from exc
933
+
934
+
901
935
  class NoProvidersAvailableError(CodeRouterError):
902
936
  """Raised when every provider in the chain has failed (or was filtered out)."""
903
937
 
@@ -3354,7 +3388,7 @@ class FallbackEngine:
3354
3388
  # keeps the default byte-for-byte identical to v2.14.0.
3355
3389
  last_empty_stream_truncated = False
3356
3390
 
3357
- for adapter, will_degrade in chain:
3391
+ for chain_idx, (adapter, will_degrade) in enumerate(chain):
3358
3392
  is_native = isinstance(adapter, AnthropicAdapter)
3359
3393
  downgrading = (not is_native) and bool(request.tools)
3360
3394
  effective_request = request
@@ -3414,6 +3448,14 @@ class FallbackEngine:
3414
3448
  # only the non-streaming generate_anthropic recorded, so the
3415
3449
  # busiest path produced no adaptive signal.
3416
3450
  attempt_started = time.monotonic()
3451
+ # v2.17.0: TTFT cut-off. Never on the last provider — there is
3452
+ # nothing left to fall back to, so waiting is the better answer.
3453
+ ttft_s = getattr(adapter.config, "ttft_timeout_s", None)
3454
+ ttft_deadline = (
3455
+ attempt_started + ttft_s
3456
+ if ttft_s is not None and chain_idx < len(chain) - 1
3457
+ else None
3458
+ )
3417
3459
  event_iter: AsyncIterator[AnthropicStreamEvent]
3418
3460
  first: AnthropicStreamEvent
3419
3461
  try:
@@ -3422,11 +3464,21 @@ class FallbackEngine:
3422
3464
  # mypy narrows for stream_anthropic (not on BaseAdapter).
3423
3465
  if isinstance(adapter, AnthropicAdapter):
3424
3466
  event_iter = adapter.stream_anthropic(effective_request, overrides=overrides)
3425
- first = await anext(event_iter)
3467
+ first = await _await_within_ttft(
3468
+ anext(event_iter),
3469
+ deadline=ttft_deadline,
3470
+ provider=adapter.name,
3471
+ ttft_timeout_s=ttft_s,
3472
+ )
3426
3473
  elif downgrading:
3427
3474
  # v0.3-D downgrade: run non-streaming, repair, replay.
3428
3475
  chat_req = to_chat_request(effective_request)
3429
3476
  chat_req.stream = False
3477
+ # v2.17.0: no TTFT deadline here. This branch is a
3478
+ # *non-streaming* call (the tool-turn downgrade), so its
3479
+ # "first output" is the whole answer — a TTFT cut would
3480
+ # become a total-generation limit and throw away slow
3481
+ # but healthy local replies. ``timeout_s`` bounds it.
3430
3482
  chat_resp = await adapter.generate(chat_req, overrides=overrides)
3431
3483
  # H-4: same as the non-streaming branch — keep the
3432
3484
  # CPU-bound repair scan off the event loop.
@@ -3441,7 +3493,12 @@ class FallbackEngine:
3441
3493
  event_iter = stream_chat_to_anthropic_events(
3442
3494
  adapter.stream(chat_req, overrides=overrides)
3443
3495
  )
3444
- first = await anext(event_iter)
3496
+ first = await _await_within_ttft(
3497
+ anext(event_iter),
3498
+ deadline=ttft_deadline,
3499
+ provider=adapter.name,
3500
+ ttft_timeout_s=ttft_s,
3501
+ )
3445
3502
  except StopAsyncIteration:
3446
3503
  # M2: record the empty-stream failure.
3447
3504
  self._adaptive.record_attempt(
@@ -3557,7 +3614,18 @@ class FallbackEngine:
3557
3614
  yield buffered_ev
3558
3615
  buffer = []
3559
3616
  try:
3560
- pending = await iterator.__anext__()
3617
+ if content_started:
3618
+ pending = await iterator.__anext__()
3619
+ else:
3620
+ # v2.17.0: the TTFT deadline also covers the
3621
+ # withheld preamble — first *real* output is
3622
+ # what counts, not the first event.
3623
+ pending = await _await_within_ttft(
3624
+ iterator.__anext__(),
3625
+ deadline=ttft_deadline,
3626
+ provider=adapter.name,
3627
+ ttft_timeout_s=ttft_s,
3628
+ )
3561
3629
  except StopAsyncIteration:
3562
3630
  break
3563
3631
  except AdapterError as exc:
@@ -3609,8 +3677,28 @@ class FallbackEngine:
3609
3677
  # message — so the hop is recorded as ``stream-truncated``
3610
3678
  # and the empty-response log is skipped. The adapter has
3611
3679
  # already emitted ``stream-truncation-detected``.
3612
- truncated = isinstance(exc, StreamTruncatedError)
3613
- if truncated:
3680
+ #
3681
+ # v2.17.0: a TTFT cut-off lands here too, and its buffer
3682
+ # is just as unterminated (opening events, no
3683
+ # ``message_stop``). Treat it like a truncation so the
3684
+ # chain-exhausted flush below never replays it as a
3685
+ # "complete empty answer" — that would hand the client a
3686
+ # 200 stream that never ends. Close the abandoned upstream
3687
+ # stream now rather than waiting for GC.
3688
+ ttft_cut = isinstance(exc, TTFTTimeoutError)
3689
+ truncated = isinstance(exc, StreamTruncatedError) or ttft_cut
3690
+ if ttft_cut:
3691
+ aclose_iter = getattr(event_iter, "aclose", None)
3692
+ if aclose_iter is not None:
3693
+ with contextlib.suppress(Exception):
3694
+ await aclose_iter()
3695
+ trace.record_failure(
3696
+ adapter.name,
3697
+ REASON_TTFT_TIMEOUT,
3698
+ detail=describe_adapter_error(exc),
3699
+ stream=True,
3700
+ )
3701
+ elif truncated:
3614
3702
  trace.record_failure(
3615
3703
  adapter.name,
3616
3704
  REASON_STREAM_TRUNCATED,
@@ -111,6 +111,8 @@ REASON_EMPTY_STREAM: Final = "empty-stream"
111
111
  # ``connection`` (the transport itself broke). Produced only when the profile
112
112
  # sets ``stream_truncation_action: error``.
113
113
  REASON_STREAM_TRUNCATED: Final = "stream-truncated"
114
+ # v2.17.0: no first real output within the provider's ``ttft_timeout_s``.
115
+ REASON_TTFT_TIMEOUT: Final = "ttft-timeout"
114
116
 
115
117
  # -- degrade reasons (pre-existing strings; never produce a hop) -----------
116
118
  # Kept here so the whole ``reason`` vocabulary is readable in one place.
@@ -141,6 +143,7 @@ ATTEMPT_FAILURE_REASONS: Final[frozenset[str]] = frozenset(
141
143
  REASON_EMPTY_RESPONSE,
142
144
  REASON_EMPTY_STREAM,
143
145
  REASON_STREAM_TRUNCATED,
146
+ REASON_TTFT_TIMEOUT,
144
147
  }
145
148
  )
146
149
 
@@ -207,6 +210,8 @@ def describe_adapter_error(exc: AdapterError) -> str:
207
210
  # Not a transport fault — the HTTP body ended cleanly; it was the
208
211
  # LLM protocol inside it that never closed.
209
212
  return "no-terminator"
213
+ if reason == REASON_TTFT_TIMEOUT:
214
+ return "no-first-token"
210
215
  return f"transport={reason}"
211
216
 
212
217
 
@@ -227,10 +232,12 @@ def classify_adapter_error(exc: AdapterError) -> str:
227
232
  ``adapters.base`` is a TYPE_CHECKING-only import at module scope here
228
233
  (the runtime import order would otherwise cycle through the registry).
229
234
  """
230
- from coderouter.adapters.base import StreamTruncatedError
235
+ from coderouter.adapters.base import StreamTruncatedError, TTFTTimeoutError
231
236
 
232
237
  if isinstance(exc, StreamTruncatedError):
233
238
  return REASON_STREAM_TRUNCATED
239
+ if isinstance(exc, TTFTTimeoutError):
240
+ return REASON_TTFT_TIMEOUT
234
241
  status = exc.status_code
235
242
  if status is None:
236
243
  message = str(exc).lower()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderouter-cli
3
- Version: 2.16.0
3
+ Version: 2.17.0
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
@@ -24,14 +24,14 @@ coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcx
24
24
  coderouter/vscode_init.py,sha256=rs3S0FOuPFZhhDvE547Zonhj98al66Jc8G1wvBnFq40,35915
25
25
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
26
26
  coderouter/adapters/anthropic_native.py,sha256=fYM4uXdYYvXfYBgz2dPjdPwv-yKY7XfnXp18YZNCsjg,30185
27
- coderouter/adapters/base.py,sha256=8TxlhYwrLk6hFg3_j84yoJq-55QxDmyfo0BLeTcRMro,14573
27
+ coderouter/adapters/base.py,sha256=Jv_a7d1RtrX_Y6fouL__-DlK4c3cjrYZmkjVKEcqf2w,15297
28
28
  coderouter/adapters/openai_compat.py,sha256=-6jD8RLjlocmnGCkGWm6C0j5fLumadJb9J-wZW4a-GQ,24121
29
29
  coderouter/adapters/registry.py,sha256=XNc1wmFv_oZlqKS0rA693TRDdbq1sZ-OIcofXOVOtq0,3680
30
30
  coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g,274
31
31
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
32
32
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
33
33
  coderouter/config/loader.py,sha256=7FoJXWaO_vN9FAjUStJDwI4aAYcUQQgMJtq5BNy8Pnc,12419
34
- coderouter/config/schemas.py,sha256=pabFnhY070rWhzl9JF9jqyfRGhrD4VSwIC-xWsHGskc,148608
34
+ coderouter/config/schemas.py,sha256=i3j1JCMhZTQm753vUhfmvGcf-k2LRlSYaxLugd3Wv34,149822
35
35
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
36
36
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
37
37
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
@@ -46,8 +46,8 @@ coderouter/guards/memory_pressure.py,sha256=9Xm-b7rv9Td5kPNmAFoL3sNmaOR5zT8NLdn6
46
46
  coderouter/guards/self_healing.py,sha256=XeUlgFWGqMQ5E9YvvzR2CsHRJOAJEyDChqRN0xmZ0vo,16841
47
47
  coderouter/guards/tool_loop.py,sha256=EzeMcmU7BLeTW2jsRVevU81l5rhWcn1oUr7EpzgXjVM,15209
48
48
  coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLug,155
49
- coderouter/ingress/anthropic_errors.py,sha256=SYmcqHeQWuiAsgBjquOemmv_KIr23WTyWtjohuzLOLg,7352
50
- coderouter/ingress/anthropic_routes.py,sha256=O2ejNl8SdXaMt-EArgsPa47NHxzvIRtVOeHMr5kJApM,39963
49
+ coderouter/ingress/anthropic_errors.py,sha256=q8F4LmH57f8Jh2hoV4Mmnvf9sZH32uiXhQXPRBZPjCE,8122
50
+ coderouter/ingress/anthropic_routes.py,sha256=0hJY_i2Zlj93jSGTTfrFETTY-_TRmEsllxWmiS-bSeU,40207
51
51
  coderouter/ingress/app.py,sha256=ZOx7dSvMxaWS4CzXOOEwgzFuycAVgAeWKCN6lFNF-F0,22433
52
52
  coderouter/ingress/dashboard_routes.py,sha256=nPW5_pJe27-hzqCs4sqVVn0BrgXB_7Wcd4qZ6cirvWo,26736
53
53
  coderouter/ingress/launcher_routes.py,sha256=uP_Otp9JE0fydUt1YP-wYcnS8Kj5RPLNIFf88YpfkPM,141535
@@ -67,8 +67,8 @@ coderouter/routing/adaptive.py,sha256=G2o377twGSjbUh65wiIFx6klnpFGjsD_nI3oDvcBwh
67
67
  coderouter/routing/auto_router.py,sha256=y4v0c8u5F9f98Vmhx1vRcKPiOgAvpzbFqr6TIh058h0,13341
68
68
  coderouter/routing/budget.py,sha256=PblmVKJGs_BwNa9uDHAA8hmZ4XIVKv38mHAeU0V3OMs,8451
69
69
  coderouter/routing/capability.py,sha256=rRQhzTgQYFTFDNYcSLKvq3xjdw6B1w7T-3jS7PTMI14,27864
70
- coderouter/routing/fallback.py,sha256=0tN6LPFijZ5cZ-Fc2VpipIVvFHpgCgNCx1COXfenxgs,172687
71
- coderouter/routing/fallback_trace.py,sha256=0BysXY1CZqSvVTuePbgmHbu1AKkHLMX7hsuG6rKlDdE,19787
70
+ coderouter/routing/fallback.py,sha256=vx1Vrl_vy7-dLxkKvipfba_pbhQPbuhRVFjmuMMWyaI,176825
71
+ coderouter/routing/fallback_trace.py,sha256=LwUw56mhVIkP4Gu-fNvZmOjL8hXxY1ci6b1MuGjl5m0,20099
72
72
  coderouter/state/__init__.py,sha256=XoGcPmmBQSiZWML2S0juSveQ78xfhtdeCliNnVyzu7E,1088
73
73
  coderouter/state/audit_log.py,sha256=CEFNJht2WZ8mqesI8CzeuL63NfzFzAZ7wF88auyl6l0,11444
74
74
  coderouter/state/replay.py,sha256=Z_YHKroTKZdrL8qObFxcoLOAQWWXZvXFdLfxzvBhEJg,11230
@@ -80,8 +80,8 @@ coderouter/translation/anthropic.py,sha256=ZQQGS9itN_3o7IKD68lKr5gR_Kem32Jsd7OcG
80
80
  coderouter/translation/convert.py,sha256=VrstDAMsUpcQ1VgMTn_-ETp1OjGX8v0ZGhCkcd4icyU,53037
81
81
  coderouter/translation/tool_repair.py,sha256=1p8rmdyyscGX2g0DDOYitMnLv-nKfR1SLuLaRB4z7GM,64244
82
82
  coderouter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
83
- coderouter_cli-2.16.0.dist-info/METADATA,sha256=qGzoESa_lbm11v64IwRodTt2j1HC7uOd2-hvsXN1ETg,18221
84
- coderouter_cli-2.16.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
85
- coderouter_cli-2.16.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
86
- coderouter_cli-2.16.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
87
- coderouter_cli-2.16.0.dist-info/RECORD,,
83
+ coderouter_cli-2.17.0.dist-info/METADATA,sha256=LcxBbvilLSKMicAtVZLtm1tQSazmg52g_Mj21bTOo8k,18221
84
+ coderouter_cli-2.17.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
85
+ coderouter_cli-2.17.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
86
+ coderouter_cli-2.17.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
87
+ coderouter_cli-2.17.0.dist-info/RECORD,,