perf(voice): prompt-caching для Claude + отсев мёртвых сущностей
All checks were successful
Deploy / deploy (push) Successful in 1m28s

- claudeRequest: system разбит на кэшируемый базовый промпт (cache_control
  ephemeral) + волатильный живой контекст отдельным блоком. Схемы инструментов
  попадают в кэшируемый префикс (render order tools->system) и читаются за ~10%.
- buildLiveContext: пропускаем entity в состоянии unavailable/unknown — меньше
  токенов и шума в снимке.
- Цель: ~20k input/запрос на Haiku давал ~$0.02+/запрос; кэш стабильной части
  (инструменты+база) режет стоимость в разы, ответы не меняются.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UYsVrPyJ5phJeVsAGciS1v
This commit is contained in:
Cosmo
2026-07-22 21:02:15 +00:00
parent d71320a5a9
commit 83f07fa29e

View File

@@ -98,7 +98,9 @@ function historyToAnthropicMessages(history: HistoryMessage[]): any[] {
return result return result
} }
async function claudeRequest(system: string, messages: any[], tools: any[]): Promise<any> { async function claudeRequest(baseSys: string, liveCtx: string, messages: any[], tools: any[]): Promise<any> {
const system: any[] = [{ type: 'text', text: baseSys, cache_control: { type: 'ephemeral' } }]
if (liveCtx) system.push({ type: 'text', text: liveCtx })
const body: any = { model: 'claude-haiku-4-5-20251001', max_tokens: MAX_TOKENS, system, messages } const body: any = { model: 'claude-haiku-4-5-20251001', max_tokens: MAX_TOKENS, system, messages }
if (tools.length > 0) body.tools = tools if (tools.length > 0) body.tools = tools
const opts: any = { const opts: any = {
@@ -137,6 +139,7 @@ async function buildLiveContext(): Promise<string> {
const s = d.sensors || {} const s = d.sensors || {}
const lines: string[] = [] const lines: string[] = []
for (const [id, st] of Object.entries<any>(states)) { for (const [id, st] of Object.entries<any>(states)) {
if (st.state === 'unavailable' || st.state === 'unknown') continue
const name = st.attributes?.friendly_name || id const name = st.attributes?.friendly_name || id
let extra = '' let extra = ''
if (st.attributes?.preset_mode) extra += `, режим ${st.attributes.preset_mode}` if (st.attributes?.preset_mode) extra += `, режим ${st.attributes.preset_mode}`
@@ -206,7 +209,8 @@ export async function POST(req: Request) {
// Живой контекст (время + снимок устройств) — только для LLM-пути (fast-path // Живой контекст (время + снимок устройств) — только для LLM-пути (fast-path
// выше уже вышел). Даёт модели понимание, что включено и что вообще есть в // выше уже вышел). Даёт модели понимание, что включено и что вообще есть в
// доме, чтобы рассуждать про умный дом и не переспрашивать очевидное. // доме, чтобы рассуждать про умный дом и не переспрашивать очевидное.
const fullSys = sysPrompt + (await buildLiveContext()) const liveCtx = await buildLiveContext()
const fullSys = sysPrompt + liveCtx
// ======== GROQ ======== // ======== GROQ ========
if (provider === 'groq') { if (provider === 'groq') {
@@ -281,7 +285,7 @@ export async function POST(req: Request) {
try { try {
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) { for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
const t0 = Date.now() const t0 = Date.now()
const resp = await claudeRequest(fullSys, messages, anthropicTools) const resp = await claudeRequest(sysPrompt, liveCtx, messages, anthropicTools)
console.log(`[voice/chat] claude ${agent} r${round+1} ${Date.now()-t0}ms stop=${resp.stop_reason} in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`) console.log(`[voice/chat] claude ${agent} r${round+1} ${Date.now()-t0}ms stop=${resp.stop_reason} in=${resp.usage?.input_tokens} out=${resp.usage?.output_tokens}`)
const content: any[] = resp.content || [] const content: any[] = resp.content || []
const stopReason = resp.stop_reason || 'end_turn' const stopReason = resp.stop_reason || 'end_turn'