Skip to content

feat: inject Claude metadata.user_id for relay provider cache support#729

Merged
ding113 merged 4 commits intoding113:devfrom
ProgramCaiCai:feat/inject-claude-metadata-userid
Feb 8, 2026
Merged

feat: inject Claude metadata.user_id for relay provider cache support#729
ding113 merged 4 commits intoding113:devfrom
ProgramCaiCai:feat/inject-claude-metadata-userid

Conversation

@ProgramCaiCai
Copy link
Contributor

@ProgramCaiCai ProgramCaiCai commented Feb 7, 2026

Summary

Inject metadata.user_id into Claude API requests and unify session_id format 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_id field. This is because:

  1. Anthropic's prompt caching uses metadata.user_id as part of the cache key association
  2. Native Claude Code clients automatically populate this field, but requests proxied through claude-code-hub do not
  3. Without a stable user_id, the relay provider cannot maintain cache continuity across requests from the same user/session — resulting in cache misses and increased costs

Additionally, the session_id format generated by generateDeterministicSessionId() (sess_{32hex}) was inconsistent with the format used by SessionManager.generateSessionId() (sess_{8hex}_{12hex}), which could cause downstream matching issues.

当通过第三方中转站使用 Claude API 时,如果请求缺少 metadata.user_id 字段,prompt cache 可能无法正确命中。原因:

  1. Anthropic 的 prompt caching 机制将 metadata.user_id 作为缓存关联键的一部分
  2. 原生 Claude Code 客户端会自动填充该字段,但经 claude-code-hub 代理的请求不会
  3. 缺少稳定的 user_id 时,中转站无法维持同一用户/会话的缓存连续性,导致 cache miss、成本上升

此外,generateDeterministicSessionId() 生成的格式(sess_{32位hex})与 SessionManager.generateSessionId() 的格式(sess_{8位hex}_{12位hex})不一致,可能导致下游匹配问题。

Solution / 解决方案

1. Auto-inject metadata.user_id for Claude requests / 自动注入 Claude 请求的 metadata.user_id

For requests targeting claude or claude-auth provider types, automatically inject metadata.user_id with the format:

对目标为 claudeclaude-auth 类型的 provider 请求,自动注入 metadata.user_id,格式:

user_{sha256(apiKeyId)}_account__session_{sessionId}
  • Stable hash / 稳定哈希: Derived from the API Key ID via SHA-256, ensuring the same key always produces the same user identifier / 基于 API Key ID 的 SHA-256 生成,同一 key 始终产生相同标识
  • Session binding / 会话绑定: Appends the current session ID for per-session cache isolation / 附加当前 session ID,实现按会话隔离缓存
  • Non-destructive / 非破坏性: If metadata.user_id already exists in the request, it is preserved as-is / 如果请求中已存在 metadata.user_id,保持原样不覆盖

2. Unify session_id format / 统一 session_id 格式

Changed generateDeterministicSessionId() output from sess_{32hex} to sess_{8hex}_{12hex}, matching the format used by SessionManager.generateSessionId().

generateDeterministicSessionId() 的输出格式从 sess_{32位hex} 改为 sess_{8位hex}_{12位hex},与 SessionManager.generateSessionId() 保持一致。

Changes / 改动

Proxy Pipeline / 代理管线

  • src/app/v1/_lib/proxy/forwarder.ts
    • Added injectClaudeMetadataUserId() — generates stable user_id from API Key ID hash + session ID / 新增 injectClaudeMetadataUserId() — 基于 API Key ID 哈希 + session ID 生成稳定的 user_id
    • Integrated into request pipeline: runs before filterPrivateParameters() for claude/claude-auth providers / 集成到请求管线:在 filterPrivateParameters() 之前对 claude/claude-auth 类型执行
    • Added crypto import for SHA-256 hashing / 新增 crypto 导入用于 SHA-256 哈希

Session ID / 会话 ID

  • src/app/v1/_lib/proxy/session.ts
    • Changed generateDeterministicSessionId() format from sess_{hash[0:32]} to sess_{hash[0:8]}_{hash[8:20]} / 将格式从 sess_{hash[0:32]} 改为 sess_{hash[0:8]}_{hash[8:20]}

Test Plan

  • TypeScript type check passes
  • Production build succeeds
  • Manual test: Send Claude request without metadata.user_id → verify it is injected with correct format
  • Manual test: Send Claude request with existing metadata.user_id → verify it is preserved
  • Manual test: Verify prompt caching works on relay provider after injection
  • Manual test: Confirm session_id format matches sess_{8}_{12} pattern

Greptile Overview

Greptile Summary

This PR adds optional support for injecting metadata.user_id into 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 adjusts ProxySession.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 via SpecialSetting, and the system settings stack (Drizzle migration/schema, repository + cache + validation, settings page + i18n strings).

Confidence Score: 2/5

  • Not safe to merge until the injection code and its tests are aligned with the real ProxySession.authState shape.
  • The core feature (metadata.user_id injection) appears to be skipped in production due to reading a non-existent authState.key.id path, while the unit tests mock a different authState shape and can still pass. There is also a minor migration formatting issue (missing newline).
  • src/app/v1/_lib/proxy/forwarder.ts, tests/unit/proxy/metadata-injection.test.ts, drizzle/0063_slippery_sharon_carter.sql

Important Files Changed

Filename Overview
drizzle/0063_slippery_sharon_carter.sql Adds system_settings.enable_claude_metadata_user_id_injection boolean column default true; file lacks trailing newline.
drizzle/meta/0063_snapshot.json Drizzle snapshot updated to include new system setting column; large auto-generated metadata change.
drizzle/meta/_journal.json Drizzle journal updated for new migration entry; auto-generated metadata change.
messages/en/settings/config.json Adds i18n strings for Claude metadata.user_id injection toggle and description.
messages/ja/settings/config.json Adds Japanese i18n strings for Claude metadata.user_id injection toggle.
messages/ru/settings/config.json Adds Russian i18n strings for Claude metadata.user_id injection toggle.
messages/zh-CN/settings/config.json Adds zh-CN i18n strings for Claude metadata.user_id injection toggle.
messages/zh-TW/settings/config.json Adds zh-TW i18n strings for Claude metadata.user_id injection toggle.
src/actions/system-config.ts Plumbs enableClaudeMetadataUserIdInjection through saveSystemSettings action validation/update payload.
src/app/[locale]/settings/config/_components/system-settings-form.tsx Adds UI switch state/submit wiring for enableClaudeMetadataUserIdInjection setting.
src/app/[locale]/settings/config/page.tsx Includes enableClaudeMetadataUserIdInjection in settings page initialSettings mapping.
src/app/v1/_lib/proxy/forwarder.ts Adds Claude metadata.user_id injection after private-parameter filtering, plus audit persistence to session/message. Needs fixes: injection uses session.authState without checking .success (can throw), and persistSpecialSettings is invoked during request send even though it writes DB per request and may run before messageContext exists.
src/app/v1/_lib/proxy/session.ts Changes deterministic session id format to sess_{8hex}_{12hex}; aligns pattern but not generation method with SessionManager (comment threads already noted).
src/drizzle/schema.ts Adds enableClaudeMetadataUserIdInjection column to systemSettings table schema with default true.
src/lib/config/system-settings-cache.ts Adds enableClaudeMetadataUserIdInjection to cached system settings default and returned settings.
src/lib/session-manager.ts Updates comments describing typical metadata.user_id format; extraction logic unchanged.
src/lib/utils/special-settings.ts Adds stable key builder case for claude_metadata_user_id_injection special setting.
src/lib/validation/schemas.ts Extends UpdateSystemSettingsSchema with enableClaudeMetadataUserIdInjection boolean optional.
src/repository/_shared/transformers.ts Maps enableClaudeMetadataUserIdInjection from DB settings to SystemSettings with default true.
src/repository/system-config.ts Adds enableClaudeMetadataUserIdInjection to fallback settings, getSystemSettings, and update payload handling.
src/types/special-settings.ts Defines ClaudeMetadataUserIdInjectionSpecialSetting audit type and adds to SpecialSetting union.
src/types/system-config.ts Adds enableClaudeMetadataUserIdInjection to SystemSettings and UpdateSystemSettingsInput types.
tests/unit/proxy/metadata-injection.test.ts Adds unit tests for metadata.user_id injection and deterministic session id format; tests rely on ProxySession prototype mocking.

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
Loading

- 修改 generateDeterministicSessionId() 生成格式为 sess_{8位}_{12位}
- 为 Claude 请求自动注入 metadata.user_id(格式:user_{hash}_account__session_{sessionId})
- user hash 基于 API Key ID 生成,保持稳定
- 如果已存在 metadata.user_id 则保持原样
@coderabbitai
Copy link

coderabbitai bot commented Feb 7, 2026

📝 Walkthrough

总体描述

该PR为Claude请求添加元数据user_id注入功能。在用户消息缺少metadata.user_id时,系统自动生成并注入稳定的user_id标识符(格式为user_{hash}account__session{sessionId})。包括核心请求转发逻辑、数据库模式更新、系统设置集成和多语言配置。

变更表

内聚组 / 文件 摘要
代理请求处理逻辑
src/app/v1/_lib/proxy/forwarder.ts, src/app/v1/_lib/proxy/session.ts
在forwarder中添加injectClaudeMetadataUserId()函数,实现基于SHA-256的稳定user_id生成、元数据注入和审计持久化。调整session ID格式输出为sess_<8hex>_<12hex>
测试
tests/unit/proxy/metadata-injection.test.ts
为元数据注入和确定性session ID生成添加全面的单元测试,覆盖注入行为、缺失字段处理、哈希一致性和边界情况。
数据库迁移与模式
drizzle/schema.ts, drizzle/0063_slippery_sharon_carter.sql, drizzle/meta/_journal.json, drizzle/meta/0063_snapshot.json
system_settings表添加enableClaudeMetadataUserIdInjection布尔字段(默认为true)。包含SQL迁移、Drizzle元数据和完整模式快照。
多语言配置
messages/{en,ja,ru,zh-CN,zh-TW}/settings/config.json
为5种语言添加两个新配置键:enableClaudeMetadataUserIdInjection(功能开关)和对应的描述文本,说明自动user_id注入行为。
系统设置集成
src/lib/config/system-settings-cache.ts, src/lib/validation/schemas.ts, src/repository/system-config.ts, src/types/system-config.ts
在系统设置类型、验证架构、缓存配置和数据库仓储中添加enableClaudeMetadataUserIdInjection字段,实现完整的读写路径。
UI与操作处理
src/actions/system-config.ts, src/app/[locale]/settings/config/_components/system-settings-form.tsx, src/app/[locale]/settings/config/page.tsx
在系统设置表单中添加新的功能开关UI元素,支持启用/禁用Claude元数据注入功能,并集成保存和状态刷新流程。
特殊设置与工具
src/types/special-settings.ts, src/lib/utils/special-settings.ts, src/repository/_shared/transformers.ts, src/lib/session-manager.ts
添加ClaudeMetadataUserIdInjectionSpecialSetting类型用于审计,在buildSettingKey中添加序列化逻辑,更新变换器和会话管理文档。

预估代码审查工作量

🎯 3 (Moderate) | ⏱️ ~25 minutes

可能相关的PR

建议的审查者

  • ding113
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately describes the main change: injecting Claude metadata.user_id and enabling relay provider prompt cache support through session_id format unification.
Description check ✅ Passed The pull request description is comprehensive and directly related to the changeset, explaining the problem, solution, and specific implementation details with both English and Chinese documentation.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @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 metadata.user_id is automatically included in relevant requests, which is critical for Anthropic's caching mechanism. Concurrently, it unifies the format of generated session_ids to eliminate inconsistencies that could lead to cache misses or other downstream problems. These adjustments are designed to optimize cache hit rates and potentially reduce operational costs for users.

Highlights

  • Claude metadata.user_id Injection: Automatically injects a stable metadata.user_id into Claude API requests for claude and claude-auth provider types. This user_id is derived from a SHA-256 hash of the API Key ID and the current session ID, ensuring consistency for prompt caching with third-party relay providers. The injection is non-destructive, preserving any user_id that already exists in the request.
  • session_id Format Unification: Standardizes the format of session_id generated by generateDeterministicSessionId() from sess_{32hex} to sess_{8hex}_{12hex}. This change aligns its format with SessionManager.generateSessionId(), preventing potential downstream matching issues and improving consistency.
Changelog
  • src/app/v1/_lib/proxy/forwarder.ts
    • Imported the node:crypto module to enable cryptographic hashing.
    • Added a new function, injectClaudeMetadataUserId, responsible for generating a unique user_id based on the API Key ID and session ID, and injecting it into the metadata field of Claude API requests.
    • Integrated injectClaudeMetadataUserId into the request processing pipeline within the ProxyForwarder.send method, specifically for claude and claude-auth provider types, ensuring it executes before private parameters are filtered.
  • src/app/v1/_lib/proxy/session.ts
    • Modified the generateDeterministicSessionId function to change the output format of the session ID. It now produces a session ID in the format sess_{8hex}_{12hex}, aligning with the format used by SessionManager.generateSessionId().
Activity
  • No human activity (comments, reviews, etc.) has been recorded on this pull request yet.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions bot added enhancement New feature or request area:provider area:session size/XS Extra Small PR (< 50 lines) labels Feb 7, 2026
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

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:

  1. The session ID format unification isn't fully achieved, as the format generated by generateDeterministicSessionId still differs from SessionManager.generateSessionId. The code comment regarding this is also inaccurate.
  2. The logic for injecting metadata.user_id could be more robust to handle cases where message.metadata is 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.

Comment on lines 215 to 243
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,
};
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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,
  };

Comment on lines 383 to 384
// 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致
return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

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.

Suggested change
// 格式: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)}`;

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 383 to 384
// 格式:sess_{8位}_{12位},与 SessionManager.generateSessionId() 保持一致
return `sess_${hash.substring(0, 8)}_${hash.substring(8, 20)}`;
Copy link

Choose a reason for hiding this comment

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

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

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() 格式变更都缺少测试覆盖,无法验证以下关键场景:

  1. metadata.user_id 已存在时,是否正确跳过注入
  2. keyIdsessionId 缺失时,是否正确返回原始 message
  3. 生成的 user_id 格式是否符合预期
  4. SHA-256 哈希是否稳定(相同 keyId 生成相同 hash)
  5. 只对 claudeclaude-auth 提供商注入
  6. 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,则保持原样不修改
*/
Copy link
Contributor

Choose a reason for hiding this comment

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

[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 覆盖以下场景:

  1. 正常注入 user_id
  2. 保留已存在的 user_id
  3. keyId/sessionId 缺失时跳过注入
  4. 哈希稳定性验证
  5. 只对 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)}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

[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 的测试建议)。

@coderabbitai coderabbitai bot requested a review from ding113 February 7, 2026 03:36
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

typeof message.metadata === "object" && message.metadata !== null
? (message.metadata as Record<string, unknown>)
: undefined;
if (existingMetadata?.user_id !== undefined && existingMetadata?.user_id !== null) {
Copy link

Choose a reason for hiding this comment

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

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.

@ProgramCaiCai ProgramCaiCai force-pushed the feat/inject-claude-metadata-userid branch from e7c9dcb to eeb358a Compare February 7, 2026 03:42
@ProgramCaiCai
Copy link
Contributor Author

PR #729 Review Fix Report

修改内容

1) src/app/v1/_lib/proxy/forwarder.ts

  • 修复 metadata 类型防御:读取 message.metadata 时增加对象类型检查(typeof message.metadata === 'object' && message.metadata !== null
  • 修复 existingMetadata?.user_id 的 truthiness 判断问题:改为仅在 user_id !== undefined && user_id !== null 时跳过注入
  • 修复 keyId 判空问题:由 !keyId 改为 keyId == null,避免 keyId = 0 被误判
  • 按最小改动方案 A 导出函数:injectClaudeMetadataUserId 增加 export,并添加 @internal 注释,供单测直接导入

2) src/app/v1/_lib/proxy/session.ts

  • 修正文案注释:将“与 SessionManager.generateSessionId() 保持一致”改为“格式对齐为 sess_{8位}_{12位}”

3) 新增测试 tests/unit/proxy/metadata-injection.test.ts

覆盖以下场景:

  1. 无 metadata 时正确注入 user_id
  2. 已有 metadata.user_id 时不覆盖
  3. metadata.user_id 为空字符串时不覆盖
  4. keyId 缺失时跳过注入并返回原始 message
  5. sessionId 缺失时跳过注入
  6. 相同 keyId 生成相同 hash(稳定性)
  7. 不同 keyId 生成不同 hash
  8. metadata 为非对象类型时安全处理
  9. generateDeterministicSessionId 输出格式匹配 sess_{8hex}_{12hex}

测试结果

已执行:

npx vitest run tests/unit/proxy/metadata-injection.test.ts

结果:

  • Test Files: 1 passed
  • Tests: 9 passed
  • 结论:全部通过

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines 1616 to 1618
let messageToSend = session.request.message;
if (provider.providerType === "claude" || provider.providerType === "claude-auth") {
messageToSend = injectClaudeMetadataUserId(
Copy link

Choose a reason for hiding this comment

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

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.

@ding113
Copy link
Owner

ding113 commented Feb 7, 2026

原生 Claude Code 客户端会自动填充该字段,但经 claude-code-hub 代理的请求不会

请问能否解释下在什么情况下会出现这个问题?

@ProgramCaiCai
Copy link
Contributor Author

ProgramCaiCai commented Feb 7, 2026

原生 Claude Code 客户端会自动填充该字段,但经 claude-code-hub 代理的请求不会

请问能否解释下在什么情况下会出现这个问题?

claude code客户端经过cch中转 ->正常缓存
非claude code客户端(例如openclaw)-> 本来就无法缓存 ->中转之后也无法缓存 【服务商foxcode】
只有仿造claude code包体的格式 修复之后就能正确缓存

@ding113
Copy link
Owner

ding113 commented Feb 7, 2026

原生 Claude Code 客户端会自动填充该字段,但经 claude-code-hub 代理的请求不会

请问能否解释下在什么情况下会出现这个问题?

claude code客户端经过cch中转 ->正常缓存 非claude code客户端(例如openclaw)-> 本来就无法缓存 ->中转之后也无法缓存 【服务商foxcode】 只有仿造claude code包体的格式 修复之后就能正确缓存

由于涉及到了修改请求体,建议将这个功能放在特殊设置管线,以类似现在 Codex 自动补全 Session ID 的方式,在系统设置内提供开关,并在日志中记录修改命中情况。

@ding113 ding113 changed the base branch from main to dev February 7, 2026 04:15
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
Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

const hash = crypto.createHash("sha256").update(parts.join(":"), "utf8").digest("hex");
// 取前 32 位作为稳定 ID,避免过长
return `sess_${hash.substring(0, 32)}`;
// 格式对齐为 sess_{8位}_{12位}
Copy link

Choose a reason for hiding this comment

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

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.

Comment on lines +51 to +62
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("");
Copy link

Choose a reason for hiding this comment

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

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.

@ProgramCaiCai
Copy link
Contributor Author

已按你建议完成 v2 重构并推送:e76dc573

这版已落地你提到的 3 点:

  1. 增加系统开关 enableClaudeMetadataUserIdInjection(默认 true
  2. 注入位置移到 filterPrivateParameters() 之后
  3. 增加审计与日志:specialSettings + logger.info(含 injected/skipped 与原因)

另外已补 DB migration:drizzle/0062_exotic_kingpin.sql
相关回归测试通过:33/33。

Please take another look when convenient. If anything else is needed, I can follow up quickly.

@miraserver
Copy link
Contributor

可能是我还没完全搞明白机制 不过 难道不能通过创建并开启一个专门的 request filter 规则来实现这种替换吗?

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

23 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +270 to +272
const keyId = session.authState?.key?.id;
const sessionId = session.sessionId;

Copy link

Choose a reason for hiding this comment

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

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.

Comment on lines +6 to +15
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;
}

Copy link

Choose a reason for hiding this comment

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

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
Copy link

Choose a reason for hiding this comment

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

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.

@ProgramCaiCai
Copy link
Contributor Author

ProgramCaiCai commented Feb 7, 2026

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.

@ding113 ding113 merged commit 021387f into ding113:dev Feb 8, 2026
2 checks passed
@github-project-automation github-project-automation bot moved this from Backlog to Done in Claude Code Hub Roadmap Feb 8, 2026
@ProgramCaiCai
Copy link
Contributor Author

[pr729-followup-2026-02-08]
Post-merge triage snapshot:

  • PR is already merged into dev (latest head commit: 4ac8516).
  • Checks on the latest commit are green (Greptile Review, CodeRabbit).
  • Review threads: 12 unresolved = 8 active + 4 outdated.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider area:session enhancement New feature or request size/XS Extra Small PR (< 50 lines)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants