Skip to content

feat: attribute requests to a user profile - #33

Merged
sophie-ant merged 1 commit into
mainfrom
sdownward/user-profiles
Sep 9, 2026
Merged

sophie-ant merged 1 commit into
mainfrom
sdownward/user-profiles

Conversation

@sophie-ant

@sophie-ant sophie-ant commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets an app attribute each request to one of its users. Set userProfileID: on ClaudeLanguageModel. Every request then sends the anthropic-user-profile-id header, with the user-profiles-2026-08-18 beta that the header needs:

ClaudeLanguageModel(name: .opus5, auth: auth, userProfileID: profileID)

Until now, .apiKey and .appAttest couldn't send the header at all, and .proxied needed both headers set by hand. The other SDKs expose the same header as a user_profile_id request parameter.

  • The executor sets the header, and it adds the beta to the list of betas it already merges. So the beta goes after any values the request already carries, such as a proxy's or the fallbacks beta, and it's never repeated.
  • The configured profile replaces a proxy header of the same name, however it's spelled, so a request carries one profile.
  • HeaderName names the anthropic-beta and anthropic-user-profile-id headers.
  • The ID isn't validated locally. The API checks it, and a malformed or unknown ID fails the request.
  • Out of scope: the user profiles endpoints (/v1/user_profiles), which create profiles and enrollment URLs. Those belong on the developer's backend, not in an app.

Rebased onto main after #32 merged. It builds on that change: both add to the same executor configuration and test stub.

Test plan

Live requests:

  • The header without the beta returns 400, and the message names the beta that's needed.
  • A request through LanguageModelSession with a valid profile ID as userProfileID succeeds, and the profile header and merged beta list reach the API as one comma-joined anthropic-beta value.

Unit tests cover the header, the beta order, one profile header however a proxy spells it, and case-insensitive header merging.

The full test suite at f899ba8 (the same tree as 428dd51, before the rebase onto the merged #32), via xcodebuild on macOS 26.6 with Xcode 27.0 beta 6 (27A5252f) and the iOS 27.0 simulator: ** TEST SUCCEEDED **, 243 tests, 240 passed, 0 failed, 3 skipped. The 3 skips are the keychain-backed AppAttestStoreTests, which skip by design under this runner.

dtmeadows-ant
dtmeadows-ant previously approved these changes Sep 9, 2026
Base automatically changed from sdownward/server-side-fallbacks to main September 9, 2026 17:36
@sophie-ant
sophie-ant dismissed dtmeadows-ant’s stale review September 9, 2026 17:36

The base branch was changed.

Lets an app attribute each request to one of its users. Set
`userProfileID:` on `ClaudeLanguageModel`, and every request sends the
`anthropic-user-profile-id` header, with the `user-profiles-2026-08-18`
beta that the header needs:

    ClaudeLanguageModel(name: .opus5, auth: auth, userProfileID: profileID)

Until now `.apiKey` and `.appAttest` couldn't send the header at all,
and `.proxied` needed both headers set by hand.

- The executor sets the header and adds the beta to the list it already
  merges, so the beta goes after any values the request already
  carries, such as a proxy's or the fallbacks beta, and is never
  repeated.
- The configured profile replaces a proxy header of the same name,
  however it's spelled, so a request carries one profile.
- `HeaderName` names the `anthropic-beta` and
  `anthropic-user-profile-id` headers.
- The ID isn't validated locally. The API checks it, and a malformed or
  unknown ID fails the request.
@sophie-ant
sophie-ant force-pushed the sdownward/user-profiles branch from f899ba8 to 428dd51 Compare September 9, 2026 17:37
@sophie-ant
sophie-ant merged commit ae91954 into main Sep 9, 2026
2 of 3 checks passed
@sophie-ant
sophie-ant deleted the sdownward/user-profiles branch September 9, 2026 17:42
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.

3 participants