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

@@ -230,10 +230,10 @@ describe('acp bridge — disposal & HMR safety', () => {
// queryable, with its session still in the store.
const harness = await makeBridgeHarness({ storageDir, script: [] })
const handleA = await harness.ctx.agents.create({
agentId: AgentId('sib-a'), sessionId: SessionId('sib-a'), agentOptions: { model: 'mock' },
agentId: AgentId('sib-a'), sessionId: SessionId('sib-a'), agentOptions: { provider: 'mock', model: 'mock' },
})
const handleB = await harness.ctx.agents.create({
agentId: AgentId('sib-b'), sessionId: SessionId('sib-b'), agentOptions: { model: 'mock' },
agentId: AgentId('sib-b'), sessionId: SessionId('sib-b'), agentOptions: { provider: 'mock', model: 'mock' },
})
expect(harness.ctx.agents.get(AgentId('sib-a'))).toBe(handleA.agent)
expect(harness.ctx.agents.get(AgentId('sib-b'))).toBe(handleB.agent)
@@ -262,7 +262,7 @@ describe('acp bridge — disposal & HMR safety', () => {
const harness = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] })
harness.ctx.on('agent/disposed', () => { throw new Error('boom disposed listener') })
const handle = await harness.ctx.agents.create({
agentId: AgentId('guard-a'), sessionId: SessionId('guard-a'), agentOptions: { model: 'mock' },
agentId: AgentId('guard-a'), sessionId: SessionId('guard-a'), agentOptions: { provider: 'mock', model: 'mock' },
})
handle.agent.send([{ type: 'text', text: 'go' }])
await handle.agent.whenIdle()
@@ -283,7 +283,7 @@ describe('acp bridge — disposal & HMR safety', () => {
// observe the same quiescence boundary.
const harness = await makeBridgeHarness({ storageDir, script: ['hang'] })
const handle = await harness.ctx.agents.create({
agentId: AgentId('conc-a'), sessionId: SessionId('conc-a'), agentOptions: { model: 'mock' },
agentId: AgentId('conc-a'), sessionId: SessionId('conc-a'), agentOptions: { provider: 'mock', model: 'mock' },
})
// Drive a turn that hangs in the model stream, so the loop is mid-turn when
// disposed — its exit runs a final session/flush we can gate to hold the

View File

@@ -27,7 +27,7 @@ describe('acp bridge — demux & config edges', () => {
await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
const before = harness.updates.length
const { agent: foreign } = await harness.ctx.agents.create({ agentId: AgentId('foreign'), sessionId: SessionId('foreign-session'), agentOptions: { model: 'mock' } })
const { agent: foreign } = await harness.ctx.agents.create({ agentId: AgentId('foreign'), sessionId: SessionId('foreign-session'), agentOptions: { provider: 'mock', model: 'mock' } })
foreign.send([{ type: 'text', text: 'hi' }])
await foreign.whenIdle()
await new Promise(r => setTimeout(r, 10))

View File

@@ -155,7 +155,7 @@ export interface BridgeHarness {
* The bridge's `apply` receives the agent-side `Stream` via `config.stream`;
* the test holds the `ClientSideConnection`.
*
* Pass `config: { model: undefined }` to override the default `model: 'mock'`
* Pass `config: { model: undefined }` to override the default mock target
* (the model key is dropped entirely when explicitly undefined).
*/
export async function makeBridgeHarness(options: {
@@ -282,10 +282,11 @@ export async function makeBridgeHarness(options: {
})
// Wire the bridge (agent side) and the client (test side). The test config
// can override `model` (including to undefined): default to 'mock' unless the
// caller explicitly set the key (even to undefined), so a `{ model: undefined }`
// override means "no model at all".
// can override either route field (including to undefined). Default both to
// `mock` unless the caller explicitly set that key, so `{ model: undefined }`
// still means "no model at all".
const cfg: AcpConfig = { stream: agentStream, ...options.config }
if (!(options.config && 'provider' in options.config)) cfg.provider = 'mock'
if (!(options.config && 'model' in options.config)) cfg.model = 'mock'
// Mount the bridge the way production does: as a cordis PLUGIN (via
// `ctx.plugin` with the real `inject`), NOT `AcpPlugin.apply(ctx, cfg)`

View File

@@ -804,5 +804,6 @@ describe('agentOptions', () => {
it('includes only the fields present in config', () => {
expect(agentOptions({})).toEqual({})
expect(agentOptions({ model: 'm' })).toEqual({ model: 'm' })
expect(agentOptions({ provider: 'p', model: 'm' })).toEqual({ provider: 'p', model: 'm' })
})
})