Warn On Cache Busts

Warn On Cache Busts

이 문서에서는 WarnOnCacheBusts capability를 소개해요. 실행 내 또는 그것을 이어가는 실행들 사이에서 대화의 프롬프트 캐시 히트가 모델 요청 사이에 무너지면 경고해요. 이동된 캐시 가능 접두사나 만료된 프로바이더 캐시가 조용히 캐시에서 서빙할 수 있었던 토큰을 재청구하는 대신 표면화되도록 해줘요.

출처: 문서

본문

실행 내 또는 그것을 이어가는 실행들 사이에서 대화의 프롬프트 캐시 히트가 모델 요청 사이에 무너지면 경고해요. 이동된 캐시 가능 접두사나 만료된 프로바이더 캐시가 조용히 캐시에서 서빙할 수 있었던 토큰을 재청구하는 대신 표면화되도록 해줘요.

Pydantic AI Harness가 0.x 릴리스인 동안에는 minor 릴리스 사이에 API가 바뀔 수 있어요. 그럴 때는 deprecation 경고와 릴리스 노트 마이그레이션 안내가 (당신이나 당신의 에이전트에게) 정확히 업그레이드 방법을 알려줘요. 버전 정책을 참고하세요.

What it watches

프롬프트 캐싱은 캐시 가능 접두사(도구, 시스템 지침, 메시지 히스토리 순)가 실행의 연속 요청에서 바이트 안정적으로 유지될 때만 보람이 있어요. 무언가가 그 접두사를 옮기면 — 도구 재정렬, 지침에 주입된 타임스탬프, 직렬화 수준 블록 점프 — 프로바이더가 캐시에서 서빙할 수 있었던 토큰을 재청구해요.

이것은 observe 신호이에요. 구조화된 요청을 추측하는 대신 프로바이더의 자체 판정을 읽어요. 각 응답에서 usage.cache_read_tokens를 읽고 대화가 확립한 가장 큰 캐시 가능 접두사(cache_read_tokens + cache_write_tokens, high-water mark)를 응답의 (provider_name, model_name)로 키하여 추적해요. 메시지 히스토리는 append-only이므로, 안정적인 접두사는 해당 모델에 대한 각 요청이 이전 요청이 캐시한 것 이상을 다시 읽는다는 것을 의미해요. 큰 하락은 무너짐의 관찰 가능한 특징이에요.

요청이 확립된 접두사의 collapse_ratio 미만을 다시 읽으면 모니터가 CacheBustWarning을 한 번 발생시키고 그 키를 래치하여, 건강한 read-back이 캐시를 재안정화할 때까지 그 무너짐에 대해 조용히 있어요. 지속적인 무너짐 — 실행 중간에 캐싱이 꺼짐(read == 0, write == 0), 또는 매 요청마다 접두사가 움직여 프로바이더가 아무도 다시 읽지 않는 캐시를 계속 쓰는 것 — 은 그래서 요청마다가 아니라 한 번만 경고해요.

from pydantic_ai import Agent
from pydantic_ai_harness import WarnOnCacheBusts

agent = Agent('anthropic:claude-sonnet-4-5', capabilities=[WarnOnCacheBusts()])
result = await agent.run('...')  # a CacheBustWarning fires if a cached prefix collapses mid-run
# ...and on the next turn, if the prefix the first turn cached no longer reads back:
await agent.run('...', message_history=result.all_messages())

판정은 공짜로 크로스 프로바이더예요 — pyai가 모든 프로바이더를 RequestUsagecache_read_tokens / cache_write_tokens 필드로 정규화하니까요.

Model switches and expiry

프로바이더와 모델별로 키를 매기므로 실행 중간의 모델 전환은 경고하지 않아요. FallbackModel 페일오버나 단계별 모델 변경은 다른 캐시 키를 사용하므로, 모니터는 이전 모델과 비교하는 대신 그것에 대해 새 마크를 시작해요. 마크는 리셋 대신 키별로 유지되므로, 캐시 TTL 안에서 이전 모델로 다시 전환해도 여전히 그 모델의 접두사와 비교해요.

무너짐에는 모니터가 구분할 수 없는 두 가지 모양이 있어요. 그래서 경고는 둘 다 이름을 지어요. 캐시 가능 접두사가 이동했거나, 변경되지 않은 접두사 아래에서 프로바이더의 캐시가 만료됐거나(요청 사이의 간격이 캐시 TTL보다 김 — Anthropic 기본은 5분, 각 히트마다 갱신). 같은 모델의 이전 요청 이후 간격이 cache_ttl_seconds를 초과하면 메시지가 그 간격을 보고해 긴 도구나 승인 일시 중지가 이동된 접두사로 오인되지 않게 해요. 간격은 모델별로 시간을 재므로, 전환했다 돌아오면 그 사이에 실행된 것이 아니라 돌아온 모델의 자체 유휴 시간을 측정해요.

Conversations

마크는 실행별이 아니라 대화별(RunContext.conversation_id)로 유지돼요. message_history를 통해 이전 실행을 이어가는 실행 — 직렬화되어 다시 로드된 히스토리 포함, 그것은 대화 id를 함께 지녀요 — 은 이전 실행이 확립한 접두사에 대해 판단되므로, 다음 턴의 첫 요청이 이전 턴이 캐시한 것에 대해 검사돼요. 거기가 이동된 접두사가 가장 흔히 숨는 곳이에요. 턴 사이에 다시 쓰여진 히스토리, 또는 턴마다 다른 도구·지침. 새 대화를 시작하는 실행(히스토리 없음, 또는 conversation_id='new')은 깨끗한 마크에서 시작해요.

cache_ttl_seconds보다 오래 유휴한 대화는 잊혀져요. 그때쯤 프로바이더 캐시도 만료됐으므로, 다음 실행 시작 시 낮은 read-back은 bust가 아니라 만료이며, 소음일 뿐이에요. 경고는 비교한 마크가 이 실행에서 왔는지 아니면 대화의 이전 실행에서 왔는지를 말해요.

Options

  • collapse_ratio(기본 0.5): 요청이 확립된 접두사의 이 비율 미만을 다시 읽으면 경고. 기본적으로 보수적이어서 평범한 반올림이나 부분 miss가 발화하지 않아요. 더 작은 회귀에 경고하려면 1.0으로 높이세요. 0.0보다 커야 해요 — 0.0 비율은 절대 경고할 수 없으므로, 조용한 비활성 스위치로 취급되는 대신 거부돼요.
  • min_prefix_tokens(기본 1024): 확립된 접두사가 이 토큰 수에 도달한 후에만 무너짐을 판단해요. 프로바이더의 최소 캐시 가능 크기(Anthropic은 1024) 아래에서 cache_read_tokens는 시끄럽거나 0이에요.
  • cache_ttl_seconds(기본 300): 가정된 프로바이더 캐시 TTL. 실행 내에서는 메시지만이에요. 같은 모델의 이전 요청 이후 간격이 그것을 초과하면 경고가 발화 여부를 바꾸지 않고 무너짐이 이동된 접두사보다 캐시 만료일 수 있다고 메모해요. 실행 사이에서는 메모리를 제한해요. 이보다 오래 유휴한 대화는 잊혀져서, 다음 실행이 프로바이더가 이미 버린 캐시에 대해 경고하는 대신 깨끗한 마크에서 시작해요. 캐시 수명이 더 짧은 프로바이더에는 낮추고, 더 긴 하나(예: Anthropic의 1시간 캐시)로 구성된 모델에는 높이세요.

Silencing and escalation

전용 억제 API는 없어요. stdlib warnings 메커니즘을 다른 어떤 UserWarning을 관리하듯 정확히 사용하세요:

import warnings
from pydantic_ai_harness.warn_on_cache_busts import CacheBustWarning

# Silence the whole category:
warnings.filterwarnings('ignore', category=CacheBustWarning)

# Silence one intentional bust, scoped to the operation that causes it:
with warnings.catch_warnings():
    warnings.simplefilter('ignore', CacheBustWarning)
    result = agent.run_sync('...')  # e.g. a step that switches models or adds a file

# Treat every bust as an error (dev/CI enforcement):
warnings.filterwarnings('error', category=CacheBustWarning)

테스트에서는 의도적인 bust를 pytest.warns(CacheBustWarning)로 단정하거나, 정당하게 bust하는 테스트를 @pytest.mark.filterwarnings('ignore::pydantic_ai_harness.warn_on_cache_busts.CacheBustWarning')로 억제하세요.

Logfire

Logfire는 stdlib logging 모듈이지 warnings 모듈이 아니므로, CacheBustWarning은 혼자서 트레이스에 도달하지 않아요. bust를 Logfire로 라우팅하려면 시작 시 Python 경고를 logging 시스템으로 한 번 리다이렉트하세요:

import logging

logging.captureWarnings(True)  # warnings.warn(...) -> the 'py.warnings' logger -> Logfire

모니터의 신호는 CacheBustWarning이에요. logging을 통해 라우팅하는 것이 그것이 Logfire에 도달하는 방법이에요.

Composition

  • 모니터는 for_runafter_model_request만 구현해요. 도구, 지침, 모델 설정을 추가하지 않으므로, 다른 어떤 capability, 툴셋, ToolSearch 설정과도 간섭 없이 조합돼요.
  • 마크는 에이전트가 빌드된 WarnOnCacheBusts 인스턴스에 살며 대화별로 키돼요. for_run은 각 실행을 그 대화의 마크에 바인딩해요. 여러 Agent.run 호출에 걸쳐 인스턴스 하나를 재사용하세요. 같은 대화의 실행은 마크를 공유하고, 다른 대화의 실행은 따로 판단돼요.

Scope

  • 관찰 전용. 캐시된 접두사가 무너졌다는 것을 보고하지, 왜인지는 보고하지 않아요 — 이동된 접두사와 프로바이더 측 캐시 만료는 토큰 수에서 같아 보이므로, 경고는 둘 다 이름을 지어요. 구조적 설명("이번 턴에 접두사를 옮긴 것은 무엇인가")은 별개의 작업이에요.
  • 캐싱이 활성화되고 보고될 때만 발화. 캐시를 확립하지 않는 실행은 절대 경고하지 않아요.
  • 실행 중간 모델 전환은 경고하지 않아요. 마크는 (provider_name, model_name)마다이므로, FallbackModel 페일오버는 이전 모델을 무너뜨리는 대신 새 마크를 시작해요.
  • 마크는 in-process 메모리. capability 인스턴스에 보관되므로, 다른 인스턴스, 다른 프로세스, 다른 워커를 통해 이어지는 대화는 깨끗한 마크에서 시작해요. 아무것도 영속되거나 공유되지 않아요.

API reference

공개 모듈은 WarnOnCacheBustsCacheBustWarning을 export해요. pydantic_ai_harness.warn_on_cache_busts에서 import하세요.

WarnOnCacheBusts

Bases: AbstractCapability[AgentDepsT]

대화의 프롬프트 캐시 히트가 요청 사이에 무너지면 경고.

프롬프트 캐싱을 사용하는 모델을 가진 어떤 에이전트에도 붙이세요. 각 응답에서 모니터는 usage.cache_read_tokens를 읽고 대화가 확립한 가장 큰 캐시 가능 접두사(cache_read_tokens + cache_write_tokens, high-water mark)를 응답의 (provider_name, model_name)으로 키하여 추적해요. 같은 키에 대한 이후 요청이 그 확립된 접두사의 collapse_ratio 미만을 다시 읽으면 CacheBustWarning을 한 번 발생시키고 건강한 read-back이 캐시를 재안정화할 때까지 그 무너짐에 대해 조용히 있어요. 지속적인 무너짐은 이후 요청마다가 아니라 한 번 경고해요.

마크는 실행별이 아닌 대화(RunContext.conversation_id)별로 유지되므로, message_history를 통해 이전 실행을 이어가는 실행 — 직렬화되어 다시 로드된 히스토리 포함, 대화 id를 함께 지님 — 은 이전 실행이 확립한 접두사에 대해 판단돼요. 거기가 이동된 접두사가 가장 흔히 숨는 곳이에요. 다음 턴의 첫 요청이 이전 턴이 캐시한 것을 재전송하니까요. 새 대화를 시작하는 실행(히스토리 없음, 또는 conversation_id='new')은 깨끗한 마크에서 시작해요. 마크는 대화가 cache_ttl_seconds보다 오래 유휴하면 잊혀져요. 그때쯤 프로바이더 캐시도 만료됐으므로, 다음 실행 시작 시 낮은 read-back은 bust가 아니라 만료이며, 대화를 기억하는 것은 메모리와 거짓 경고만 낭비해요.

프로바이더와 모델별로 키를 매기므로 실행 중간의 모델 전환은 경고하지 않아요. FallbackModel 페일오버나 단계별 모델 변경은 다른 캐시 키를 사용하므로, 이전 모델과 비교하는 대신 그 키에 대해 새 마크를 시작해요. 마크는 리셋 대신 키별로 유지되므로, 캐시 TTL 안에서 이전 모델로 다시 전환해도 여전히 그 모델의 확립된 접두사와 비교하며, 만료 헤지는 그 사이에 실행된 것 대신 그 모델의 이전 요청에 대해 간격을 측정해요.

메시지 히스토리는 append-only이므로, 안정적인 접두사는 각 요청이 이전 요청이 캐시한 것 이상을 다시 읽는다는 것을 의미해요. 큰 하락은 무너짐의 관찰 가능한 특징이에요. 원인이 이동된 접두사(재정렬된 도구, 주입된 타임스탬프, 직렬화 수준 블록 점프, 턴 사이에 다시 쓰여진 히스토리)든 요청 간격이 캐시 TTL을 초과할 때의 프로바이더 측 캐시 만료든. 모니터는 무너짐을 표면화할 뿐 원인을 귀속하지 않아요.

from pydantic_ai import Agent
from pydantic_ai_harness.warn_on_cache_busts import WarnOnCacheBusts

agent = Agent('anthropic:claude-sonnet-4-5', capabilities=[WarnOnCacheBusts()])
result = await agent.run('...')  # a CacheBustWarning fires if a cached prefix collapses mid-run
# ...and on the next turn, if the prefix the first turn cached no longer reads back:
await agent.run('...', message_history=result.all_messages())

캐싱이 꺼져 있거나 보고되지 않으면(cache_read_tokens가 0으로 유지) 모니터는 조용해요. 캐싱을 연습하지 않는 테스트에서 절대 잘못 발화하지 않아요. 억제와 dev/CI 상향 둘 다 stdlib warnings 필터를 통해 갑니다 — CacheBustWarning 참고.

Attributes

collapse_ratio

요청이 확립된 접두사의 이 비율 미만을 다시 읽으면 경고.

기본적으로 보수적(0.5). 이전에 캐시된 접두사의 절반 아래로만 떨어지면 무너짐으로 계산되므로, 평범한 프로바이더 반올림이나 부분 캐시 miss가 발화하지 않아요. 더 작은 회귀에 경고하려면 1.0으로 높이세요. 0.0보다 커야 해요(0.0 비율은 절대 경고할 수 없으므로, 조용한 비활성 스위치로 취급되는 대신 거부돼요).

Type: float Default: 0.5

min_prefix_tokens

확립된 접두사가 이 토큰 수에 도달한 후에만 무너짐을 판단.

프로바이더의 최소 캐시 가능 크기(Anthropic은 1024) 아래에서 cache_read_tokens는 시끄럽거나 0이므로, 거짓 양성 피하기 위해 작은 접두사는 무시돼요.

Type: int Default: 1024

cache_ttl_seconds

가정된 프로바이더 캐시 TTL(초). Anthropic 기본은 300, 각 히트마다 갱신.

두 용도. 실행 내에서는 메시지만이에요. 같은 모델의 이전 요청 이후 간격이 이것을 초과하면 경고가 발화 여부를 바꾸지 않고 무너짐이 이동된 접두사보다 프로바이더 측 캐시 만료일 수 있다고 메모해요. 실행 사이에서는 메모리를 제한해요. 이보다 오래 유휴한 대화는 잊혀져서, 다음 실행이 프로바이더가 이미 버린 캐시에 대해 경고하는 대신 깨끗한 마크에서 시작해요. 캐시 수명이 더 짧은 프로바이더에는 낮추고, 더 긴 하나(예: Anthropic의 1시간 캐시)로 구성된 모델에는 높이세요.

Type: float Default: 300.0

Methods

for_run

@async

def for_run(ctx: RunContext[AgentDepsT]) -> AbstractCapability[AgentDepsT]

캐시가 만료된 대화를 잊으며 이 실행을 그 대화의 마크에 바인딩.

마크는 에이전트가 빌드된 인스턴스에 살므로, 이 프로세스에서 그것을 거치는 대화의 모든 실행이 공유해요. 대화 id가 없는 실행은 개인 마크를 얻고 혼자 판단돼요.

Returns

AbstractCapability[AgentDepsT]

after_model_request

@async

def after_model_request(
    ctx: RunContext[AgentDepsT],
    *,
    request_context: ModelRequestContext,
    response: ModelResponse,
) -> ModelResponse

이 응답의 캐시 읽기를 그 모델의 확립된 접두사와 비교한 다음 갱신.

Returns

ModelResponse

CacheBustWarning

Bases: UserWarning

이전에 확립된 프롬프트 캐시 히트가 이후 요청에서 무너지면 경고.

WarnOnCacheBusts가 요청이 같은 대화의 이전 요청이 확립한 것보다 훨씬 적은 캐시 토큰을 같은 프로바이더·모델에 대해 다시 읽었을 때 발생시켜요 — 그 이전 요청이 이 실행의 이전이든 message_history를 통해 이어지는 이전 실행이든. 가능한 원인은 이동된 캐시 가능 접두사(재정렬된 도구, 주입된 타임스탬프, 직렬화 수준 블록 점프, 턴 사이에 다시 쓰여진 히스토리) 또는 변경되지 않은 접두사 아래의 프로바이더 측 캐시 만료(요청 간격이 캐시 TTL보다 김). 모니터는 무너짐을 관찰할 뿐 원인을 귀속하지 않아요.

stdlib warnings 메커니즘(전용 API 없음)으로 억제하거나 dev/CI에서 오류로 상향하세요:

import warnings from pydantic_ai_harness.warn_on_cache_busts import CacheBustWarning

Silence the whole category:

warnings.filterwarnings('ignore', category=CacheBustWarning)

Silence one intentional bust, scoped to the operation that causes it:

with warnings.catch_warnings(): warnings.simplefilter('ignore', CacheBustWarning) result = agent.run_sync('...') # e.g. a step that switches models or adds a file

Treat every bust as an error (dev/CI enforcement):

warnings.filterwarnings('error', category=CacheBustWarning)

테스트에서는 의도적인 bust를 pytest.warns(CacheBustWarning)로 단정하거나, 정당하게 bust하는 테스트를 @pytest.mark.filterwarnings('ignore::pydantic_ai_harness.warn_on_cache_busts.CacheBustWarning')로 억제하세요.

더 알아보기 (Learn more)