import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { Context } from '@deepseek-ai/cordis' import LlmRuntime, { createUserMessage } from '@deepseek-ai/dsh-llm' import type { StreamChunk } from '@deepseek-ai/dsh-llm' import FileSettingsProvider from '@deepseek-ai/dsh-settings-file' import { settingsNamespace } from '@deepseek-ai/dsh-settings' import * as LlmPiAi from '@deepseek-ai/dsh-llm-pi-ai' import { PiAiAdapter } from '@deepseek-ai/dsh-llm-pi-ai' import { getBuiltinModels } from '@earendil-works/pi-ai/providers/all' import { createModels, getSupportedThinkingLevels } from '@earendil-works/pi-ai' import type { Api, Model, OpenAICompletionsCompat, Provider } from '@earendil-works/pi-ai' import { resolveProfiles } from '../src/config.ts' import { buildProvider, supportedProtocols } from '../src/provider.ts' import { assemble } from './assemble.ts' import { closeMockServers, mockServer, textEvents } from './mock-server.ts' const homes: string[] = [] // Routes name their credential by reference; the value lives in the // environment, which is the layer the adapter falls back to without a // mounted credentials seam. const KEY_ENV = 'PI_TEST_KEY' beforeEach(() => { vi.stubEnv(KEY_ENV, 'test-key') }) afterEach(async () => { vi.unstubAllEnvs() await closeMockServers() await Promise.all(homes.splice(0).map(dir => rm(dir, { recursive: true, force: true }))) }) /** A throwaway $DSH_HOME with an empty settings document. */ async function home(): Promise { const dir = await mkdtemp(join(tmpdir(), 'dsh-pi-catalog-')) homes.push(dir) await writeFile(join(dir, 'settings.yaml'), '') return dir } /** The dormant composition plus a real settings service, as the product mounts it. */ async function bootWithSettings(dir: string, config: LlmPiAi.Config): Promise { const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(FileSettingsProvider, { path: join(dir, 'settings.yaml'), watch: false }) await ctx.plugin(LlmPiAi, config) return ctx } /** A complete hand-declared route: nothing about it exists in pi-ai's catalog. */ function gateway(baseURL: string, overrides: Record = {}): LlmPiAi.Config { return { providers: { 'acme-gateway': { apiKeyEnv: KEY_ENV, displayName: 'Acme Gateway', api: 'openai-completions', baseURL, models: [{ id: 'acme-large', name: 'Acme Large', contextWindow: 65_536, maxTokens: 4096 }], ...overrides, }, }, } } async function harness(config: LlmPiAi.Config): Promise { const ctx = new Context() await ctx.plugin(LlmRuntime) await ctx.plugin(LlmPiAi, config) return ctx } describe('hand-declared providers', () => { it('serves a route pi-ai has never heard of from its own declaration', async () => { const server = await mockServer([{ events: textEvents }]) const ctx = await harness(gateway(`${server.url}/v1`)) const result = await assemble(ctx, { provider: 'acme-gateway', model: 'acme-large', messages: [createUserMessage({ content: [{ type: 'text', text: 'hi' }], source: { kind: 'plugin', plugin: 'test' }, })], }) expect(result.message.content).toEqual([{ type: 'text', text: 'hello' }]) expect(result.finish).toEqual({ kind: 'stop' }) expect(server.paths).toEqual(['/v1/chat/completions']) // The reference resolved through the environment and reached the wire. expect(server.headers[0]?.authorization).toBe('Bearer test-key') }) it('lists and resolves the declared models rather than a catalog', async () => { const server = await mockServer([]) const ctx = await harness(gateway(`${server.url}/v1`)) expect(await ctx.llm.listModels('acme-gateway')).toEqual([ { provider: 'acme-gateway', id: 'acme-large', name: 'Acme Large', inputModalities: ['text'] }, ]) const info = await ctx.llm.resolveModelInfo('acme-gateway', 'acme-large') expect(info).toMatchObject({ provider: 'acme-gateway', id: 'acme-large', name: 'Acme Large', context: { contextWindow: 65_536 }, defaultMaxTokens: 4096, }) }) it('offers no reasoning control it could not honour', async () => { const server = await mockServer([]) const ctx = await harness(gateway(`${server.url}/v1`)) // pi-ai reports a model with no reasoning metadata as supporting the single // level `off`, but `off` is translated to *omitting* the reasoning option — // byte-for-byte the same request as naming no effort — so a provider whose // own default is to think would keep thinking with `off` selected. The // capability is reported unavailable instead of offering that control. expect((await ctx.llm.resolveModelInfo('acme-gateway', 'acme-large')).reasoning).toBeUndefined() // A catalog route is unaffected: its models carry the metadata that makes // `off` actually disable thinking. const withCatalog = await harness({ providers: { deepseek: { baseURL: server.url } } }) const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') expect((await withCatalog.llm.resolveModelInfo('deepseek', catalogModel.id)).reasoning?.efforts.map(e => e.id)) .toContain('off') }) it('joins the configurable-provider directory so a settings surface can reach it', async () => { const server = await mockServer([]) const ctx = await harness(gateway(`${server.url}/v1`)) const directory = ctx.llm.listConfigurableProviders() expect(directory).toContainEqual({ provider: 'acme-gateway', displayName: 'Acme Gateway', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'acme-gateway'], // Nothing in the installed catalog answers for this route, which is what // configuration surfaces mark as a route this deployment declared. declared: true, }) // Membership of the catalog, not of the settings document: a shipped // provider carries a stored profile the moment anyone corrects it. expect(directory.filter(entry => entry.declared).map(entry => entry.provider)) .toEqual(['acme-gateway']) expect(directory.find(entry => entry.provider === 'deepseek')?.declared).toBe(false) }) it('sizes a model the catalog cannot describe from the route\u2019s own fallbacks', () => { const resolved = resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', // A listing endpoint that discloses nothing but ids still yields a // serviceable route. models: [{ id: 'bare' }, { id: 'sized', contextWindow: 8192, maxTokens: 512 }], }, 'tuned-gateway': { api: 'openai-completions', baseURL: 'https://tuned.test', defaultContextWindow: 4096, defaultMaxTokens: 256, models: [{ id: 'bare' }], }, }) const modelsOf = (route: string): readonly { id: string; contextWindow: number; maxTokens: number }[] => resolved.get(route)?.piProvider.getModels() ?? [] expect(modelsOf('acme-gateway')).toMatchObject([ { id: 'bare', contextWindow: 262_144, maxTokens: 32_768 }, { id: 'sized', contextWindow: 8192, maxTokens: 512 }, ]) // The fallback is a guess, so a deployment whose gateway serves smaller // models corrects it once for the whole route. expect(modelsOf('tuned-gateway')).toMatchObject([{ id: 'bare', contextWindow: 4096, maxTokens: 256 }]) // Only an explicitly configured cap is a request default; a fallback is // the model's capability and stops there. expect(resolved.get('acme-gateway')?.configuredMaxTokens.get('bare')).toBeUndefined() expect(resolved.get('acme-gateway')?.configuredMaxTokens.get('sized')).toBe(512) }) it('takes a model’s declared modalities, then the catalog’s, then the route’s', () => { const vision = getBuiltinModels('anthropic').find(model => model.input.includes('image')) if (vision === undefined) throw new Error('the installed catalog ships no anthropic vision model') const resolved = resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', // One route, two modality sets: the entry field is what says so. models: [{ id: 'bare' }, { id: 'seeing', input: ['text', 'image'] }, { id: 'deaf', input: ['text'] }], }, 'seeing-gateway': { api: 'openai-completions', baseURL: 'https://seeing.test', // A gateway whose undescribed models all take images says so once // rather than on every entry; an entry still outranks it. defaultInput: ['text', 'image'], models: [{ id: 'bare' }, { id: 'deaf', input: ['text'] }], }, // The route value is a fallback, never an override: a catalog model // keeps what the catalog records even under a narrower route default, // exactly as it keeps its own contextWindow. 'anthropic': { defaultInput: ['text'] }, }) const inputOf = (route: string, id: string): readonly string[] | undefined => resolved.get(route)?.piProvider.getModels().find(model => model.id === id)?.input expect(inputOf('acme-gateway', 'bare')).toEqual(['text']) expect(inputOf('acme-gateway', 'seeing')).toEqual(['text', 'image']) expect(inputOf('acme-gateway', 'deaf')).toEqual(['text']) expect(inputOf('seeing-gateway', 'bare')).toEqual(['text', 'image']) expect(inputOf('seeing-gateway', 'deaf')).toEqual(['text']) expect(inputOf('anthropic', vision.id)).toEqual(vision.input) }) it('carries a written modality declaration all the way to the seam’s model metadata', async () => { // The resolver-level cases above cannot see a break between the settings // document and `LlmModelInfo`, so each rung is asserted once more through // a written section, the plugin's own registration, and `ctx.llm`. const dir = await home() const ctx = await bootWithSettings(dir, {}) await ctx.settings.update(settingsNamespace('llm-pi-ai'), { providers: { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test/v1', models: [{ id: 'bare' }, { id: 'seeing', input: ['text', 'image'] }], }, 'vision-gateway': { api: 'openai-completions', baseURL: 'https://vision.test/v1', defaultInput: ['text', 'image'], models: [{ id: 'bare' }, { id: 'deaf', input: ['text'] }], }, 'anthropic': { defaultInput: ['text'] }, }, }) const listed = async (provider: string): Promise> => Object.fromEntries((await ctx.llm.listModels(provider)).map(model => [model.id, model.inputModalities])) expect(await listed('acme-gateway')).toEqual({ bare: ['text'], seeing: ['text', 'image'] }) expect(await listed('vision-gateway')).toEqual({ bare: ['text', 'image'], deaf: ['text'] }) expect((await ctx.llm.resolveModelInfo('acme-gateway', 'seeing')).inputModalities).toEqual(['text', 'image']) // A catalog vision model keeps what the catalog records even under a // narrower route default: the route value is a fallback, not an override. const vision = getBuiltinModels('anthropic').find(model => model.input.includes('image')) if (vision === undefined) throw new Error('the installed catalog ships no anthropic vision model') expect((await ctx.llm.resolveModelInfo('anthropic', vision.id)).inputModalities).toEqual(vision.input) }) it('reads an entry’s empty modality list as no answer, and the route’s as unserviceable', () => { // Absent and empty are the same request on an entry, exactly as they are // for the route's `models` list — which matters because the config schema // materializes `[]` for an absent array, so an entry naming a catalog // model without declaring modalities must keep the catalog's rather than // describe a model that accepts nothing. const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') const resolved = resolveProfiles({ 'deepseek': { baseURL: 'https://catalog.test', models: [{ id: catalogModel.id, input: [] }] }, 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [{ id: 'bare', input: [] }], }, }) expect(resolved.get('acme-gateway')?.piProvider.getModels()[0]?.input).toEqual(['text']) expect(resolved.get('deepseek')?.piProvider.getModels()[0]?.input).toEqual(catalogModel.input) // Nothing sits below the route value, so its empty list states no answer // anything could take, and is refused where it is written. expect(() => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', defaultInput: [], models: [{ id: 'bare' }], }, })).toThrow(/defaultInput must name at least one modality/) }) it('rejects a model the route cannot identify', () => { const declare = (model: LlmPiAi.PiAiModelProfile): (() => unknown) => () => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [model] } }) expect(declare({ id: '' })).toThrow(/empty id/) expect(() => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [{ id: 'dup', contextWindow: 1, maxTokens: 1 }, { id: 'dup', contextWindow: 2, maxTokens: 2 }], }, })).toThrow(/more than once/) }) it('rejects a declaration that names no wire protocol or endpoint', () => { expect(() => resolveProfiles({ 'acme-gateway': { baseURL: 'https://acme.test', models: [{ id: 'm', contextWindow: 1, maxTokens: 1 }] }, })).toThrow(/needs an api/) expect(() => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', models: [{ id: 'm', contextWindow: 1, maxTokens: 1 }] }, })).toThrow(/needs a baseURL/) }) it.each(['bedrock-converse-stream', 'google-vertex', 'azure-openai-responses', 'openai-codex-responses'])( 'refuses %s, whose authentication a profile cannot express', (api) => { // These need SigV4 credentials and a region, a project plus ADC, provider // environment and an api-version, or OAuth — none of which a key, an // endpoint, and headers can carry, so a route naming one would be built // unable to authenticate. expect(supportedProtocols()).not.toContain(api) expect(() => buildProvider({ provider: 'acme-gateway', displayName: 'Acme', api, models: [], namesCredential: true })) .toThrow(/cannot serve; supported protocols are/) }, ) it('rejects a protocol this build cannot serve, and a route that names none', () => { const spec = { provider: 'acme-gateway', displayName: 'Acme Gateway', models: [], namesCredential: true } expect(() => buildProvider({ ...spec, api: 'quantum-telepathy' })) .toThrow(/cannot serve; supported protocols are/) expect(() => buildProvider(spec)).toThrow(/cannot serve; supported protocols are/) }) it('leaves an unauthenticated route to its protocol rather than inventing a credential', async () => { const server = await mockServer([{ events: textEvents }]) // Naming no credential is the deliberately unauthenticated posture — a // named reference that resolved to nothing would have failed with // MISSING_CREDENTIAL long before this point. The route resolves as // configured and the protocol decides: pi-ai's OpenAI-compatible // implementation wants a key or an Authorization header of its own, and // says so instead of the harness guessing a placeholder. const ctx = await harness({ providers: { 'local-llm': { api: 'openai-completions', baseURL: `${server.url}/v1`, models: [{ id: 'qwen3', contextWindow: 32_768, maxTokens: 2048 }], }, }, }) const result = await assemble(ctx, { provider: 'local-llm', model: 'qwen3', messages: [] }) expect(result.finish).toMatchObject({ kind: 'error', failure: { message: 'No API key for provider: local-llm' }, }) expect(server.requests).toHaveLength(0) }) it('authenticates an unauthenticated route through a configured header', async () => { const server = await mockServer([{ events: textEvents }]) const ctx = await harness({ providers: { 'local-llm': { api: 'openai-completions', baseURL: `${server.url}/v1`, headers: { Authorization: 'Bearer local' }, models: [{ id: 'qwen3', contextWindow: 32_768, maxTokens: 2048 }], }, }, }) const result = await assemble(ctx, { provider: 'local-llm', model: 'qwen3', messages: [] }) expect(result.finish).toEqual({ kind: 'stop' }) expect(server.headers[0]?.authorization).toBe('Bearer local') }) it('rejects a capacity that is not a positive integer', () => { const declare = (model: LlmPiAi.PiAiModelProfile): (() => unknown) => () => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [model] } }) expect(declare({ id: 'm', contextWindow: 0, maxTokens: 1 })).toThrow(/contextWindow must be a positive integer/) expect(declare({ id: 'm', contextWindow: 1.5, maxTokens: 1 })).toThrow(/contextWindow must be a positive integer/) expect(declare({ id: 'm', contextWindow: 1, maxTokens: 0 })).toThrow(/maxTokens must be a positive integer/) expect(declare({ id: 'm', contextWindow: 1, maxTokens: 1.5 })).toThrow(/maxTokens must be a positive integer/) }) it('names the route key when no displayName is configured', () => { const resolved = resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [{ id: 'm', contextWindow: 1, maxTokens: 1 }], }, }) expect(resolved.get('acme-gateway')?.displayName).toBe('acme-gateway') expect(() => resolveProfiles({ 'acme-gateway': { displayName: '' } })).toThrow(/empty displayName/) }) }) describe('catalog routes with per-model configuration', () => { it('serves the installed catalog untouched when the profile lists no models', async () => { const server = await mockServer([]) const ctx = await harness({ providers: { deepseek: { baseURL: server.url } } }) const listed = await ctx.llm.listModels('deepseek') expect(listed.map(model => model.id).sort()) .toEqual(getBuiltinModels('deepseek').map(model => model.id).sort()) }) it('overrides one catalog model field and defaults the rest from the catalog', async () => { const server = await mockServer([]) const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') const ctx = await harness({ providers: { deepseek: { baseURL: server.url, models: [{ id: catalogModel.id, contextWindow: 4096 }], }, }, }) const info = await ctx.llm.resolveModelInfo('deepseek', catalogModel.id) // The configured field wins and the name still comes from the catalog. The // catalog's own output cap is the model's capability, not a cap anyone // chose, so it must not arrive as the request default. expect(info.context).toEqual({ contextWindow: 4096 }) expect(info.name).toBe(catalogModel.name) expect(info.defaultMaxTokens).toBeUndefined() // An explicit list replaces the catalog rather than adding to it. expect((await ctx.llm.listModels('deepseek')).map(model => model.id)).toEqual([catalogModel.id]) }) it('materializes a request default only from a configured output cap', async () => { const server = await mockServer([]) const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') const ctx = await harness({ providers: { deepseek: { baseURL: server.url, models: [{ id: catalogModel.id, maxTokens: 4096 }], }, }, }) // Configuring the cap is the deployment choosing one, so it becomes the // default the seam materializes into requests that name none. expect((await ctx.llm.resolveModelInfo('deepseek', catalogModel.id)).defaultMaxTokens).toBe(4096) }) it('adds a model the installed catalog does not describe to a catalog route', async () => { const server = await mockServer([{ events: textEvents }]) const ctx = await harness({ providers: { deepseek: { apiKeyEnv: KEY_ENV, baseURL: `${server.url}/v1`, models: [{ id: 'deepseek-preview', contextWindow: 200_000, maxTokens: 8192 }], }, }, }) const result = await assemble(ctx, { provider: 'deepseek', model: 'deepseek-preview', messages: [] }) expect(result.finish).toEqual({ kind: 'stop' }) // The catalog route keeps its catalog protocol, so the new model reaches // the same endpoint shape the shipped models use. expect(server.paths).toEqual(['/v1/chat/completions']) }) it('fails an unconfigured model id before any provider request', async () => { const server = await mockServer([]) const ctx = await harness({ providers: { deepseek: { baseURL: server.url, models: [{ id: 'deepseek-preview', contextWindow: 1, maxTokens: 1 }] }, }, }) const result = await assemble(ctx, { provider: 'deepseek', model: 'not-configured', messages: [] }) expect(result.finish).toMatchObject({ kind: 'error', failure: { code: 'UNKNOWN_MODEL' } }) expect(server.requests).toHaveLength(0) }) it('preserves catalog-only model metadata the profile cannot express', () => { // Some catalog models carry provider-required request headers; overriding a // capacity must not drop them, because configuration has no way to restate // them. const headered = (getBuiltinModels('nvidia') as { id: string; headers?: unknown }[]) .find(model => model.headers !== undefined) if (headered === undefined) throw new Error('the installed catalog ships no nvidia model with headers') const resolved = resolveProfiles({ nvidia: { models: [{ id: headered.id, contextWindow: 4096 }] }, }) const [model] = resolved.get('nvidia')?.piProvider.getModels() ?? [] expect(model?.headers).toEqual(headered.headers) expect(model?.contextWindow).toBe(4096) }) it('delegates both stream methods back to the reused catalog provider', async () => { const server = await mockServer([{ events: textEvents }, { events: textEvents }]) const resolved = resolveProfiles({ deepseek: { baseURL: `${server.url}/v1` } }) const built = resolved.get('deepseek')?.piProvider if (built === undefined) throw new Error('the deepseek route built no provider') const [model] = built.getModels() if (model === undefined) throw new Error('the deepseek route resolved no models') const context = { messages: [{ role: 'user' as const, content: 'hi', timestamp: 0 }] } // `stream` is interface-required and unused by the harness adapter, which // only calls `streamSimple`; both must still reach the catalog provider. for await (const _event of built.stream(model, context, { apiKey: 'k' })) { /* drain */ } for await (const _event of built.streamSimple(model, context, { apiKey: 'k' })) { /* drain */ } expect(server.paths).toEqual(['/v1/chat/completions', '/v1/chat/completions']) }) it('keeps each model its own endpoint when the catalog route declares none', () => { // `opencode` ships no provider-level endpoint: the address lives on every // catalog model, so the route resolves without any configured baseURL. const resolved = resolveProfiles({ opencode: {} }) const models = resolved.get('opencode')?.piProvider.getModels() ?? [] expect(models.length).toBeGreaterThan(0) expect(models.every(model => model.baseUrl.length > 0)).toBe(true) expect(resolved.get('opencode')?.piProvider.baseUrl).toBeUndefined() }) it('repoints a catalog route at another wire protocol without restating its endpoint', () => { const resolved = resolveProfiles({ openai: { api: 'openai-completions' } }) const models = resolved.get('openai')?.piProvider.getModels() ?? [] // The protocol changes for the whole route; each model keeps the catalog // endpoint it already had. expect(models.every(model => model.api === 'openai-completions')).toBe(true) expect(models.every(model => model.baseUrl === 'https://api.openai.com/v1')).toBe(true) }) it('repoints a catalog route at another wire protocol', async () => { const server = await mockServer([{ events: textEvents }]) const ctx = await harness({ providers: { // openai's catalog models speak the Responses API; naming the protocol // explicitly moves the whole route onto Chat Completions. openai: { apiKeyEnv: KEY_ENV, api: 'openai-completions', baseURL: `${server.url}/v1`, models: [{ id: 'gpt-4.1', contextWindow: 100_000, maxTokens: 4096 }], }, }, }) await assemble(ctx, { provider: 'openai', model: 'gpt-4.1', messages: [] }) expect(server.paths).toEqual(['/v1/chat/completions']) }) it('keeps the catalog provider’s own auth when the route repoints its protocol', () => { // Which environment a provider reads is a property of the provider, not of // the wire format its models speak: naming an api must not cost a profile // its provider-native discovery. const resolved = resolveProfiles({ openai: { api: 'openai-completions' } }) expect(resolved.get('openai')?.piProvider.auth.apiKey?.name).toBe('OpenAI API key') }) it('lets an OAuth-only catalog route authenticate with the key its profile names', async () => { // pi-ai honours a request's `apiKey` override only when the provider // declares an api-key method. `openai-codex` ships OAuth alone, so without // the harness method beside it the route refuses its own configured key as // `Provider is not configured` before any request goes out. const resolved = resolveProfiles({ 'openai-codex': { apiKeyEnv: 'CODEX_TOKEN' } }) const provider = resolved.get('openai-codex')?.piProvider expect(provider?.auth.oauth).toBeDefined() const models = createModels() models.setProvider(provider as Provider) const model = provider?.getModels()[0] as Model const auth = await models.getAuth(model, { apiKey: 'codex-token' }) expect(auth?.auth.apiKey).toBe('codex-token') }) it('leaves an OAuth-only catalog route unconfigured when its profile names no key', () => { // Nothing to add: this adapter resolves credentials through its own seam // and holds no OAuth store, so declaring the provider configured would // trade a truthful refusal for an endpoint's 401. const resolved = resolveProfiles({ 'openai-codex': {} }) expect(resolved.get('openai-codex')?.piProvider.auth.apiKey).toBeUndefined() }) }) describe('per-model reasoning efforts', () => { /** One hand-declared route holding exactly the given models. */ function declared(models: LlmPiAi.PiAiModelProfile[]): Record { return { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models } } } /** The first materialized model of one route, or throw. */ function modelOf(providers: Record, route = 'acme-gateway'): Model { const [model] = resolveProfiles(providers).get(route)?.piProvider.getModels() ?? [] if (model === undefined) throw new Error(`route "${route}" resolved no models`) return model } it('declares selectable levels with their wire spellings on a hand-declared model', () => { const model = modelOf(declared([{ id: 'acme-think', reasoningEfforts: { off: null, low: 'low', high: 'high', max: 'ultra' }, }])) expect(model.reasoning).toBe(true) // Undeclared levels are pinned null rather than left to pi-ai's own // defaulting, which is asymmetric: an absent key means "supported" for the // five base levels but "unsupported" for xhigh/max. A profile author // should not need to know that. Declared `off` with no value stays absent // from the map — supported, send nothing. expect(model.thinkingLevelMap).toEqual({ minimal: null, medium: null, xhigh: null, low: 'low', high: 'high', max: 'ultra', }) expect(getSupportedThinkingLevels(model)).toEqual(['off', 'low', 'high', 'max']) }) it('keeps a declared off value in the map for dispatch to send', () => { const model = modelOf(declared([{ id: 'm', reasoningEfforts: { off: 'none', high: 'high' } }])) expect(model.thinkingLevelMap?.off).toBe('none') expect(getSupportedThinkingLevels(model)).toEqual(['off', 'high']) }) it('offers exactly the declared keys: leaving off out makes thinking mandatory', () => { const model = modelOf(declared([{ id: 'm', reasoningEfforts: { high: 'high' } }])) expect(getSupportedThinkingLevels(model)).toEqual(['high']) }) it('narrows a catalog model’s levels in place', () => { const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') expect(getSupportedThinkingLevels(catalogModel as Model)).toEqual(['off', 'high', 'max']) const model = modelOf({ deepseek: { models: [{ id: catalogModel.id, reasoningEfforts: { off: null, high: 'high' } }] }, }, 'deepseek') expect(getSupportedThinkingLevels(model)).toEqual(['off', 'high']) // Only the reasoning fields change; identity and capacities stay catalog. expect(model.name).toBe(catalogModel.name) expect(model.contextWindow).toBe(catalogModel.contextWindow) }) it('strips reasoning from a catalog model with false', () => { const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') expect(catalogModel.reasoning).toBe(true) const model = modelOf({ deepseek: { models: [{ id: catalogModel.id, reasoningEfforts: false }] } }, 'deepseek') expect(model.reasoning).toBe(false) expect(getSupportedThinkingLevels(model)).toEqual(['off']) }) it('inherits the catalog capability when the field is absent', () => { const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') const model = modelOf({ deepseek: { models: [{ id: catalogModel.id }] } }, 'deepseek') expect(model.reasoning).toBe(catalogModel.reasoning) expect(model.thinkingLevelMap).toEqual(catalogModel.thinkingLevelMap) }) it('rejects a declaration that offers nothing or spells a level it cannot send', () => { const declare = (efforts: NonNullable): (() => unknown) => () => resolveProfiles(declared([{ id: 'm', reasoningEfforts: efforts }])) expect(declare({})).toThrow(/empty reasoningEfforts/) // A YAML `reasoningEfforts:` left valueless arrives as null through the // schema union; it declares nothing and is not a spelling of "inherit". expect(declare(null as never)).toThrow(/empty reasoningEfforts/) expect(declare({ off: null })).toThrow(/offers no level beyond "off"/) expect(declare({ off: 'none' })).toThrow(/offers no level beyond "off"/) expect(declare({ high: null })).toThrow(/only "off" may leave it empty/) expect(declare({ high: '' })).toThrow(/must not be an empty string/) }) }) describe('modelOverrides', () => { const deepseekModel = (): Model => { const [model] = getBuiltinModels('deepseek') if (model === undefined) throw new Error('the installed catalog ships no deepseek model') return model } it('reshapes one catalog model while the rest of the catalog keeps serving', () => { const catalogSize = getBuiltinModels('deepseek').length const target = deepseekModel() const resolved = resolveProfiles({ deepseek: { modelOverrides: { [target.id]: { name: 'DeepSeek (proxied)', maxTokens: 4096, reasoningEfforts: { off: null, high: 'high' }, }, }, }, }) const models = resolved.get('deepseek')?.piProvider.getModels() ?? [] const reshaped = models.find(model => model.id === target.id) if (reshaped === undefined) throw new Error('the overridden model vanished from the route') // The whole catalog still serves — that is the difference from `models`, // which replaces it. expect(models).toHaveLength(catalogSize) expect(reshaped.name).toBe('DeepSeek (proxied)') expect(getSupportedThinkingLevels(reshaped)).toEqual(['off', 'high']) // An override's cap is explicit configuration, so it becomes the request // default exactly as a models entry's would. expect(resolved.get('deepseek')?.configuredMaxTokens.get(target.id)).toBe(4096) // A sibling the overrides do not name is byte-identical to the catalog. const sibling = models.find(model => model.id !== target.id) expect(sibling?.maxTokens).toBe(getBuiltinModels('deepseek').find(model => model.id === sibling?.id)?.maxTokens) }) it('refuses every override that lands nowhere instead of skipping it', () => { expect(() => resolveProfiles({ deepseek: { modelOverrides: { 'no-such-model': { name: 'ghost' } } }, })).toThrow(/which the installed catalog does not describe/) expect(() => resolveProfiles({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models: [{ id: 'm' }], modelOverrides: { m: { name: 'renamed' } }, }, })).toThrow(/a declared route spells every model out/) const declaredOnly = deepseekModel() expect(() => resolveProfiles({ deepseek: { models: [{ id: declaredOnly.id }], modelOverrides: { [declaredOnly.id]: { name: 'renamed' } }, }, })).toThrow(/models already replaces the served catalog/) expect(() => resolveProfiles({ deepseek: { modelOverrides: { '': { name: 'nameless' } } }, })).toThrow(/empty model id/) // The dict key is the id; a value smuggling its own would quietly rename // the model it meant to customize. The schema passes unknown keys // through, so resolution is the boundary that refuses it — the variable // indirection mirrors that boundary by sidestepping the literal check. const smuggled = { name: 'x', id: 'other' } expect(() => resolveProfiles({ deepseek: { modelOverrides: { [deepseekModel().id]: smuggled } }, })).toThrow(/sets "id", which is the dict key/) }) }) describe('reasoning-dispatch compat switches', () => { /** The materialized models of one route, keyed by id. */ function modelsOf(providers: Record, route: string): Map> { const models = resolveProfiles(providers).get(route)?.piProvider.getModels() ?? [] return new Map(models.map(model => [model.id, model])) } it('applies route switches to every openai-completions model, entries winning per field', () => { const models = modelsOf({ 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', compat: { thinkingFormat: 'deepseek' }, models: [ { id: 'dialect-default', reasoningEfforts: { off: null, high: 'high' } }, { id: 'dialect-odd', compat: { thinkingFormat: 'openai', supportsReasoningEffort: false } }, ], }, }, 'acme-gateway') expect(models.get('dialect-default')?.compat).toEqual({ thinkingFormat: 'deepseek' }) expect(models.get('dialect-odd')?.compat).toEqual({ thinkingFormat: 'openai', supportsReasoningEffort: false }) }) it('merges the switches over the catalog entry’s own compat instead of replacing it', () => { const [catalogModel] = getBuiltinModels('deepseek') if (catalogModel === undefined) throw new Error('the installed catalog ships no deepseek model') const inherited = catalogModel.compat as OpenAICompletionsCompat expect(inherited.requiresReasoningContentOnAssistantMessages).toBe(true) const models = modelsOf({ deepseek: { models: [{ id: catalogModel.id, compat: { thinkingFormat: 'openai' } }] }, }, 'deepseek') // The one switched field changes; the catalog's other quirks survive, // because configuration has no way to restate them. expect(models.get(catalogModel.id)?.compat).toEqual({ ...inherited, thinkingFormat: 'openai' }) }) it('skips models of other protocols on a mixed route instead of failing them', () => { // xai ships both completions and responses models, so a route-level switch // must land on the former without invalidating the latter. const catalog = getBuiltinModels('xai') as readonly Model[] const completions = catalog.find(model => model.api === 'openai-completions') const responses = catalog.find(model => model.api === 'openai-responses') if (completions === undefined || responses === undefined) throw new Error('xai no longer ships a mixed catalog') const models = modelsOf({ xai: { compat: { supportsReasoningEffort: false }, models: [{ id: completions.id }, { id: responses.id }], }, }, 'xai') expect((models.get(completions.id)?.compat as OpenAICompletionsCompat).supportsReasoningEffort).toBe(false) expect(models.get(responses.id)?.compat).toEqual(responses.compat) }) it('rejects a model-level switch on a protocol that has no such field', () => { expect(() => resolveProfiles({ anthropic: { models: [{ id: 'claude-sonnet-4-5', compat: { thinkingFormat: 'openai' } }], }, })).toThrow(/exist only on openai-completions/) }) it('rejects route switches no model on the route can take', () => { expect(() => resolveProfiles({ anthropic: { compat: { thinkingFormat: 'openai' } }, })).toThrow(/no model on the route speaks openai-completions/) }) }) describe('resolution snapshots', () => { it('finishes an in-flight request under the configuration it started with', async () => { const server = await mockServer([{ events: textEvents }]) let current = resolveProfiles({ deepseek: { baseURL: `${server.url}/v1` } }) let release: () => void = () => {} const held = new Promise((resolve) => { release = resolve }) const adapter = new PiAiAdapter({ profiles: () => current, // Credential resolution is the real await inside a stream call, and the // window a configuration change has to land in. resolveApiKey: async () => { await held; return 'k' }, }) const chunks: StreamChunk[] = [] const inFlight = (async () => { for await (const chunk of adapter.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], })) chunks.push(chunk) })() // The route set changes while the request waits, and something else reads // the adapter meanwhile, which is what would rebuild a shared collection. current = resolveProfiles({ openai: { baseURL: `${server.url}/v1` } }) await expect(adapter.listModels('openai')).resolves.not.toHaveLength(0) release() await inFlight // The in-flight request keeps its own snapshot: it reaches the endpoint it // resolved against instead of failing on a provider that no longer exists. expect(chunks.at(-1)).toMatchObject({ type: 'finish', reason: { kind: 'stop' } }) expect(server.paths).toEqual(['/v1/chat/completions']) }) it('serves the next request from the new configuration', async () => { const first = await mockServer([{ events: textEvents }]) const second = await mockServer([{ events: textEvents }]) let current = resolveProfiles({ deepseek: { baseURL: `${first.url}/v1` } }) const adapter = new PiAiAdapter({ profiles: () => current, resolveApiKey: () => Promise.resolve('k') }) const drain = async (): Promise => { for await (const _chunk of adapter.stream({ provider: 'deepseek', model: 'deepseek-v4-flash', messages: [], })) { /* drain */ } } await drain() current = resolveProfiles({ deepseek: { baseURL: `${second.url}/v1` } }) await drain() expect(first.paths).toHaveLength(1) expect(second.paths).toHaveLength(1) }) }) describe('configurable-provider directory', () => { it('keeps the previous directory when a route collides with another adapter family', async () => { const dir = await home() const ctx = await bootWithSettings(dir, {}) // Another adapter family owns this route id, exactly as llm-deepseek does. ctx.llm.registerConfigurableProviders([ { provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] }, ]) const before = ctx.llm.listConfigurableProviders().length expect(before).toBeGreaterThan(30) await ctx.settings.update(settingsNamespace('llm-pi-ai'), { providers: { 'deepseek-official': { api: 'openai-completions', baseURL: 'https://acme.test/v1', models: [{ id: 'm', contextWindow: 1, maxTokens: 1 }], }, }, }) // The refused swap costs a diagnostic, not the directory: every entry the // page needs is still declared. expect(ctx.llm.listConfigurableProviders()).toHaveLength(before) expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'deepseek-official')?.settingsNs) .toBe('llm-deepseek') }) it('replaces its entries atomically as declared routes come and go', async () => { const dir = await home() const ctx = await bootWithSettings(dir, {}) const catalogOnly = ctx.llm.listConfigurableProviders().length await ctx.settings.update(settingsNamespace('llm-pi-ai'), { providers: { 'acme-gateway': { displayName: 'Acme Gateway', api: 'openai-completions', baseURL: 'https://acme.test/v1', models: [{ id: 'm', contextWindow: 1, maxTokens: 1 }], }, }, }) expect(ctx.llm.listConfigurableProviders()).toHaveLength(catalogOnly + 1) expect(ctx.llm.listConfigurableProviders().find(entry => entry.provider === 'acme-gateway')?.displayName) .toBe('Acme Gateway') await ctx.settings.replace(settingsNamespace('llm-pi-ai'), {}) expect(ctx.llm.listConfigurableProviders()).toHaveLength(catalogOnly) }) it('withholds a catalog route this adapter cannot authenticate', async () => { const ctx = await harness({}) const offered = ctx.llm.listConfigurableProviders().map(entry => entry.provider) // `openai-codex` is the one installed provider that authenticates through // OAuth alone. pi-ai resolves OAuth only from a *stored* credential, this // adapter constructs its collection with no credential store, and nothing // here runs a login flow — so every request on such a route fails with // `Provider is not configured` before it goes out. Offering it would put a // provider on the settings page that no amount of configuration can make // work. expect(offered).not.toContain('openai-codex') // A provider that offers OAuth *beside* an api-key method keeps its entry: // the key is a path this adapter can serve. expect(offered).toContain('anthropic') expect(offered).toContain('openai') }) it('still lists a withheld route a stored profile names, as a catalog route', async () => { // Withholding the offer must not strand a profile someone already stored: // the route keeps its entry so a configuration surface can edit or delete // it, and `declared` still answers catalog membership rather than the // offer, so the page does not mislabel it as a route this deployment // invented. const ctx = await harness({ providers: { 'openai-codex': { apiKeyEnv: KEY_ENV } } }) expect(ctx.llm.listConfigurableProviders()).toContainEqual({ provider: 'openai-codex', displayName: 'openai-codex', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai-codex'], declared: false, }) }) })