Repository navigation
perf(llm): wire response/semantic cache + tiered router + prompt caching into LLMService.chat() #10597
Description
Task 1 — Wire LLM cost machinery into the hot path
Theme: LLM token usage / cost. Highest ROI in the audit — the infrastructure is already built; production LLMService.chat() just bypasses it.
Problem
services/llm_service.py initializes a response/semantic cache (:136) and a TieredModelRouter (:141) but chat() (:250) calls provider.chat_completion() directly — neither cache nor router is consulted. The cache is only used by chat_optimized() (:688), whose only callers are the vLLM path (effectively dead). Anthropic prompt caching (llm_shared/providers/anthropic.py:203) fires only on escalation (complexity_router.py:175), so every direct Claude call forfeits the ~90% input-token discount on large static system prompts. And _TASK_TYPE_DEFAULTS (:12-20) maps max_tokens/temperature per llm_type but never maps a cheaper model.
Subtasks
- 1.1 Wire response + semantic cache into
chat()—services/llm_service.py:250,llm_shared/base_provider.py:61,llm_shared/semantic_cache.py- Step: read existing
chat_optimized()cache usage (:688-700) as the reference pattern - Step: add cache lookup before
provider.chat_completion()and cache-write after, keyed on (model, normalized messages, params) - Step: gate semantic cache behind existing config flag; default conservative similarity threshold
- Step: add cache-hit metric/log
- Step: read existing
- 1.2 Invoke
TieredModelRouter.route()inchat()—services/llm_service.py:141,llm_shared/tiered_routing/complexity_router.py:42-119- Step: call
.route()to pick tier when caller doesn't pin a model - Step: respect explicit caller model override; only route when unset
- Step: emit chosen-tier metric
- Step: call
- 1.3 Enable Anthropic prompt caching by default for static system prefix —
llm_shared/providers/anthropic.py:203-204- Step: set
cache_control: ephemeralon the system block for all calls (not just escalation), behind a config flag defaulting ON - Step: add a second breakpoint for large static tool/context blocks where present
- Step: verify via response
usage.cache_creation_input_tokens/cache_read_input_tokens
- Step: set
- 1.4 Add
llm_type → modelmapping for cheap tasks —services/llm_service.py:12-20,autobot_shared/ssot_config.py:71-82- Step: map
CLASSIFICATION/EXTRACTIONto cheap/local models (gemma2:2b / phi3:mini / mistral:7b per ssot_config) - Criterion corrected 2026-10-03. This step originally also named
ROUTINGandSUMMARIZATION. Neither is a member ofLLMType, which has exactly eight — ORCHESTRATOR, TASK, CHAT, RAG, ANALYSIS, CLASSIFICATION, EXTRACTION, GENERAL (autobot-backend/llm_shared/types.py). The merged two-entry map in_select_chat_model(services/llm_service.py:1086-1089) is therefore complete against the real type set. The original wording is recorded here rather than deleted because measuring delivery against it reports a shortfall that does not exist, which is how it was nearly reported during the [Umbrella] AutoBot Efficiency & Precision — wire built-but-disconnected machinery + runtime fixes #10603 Task 1 tick review. - Step: ensure mapping is overridable per call
- Step: map
Verification
- Unit tests for cache hit/miss and router tier selection
- Manual: identical repeated
chat()returns cached (log shows hit); trivial query routes to cheap tier; Anthropic call shows cache_read tokens > 0 on 2nd call
Notes
Do not delete chat_optimized() — converge its logic into chat() (no _v2 suffixes). Reuse existing get_llm_cache() / SemanticLLMCache.
Activity
mrveiss commentedon Jun 28, 2026
Part of umbrella #10603 (AutoBot Efficiency & Precision).
mrveiss commentedon Jun 28, 2026
Deep-dive design — wiring cost machinery into
LLMService.chat()Grounded in a read of
autobot-backend/services/llm_service.py(chat()at :162, init at :130, optimized path at :688). The infra is all present;chat()simply never calls it.Current state (verified)
__init__buildsself._response_cache = get_llm_cache()(:136) andself._tier_router = TieredModelRouter(TierConfig.from_config())(:141).chat()(:162→:325) builds anLLMRequest(:202), then loops the fallback chain callingprovider.chat_completion(request)(:250). It never readsself._response_cacheand never callsself._tier_router._optimized_check_cache(:973) /_optimized_store_cache(:1012) — wrappers aroundself._response_cache.get(key)/.set(key, CachedResponse)— invoked only bychat_optimized()(:696/:734), whose only path is vLLM (effectively dead; real callers usechat()).get_llm_cache()→async get(cache_key) -> CachedResponse | None,async set(cache_key, CachedResponse)(llm_shared/cache.py:203,272).route(...)exists per strategy (complexity_router.py:42), plusregistry.route(llm_shared/tiered_routing/registry.py:91) androute_with_escalation(complexity_router.py:121).Design — 4 surgical insertions, all reusing existing code
1.1 Cache lookup/store in
chat()— reuse the existing_optimized_check_cache/_optimized_store_cachehelpers (generalize them to take a provider name instead of hardcoded"vllm"):_apply_task_defaults(:200) and before the fallback loop (:219), only when not streaming and the caller hasn't disabled caching:return response), store:_optimized_check_cache(:973) currently bakes in"vllm"; parameterize the provider so bothchat()andchat_optimized()share it. No new cache class — converge, don't duplicate.1.2 Tiered routing — when the caller does not pin
model_name/provider_name, ask the router for a tier before the loop:is None).llm.tierspan attribute + metric for observability.1.3 Anthropic prompt caching by default —
llm_shared/providers/anthropic.py:203only setscache_control: ephemeralwhenmetadata["enable_prompt_cache"]. Change to default-on for the system block behind a config flag (LLM_PROMPT_CACHE_DEFAULT=true); verify viausage.cache_read_input_tokens > 0on the 2nd identical call. Add a second breakpoint on large static tool/context blocks.1.4
llm_type → modelmapping — extend_TASK_TYPE_DEFAULTS(:71) OR add a parallel_TASK_TYPE_MODELmap keyed byLLMType, resolving cheap tasks (CLASSIFICATION/EXTRACTION/ROUTING/SUMMARIZATION) to the cheap models already inautobot_shared/ssot_config.py:71-82(gemma2:2b / phi3:mini / mistral:7b). Applied inchat()only whenmodel_name is Noneand routing (1.2) didn't pin one.Ordering & risk
TieredModelRouter.route()return shape (Decision object vs tuple) — readbase_strategy.py:29+registry.py:91before coding.Why this is safe
No new classes, no
_v2. We connectchat()to the same_response_cache/_tier_routerthe constructor already builds and the optimized path already proves. Streaming (stream(), :327) is deliberately left out of caching (correct — can't cache a stream cheaply); routing could still apply tostream()in a follow-up.perf(llm): wire response cache + default prompt caching into LLMServi…
github-actions commentedon Jun 28, 2026
PR #10619 (merged to
Dev_new_gui) references this issue with a close keyword.If this issue is fully resolved, close it manually. If work remains, no action is needed.
mrveiss commentedon Jun 28, 2026
✅ Closed — implementation + integration verified via PR #10619 (squash 40311b3).
Implemented (active):
LLMService.chat()—chat()←_is_cacheable()+_optimized_check_cache()/_optimized_store_cache()(gated to safely-reusable: low-temp, no tools/structured/thinking;max_tokensin key).config.llm_prompt_cache_default(llm_shared/providers/anthropic.py).Wired, gated OFF pending benchmark → #10621:
llm_type→modelmap:chat()←_select_chat_model(), gated byconfig.chat_tiered_routing.Verification: 9 unit tests (tests/services/test_llm_service_caching.py); flake8/black clean; code-review (2 HIGH issues fixed). New methods all have callers (no orphan modules).
Part of umbrella #10603.
perf(knowledge): set llm_type on cognifier LLM calls + fix entity pro…
mrveiss commentedon Oct 2, 2026
Subtask 1.4's step line is corrected in the body above, with the original wording kept inline rather than deleted.
It named four task types —
CLASSIFICATION,EXTRACTION,ROUTING,SUMMARIZATION.LLMTypehas eight members and neitherROUTINGnorSUMMARIZATIONis among them (autobot-backend/llm_shared/types.py: ORCHESTRATOR, TASK, CHAT, RAG, ANALYSIS, CLASSIFICATION, EXTRACTION, GENERAL). The merged map atservices/llm_service.py:1086-1089has two entries and is complete against the real type set.This matters because the criterion, not the code, was wrong: anyone auditing delivery against the original wording finds two of four and reports a 50% shortfall that does not exist. That nearly happened while reviewing the Task 1 tick on #10603 — the verdict there is that the tick is correct (comment 5962686192).
Only the criterion wording changed. No checkbox, state or scope was touched.