refactor: hide llm adapter helpers

This commit is contained in:
Tianyi Cui
2026-07-14 04:03:44 +08:00
parent 5c82310f47
commit 91075e010a
11 changed files with 36 additions and 14 deletions

View File

@@ -4,6 +4,8 @@ DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch`
A second, independent implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai` (library-backed). Same Config shape — pick one per context (registering both for the same model names throws by design).
The package root exposes the Cordis plugin contract and `DeepSeekAdapter`; wire serialization, SSE parsing, and chunk translation helpers are not part of that root contract.
## Config
```yaml

View File

@@ -23,12 +23,9 @@ import z from 'schemastery'
import type {} from '@deepseek-ai/dsh-llm'
import { DeepSeekAdapter } from './adapter.ts'
export { DeepSeekAdapter, httpErrorCode } from './adapter.ts'
export { DeepSeekAdapter } 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'

View File

@@ -4,7 +4,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import LlmService, { LlmError, userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmDeepSeek from '@deepseek-ai/dsh-llm-deepseek'
import { DeepSeekAdapter, httpErrorCode } from '@deepseek-ai/dsh-llm-deepseek'
import { DeepSeekAdapter } from '@deepseek-ai/dsh-llm-deepseek'
import { httpErrorCode } from '../src/adapter.ts'
import { assemble } from './assemble.ts'
/** One scripted behavior for the next request the mock server receives. */
@@ -234,6 +235,19 @@ describe('DeepSeekAdapter against a mock server', () => {
})
describe('plugin registration and config', () => {
it('keeps wire helpers off the package root', () => {
for (const helper of [
'httpErrorCode',
'serializeMessages',
'serializeRequest',
'DONE',
'parseSse',
'mapFinishReason',
'mapUsage',
'translate',
]) expect(LlmDeepSeek).not.toHaveProperty(helper)
})
it('registers the configured models and unregisters on dispose (HMR safety)', async () => {
const server = await mockServer([])
const ctx = new Context()

View File

@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, GenerateOptions, Message } from '@deepseek-ai/dsh-llm'
import { serializeMessages, serializeRequest } from '@deepseek-ai/dsh-llm-deepseek'
import { serializeMessages, serializeRequest } from '../src/serialize.ts'
function request(overrides: Partial<GenerateOptions> = {}): GenerateOptions {
return { model: 'deepseek-v4-flash', messages: [], ...overrides }

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'
import { LlmError } from '@deepseek-ai/dsh-llm'
import { DONE, parseSse } from '@deepseek-ai/dsh-llm-deepseek'
import { DONE, parseSse } from '../src/sse.ts'
/** Build a byte stream from string fragments (fragments = network reads). */
async function* bytes(...fragments: (string | Uint8Array)[]): AsyncGenerator<Uint8Array> {

View File

@@ -1,7 +1,8 @@
import { describe, expect, it } from 'vitest'
import { BlockAssembler, LlmError } from '@deepseek-ai/dsh-llm'
import type { StreamChunk } from '@deepseek-ai/dsh-llm'
import { DONE, mapFinishReason, mapUsage, translate } from '@deepseek-ai/dsh-llm-deepseek'
import { DONE } from '../src/sse.ts'
import { mapFinishReason, mapUsage, translate } from '../src/translate.ts'
async function* feed(...payloads: (string | object)[]): AsyncGenerator<string> {
for (const payload of payloads) {

View File

@@ -2,6 +2,8 @@
DeepSeek adapter for the harness LLM seam backed by [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai) (the LLM library behind the pi agent).
The package root exposes the Cordis plugin contract and `PiAiAdapter`; model construction and event-conversion helpers are not part of that root contract.
## Why a second adapter exists
`@deepseek-ai/dsh-llm-deepseek` already talks to the same endpoint. This package is its **design-verification twin**: same models, same wire protocol, completely different internals — a unified LLM library with its own event vocabulary versus hand-rolled fetch/SSE. Anything the harness `StreamChunk` protocol cannot express for BOTH implementations is a core-vocabulary bug. The differences it exercised on purpose:

View File

@@ -22,9 +22,8 @@ import type {} from '@deepseek-ai/dsh-llm'
import { PiAiAdapter } from './adapter.ts'
import type { PiAiReasoning } from './adapter.ts'
export { buildModel, PiAiAdapter } from './adapter.ts'
export { PiAiAdapter } from './adapter.ts'
export type { PiAiAdapterOptions, PiAiReasoning } from './adapter.ts'
export { mapStopReason, mapUsage, toPiContext, toStreamChunks } from './convert.ts'
export const name = 'llm-pi-ai'
export const inject = ['llm']

View File

@@ -4,7 +4,8 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import LlmService, { CallId, userAgent } from '@deepseek-ai/dsh-llm'
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
import { buildModel, PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai'
import { buildModel } from '../src/adapter.ts'
import { assemble } from './assemble.ts'
/** Scripted SSE responses, one per request (OpenAI chat-completions shape). */
@@ -245,6 +246,12 @@ describe('PiAiAdapter against a mock server', () => {
})
describe('option spreads and env fallbacks', () => {
it('keeps adapter conversion helpers off the package root', () => {
for (const helper of ['buildModel', 'mapStopReason', 'mapUsage', 'toPiContext', 'toStreamChunks']) {
expect(LlmPiAi).not.toHaveProperty(helper)
}
})
it('forwards temperature, maxTokens, and signal', async () => {
const server = await mockServer([{ events: textEvents }])
const ctx = await harness(server.url)

View File

@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import { CallId } from '@deepseek-ai/dsh-llm'
import type { ContentBlock, StreamChunk } from '@deepseek-ai/dsh-llm'
import type { AssistantMessage, AssistantMessageEvent, Usage } from '@earendil-works/pi-ai'
import { mapStopReason, mapUsage, toPiContext, toStreamChunks } from '@deepseek-ai/dsh-llm-pi-ai'
import { mapStopReason, mapUsage, toPiContext, toStreamChunks } from '../src/convert.ts'
function usage(input = 0, output = 0, cacheRead = 0, cacheWrite = 0): Usage {
return {