coderouter-cli 2.15.1__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.
@@ -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,
@@ -436,6 +438,16 @@ def _apply_context_budget_guard(
436
438
  if profile.context_budget_action == "warn" or not estimate.over_trim_threshold:
437
439
  return request, "warning"
438
440
 
441
+ # v2.16.0: reject → let the client compact instead of trimming here.
442
+ if profile.context_budget_action == "reject":
443
+ from coderouter.guards.context_budget import ContextBudgetRejectError
444
+
445
+ raise ContextBudgetRejectError(
446
+ estimated_tokens=estimate.estimated_tokens,
447
+ max_context_tokens=estimate.max_context_tokens,
448
+ profile=profile.name,
449
+ )
450
+
439
451
  # Over trim threshold + action is trim → trim messages
440
452
  trimmed_request, trim_result = trim_to_budget(
441
453
  request,
@@ -888,6 +900,38 @@ def _emit_cache_observed_streaming(
888
900
  )
889
901
 
890
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
+
891
935
  class NoProvidersAvailableError(CodeRouterError):
892
936
  """Raised when every provider in the chain has failed (or was filtered out)."""
893
937
 
@@ -3344,7 +3388,7 @@ class FallbackEngine:
3344
3388
  # keeps the default byte-for-byte identical to v2.14.0.
3345
3389
  last_empty_stream_truncated = False
3346
3390
 
3347
- for adapter, will_degrade in chain:
3391
+ for chain_idx, (adapter, will_degrade) in enumerate(chain):
3348
3392
  is_native = isinstance(adapter, AnthropicAdapter)
3349
3393
  downgrading = (not is_native) and bool(request.tools)
3350
3394
  effective_request = request
@@ -3404,6 +3448,14 @@ class FallbackEngine:
3404
3448
  # only the non-streaming generate_anthropic recorded, so the
3405
3449
  # busiest path produced no adaptive signal.
3406
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
+ )
3407
3459
  event_iter: AsyncIterator[AnthropicStreamEvent]
3408
3460
  first: AnthropicStreamEvent
3409
3461
  try:
@@ -3412,11 +3464,21 @@ class FallbackEngine:
3412
3464
  # mypy narrows for stream_anthropic (not on BaseAdapter).
3413
3465
  if isinstance(adapter, AnthropicAdapter):
3414
3466
  event_iter = adapter.stream_anthropic(effective_request, overrides=overrides)
3415
- 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
+ )
3416
3473
  elif downgrading:
3417
3474
  # v0.3-D downgrade: run non-streaming, repair, replay.
3418
3475
  chat_req = to_chat_request(effective_request)
3419
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.
3420
3482
  chat_resp = await adapter.generate(chat_req, overrides=overrides)
3421
3483
  # H-4: same as the non-streaming branch — keep the
3422
3484
  # CPU-bound repair scan off the event loop.
@@ -3431,7 +3493,12 @@ class FallbackEngine:
3431
3493
  event_iter = stream_chat_to_anthropic_events(
3432
3494
  adapter.stream(chat_req, overrides=overrides)
3433
3495
  )
3434
- 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
+ )
3435
3502
  except StopAsyncIteration:
3436
3503
  # M2: record the empty-stream failure.
3437
3504
  self._adaptive.record_attempt(
@@ -3547,7 +3614,18 @@ class FallbackEngine:
3547
3614
  yield buffered_ev
3548
3615
  buffer = []
3549
3616
  try:
3550
- 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
+ )
3551
3629
  except StopAsyncIteration:
3552
3630
  break
3553
3631
  except AdapterError as exc:
@@ -3599,8 +3677,28 @@ class FallbackEngine:
3599
3677
  # message — so the hop is recorded as ``stream-truncated``
3600
3678
  # and the empty-response log is skipped. The adapter has
3601
3679
  # already emitted ``stream-truncation-detected``.
3602
- truncated = isinstance(exc, StreamTruncatedError)
3603
- 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:
3604
3702
  trace.record_failure(
3605
3703
  adapter.name,
3606
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()
coderouter/vscode_init.py CHANGED
@@ -112,6 +112,37 @@ _MANAGED_ENV_KEYS: tuple[str, ...] = (
112
112
  "CODEROUTER_MODE",
113
113
  )
114
114
 
115
+ # v2.16.0: Claude Code settings for a gateway that is not api.anthropic.com.
116
+ # Written by default (``--no-claude-code-env`` opts out):
117
+ #
118
+ # * ``ENABLE_TOOL_SEARCH=false`` — 2.1.304 turned tool search on behind a
119
+ # custom ANTHROPIC_BASE_URL. Only api.anthropic.com runs the search; the
120
+ # gateway would answer 400 on every session's first request and Claude
121
+ # Code would retry without it. Saying so up front skips that round trip.
122
+ # * ``CLAUDE_CODE_AUTO_MODE_SERVER=0`` — on gateways auto mode defaults to
123
+ # the server-side permission classifier, which a local backend does not
124
+ # provide; ``0`` keeps the classifier client-side (its requests then go
125
+ # through CodeRouter like any other request).
126
+ # * ``CLAUDE_CODE_MAX_CONTEXT_TOKENS`` — only when ``--max-context-tokens``
127
+ # is given. Since 2.1.285 Claude Code assumes a 1M window behind a
128
+ # gateway for Opus 4.7+ / Sonnet 5+ / Fable ids, so auto-compaction would
129
+ # wait far past what a local model holds.
130
+ CLAUDE_CODE_ENV_KEYS: tuple[str, ...] = (
131
+ "ENABLE_TOOL_SEARCH",
132
+ "CLAUDE_CODE_AUTO_MODE_SERVER",
133
+ "CLAUDE_CODE_MAX_CONTEXT_TOKENS",
134
+ )
135
+
136
+
137
+ def claude_code_env(*, max_context_tokens: int | None = None) -> dict[str, str]:
138
+ """v2.16.0: the Claude Code env vars ``vscode-init`` writes by default."""
139
+ env = {"ENABLE_TOOL_SEARCH": "false", "CLAUDE_CODE_AUTO_MODE_SERVER": "0"}
140
+ if max_context_tokens is not None:
141
+ if max_context_tokens <= 0:
142
+ raise ValueError("max_context_tokens must be positive")
143
+ env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = str(max_context_tokens)
144
+ return env
145
+
115
146
 
116
147
  @dataclass(frozen=True)
117
148
  class FileOutcome:
@@ -155,6 +186,7 @@ def run_vscode_init(
155
186
  with_envrc: bool = False,
156
187
  dry_run: bool = False,
157
188
  force: bool = False,
189
+ extra_env: dict[str, str] | None = None,
158
190
  ) -> VSCodeInitResult:
159
191
  """Scaffold VSCode workspace settings for CodeRouter.
160
192
 
@@ -182,6 +214,9 @@ def run_vscode_init(
182
214
  When true, existing conflicting values are overwritten. Without
183
215
  this flag, a conflict is reported with a diff and the file is
184
216
  left untouched.
217
+ extra_env:
218
+ v2.16.0: additional variables to write next to the base ones
219
+ (the CLI passes :func:`claude_code_env`). Same conflict rules.
185
220
 
186
221
  Raises
187
222
  ------
@@ -205,6 +240,7 @@ def run_vscode_init(
205
240
  profile=profile,
206
241
  force=force,
207
242
  dry_run=dry_run,
243
+ extra_env=extra_env,
208
244
  )
209
245
  )
210
246
 
@@ -217,6 +253,7 @@ def run_vscode_init(
217
253
  profile=profile,
218
254
  force=force,
219
255
  dry_run=dry_run,
256
+ extra_env=extra_env,
220
257
  )
221
258
  )
222
259
 
@@ -235,6 +272,7 @@ def _apply_settings_json(
235
272
  profile: str | None,
236
273
  force: bool,
237
274
  dry_run: bool,
275
+ extra_env: dict[str, str] | None = None,
238
276
  ) -> FileOutcome:
239
277
  desired_env: dict[str, str] = {
240
278
  "ANTHROPIC_BASE_URL": base_url,
@@ -242,6 +280,8 @@ def _apply_settings_json(
242
280
  }
243
281
  if profile:
244
282
  desired_env["CODEROUTER_MODE"] = profile
283
+ if extra_env:
284
+ desired_env.update(extra_env)
245
285
 
246
286
  old_text = ""
247
287
  if path.exists():
@@ -401,6 +441,7 @@ def _apply_envrc(
401
441
  profile: str | None,
402
442
  force: bool,
403
443
  dry_run: bool,
444
+ extra_env: dict[str, str] | None = None,
404
445
  ) -> FileOutcome:
405
446
  """Write (or refresh) the managed block in ``.envrc``.
406
447
 
@@ -413,7 +454,7 @@ def _apply_envrc(
413
454
  which now *appends* the block rather than replacing the file. The
414
455
  operator's lines survive either way.
415
456
  """
416
- block = _render_envrc_block(base_url=base_url, profile=profile)
457
+ block = _render_envrc_block(base_url=base_url, profile=profile, extra_env=extra_env)
417
458
 
418
459
  old_text = ""
419
460
  if path.exists():
@@ -467,7 +508,7 @@ def _apply_envrc(
467
508
  before.rstrip("\n") + ("\n\n" if before.strip() else "")
468
509
  ) + block + ("\n\n" + after.lstrip("\n") if after.strip() else "\n")
469
510
 
470
- conflicts = _unmanaged_conflicts(outside)
511
+ conflicts = _unmanaged_conflicts(outside, extra_keys=tuple(extra_env or ()))
471
512
  if conflicts:
472
513
  # direnv applies exports in order, so a duplicate outside the
473
514
  # fence would silently win or lose depending on placement.
@@ -568,7 +609,9 @@ def _split_managed_block(text: str) -> tuple[str, str] | None:
568
609
  return text[:start], text[end + len(_ENVRC_END) :].lstrip("\n")
569
610
 
570
611
 
571
- def _render_envrc_block(*, base_url: str, profile: str | None) -> str:
612
+ def _render_envrc_block(
613
+ *, base_url: str, profile: str | None, extra_env: dict[str, str] | None = None
614
+ ) -> str:
572
615
  """The fenced block CodeRouter owns, markers included."""
573
616
  lines = [
574
617
  _ENVRC_BEGIN,
@@ -579,18 +622,25 @@ def _render_envrc_block(*, base_url: str, profile: str | None) -> str:
579
622
  ]
580
623
  if profile:
581
624
  lines.append(f'export CODEROUTER_MODE="{profile}"')
625
+ for key, value in (extra_env or {}).items():
626
+ lines.append(f'export {key}="{value}"')
582
627
  lines.append(_ENVRC_END)
583
628
  return "\n".join(lines)
584
629
 
585
630
 
586
- def _unmanaged_conflicts(outside_text: str) -> list[str]:
587
- """Managed variables the operator exports outside our block."""
631
+ def _unmanaged_conflicts(outside_text: str, *, extra_keys: tuple[str, ...] = ()) -> list[str]:
632
+ """Managed variables the operator exports outside our block.
633
+
634
+ v2.16.0: ``extra_keys`` (the Claude Code variables this run writes) are
635
+ checked too — but only when this run writes them, so an operator who
636
+ passed ``--no-claude-code-env`` can keep their own exports.
637
+ """
588
638
  found: list[str] = []
589
639
  for line in outside_text.splitlines():
590
640
  stripped = line.strip()
591
641
  if stripped.startswith("#"):
592
642
  continue
593
- for var in _MANAGED_ENVRC_VARS:
643
+ for var in (*_MANAGED_ENVRC_VARS, *extra_keys):
594
644
  if stripped.startswith((f"export {var}=", f"{var}=")) and var not in found:
595
645
  found.append(var)
596
646
  return found
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: coderouter-cli
3
- Version: 2.15.1
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
@@ -1,11 +1,12 @@
1
1
  coderouter/__init__.py,sha256=ghdjPrLtnRzY8fyQ4CJZI1UJKADyNTLtA3G7se8H7Ns,696
2
2
  coderouter/__main__.py,sha256=-LCgxJnvgUV240HjQKv7ly-mn2NuKHpC4nCpvTHjeSU,130
3
- coderouter/cli.py,sha256=Nbx1ur_HnssFtuM48yPELwhY40veVNnTAfK7OVF2uVc,42400
3
+ coderouter/cli.py,sha256=-QPJ2wTJIpa4TvIFObNaV4hyv_OeDtoLebc692ZWYQA,44775
4
4
  coderouter/cli_stats.py,sha256=CCjzc1G4hTRHZ2gG1XhxhDpUkJnnl3NXbcbp1T18jpg,29894
5
5
  coderouter/cost.py,sha256=32h6uzb4nxh2eA5d2Hn3kD9yJbtis6CFDAbeIy5KRkM,7431
6
6
  coderouter/credentials.py,sha256=R_PdrcA941_jJX_U8hxrjPDTCVVzLd4dsw2c00KwAwE,12991
7
7
  coderouter/doctor.py,sha256=0EUqKaEw2GaqX6zGKjR-bVDdlmBznFIklSz3yIoQbE0,91969
8
8
  coderouter/doctor_apply.py,sha256=tZT3bEd1G6t67YRhf1TGY4NlSvZuJJbnEqN-3AKy8Hc,29972
9
+ coderouter/doctor_claude_code.py,sha256=15ZdyUg0M_wZGEBN9Q3eb_D75lncqxQi1dNV6irWh2U,6621
9
10
  coderouter/env_security.py,sha256=FEBZnXfJ0xE39kmMMn39zk0W_DRRnmcB_REmP9f4xWo,14796
10
11
  coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
11
12
  coderouter/gguf_introspect.py,sha256=Scu0JjUfLHj7T7TL0HQIm1AZcGYrXsjkaMMSN9HkTro,16121
@@ -20,23 +21,23 @@ coderouter/rollback.py,sha256=Bqxx1MALTDLYc0BtGMEMqRyiZIWFoAZJH_RBQw-M5ow,7675
20
21
  coderouter/secret_redaction.py,sha256=Rg71gqKN6jnybZ6UvCvXIvnThI9uRBvmOqC2grSUSUk,23159
21
22
  coderouter/token_estimation.py,sha256=7klQYA4FIg1vtjQGWAhwiBhiLwyz8KTetrLVXFJLGIc,16957
22
23
  coderouter/token_estimation_accurate.py,sha256=GTfzrBVnvAGjeVzmzAeUdOYZvWZKLAxcxPpFiJGlzjk,4609
23
- coderouter/vscode_init.py,sha256=ZEROtETsHjyBkLzZKPnF-zsBpLJx4ph0vmgyty6kNYQ,33433
24
+ coderouter/vscode_init.py,sha256=rs3S0FOuPFZhhDvE547Zonhj98al66Jc8G1wvBnFq40,35915
24
25
  coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
25
- coderouter/adapters/anthropic_native.py,sha256=JxOaU-ymU1K3Tnvhufi_li1qChW69XtUszKJ2DdFlDQ,29936
26
- coderouter/adapters/base.py,sha256=qW0QMbO08Dw2ZDruL9rCvCxA0FXhKnSodlK9g-8Y350,13535
27
- coderouter/adapters/openai_compat.py,sha256=Kl5AACqOHxTPUn8c-4IFu4ug9_E3loLKTm2yYmvalho,23872
26
+ coderouter/adapters/anthropic_native.py,sha256=fYM4uXdYYvXfYBgz2dPjdPwv-yKY7XfnXp18YZNCsjg,30185
27
+ coderouter/adapters/base.py,sha256=Jv_a7d1RtrX_Y6fouL__-DlK4c3cjrYZmkjVKEcqf2w,15297
28
+ coderouter/adapters/openai_compat.py,sha256=-6jD8RLjlocmnGCkGWm6C0j5fLumadJb9J-wZW4a-GQ,24121
28
29
  coderouter/adapters/registry.py,sha256=XNc1wmFv_oZlqKS0rA693TRDdbq1sZ-OIcofXOVOtq0,3680
29
30
  coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g,274
30
31
  coderouter/config/capability_registry.py,sha256=RZgzHF54dXy0CCDocRCz2ee21qXGbbgTSKTkRI9iLL4,16936
31
32
  coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
32
33
  coderouter/config/loader.py,sha256=7FoJXWaO_vN9FAjUStJDwI4aAYcUQQgMJtq5BNy8Pnc,12419
33
- coderouter/config/schemas.py,sha256=0NM5KtVGsjH8BPBeBfgM-ItfOYQKhyI-P_3EbJjIaps,146436
34
+ coderouter/config/schemas.py,sha256=i3j1JCMhZTQm753vUhfmvGcf-k2LRlSYaxLugd3Wv34,149822
34
35
  coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
35
36
  coderouter/data/model-capabilities.yaml,sha256=S9jt6SC6-3s2-icZ_n-a14iEMnc2yB1C2R6q-N_tZWQ,19309
36
37
  coderouter/guards/__init__.py,sha256=5qliYBqygvVPneej7nx0uSjxDKsz7t8VzvrDgVBJlvU,1170
37
38
  coderouter/guards/_fingerprint.py,sha256=qsgNzIq9jv3FHrKL39nGJARp0cMenpN_QmWoJu87vU4,4835
38
39
  coderouter/guards/backend_health.py,sha256=GLUVQ5BhU6i8Cxz_FrgIMs5AZ-YSKAW8veFWgvn9GFI,12565
39
- coderouter/guards/context_budget.py,sha256=gw3MkiHDjpLmQTlTdJT6gRAxYEnoU0xmwSvPZk_miOQ,23059
40
+ coderouter/guards/context_budget.py,sha256=zbplo_1G54kHITdr4kkgUxjQX20Y8MN2Z4XdxETAGwI,23834
40
41
  coderouter/guards/continuous_probe.py,sha256=WIfS-apVMWXGv7bPBxxkJssePa95I4fT0mi4PNqK5iE,12181
41
42
  coderouter/guards/drift_actions.py,sha256=A6pY5CR480Ct5rCVyjlBvjPFVc93eu_r5qcUpK9mWKc,3602
42
43
  coderouter/guards/drift_detection.py,sha256=lW45EZDGRleDUTstw_CHlfUiwLUrlDHX8g7ncuOYNEw,13962
@@ -45,12 +46,14 @@ coderouter/guards/memory_pressure.py,sha256=9Xm-b7rv9Td5kPNmAFoL3sNmaOR5zT8NLdn6
45
46
  coderouter/guards/self_healing.py,sha256=XeUlgFWGqMQ5E9YvvzR2CsHRJOAJEyDChqRN0xmZ0vo,16841
46
47
  coderouter/guards/tool_loop.py,sha256=EzeMcmU7BLeTW2jsRVevU81l5rhWcn1oUr7EpzgXjVM,15209
47
48
  coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLug,155
48
- coderouter/ingress/anthropic_routes.py,sha256=wlOb7ChWHMvuERR0lj8TF4MfPis2ZKegceoak4eREes,31131
49
+ coderouter/ingress/anthropic_errors.py,sha256=q8F4LmH57f8Jh2hoV4Mmnvf9sZH32uiXhQXPRBZPjCE,8122
50
+ coderouter/ingress/anthropic_routes.py,sha256=0hJY_i2Zlj93jSGTTfrFETTY-_TRmEsllxWmiS-bSeU,40207
49
51
  coderouter/ingress/app.py,sha256=ZOx7dSvMxaWS4CzXOOEwgzFuycAVgAeWKCN6lFNF-F0,22433
50
52
  coderouter/ingress/dashboard_routes.py,sha256=nPW5_pJe27-hzqCs4sqVVn0BrgXB_7Wcd4qZ6cirvWo,26736
51
53
  coderouter/ingress/launcher_routes.py,sha256=uP_Otp9JE0fydUt1YP-wYcnS8Kj5RPLNIFf88YpfkPM,141535
52
54
  coderouter/ingress/metrics_routes.py,sha256=4s_f9CY8nVIdQBfXxyUMlN7r5_cXHOaAu7PvNbUuoxg,4609
53
55
  coderouter/ingress/openai_routes.py,sha256=9DHVtGe1mpcUBH3ziDL4RGpEA-LzjyANpBIN3Kfbpxc,14193
56
+ coderouter/ingress/tool_search.py,sha256=JYnJ8SUqPmBfZmsl41TdBQNnux0mwbidKdsr5qn15Hk,3753
54
57
  coderouter/ingress/ui_auth.py,sha256=uCWB5IaMQRNFYdY39FQ5DDVhsoRQJoqimZNSAu_QK_Q,3742
55
58
  coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
56
59
  coderouter/metrics/collector.py,sha256=-744UrUS6_aEY7hbwuvoX0Nn7wuOsQ57IIVr52nPe2M,63268
@@ -64,8 +67,8 @@ coderouter/routing/adaptive.py,sha256=G2o377twGSjbUh65wiIFx6klnpFGjsD_nI3oDvcBwh
64
67
  coderouter/routing/auto_router.py,sha256=y4v0c8u5F9f98Vmhx1vRcKPiOgAvpzbFqr6TIh058h0,13341
65
68
  coderouter/routing/budget.py,sha256=PblmVKJGs_BwNa9uDHAA8hmZ4XIVKv38mHAeU0V3OMs,8451
66
69
  coderouter/routing/capability.py,sha256=rRQhzTgQYFTFDNYcSLKvq3xjdw6B1w7T-3jS7PTMI14,27864
67
- coderouter/routing/fallback.py,sha256=Hzy-_2_zK64GUqGGcOZP-RgOq8wZ4PdBpVu4OBTx8dM,172282
68
- 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
69
72
  coderouter/state/__init__.py,sha256=XoGcPmmBQSiZWML2S0juSveQ78xfhtdeCliNnVyzu7E,1088
70
73
  coderouter/state/audit_log.py,sha256=CEFNJht2WZ8mqesI8CzeuL63NfzFzAZ7wF88auyl6l0,11444
71
74
  coderouter/state/replay.py,sha256=Z_YHKroTKZdrL8qObFxcoLOAQWWXZvXFdLfxzvBhEJg,11230
@@ -77,8 +80,8 @@ coderouter/translation/anthropic.py,sha256=ZQQGS9itN_3o7IKD68lKr5gR_Kem32Jsd7OcG
77
80
  coderouter/translation/convert.py,sha256=VrstDAMsUpcQ1VgMTn_-ETp1OjGX8v0ZGhCkcd4icyU,53037
78
81
  coderouter/translation/tool_repair.py,sha256=1p8rmdyyscGX2g0DDOYitMnLv-nKfR1SLuLaRB4z7GM,64244
79
82
  coderouter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
- coderouter_cli-2.15.1.dist-info/METADATA,sha256=oJXnZHAWbbgbFy7wWTJW4LxBTHkA0NSDMWubhzUqo_0,18221
81
- coderouter_cli-2.15.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
82
- coderouter_cli-2.15.1.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
83
- coderouter_cli-2.15.1.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
84
- coderouter_cli-2.15.1.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,,