Implement mandatory app-attribution headers per the RFC

dsh-llm owns the vocabulary (attribution.ts): AppIdentity with the version
read from the package manifest, userAgent(), and attributionHeaders(target,
identity) over a closed AttributionTarget union ('generic' | 'openrouter').
Both adapters send the headers on every provider request — llm-deepseek in
its fetch headers, llm-pi-ai through pi-ai's StreamOptions.headers — behind
an explicit attributionTarget config (never inferred from baseURL), with
mock-server tests asserting exact wire arrival and the absence of the
OpenRouter set by default.

The RFC moves to implemented/ amended with the settled identity (the
deepseek-harness token, the DeepSeek Harness title, the planned
deepseek-ai/deepseek-harness-sdk URL behind a FIXME until that repo exists)
and the explicit-config OpenRouter decision.
This commit is contained in:
Tianyi Cui
2026-07-04 18:14:42 +08:00
parent 7e6edb7474
commit 0ebb86e70f
18 changed files with 403 additions and 100 deletions

View File

@@ -13,9 +13,9 @@
import { stream as piStream } from '@earendil-works/pi-ai'
import type { Model } from '@earendil-works/pi-ai'
import { LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import { attributionHeaders, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm'
import type { AttributionTarget, GenerateOptions, StreamChunk, ToolSchema } from '@deepseek-ai/dsh-llm'
import { toPiContext, toStreamChunks } from './convert.ts'
/** Reasoning levels surfaced by this adapter (DeepSeek wire: high|max). */
@@ -26,6 +26,12 @@ export interface PiAiAdapterOptions {
baseURL: string
/** Thinking level applied to every request ('off' disables thinking). */
reasoning?: PiAiReasoning | undefined
/**
* Provider-specific attribution set on top of the mandatory `User-Agent`
* baseline (dsh-llm's `attributionHeaders`). Set to `'openrouter'` when
* `baseURL` points at OpenRouter; never inferred from the URL.
*/
attributionTarget?: AttributionTarget | undefined
}
/** Build the inline pi-ai model descriptor for one DeepSeek model name. */
@@ -171,6 +177,9 @@ export class PiAiAdapter extends LlmAdapter {
try {
const events = piStream(model, toPiContext(options), {
apiKey: this.options.apiKey,
// pi-ai merges caller headers last over its provider defaults, so the
// harness attribution always reaches the wire.
headers: attributionHeaders(this.options.attributionTarget),
...options.temperature !== undefined ? { temperature: options.temperature } : {},
...options.maxTokens !== undefined ? { maxTokens: options.maxTokens } : {},
signal: controller.signal,

View File

@@ -42,6 +42,12 @@ export interface Config {
* (thinking enabled), matching llm-deepseek's omission semantics.
*/
reasoning?: PiAiReasoning
/**
* Provider-specific attribution set to send alongside the mandatory
* `User-Agent`: `'openrouter'` when `baseURL` points at OpenRouter.
* Omitted = the provider-neutral baseline.
*/
attributionTarget?: 'generic' | 'openrouter'
}
export const Config: z<Config> = z.object({
@@ -49,6 +55,7 @@ export const Config: z<Config> = z.object({
baseURL: z.string(),
models: z.array(z.string()).default(['deepseek-v4-flash', 'deepseek-v4-pro']),
reasoning: z.union(['off', 'high', 'xhigh']),
attributionTarget: z.union(['generic', 'openrouter']),
})
/** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
@@ -67,5 +74,6 @@ export function apply(ctx: Context, config: Config): void {
apiKey,
baseURL,
reasoning: config.reasoning,
attributionTarget: config.attributionTarget,
}))
}