Merge remote-tracking branch 'origin/master' into worktree/dsbench-patch

# Conflicts:
#	packages/bash/bash-local/README.md
#	packages/bash/bash-local/src/index.ts
#	packages/bash/bash-local/src/run.ts
#	packages/bash/bash-local/tests/run.spec.ts
#	packages/examples/agent-spine-demo/README.md
#	packages/examples/agent-spine-demo/src/index.ts
#	packages/ui/jsonrpc/README.md
#	packages/ui/jsonrpc/src/server.ts
This commit is contained in:
Yichen Jiang
2026-07-19 14:53:03 +08:00
876 changed files with 48016 additions and 13119 deletions

View File

@@ -16,11 +16,11 @@ const FLASH = 'deepseek-v4-flash'
const PRO = 'deepseek-v4-pro'
const contexts: Context[] = []
async function harness(model: string, config: Partial<Config> = {}) {
async function harness(_model: string, config: Partial<Config> = {}) {
const ctx = new Context()
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, { models: [model], ...config })
await ctx.plugin(LlmDeepSeek, config)
return ctx
}
@@ -134,6 +134,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('llm-deepseek e2e (real API)', ()
const ctx = await harness(FLASH, { thinking: 'disabled' })
const kinds: string[] = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
model: FLASH,
messages: ask('Count from 1 to 5, digits only.'),
maxTokens: 50,

View File

@@ -5,7 +5,8 @@ import { Context } from 'cordis'
import LlmService, { LlmError, userAgent } from '@deepseek-ai/dsh-llm'
import { SessionId } from '@deepseek-ai/dsh-session'
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. */
@@ -87,7 +88,7 @@ const textEvents = [
async function harness(baseURL: string, config: object = {}) {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, { apiKey: 'test-key', baseURL, models: ['deepseek-v4-flash'], ...config })
await ctx.plugin(LlmDeepSeek, { apiKey: 'test-key', baseURL, ...config })
return ctx
}
@@ -124,6 +125,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const kinds: string[] = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
model: 'deepseek-v4-flash',
messages: [{ role: 'user', content: [{ type: 'text', text: 'hi' }] }],
})) {
@@ -172,7 +174,7 @@ describe('DeepSeekAdapter against a mock server', () => {
status,
body: JSON.stringify({ error: { message: `failed with ${status}`, type: 't', code: 'c' } }),
}
const server = await mockServer([behavior, behavior, behavior])
const server = await mockServer([behavior, behavior])
const ctx = await harness(server.url)
await expect(assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] }))
.rejects.toThrow(`failed with ${status}`)
@@ -180,11 +182,6 @@ describe('DeepSeekAdapter against a mock server', () => {
assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
.catch((error: unknown) => (error as LlmError).code),
).resolves.toBe(code)
// The numeric HTTP status is carried on the error for explicit handling.
await expect(
assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
.catch((error: unknown) => (error as LlmError).status),
).resolves.toBe(status)
})
it('keeps the status-line message for JSON error bodies without a message', async () => {
@@ -212,7 +209,7 @@ describe('DeepSeekAdapter against a mock server', () => {
)
try {
const iterate = async (): Promise<void> => {
for await (const _chunk of adapter.stream({ model: 'm', messages: [] })) { /* drain */ }
for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'm', messages: [] })) { /* drain */ }
}
await expect(iterate()).rejects.toThrow(/no response body/)
} finally {
@@ -238,6 +235,7 @@ describe('DeepSeekAdapter against a mock server', () => {
const pending = (async () => {
const chunks = []
for await (const chunk of ctx.llm.stream({
provider: 'deepseek',
model: 'deepseek-v4-flash',
messages: [],
signal: controller.signal,
@@ -253,25 +251,94 @@ describe('DeepSeekAdapter against a mock server', () => {
})
describe('plugin registration and config', () => {
it('registers the configured models and unregisters on dispose (HMR safety)', async () => {
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()
await ctx.plugin(LlmService)
const fiber = await ctx.plugin(LlmDeepSeek, {
apiKey: 'k',
baseURL: server.url,
models: ['deepseek-v4-flash', 'deepseek-v4-pro'],
})
expect(ctx.llm.models().sort()).toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await fiber.dispose()
expect(ctx.llm.models()).toEqual([])
expect(ctx.llm.listProviders()).toEqual([])
})
it('defaults the model list', async () => {
it('owns the deepseek provider and advertises the default models', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
expect(ctx.llm.models().sort()).toEqual(['deepseek-v4-flash', 'deepseek-v4-pro'])
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'deepseek-v4-flash' },
{ provider: 'deepseek', id: 'deepseek-v4-pro', name: 'deepseek-v4-pro' },
])
})
it('uses the default model catalog when apply is called directly', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
LlmDeepSeek.apply(ctx, { apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'deepseek-v4-flash', name: 'deepseek-v4-flash' },
{ provider: 'deepseek', id: 'deepseek-v4-pro', name: 'deepseek-v4-pro' },
])
})
it('advertises configured models without restricting arbitrary request ids', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, {
apiKey: 'k',
baseURL: 'http://127.0.0.1:1',
models: [
{ id: 'private-fast' },
{ id: 'private-reasoner', name: 'Private Reasoner', description: 'Higher reasoning budget' },
],
})
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([
{ provider: 'deepseek', id: 'private-fast', name: 'private-fast' },
{ provider: 'deepseek', id: 'private-reasoner', name: 'Private Reasoner', description: 'Higher reasoning budget' },
])
})
it('allows an explicit empty model catalog', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, {
apiKey: 'k',
baseURL: 'http://127.0.0.1:1',
models: [],
})
await expect(ctx.llm.listModels('deepseek')).resolves.toEqual([])
})
it.each([
[[{ id: '' }], /ids must be non-empty/],
[[{ id: 'm', name: '' }], /empty name/],
[[{ id: 'm' }, { id: 'm' }], /duplicate catalog model/],
] as const)('rejects invalid advisory model config', async (models, message) => {
const ctx = new Context()
await ctx.plugin(LlmService)
await expect(ctx.plugin(LlmDeepSeek, {
apiKey: 'k',
baseURL: 'http://127.0.0.1:1',
models: [...models],
})).rejects.toThrow(message)
expect(ctx.llm.listProviders()).toEqual([])
})
it('falls back to DEEPSEEK_API_KEY and DEEPSEEK_BASE_URL env vars', async () => {
@@ -280,7 +347,7 @@ describe('plugin registration and config', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.models().length).toBeGreaterThan(0)
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
})
it('throws a clear error when no API key is available', async () => {
@@ -289,7 +356,7 @@ describe('plugin registration and config', () => {
await ctx.plugin(LlmService)
await expect(ctx.plugin(LlmDeepSeek, {}))
.rejects.toThrow(/an API key is required/)
expect(ctx.llm.models()).toEqual([])
expect(ctx.llm.listProviders()).toEqual([])
})
it('prefers explicit config over env for key and base URL', async () => {
@@ -306,7 +373,7 @@ describe('plugin registration and config', () => {
vi.stubEnv('DEEPSEEK_BASE_URL', server.url)
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmDeepSeek, { apiKey: 'k', models: ['deepseek-v4-flash'] })
await ctx.plugin(LlmDeepSeek, { apiKey: 'k' })
await assemble(ctx,{ model: 'deepseek-v4-flash', messages: [] })
expect(server.requests).toHaveLength(1)
})
@@ -318,11 +385,12 @@ describe('plugin registration and config', () => {
await ctx.plugin(LlmService)
// Registration succeeds; no call is made (would hit api.deepseek.com).
await ctx.plugin(LlmDeepSeek, {})
expect(ctx.llm.models().length).toBeGreaterThan(0)
expect(ctx.llm.listProviders()).toEqual([{ id: 'deepseek', name: 'DeepSeek' }])
})
it('adapter is constructible directly for embedding', () => {
it('adapter is constructible directly for embedding', async () => {
const adapter = new DeepSeekAdapter({ apiKey: 'k', baseURL: 'http://127.0.0.1:1' })
expect(adapter).toBeInstanceOf(DeepSeekAdapter)
await expect(adapter.listModels('deepseek')).resolves.toEqual([])
})
})

View File

@@ -15,11 +15,19 @@ export interface AssembledResult {
finish: FinishReason
}
export async function assemble(ctx: Context, options: GenerateOptions): Promise<AssembledResult> {
export async function assemble(ctx: Context, options: Omit<GenerateOptions, 'provider'> & { provider?: string }): Promise<AssembledResult> {
const assembler = new BlockAssembler()
for await (const chunk of ctx.llm.stream(options)) assembler.push(chunk)
const request = { provider: 'deepseek', ...options }
for await (const chunk of ctx.llm.stream(request)) assembler.push(chunk)
return {
message: assembler.message(),
message: {
...assembler.message(),
provenance: {
provider: request.provider,
model: request.model,
...assembler.replayState === undefined ? {} : { replayState: assembler.replayState },
},
},
...assembler.usage !== undefined ? { usage: assembler.usage } : {},
finish: assembler.finish,
}

View File

@@ -1,10 +1,10 @@
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 }
return { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], ...overrides }
}
describe('serializeMessages', () => {

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) {