Files
deepseek-harness/packages/llm/llm-deepseek/src/index.ts
T
Tianyi Cui 0ebb86e70f 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.
2026-07-04 18:14:42 +08:00

79 lines
2.8 KiB
TypeScript

/**
* DeepSeek LLM adapter plugin: registers a {@link DeepSeekAdapter} for the
* configured model names on `ctx.llm`.
*
* Config is cordis-native (schemastery). Secrets flow per the repo policy:
* `apiKey` from cordis.yml via the `!!js` tag (`!!js process.env.DEEPSEEK_API_KEY`)
* or from the environment directly; never from ad-hoc files.
*
* ```yaml
* - id: llm-deepseek
* name: '@deepseek-ai/dsh-llm-deepseek'
* config:
* apiKey: !!js process.env.DEEPSEEK_API_KEY
* baseURL: !!js process.env.DEEPSEEK_BASE_URL
* models: [deepseek-v4-flash, deepseek-v4-pro]
* ```
*
* @module @deepseek-ai/dsh-llm-deepseek
*/
import type { Context } from 'cordis'
import z from 'schemastery'
import type {} from '@deepseek-ai/dsh-llm'
import { DeepSeekAdapter } from './adapter.ts'
export { DeepSeekAdapter, httpErrorCode } from './adapter.ts'
export type { DeepSeekAdapterOptions } from './adapter.ts'
export { serializeMessages, serializeRequest } from './serialize.ts'
export type { RequestDefaults } from './serialize.ts'
export { DONE, parseSse } from './sse.ts'
export { mapFinishReason, mapUsage, translate } from './translate.ts'
export type * from './types.ts'
export const name = 'llm-deepseek'
export const inject = ['llm']
export interface Config {
/** API key; falls back to $DEEPSEEK_API_KEY. Required one way or the other. */
apiKey?: string
/** Endpoint base; falls back to $DEEPSEEK_BASE_URL, then the public API. */
baseURL?: string
/** Model names to register (sent verbatim on the wire). */
models?: string[]
/** Thinking-mode default for every request (provider default: enabled). */
thinking?: 'enabled' | 'disabled'
/** Thinking effort (only meaningful with thinking enabled). */
reasoningEffort?: 'high' | 'max'
}
export const Config: z<Config> = z.object({
apiKey: z.string(),
baseURL: z.string(),
models: z.array(z.string()).default(['deepseek-v4-flash', 'deepseek-v4-pro']),
thinking: z.union(['enabled', 'disabled']),
reasoningEffort: z.union(['high', 'max']),
})
/** Public API default; the internal endpoint comes from $DEEPSEEK_BASE_URL. */
export const PUBLIC_BASE_URL = 'https://api.deepseek.com'
export function apply(ctx: Context, config: Config): void {
const apiKey = config.apiKey ?? process.env.DEEPSEEK_API_KEY
if (apiKey === undefined || apiKey.length === 0) {
throw new Error('llm-deepseek: an API key is required (Config.apiKey or $DEEPSEEK_API_KEY)')
}
const baseURL = config.baseURL ?? process.env.DEEPSEEK_BASE_URL ?? PUBLIC_BASE_URL
// schemastery's .default() guarantees models is set after validation.
const models = config.models as string[]
ctx.llm.registerAdapter(models, new DeepSeekAdapter({
apiKey,
baseURL,
defaults: {
thinking: config.thinking,
reasoningEffort: config.reasoningEffort,
},
}))
}