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.
- coderouter/adapters/anthropic_native.py +4 -0
- coderouter/adapters/base.py +37 -0
- coderouter/adapters/openai_compat.py +4 -0
- coderouter/cli.py +67 -2
- coderouter/config/schemas.py +58 -2
- coderouter/doctor_claude_code.py +181 -0
- coderouter/guards/context_budget.py +19 -0
- coderouter/ingress/anthropic_errors.py +204 -0
- coderouter/ingress/anthropic_routes.py +221 -8
- coderouter/ingress/tool_search.py +100 -0
- coderouter/routing/fallback.py +104 -6
- coderouter/routing/fallback_trace.py +8 -1
- coderouter/vscode_init.py +56 -6
- {coderouter_cli-2.15.1.dist-info → coderouter_cli-2.17.0.dist-info}/METADATA +1 -1
- {coderouter_cli-2.15.1.dist-info → coderouter_cli-2.17.0.dist-info}/RECORD +18 -15
- {coderouter_cli-2.15.1.dist-info → coderouter_cli-2.17.0.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.15.1.dist-info → coderouter_cli-2.17.0.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.15.1.dist-info → coderouter_cli-2.17.0.dist-info}/licenses/LICENSE +0 -0
coderouter/routing/fallback.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3603
|
-
|
|
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(
|
|
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.
|
|
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
|
|
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=
|
|
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=
|
|
26
|
-
coderouter/adapters/base.py,sha256=
|
|
27
|
-
coderouter/adapters/openai_compat.py,sha256
|
|
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=
|
|
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=
|
|
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/
|
|
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=
|
|
68
|
-
coderouter/routing/fallback_trace.py,sha256=
|
|
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.
|
|
81
|
-
coderouter_cli-2.
|
|
82
|
-
coderouter_cli-2.
|
|
83
|
-
coderouter_cli-2.
|
|
84
|
-
coderouter_cli-2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|