mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
# Conflicts: # packages/llm/llm-pi-ai/tests/adapter.spec.ts # packages/llm/llm/README.i18n.yaml
203 lines
8.9 KiB
TypeScript
203 lines
8.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { Context } from 'cordis'
|
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import LlmService, { LlmAdapter } from '@deepseek-ai/dsh-llm'
|
|
import { credentialRef } from '@deepseek-ai/dsh-credentials'
|
|
import { CredentialsLocal } from '@deepseek-ai/dsh-credentials-local'
|
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
import { SettingsLocal } from '@deepseek-ai/dsh-settings-local'
|
|
import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai'
|
|
import { assemble } from './assemble.ts'
|
|
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
|
|
|
|
const NS = settingsNamespace('llm-pi-ai')
|
|
|
|
/** Minimal foreign adapter: only needs to own a route the pi-ai plugin then wants. */
|
|
class StubAdapter extends LlmAdapter {
|
|
|
|
override async * stream(): AsyncIterable<never> {
|
|
throw new Error('stub adapter must never stream')
|
|
}
|
|
}
|
|
|
|
const cleanups: Array<() => Promise<void>> = []
|
|
|
|
afterEach(async () => {
|
|
while (cleanups.length > 0) await cleanups.pop()!()
|
|
await closeMockServers()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
async function home(): Promise<string> {
|
|
const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-dynamic-'))
|
|
cleanups.push(() => rm(dir, { recursive: true, force: true }))
|
|
return dir
|
|
}
|
|
|
|
/** Real dynamic composition mirroring the deepseek twin's harness. */
|
|
async function boot(dir: string, config: LlmPiAi.Config): Promise<Context> {
|
|
const ctx = new Context()
|
|
cleanups.push(async () => {
|
|
await ctx.fiber.dispose()
|
|
})
|
|
await ctx.plugin(LlmService)
|
|
await ctx.plugin(SettingsLocal, { path: join(dir, 'settings.yaml'), watch: false })
|
|
await ctx.plugin(CredentialsLocal, { path: join(dir, '.env'), watch: false })
|
|
await ctx.plugin(LlmPiAi, config)
|
|
return ctx
|
|
}
|
|
|
|
describe('request-level dynamic profiles', () => {
|
|
it('mounts bare and dormant, then registers routes the moment settings supply providers', async () => {
|
|
vi.stubEnv('PI_DYNAMIC_KEY', '')
|
|
const dir = await home()
|
|
await writeFile(join(dir, '.env'), 'PI_DYNAMIC_KEY=pk-from-settings\n')
|
|
const server = await mockServer([{ events: textEvents }])
|
|
// The exact product posture: `- id: llm-pi-ai` with no config at all.
|
|
const ctx = await boot(dir, {})
|
|
|
|
expect(ctx.llm.listProviders()).toEqual([])
|
|
// Dormant ≠ invisible: every installed catalog provider is configurable
|
|
// before any route exists, each addressed inside the providers dict.
|
|
const directory = ctx.llm.listConfigurableProviders()
|
|
expect(directory.length).toBeGreaterThan(30)
|
|
expect(directory).toContainEqual({
|
|
provider: 'openai',
|
|
displayName: 'openai',
|
|
settingsNs: 'llm-pi-ai',
|
|
settingsPath: ['providers', 'openai'],
|
|
})
|
|
await ctx.settings.update(NS, {
|
|
providers: { deepseek: { apiKeyEnv: 'PI_DYNAMIC_KEY', baseURL: server.url } },
|
|
})
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['deepseek'])
|
|
await expect(ctx.llm.listModels('deepseek')).resolves.not.toHaveLength(0)
|
|
|
|
const result = await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(result.message.content).toEqual([{ type: 'text', text: 'hello' }])
|
|
expect(server.headers[0]?.authorization).toBe('Bearer pk-from-settings')
|
|
|
|
// Emptying the user layer returns the adapter to its dormant state.
|
|
await ctx.settings.replace(NS, {})
|
|
expect(ctx.llm.listProviders()).toEqual([])
|
|
})
|
|
|
|
it('adds a provider route from settings and drops it when the user layer resets', async () => {
|
|
const dir = await home()
|
|
const server = await mockServer([{ events: textEvents }])
|
|
const ctx = await boot(dir, {
|
|
providers: { openai: { apiKey: 'k', baseURL: 'http://127.0.0.1:1/v1' } },
|
|
})
|
|
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai'])
|
|
await ctx.settings.update(NS, {
|
|
providers: { deepseek: { apiKey: 'live-key', baseURL: server.url } },
|
|
})
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai', 'deepseek'])
|
|
|
|
const result = await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(result.message.content).toEqual([{ type: 'text', text: 'hello' }])
|
|
expect(server.headers[0]?.authorization).toBe('Bearer live-key')
|
|
|
|
// Reset the user layer: the settings-born route unregisters, the
|
|
// composition route stays.
|
|
await ctx.settings.replace(NS, {})
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai'])
|
|
const removed = await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(removed.finish).toMatchObject({ kind: 'error', failure: { code: 'NO_ADAPTER' } })
|
|
})
|
|
|
|
it('rotates the per-request credential referenced by apiKeyEnv', async () => {
|
|
vi.stubEnv('PI_DYNAMIC_KEY', '')
|
|
const dir = await home()
|
|
await writeFile(join(dir, '.env'), 'PI_DYNAMIC_KEY=pk-one\n')
|
|
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
|
|
const ctx = await boot(dir, {
|
|
providers: { deepseek: { apiKeyEnv: 'PI_DYNAMIC_KEY', baseURL: server.url } },
|
|
})
|
|
|
|
await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(server.headers[0]?.authorization).toBe('Bearer pk-one')
|
|
|
|
await ctx.credentials.set(credentialRef('PI_DYNAMIC_KEY'), 'pk-two')
|
|
await assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] })
|
|
expect(server.headers[1]?.authorization).toBe('Bearer pk-two')
|
|
})
|
|
|
|
it('re-registers routes in place when a captured retry policy changes', async () => {
|
|
const dir = await home()
|
|
const ctx = await boot(dir, { providers: { openai: {} } })
|
|
|
|
await ctx.settings.update(NS, {
|
|
providers: {
|
|
openai: {
|
|
retryPolicy: { mode: 'always', backoff: { initialDelayMs: 25, maxDelayMs: 100, jitterRatio: 0.2 } },
|
|
},
|
|
},
|
|
})
|
|
expect(ctx.llm.providerRetryPolicy('openai')).toEqual({
|
|
mode: 'always',
|
|
initialDelayMs: 25,
|
|
maxDelayMs: 100,
|
|
jitterRatio: 0.2,
|
|
})
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai'])
|
|
})
|
|
|
|
it('refuses a settings write this adapter could not serve, leaving its routes alone', async () => {
|
|
const dir = await home()
|
|
const ctx = await boot(dir, { providers: { openai: {} } })
|
|
|
|
// Shape-valid but unserviceable: a route the catalog does not ship and
|
|
// that lists no models of its own. The section schema resolves the whole
|
|
// profile set, so this is refused where it is written rather than stored
|
|
// and then quietly disabling every route in the namespace.
|
|
await expect(ctx.settings.update(NS, { providers: { 'not-a-real-provider': {} } }))
|
|
.rejects.toThrow(/resolves no models/)
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['openai'])
|
|
})
|
|
|
|
it('keeps serving its routes when a settings-born route collides with another adapter', async () => {
|
|
const dir = await home()
|
|
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
|
|
const ctx = await boot(dir, { providers: { openai: { apiKey: 'pk', baseURL: `${server.url}/v1` } } })
|
|
// Another adapter owns `anthropic`; the registry must refuse to hand it over.
|
|
ctx.llm.registerAdapter(['anthropic'], new StubAdapter())
|
|
|
|
await ctx.settings.update(NS, {
|
|
providers: {
|
|
openai: { apiKey: 'pk', baseURL: `${server.url}/v1` },
|
|
anthropic: { apiKey: 'other' },
|
|
},
|
|
})
|
|
|
|
// The conflicting swap was refused whole: the previous route set still
|
|
// owns openai (an eager dispose would have dropped it), and anthropic
|
|
// still belongs to its original adapter.
|
|
expect(ctx.llm.listProviders().map(provider => provider.id).sort()).toEqual(['anthropic', 'openai'])
|
|
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
|
|
expect(result.finish.kind).toBe('error')
|
|
expect(server.paths).toEqual(['/v1/responses'])
|
|
|
|
// Reverting to the working configuration re-applies, even though its
|
|
// facts equal the ones the registry already holds.
|
|
await ctx.settings.replace(NS, {})
|
|
expect(ctx.llm.listProviders().map(provider => provider.id).sort()).toEqual(['anthropic', 'openai'])
|
|
await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
|
|
expect(server.paths).toEqual(['/v1/responses', '/v1/responses'])
|
|
})
|
|
|
|
it('ignores a settings document that merely reorders its provider keys', async () => {
|
|
const dir = await home()
|
|
const ctx = await boot(dir, { providers: { openai: {}, anthropic: {} } })
|
|
const before = ctx.llm.listProviders().map(provider => provider.id)
|
|
|
|
// Same routes, different YAML key order: nothing about the registration
|
|
// changed, so no swap should happen at all.
|
|
await ctx.settings.update(NS, { providers: { anthropic: {}, openai: {} } })
|
|
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(before)
|
|
})
|
|
})
|