Skip to content

fix(claude): bound Responses user to 64 chars for long metadata.user_id - #5706

Closed
giulioleone097 wants to merge 1 commit into
lidge-jun:devfrom
giulioleone097:fix/claude-user-id-length
Closed

giulioleone097 wants to merge 1 commit into
lidge-jun:devfrom
giulioleone097:fix/claude-user-id-length

Conversation

@giulioleone097

@giulioleone097 giulioleone097 commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes #5705.

Claude Code sends metadata.user_id as a JSON string of about 186 characters. anthropicToResponsesTranslation copied it verbatim into the Responses user field. Azure OpenAI, like other OpenAI-compatible Responses backends, caps user at 64 characters, so every Claude Code or Claude Desktop Code-tab request routed to an Azure model failed:

API Error: 400 Invalid 'user': string too long. Expected a string with maximum length 64, but got a string with length 186 instead.

Change in src/claude/inbound.ts:

  • An id of 64 characters or fewer is still forwarded unchanged, so existing behavior and the user-abc assertion stay as they are.
  • A longer id is replaced by its SHA-256 hex digest (64 characters). The block already computed this digest for prompt_cache_key, so the change computes it once and reuses it. prompt_cache_key values are unchanged.

Verification

  • bun test tests/claude-integration/: 778 pass, 2 fail. The two failures are in the intercept local-CA tests ("CA certificate is a self-signed X.509 v3 authority" and "leaf is issued by the CA and names every requested host in SAN"). They fail the same way on unmodified dev (782bfb8) in this environment and are unrelated to this change.
  • New regression test in tests/claude-integration/claude-inbound.test.ts: a metadata.user_id longer than 64 characters produces a 64-character hex user, and prompt_cache_key matches its first 32 characters.
  • bun run typecheck (tsc --noEmit): clean.
  • Manual check on an installed 2.64.0 with the same patch, Claude Code 2.x through the first-party intercept: claude -p --model <slot> returns OK for azure-openai/gpt-6-astra, azure-openai/gpt-5.6-sol and native gpt-5.6-sol. Before the patch the Azure slots returned the 400 above.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed (none needed).
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. The change sends a hash instead of the raw id, so less identifying data goes upstream.

Review readiness checklist

This PR stays in draft until every box below is ticked. Tick all four boxes once the requirements are met:

  • Required local validation passed; commands, results, and any full-suite exception are documented.

  • I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).

  • I resolved all correct Codex and CodeRabbit findings.

  • My PR is ready for review.

Summary by CodeRabbit

  • Bug Fixes
    • Long user IDs are now converted to a supported format when sending requests, while prompt caching continues to use a consistent key.

Claude Code sends metadata.user_id as a ~186-char JSON string. It was copied
verbatim into the Responses 'user' field, which Azure OpenAI and other
OpenAI-compatible backends cap at 64 chars, so every Claude Code request
routed to Azure failed with 400 "Invalid 'user': string too long".

Keep short ids as-is and send the SHA-256 hex digest (already computed for
prompt_cache_key) when the id exceeds 64 chars.

Fixes lidge-jun#5705
@github-actions

Copy link
Copy Markdown
Contributor

✅ Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the bug Something isn't working label Sep 23, 2026
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: lidge-jun/opencodex/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 341cab45-b524-441a-b46a-220e6efe5011

📥 Commits

Reviewing files that changed from the base of the PR and between 782bfb8 and ec9702d.

📒 Files selected for processing (2)
  • src/claude/inbound.ts
  • tests/claude-integration/claude-inbound.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The Claude inbound translation now hashes metadata user IDs longer than 64 characters before placing them in body.user. A test checks the digest length and the related prompt cache key.

Changes

Claude request translation

Layer / File(s) Summary
Bound metadata user IDs
src/claude/inbound.ts, tests/claude-integration/claude-inbound.test.ts
At src/claude/inbound.ts:445–454, the translation uses the raw metadata.user_id when its length is at most 64 characters; otherwise it uses the full SHA-256 hex digest. The prompt cache key remains the first 32 digest characters. At tests/claude-integration/claude-inbound.test.ts:522–531, a test checks the 64-character body.user digest and that the prompt cache key matches its first 32 characters.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to ec970

Long Claude user IDs are bounded in the Responses payload, while short IDs and prompt-cache keys retain their existing behavior. No actionable merge risk is established by the reviewed changes.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: limiting the Responses user value to 64 characters when metadata.user_id is long.
Linked Issues check ✅ Passed PR #5706 satisfies the coding requirements in issue #5705. In src/claude/inbound.ts, anthropicToResponsesTranslation keeps metadata.user_id unchanged when its length is 64 characters or less. It…
Out of Scope Changes check ✅ Passed The reviewed changes stay within issue #5705. The source change updates only the metadata.user_id to Responses user mapping in src/claude/inbound.ts. The test change in `tests/claude-integration…
  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR

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.

@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

⏳ DRAFT

  • review readiness checklist open (0/4 boxes ticked).

What to do

  • Tick all four boxes in the PR description once you're done (currently 0/4).

Review readiness checklist

  • ⬜ Required local validation passed; commands, results, and any full-suite exception are documented.
  • ⬜ I pushed my PR to a recent dev commit (at most 10 behind; a maintainer may still ask for the exact tip before merge).
  • ⬜ I resolved all correct Codex and CodeRabbit findings.
  • ⬜ My PR is ready for review.

0/4 boxes ticked.

This PR stays in draft until every box above is ticked.

@github-actions
github-actions Bot marked this pull request as draft September 23, 2026 17:05
@lidge-jun

Copy link
Copy Markdown
Owner

리뷰 · 우선순위 70 / 80

이 PR은 Claude Code가 Azure OpenAI로 요청을 보낼 때 나는 400 에러를 고칩니다. Claude Code는 metadata.user_id에 기기 번호, 계정 번호, 세션 번호가 들어 있는 JSON 문자열을 넣습니다. 길이는 약 186자입니다. 번역이 그 문자열을 Responses의 user에 그대로 넣었습니다. Azure는 user를 64자까지만 받습니다. 그래서 Claude Code와 Claude Desktop 코드 탭에서 Azure 모델로 가는 요청이 전부 거절됐습니다. 같은 프롬프트도 ChatGPT 쪽 슬롯은 됐습니다. 그 쪽은 긴 값을 받기 때문입니다.

고친 곳은 src/claude/inbound.ts입니다. 64자 이하면 예전처럼 그대로 보냅니다. user-abc 같은 짧은 값은 바뀌지 않습니다. 64자를 넘으면 SHA-256 해시의 64글자를 보냅니다. 이 해시는 prompt_cache_key를 만들 때 이미 계산하던 값입니다. 이제 한 번만 계산해서 둘 다 씁니다. 캐시 키는 그 해시의 앞 32자라서, 예전에 쌓인 캐시와 어긋나지 않습니다. 앞 64글자만 자르면 같은 기기의 다른 세션이 한 사용자로 붙을 수 있습니다. 문자열 전체를 해시하는 편이 맞습니다. 베이스는 dev입니다. 아직 드래프트이고 준비 체크는 0/4입니다. #5705를 고치는 다른 열린 PR은 없습니다.

tests/claude-integration/claude-inbound.test.ts:528 - 새 테스트는 user가 16진수 64자인지만 봅니다. 그 값이 이 user_id의 SHA-256인지는 확인하지 않습니다. 아무 64글자 해시나 넣어도 통과합니다. 64자는 원문 그대로 두고 65자부터 해시하는지도 없습니다. 짧은 값 user-abc가 그대로 가는 검사는 90행에 이미 있습니다.

메인테이너의 판단이 필요한 지점

Azure 기록에는 세션 번호 원문 대신 해시만 남습니다. 같은 입력은 항상 같은 해시라서 남용 추적은 됩니다. 원문은 해시에서 되돌릴 수 없습니다. 로그에서 세션 번호를 다시 읽어야 하면 이 방식은 안 맞습니다. 준비 체크는 0/4입니다. 작성자는 bun test tests/claude-integration/에서 778개가 통과하고, 로컬 CA 테스트 2개는 수정 없는 dev에서도 실패한다고 적었습니다. 이 헤드의 저장소 CI는 hygiene와 enforce-target만 통과했습니다. 테스트 스위트는 여기서 돌지 않았습니다.

너의 추천

이 방향으로 두세요. 긴 아이디는 자르지 말고 해시하세요. 528행 테스트에 그 user_id의 SHA-256과 body.user가 같은지 한 줄 넣으세요. 64자는 원문, 65자는 해시인 경우도 옆에 두면 경계가 고정됩니다. 그 다음 준비 체크를 채우면 됩니다. 닫을 중복 PR은 없습니다.

이 댓글은 grok-bot이 작성했습니다

lidge-jun added a commit that referenced this pull request Sep 24, 2026
…, #5714, #5704, #5707) (#5738)

* fix(claude): bound Responses user to 64 chars for long metadata.user_id

Claude Code sends metadata.user_id as a ~186-char JSON string. It was copied
verbatim into the Responses 'user' field, which Azure OpenAI and other
OpenAI-compatible backends cap at 64 chars, so every Claude Code request
routed to Azure failed with 400 "Invalid 'user': string too long".

Keep short ids as-is and send the SHA-256 hex digest (already computed for
prompt_cache_key) when the id exceeds 64 chars.

Fixes #5705

Co-authored-by: Giulio Leone <giulioleone097@gmail.com>

* Fix plaintext V2 SSE responses with missing content type

Co-authored-by: Jerry WANG <jerrywang@Jerrys-MacBook-Pro-2.local>

* fix(responses): exclude dropped chat reasoning from input admission

* test(admission): verify reasoning payload matches gate decisions

Co-authored-by: 정우철 <oocheol@naver.com>

* Allow HTTP upstream for canonical ChatGPT provider

An explicit upstreamWebsocket: false now routes streaming canonical ChatGPT turns over HTTP/SSE before sending. This gives operators a supported escape from intermittent post-send WebSocket closes without replaying ambiguous turns. The default remains WebSocket and native WS controls are unavailable when HTTP is selected.

Verified: focused Responses/provider suites, typecheck, structure check, privacy scan, docs build. Changed-area suite rerun pending.
Co-authored-by: kosta <kosta963@gmail.com>

* test(claude): pin exact user hash and the 64/65-char boundary

Refs #5705

Co-authored-by: Giulio Leone <giulioleone097@gmail.com>

* docs(architecture): attribute the admission fix to excluding unsent thinking

Split the preserved-reasoning statement from the refusal rationale in the English and Korean paragraphs, as review of #5714 asked.

Refs #5696

Co-authored-by: 정우철 <oocheol@naver.com>

* fix(anthropic): count ping events as upstream liveness

Anthropic streams may include any number of ping events. The adapter only turned SSE comments into heartbeats, so a long silent thinking phase that pinged was cut off at stallTimeoutSec with upstream_stall_timeout. Named and data-only ping records now yield the same heartbeat.

Refs #5707

* fix(management): report upstreamWebsocket as configured in GET /api/providers

The row coerced an unset value to false. After the canonical ChatGPT opt-out, unset means upstream WebSocket and false means HTTP/SSE, so a save built from the row could turn WebSocket off. The row now omits the key when it is unset.

Co-authored-by: kosta <kosta963@gmail.com>

* docs(providers): describe the canonical ChatGPT upstreamWebsocket opt-out

The reference row, its seven translations, and the provider type and schema comments still said the canonical ChatGPT transport ignores upstreamWebsocket. It now selects HTTP/SSE when set to false. The locale rows were also behind the English row on the first-party-only restriction; they are retranslated from it.

Co-authored-by: kosta <kosta963@gmail.com>

* fix(responses): bound the plaintext V2 SSE prefix probe by stallTimeoutSec

The carried probe read the first chunk of an unlabeled body with no deadline, before the passthrough stall guard is attached. An upstream that sent headers but no body held the request and its host lease until the client gave up, and a failed read escaped the classifier. Each probe read now races a per-read inactivity window and one total budget, both stallTimeoutSec, plus the client abort signal. Timeout, abort, and read errors cancel the reader and return the existing unsupported-content-type 502. Docs now scope the recovery to missing or unrecognized non-JSON content types.

Follow-up to #5683 review (maintainer, Codex, CodeRabbit).

Co-authored-by: Jerry WANG <jerrywang@Jerrys-MacBook-Pro-2.local>

---------

Co-authored-by: Giulio Leone <giulioleone097@gmail.com>
Co-authored-by: Jerry WANG <jerrywang@Jerrys-MacBook-Pro-2.local>
Co-authored-by: 정우철 <oocheol@naver.com>
Co-authored-by: kosta <kosta963@gmail.com>
@lidge-jun

Copy link
Copy Markdown
Owner

Carried into bundle #5738, which is now on dev (squash-merged as df61bce) with a Co-authored-by trailer for you, so this PR is closing as landed. Thank you for the fix. If something from this branch did not make it in, the bundle description lists what was changed during the carry.

@lidge-jun lidge-jun closed this Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants