providers becomes a dict keyed by provider route, so the composition base and the llm-pi-ai settings section merge per provider and the route set is structural; the pre-release array shape and per-profile provider field fail loud with migration directions. The adapter reads a profiles thunk once per operation and resolves the credential per stream call (literal apiKey, then apiKeyEnv through ctx.credentials with an ambient env fallback, then pi-ai's provider-native discovery), so key, endpoint, and knob changes reach the next request without restarts. Route-set or captured-retry-policy changes re-register the same adapter instance in one synchronous section; an invalid settings snapshot keeps the last good profiles.
42 lines
1.5 KiB
TypeScript
42 lines
1.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const streamSimple = vi.hoisted(() => vi.fn())
|
|
|
|
// The 0.81 SDK moved `streamSimple` to the compat entry; the adapter imports it
|
|
// from there, so the mock must target the same specifier.
|
|
vi.mock('@earendil-works/pi-ai/compat', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof import('@earendil-works/pi-ai/compat')>()
|
|
return { ...actual, streamSimple }
|
|
})
|
|
|
|
import { PiAiAdapter } from '../src/adapter.ts'
|
|
import { resolveProfiles } from '../src/config.ts'
|
|
|
|
afterEach(() => { streamSimple.mockReset() })
|
|
|
|
describe('pi-ai SDK retry boundary', () => {
|
|
it('pins one SDK attempt even when the installed provider currently defaults to zero retries', async () => {
|
|
const failure = new Error('mock SDK boundary')
|
|
streamSimple.mockReturnValue({
|
|
async * [Symbol.asyncIterator](): AsyncGenerator<never> {
|
|
throw failure
|
|
},
|
|
})
|
|
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',
|
|
model: 'gpt-4.1',
|
|
messages: [],
|
|
})) { /* drain */ }
|
|
}
|
|
|
|
await expect(drain()).rejects.toBe(failure)
|
|
expect(streamSimple).toHaveBeenCalledOnce()
|
|
expect(streamSimple.mock.calls[0]?.[2]).toMatchObject({ maxRetries: 0 })
|
|
})
|
|
})
|