Skip to content

Commit e6995ae

Browse files
Trecekclaude
andauthored
Implementation Plan: T5-P4-A7-WP1 Eliminate 'anthropic'.casefold() from _quota_refresh_loop and resolve_provider() from _prime_quota_cache (#4098)
## Summary Replace the hardcoded `'anthropic'.casefold()` string comparison in `_quota_refresh_loop` and the `resolve_provider()` call in `_prime_quota_cache` with a typed `QuotaPolicy` frozen dataclass carrying a `supports_quota_check: bool` field. Call sites derive the bool from `ctx.backend.capabilities.anthropic_provider_capable`, removing all raw provider string passing to quota functions. `resolve_provider()` is deleted from `_misc.py`. Closes #4025 ## Implementation Plan Plan file: `/home/talon/projects/autoskillit-runs/impl-20260612-160904-076305/.autoskillit/temp/make-plan/t5_p4_a7_wp1_quota_policy_plan_2026-06-12_161500.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) via AutoSkillit <!-- autoskillit:pipeline-signature steps=prepare_pr,run_arch_lenses,compose_pr,annotate_pr_diff,review_pr --> ## Token Usage Summary | Step | Model | count | uncached | output | cache_read | peak_ctx | turns | cache_write | time | |------|-------|-------|----------|--------|------------|----------|-------|-------------|------| | plan* | opus[1m] | 1 | 79 | 31.4k | 2.0M | 114.7k | 63 | 105.3k | 19m 25s | | verify* | sonnet | 1 | 52 | 5.7k | 233.5k | 53.6k | 17 | 32.7k | 5m 8s | | implement* | MiniMax-M3 | 1 | 7.3M | 18.0k | 0 | 0 | 179 | 0 | 10m 15s | | fix* | sonnet | 1 | 182 | 8.7k | 1.2M | 72.1k | 54 | 51.2k | 8m 8s | | audit_impl* | sonnet | 1 | 36 | 12.3k | 134.2k | 46.5k | 13 | 40.3k | 6m 42s | | prepare_pr* | MiniMax-M3 | 1 | 200.0k | 2.1k | 0 | 0 | 12 | 0 | 40s | | compose_pr* | MiniMax-M3 | 1 | 215.9k | 1.6k | 0 | 0 | 14 | 0 | 40s | | **Total** | | | 7.7M | 79.9k | 3.6M | 114.7k | | 229.5k | 51m 0s | \* *Step used a non-Anthropic provider; caching behavior may differ.* ## Token Efficiency | Step | LoC Changed | cache_read/LoC | cache_write/LoC | output/LoC | |------|-------------|----------------|-----------------|------------| | plan | 0 | — | — | — | | verify | 0 | — | — | — | | implement | 135 | 0.0 | 0.0 | 133.7 | | fix | 16 | 76366.2 | 3197.8 | 545.6 | | audit_impl | 0 | — | — | — | | prepare_pr | 0 | — | — | — | | compose_pr | 0 | — | — | — | | **Total** | **151** | 23738.0 | 1520.2 | 529.3 | ## Model Usage Breakdown | Model | steps | uncached | output | cache_read | cache_write | time | |-------|-------|----------|--------|------------|-------------|------| | opus[1m] | 1 | 79 | 31.4k | 2.0M | 105.3k | 19m 25s | | sonnet | 3 | 270 | 26.8k | 1.6M | 124.2k | 19m 59s | | MiniMax-M3 | 3 | 7.7M | 21.8k | 0 | 0 | 11m 35s | --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ea68825 commit e6995ae

15 files changed

Lines changed: 104 additions & 45 deletions

src/autoskillit/core/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,7 @@ from .types import ProcessStaleError as ProcessStaleError
304304
from .types import PromptContractError as PromptContractError
305305
from .types import ProviderOutcome as ProviderOutcome
306306
from .types import PRState as PRState
307+
from .types import QuotaPolicy as QuotaPolicy
307308
from .types import QuotaRefreshTask as QuotaRefreshTask
308309
from .types import ReadingToken as ReadingToken
309310
from .types import ReadOnlyResolver as ReadOnlyResolver

src/autoskillit/core/types/_type_protocols_infra.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
from __future__ import annotations
44

5+
from dataclasses import dataclass
56
from pathlib import Path
67
from typing import Any, Protocol, runtime_checkable
78

89
__all__ = [
910
"GateState",
1011
"BackgroundSupervisor",
1112
"FleetLock",
13+
"QuotaPolicy",
1214
"QuotaRefreshTask",
1315
"TokenFactory",
1416
"CampaignProtector",
@@ -104,3 +106,15 @@ class CampaignProtector(Protocol):
104106
"""
105107

106108
def __call__(self, project_dir: Path) -> frozenset[str]: ...
109+
110+
111+
@dataclass(frozen=True, slots=True)
112+
class QuotaPolicy:
113+
"""Typed quota capability derived from backend capabilities.
114+
115+
supports_quota_check is True when the active backend is
116+
Anthropic-provider-capable; False when ctx.backend is None or
117+
non-Anthropic.
118+
"""
119+
120+
supports_quota_check: bool = False

src/autoskillit/server/_guards.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import os
66
from pathlib import Path
7-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, Any
88

99
from autoskillit.core import (
1010
InputContractResolver,
@@ -239,6 +239,11 @@ def _check_input_contracts(
239239
return None
240240

241241

242+
def _backend_supports_quota(ctx: Any) -> bool:
243+
"""Return True when the active backend is Anthropic-provider-capable."""
244+
return ctx.backend is not None and ctx.backend.capabilities.anthropic_provider_capable
245+
246+
242247
def _provider_result(
243248
provider: str,
244249
profiles: dict[str, dict[str, str | None]],

src/autoskillit/server/_lifespan.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
validate_plugin_cache_hooks,
6565
)
6666
from autoskillit.pipeline import create_background_task
67+
from autoskillit.server._guards import _backend_supports_quota
6768
from autoskillit.server._state import _get_ctx_or_none, deferred_initialize
6869

6970
logger = get_logger(__name__)
@@ -253,7 +254,6 @@ async def _fleet_auto_gate_boot(ctx: Any) -> None:
253254
from autoskillit.server._misc import ( # circular-break
254255
_prime_quota_cache,
255256
_quota_refresh_loop,
256-
resolve_provider,
257257
)
258258
from autoskillit.server.tools.tools_kitchen import _write_hook_config # circular-break
259259

@@ -269,16 +269,18 @@ async def _fleet_auto_gate_boot(ctx: Any) -> None:
269269
except Exception:
270270
logger.warning("fleet_auto_gate_boot_write_hook_config_failed", exc_info=True)
271271

272+
_supports_quota = _backend_supports_quota(ctx)
273+
272274
try:
273-
await _prime_quota_cache()
275+
await _prime_quota_cache(supports_quota_check=_supports_quota)
274276
except Exception:
275277
logger.warning("fleet_auto_gate_boot_prime_quota_cache_failed", exc_info=True)
276278

277279
try:
278280
ctx.quota_refresh_task = create_background_task(
279281
_quota_refresh_loop(
280282
ctx.config.quota_guard,
281-
provider=resolve_provider(ctx.config.providers.default_provider),
283+
supports_quota_check=_supports_quota,
282284
),
283285
label="quota_refresh_loop",
284286
)
@@ -341,7 +343,8 @@ async def _pre_reveal_kitchen(ctx: Any) -> None:
341343
_mcp.disable(tags={tag})
342344
register_active_kitchen(ctx.kitchen_id, os.getpid(), str(ctx.project_dir))
343345
_write_hook_config()
344-
await _prime_quota_cache()
346+
_supports_quota = _backend_supports_quota(ctx)
347+
await _prime_quota_cache(supports_quota_check=_supports_quota)
345348
ctx.gate_infrastructure_ready = True
346349

347350

@@ -355,7 +358,6 @@ async def _food_truck_auto_gate_boot(ctx: Any) -> None:
355358
from autoskillit.server._misc import ( # circular-break
356359
_prime_quota_cache,
357360
_quota_refresh_loop,
358-
resolve_provider,
359361
)
360362
from autoskillit.server.tools.tools_kitchen import _write_hook_config # circular-break
361363

@@ -401,8 +403,10 @@ async def _food_truck_auto_gate_boot(ctx: Any) -> None:
401403
except Exception:
402404
logger.warning("food_truck_auto_gate_boot_hook_config_failed", exc_info=True)
403405

406+
_supports_quota = _backend_supports_quota(ctx)
407+
404408
try:
405-
await _prime_quota_cache()
409+
await _prime_quota_cache(supports_quota_check=_supports_quota)
406410
except Exception:
407411
logger.warning("food_truck_auto_gate_boot_quota_cache_failed", exc_info=True)
408412

@@ -411,7 +415,7 @@ async def _food_truck_auto_gate_boot(ctx: Any) -> None:
411415
ctx.quota_refresh_task = create_background_task(
412416
_quota_refresh_loop(
413417
ctx.config.quota_guard,
414-
provider=resolve_provider(ctx.config.providers.default_provider),
418+
supports_quota_check=_supports_quota,
415419
),
416420
label="quota_refresh_loop",
417421
)
@@ -514,7 +518,8 @@ async def _skill_auto_gate_boot(ctx: Any) -> None:
514518
try:
515519
from autoskillit.server._misc import _prime_quota_cache # circular-break
516520

517-
await _prime_quota_cache()
521+
_supports_quota = _backend_supports_quota(ctx)
522+
await _prime_quota_cache(supports_quota_check=_supports_quota)
518523
except Exception:
519524
logger.warning("skill_auto_gate_boot_quota_cache_failed", exc_info=True)
520525

src/autoskillit/server/_misc.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -236,30 +236,27 @@ async def resolve_repo_from_remote(cwd: str, hint: str | None = None) -> str:
236236
return await resolve_remote_repo(cwd, hint=hint) or ""
237237

238238

239-
def resolve_provider(default_provider: str | None) -> str:
240-
"""Return the effective provider name, falling back to ``"anthropic"``."""
241-
return default_provider if default_provider else "anthropic"
242-
243-
244-
async def _prime_quota_cache() -> None:
239+
async def _prime_quota_cache(*, supports_quota_check: bool) -> None:
245240
"""Fetch quota from the Anthropic API and write the local cache.
246241
247242
Called at open_kitchen so the cache is primed before any run_skill hook fires.
248243
Fails open: a quota fetch failure must not abort kitchen open.
249244
"""
245+
if not supports_quota_check:
246+
return
250247
from autoskillit.server._state import _get_ctx as _ctx_fn # circular-break
251248

252249
try:
253250
_ctx = _ctx_fn()
254251
await check_and_sleep_if_needed(
255252
_ctx.config.quota_guard,
256-
provider=resolve_provider(_ctx.config.providers.default_provider),
253+
provider="anthropic",
257254
)
258255
except Exception:
259256
logger.warning("quota_prime_failed", exc_info=True)
260257

261258

262-
async def _quota_refresh_loop(config: QuotaGuardConfig, *, provider: str = "anthropic") -> None:
259+
async def _quota_refresh_loop(config: QuotaGuardConfig, *, supports_quota_check: bool) -> None:
263260
"""Long-running coroutine: refreshes the quota cache every cache_refresh_interval seconds.
264261
265262
Designed to run as a background asyncio.Task for the duration of a kitchen session.
@@ -273,9 +270,10 @@ async def _quota_refresh_loop(config: QuotaGuardConfig, *, provider: str = "anth
273270
274271
Args:
275272
config: QuotaGuardConfig instance.
276-
provider: Provider name. Non-anthropic providers skip the refresh loop entirely.
273+
supports_quota_check: When False, exits immediately — the active backend
274+
does not support Anthropic quota checks.
277275
"""
278-
if provider.casefold() != "anthropic":
276+
if not supports_quota_check:
279277
return
280278

281279
while True:

src/autoskillit/server/tools/tools_fleet_dispatch.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,6 @@ async def dispatch_food_truck(
271271
_refresh_quota_cache,
272272
check_and_sleep_if_needed,
273273
invalidate_cache,
274-
resolve_provider,
275274
)
276275

277276
parsed_checkpoint = (
@@ -357,6 +356,10 @@ async def dispatch_food_truck(
357356
if _preflight_err is not None:
358357
return _preflight_err
359358

359+
_supports_quota = (
360+
tool_ctx.backend is not None
361+
and tool_ctx.backend.capabilities.anthropic_provider_capable
362+
)
360363
result = await execute_dispatch(
361364
tool_ctx=tool_ctx,
362365
recipe=recipe,
@@ -373,7 +376,7 @@ async def dispatch_food_truck(
373376
),
374377
quota_checker=lambda cfg: check_and_sleep_if_needed(
375378
cfg,
376-
provider=resolve_provider(tool_ctx.config.providers.default_provider),
379+
provider="anthropic" if _supports_quota else "",
377380
),
378381
quota_refresher=_refresh_quota_cache,
379382
cache_invalidator=invalidate_cache,

src/autoskillit/server/tools/tools_kitchen.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
from autoskillit.fleet import FleetSemaphore
4343
from autoskillit.pipeline import create_background_task
4444
from autoskillit.server import mcp
45-
from autoskillit.server._guards import _require_orchestrator_exact
45+
from autoskillit.server._guards import _backend_supports_quota, _require_orchestrator_exact
4646
from autoskillit.server._misc import (
4747
_apply_triage_gate,
4848
_build_hook_diagnostic_warning,
@@ -52,7 +52,6 @@
5252
_prime_quota_cache,
5353
_quota_refresh_loop,
5454
resolve_log_dir,
55-
resolve_provider,
5655
strip_ingredients_only_keys,
5756
)
5857
from autoskillit.server._notify import track_response_size
@@ -271,6 +270,7 @@ async def _open_kitchen_handler() -> str | None:
271270
ctx.active_recipe_steps = {}
272271
ctx.active_recipe_ingredients = frozenset()
273272
logger.info("open_kitchen", gate_state="open", kitchen_id=ctx.kitchen_id)
273+
_supports_quota = _backend_supports_quota(ctx)
274274

275275
try:
276276
_write_hook_config()
@@ -280,7 +280,7 @@ async def _open_kitchen_handler() -> str | None:
280280
return _kitchen_failure_envelope(exc, stage="write_hook_config")
281281

282282
try:
283-
await _prime_quota_cache()
283+
await _prime_quota_cache(supports_quota_check=_supports_quota)
284284
except Exception as exc:
285285
ctx.gate.disable()
286286
logger.warning("open_kitchen_failure", stage="prime_quota_cache", exc_info=True)
@@ -292,7 +292,7 @@ async def _open_kitchen_handler() -> str | None:
292292
ctx.quota_refresh_task = create_background_task(
293293
_quota_refresh_loop(
294294
ctx.config.quota_guard,
295-
provider=resolve_provider(ctx.config.providers.default_provider),
295+
supports_quota_check=_supports_quota,
296296
),
297297
label="quota_refresh_loop",
298298
)
@@ -565,11 +565,12 @@ async def open_kitchen(
565565
else:
566566
_ctx_post = _get_ctx()
567567
if _ctx_post.quota_refresh_task is None:
568+
_supports_quota_post = _backend_supports_quota(_ctx_post)
568569
try:
569570
_ctx_post.quota_refresh_task = create_background_task(
570571
_quota_refresh_loop(
571572
_ctx_post.config.quota_guard,
572-
provider=resolve_provider(_ctx_post.config.providers.default_provider),
573+
supports_quota_check=_supports_quota_post,
573574
),
574575
label="quota_refresh_loop",
575576
)

tests/arch/test_quota_capability_isolation.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
"""AST guard: quota modules must not reference BackendCapabilities fields.
22
3-
Quota provider gates must use config-string comparison (resolve_provider),
4-
never backend capability fields. This prevents accidental coupling between
5-
the quota system and the backend capabilities system.
3+
Quota provider gates use a supports_quota_check bool passed by callers,
4+
never backend capability fields directly. This prevents accidental coupling
5+
between the quota system and the backend capabilities system.
66
"""
77

88
from __future__ import annotations
@@ -61,7 +61,7 @@ def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
6161

6262

6363
def test_quota_modules_do_not_reference_backend_capabilities() -> None:
64-
"""Quota code paths must gate on provider string, never BackendCapabilities fields."""
64+
"""Quota code paths must gate on supports_quota_check bool, never BackendCapabilities fields.""" # noqa: E501
6565
from autoskillit.core import paths
6666

6767
src_root = paths.pkg_root()
@@ -81,6 +81,6 @@ def test_quota_modules_do_not_reference_backend_capabilities() -> None:
8181

8282
assert not violations, (
8383
"Quota modules must not reference BackendCapabilities or its fields "
84-
"(use resolve_provider config string instead):\n"
84+
"(quota capability is passed as supports_quota_check bool by callers):\n"
8585
+ "\n".join(f" {v}" for v in sorted(violations))
8686
)

tests/arch/test_subpackage_isolation.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ def test_data_directories_are_not_python_packages() -> None:
962962
"co-located with the execution engine that calls them",
963963
),
964964
"tools_kitchen.py": (
965-
1275,
965+
1280,
966966
"REQ-CNST-010-E7: kitchen tool handlers — open_kitchen and lock_ingredients require "
967967
"inline validation helpers (_check_override_keys, _build_ingredient_key_suggestions) "
968968
"for ingredient key validation; splitting would cross import-layer boundaries; "
@@ -974,7 +974,8 @@ def test_data_directories_are_not_python_packages() -> None:
974974
"quota_refresh_loop deferred start, and all four gate.disable() rollback resets; "
975975
"_dispatch_infeasible_response helper for DOA pipeline refusal"
976976
"; capability admission control dispatch_feasible gating on deferred-recall and "
977-
"get_recipe paths (+22 net lines)",
977+
"get_recipe paths (+22 net lines)"
978+
"; supports_quota_check bool at call sites replaces resolve_provider (+3 net lines)",
978979
),
979980
"tools_execution.py": (
980981
1130,

tests/core/test_type_protocol_shards.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def test_infra_shard_all():
6969
"QuotaRefreshTask",
7070
"TokenFactory",
7171
"CampaignProtector",
72+
"QuotaPolicy",
7273
}
7374

7475

0 commit comments

Comments
 (0)