Skip to content

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

Merged
lizhengfeng101 merged 3 commits into
alibaba:mainfrom
cometkim:x-session-affinity
Aug 14, 2026
Merged

feat(llm): task-scoped session affinity for prompt caching#332
lizhengfeng101 merged 3 commits 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) 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:

<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

  • expands the {ocr_session_key} template variable per request in extra_headers values and (recursively) extra_body values, so any provider's convention can be expressed with existing config fields — no new config surface:

    # OpenAI: prompt_cache_key request body field
    ocr config set providers.openai.extra_body '{"prompt_cache_key": "{ocr_session_key}"}'
    
    # Header-routed gateways (Cloudflare, Fireworks, Mistral, ...)
    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

  • requests 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

  • 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:

  • no injection without the placeholder: requests carry no session-related fields unless explicitly configured
  • 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 (including the OpenAI prompt_cache_key recipe via extra_body)
  • resolver: the raw {ocr_session_key} placeholder survives endpoint 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

@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
cometkim added a commit to cometkim/open-code-review that referenced this pull request Jul 13, 2026
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>
@cometkim
cometkim force-pushed the x-session-affinity branch from 5a6fe95 to f2e6d72 Compare July 13, 2026 07:07
cometkim added a commit to cometkim/open-code-review that referenced this pull request Jul 13, 2026
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>
@cometkim

This comment was marked as outdated.

@cometkim
cometkim force-pushed the x-session-affinity branch from f2e6d72 to 870332f Compare July 13, 2026 07:28
@cometkim

Copy link
Copy Markdown
Contributor Author

Simplified the configuration interface to use only the {ocr_session_key} template variable.

Users can opt in prompt-caching with extra_body or extra_headers. No extra config or env is needed for this.

@cometkim
cometkim force-pushed the x-session-affinity branch from 870332f to cc6a342 Compare July 15, 2026 05:31
@cometkim

Copy link
Copy Markdown
Contributor Author

@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.

@cometkim

cometkim commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

rebased

@cometkim
cometkim force-pushed the x-session-affinity branch 2 times, most recently from 66b03d3 to 422f0d2 Compare August 11, 2026 05:41
@cometkim

Copy link
Copy Markdown
Contributor Author

Rebased again.

@lizhengfeng101 Can I get a review for this? I think this is a pretty important feature for cost management. While providers like DeepSeek that offer automatic caching are popular right now, there are still times when I need to use providers like Fireworks AI that don't.

@css521 css521 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for working on this. The task-scoped key derivation and opt-in template approach look reasonable, but I found one blocking gap.

The new expansion is wired into OpenAIClient and AnthropicClient, but not OpenAIResponsesClient. That client still applies ExtraHeaders at construction time and iterates the raw ExtraBody map per request without consulting SessionKeyFromContext. With the supported openai-responses protocol, both:

  • prompt_cache_key: "{ocr_session_key}"
  • X-Session-Affinity: {ocr_session_key}

are therefore sent literally. I reproduced this with an HTTP test server. This collapses all runs/tasks onto the same literal affinity key (or can make a gateway reject the request), instead of providing the task-scoped affinity promised by this PR.

Could you please mirror the per-request key resolution and header/body expansion in OpenAIResponsesClient, initialize its fallback key, and add regression coverage through NewLLMClient using ProtocolOpenAIResponses? Since this client already maps ChatRequest.SessionID to PromptCacheKey, the precedence between that typed field and extra_body.prompt_cache_key should also be made explicit and tested.

Non-blocking: main was force-pushed after the latest CI run and the branch still contains the old #827 merge commit, so a rebase and fresh CI run would also be helpful.

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

@cometkim nice work! Please rebase main

@cometkim

Copy link
Copy Markdown
Contributor Author

I'm on it. Maybe I made some mistakes while rebasing this multiple times 😅

Btw, I just noticed that Alibaba Cloud's Model Studio also supports implicit prompt caching, but it charges different pricing than the explicit one!

@cometkim
cometkim force-pushed the x-session-affinity branch 2 times, most recently from f884059 to 98604c2 Compare August 13, 2026 09:41
@cometkim
cometkim requested a review from css521 August 13, 2026 09:56
@cometkim

Copy link
Copy Markdown
Contributor Author

@css521 rebased with fixes. I added a test suite for the responses API client to match other two clients.

@yingjiexu2002

Copy link
Copy Markdown
Collaborator

Could you also sync the ru docs (pages/src/content/docs/ru/configuration.md)? It exists alongside the en/ja/zh ones but wasn't updated in this PR. Thanks!

cometkim and others added 2 commits August 13, 2026 19:15
…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>
@cometkim
cometkim force-pushed the x-session-affinity branch from 98604c2 to 9e6c5a9 Compare August 13, 2026 10:16
@cometkim

Copy link
Copy Markdown
Contributor Author

@yingjiexu2002 done by Claude. The content looks good when cross-checked with Google Translate.

@lizhengfeng101

Copy link
Copy Markdown
Collaborator

Missing ru locale for the new documentation section

This PR adds the "Session affinity for prompt caching" section to three of the four maintained doc locales — en, ja, zh — but not ru.

pages/src/content/docs/ carries four locales, and AGENTS.md names them explicitly:

the doc pages under pages/src/content/docs/<locale>/ (en, zh, ja, ru, Markdown throughout)

The anchor already exists in the Russian file: pages/src/content/docs/ru/configuration.md has the matching ### Отправка полей, специфичных для поставщика section (the extra_body block, around L217–226), immediately followed by ## Настройка языка ревью. The new section belongs between those two, mirroring the placement in the other three locales.

Since the whole feature is opt-in and discoverable only through this doc — nothing is sent unless the user writes {ocr_session_key} into extra_headers/extra_body — a missing locale means Russian-language users have no way to find the feature at all. That makes the gap more consequential here than for a doc change that merely describes existing behaviour.

Suggested addition to pages/src/content/docs/ru/configuration.md:

### Аффинити сессии для кеширования промптов

OCR выводит ключ аффинити кеша промптов для каждого диалога с LLM,
ограниченный областью сессии ревью и задачи внутри неё
(`<ID сессии>-<тип задачи>-<хеш области>`). Кеши промптов сопоставляются
по префиксам, поэтому ключи на уровне диалога удерживают каждый растущий
диалог (например, цикл инструментов при ревью одного файла) на одном и том
же узле кеша, вместо того чтобы стягивать весь запуск к одному «горячему»
ключу; префикс с ID сессии позволяет сопоставлять журналы кеша на стороне
поставщика с записями `ocr session`.

Чтобы включить это, вставьте шаблонную переменную `{ocr_session_key}`
в значения `extra_headers` или `extra_body` там, где её ожидает ваш
поставщик — OCR подставляет ключ диалога в каждый запрос, а без такой
настройки не отправляет ничего:

```bash
# OpenAI: поле prompt_cache_key в теле запроса
ocr config set providers.openai.extra_body '{"prompt_cache_key": "{ocr_session_key}"}'

# Шлюзы с маршрутизацией по заголовкам
ocr config set custom_providers.my-gateway.extra_headers "x-session-affinity={ocr_session_key}"
```

One thing to note about that draft: the second example deliberately omits the vendor list that the en/ja/zh versions carry ("Cloudflare, Fireworks, Mistral commonly use x-session-affinity"). Cloudflare AI Gateway routes on cf-aig-* headers rather than x-session-affinity, so that list looks worth double-checking against each provider's docs before it is translated further. Whatever wording it settles on should then be applied consistently across all four locales.

@cometkim

cometkim commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

One thing to note about that draft: the second example deliberately omits the vendor list that the en/ja/zh versions carry ("Cloudflare, Fireworks, Mistral commonly use x-session-affinity"). Cloudflare AI Gateway routes on cf-aig-* headers rather than x-session-affinity, so that list looks worth double-checking against each provider's docs before it is translated further. Whatever wording it settles on should then be applied consistently across all four locales.

Cloudflare uses the x-session-affinity header (Workers AI provider, not the gateway) for prompt caching, but I noticed that Mistral actually uses the OpenAI-style prompt_cache_key body field.

I agree that not mentioning specific vendor names here would be better.

@lizhengfeng101 lizhengfeng101 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@cometkim

Copy link
Copy Markdown
Contributor Author

Edited the docs based on the section written in English as canonical:

### Session affinity for prompt caching

OCR 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,
so per-conversation keys keep each growing conversation (such as a file's
review tool-loop) on a consistent cache node instead of pinning the whole
run to one hot key; the session-ID prefix lets provider-side cache logs be
correlated with `ocr session` records.

To opt in, embed the `{ocr_session_key}` template variable in
`extra_headers` or `extra_body` values wherever your provider expects the
key — OCR substitutes the conversation's key per request and sends nothing
otherwise:

    # By OpenAI-style request body field (e.g. prompt_cache_key)
    ocr config set providers.openai.extra_body '{"prompt_cache_key": "{ocr_session_key}"}'

    # By HTTP header (e.g. x-session-affinity)
    ocr config set custom_providers.my-gateway.extra_headers "x-session-affinity={ocr_session_key}"

@css521 css521 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@lizhengfeng101
lizhengfeng101 merged commit 144607f into alibaba:main Aug 14, 2026
13 checks passed
@cometkim
cometkim deleted the x-session-affinity branch August 14, 2026 05:08
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

5 participants