Skip to content

fix(llm): sanitize short user_id across transformers to comply with c… - #1812

Open
survivor998 wants to merge 1 commit into
looplj:unstablefrom
survivor998:fix-user-id-length
Open

fix(llm): sanitize short user_id across transformers to comply with c…#1812
survivor998 wants to merge 1 commit into
looplj:unstablefrom
survivor998:fix-user-id-length

Conversation

@survivor998

Copy link
Copy Markdown

…onstraints

@greptile-apps

greptile-apps Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces a shared SanitizeUserID helper that normalises user IDs to ^[a-zA-Z0-9_-]{6,128}$ and applies it across the Anthropic, Doubao, and Zai outbound transformers. It also adds a User field fallback and correctly separates request_id extraction for Doubao and Zai.

  • New shared/userid.go: replaces disallowed characters with _, pads short IDs with a "user_" prefix, and truncates at 128 bytes; contains one unreachable inner-padding branch.
  • Doubao & Zai: when llmReq.User is the fallback source, the sanitized value lands in the platform-specific user_id field, but openai.RequestFromLLM already copies the original pointer into the embedded openai.Request.User, so the unsanitized value is still serialised in the outgoing JSON alongside the sanitized one.
  • Anthropic: the fix is clean — no embedded OpenAI request means no duplicate field.

Confidence Score: 3/5

Safe to merge for Anthropic; Doubao and Zai have an incomplete fix on the User-field fallback path that could still send unsanitized data to the upstream API.

The Anthropic transformer is clean. For Doubao and Zai, when llmReq.User is used as the fallback source, openai.RequestFromLLM has already placed the original pointer into the embedded openai.Request.User field before the new sanitization code runs. That original, potentially non-conforming value is serialised as user in the JSON payload alongside the new sanitized user_id, which means the constraint violation this PR targets can still reach the API via a different field on the same request.

llm/transformer/doubao/outbound.go and llm/transformer/zai/outbound.go need a null-out or replacement of the embedded Request.User after setting UserID.

Important Files Changed

Filename Overview
llm/transformer/shared/userid.go New shared helper that strips illegal characters and pads/truncates to [6,128] chars; contains an unreachable inner-padding branch but is otherwise correct.
llm/transformer/anthropic/outbound_convert.go Adds fallback to chatReq.User when Metadata user_id is absent, and applies SanitizeUserID; no embedded OpenAI user field, so the dual-field issue does not apply here.
llm/transformer/doubao/outbound.go Sanitizes user_id and splits request_id extraction; the llmReq.User fallback path leaves the original unsanitized value in the embedded openai.Request.User field, which is also serialized in the JSON payload.
llm/transformer/zai/outbound.go Same sanitization and request_id refactor as Doubao; same residual unsanitized user field issue in the embedded openai.Request when using the User fallback path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[llm.Request] --> B{Metadata user_id set?}
    B -- Yes --> C[userID = Metadata user_id]
    B -- No --> D{User field set?}
    D -- Yes --> E[userID = *User]
    D -- No --> F[userID stays empty]
    C --> G{userID != empty?}
    E --> G
    F --> G
    G -- Yes --> H[SanitizeUserID]
    G -- No --> I[Skip user field]
    H --> H1[Replace invalid chars with _]
    H1 --> H2{len < 6?}
    H2 -- Yes --> H3[Prepend user_]
    H2 -- No --> H4{len > 128?}
    H3 --> H4
    H4 -- Yes --> H5[Truncate to 128]
    H4 -- No --> H6[Return sanitized]
    H5 --> H6
    H6 --> J{Transformer}
    J -- Anthropic --> K[AnthropicMetadata UserID - clean]
    J -- Doubao --> L[doubaoReq.UserID sanitized BUT doubaoReq.Request.User still has original]
    J -- Zai --> M[zaiReq.UserID sanitized BUT zaiReq.Request.User still has original]
Loading

Reviews (1): Last reviewed commit: "fix(llm): sanitize short user_id across ..." | Re-trigger Greptile

Comment on lines +146 to +155
var userID string
if llmReq.Metadata != nil && llmReq.Metadata["user_id"] != "" {
userID = llmReq.Metadata["user_id"]
} else if llmReq.User != nil && *llmReq.User != "" {
userID = *llmReq.User
}

if userID != "" {
doubaoReq.UserID = shared.SanitizeUserID(userID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Unsanitized user leaks into the upstream request on the fallback path

When llmReq.User is used as the source (i.e., Metadata["user_id"] is absent), openai.RequestFromLLM has already copied that same pointer to doubaoReq.Request.User (line 29 of openai/outbound_convert.go). The JSON payload therefore contains both "user_id": "<sanitized>" and "user": "<original unsanitized>" simultaneously. If the Doubao API validates the user field under the same ^[a-zA-Z0-9_-]{6,128}$ constraint, the sanitization here is bypassed. The same issue exists in zai/outbound.go. Consider clearing doubaoReq.Request.User = nil (or setting it to the sanitized value) after setting doubaoReq.UserID.

Comment on lines +132 to +141
var userID string
if llmReq.Metadata != nil && llmReq.Metadata["user_id"] != "" {
userID = llmReq.Metadata["user_id"]
} else if llmReq.User != nil && *llmReq.User != "" {
userID = *llmReq.User
}

if userID != "" {
zaiReq.UserID = shared.SanitizeUserID(userID)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Unsanitized user leaks into the upstream request on the fallback path

Same issue as in doubao/outbound.go: when llmReq.User is the source, openai.RequestFromLLM copies llmReq.User into zaiReq.Request.User. The serialized request therefore includes both the sanitized "user_id" and the original unsanitized "user" field. If the Zai API validates both fields, the constraint violation this PR is fixing is still present via the user field. Clearing zaiReq.Request.User after setting zaiReq.UserID would fully close the gap.

Comment on lines +24 to +27
sanitized = "user_" + sanitized
if len(sanitized) < 6 {
sanitized = sanitized + strings.Repeat("_", 6-len(sanitized))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Inner padding branch is unreachable dead code

After the loop, sanitized is always non-empty for any non-empty userID (every rune, including invalid UTF-8 replacement characters, writes at least one byte to the builder). So "user_" + a non-empty sanitized is always ≥ 6 bytes, and the inner if len(sanitized) < 6 condition can never be true. The block can be dropped without changing behavior.

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!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant