| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194 |
- 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, ReasoningEffortId } 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 { memoryAuth } from './auth-double.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<string> {
- 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<Context> {
- 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<string, unknown> = {}): 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<Context> {
- 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<Record<string, readonly string[] | undefined>> =>
- 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<Api>
- 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<string, LlmPiAi.PiAiProviderProfile> {
- return { 'acme-gateway': { api: 'openai-completions', baseURL: 'https://acme.test', models } }
- }
- /** The first materialized model of one route, or throw. */
- function modelOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>, route = 'acme-gateway'): Model<Api> {
- 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<Api>)).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<LlmPiAi.PiAiModelProfile['reasoningEfforts']>): (() => 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<Api> => {
- 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('compat switches', () => {
- /** The materialized models of one route, keyed by id. */
- function modelsOf(providers: Record<string, LlmPiAi.PiAiProviderProfile>, route: string): Map<string, Model<Api>> {
- 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<Api>[]
- 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, naming what it offers', () => {
- expect(() => resolveProfiles({
- anthropic: {
- models: [{ id: 'claude-sonnet-4-5', compat: { thinkingFormat: 'openai' } }],
- },
- })).toThrow(/its api is "anthropic-messages", which does not take it.*exists on openai-completions/s)
- })
- it('rejects route switches no model on the route can take', () => {
- expect(() => resolveProfiles({
- anthropic: { compat: { thinkingFormat: 'openai' } },
- })).toThrow(/no model on the route speaks a protocol that takes it/)
- })
- it('carries the developer-role switch onto a hand-declared reasoning model', () => {
- // pi-ai reads this switch only for a reasoning model, and detects it from
- // the endpoint URL — which for a private gateway answers as though it were
- // OpenAI itself, so the route must be able to say otherwise.
- const models = modelsOf({
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- compat: { supportsDeveloperRole: false, maxTokensField: 'max_tokens' },
- models: [{ id: 'acme-think', reasoningEfforts: { off: null, high: 'high' } }],
- },
- }, 'acme-gateway')
- expect(models.get('acme-think')?.compat).toEqual({
- supportsDeveloperRole: false,
- maxTokensField: 'max_tokens',
- })
- })
- it('carries a switch both OpenAI protocols declare onto an openai-responses route', () => {
- const models = modelsOf({
- 'acme-responses': {
- api: 'openai-responses',
- baseURL: 'https://acme.test',
- compat: { supportsDeveloperRole: false },
- models: [{ id: 'acme-r', reasoningEfforts: { off: null, high: 'high' } }],
- },
- }, 'acme-responses')
- expect(models.get('acme-r')?.compat).toEqual({ supportsDeveloperRole: false })
- })
- it('carries an anthropic-only switch onto an anthropic-messages route', () => {
- const models = modelsOf({
- 'acme-claude': {
- api: 'anthropic-messages',
- baseURL: 'https://acme.test',
- compat: { supportsTemperature: false, supportsCacheControlOnTools: false },
- models: [{ id: 'acme-opus' }],
- },
- }, 'acme-claude')
- expect(models.get('acme-opus')?.compat).toEqual({
- supportsTemperature: false,
- supportsCacheControlOnTools: false,
- })
- })
- it('lands each route switch only on the models whose protocol declares it', () => {
- const catalog = getBuiltinModels('xai') as readonly Model<Api>[]
- 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: {
- // Both protocols take the first switch; only completions takes the second.
- compat: { supportsDeveloperRole: false, thinkingFormat: 'openai' },
- models: [{ id: completions.id }, { id: responses.id }],
- },
- }, 'xai')
- const onCompletions = models.get(completions.id)?.compat as OpenAICompletionsCompat
- expect(onCompletions.supportsDeveloperRole).toBe(false)
- expect(onCompletions.thinkingFormat).toBe('openai')
- const onResponses = models.get(responses.id)?.compat as { supportsDeveloperRole?: boolean; thinkingFormat?: string }
- expect(onResponses.supportsDeveloperRole).toBe(false)
- expect(onResponses.thinkingFormat).toBeUndefined()
- })
- it('carries chat-template kwargs beside the thinking format that dispatches through them', () => {
- const models = modelsOf({
- 'acme-qwen': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- models: [{
- id: 'qwen-local',
- reasoningEfforts: { off: null, medium: 'medium' },
- compat: {
- thinkingFormat: 'qwen-chat-template',
- chatTemplateKwargs: { enable_thinking: { $var: 'thinking.enabled' } },
- },
- }],
- },
- }, 'acme-qwen')
- expect(models.get('qwen-local')?.compat).toEqual({
- thinkingFormat: 'qwen-chat-template',
- chatTemplateKwargs: { enable_thinking: { $var: 'thinking.enabled' } },
- })
- })
- it('rejects a model switch on an unrecognized protocol as having no configurable compat', () => {
- expect(() => resolveProfiles({
- 'acme-gateway': {
- api: 'acme-chat',
- baseURL: 'https://acme.test',
- models: [{ id: 'acme-a', compat: { supportsStore: false } }],
- },
- })).toThrow(/its api is "acme-chat", which does not take it.*"acme-chat" offers no configurable compat/s)
- })
- it('refuses a valueless compat key written through the composed settings path', async () => {
- // The write path an operator reaches: a section resolved by schemastery,
- // judged by this adapter's section validator before it is stored.
- // schemastery keeps the null, so nothing but that check stands between it
- // and `Model.compat`.
- const dir = await home()
- const ctx = await bootWithSettings(dir, {})
- await expect(ctx.settings.update(settingsNamespace('llm-pi-ai'), {
- providers: {
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test/v1',
- compat: { supportsDeveloperRole: null },
- models: [{ id: 'acme-a' }],
- },
- },
- })).rejects.toThrow(/compat "supportsDeveloperRole" with no value/)
- })
- it('carries a compat switch from a written settings section onto the wire', async () => {
- // End to end for the reported gap: the switch enters as configuration and
- // changes the request the provider receives, not merely the resolved model.
- vi.stubEnv(KEY_ENV, 'test-key')
- const server = await mockServer([{ events: textEvents }])
- const dir = await home()
- const ctx = await bootWithSettings(dir, {})
- await ctx.settings.update(settingsNamespace('llm-pi-ai'), {
- providers: {
- 'acme-gateway': {
- apiKeyEnv: KEY_ENV,
- api: 'openai-completions',
- baseURL: `${server.url}/v1`,
- compat: { supportsDeveloperRole: false },
- models: [{ id: 'acme-think', reasoningEfforts: { off: null, high: 'high' } }],
- },
- },
- })
- await assemble(ctx, {
- provider: 'acme-gateway',
- model: 'acme-think',
- reasoningEffort: ReasoningEffortId('high'),
- system: 'you are a harness',
- messages: [],
- })
- const request = server.requests[0] as { messages: { role: string }[] }
- expect(request.messages.map(message => message.role)).toEqual(['system'])
- })
- it('refuses a valueless compat key rather than writing null over the catalog', () => {
- // schemastery passes a YAML bare key through as null. Carried forward it
- // would replace the installed entry's value, and pi-ai's `??` would then
- // reach for its baseURL detection — the "written but not applied" outcome.
- expect(() => resolveProfiles({
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- compat: { supportsDeveloperRole: null } as never,
- models: [{ id: 'acme-a' }],
- },
- })).toThrow(/compat "supportsDeveloperRole" with no value/)
- })
- it('refuses a compat key whose value is undefined, as a cordis.yml entry can write', () => {
- // `!!js undefined` reaches the same state as a YAML bare key, and
- // schemastery keeps the key either way, so both are refused together.
- expect(() => resolveProfiles({
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- compat: { supportsDeveloperRole: undefined } as never,
- models: [{ id: 'acme-a' }],
- },
- })).toThrow(/compat "supportsDeveloperRole" with no value/)
- })
- it('refuses a valueless compat key on a model entry too', () => {
- expect(() => resolveProfiles({
- deepseek: {
- modelOverrides: { 'deepseek-v4-flash': { compat: { requiresReasoningContentOnAssistantMessages: null } } as never },
- },
- })).toThrow(/model "deepseek-v4-flash" sets compat "requiresReasoningContentOnAssistantMessages" with no value/)
- })
- it('serves the Responses compat type on every protocol pi-ai gives it to', () => {
- // pi-ai types azure-openai-responses and openai-codex-responses with the
- // same OpenAIResponsesCompat, so a switch settable on one is settable on all.
- for (const route of ['azure-openai-responses', 'openai-codex']) {
- const models = modelsOf({ [route]: { compat: { supportsDeveloperRole: false } } }, route)
- const [first] = [...models.values()]
- expect((first?.compat as { supportsDeveloperRole?: boolean }).supportsDeveloperRole).toBe(false)
- }
- })
- it('serves the Bedrock compat type on its own protocol', () => {
- const models = modelsOf({ 'amazon-bedrock': { compat: { supportsStrictMode: false } } }, 'amazon-bedrock')
- const [first] = [...models.values()]
- expect((first?.compat as { supportsStrictMode?: boolean }).supportsStrictMode).toBe(false)
- })
- it('refuses a compat key no wire protocol declares instead of dropping it', () => {
- // Schemastery passes unknown keys through, so silently dropping one would
- // make an unreadable switch look applied; the resolver must refuse it.
- expect(() => resolveProfiles({
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- compat: { supportsDevelperRole: false } as never,
- models: [{ id: 'acme-a' }],
- },
- })).toThrow(/compat "supportsDevelperRole", which no wire protocol declares; the configurable switches are .*\bsupportsDeveloperRole\b/)
- })
- it('refuses a compat key pi-ai’s catalog owns, pointing at the catalog route', () => {
- expect(() => resolveProfiles({
- 'acme-gateway': {
- api: 'openai-completions',
- baseURL: 'https://acme.test',
- models: [{ id: 'acme-a', compat: { openRouterRouting: {} } as never }],
- },
- })).toThrow(/compat "openRouterRouting", which is not configurable here/)
- })
- })
- 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<void>((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' },
- auth: memoryAuth(),
- })
- 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'),
- auth: memoryAuth(),
- })
- const drain = async (): Promise<void> => {
- 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, {})
- 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('offers every installed catalog route, including one that only signs in', 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. It is offered like any other because the collection now
- // carries a durable credential store and a login flow writes into it, so
- // the route has a posture that works rather than only one that fails.
- expect(offered).toContain('openai-codex')
- expect(offered).toContain('anthropic')
- expect(offered).toContain('openai')
- })
- it('lists a route a stored profile names as a catalog route, not a declared one', async () => {
- // `declared` answers catalog membership, so a profile stored against a
- // route pi-ai ships is not mislabelled as one 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,
- })
- })
- })
|