Skip to content

🤖 feat: per-workspace OpenRouter session_id (sticky routing + session grouping) - #3866

Open
asm wants to merge 2 commits into
coder:mainfrom
asm:openrouter-session-id
Open

🤖 feat: per-workspace OpenRouter session_id (sticky routing + session grouping)#3866
asm wants to merge 2 commits into
coder:mainfrom
asm:openrouter-session-id

Conversation

@asm

@asm asm commented Aug 17, 2026

Copy link
Copy Markdown

Summary

Workspace requests sent through the OpenRouter provider now carry a per-workspace session_id (mux-<workspace id>). OpenRouter uses session_id as its explicit sticky-routing key — all of a session's requests route to the same upstream provider to maximize prompt-cache hits even as the opening messages change between turns (exactly the shape of agentic traffic) — and groups the session's requests in the Sessions view of the Logs page, linking turns, retries, and compaction into one trace.

A workspace is mux's conversation unit (its chat.jsonl is the session transcript), so the workspace id is the natural session key: every turn, retry, compaction, status generation, and title generation for a workspace shares one session, while child task workspaces get their own.

Implementation

  • Factory stamping (ProviderModelFactory, openrouter branch): when opts.workspaceId is present and no explicit session key is configured, extraBody.session_id = "mux-<workspaceId>" (prefix derived from MUX_APP_ATTRIBUTION_TITLE so the app's identity strings stay in lockstep; value sliced to OpenRouter's 256-char cap because legacy workspace ids embed project/branch basenames). Models are created per-request with workspaceId already threaded for DevTools, so this adds one field to _createModelCore's opts — no new plumbing.
  • Override & opt-out semantics: an explicit session_id in the provider config wins verbatim; "session_id": null (or any non-string) disables tagging entirely and is stripped rather than forwarded; a configured x-session-id header — OpenRouter's documented header spelling of the same key, which the body field would beat server-side — also suppresses the stamp. session_id is declared on OpenRouterProviderConfigSchema (typed override + null opt-out) alongside its sibling body options.
  • Config hygiene in the same block (pre-existing bugs surfaced by review): the provider-branch-wide baseUrl → baseURL rename happens before the openrouter destructure, which stripped only the stale lowercase key — so baseURL, apiKeyFile (a local filesystem path), and enabled leaked into extraBody and were transmitted as junk top-level body fields on every request. The destructure now strips the full mux-internal config surface (baseURL, apiKeyFile, enabled, providerType, displayName, modelParameters). A config-level extraBody key — the OpenRouter SDK's own settings option, promised verbatim parity by the factory's doc comment — was likewise demoted into a literal nested "extraBody" body field; its entries are now lifted to the body root (and a session_id inside it counts as the explicit override).
  • Coverage: workspaceStatusGenerator and workspaceTitleGenerator now accept and forward workspaceId from the callers that hold one, so recurring status/title traffic joins the workspace's session. Requests with no workspace context (pre-creation naming) are deliberately untagged, and the docs say so.
  • Docs: an "OpenRouter Sessions" section in docs/config/providers.mdx describing the tag, the static override, the null opt-out, and the header interaction.

Review

An exhaustive multi-angle review ran before this PR (10 finder angles, adversarial verification, gap sweep): 15 findings, 14 fixed here — including the pre-existing baseURL/apiKeyFile body leak, the missing opt-out, the x-session-id header conflict, the config-extraBody demotion, the 256-char cap for legacy ids, and the status/title coverage gaps. The one non-fix: empty-string workspace ids were verified unreachable (every producer asserts or guards non-empty).

Validation

  • 11 session_id tests in providerModelFactory.test.ts behind a shared registry-capture helper: per-workspace stamping, the resolveAndCreateModel seam production callers use, config override precedence, null opt-out, x-session-id header suppression, SDK-conventional extraBody lift, omission without workspace context, the 256-char clamp, mux-internal key stripping, composition with nested provider routing options, and a wire-level test proving session_id lands at the top level of the actual HTTP body via a captured fetch.
  • Full providerModelFactory.test.ts (123 tests), plus the touched-service suites (agentStatusService, workspaceStatusGenerator, workspaceTitleGenerator, providersConfig schema — 68 tests) all green; typecheck, eslint, prettier clean.

Risks

Low and scoped to the openrouter factory branch. Users who configured nothing see one new body field on workspace requests (OpenRouter's own documented option, and creation paths without workspace context are unchanged). The strip-list change removes junk fields that were never intended to reach the wire; anyone who somehow relied on baseURL/apiKeyFile/enabled appearing in request bodies would be relying on an accident. Gateway paths (mux-gateway, coder) and custom openai-compatible providers are untouched.


🤖 Generated with Claude Code

…and session grouping

Workspace requests through the OpenRouter provider carry
session_id "mux-<workspace id>" — OpenRouter's explicit sticky-routing
key (same upstream provider per session for prompt-cache hits) and the
grouping key for its Sessions log view. Explicit config session_id wins
verbatim, null disables tagging, and a configured x-session-id header
suppresses the stamp. Status and title generation forward workspaceId
so their traffic joins the workspace session.

Also fixes pre-existing config leakage in the same block: the
baseUrl→baseURL rename ran before a destructure that stripped only the
stale key, so baseURL, apiKeyFile (a local path), and enabled shipped
as junk top-level body fields; config-level extraBody was demoted into
a literal nested body field instead of being lifted to the root.

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

asm commented Aug 17, 2026

Copy link
Copy Markdown
Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26f6e30472

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/node/services/providerModelFactory.ts
Comment thread src/node/services/providerModelFactory.ts Outdated
…sion-free

Explicit config overrides (either spelling) are clamped like the
generated stamp instead of forwarded verbatim, and overlong values keep
a readable head plus a sha256 digest tail so two legacy workspace ids
sharing a long project prefix cannot merge into one session.

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

asm commented Aug 17, 2026

Copy link
Copy Markdown
Author

@codex review

Both findings addressed in 450033f:

  • Enforce the session limit for explicit overrides — explicit config session_id values (top-level or via extraBody) are now normalized through the same clampOpenRouterSessionId helper as the generated stamp, instead of being forwarded verbatim. Covered by a new test ("clamps an overlong explicit config session_id instead of forwarding it").
  • Preserve uniqueness when shortening legacy session IDs — overlong values now keep a readable head plus a 16-hex sha256 digest of the full value, so two legacy <project>-<branch> ids sharing a long project prefix map to distinct session keys. The clamp test now creates two workspaces differing only in the tail and asserts their session_ids differ at exactly 256 chars.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep it up!

Reviewed commit: 450033f412

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

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.

1 participant