New doc-sync gate verify-export-jsdoc walks every module-level exported name under packages/*/*/src and requires description prose everywhere, plus @param per parameter and @returns on non-void annotated returns for function-like exports, public class methods, properties, and accessors. The parsing + check helpers move out of gen-cordis-catalog.ts into a shared scripts/jsdoc.ts so 'documented' means one thing on both gated surfaces. Deliberate exemptions (documented in the RFC): heritage-declared class members (the seam declaration is the doc's one home — the one checker query in an otherwise pure-AST walk), cordis plugin-protocol slots (name/inject/reusable/Config/apply, top-level and static), constructors, overload implementations, declare-module augmentation bodies, and re-export statements (checked at the defining module). The 203 under-documented exports the gate found at adoption are filled in this change, so the gate lands green; generated catalogs/graphs are regenerated for the shifted line pointers. RFC: docs/rfc/implemented/process/2026-07-06-export-surface-jsdoc-gate.md
101 lines
4.3 KiB
TypeScript
101 lines
4.3 KiB
TypeScript
/**
|
|
* `DeepSeekAdapter`: fetch + SSE against a DeepSeek (OpenAI-compatible)
|
|
* chat-completions endpoint, emitting harness StreamChunks.
|
|
*
|
|
* @module dsh-llm-deepseek/adapter
|
|
*/
|
|
|
|
import { attributionHeaders, LlmAdapter, LlmError } from '@deepseek-ai/dsh-llm'
|
|
import type { GenerateOptions, StreamChunk } from '@deepseek-ai/dsh-llm'
|
|
import { serializeRequest } from './serialize.ts'
|
|
import type { RequestDefaults } from './serialize.ts'
|
|
import { parseSse } from './sse.ts'
|
|
import { translate } from './translate.ts'
|
|
import type { WireError } from './types.ts'
|
|
|
|
/** Constructor options for {@link DeepSeekAdapter}; the plugin's `apply` resolves them from Config + environment. */
|
|
export interface DeepSeekAdapterOptions {
|
|
/** Bearer token sent in the `authorization` header on every request. */
|
|
apiKey: string
|
|
/** Endpoint base; `/chat/completions` is appended. */
|
|
baseURL: string
|
|
/** Request defaults applied to every call (thinking mode, effort). */
|
|
defaults?: RequestDefaults
|
|
}
|
|
|
|
/**
|
|
* Map an HTTP status to a stable LlmError code.
|
|
* @param status - status of a non-2xx provider response.
|
|
* @returns `AUTH` (401/403), `RATE_LIMIT` (429), `INVALID_REQUEST` (400), `SERVER` (5xx), or `HTTP_<status>` for anything else.
|
|
*/
|
|
export function httpErrorCode(status: number): string {
|
|
if (status === 401 || status === 403) return 'AUTH'
|
|
if (status === 429) return 'RATE_LIMIT'
|
|
if (status === 400) return 'INVALID_REQUEST'
|
|
if (status >= 500) return 'SERVER'
|
|
return `HTTP_${status}`
|
|
}
|
|
|
|
/**
|
|
* The first real `LlmAdapter`. One instance serves every model name it was
|
|
* registered under (the harness model name IS the wire model name).
|
|
*
|
|
* Abort: `options.signal` is handed to fetch — both the initial request and
|
|
* the body stream reject on abort, which surfaces to the loop as a rejected
|
|
* step (the loop already contains step errors).
|
|
*/
|
|
export class DeepSeekAdapter extends LlmAdapter {
|
|
constructor(private readonly options: DeepSeekAdapterOptions) {
|
|
super()
|
|
}
|
|
|
|
async * stream(options: GenerateOptions): AsyncIterable<StreamChunk> {
|
|
const body = serializeRequest(options, this.options.defaults ?? {})
|
|
|
|
// TODO(http): deliberately raw `fetch` for the hand-rolled SSE body.
|
|
// `@cordisjs/plugin-http` (ctx.http) would give proxy/intercept/timeout
|
|
// uniformity AND can stream (`responseType: 'stream'` yields the same
|
|
// ReadableStream<Uint8Array> parseSse consumes), but adopting it today
|
|
// costs a hard `undici` dependency (it does `require('undici')` with no
|
|
// globalThis.fetch fallback) plus an unconditional `@cordisjs/fetch-file`
|
|
// import (pulling file-type + mime-types) for a file:// path we never hit.
|
|
// Revisit when a second adapter wants shared proxy/intercept config.
|
|
const response = await fetch(`${this.options.baseURL}/chat/completions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'authorization': `Bearer ${this.options.apiKey}`,
|
|
'content-type': 'application/json',
|
|
'accept': 'text/event-stream',
|
|
...attributionHeaders(),
|
|
},
|
|
body: JSON.stringify(body),
|
|
...options.signal ? { signal: options.signal } : {},
|
|
})
|
|
|
|
if (!response.ok) {
|
|
const code = httpErrorCode(response.status)
|
|
let message = `DeepSeek API error (HTTP ${response.status})`
|
|
try {
|
|
const parsed = await response.json() as WireError
|
|
if (parsed.error?.message) message = parsed.error.message
|
|
} catch {
|
|
// Paranoid by design: `code` and the HTTP status are ALREADY captured
|
|
// above (and passed to LlmError below), so the only thing this `try`
|
|
// can add is a richer provider-supplied message. A malformed, empty,
|
|
// or non-JSON error body is a normal thing for gateways/proxies to
|
|
// return on a 5xx/429 — swallowing the parse failure keeps the usable
|
|
// status-line message instead of letting a JSON.parse throw mask the
|
|
// real HTTP error. Nothing else reaches this catch: response.json()
|
|
// is the sole statement, and any non-parse failure (e.g. body already
|
|
// consumed) is equally non-actionable here.
|
|
}
|
|
throw new LlmError(message, code, response.status)
|
|
}
|
|
if (!response.body) {
|
|
throw new LlmError('DeepSeek API returned no response body', 'EMPTY_RESPONSE')
|
|
}
|
|
|
|
yield* translate(parseSse(response.body))
|
|
}
|
|
}
|