Commit 3e9740e
authored
🤖 feat: add AI app attribution headers (#1141)
Adds default app attribution headers (`HTTP-Referer`, `X-Title`) to all
AI SDK provider requests, enabling OpenRouter (and compatible gateways)
to attribute mux usage.
- Implemented `buildAppAttributionHeaders` (case-insensitive merge,
never overwrites user-provided values)
- Applied during model creation so all `streamText()` calls inherit it
- Added unit tests
---
<details>
<summary>📋 Implementation Plan</summary>
# 🤖 Plan: Add app attribution headers to AI SDK streaming (OpenRouter
leaderboard)
## Goal
Ensure mux’s `streamText()` calls include **app attribution** so
OpenRouter (and any other compatible gateway) can attribute traffic to
mux (e.g., for leaderboards).
For OpenRouter specifically, this means sending these request headers:
- `HTTP-Referer`: a stable URL for the app (recommended by OpenRouter)
- `X-Title`: a human-readable app name
## Recommended approach (A): Inject attribution headers for **all
providers** in `AIService.createModel()` (default-on)
**Net new product LoC:** ~30–55
### Why this approach
- Covers **every** provider (OpenRouter + others) without needing to
touch `streamText()` call sites.
- Future-proof: any future non-streaming usage will automatically carry
the same attribution.
- Allows user overrides via `providers.jsonc` `headers` (we never
overwrite existing `HTTP-Referer` / `X-Title`, case-insensitive).
### Implementation steps
1. **Add centralized constants** for mux attribution values
- Add `src/constants/appAttribution.ts` exporting:
- `MUX_APP_ATTRIBUTION_TITLE` = `"mux"`
- `MUX_APP_ATTRIBUTION_URL` = `"https://mux.coder.com"`
2. **Add a small, tested header-merge helper** (defensive +
case-insensitive)
- In `src/node/services/aiService.ts`, export:
- `buildAppAttributionHeaders(existing?: Record<string, string>):
Record<string, string>`
- Behavior:
- Preserve all unrelated headers.
- Add missing headers using canonical casing (`HTTP-Referer`,
`X-Title`).
- Never overwrite existing values (match keys case-insensitively).
3. **Wire into model creation once**
- In `AIService.createModel()` (after baseUrl → baseURL normalization),
apply:
- `providerConfig.headers =
buildAppAttributionHeaders(providerConfig.headers)`
- This automatically affects OpenRouter and every other provider branch
because they all pass `headers` through to the provider factory.
4. **Unit tests**
- Extend `src/node/services/aiService.test.ts` with
`describe("buildAppAttributionHeaders", ...)`.
- Test cases:
- adds both headers when no headers exist
- adds only the missing header when one is present
- does not overwrite existing values (including different casing like
`http-referer`, `X-TITLE`)
- preserves unrelated headers
### Validation
- `make typecheck`
- `bun test src/node/services/aiService.test.ts`
- Manual: run a request through any provider (especially OpenRouter) and
confirm the headers appear upstream.
## Rollout / compatibility notes
- This should be a non-breaking behavioral addition.
- We must ensure we **never** overwrite user-provided `HTTP-Referer` /
`X-Title` (case-insensitive check).
</details>
---
_Generated with `mux` • Model: "unknown" • Thinking: "unknown"_
Signed-off-by: Thomas Kosiewski <tk@coder.com>1 parent fe3c94d commit 3e9740e
File tree
3 files changed
+89
-0
lines changed- src
- constants
- node/services
3 files changed
+89
-0
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
| 10 | + | |
10 | 11 | | |
11 | 12 | | |
12 | 13 | | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| 17 | + | |
16 | 18 | | |
17 | 19 | | |
18 | 20 | | |
| |||
117 | 119 | | |
118 | 120 | | |
119 | 121 | | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
60 | 60 | | |
61 | 61 | | |
62 | 62 | | |
| 63 | + | |
63 | 64 | | |
64 | 65 | | |
65 | 66 | | |
| |||
238 | 239 | | |
239 | 240 | | |
240 | 241 | | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
241 | 271 | | |
242 | 272 | | |
243 | 273 | | |
| |||
435 | 465 | | |
436 | 466 | | |
437 | 467 | | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
438 | 475 | | |
439 | 476 | | |
440 | 477 | | |
| |||
0 commit comments