Skip to content

feat(llm): task-scoped session affinity for prompt caching#332

Open
cometkim wants to merge 1 commit into
alibaba:mainfrom
cometkim:x-session-affinity
Open

feat(llm): task-scoped session affinity for prompt caching#332
cometkim wants to merge 1 commit into
alibaba:mainfrom
cometkim:x-session-affinity

Conversation

@cometkim

@cometkim cometkim commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Description

Adds provider-side session affinity for prompt caching (#229).

OCR now derives a prompt-cache affinity key for every LLM conversation, scoped to the review session and the task within it:

<session-id>-<task-type>-<scope-hash>

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_key once it exceeds ~15 req/min). The session-ID prefix keeps provider-side cache logs correlatable with ocr session records.

This PR:

  • adds llm.SessionTaskKey and context helpers (ContextWithSessionKey / SessionKeyFromContext); review/scan runs bind the real session's ID as a base key at Run, 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 history

  • injects the key automatically as prompt_cache_key in the request body for the openai provider preset (new Provider.SessionKeyBody field, propagated via ResolvedEndpoint); an explicit extra_body entry with the same name wins

  • adds an {ocr_session_key} template variable, expanded per request in extra_headers and (recursively) extra_body values, so providers that route prompt-cache traffic by header can be configured without OCR knowing each provider's convention:

    ocr config set custom_providers.my-gateway.extra_headers "x-session-affinity={ocr_session_key}"
  • moves extra_headers/extra_body application 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 key

  • Anthropic protocol gets no body injection (the API manages cache affinity server-side); the template variable still works for anthropic-protocol gateways

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Refactoring (no functional changes)
  • Documentation update
  • CI / Build / Tooling

How Has This Been Tested?

  • make test passes locally
  • Manual testing (describe below)

Also verified with:

  • go test ./...
  • go vet ./...

New tests cover:

  • llmloop: every round of RunPerFile reaches the client with the same task-scoped key (TestRunPerFile_TagsRequestsWithTaskSessionKey)
  • clients: context key takes precedence over the client fallback for both protocols; {ocr_session_key} expansion in headers and nested body values; prompt_cache_key injection and user extra_body override; fallback key auto-generation
  • resolver: openai preset resolves SessionKeyBody: "prompt_cache_key"; custom providers don't, and the raw {ocr_session_key} placeholder survives resolution untouched
  • SessionTaskKey: deterministic, distinct per task type and scope, header-safe for non-ASCII paths

Context propagation was verified end-to-end: the async comment worker pool and background compression use context.WithoutCancel, which preserves context values.

Checklist

  • My code follows the project's coding style (go fmt, go vet)
  • I have performed a self-review of my code
  • I have added tests that prove my fix is effective or my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly (if applicable)
  • I have signed the CLA

Related Issues

Closes #229

🤖 Generated with Claude Code

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 openai provider preset sends the key as prompt_cache_key in the
request body automatically; other providers can route it via the new
{ocr_session_key} template variable in extra_headers/extra_body, which
clients expand per request.

Closes alibaba#229

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 OpenCodeReview found 1 issue(s) in this PR.

  • ✅ 1 posted as inline comment(s)
  • 📝 0 posted as summary

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())

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
return fmt.Sprintf("fallback-%d", time.Now().UnixNano())
return fmt.Sprintf("fallback-%d-%x", time.Now().UnixNano(), b[:4])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This never runs concurrently

@lizhengfeng101 lizhengfeng101 requested a review from MuoDoo July 9, 2026 09:10
@MuoDoo

MuoDoo commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Overall this direction looks good to me.

One thing I’d like to see is an explicit opt-out. Right now the built-in openai provider always sends prompt_cache_key. That’s fine for the real OpenAI API, but if someone points providers.openai.url at an OpenAI-compatible gateway that rejects unknown body fields, this could break the main LLM path. The workaround is to switch to custom_providers, but that’s not very obvious.

Should we add a config flag to disable this for a provider, e.g. providers.<name>.session_affinity = false or similar?

FYI @lizhengfeng101

@cometkim

cometkim commented Jul 9, 2026

Copy link
Copy Markdown
Contributor Author

@MuoDoo @lizhengfeng101 I'd like to ask for your opinions to finalize the changes.

  • Since this is a breaking change, would it be a good idea to even introduce it as an opt-in feature?
  • And would it be better to use a provider-oriented term like prompt_cache?

@lizhengfeng101 lizhengfeng101 requested a review from css521 July 10, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Session affinity (for prompt caching) per provider

2 participants