diff --git a/docs/config-catalog.md b/docs/config-catalog.md index 4cac932a6f..53e5142dff 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -394,7 +394,7 @@ export interface DeepSeekCatalogModel { } ``` -Source: [`packages/llm/llm-deepseek/src/index.ts:36`](../packages/llm/llm-deepseek/src/index.ts) +Source: [`packages/llm/llm-deepseek/src/index.ts:33`](../packages/llm/llm-deepseek/src/index.ts) ## `@deepseek-ai/dsh-llm-pi-ai` diff --git a/packages/llm/llm-deepseek/README.md b/packages/llm/llm-deepseek/README.md index 7da98cdebf..a4b43342e4 100644 --- a/packages/llm/llm-deepseek/README.md +++ b/packages/llm/llm-deepseek/README.md @@ -4,6 +4,8 @@ DeepSeek chat-completions adapter for the harness LLM seam: hand-rolled `fetch` A second, library-backed implementation of the same seam exists in `@deepseek-ai/dsh-llm-pi-ai`. This package always owns the `deepseek` provider route; mounting a pi-ai profile with `provider: deepseek` in the same context throws `LlmError('DUPLICATE_ADAPTER')` 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 diff --git a/packages/llm/llm-deepseek/src/index.ts b/packages/llm/llm-deepseek/src/index.ts index 695831b1bd..f9f223b6ff 100644 --- a/packages/llm/llm-deepseek/src/index.ts +++ b/packages/llm/llm-deepseek/src/index.ts @@ -11,12 +11,9 @@ import type {} from '@deepseek-ai/dsh-llm' import { DeepSeekAdapter } from './adapter.ts' import type { DeepSeekCatalogModel } from './adapter.ts' -export { DeepSeekAdapter, httpErrorCode } from './adapter.ts' +export { DeepSeekAdapter } from './adapter.ts' export type { DeepSeekAdapterOptions, DeepSeekCatalogModel } 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' diff --git a/packages/llm/llm-deepseek/tests/adapter.spec.ts b/packages/llm/llm-deepseek/tests/adapter.spec.ts index d1fc163651..77a8f1d8a4 100644 --- a/packages/llm/llm-deepseek/tests/adapter.spec.ts +++ b/packages/llm/llm-deepseek/tests/adapter.spec.ts @@ -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. */ @@ -236,6 +237,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 deepseek provider and unregisters on dispose (HMR safety)', async () => { const server = await mockServer([]) const ctx = new Context() diff --git a/packages/llm/llm-deepseek/tests/serialize.spec.ts b/packages/llm/llm-deepseek/tests/serialize.spec.ts index 0a5d10e23b..8d3b190ed1 100644 --- a/packages/llm/llm-deepseek/tests/serialize.spec.ts +++ b/packages/llm/llm-deepseek/tests/serialize.spec.ts @@ -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 { return { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], ...overrides } diff --git a/packages/llm/llm-deepseek/tests/sse.spec.ts b/packages/llm/llm-deepseek/tests/sse.spec.ts index 2fc297bbec..b18862e4f3 100644 --- a/packages/llm/llm-deepseek/tests/sse.spec.ts +++ b/packages/llm/llm-deepseek/tests/sse.spec.ts @@ -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 { diff --git a/packages/llm/llm-deepseek/tests/translate.spec.ts b/packages/llm/llm-deepseek/tests/translate.spec.ts index d6968faed5..e62cebc4af 100644 --- a/packages/llm/llm-deepseek/tests/translate.spec.ts +++ b/packages/llm/llm-deepseek/tests/translate.spec.ts @@ -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 { for (const payload of payloads) { diff --git a/packages/llm/llm-pi-ai/README.md b/packages/llm/llm-pi-ai/README.md index 22369dc187..cb2abc8205 100644 --- a/packages/llm/llm-pi-ai/README.md +++ b/packages/llm/llm-pi-ai/README.md @@ -2,6 +2,8 @@ Generic multi-provider adapter for the harness LLM seam backed by [`@earendil-works/pi-ai`](https://www.npmjs.com/package/@earendil-works/pi-ai). One plugin instance owns an explicit list of provider profiles; every request selects a profile with `GenerateOptions.provider` and resolves `GenerateOptions.model` dynamically from pi-ai's installed catalog. +The package root exposes the Cordis plugin contract and `PiAiAdapter`; profile resolution, model construction, replay conversion, and stream conversion remain package-internal. + ## Config Configure credentials and deployment-specific transport settings per provider. Omitting `apiKey` delegates authentication to pi-ai's provider-native ambient discovery. `baseURL` overrides only the endpoint of the selected catalog model, preserving its API family and compatibility metadata, so private proxies such as `https://proxy.example.com:8443` remain supported. diff --git a/packages/llm/llm-pi-ai/src/index.ts b/packages/llm/llm-pi-ai/src/index.ts index cbd0dcd435..ab08f21b81 100644 --- a/packages/llm/llm-pi-ai/src/index.ts +++ b/packages/llm/llm-pi-ai/src/index.ts @@ -27,12 +27,8 @@ import { Config, resolveProfiles } from './config.ts' export { PiAiAdapter } from './adapter.ts' export type { PiAiAdapterOptions } from './adapter.ts' -export { Config, resolveProfiles } from './config.ts' +export { Config } from './config.ts' export type { PiAiProviderProfile } from './config.ts' -export { toPiContext } from './context.ts' -export { toPiReplayState } from './replay.ts' -export type { PiAiReplayState } from './replay.ts' -export { mapStopReason, mapUsage, toStreamChunks } from './stream.ts' export const name = 'llm-pi-ai' export const inject = ['llm'] diff --git a/packages/llm/llm-pi-ai/tests/adapter.spec.ts b/packages/llm/llm-pi-ai/tests/adapter.spec.ts index 1767447d7d..559c36bf27 100644 --- a/packages/llm/llm-pi-ai/tests/adapter.spec.ts +++ b/packages/llm/llm-pi-ai/tests/adapter.spec.ts @@ -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 LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' -import { PiAiAdapter, resolveProfiles } from '@deepseek-ai/dsh-llm-pi-ai' +import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai' +import { resolveProfiles } from '../src/config.ts' import { assemble } from './assemble.ts' interface MockServer { @@ -180,6 +181,18 @@ describe('PiAiAdapter provider routing', () => { }) describe('provider profile lifecycle', () => { + it('keeps adapter helpers off the package root', () => { + for (const helper of [ + 'resolveProfiles', + 'toPiContext', + 'toPiReplayState', + 'toPiAssistant', + 'mapStopReason', + 'mapUsage', + 'toStreamChunks', + ]) expect(LlmPiAi).not.toHaveProperty(helper) + }) + it('registers every profile atomically and unregisters on dispose', async () => { const ctx = new Context() await ctx.plugin(LlmService) diff --git a/packages/llm/llm-pi-ai/tests/convert.spec.ts b/packages/llm/llm-pi-ai/tests/convert.spec.ts index 50955f0607..2b405c7c91 100644 --- a/packages/llm/llm-pi-ai/tests/convert.spec.ts +++ b/packages/llm/llm-pi-ai/tests/convert.spec.ts @@ -2,7 +2,9 @@ import { describe, expect, it } from 'vitest' import { CallId, LlmError } 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, toPiReplayState, toStreamChunks } from '@deepseek-ai/dsh-llm-pi-ai' +import { toPiContext } from '../src/context.ts' +import { toPiReplayState } from '../src/replay.ts' +import { mapStopReason, mapUsage, toStreamChunks } from '../src/stream.ts' function usage(input = 0, output = 0, cacheRead = 0, cacheWrite = 0): Usage { return {