feat(llm): route adapters by provider

This commit is contained in:
Yichen Jiang
2026-07-14 21:57:52 +08:00
parent a0359bc4a9
commit e547980d77
218 changed files with 2605 additions and 1844 deletions

View File

@@ -37,7 +37,7 @@ async function setup(script: Script) {
await ctx.plugin(spawn, { providerName: 'spawn' })
await ctx.plugin(WorkerWorkflowEngine, {})
ctx.llm.registerAdapter(['mock'], adapter)
const parent = ctx.agentLoop.create(AgentId('parent'), { model: 'mock' })
const parent = ctx.agentLoop.create(AgentId('parent'), { provider: 'mock', model: 'mock' })
return { ctx, parent, adapter }
}

View File

@@ -33,7 +33,7 @@ describe('validateMeta', () => {
description: 'migrate call sites',
whenToUse: 'large mechanical sweeps',
phases: [
{ title: 'Discover' },
{ title: 'Discover', provider: 'openai' },
{ title: 'Transform', detail: 'one agent per file', model: 'deepseek-v4-pro' },
],
})
@@ -42,7 +42,7 @@ describe('validateMeta', () => {
description: 'migrate call sites',
whenToUse: 'large mechanical sweeps',
phases: [
{ title: 'Discover' },
{ title: 'Discover', provider: 'openai' },
{ title: 'Transform', detail: 'one agent per file', model: 'deepseek-v4-pro' },
],
})
@@ -73,6 +73,7 @@ describe('validateMeta', () => {
expectInvalid({ name: 'x', description: 'd', phases: [{ title: '' }] }, 'meta.phases[0].title must be a non-empty string')
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', order: 1 }] }, 'meta.phases[0].order is not a recognized field')
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', detail: 9 }] }, 'meta.phases[0].detail must be a string')
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', provider: 9 }] }, 'meta.phases[0].provider must be a string')
expectInvalid({ name: 'x', description: 'd', phases: [{ title: 'Scan', model: 9 }] }, 'meta.phases[0].model must be a string')
})

View File

@@ -35,7 +35,7 @@ interface FakeHost {
interface FakeHostOptions {
/** Auto-respond to child-start: reply started + settled per child index. Omit a reply to leave the child pending. */
reply?: (request: { prompt: string; schema?: unknown; model?: string }, index: number) => ChildResult | undefined
reply?: (request: { prompt: string; schema?: unknown; provider?: string; model?: string }, index: number) => ChildResult | undefined
/** Reject the start instead (child-start-error) when returning a string. */
refuse?: (index: number) => string | undefined
/** Auto-send `go` on `ready` (default true). */
@@ -143,6 +143,17 @@ describe('runWorkerSession over an in-process MessageChannel', () => {
host.close()
})
it('agent({provider}) forwards a provider without inventing a model', async () => {
const host = fakeHost({ reply: () => text('ok') })
void runWorkerSession(host.port, init("return await agent('route me', { provider: 'openai' })"))
const result = await host.result()
expect(result.value).toBe('ok')
const start = host.ofType(WorkerToHostType.ChildStart)[0]!
expect(start.request.provider).toBe('openai')
expect(start.request.model).toBeUndefined()
host.close()
})
it('a schema child completing WITHOUT a structured value resolves null with a failed outcome', async () => {
const host = fakeHost({ reply: () => text('prose, no structure') })
void runWorkerSession(host.port, init("return await agent('p', { schema: { type: 'object' } })"))

View File

@@ -35,7 +35,7 @@ async function harness(): Promise<Context> {
await built.plugin(ToolRegistry)
await built.plugin(AgentRegistry)
await built.plugin(AgentLoop, { agents: [] })
await built.plugin(LlmDeepSeek, { models: ['deepseek-v4-flash'] })
await built.plugin(LlmDeepSeek)
await built.plugin(SubagentService)
await built.plugin(Spawn, { providerName: 'spawn' })
await built.plugin(WorkerWorkflowEngine, { provider: 'spawn' })
@@ -64,7 +64,7 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY)('worker workflow engine with-key
const parentHandle = await ctx.agents.create({
agentId: AgentId('wf-worker-e2e-parent'),
sessionId: 'wf-worker-e2e-session' as never,
agentOptions: { model: 'deepseek-v4-flash' },
agentOptions: { provider: 'deepseek', model: 'deepseek-v4-flash' },
})
const events: string[] = []

View File

@@ -236,6 +236,14 @@ describe('dsh-workflow-workerthread', () => {
expect(provider.runs[0]!.request.parent).toBeDefined()
})
it('agent({provider}) forwards provider-only agentOptions across the thread', async () => {
const { ctx, parent, provider } = await setup()
const result = await run(ctx, parent, scripted("return await agent('route me', { provider: 'openai' })"))
expect(result.value).toBe('stub reply')
expect(provider.runs[0]!.request.agentOptions).toEqual({ provider: 'openai' })
})
it('a fatal hook error inside the worker kills the script and reports the error', async () => {
const { ctx, parent } = await setup()
const result = await run(ctx, parent, scripted("return await parallel([() => agent('x', { isolation: 'worktree' })])"))