Files
oh-my-pi/packages/coding-agent/src/session/session-metadata.ts
T
roboomp c5af78d403 fix(advisor): propagate advisor provider session id via metadata
The advisor Agent is constructed separately in session-advisors.ts with
its own advisorProviderSessionId, but unlike AgentSession it never had a
metadata resolver installed. Its outbound requests therefore omitted the
metadata.user_id session identity that main and subagent requests carry,
so custom Anthropic-compatible proxies saw advisor traffic with no stable
session id to route or attribute on.

Extract buildSessionMetadata into session/session-metadata.ts and install
it as the advisor agent's metadata resolver, scoped to the advisor's own
provider session id and resolved live so token refreshes surface the
current account_uuid.

Fixes #6625
2026-07-25 17:30:18 +00:00

54 lines
2.8 KiB
TypeScript

import { deriveClaudeDeviceId } from "@oh-my-pi/pi-ai";
import { getInstallId } from "@oh-my-pi/pi-utils";
import type { AuthStorage } from "./auth-storage";
/**
* Build the per-request `metadata` payload for the Anthropic provider, shaped
* like real Claude Code's `getAPIMetadata` output (`{ session_id, account_uuid,
* device_id }`) so the backend buckets requests under one session and attributes
* them to the authenticated OAuth account when available. Resolved at request
* time so token refreshes and login/logout transitions don't strand a stale
* account UUID in memory. `account_uuid` and `device_id` are omitted for
* non-Anthropic providers to avoid leaking the user's Claude identity to
* third-party APIs (including Anthropic-format-compatible proxies such as
* cloudflare-ai-gateway or gitlab-duo).
*
* Installed via `Agent#setMetadataResolver` on the main `AgentSession`, each
* subagent session, and the separately constructed advisor `Agent` — each with
* its own provider session id — so Main, subagent, and Advisor requests each
* expose a distinct, stable provider-facing session identity.
*
* `provider` is the target provider string (e.g. `"anthropic"`) and gates the
* `account_uuid` and `device_id` lookups — only `"anthropic"` requests carry them.
*
* `sessionId` is forwarded to the auth-storage session-sticky lookup so that
* multi-credential setups attribute to the same OAuth account used for the
* actual API request rather than always picking the first credential.
*
* `authStorage` is treated as optional so test fixtures that stub `modelRegistry`
* without a real storage layer still work; the resolver simply skips the lookup
* and emits `{ session_id }` alone, matching the no-OAuth-credential path.
*/
export function buildSessionMetadata(
sessionId: string,
provider: string,
authStorage: AuthStorage | undefined,
): Record<string, unknown> {
const userId: Record<string, string> = { session_id: sessionId };
// Only look up account_uuid when the request is going to Anthropic. Injecting
// a Claude OAuth account_uuid into requests bound for other providers (including
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
// would leak the user's Anthropic identity to unrelated third-party APIs.
if (provider === "anthropic") {
const accountUuid = authStorage?.getOAuthAccountId("anthropic", sessionId);
if (typeof accountUuid === "string" && accountUuid.length > 0) {
userId.account_uuid = accountUuid;
// Claude Code's `device_id` is a stable 64-hex account-scoped install
// identifier. Include both omp's persistent install id and the Claude
// account UUID so two accounts on the same install do not share a device.
userId.device_id = deriveClaudeDeviceId(getInstallId(), accountUuid);
}
}
return { user_id: JSON.stringify(userId) };
}