feat(llm): task-scoped session affinity for prompt caching#332
Conversation
| b := make([]byte, 16) | ||
| if _, err := io.ReadFull(rand.Reader, b); err != nil { | ||
| // Fallback — extremely unlikely but keeps things working without panics. | ||
| return fmt.Sprintf("fallback-%d", time.Now().UnixNano()) |
There was a problem hiding this comment.
The fallback key generation uses only time.Now().UnixNano(), which can produce identical values when called concurrently within the same nanosecond (e.g., multiple clients initializing simultaneously). While crypto/rand failure is extremely rare, the fallback should still avoid collisions. Consider adding an atomic counter or mixing in a monotonic source to guarantee uniqueness even in the fallback path.
Suggestion:
| return fmt.Sprintf("fallback-%d", time.Now().UnixNano()) | |
| return fmt.Sprintf("fallback-%d-%x", time.Now().UnixNano(), b[:4]) |
There was a problem hiding this comment.
This never runs concurrently
|
Overall this direction looks good to me. One thing I’d like to see is an explicit opt-out. Right now the built-in Should we add a config flag to disable this for a provider, e.g. FYI @lizhengfeng101 |
|
@MuoDoo @lizhengfeng101 I'd like to ask for your opinions to finalize the changes.
|
Per review feedback on alibaba#332: the openai preset unconditionally sending prompt_cache_key could break OpenAI-compatible gateways (pointed at via providers.openai.url) that reject unknown body fields. Gate the whole mechanism behind an explicit opt-in: session_affinity on provider entries and the legacy llm block (also settable via ocr config set and OCR_LLM_SESSION_AFFINITY). When off (the default), no key is injected and {ocr_session_key} placeholders pass through verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5a6fe95 to
f2e6d72
Compare
Per review feedback on alibaba#332: the openai preset unconditionally sending prompt_cache_key could break OpenAI-compatible gateways (pointed at via providers.openai.url) that reject unknown body fields. Gate the whole mechanism behind an explicit opt-in: session_affinity on provider entries and the legacy llm block (also settable via ocr config set and OCR_LLM_SESSION_AFFINITY). When off (the default), no key is injected and {ocr_session_key} placeholders pass through verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
f2e6d72 to
870332f
Compare
|
Simplified the configuration interface to use only the Users can opt in prompt-caching with |
870332f to
cc6a342
Compare
|
@MuoDoo Done rebasing. Since this is now an explicit opt-in via extra body or headers, I think it is safe even without other flags. |
…e variable
Derive a prompt-cache affinity key per LLM conversation, scoped to the
review session and the task within it (<session-id>-<task-type>-<hash>).
Review/scan runs bind the session ID into the request context and each
task conversation refines it where it starts, so every request carries
the real OCR session's key at per-conversation granularity — the
granularity provider prompt caches reuse prefixes at.
Embedding the {ocr_session_key} placeholder in extra_headers or
extra_body values is the opt-in: clients expand it per request, and
requests without it are unchanged. OCR never enforces a parameter or
header name, so any provider convention works with existing config
fields, e.g.:
extra_body: {"prompt_cache_key": "{ocr_session_key}"} (OpenAI)
extra_headers: x-session-affinity={ocr_session_key} (gateways)
Closes alibaba#229
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cc6a342 to
50e401b
Compare
Description
Adds provider-side session affinity for prompt caching (#229) via a
{ocr_session_key}template variable. Writing the placeholder is the opt-in — OCR never invents a parameter or header name, and sends nothing session-related unless configured.OCR derives a prompt-cache affinity key for every LLM conversation, scoped to the review session and the task within it:
Prompt caches match on prefixes, and OCR's task types (plan, per-file main tool-loop, compression, dedup, filter, relocation) use unrelated prompts — while each file's main tool-loop re-sends a growing conversation prefix every round, which is where cache hits actually come from. Scoping the key per task conversation keeps each conversation on a consistent cache node instead of pinning a whole run to one hot key (e.g. OpenAI reroutes a
prompt_cache_keyonce it exceeds ~15 req/min). The session-ID prefix keeps provider-side cache logs correlatable withocr sessionrecords.This PR:
adds
llm.SessionTaskKeyand context helpers (ContextWithSessionKey/SessionKeyFromContext); review/scan runs bind the real session's ID as a base key atRun, and each task conversation refines it where it starts (llmloop.RunPerFile, plan, review filter, compression, dedup, project summary, relocation) — using the same(session, task type, path)triple those sites already record into session historyexpands the
{ocr_session_key}template variable per request inextra_headersvalues and (recursively)extra_bodyvalues, so any provider's convention can be expressed with existing config fields — no new config surface:moves
extra_headers/extra_bodyapplication from client construction to per-request SDK options so the template variable can expand to the key each request's context carries; session-less callers (ocr llm test) fall back to a per-client generated keyrequests without the placeholder are byte-for-byte unchanged — no behavior change for existing configurations, and nothing is sent to gateways that reject unknown fields
Type of Change
How Has This Been Tested?
make testpasses locallyAlso verified with:
go test ./...go vet ./...New tests cover:
llmloop: every round ofRunPerFilereaches the client with the same task-scoped key (TestRunPerFile_TagsRequestsWithTaskSessionKey){ocr_session_key}expansion in headers and nested body values (including the OpenAIprompt_cache_keyrecipe viaextra_body){ocr_session_key}placeholder survives endpoint resolution untouchedSessionTaskKey: deterministic, distinct per task type and scope, header-safe for non-ASCII pathsContext propagation was verified end-to-end: the async comment worker pool and background compression use
context.WithoutCancel, which preserves context values.Checklist
go fmt,go vet)Related Issues
Closes #229
🤖 Generated with Claude Code