Files
deepseek-harness/packages/host/apiproxy/tests/api-proxy-config.spec.ts
T
Yichen Jiang ffd2f188f2 fix(llm): answer a catalog route's models from pi-ai's own registry
Clicking "fetch available models" on a built-in provider went to the
network. That is the wrong source: pi-ai's registry is the authoritative
list for its own providers, and it carries the context windows and output
caps a `GET /models` listing does not disclose. Asking api.deepseek.com
what DeepSeek serves is both slower and worse, and against an endpoint
that answers a different shape it failed outright.

Interrogation is still keyed by settings namespace — the provider being
added has no route — but the request may now name the route it is
editing. An adapter that already describes that route answers from what
it knows, needs no endpoint at all, and never touches the network; only a
route the catalog does not describe reaches the wire, and one naming no
endpoint is told to set one or enter its models by hand.

`ConfigurableProviderView` gained `supportsDiscovery` so a surface offers
the action where a namespace can answer instead of hardcoding an adapter
family.

Three narrower corrections ride along. Discovery no longer claims Azure
or Codex: Azure authenticates with an `api-key` header and an
`api-version` query despite its OpenAI lineage, and Codex uses OAuth, so
both reported an authentication failure as a provider with no models.
Cancellation during the body read escaped as the raw abort reason rather
than a coded ABORTED. And the schema comment claiming the probe key is
never logged overstated it: the host neither stores nor returns it, but
it rides the client's outgoing envelope like every other secret-bearing
payload, and redacting that tap is a configuration-plane-wide change.
2026-08-05 19:51:02 +08:00

673 lines
30 KiB
TypeScript

/**
* Settings/credentials/llm RPC domains and their host-stream frames over
* createApiProxy: layered redacted describe, write-path rejection mapping,
* value-free credential views, the directory/live-route merge, and the three
* invalidation frames (settings/credentials/models changed).
*/
import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import z from 'schemastery'
import AgentRegistry from '@deepseek-ai/dsh-agent'
import SessionStore from '@deepseek-ai/dsh-session'
import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry from '@deepseek-ai/dsh-tools'
import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
import LlmService, { LlmAdapter } from '@deepseek-ai/dsh-llm'
import type { GenerateOptions, LlmModelInfo, LlmProviderInfo, StreamChunk } from '@deepseek-ai/dsh-llm'
import { Settings, settingsNamespace } from '@deepseek-ai/dsh-settings'
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings'
import { Credentials } from '@deepseek-ai/dsh-credentials'
import type { CredentialInfo, CredentialRef, ResolvedCredential } from '@deepseek-ai/dsh-credentials'
import type { HostFrame } from '../src/api/index.ts'
import type { RpcRequest, RpcResponse } from '../src/api/rpc.ts'
import { RpcId } from '../src/api/rpc.ts'
import { createApiProxy } from '../src/api-proxy.ts'
const DEFAULTS = { provider: 'p', model: 'm', cwd: '/tmp', workspaceRoot: '/tmp' }
let nextRpc = 1
function request<P>(payload: P): RpcRequest<P> {
return { rpcId: RpcId(`req-${String(nextRpc++)}`), payload }
}
function expectOk<T>(response: RpcResponse<T>): T {
expect(response.result.ok).toBe(true)
if (!response.result.ok) throw new Error('unreachable')
return response.result.value
}
function expectErr<T>(response: RpcResponse<T>): { code: string; message: string; details: unknown } {
expect(response.result.ok).toBe(false)
if (response.result.ok) throw new Error('unreachable')
return response.result.error
}
/** In-memory settings provider: the seam base class owns all tested behavior. */
class MemorySettings extends Settings {
doc: Record<string, unknown>
constructor(ctx: ConstructorParameters<typeof Settings>[0], options?: {
doc?: Record<string, unknown>
readOnly?: boolean
documentPath?: string
preparedPath?: string
}) {
super(ctx)
this.doc = structuredClone(options?.doc ?? {})
this.readOnly = options?.readOnly ?? false
this.path = options?.documentPath
this.preparedPath = options?.preparedPath
}
private readonly readOnly: boolean
private readonly path: string | undefined
private readonly preparedPath: string | undefined
get writable(): boolean {
return !this.readOnly
}
override get documentPath(): string | undefined {
return this.path
}
override prepareDocument(): Promise<string | undefined> {
return Promise.resolve(this.preparedPath ?? this.documentPath)
}
protected load(): Promise<Record<string, unknown>> {
return Promise.resolve(structuredClone(this.doc))
}
protected persist(ns: SettingsNamespace, section: Record<string, unknown>): Promise<void> {
this.doc[ns] = structuredClone(section)
return Promise.resolve()
}
}
/** In-memory credential provider with an env-shadow double for the rejection path. */
class MemoryCredentials extends Credentials {
private readonly values = new Map<string, string>()
constructor(ctx: ConstructorParameters<typeof Credentials>[0], options?: { shadowed?: string[] }) {
super(ctx)
this.shadowed = new Set(options?.shadowed ?? [])
}
private readonly shadowed: Set<string>
resolve(ref: CredentialRef): Promise<ResolvedCredential | undefined> {
if (this.shadowed.has(ref)) return Promise.resolve({ value: 'from-env', source: 'env' })
const value = this.values.get(ref)
return Promise.resolve(value === undefined ? undefined : { value, source: 'file' })
}
describe(ref: CredentialRef): Promise<CredentialInfo> {
if (this.shadowed.has(ref)) return Promise.resolve({ configured: true, source: 'env', writable: false })
const configured = this.values.has(ref)
return Promise.resolve({ configured, ...configured ? { source: 'file' } : {}, writable: true })
}
set(ref: CredentialRef, value: string): Promise<void> {
if (this.shadowed.has(ref)) {
return Promise.reject(new Error(`credentials: ${ref} is shadowed by the read-only environment`))
}
this.values.set(ref, value)
this.ctx.emit('credentials/updated', ref)
return Promise.resolve()
}
unset(ref: CredentialRef): Promise<void> {
if (this.shadowed.has(ref)) {
return Promise.reject(new Error(`credentials: ${ref} is shadowed by the read-only environment`))
}
this.values.delete(ref)
this.ctx.emit('credentials/updated', ref)
return Promise.resolve()
}
}
/** Catalog-serving adapter stub for the llm.models path. */
class CatalogAdapter extends LlmAdapter {
constructor(private readonly name: string, private readonly models: readonly string[]) {
super()
}
override providerInfo(provider: string): LlmProviderInfo {
return { id: provider, name: this.name }
}
override listModels(provider: string): Promise<readonly LlmModelInfo[]> {
return Promise.resolve(this.models.map(id => ({ provider, id, name: id })))
}
async * stream(_options: GenerateOptions): AsyncIterable<StreamChunk> {
throw new Error('not exercised')
}
}
class BrokenCatalogAdapter extends CatalogAdapter {
override listModels(): Promise<readonly LlmModelInfo[]> {
return Promise.reject(new Error('catalog backend down'))
}
}
const NS = settingsNamespace('llm-deepseek')
const AdapterConfig = z.object({
apiKey: z.string().role('secret'),
apiKeyEnv: z.string().default('DEEPSEEK_API_KEY'),
baseURL: z.string(),
})
async function harness(options?: {
settings?: false | {
doc?: Record<string, unknown>
readOnly?: boolean
documentPath?: string
preparedPath?: string
}
credentials?: false | { shadowed?: string[] }
/** Skip the directory registration to exercise a namespace the proxy does not expose. */
configurableProviders?: false
}): Promise<Context> {
const ctx = new Context()
await ctx.plugin(SessionStore)
await ctx.plugin(SystemPrompt, { persona: '' })
await ctx.plugin(ToolRegistry)
await ctx.plugin(UserInteractionService)
await ctx.plugin(AgentRegistry)
await ctx.plugin(LlmService)
if (options?.settings !== false) await ctx.plugin(MemorySettings, options?.settings)
if (options?.credentials !== false) await ctx.plugin(MemoryCredentials, options?.credentials)
// Model-provider namespaces plus the explicit Web preference and product
// onboarding allowlists are the proxy's complete settings surface.
if (options?.configurableProviders !== false) {
ctx.llm.registerConfigurableProviders([
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
])
}
// Host-stream opener reads the committed-workspace baseline; the stub
// suffices — the real workspace composition is api-proxy-workspace.spec's.
ctx.provide('workspace', { list: () => [] } as never)
return ctx
}
/** Drain `count` host frames matching `types`, then abort the stream. */
async function collectHost(
api: ReturnType<typeof createApiProxy>,
types: string[],
count: number,
run: () => Promise<void>,
): Promise<HostFrame[]> {
const abort = new AbortController()
const frames: HostFrame[] = []
const stream = api.events.host(request({}), abort.signal)
const consume = (async () => {
for await (const frame of stream) {
if (!types.includes(frame.payload.type)) continue
frames.push(frame.payload)
if (frames.length >= count) abort.abort()
}
})()
await run()
await consume
return frames
}
describe('settings domain', () => {
it('reports an actionable error when no settings provider is mounted', async () => {
const ctx = await harness({ settings: false })
const api = createApiProxy(ctx, DEFAULTS)
const error = expectErr(await api.settings.describe(request({})))
expect(error.code).toBe('internal')
expect(error.message).toContain('dsh-settings-local')
})
it('describes layered redacted namespaces with their secret slots', async () => {
const ctx = await harness({ settings: {
doc: { 'llm-deepseek': { apiKey: 'user-secret', baseURL: 'https://user' } },
documentPath: '/tmp/custom-settings.yaml',
} })
ctx.settings.register(NS, AdapterConfig, { base: { baseURL: 'https://base' } })
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.settings.describe(request({})))
expect(value.writable).toBe(true)
expect(value.hasDocument).toBe(true)
expect(value.namespaces).toHaveLength(1)
const view = value.namespaces[0]!
expect(view.ns).toBe('llm-deepseek')
expect(view.applies).toBe('live')
expect((view.schema as { refs?: unknown }).refs).toBeDefined()
expect(view.value).toEqual({ apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://user' })
expect(view.base).toEqual({ baseURL: 'https://base' })
expect(view.user).toEqual({ baseURL: 'https://user' })
expect(view.secrets).toEqual([{ path: ['apiKey'], set: true }])
expect(JSON.stringify(value)).not.toContain('user-secret')
})
it('opens the provider-resolved document without accepting a browser path', async () => {
const ctx = await harness({ settings: {
documentPath: '/tmp/described-settings.yaml',
preparedPath: '/tmp/custom-settings.yaml',
} })
const opened: string[] = []
const api = createApiProxy(ctx, {
...DEFAULTS,
openTextFile: (path) => {
opened.push(path)
return Promise.resolve()
},
})
expect(expectOk(await api.settings.openDocument(request({}), new AbortController().signal)))
.toEqual({ opened: true })
expect(opened).toEqual(['/tmp/custom-settings.yaml'])
})
it('refuses to open settings when the provider has no local document', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
expect(expectOk(await api.settings.describe(request({}))).hasDocument).toBe(false)
const error = expectErr(await api.settings.openDocument(request({}), new AbortController().signal))
expect(error.code).toBe('internal')
expect(error.message).toContain('no local document')
})
it('does not prepare or open a settings document after cancellation', async () => {
const ctx = await harness({ settings: { documentPath: '/tmp/settings.yaml' } })
const opened: string[] = []
const api = createApiProxy(ctx, {
...DEFAULTS,
openTextFile: (path) => {
opened.push(path)
return Promise.resolve()
},
})
const prepare = vi.spyOn(ctx.settings, 'prepareDocument')
const cancelled = new AbortController()
cancelled.abort()
expect(expectErr(await api.settings.openDocument(request({}), cancelled.signal)).code)
.toBe('cancelled')
expect(prepare).not.toHaveBeenCalled()
const pending = Promise.withResolvers<string | undefined>()
prepare.mockReturnValueOnce(pending.promise)
const duringPrepare = new AbortController()
const opening = api.settings.openDocument(request({}), duringPrepare.signal)
await vi.waitFor(() => { expect(prepare).toHaveBeenCalledOnce() })
duringPrepare.abort()
pending.resolve('/tmp/settings.yaml')
expect(expectErr(await opening).code).toBe('cancelled')
expect(opened).toEqual([])
})
it('serves model-provider and explicitly allowlisted Web namespaces only', async () => {
// The settings seam is general: any plugin may register a namespace for
// its own configuration. The Web configuration plane remains opt-in, so a
// future internal plugin cannot become remotely configurable just by
// registering; permission and the product onboarding namespace are the
// non-model namespaces intentionally admitted by this surface.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
ctx.settings.register(settingsNamespace('some-other-plugin'), z.object({ secretPath: z.string() }))
ctx.settings.register(settingsNamespace('permission'), z.object({
defaultPreset: z.union(['read-only', 'workspace-write']).required(),
}), {
base: { defaultPreset: 'read-only' },
})
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.settings.describe(request({})))
expect(value.namespaces.map(view => view.ns)).toEqual(['llm-deepseek', 'permission'])
const permission = expectOk(await api.settings.mutate(request({
ns: 'permission',
ops: [{ op: 'set', path: ['defaultPreset'], value: 'workspace-write' }],
})))
expect(permission.value).toEqual({ defaultPreset: 'workspace-write' })
for (const response of [
await api.settings.update(request({ ns: 'some-other-plugin', patch: { secretPath: '/etc/shadow' } })),
await api.settings.replace(request({ ns: 'some-other-plugin', section: {} })),
]) {
const error = expectErr(response)
expect(error.code).toBe('settings-not-exposed')
expect(error.details).toEqual({ ns: 'some-other-plugin' })
}
// The write never reached the seam.
expect(ctx.settings.describe().find(d => String(d.ns) === 'some-other-plugin')?.value).toEqual({})
})
it('serves the product onboarding namespace without invalidating the model catalog', async () => {
const ctx = await harness()
ctx.settings.register(settingsNamespace('ui-onboarding'), z.object({ welcomeNoticeVersion: z.string() }))
const api = createApiProxy(ctx, DEFAULTS)
expect(expectOk(await api.settings.describe(request({}))).namespaces.map(view => view.ns))
.toEqual(['ui-onboarding'])
const frames = await collectHost(api, ['host/settings-changed'], 1, async () => {
expectOk(await api.settings.mutate(request({
ns: 'ui-onboarding',
ops: [{ op: 'set', path: ['welcomeNoticeVersion'], value: 'v1' }],
})))
})
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'ui-onboarding' }])
})
it('refuses even a model-provider namespace once its directory entry is gone', async () => {
const ctx = await harness({ configurableProviders: false })
ctx.settings.register(NS, AdapterConfig)
const api = createApiProxy(ctx, DEFAULTS)
expect(expectOk(await api.settings.describe(request({}))).namespaces).toEqual([])
expect(expectErr(await api.settings.update(request({ ns: 'llm-deepseek', patch: { baseURL: 'https://x' } }))).code)
.toBe('settings-not-exposed')
})
it('invalidates the model catalog when a provider namespace changes, and broadcasts a raw-only change', async () => {
// Editing `models` changes no route, so llm/adapters-updated never fires
// and an open model picker kept serving the old catalog. And storing an
// override equal to the resolved value emits nothing on settings/updated,
// so another tab never learned the field became overridden.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig, { base: { baseURL: 'https://base' } })
const api = createApiProxy(ctx, DEFAULTS)
const frames = await collectHost(api, ['host/settings-changed', 'host/models-changed'], 2, async () => {
await api.settings.update(request({ ns: 'llm-deepseek', patch: { baseURL: 'https://base' } }))
})
expect(frames).toEqual([
{ type: 'host/settings-changed', ns: 'llm-deepseek' },
{ type: 'host/models-changed' },
])
// The resolved value never moved: base already said https://base.
expect(expectOk(await api.settings.describe(request({}))).namespaces[0]!.value)
.toEqual({ apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://base' })
})
it('broadcasts a permission change without invalidating the model catalog', async () => {
const ctx = await harness()
const permission = ctx.settings.register(settingsNamespace('permission'), z.object({
defaultPreset: z.union(['read-only', 'workspace-write']).required(),
}), {
base: { defaultPreset: 'read-only' },
})
const api = createApiProxy(ctx, DEFAULTS)
const frames = await collectHost(api, ['host/settings-changed', 'host/models-changed'], 1, async () => {
await permission.update({ defaultPreset: 'workspace-write' })
})
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'permission' }])
})
it('maps a stale expectedRevision to settings-conflict carrying both revisions', async () => {
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
const api = createApiProxy(ctx, DEFAULTS)
const opened = expectOk(await api.settings.describe(request({}))).namespaces[0]!.revision
expect(expectOk(await api.settings.update(request({ ns: 'llm-deepseek', patch: { baseURL: 'https://first' }, expectedRevision: opened })))
.revision).toBe(opened + 1)
const error = expectErr(await api.settings.update(request({ ns: 'llm-deepseek', patch: { baseURL: 'https://second' }, expectedRevision: opened })))
expect(error.code).toBe('settings-conflict')
expect(error.details).toEqual({ ns: 'llm-deepseek', expected: opened, actual: opened + 1 })
// The refused write changed nothing.
expect(expectOk(await api.settings.describe(request({}))).namespaces[0]!.user).toEqual({ baseURL: 'https://first' })
})
it('updates the user layer, answers with the new redacted view, and broadcasts the frame', async () => {
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig, { base: { baseURL: 'https://base' } })
const api = createApiProxy(ctx, DEFAULTS)
const frames = await collectHost(api, ['host/settings-changed'], 1, async () => {
const view = expectOk(await api.settings.update(request({ ns: 'llm-deepseek', patch: { apiKey: 'sk-new', baseURL: 'https://next' } })))
expect(view.value).toEqual({ apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://next' })
expect(view.user).toEqual({ baseURL: 'https://next' })
expect(view.secrets).toEqual([{ path: ['apiKey'], set: true }])
expect(JSON.stringify(view)).not.toContain('sk-new')
})
expect(frames).toEqual([{ type: 'host/settings-changed', ns: 'llm-deepseek' }])
})
it('replace resets the user layer wholesale', async () => {
const ctx = await harness({ settings: { doc: { 'llm-deepseek': { baseURL: 'https://user' } } } })
ctx.settings.register(NS, AdapterConfig)
const api = createApiProxy(ctx, DEFAULTS)
const view = expectOk(await api.settings.replace(request({ ns: 'llm-deepseek', section: {} })))
expect(view.value).toEqual({ apiKeyEnv: 'DEEPSEEK_API_KEY' })
expect(view.user).toEqual({})
})
it.each([
['an invalid namespace name', 'Not A Namespace', {}],
['a schema-invalid patch', 'llm-deepseek', { baseURL: 42 }],
])('rejects %s as settings-rejected', async (_case, ns, patch) => {
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
const api = createApiProxy(ctx, DEFAULTS)
const error = expectErr(await api.settings.update(request({ ns, patch })))
expect(error.code).toBe('settings-rejected')
expect(error.details).toEqual({ ns })
})
it('answers an unregistered namespace exactly like an unexposed one', async () => {
// Deliberately indistinguishable: separating "does not exist" from
// "exists but is not yours to configure" would let a caller enumerate the
// registered namespaces one probe at a time.
const ctx = await harness()
ctx.settings.register(NS, AdapterConfig)
ctx.settings.register(settingsNamespace('some-other-plugin'), z.object({ secretPath: z.string() }))
const api = createApiProxy(ctx, DEFAULTS)
const unknown = expectErr(await api.settings.update(request({ ns: 'unknown-ns', patch: {} })))
const unexposed = expectErr(await api.settings.update(request({ ns: 'some-other-plugin', patch: {} })))
expect(unknown.code).toBe('settings-not-exposed')
expect(unexposed.code).toBe(unknown.code)
expect(unexposed.message.replace('some-other-plugin', 'unknown-ns')).toBe(unknown.message)
})
it('maps a read-only provider refusal onto the same rejection', async () => {
const ctx = await harness({ settings: { readOnly: true } })
ctx.settings.register(NS, AdapterConfig)
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.settings.describe(request({})))
expect(value.writable).toBe(false)
const error = expectErr(await api.settings.update(request({ ns: 'llm-deepseek', patch: {} })))
expect(error.code).toBe('settings-rejected')
expect(error.message).toContain('read-only')
})
})
describe('credentials domain', () => {
it('reports an actionable error when no credential provider is mounted', async () => {
const ctx = await harness({ credentials: false })
const api = createApiProxy(ctx, DEFAULTS)
const error = expectErr(await api.credentials.describe(request({ refs: ['A'] })))
expect(error.code).toBe('internal')
expect(error.message).toContain('dsh-credentials-local')
})
it('describes value-free views and flips state through set/unset with frames', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
const before = expectOk(await api.credentials.describe(request({ refs: ['OPENAI_API_KEY'] })))
expect(before.credentials).toEqual({ OPENAI_API_KEY: { configured: false, writable: true } })
const frames = await collectHost(api, ['host/credentials-changed'], 2, async () => {
expectOk(await api.credentials.set(request({ ref: 'OPENAI_API_KEY', value: 'sk-secret' })))
const after = expectOk(await api.credentials.describe(request({ refs: ['OPENAI_API_KEY'] })))
expect(after.credentials).toEqual({ OPENAI_API_KEY: { configured: true, source: 'file', writable: true } })
expect(JSON.stringify(after)).not.toContain('sk-secret')
expectOk(await api.credentials.unset(request({ ref: 'OPENAI_API_KEY' })))
})
expect(frames).toEqual([
{ type: 'host/credentials-changed', ref: 'OPENAI_API_KEY' },
{ type: 'host/credentials-changed', ref: 'OPENAI_API_KEY' },
])
})
it('maps a shadowed write onto credential-rejected for set and unset alike', async () => {
const ctx = await harness({ credentials: { shadowed: ['DEEPSEEK_API_KEY'] } })
const api = createApiProxy(ctx, DEFAULTS)
const described = expectOk(await api.credentials.describe(request({ refs: ['DEEPSEEK_API_KEY'] })))
expect(described.credentials['DEEPSEEK_API_KEY']).toEqual({ configured: true, source: 'env', writable: false })
const setError = expectErr(await api.credentials.set(request({ ref: 'DEEPSEEK_API_KEY', value: 'x' })))
expect(setError.code).toBe('credential-rejected')
expect(setError.details).toEqual({ ref: 'DEEPSEEK_API_KEY' })
const unsetError = expectErr(await api.credentials.unset(request({ ref: 'DEEPSEEK_API_KEY' })))
expect(unsetError.code).toBe('credential-rejected')
})
})
describe('llm domain', () => {
it('merges the configurable directory with live routes and appends undeclared ones', async () => {
const ctx = await harness({ configurableProviders: false })
ctx.llm.registerConfigurableProviders([
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [] },
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'] },
])
ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', ['deepseek-v4-flash']))
ctx.llm.registerAdapter(['undeclared'], new CatalogAdapter('Undeclared', ['u-1']))
// Only one namespace can answer an interrogation, so the flag follows the
// entry's namespace rather than being assumed for every row.
ctx.llm.registerModelDiscovery('llm-pi-ai', () => Promise.resolve([]))
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.llm.providers(request({})))
expect(value.providers).toEqual([
{ provider: 'deepseek-official', displayName: 'DeepSeek', settingsNs: 'llm-deepseek', settingsPath: [], active: true, supportsDiscovery: false },
{ provider: 'openai', displayName: 'openai', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'openai'], active: false, supportsDiscovery: true },
// An undeclared live route has no settings address, so nothing can be
// interrogated on its behalf either.
{ provider: 'undeclared', displayName: 'Undeclared', settingsNs: '', settingsPath: [], active: true, supportsDiscovery: false },
])
})
it('serves the host-scoped catalog with per-provider failures contained', async () => {
const ctx = await harness()
ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', ['deepseek-v4-flash', 'deepseek-v4-pro']))
ctx.llm.registerAdapter(['broken'], new BrokenCatalogAdapter('Broken', []))
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.llm.models(request({})))
expect(value.groups).toEqual([{
id: 'deepseek-official',
name: 'DeepSeek',
models: [
{ id: 'deepseek-v4-flash', name: 'deepseek-v4-flash' },
{ id: 'deepseek-v4-pro', name: 'deepseek-v4-pro' },
],
}])
expect(value.failures).toEqual([{ id: 'broken', name: 'Broken', message: 'catalog backend down' }])
})
it('broadcasts host/models-changed at every topology commit point', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
const frames = await collectHost(api, ['host/models-changed'], 2, async () => {
const dispose = ctx.llm.registerAdapter(['deepseek-official'], new CatalogAdapter('DeepSeek', []))
dispose()
return Promise.resolve()
})
expect(frames).toEqual([{ type: 'host/models-changed' }, { type: 'host/models-changed' }])
})
})
describe('llm.discoverModels', () => {
it('carries a draft to its namespace and returns candidates without storing anything', async () => {
const ctx = await harness()
const seen: unknown[] = []
ctx.llm.registerModelDiscovery('llm-pi-ai', (probe) => {
seen.push({ baseURL: probe.baseURL, api: probe.api, apiKey: probe.apiKey })
return Promise.resolve([
{ id: 'acme-large', name: 'Acme Large', contextWindow: 65_536, maxTokens: 4096 },
{ id: 'acme-small' },
])
})
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.llm.discoverModels(request({
settingsNs: 'llm-pi-ai',
baseURL: 'https://gateway.acme.example/v1',
api: 'openai-completions',
apiKey: 'probe-key',
})))
expect(value.models).toEqual([
{ id: 'acme-large', name: 'Acme Large', contextWindow: 65_536, maxTokens: 4096 },
{ id: 'acme-small' },
])
expect(seen).toEqual([{
baseURL: 'https://gateway.acme.example/v1',
api: 'openai-completions',
apiKey: 'probe-key',
}])
// Interrogating a draft is a read: no namespace gained a section, and no
// credential reference was written.
expect(expectOk(await api.settings.describe(request({}))).namespaces.map(view => view.ns))
.not.toContain('llm-pi-ai')
})
it('carries the route being edited so an adapter can answer from its own registry', async () => {
const ctx = await harness()
let probe: unknown
ctx.llm.registerModelDiscovery('llm-pi-ai', (request_) => {
probe = request_
return Promise.resolve([{ id: 'from-registry', contextWindow: 65_536, maxTokens: 4096 }])
})
const api = createApiProxy(ctx, DEFAULTS)
const value = expectOk(await api.llm.discoverModels(request({
settingsNs: 'llm-pi-ai',
provider: 'deepseek',
})))
// No endpoint at all: a route the adapter already describes needs none.
expect(probe).toEqual({ provider: 'deepseek' })
expect(value.models).toEqual([{ id: 'from-registry', contextWindow: 65_536, maxTokens: 4096 }])
})
it('omits a credential and protocol the draft does not name', async () => {
const ctx = await harness()
let probe: unknown
ctx.llm.registerModelDiscovery('llm-pi-ai', (request_) => {
probe = request_
return Promise.resolve([])
})
const api = createApiProxy(ctx, DEFAULTS)
expectOk(await api.llm.discoverModels(request({
settingsNs: 'llm-pi-ai',
baseURL: 'https://gateway.acme.example/v1',
})))
// Absent fields stay absent rather than crossing as explicit undefined:
// the adapter distinguishes "no protocol named" from "protocol undefined".
expect(probe).toEqual({ baseURL: 'https://gateway.acme.example/v1' })
})
it('reports a failed interrogation as the form\'s next move, naming no credential', async () => {
const ctx = await harness()
ctx.llm.registerModelDiscovery('llm-pi-ai', () =>
Promise.reject(new Error('https://gateway.acme.example/v1/models answered 401; check the API key')))
const api = createApiProxy(ctx, DEFAULTS)
const error = expectErr(await api.llm.discoverModels(request({
settingsNs: 'llm-pi-ai',
baseURL: 'https://gateway.acme.example/v1',
apiKey: 'wrong',
})))
expect(error.code).toBe('model-discovery-failed')
expect(error.message).toContain('answered 401; check the API key')
expect(error.details).toEqual({ settingsNs: 'llm-pi-ai', baseURL: 'https://gateway.acme.example/v1' })
expect(JSON.stringify(error)).not.toContain('wrong')
})
it('reports a namespace no adapter family serves', async () => {
const ctx = await harness()
const api = createApiProxy(ctx, DEFAULTS)
const error = expectErr(await api.llm.discoverModels(request({
settingsNs: 'llm-deepseek',
baseURL: 'https://api.deepseek.com',
})))
expect(error.code).toBe('model-discovery-failed')
expect(error.message).toContain('no model discovery is registered')
})
})