Files
deepseek-harness/docs/rfc/implemented/architecture/2026-06-21-mandatory-app-attribution-headers.md
T
Tianyi Cui e6fad266a6 docs(rfc): define and enforce a uniform RFC format; adopt it across the corpus
Define the in-file RFC contract in docs/rfc/README.md § The file format:
the header block (`# RFC: <title>` plus a dateless Status enum
cross-checked against the lifecycle folder), the per-lifecycle body
skeleton (a Problem opener everywhere; Proposal/Alternatives considered/
Acceptance criteria/Risks in proposed/; present-tense Decision/
Consequences with proposal-era headings banned in implemented/; the
frozen proposal shape in rejected/), and a mandatory Alternatives
considered section with a date-fenced grandfather comment for pre-format
RFCs whose alternatives are not reconstructible from the record.

Enforce it with a new doc-sync gate, scripts/verify-rfc-format.ts, and
normalize all 112 RFCs to it: ~15 Status-line spellings collapse to the
enum, 29 Context openers become Problem, the 39 legacy-format XXX debt
markers are resolved and banned from reappearing, proposal-era sections
in implemented RFCs are rewritten to shipped reality (including the
web/fs/subagent seam RFCs' migration plans and test checklists, closing
the doc-tiers deferred-work item on the web seam), every RFC gains an
Alternatives considered section or the grandfather comment, and the
bilingual pair is re-mirrored and re-recorded.

Move the generated index tables out of README.md into a fully generated
docs/rfc/INDEX.md — gen-rfc-index now writes the whole file, and
verify-rfc-classification checks its freshness and rejects index-shaped
rows in the curated README — which makes room for the format contract to
live in the README front door instead of a separate FORMAT.md.

The decision record, and the first RFC written in the new format, is
docs/rfc/implemented/process/2026-07-05-uniform-rfc-format.md.
2026-07-05 22:58:25 +08:00

13 KiB

RFC: Mandatory User-Agent attribution for provider requests

Status: implemented

Problem

LLM provider requests should identify the product making them. That is useful for provider-side support, abuse investigation, compatibility debugging, and traffic analytics. Before this RFC the harness only partially did this: the hand-rolled DeepSeek adapter sent a hand-copied User-Agent constant (packages/llm/llm-deepseek/src/adapter.ts), while the pi-ai-backed twin sent no harness-owned headers at all (packages/llm/llm-pi-ai/src/adapter.ts). New adapters could therefore omit attribution silently, and a library-backed adapter could drift from the hand-rolled adapter even though the twin-adapter RFC exists to keep the provider seam honest across both implementations.

The immediate prompt came from OpenRouter's App Attribution docs. OpenRouter creates app pages and rankings from HTTP-Referer plus display/category headers. That is valuable, but it is not the HTTP standard for application identity. The risk is adopting OpenRouter's exact header set as if it were universal, then leaking provider-specific headers to direct DeepSeek requests, future OpenAI/Anthropic/Vertex adapters, test servers, or proxies that log unknown fields indefinitely.

Investigation

  • OpenRouter's mechanism is provider-specific. Their current docs say app attribution is tracked through HTTP-Referer (required), X-OpenRouter-Title, and X-OpenRouter-Categories; X-Title is only accepted for backward compatibility. Their API reference calls the headers optional and says they make the app discoverable on OpenRouter. This is a concrete OpenRouter contract, not an IETF or OpenAI-compatible API standard.
  • In agent tooling, HTTP-Referer is an OpenRouter-aware convention, not a general agent convention. It is common enough that OpenRouter SDKs and OpenRouter examples expose it directly, and frameworks that target OpenRouter usually need a way to pass it through. But agent protocols such as ACP negotiate names, versions, and capabilities in their own initialize messages, while model-provider requests still need HTTP-level identity. "Accepted in the agent world" therefore means "recognized by OpenRouter integrations," not "portable across agent runtimes or providers."
  • Observed coding agents use product/version User-Agent strings, sometimes with environment context. A non-exhaustive public-code survey found OpenAI Codex building {originator}/{version} ({os} {os_version}; {arch}) ... and carrying an originator header; Google Gemini CLI sending GeminiCLI[-clientName]/{version}/{model} ({platform}; {arch}; {surface}) or a Cloud Code VS Code variant; Cline's Codex backend client sending cline/{version} ({platform} {release}; {arch}) node/{nodeVersion} plus originator: cline; SWE-agent setting swe-agent/{version} unless the user already supplied a header; Continue setting Continue/{version} for its ClawRouter provider plus X-Continue-Provider. Aider also appends Aider/{version} +{website} to browser-like user agents for web scraping, but that is not a model-provider request path. The pattern is not one exact format; it is product identity in User-Agent, with provider-specific side headers only where a provider/backend asks for them.
  • The standards-track general client identity header is User-Agent. RFC 9110 section 10.1.5 defines User-Agent as the user-agent software identity, says it is used for interoperability reports and analytics, and says a user agent SHOULD send it on each request unless configured not to. This is the only standard header that directly matches "what product is making this HTTP request."
  • Referer is standard, but OpenRouter's HTTP-Referer is not the standard field. RFC 9110 section 10.1.3 defines Referer as the URI from which the target URI was obtained and spends significant text on privacy restrictions. OpenRouter instead asks for HTTP-Referer, using it as an app URL identifier. That name and meaning are OpenRouter-specific even though it resembles the CGI environment variable form of the standard Referer header.
  • From is standard but not suitable as a mandatory default. RFC 9110 section 10.1.2 defines From as an email address for the human responsible for a user agent. Robotic agents SHOULD send it so servers can contact an operator, but non-robotic agents should not send it without explicit user configuration because of privacy and security policy concerns. The harness can support an operator contact later, but must not invent one or require it globally.
  • Request-body user or metadata fields are not app attribution. Some model APIs expose a stable end-user identifier, request metadata, labels, or project/account headers. Those are useful for abuse monitoring, internal billing, dashboards, or trace correlation, but they either identify the end user rather than the product, are provider-specific body schema, or are not guaranteed to be forwarded through OpenAI-compatible gateways. They are not a substitute for a static application identity header.
  • SDK telemetry headers identify the SDK, not the app. Official and third-party SDKs often send library/version headers. Those help the SDK maintainer debug their client, but they do not identify the harness as the application unless the application explicitly supplies a product attribution layer.
  • pi-ai has a first-class header hook. @earendil-works/pi-ai's StreamOptions.headers merges caller headers last over provider defaults, so a library-backed adapter can satisfy the same wire contract as the hand-rolled one without wrapping or upstream work. The mock-server suites assert arrival on the wire for both adapters.

Decision

Provider request attribution is mandatory at the LLM adapter boundary, using the standard User-Agent header only. The rule: every product LLM adapter sends a static, non-secret application identity on every provider HTTP request, and every adapter has tests proving that User-Agent reaches the wire (a mock server asserting received headers; for a library-backed adapter, the library's header hook feeding the same mock-server assertion).

Do not implement OpenRouter app attribution in this RFC. HTTP-Referer, X-OpenRouter-Title, X-Title, and X-OpenRouter-Categories are OpenRouter-specific product-surface headers, not provider-neutral model-request attribution. They can be proposed later by an OpenRouter adapter or explicit OpenRouter mode, with its own privacy/product decision, tests, and docs. Until then, even requests pointed at OpenRouter send only the shared User-Agent attribution from this RFC.

The provider-neutral identity is owned by dsh-llm (packages/llm/llm/src/attribution.ts), not by individual adapters. AppIdentity contains only public product facts needed to build User-Agent, and the default APP_IDENTITY settles the values the proposal left open:

  • product token for User-Agent: deepseek-harness (continuity with the pre-RFC wire value and the repo/org identity)
  • version: read from the owning package's manifest via createRequire, never a hand-copied constant
  • app URL: https://github.com/deepseek-ai/deepseek-harness-sdk - the planned public home; a FIXME in attribution.ts blocks release until that repository actually exists

The default is mandatory and non-empty. White-label deployments pass their own AppIdentity to attributionHeaders(identity) - the override seam is the function parameter, with no deployment config plumbing until a consumer needs it - and omission falls back to the harness default rather than suppressing attribution. There is no per-request API for the model, user prompt, session id, cwd, user email, API key owner, or local machine identity to influence these fields.

Wire mapping (attributionHeaders; header names lowercase in code - HTTP field names are case-insensitive on the wire):

Target Mapping
All HTTP-based adapters User-Agent: {product}/{version} (+{url}) - the parenthesized +url comment stays within RFC 9110's conservative product/comment syntax.
Direct DeepSeek endpoint User-Agent; do not send OpenRouter-only headers unless DeepSeek documents an equivalent contract.
OpenRouter endpoints User-Agent only for now. Do not send HTTP-Referer, X-OpenRouter-Title, X-Title, or X-OpenRouter-Categories under this RFC.
Future providers User-Agent only unless a later provider-specific RFC accepts additional headers. Do not reuse HTTP-Referer by analogy.

Endpoint detection is not part of this RFC because no endpoint-specific mapping is accepted here. If OpenRouter support lands later, detection must be explicit: either a dedicated OpenRouter provider package or an explicit provider: 'openrouter' / attributionTarget: 'openrouter' config, not arbitrary path fragments or model names.

Verification

The landed contract:

  • dsh-llm documents the mandatory User-Agent attribution contract for LlmAdapter authors (LlmAdapter JSDoc, package README, and the adapter-contract section of docs/core-data-structures/llm-streaming.md).
  • A shared helper (attributionHeaders / userAgent) constructs the app identity and the standard User-Agent value from package metadata, so adapters do not hand-copy version constants.
  • dsh-llm-deepseek sends the shared User-Agent on every request and its mock-server suite asserts the exact value.
  • dsh-llm-pi-ai sends the same User-Agent through pi-ai's StreamOptions.headers hook and its mock-server suite asserts the exact value.
  • No adapter sends OpenRouter-specific attribution headers (HTTP-Referer, X-OpenRouter-Title, X-Title, X-OpenRouter-Categories) as part of this RFC.
  • No app-attribution field carries secrets, local paths, session ids, prompt text, model output, user email, or per-user stable identifiers.
  • The adapter READMEs state the User-Agent attribution policy and explicitly avoid documenting OpenRouter app attribution as implemented behavior.

Alternatives considered

OpenRouter app attribution now. Rejected for this RFC. Sending HTTP-Referer plus X-OpenRouter-Title would satisfy OpenRouter rankings, but those headers are a provider-specific product feature, not the provider-neutral model-request attribution this RFC is trying to standardize. Supporting them should be an explicit OpenRouter adapter/mode decision later, not hidden inside the first shared attribution helper.

OpenRouter headers everywhere. Rejected. It would treat a custom OpenRouter contract as a universal standard and send fields with misleading semantics to providers that did not ask for them. It also risks using HTTP-Referer as a generic app URL field even though standard HTTP already has User-Agent for product identity and Referer for a different browsing-context concept.

Only provider account/project identity. Rejected. Organization/project headers, API keys, cloud accounts, and billing projects identify who pays or owns the request, not which application is sending traffic. They also expose no public app title/category and do not help gateways like OpenRouter build app rankings.

End-user user/metadata fields. Rejected for this RFC. Those are valuable for abuse monitoring and customer support but describe the human or tenant behind a request. App attribution must be static product identity and safe to send on every request.

Config-only opt-in attribution. Rejected. A default-off setting is exactly how adapters keep drifting. The policy is mandatory default attribution with overrideable public values, not optional attribution.

Product-named token (deepseek-harness-sdk). Considered for the User-Agent token, since the product name is DeepSeek Harness SDK. deepseek-harness won on continuity: it is the identity providers already see from this codebase, it matches the org/repo identity and package scope, and it keeps wire attribution stable while display copy carries the product name.

Consequences

Providers see that traffic comes from the harness. That is the point, but it means deployments that previously blended into generic SDK traffic become identifiable. Mitigation: send only static public product data and let forks/white-label deployments pass their own AppIdentity.

The app URL points at a repository that does not exist yet. deepseek-ai/deepseek-harness-sdk is the planned public home; until it is created the URL is a dangling promise. The FIXME marker on the constant blocks a release from shipping with it unresolved (see docs/development.md marker semantics).

Header support differs by client library. The hand-rolled adapter sets headers directly; the pi-ai-backed adapter depends on pi-ai continuing to honor StreamOptions.headers (merged last over provider defaults). The wire-level mock-server tests are the guard: if a pi-ai upgrade stops delivering the header, the suite goes red. This is useful pressure on the abstraction: a provider adapter that cannot set mandatory headers cannot fully implement the harness LLM contract.

OpenRouter rankings do not benefit yet. User-Agent is the correct baseline for provider-neutral HTTP identity, but it will not create OpenRouter app pages or rankings because OpenRouter requires HTTP-Referer for that product feature. That is deliberate: public app marketplace participation is a separate product decision, not a prerequisite for mandatory request attribution.