June 7, 2026

TKT-846 refactor lands, plus mobile picker fix

TKT-846 introduces a unified provider API: ProviderApiRequest/ProviderApiResponse DTOs plus a thin ProviderClient ABC per platform (anthropic, openai, gemini, o

TKT-846 introduces a unified provider API: ProviderApiRequest/ProviderApiResponse DTOs plus a thin ProviderClient ABC per platform (anthropic, openai, gemini, ollama) under llm_clients/. Providers becomes a standalone orchestrator owning pre-flight cap checks, telemetry chokepoint (_log_after_call absorbs LoggingLLMService), and token measurement for the compactor. The change deletes AnthropicService, OpenAIService, GeminiService, FallbackLLMService, LoggingLLMService, create_llm_service, _build_service, send_message, build_request_body, LLMResponse, the OVER_CAP sentinel, PayloadTooLargeError, and NonRetryableError — 1223 lines gone from llm_service.py. RequestOverCapError and ResponseOverLimitError replace the OVER_CAP sentinel; MessageProcessor._build_send_dto() is the single DTO construction point.

A rework pass closed the critic loop. The Anthropic 413 catch flips from UNVERIFIED to VERIFIED (estimate_request_tokens is local-heuristic only, so no extra round-trip per ACT pre-flight). Gemini over-limit detection is now structured on exc.code/exc.status (429 → RateLimitError, 400 INVALID_ARGUMENT with token-limit strings → ResponseOverLimitError). DTO telemetry fields (_job_name/_usage_class/_caller) are declared as field(compare=False, repr=False) instead of monkey-patched instance attrs, and _force_send telemetry now fires only on success — restoring pre-refactor behaviour and keeping get_last_chat_request_tokens clean.

P0 dead-code cleanup drops 4 backward-compat delegating wrappers plus the getattr moved-symbol shim from llm_service.py (192 → 133 lines), and vision_service.py loses DOCUMENT_VISION_PROMPT. Tests stop mocking the provider-send layer: test_pattern_match_processor uses a ProviderClient subclass with _inject_fake_client, and a new test_provider_api_contract.py covers DTO/measure/vision contracts. The unit gate reports 1064 passed with no new failures.

Senior architecture review follow-ups add an explicit raise ProviderError in providers._resolve for any ProviderType other than CHAT/VISION (previously a silent fall-through to the CHAT provider), with get_context_limit() inheriting the guard via self._resolve(pt). Stale docstrings in llm_service.py and provider_token_limits.py are scrubbed now that the legacy shims are gone.

docs/04-ARCHITECTURE.md is updated to replace OVER_CAP sentinel, send(force=True), _over_cap(), and the per-platform service class references with the new ProviderApiRequest flow, RequestOverCapError, and llm_clients/ package. Separately, a docs reconciliation corrects policy-API and tool-record drift: policy seed is 234 rows (78/channel) with a {"policies": rows} envelope (no meta key, no POLICY_CATEGORY/POLICY_LABELS), and ability return values are recorded via ActTrail().record(), not the deleted ToolRenderAndRecordService.

TKT-845 fixes mobile image attach: removing capture=“environment” from #imageFileInput restores the standard OS picker with both library and take-photo options, WhatsApp-style.

  • ProviderApiRequest/Response DTOs and thin ProviderClient ABC per platform (anthropic/openai/gemini/ollama) introduced under llm_clients/

  • 1223 lines deleted from llm_service.py; AnthropicService, OpenAIService, GeminiService, FallbackLLMService, LoggingLLMService, create_llm_service, LLMResponse, OVER_CAP sentinel, PayloadTooLargeError, and NonRetryableError all removed

  • Anthropic 413 catch flipped UNVERIFIED→VERIFIED (no extra round-trip); Gemini over-limit uses structured exc.code/exc.status detection (429→RateLimitError, 400+token-limit→ResponseOverLimitError)

  • _resolve() guard now raises ProviderError for non-CHAT/VISION ProviderType instead of silently falling through to CHAT

  • _force_send telemetry fires only on success, restoring pre-refactor behaviour and protecting get_last_chat_request_tokens from 0-token rows

  • Mobile image attach: capture=“environment” removed from #imageFileInput so the picker shows both photo library and camera