feat: inject Claude metadata.user_id for relay provider cache support#729
Conversation
- 修改 generateDeterministicSessionId() 生成格式为 sess_{8位}_{12位}
- 为 Claude 请求自动注入 metadata.user_id(格式:user_{hash}_account__session_{sessionId})
- user hash 基于 API Key ID 生成,保持稳定
- 如果已存在 metadata.user_id 则保持原样
📝 Walkthrough总体描述该PR为Claude请求添加元数据user_id注入功能。在用户消息缺少metadata.user_id时,系统自动生成并注入稳定的user_id标识符(格式为user_{hash}account__session{sessionId})。包括核心请求转发逻辑、数据库模式更新、系统设置集成和多语言配置。 变更表
预估代码审查工作量🎯 3 (Moderate) | ⏱️ ~25 minutes 可能相关的PR
建议的审查者
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Summary of ChangesHello @ProgramCaiCai, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request enhances the proxy's handling of Claude API requests to improve prompt caching efficiency when utilizing third-party relay providers. It achieves this by ensuring that a consistent and stable Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a feature to automatically inject metadata.user_id for Claude API requests to improve caching with third-party providers, and it also aims to unify the format of session IDs. The implementation is generally sound and addresses the problem described.
I've identified a couple of areas for improvement:
- The session ID format unification isn't fully achieved, as the format generated by
generateDeterministicSessionIdstill differs fromSessionManager.generateSessionId. The code comment regarding this is also inaccurate. - The logic for injecting
metadata.user_idcould be more robust to handle cases wheremessage.metadatais not an object.
I've provided specific comments and code suggestions to address these points. Overall, this is a valuable addition for improving compatibility with relay providers.
src/app/v1/_lib/proxy/forwarder.ts
Outdated
| const existingMetadata = message.metadata as Record<string, unknown> | undefined; | ||
| if (existingMetadata?.user_id) { | ||
| logger.debug("[ProxyForwarder] metadata.user_id already exists, skipping injection"); | ||
| return message; | ||
| } | ||
|
|
||
| // 获取必要信息 | ||
| const keyId = session.authState?.key?.id; | ||
| const sessionId = session.sessionId; | ||
|
|
||
| if (!keyId || !sessionId) { | ||
| logger.debug("[ProxyForwarder] Missing keyId or sessionId, skipping metadata injection"); | ||
| return message; | ||
| } | ||
|
|
||
| // 生成稳定的 user hash(基于 API Key ID) | ||
| const stableHash = crypto | ||
| .createHash("sha256") | ||
| .update(`claude_user_${keyId}`) | ||
| .digest("hex"); | ||
|
|
||
| // 构建 user_id | ||
| const userId = `user_${stableHash}_account__session_${sessionId}`; | ||
|
|
||
| // 注入 metadata | ||
| const newMetadata = { | ||
| ...existingMetadata, | ||
| user_id: userId, | ||
| }; |
There was a problem hiding this comment.
The current implementation has a potential issue when message.metadata is a non-object type like a string. The type assertion as Record<string, unknown> can hide this, and spreading a string (...existingMetadata) will result in an object with numeric keys (e.g., ..."foo" becomes { '0': 'f', '1': 'o', '2': 'o' }), which is likely not the intended behavior.
To make this more robust, it's better to perform a type check on message.metadata before using it as an object.
const metadata = message.metadata;
const existingUserId =
metadata && typeof metadata === "object" && "user_id" in metadata
? (metadata as Record<string, unknown>).user_id
: undefined;
if (existingUserId) {
logger.debug("[ProxyForwarder] metadata.user_id already exists, skipping injection");
return message;
}
// 获取必要信息
const keyId = session.authState?.key?.id;
const sessionId = session.sessionId;
if (!keyId || !sessionId) {
logger.debug("[ProxyForwarder] Missing keyId or sessionId, skipping metadata injection");
return message;
}
// 生成稳定的 user hash(基于 API Key ID)
const stableHash = crypto
.createHash("sha256")
.update(`claude_user_${keyId}`)
.digest("hex");
// 构建 user_id
const userId = `user_${stableHash}_account__session_${sessionId}`;
// 注入 metadata
const newMetadata = {
...(metadata && typeof metadata === "object" ? metadata : {}),
user_id: userId,
};
src/app/v1/_lib/proxy/session.ts
Outdated
| // 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致 | ||
| return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`; |
There was a problem hiding this comment.
The goal of unifying the session ID format is good, but this change doesn't fully achieve it, and the comment is inaccurate.
SessionManager.generateSessionId() produces an ID with the format sess_{variable-length-base36}_{12hex} because Date.now().toString(36) results in a variable-length base36 string (e.g., it will be 9 characters for dates in 2030 and beyond).
This function now produces sess_{8hex}_{12hex}.
While the new format is arguably better due to its fixed length, it's not consistent with the other function. This could lead to confusion or issues if other parts of the system expect a truly unified format.
To truly unify them, SessionManager.generateSessionId() would also need to be changed. If that's out of scope, I'd suggest updating the comment to be more accurate.
| // 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致 | |
| return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`; | |
| // 格式:sess_{8位hex}_{12位hex},以提供固定长度的确定性 session ID | |
| return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`; |
src/app/v1/_lib/proxy/session.ts
Outdated
| // 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致 | ||
| return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`; |
There was a problem hiding this comment.
Format claim might be misleading - SessionManager.generateSessionId() uses Date.now().toString(36) (variable 8-9 chars) + random hex (12 chars), while this uses fixed 8 hex + 12 hex from SHA-256. Formats are similar but not identical (base36 timestamp vs hex hash).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/session.ts
Line: 383:384
Comment:
Format claim might be misleading - `SessionManager.generateSessionId()` uses `Date.now().toString(36)` (variable 8-9 chars) + random hex (12 chars), while this uses fixed 8 hex + 12 hex from SHA-256. Formats are similar but not identical (base36 timestamp vs hex hash).
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/app/v1/_lib/proxy/forwarder.ts`:
- Around line 214-218: The current falsy check if (existingMetadata?.user_id)
can incorrectly treat valid values like "" or 0 as missing; update the presence
check in ProxyForwarder to detect actual existence of the property instead of
truthiness — e.g., ensure you inspect message.metadata via existingMetadata and
use a precise test such as
Object.prototype.hasOwnProperty.call(existingMetadata, 'user_id') or
existingMetadata.user_id !== undefined && existingMetadata.user_id !== null
before skipping injection, and keep the existing logger.debug("[ProxyForwarder]
metadata.user_id already exists, skipping injection") and return message
behavior.
🧹 Nitpick comments (1)
src/app/v1/_lib/proxy/forwarder.ts (1)
201-255: 函数设计合理,有一个小建议:keyId可能为0时会被跳过Line 225 的
if (!keyId || !sessionId)中,若keyId的数据库主键从 0 开始(虽然少见),!keyId会将其判为缺失。如果该仓库的 ID 一定 > 0,可忽略。更严谨的判空
- if (!keyId || !sessionId) { + if (keyId == null || !sessionId) {
There was a problem hiding this comment.
Code Review Summary
此 PR 为 Claude API 请求自动注入 metadata.user_id 以支持第三方中转站的 prompt cache 功能,并统一了 session ID 格式。代码逻辑清晰,防御性编程到位,但缺少测试覆盖。
PR Size: XS
- Lines changed: 72 (69 additions + 3 deletions)
- Files changed: 2
Issues Found
| Category | Critical | High | Medium | Low |
|---|---|---|---|---|
| Logic/Bugs | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Error Handling | 0 | 0 | 0 | 0 |
| Types | 0 | 0 | 0 | 0 |
| Comments/Docs | 0 | 0 | 0 | 0 |
| Tests | 0 | 1 | 0 | 0 |
| Simplification | 0 | 0 | 0 | 0 |
High Priority Issues (Should Fix)
1. 缺少单元测试覆盖 (TEST-MISSING-CRITICAL)
位置: 整个 PR
问题: 根据 CLAUDE.md 规则 #2:"Test Coverage - All new features must have unit test coverage of at least 80%",但此 PR 未添加任何测试文件。
新增的 injectClaudeMetadataUserId() 函数和 generateDeterministicSessionId() 格式变更都缺少测试覆盖,无法验证以下关键场景:
- 当
metadata.user_id已存在时,是否正确跳过注入 - 当
keyId或sessionId缺失时,是否正确返回原始 message - 生成的
user_id格式是否符合预期 - SHA-256 哈希是否稳定(相同 keyId 生成相同 hash)
- 只对
claude和claude-auth提供商注入 generateDeterministicSessionId()新格式是否与SessionManager.generateSessionId()一致
建议修复:
创建测试文件 tests/unit/proxy/proxy-forwarder-metadata-injection.test.ts:
import { describe, expect, it, vi } from "vitest";
import crypto from "node:crypto";
import type { Provider } from "@/types/provider";
import { ProxySession } from "@/app/v1/_lib/proxy/session";
// 注意:由于 injectClaudeMetadataUserId 是私有函数,需要通过集成测试验证
// 或者将其导出为命名导出以便测试
describe("Claude metadata.user_id injection", () => {
it("should inject user_id when not present for claude provider", () => {
const session = createMockSession({
authState: { key: { id: 123 }, success: true },
sessionId: "sess_abc123_def456",
providerType: "claude",
});
const message = { messages: [] };
const result = injectClaudeMetadataUserId(message, session);
expect(result.metadata).toBeDefined();
expect(result.metadata.user_id).toMatch(/^user_[a-f0-9]{64}_account__session_sess_abc123_def456$/);
});
it("should preserve existing user_id", () => {
const session = createMockSession({
authState: { key: { id: 123 }, success: true },
sessionId: "sess_abc123_def456",
});
const message = {
messages: [],
metadata: { user_id: "existing_user_123" }
};
const result = injectClaudeMetadataUserId(message, session);
expect(result.metadata.user_id).toBe("existing_user_123");
});
it("should skip injection when keyId is missing", () => {
const session = createMockSession({
authState: { key: null, success: true },
sessionId: "sess_abc123_def456",
});
const message = { messages: [] };
const result = injectClaudeMetadataUserId(message, session);
expect(result.metadata).toBeUndefined();
});
it("should skip injection when sessionId is missing", () => {
const session = createMockSession({
authState: { key: { id: 123 }, success: true },
sessionId: null,
});
const message = { messages: [] };
const result = injectClaudeMetadataUserId(message, session);
expect(result.metadata).toBeUndefined();
});
it("should generate stable hash for same keyId", () => {
const keyId = 123;
const hash1 = crypto.createHash("sha256").update(`claude_user_${keyId}`).digest("hex");
const hash2 = crypto.createHash("sha256").update(`claude_user_${keyId}`).digest("hex");
expect(hash1).toBe(hash2);
expect(hash1).toHaveLength(64);
});
});
describe("generateDeterministicSessionId format", () => {
it("should match SessionManager.generateSessionId format", () => {
const session = createMockSession({
headers: new Headers({
"x-api-key": "sk-ant-1234567890",
"user-agent": "test-agent",
"x-forwarded-for": "1.2.3.4",
}),
});
const sessionId = session.generateDeterministicSessionId();
// 格式:sess_{8hex}_{12hex}
expect(sessionId).toMatch(/^sess_[a-f0-9]{8}_[a-f0-9]{12}$/);
});
});Review Coverage
- Logic and correctness - Clean
- Security (OWASP Top 10) - Clean
- Error handling - Clean (defensive checks + global error handler)
- Type safety - Clean
- Documentation accuracy - Clean
- Test coverage - Missing (违反 CLAUDE.md 规则 #2)
- Code clarity - Good
Automated review by Claude AI
| * - sessionId: 当前请求的 session ID | ||
| * | ||
| * 注意:如果请求体中已存在 metadata.user_id,则保持原样不修改 | ||
| */ |
There was a problem hiding this comment.
[HIGH] [TEST-MISSING-CRITICAL] 缺少单元测试覆盖
Why this is a problem: 根据 CLAUDE.md 规则 #2:"Test Coverage - All new features must have unit test coverage of at least 80%"。新增的 injectClaudeMetadataUserId() 函数缺少测试,无法验证关键场景。
Suggested fix: 创建测试文件 tests/unit/proxy/proxy-forwarder-metadata-injection.test.ts 覆盖以下场景:
- 正常注入 user_id
- 保留已存在的 user_id
- keyId/sessionId 缺失时跳过注入
- 哈希稳定性验证
- 只对 claude/claude-auth 提供商注入
Confidence: 95/100
| // 取前 32 位作为稳定 ID,避免过长 | ||
| return `sess_${hash.substring(0, 32)}`; | ||
| // 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致 | ||
| return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`; |
There was a problem hiding this comment.
[INFO] Session ID 格式统一 ✓
此变更将 generateDeterministicSessionId() 的格式从 sess_{32hex} 改为 sess_{8hex}_{12hex},与 SessionManager.generateSessionId() 保持一致。
验证:
- SessionManager 格式:
sess_{timestamp.toString(36)}_{randomBytes(6).hex}≈sess_{8-9}_{12} - 新的确定性格式:
sess_{hash[0:8]}_{hash[8:20]}=sess_{8}_{12}
格式现已统一,有助于下游系统的 session ID 匹配和处理。
建议: 虽然这是格式修正,但仍建议添加测试验证格式一致性(参见 forwarder.ts 的测试建议)。
| typeof message.metadata === "object" && message.metadata !== null | ||
| ? (message.metadata as Record<string, unknown>) | ||
| : undefined; | ||
| if (existingMetadata?.user_id !== undefined && existingMetadata?.user_id !== null) { |
There was a problem hiding this comment.
Preserving empty string "" for user_id prevents cache injection. Consider documenting this design decision in the comment above if intentional.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 220:220
Comment:
Preserving empty string `""` for `user_id` prevents cache injection. Consider documenting this design decision in the comment above if intentional.
How can I resolve this? If you propose a fix, please make it concise.e7c9dcb to
eeb358a
Compare
PR #729 Review Fix Report修改内容1)
|
src/app/v1/_lib/proxy/forwarder.ts
Outdated
| let messageToSend = session.request.message; | ||
| if (provider.providerType === "claude" || provider.providerType === "claude-auth") { | ||
| messageToSend = injectClaudeMetadataUserId( |
There was a problem hiding this comment.
Injected metadata may be filtered
injectClaudeMetadataUserId() runs before filterPrivateParameters(), which strips underscore-prefixed keys. Since this injects metadata.user_id, the injected field will be removed and never reach the upstream provider, defeating the cache-key goal. Consider using the provider-expected field name (if it should be user_id, don’t filter it) or adjusting filterPrivateParameters() to preserve metadata.user_id.
Also appears in filterPrivateParameters() usage at src/app/v1/_lib/proxy/forwarder.ts:165-199 (underscore stripping logic).
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 1616:1618
Comment:
**Injected metadata may be filtered**
`injectClaudeMetadataUserId()` runs before `filterPrivateParameters()`, which strips underscore-prefixed keys. Since this injects `metadata.user_id`, the injected field will be removed and never reach the upstream provider, defeating the cache-key goal. Consider using the provider-expected field name (if it should be `user_id`, don’t filter it) or adjusting `filterPrivateParameters()` to preserve `metadata.user_id`.
Also appears in `filterPrivateParameters()` usage at `src/app/v1/_lib/proxy/forwarder.ts:165-199` (underscore stripping logic).
How can I resolve this? If you propose a fix, please make it concise.
请问能否解释下在什么情况下会出现这个问题? |
claude code客户端经过cch中转 ->正常缓存 |
由于涉及到了修改请求体,建议将这个功能放在特殊设置管线,以类似现在 Codex 自动补全 Session ID 的方式,在系统设置内提供开关,并在日志中记录修改命中情况。 |
add system setting enableClaudeMetadataUserIdInjection with full config pipeline and UI toggle move injection after private parameter filtering and persist audit special settings for hit/skip reasons add drizzle migration for enable_claude_metadata_user_id_injection
| const hash = crypto.createHash("sha256").update(parts.join(":"), "utf8").digest("hex"); | ||
| // 取前 32 位作为稳定 ID,避免过长 | ||
| return `sess_${hash.substring(0, 32)}`; | ||
| // 格式对齐为 sess_{8位}_{12位} |
There was a problem hiding this comment.
Comment says format aligns with SessionManager.generateSessionId(), but they're not identical. SessionManager.generateSessionId() uses Date.now().toString(36) (variable 8-9 chars base36 timestamp) + 12 hex random, while this uses fixed 8 hex + 12 hex from SHA-256 hash. Update comment to clarify they're similar format but different generation methods.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/session.ts
Line: 383:383
Comment:
Comment says format aligns with `SessionManager.generateSessionId()`, but they're not identical. `SessionManager.generateSessionId()` uses `Date.now().toString(36)` (variable 8-9 chars base36 timestamp) + 12 hex random, while this uses fixed 8 hex + 12 hex from SHA-256 hash. Update comment to clarify they're similar format but different generation methods.
How can I resolve this? If you propose a fix, please make it concise.| it("metadata.user_id 为空字符串时应保持原样不注入", () => { | ||
| const message: Record<string, unknown> = { | ||
| metadata: { | ||
| user_id: "", | ||
| }, | ||
| }; | ||
| const session = createSession(42, "sess_abc123"); | ||
|
|
||
| const result = injectClaudeMetadataUserId(message, session); | ||
|
|
||
| expect(result).toBe(message); | ||
| expect((result.metadata as Record<string, unknown>).user_id).toBe(""); |
There was a problem hiding this comment.
Test documents that empty string "" for user_id prevents injection, treating it as "already exists". Verify this matches product requirements - should empty strings be preserved or replaced with valid IDs for proper cache behavior?
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/proxy/metadata-injection.test.ts
Line: 51:62
Comment:
Test documents that empty string `""` for `user_id` prevents injection, treating it as "already exists". Verify this matches product requirements - should empty strings be preserved or replaced with valid IDs for proper cache behavior?
How can I resolve this? If you propose a fix, please make it concise.|
已按你建议完成 v2 重构并推送: 这版已落地你提到的 3 点:
另外已补 DB migration: Please take another look when convenient. If anything else is needed, I can follow up quickly. |
|
可能是我还没完全搞明白机制 不过 难道不能通过创建并开启一个专门的 request filter 规则来实现这种替换吗? |
…zle 0063 migration
| const keyId = session.authState?.key?.id; | ||
| const sessionId = session.sessionId; | ||
|
|
There was a problem hiding this comment.
AuthState shape mismatch
injectClaudeMetadataUserId() reads session.authState?.key?.id, but ProxySession.authState is AuthState | null where the key lives at authState.key (not authState.key.id). With a real AuthState object this will always evaluate to undefined, causing injection to be skipped even when authenticated. This will also make the unit tests misleading since they mock a different shape. Adjust to session.authState?.key?.id -> session.authState?.key?.id only if key is nested, otherwise use session.authState?.key?.id? (In this repo it should be session.authState?.key?.id => session.authState?.key?.id is same; but here authState?.key?.id is wrong because key is not under authState.key? Please align with src/app/v1/_lib/proxy/session.ts’s AuthState interface.)
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/app/v1/_lib/proxy/forwarder.ts
Line: 270:272
Comment:
**AuthState shape mismatch**
`injectClaudeMetadataUserId()` reads `session.authState?.key?.id`, but `ProxySession.authState` is `AuthState | null` where the key lives at `authState.key` (not `authState.key.id`). With a real `AuthState` object this will always evaluate to `undefined`, causing injection to be skipped even when authenticated. This will also make the unit tests misleading since they mock a different shape. Adjust to `session.authState?.key?.id` -> `session.authState?.key?.id` only if `key` is nested, otherwise use `session.authState?.key?.id`? (In this repo it should be `session.authState?.key?.id` => `session.authState?.key?.id` is same; but here `authState?.key?.id` is wrong because key is not under `authState.key`? Please align with `src/app/v1/_lib/proxy/session.ts`’s `AuthState` interface.)
How can I resolve this? If you propose a fix, please make it concise.| keyId: number | null | undefined = 123, | ||
| sessionId: string | null | undefined = "sess_test" | ||
| ): ProxySession { | ||
| const session = Object.create(ProxySession.prototype) as ProxySession; | ||
| (session as Record<string, unknown>).authState = | ||
| keyId === undefined ? undefined : { key: { id: keyId } }; | ||
| (session as Record<string, unknown>).sessionId = sessionId ?? null; | ||
| return session; | ||
| } | ||
|
|
There was a problem hiding this comment.
Mocked authState is wrong
createSession() sets session.authState = { key: { id: keyId } }, but ProxySession.AuthState requires { user, key, apiKey, success } and the proxy code relies on the real shape. As written, the tests can pass even if production injection never runs (because it’s reading a different field path). Use a real AuthState object shape in the mock (including success: true, key: { id: ... }, etc.) so the test exercises the same code path used in production.
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/unit/proxy/metadata-injection.test.ts
Line: 6:15
Comment:
**Mocked authState is wrong**
`createSession()` sets `session.authState = { key: { id: keyId } }`, but `ProxySession.AuthState` requires `{ user, key, apiKey, success }` and the proxy code relies on the real shape. As written, the tests can pass even if production injection never runs (because it’s reading a different field path). Use a real `AuthState` object shape in the mock (including `success: true`, `key: { id: ... }`, etc.) so the test exercises the same code path used in production.
How can I resolve this? If you propose a fix, please make it concise.| @@ -0,0 +1 @@ | |||
| ALTER TABLE "system_settings" ADD COLUMN "enable_claude_metadata_user_id_injection" boolean DEFAULT true NOT NULL; No newline at end of file | |||
There was a problem hiding this comment.
Missing trailing newline
This migration file ends without a newline (\ No newline at end of file). Some tooling (formatters/linters, concatenation in scripts) expects a terminating newline; add one to avoid noisy diffs and occasional parsing issues.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: drizzle/0063_slippery_sharon_carter.sql
Line: 1:1
Comment:
**Missing trailing newline**
This migration file ends without a newline (`\ No newline at end of file`). Some tooling (formatters/linters, concatenation in scripts) expects a terminating newline; add one to avoid noisy diffs and occasional parsing issues.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
How can I resolve this? If you propose a fix, please make it concise.|
Follow-up (2026-02-08 03:15 GMT+8): branch is up-to-date with dev (behind 0), checks are green (CodeRabbit, Greptile Review), and current review decision is APPROVED. @ding113 this should be ready to merge when you are available. |
|
[pr729-followup-2026-02-08]
Given this PR is merged, I am not force-pushing additional changes here. Suggested follow-up is a dedicated patch PR for the active bot-raised items (especially authState shape validation + metadata empty-string behavior) if maintainers decide they are still relevant. |
Summary
Inject
metadata.user_idinto Claude API requests and unifysession_idformat to enable proper prompt caching on third-party relay providers.为 Claude API 请求自动注入
metadata.user_id,并统一session_id格式,使第三方中转站能正确命中 prompt cache。Problem
When using Claude API through third-party relay providers (中转站), prompt caching may not work correctly if the request lacks a
metadata.user_idfield. This is because:metadata.user_idas part of the cache key associationuser_id, the relay provider cannot maintain cache continuity across requests from the same user/session — resulting in cache misses and increased costsAdditionally, the
session_idformat generated bygenerateDeterministicSessionId()(sess_{32hex}) was inconsistent with the format used bySessionManager.generateSessionId()(sess_{8hex}_{12hex}), which could cause downstream matching issues.当通过第三方中转站使用 Claude API 时,如果请求缺少
metadata.user_id字段,prompt cache 可能无法正确命中。原因:metadata.user_id作为缓存关联键的一部分user_id时,中转站无法维持同一用户/会话的缓存连续性,导致 cache miss、成本上升此外,
generateDeterministicSessionId()生成的格式(sess_{32位hex})与SessionManager.generateSessionId()的格式(sess_{8位hex}_{12位hex})不一致,可能导致下游匹配问题。Solution / 解决方案
1. Auto-inject
metadata.user_idfor Claude requests / 自动注入 Claude 请求的metadata.user_idFor requests targeting
claudeorclaude-authprovider types, automatically injectmetadata.user_idwith the format:对目标为
claude或claude-auth类型的 provider 请求,自动注入metadata.user_id,格式:metadata.user_idalready exists in the request, it is preserved as-is / 如果请求中已存在metadata.user_id,保持原样不覆盖2. Unify
session_idformat / 统一session_id格式Changed
generateDeterministicSessionId()output fromsess_{32hex}tosess_{8hex}_{12hex}, matching the format used bySessionManager.generateSessionId().将
generateDeterministicSessionId()的输出格式从sess_{32位hex}改为sess_{8位hex}_{12位hex},与SessionManager.generateSessionId()保持一致。Changes / 改动
Proxy Pipeline / 代理管线
src/app/v1/_lib/proxy/forwarder.tsinjectClaudeMetadataUserId()— generates stableuser_idfrom API Key ID hash + session ID / 新增injectClaudeMetadataUserId()— 基于 API Key ID 哈希 + session ID 生成稳定的user_idfilterPrivateParameters()forclaude/claude-authproviders / 集成到请求管线:在filterPrivateParameters()之前对claude/claude-auth类型执行cryptoimport for SHA-256 hashing / 新增crypto导入用于 SHA-256 哈希Session ID / 会话 ID
src/app/v1/_lib/proxy/session.tsgenerateDeterministicSessionId()format fromsess_{hash[0:32]}tosess_{hash[0:8]}_{hash[8:20]}/ 将格式从sess_{hash[0:32]}改为sess_{hash[0:8]}_{hash[8:20]}Test Plan
metadata.user_id→ verify it is injected with correct formatmetadata.user_id→ verify it is preservedsession_idformat matchessess_{8}_{12}patternGreptile Overview
Greptile Summary
This PR adds optional support for injecting
metadata.user_idinto Claude/Claude-auth upstream requests (to improve relay prompt-cache hit rate) and adds a system setting + UI toggle to control that behavior. It also adjustsProxySession.generateDeterministicSessionId()output formatting and introduces unit tests for the injection helper and deterministic session id format.The changes span the proxy forwarder pipeline (
src/app/v1/_lib/proxy/forwarder.ts), persistence/audit plumbing viaSpecialSetting, and the system settings stack (Drizzle migration/schema, repository + cache + validation, settings page + i18n strings).Confidence Score: 2/5
authState.key.idpath, while the unit tests mock a different authState shape and can still pass. There is also a minor migration formatting issue (missing newline).Important Files Changed
Sequence Diagram
sequenceDiagram autonumber participant Client participant Proxy as claude-code-hub ProxyForwarder participant Settings as SystemSettingsCache participant Sess as ProxySession participant DB as SessionManager/Repo participant Upstream as Claude Provider Client->>Proxy: HTTP request (message JSON) Proxy->>Sess: Build ProxySession (authState, sessionId, requestSequence) Proxy->>Proxy: filterPrivateParameters(message) alt providerType is claude/claude-auth Proxy->>Settings: getCachedSystemSettings() Settings-->>Proxy: enableClaudeMetadataUserIdInjection alt injection enabled AND missing metadata.user_id AND has keyId+sessionId Proxy->>Proxy: injectClaudeMetadataUserId(filteredMessage, session) Proxy->>Sess: addSpecialSetting(audit) Proxy->>DB: storeSessionSpecialSettings(sessionId, settings, seq) Proxy->>DB: updateMessageRequestDetails(messageContext.id, settings) else skip injection Proxy->>Sess: (no message mutation) end end Proxy->>Upstream: Forward request with body JSON Upstream-->>Proxy: Response/stream Proxy-->>Client: Response/stream