Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input

# Conflicts:
#	docs/architecture.i18n.yaml
#	docs/cordis-catalog/services.md
#	docs/core-data-structures/core.i18n.yaml
#	docs/module-graph.md
#	examples/acp-agent/tests/snapshots/cordis-inspect-jsdoc/session.jsonl
#	packages/README.i18n.yaml
#	packages/llm/llm-pi-ai/README.i18n.yaml
#	packages/llm/llm-pi-ai/package.json
#	packages/llm/llm-pi-ai/src/adapter.ts
#	packages/llm/llm-pi-ai/src/index.ts
#	packages/llm/llm-pi-ai/tests/adapter.spec.ts
#	packages/llm/llm-pi-ai/tsconfig.json
#	packages/llm/llm/README.i18n.yaml
#	pnpm-lock.yaml
#	tsconfig.host.json
This commit is contained in:
creatixchu
2026-07-31 01:11:50 +08:00
166 changed files with 6309 additions and 750 deletions
+7 -6
View File
@@ -23,12 +23,13 @@ async function harness(_model: string, config: Partial<PiAiProviderProfile> = {}
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{
provider: 'deepseek',
...process.env.DEEPSEEK_API_KEY === undefined ? {} : { apiKey: process.env.DEEPSEEK_API_KEY },
...process.env.DEEPSEEK_BASE_URL === undefined ? {} : { baseURL: process.env.DEEPSEEK_BASE_URL },
...config,
}],
providers: {
deepseek: {
...process.env.DEEPSEEK_API_KEY === undefined ? {} : { apiKey: process.env.DEEPSEEK_API_KEY },
...process.env.DEEPSEEK_BASE_URL === undefined ? {} : { baseURL: process.env.DEEPSEEK_BASE_URL },
...config,
},
},
})
return ctx
}
+87 -130
View File
@@ -1,5 +1,3 @@
import { createServer } from 'node:http'
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { AttachmentId, AttachmentStore } from '@deepseek-ai/dsh-attachment'
@@ -16,85 +14,13 @@ import { MAX_TIMER_DELAY_MS } from '@deepseek-ai/dsh-timeout'
import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all'
import { resolveProfiles } from '../src/config.ts'
import { assemble } from './assemble.ts'
interface MockServer {
url: string
paths: string[]
requests: unknown[]
headers: IncomingMessage['headers'][]
readonly closedResponses: number
responseClosed: Promise<void>
}
const servers: Server[] = []
import { closeMockServers, mockServer, textEvents } from './mock-server.ts'
afterEach(async () => {
vi.unstubAllEnvs()
await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve))))
await closeMockServers()
})
async function mockServer(script: {
status?: number
events?: string[]
body?: string
delayMs?: number
headers?: Record<string, string>
}[]): Promise<MockServer> {
const paths: string[] = []
const requests: unknown[] = []
const headers: IncomingMessage['headers'][] = []
let closedResponses = 0
const responseClosed = Promise.withResolvers<undefined>()
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
response.on('close', () => {
closedResponses += 1
responseClosed.resolve(undefined)
})
let body = ''
request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8') })
request.on('end', () => {
paths.push(request.url ?? '')
requests.push(body.length === 0 ? undefined : JSON.parse(body))
headers.push(request.headers)
const behavior = script.shift() ?? { status: 500, body: 'script exhausted' }
if (behavior.status !== undefined && behavior.status !== 200) {
response.writeHead(behavior.status, { 'content-type': 'application/json', ...behavior.headers })
response.end(behavior.body ?? '{}')
return
}
response.writeHead(200, { 'content-type': 'text/event-stream' })
let index = 0
const writeNext = (): void => {
const event = behavior.events?.[index++]
if (event === undefined) { response.end(); return }
response.write(`data: ${event}\n\n`)
if (behavior.delayMs === undefined) writeNext()
else setTimeout(writeNext, behavior.delayMs)
}
writeNext()
})
})
servers.push(server)
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('no port')
return {
url: `http://127.0.0.1:${address.port}`,
paths,
requests,
headers,
responseClosed: responseClosed.promise,
get closedResponses() { return closedResponses },
}
}
const textEvents = [
'{"choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}',
'{"choices":[{"delta":{"content":"hello"},"index":0,"finish_reason":null}]}',
'{"choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
'[DONE]',
]
const IMAGE_REF: ImageAttachmentRef = {
attachmentId: AttachmentId(`sha256:${'a'.repeat(64)}`),
mediaType: 'image/png',
@@ -107,11 +33,19 @@ async function harness(baseURL: string, overrides: Record<string, unknown> = {})
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{ provider: 'deepseek', apiKey: 'test-key', baseURL, ...overrides }],
providers: { deepseek: { apiKey: 'test-key', baseURL, ...overrides } },
})
return ctx
}
/** Direct adapter over the real profile resolver, with literal-key resolution. */
function adapterOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>): PiAiAdapter {
return new PiAiAdapter({
profiles: () => resolveProfiles(providers),
resolveApiKey: (_provider, profile) => Promise.resolve(profile.apiKey),
})
}
describe('PiAiAdapter provider routing', () => {
it('resolves a catalog model dynamically and uses a private endpoint', async () => {
const server = await mockServer([{ events: textEvents }])
@@ -197,8 +131,8 @@ describe('PiAiAdapter provider routing', () => {
const server = await mockServer([{ events: textEvents }])
const ctx = new Context()
await ctx.plugin(LlmService)
ctx.llm.registerAdapter(['deepseek'], new PiAiAdapter({
profiles: [{ provider: 'deepseek', apiKey: 'test-key', baseURL: server.url }],
ctx.llm.registerAdapter(['deepseek'], adapterOf({
deepseek: { apiKey: 'test-key', baseURL: server.url },
}))
const result = await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
@@ -227,7 +161,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{ provider: 'openai', apiKey: 'test-key', baseURL: `${server.url}/v1` }],
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
})
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
expect(result.finish.kind).toBe('error')
@@ -272,7 +206,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{ provider: 'openai', apiKey: 'test-key', baseURL: `${server.url}/v1` }],
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
})
await ctx.plugin(LateAttachmentStore)
@@ -303,7 +237,7 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{ provider: 'openai', apiKey: 'test-key', baseURL: `${server.url}/v1` }],
providers: { openai: { apiKey: 'test-key', baseURL: `${server.url}/v1` } },
})
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] })
@@ -317,12 +251,13 @@ describe('PiAiAdapter provider routing', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{
provider: 'openai',
apiKey: 'test-key',
baseURL: `${server.url}/api/projects/openai/openai/v1`,
headers: { 'api-key': 'test-key', Authorization: '' },
}],
providers: {
openai: {
apiKey: 'test-key',
baseURL: `${server.url}/api/projects/openai/openai/v1`,
headers: { 'api-key': 'test-key', Authorization: '' },
},
},
})
const result = await assemble(ctx, { provider: 'openai', model: 'gpt-5.5', messages: [] })
expect(result.finish.kind).toBe('error')
@@ -404,16 +339,15 @@ describe('provider profile lifecycle', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
const fiber = await ctx.plugin(LlmPiAi, {
providers: [
{
provider: 'openai',
providers: {
openai: {
retryPolicy: {
mode: 'always',
backoff: { initialDelayMs: 25, maxDelayMs: 100, jitterRatio: 0.2 },
},
},
{ provider: 'anthropic' },
],
anthropic: {},
},
})
expect(ctx.llm.listProviders()).toEqual([
{ id: 'openai', name: 'openai' },
@@ -436,7 +370,7 @@ describe('provider profile lifecycle', () => {
it('exposes the installed pi-ai model catalog through provider-neutral metadata', async () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, { providers: [{ provider: 'openai' }] })
await ctx.plugin(LlmPiAi, { providers: { openai: {} } })
const models = await ctx.llm.listModels('openai')
expect(models.find(model => model.id === 'gpt-4.1')).toEqual({
provider: 'openai', id: 'gpt-4.1', name: 'GPT-4.1',
@@ -451,7 +385,7 @@ describe('provider profile lifecycle', () => {
const ctx = new Context()
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: [{ provider: 'deepseek' }, { provider: 'openai' }],
providers: { deepseek: {}, openai: {} },
})
await expect(ctx.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
@@ -485,7 +419,7 @@ describe('provider profile lifecycle', () => {
const supported = new Context()
await supported.plugin(LlmService)
await supported.plugin(LlmPiAi, {
providers: [{ provider: 'deepseek', reasoning: 'max' }],
providers: { deepseek: { reasoning: 'max' } },
})
await expect(supported.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
.resolves.toMatchObject({ reasoning: { defaultEffort: ReasoningEffortId('max') } })
@@ -493,7 +427,7 @@ describe('provider profile lifecycle', () => {
const unsupported = new Context()
await unsupported.plugin(LlmService)
await unsupported.plugin(LlmPiAi, {
providers: [{ provider: 'deepseek', reasoning: 'medium' }],
providers: { deepseek: { reasoning: 'medium' } },
})
await expect(unsupported.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
.rejects.toMatchObject({ code: 'UNSUPPORTED_REASONING_EFFORT' })
@@ -501,7 +435,7 @@ describe('provider profile lifecycle', () => {
const disabled = new Context()
await disabled.plugin(LlmService)
await disabled.plugin(LlmPiAi, {
providers: [{ provider: 'deepseek', reasoning: 'off' }],
providers: { deepseek: { reasoning: 'off' } },
})
await expect(disabled.llm.resolveModelInfo('deepseek', 'deepseek-v4-flash'))
.resolves.toMatchObject({ reasoning: { defaultEffort: ReasoningEffortId('off') } })
@@ -515,24 +449,53 @@ describe('provider profile lifecycle', () => {
expect(server.headers[0]?.authorization).toBe('Bearer ambient-key')
})
it('validates empty, duplicate, unknown, and explicitly blank profiles', () => {
expect(() => resolveProfiles([])).toThrow(/at least one/)
expect(() => resolveProfiles([{ provider: '' }])).toThrow(/non-empty/)
expect(() => resolveProfiles([{ provider: 'not-real' }])).toThrow(/unknown/)
expect(() => resolveProfiles([{ provider: 'openai' }, { provider: 'openai' }])).toThrow(/duplicate/)
expect(() => resolveProfiles([{ provider: 'openai', apiKey: '' }])).toThrow(/empty apiKey/)
expect(() => resolveProfiles([{ provider: 'openai', apiKey: ' ' }])).toThrow(/empty apiKey/)
expect(() => resolveProfiles([{ provider: 'openai', baseURL: '' }])).toThrow(/empty baseURL/)
it('falls back to the ambient environment for apiKeyEnv without the credentials seam', async () => {
vi.stubEnv('PI_CUSTOM_REF_KEY', 'custom-ref-key')
const server = await mockServer([{ events: textEvents }])
const ctx = await harness(server.url, { apiKey: undefined, apiKeyEnv: 'PI_CUSTOM_REF_KEY' })
await assemble(ctx, { model: 'deepseek-v4-flash', messages: [] })
expect(server.headers[0]?.authorization).toBe('Bearer custom-ref-key')
})
it('fails a named-but-missing apiKeyEnv instead of using another ambient key', async () => {
// The exact confusion this guards: the named reference is empty while an
// unrelated provider key sits in the environment. Deferring to pi-ai's own
// discovery here would authenticate as another tenant.
vi.stubEnv('PI_CUSTOM_REF_KEY', '')
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
const server = await mockServer([{ events: textEvents }])
const ctx = await harness(server.url, { apiKey: undefined, apiKeyEnv: 'PI_CUSTOM_REF_KEY' })
await expect(assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }))
.rejects.toMatchObject({ code: 'MISSING_CREDENTIAL' })
await expect(assemble(ctx, { model: 'deepseek-v4-flash', messages: [] }))
.rejects.toThrow(/provider route "deepseek".*PI_CUSTOM_REF_KEY/s)
expect(server.requests).toHaveLength(0)
})
it('validates empty, unknown, legacy-shaped, and explicitly blank profiles', () => {
// Empty and omitted dicts are the dormant zero-route posture, not errors.
expect(resolveProfiles({}).size).toBe(0)
expect(resolveProfiles(undefined).size).toBe(0)
expect(() => resolveProfiles({ '': {} })).toThrow(/non-empty/)
expect(() => resolveProfiles({ 'not-real': {} })).toThrow(/unknown/)
// The pre-release array shape and its per-profile provider field fail
// loud with migration directions instead of half-working.
expect(() => resolveProfiles([{ provider: 'openai' }] as never)).toThrow(/dict keyed by provider/)
expect(() => resolveProfiles({ openai: { provider: 'openai' } as never })).toThrow(/moved to the providers dict key/)
expect(() => resolveProfiles({ openai: { apiKey: '' } })).toThrow(/empty apiKey/)
expect(() => resolveProfiles({ openai: { apiKey: ' ' } })).toThrow(/empty apiKey/)
expect(() => resolveProfiles({ openai: { baseURL: '' } })).toThrow(/empty baseURL/)
expect(() => resolveProfiles({ openai: { apiKeyEnv: 'not-a-var!' } })).toThrow(/must match/)
})
it.each(['maxRetries', 'maxRetryDelayMs'] as const)(
'rejects removed profile field %s instead of silently restoring hidden SDK retries',
async (field) => {
const legacy = { provider: 'openai', [field]: 2 }
expect(() => resolveProfiles([legacy as never])).toThrow(/removed.*agent recovery/i)
const legacy = { [field]: 2 }
expect(() => resolveProfiles({ openai: legacy })).toThrow(/removed.*agent recovery/i)
const ctx = new Context()
await ctx.plugin(LlmService)
await expect(ctx.plugin(LlmPiAi, { providers: [legacy as never] }))
await expect(ctx.plugin(LlmPiAi, { providers: { openai: legacy } }))
.rejects.toThrow(/removed.*agent recovery/i)
},
)
@@ -548,30 +511,26 @@ describe('provider profile lifecycle', () => {
for (const entry of invalid) {
const ctx = new Context()
await ctx.plugin(LlmService)
await expect(ctx.plugin(LlmPiAi, { providers: [{ provider: 'openai', ...entry }] }))
await expect(ctx.plugin(LlmPiAi, { providers: { openai: { ...entry } } }))
.rejects.toThrow()
}
})
it('rejects invalid nested retryPolicy at the provider-profile boundary', async () => {
expect(() => resolveProfiles([{
provider: 'openai',
retryPolicy: { mode: 'always', backoff: { jitterRatio: -1 } },
}])).toThrow(/retryPolicy\.backoff\.jitterRatio/)
expect(() => resolveProfiles({
openai: { retryPolicy: { mode: 'always', backoff: { jitterRatio: -1 } } },
})).toThrow(/retryPolicy\.backoff\.jitterRatio/)
const ctx = new Context()
await ctx.plugin(LlmService)
await expect(ctx.plugin(LlmPiAi, {
providers: [{
provider: 'openai',
retryPolicy: { mode: 'normal', maxRetries: -1 },
}],
providers: { openai: { retryPolicy: { mode: 'normal', maxRetries: -1 } } },
})).rejects.toThrow(/retryPolicy/)
expect(ctx.llm.listProviders()).toEqual([])
})
it('constructs the adapter directly and rejects routes it does not own', async () => {
const adapter = new PiAiAdapter({ profiles: [{ provider: 'openai' }] })
const adapter = adapterOf({ openai: {} })
await expect(adapter.listModels('anthropic')).rejects.toMatchObject({ code: 'NO_ADAPTER' })
await expect(adapter.resolveModel('anthropic', 'claude-sonnet-4'))
.rejects.toMatchObject({ code: 'NO_ADAPTER' })
@@ -584,9 +543,7 @@ describe('provider profile lifecycle', () => {
})
it('rejects unsupported or unresolved image input before provider I/O', async () => {
const adapter = new PiAiAdapter({
profiles: [{ provider: 'openai' }, { provider: 'deepseek' }],
})
const adapter = adapterOf({ openai: {}, deepseek: {} })
const drain = async (options: Parameters<PiAiAdapter['stream']>[0]): Promise<void> => {
for await (const _chunk of adapter.stream(options)) { /* drain */ }
}
@@ -625,12 +582,12 @@ describe('provider profile lifecycle', () => {
})).rejects.toMatchObject({ code: 'UNSUPPORTED_CONTENT' })
})
it('validates direct-constructor profiles at the embedding boundary', () => {
expect(() => new PiAiAdapter({
profiles: [{ provider: 'openai', streamIdleTimeoutMs: 0 }],
it('validates profiles at the shared resolver boundary', () => {
expect(() => resolveProfiles({
openai: { streamIdleTimeoutMs: 0 },
})).toThrow(/streamIdleTimeoutMs.*positive finite/)
expect(() => new PiAiAdapter({
profiles: [{ provider: 'openai', streamIdleTimeoutMs: MAX_TIMER_DELAY_MS + 1 }],
expect(() => resolveProfiles({
openai: { streamIdleTimeoutMs: MAX_TIMER_DELAY_MS + 1 },
})).toThrow(/streamIdleTimeoutMs.*no greater/)
})
})
@@ -641,7 +598,7 @@ describe('abort wiring', () => {
const message = Object.defineProperty({}, 'content', {
get() { throw original },
})
const adapter = new PiAiAdapter({ profiles: [{ provider: 'deepseek', apiKey: 'test-key' }] })
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({
provider: 'deepseek',
@@ -662,7 +619,7 @@ describe('abort wiring', () => {
throw original
},
})
const adapter = new PiAiAdapter({ profiles: [{ provider: 'deepseek', apiKey: 'test-key' }] })
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({
provider: 'deepseek',
@@ -676,7 +633,7 @@ describe('abort wiring', () => {
})
it('resolves catalog endpoints without an override before honoring pre-abort', async () => {
const adapter = new PiAiAdapter({ profiles: [{ provider: 'deepseek', apiKey: 'test-key' }] })
const adapter = adapterOf({ deepseek: { apiKey: 'test-key' } })
const controller = new AbortController()
controller.abort('already stopped')
const chunks = []
@@ -0,0 +1,189 @@
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([])
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'])
await expect(assemble(ctx, { provider: 'deepseek', model: 'deepseek-v4-flash', messages: [] }))
.rejects.toMatchObject({ 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('keeps the last good profiles when a settings snapshot names an unknown provider', async () => {
const dir = await home()
const ctx = await boot(dir, { providers: { openai: {} } })
// Schema-valid but catalog-invalid: the resolver rejects it and the
// last good route set keeps serving.
await ctx.settings.update(NS, { providers: { 'not-a-real-provider': {} } })
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)
})
})
@@ -0,0 +1,116 @@
/**
* Real-composition guard for the dormant pi-ai posture: LlmService,
* settings-local, credentials-local, and a bare `llm-pi-ai` row boot from a
* test-only cordis.yml through the actual Loader + Include path, an external
* edit of settings.yaml registers the route live, and the next request
* carries the credential the .env supplies. A hand-mounted `ctx.plugin` cannot
* catch Loader export-shape failures, which is why the twin adapter has the
* same guard.
*/
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
import LlmService from '@deepseek-ai/dsh-llm'
import CredentialsLocal from '@deepseek-ai/dsh-credentials-local'
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'
let root: string | undefined
let context: Context | undefined
afterEach(async () => {
await context?.fiber.dispose()
context = undefined
if (root !== undefined) await rm(root, { recursive: true, force: true })
root = undefined
await closeMockServers()
vi.unstubAllEnvs()
})
/** Boot the dormant composition: a bare `llm-pi-ai` row with no config at all. */
async function loadComposition(): Promise<{ ctx: Context; settingsPath: string }> {
root = await mkdtemp(join(tmpdir(), 'dsh-pi-composition-'))
const settingsPath = join(root, 'settings.yaml')
await writeFile(settingsPath, '# personal settings\n')
await writeFile(join(root, '.env'), 'PI_COMPOSITION_KEY=key-from-store\n')
const configPath = join(root, 'cordis.yml')
await writeFile(configPath, [
'- id: llm',
" name: 'test-llm-service'",
'- id: settings',
" name: '@deepseek-ai/dsh-settings-local'",
' config:',
` path: ${JSON.stringify(settingsPath)}`,
' debounceMs: 10',
'- id: credentials',
" name: '@deepseek-ai/dsh-credentials-local'",
' config:',
` path: ${JSON.stringify(join(root, '.env'))}`,
' debounceMs: 10',
'- id: llm-pi-ai',
" name: '@deepseek-ai/dsh-llm-pi-ai'",
'',
].join('\n'))
const ctx = new Context()
context = ctx
ctx.baseUrl = pathToFileURL(root).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
const modules = new Map<string, unknown>([
['test-llm-service', LlmService],
['@deepseek-ai/dsh-settings-local', SettingsLocal],
['@deepseek-ai/dsh-credentials-local', CredentialsLocal],
['@deepseek-ai/dsh-llm-pi-ai', LlmPiAi],
])
ctx.loader.internal = {
version: 'v2',
async import(specifier: string) {
if (!modules.has(specifier)) throw new Error(`unexpected Loader import: ${specifier}`)
return modules.get(specifier)
},
} as unknown as NonNullable<typeof ctx.loader.internal>
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(configPath).href },
})
await ctx.loader.await()
return { ctx, settingsPath }
}
describe('llm-pi-ai real dormant composition', () => {
it('boots with zero routes and registers one the moment settings supply a profile', async () => {
vi.stubEnv('PI_COMPOSITION_KEY', '')
const server = await mockServer([{ events: textEvents }])
const { ctx, settingsPath } = await loadComposition()
// The shipped posture: the adapter exists, no route does.
expect(ctx.llm.listProviders()).toEqual([])
// Exactly what the web Models page leaves on disk.
await writeFile(settingsPath, [
'llm-pi-ai:',
' providers:',
' deepseek:',
' apiKeyEnv: PI_COMPOSITION_KEY',
` baseURL: ${server.url}`,
'',
].join('\n'))
await vi.waitFor(() => {
expect(ctx.llm.listProviders().map(provider => provider.id)).toEqual(['deepseek'])
}, { timeout: 5000 })
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 key-from-store')
})
})
@@ -0,0 +1,82 @@
import { createServer } from 'node:http'
import type { IncomingMessage, Server, ServerResponse } from 'node:http'
export interface MockServer {
url: string
paths: string[]
requests: unknown[]
headers: IncomingMessage['headers'][]
readonly closedResponses: number
responseClosed: Promise<void>
}
const servers: Server[] = []
/** Close every server opened since the last call; run from each spec's afterEach. */
export async function closeMockServers(): Promise<void> {
await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(resolve))))
}
/** A minimal complete text generation in pi-ai's chat-completions shape. */
export const textEvents = [
'{"choices":[{"delta":{"role":"assistant","content":""},"index":0,"finish_reason":null}]}',
'{"choices":[{"delta":{"content":"hello"},"index":0,"finish_reason":null}]}',
'{"choices":[{"delta":{},"index":0,"finish_reason":"stop"}],"usage":{"prompt_tokens":3,"completion_tokens":1}}',
'[DONE]',
]
/** Local provider stand-in: replays scripted behaviors per request. */
export async function mockServer(script: {
status?: number
events?: string[]
body?: string
delayMs?: number
headers?: Record<string, string>
}[]): Promise<MockServer> {
const paths: string[] = []
const requests: unknown[] = []
const headers: IncomingMessage['headers'][] = []
let closedResponses = 0
const responseClosed = Promise.withResolvers<undefined>()
const server = createServer((request: IncomingMessage, response: ServerResponse) => {
response.on('close', () => {
closedResponses += 1
responseClosed.resolve(undefined)
})
let body = ''
request.on('data', (chunk: Buffer) => { body += chunk.toString('utf8') })
request.on('end', () => {
paths.push(request.url ?? '')
requests.push(body.length === 0 ? undefined : JSON.parse(body))
headers.push(request.headers)
const behavior = script.shift() ?? { status: 500, body: 'script exhausted' }
if (behavior.status !== undefined && behavior.status !== 200) {
response.writeHead(behavior.status, { 'content-type': 'application/json', ...behavior.headers })
response.end(behavior.body ?? '{}')
return
}
response.writeHead(200, { 'content-type': 'text/event-stream' })
let index = 0
const writeNext = (): void => {
const event = behavior.events?.[index++]
if (event === undefined) { response.end(); return }
response.write(`data: ${event}\n\n`)
if (behavior.delayMs === undefined) writeNext()
else setTimeout(writeNext, behavior.delayMs)
}
writeNext()
})
})
servers.push(server)
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve))
const address = server.address()
if (address === null || typeof address === 'string') throw new Error('no port')
return {
url: `http://127.0.0.1:${address.port}`,
paths,
requests,
headers,
responseClosed: responseClosed.promise,
get closedResponses() { return closedResponses },
}
}
@@ -56,12 +56,11 @@ async function harness(image?: StoredImageAttachment): Promise<Context> {
contexts.push(ctx)
await ctx.plugin(LlmService)
await ctx.plugin(LlmPiAi, {
providers: providerCases.map(profile => ({
provider: profile.provider,
providers: Object.fromEntries(providerCases.map(profile => [profile.provider, {
...profile.apiKey === undefined ? {} : { apiKey: profile.apiKey },
...profile.baseURL === undefined ? {} : { baseURL: profile.baseURL },
...profile.headers === undefined ? {} : { headers: profile.headers },
})),
}])),
})
if (image !== undefined) {
const fixture = image
@@ -10,6 +10,7 @@ vi.mock('@earendil-works/pi-ai/compat', async (importOriginal) => {
})
import { PiAiAdapter } from '../src/adapter.ts'
import { resolveProfiles } from '../src/config.ts'
afterEach(() => { streamSimple.mockReset() })
@@ -21,7 +22,10 @@ describe('pi-ai SDK retry boundary', () => {
throw failure
},
})
const adapter = new PiAiAdapter({ profiles: [{ provider: 'openai', apiKey: 'test-key' }] })
const adapter = new PiAiAdapter({
profiles: () => resolveProfiles({ openai: { apiKey: 'test-key' } }),
resolveApiKey: () => Promise.resolve('test-key'),
})
const drain = async (): Promise<void> => {
for await (const _chunk of adapter.stream({
provider: 'openai',