Merge remote-tracking branch 'origin/master' into worktree/web-multimodal-image-input
# Conflicts: # .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.i18n.yaml # docs/core-data-structures/core.i18n.yaml # docs/module-graph.md # packages/client/ui-conversation/src/client/chat/AssistantMarkdown.tsx # packages/client/ui-conversation/src/client/index.ts # packages/compact/compact-basic/README.i18n.yaml
This commit is contained in:
@@ -481,6 +481,185 @@ describe('provider profile lifecycle', () => {
|
||||
.resolves.toMatchObject({ reasoning: { defaultEffort: ReasoningEffortId('off') } })
|
||||
})
|
||||
|
||||
it('serves declared reasoning efforts to selectors and honours the profile default', async () => {
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: 'https://acme.test/v1',
|
||||
reasoning: 'high',
|
||||
models: [{
|
||||
id: 'acme-think',
|
||||
contextWindow: 65_536,
|
||||
maxTokens: 4096,
|
||||
reasoningEfforts: { off: null, low: 'low', high: 'high' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Declared levels reach the same seam catalog metadata does, so the
|
||||
// effort picker works for a model pi-ai has never heard of.
|
||||
await expect(ctx.llm.resolveModelInfo('acme-gateway', 'acme-think')).resolves.toMatchObject({
|
||||
reasoning: {
|
||||
efforts: [
|
||||
{ id: ReasoningEffortId('off'), name: 'Off' },
|
||||
{ id: ReasoningEffortId('low'), name: 'Low' },
|
||||
{ id: ReasoningEffortId('high'), name: 'High' },
|
||||
],
|
||||
defaultEffort: ReasoningEffortId('high'),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('sends the declared wire spelling and refuses undeclared levels before network I/O', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
models: [{
|
||||
id: 'acme-think',
|
||||
contextWindow: 65_536,
|
||||
maxTokens: 4096,
|
||||
reasoningEfforts: { off: null, high: 'ultra' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await assemble(ctx, {
|
||||
provider: 'acme-gateway',
|
||||
model: 'acme-think',
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
messages: [],
|
||||
})
|
||||
// The declared value, not the canonical level name, goes on the wire.
|
||||
expect(server.requests[0]).toMatchObject({ reasoning_effort: 'ultra' })
|
||||
|
||||
const undeclared = await assemble(ctx, {
|
||||
provider: 'acme-gateway',
|
||||
model: 'acme-think',
|
||||
reasoningEffort: ReasoningEffortId('max'),
|
||||
messages: [],
|
||||
})
|
||||
expect(undeclared.finish).toMatchObject({
|
||||
kind: 'error',
|
||||
failure: { code: 'UNSUPPORTED_REASONING_EFFORT' },
|
||||
})
|
||||
expect(server.requests).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('dispatches the compat-switched dialect on a declared route', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }, { events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
// Without the switch pi-ai guesses the dialect from the endpoint
|
||||
// URL, and a private gateway's URL says nothing.
|
||||
compat: { thinkingFormat: 'deepseek' },
|
||||
models: [{
|
||||
id: 'acme-think',
|
||||
contextWindow: 65_536,
|
||||
maxTokens: 4096,
|
||||
reasoningEfforts: { off: null, high: 'high' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
const prompt = (effort: string): Promise<unknown> => assemble(ctx, {
|
||||
provider: 'acme-gateway',
|
||||
model: 'acme-think',
|
||||
reasoningEffort: ReasoningEffortId(effort),
|
||||
messages: [],
|
||||
})
|
||||
|
||||
await prompt('high')
|
||||
expect(server.requests[0]).toMatchObject({ thinking: { type: 'enabled' }, reasoning_effort: 'high' })
|
||||
|
||||
await prompt('off')
|
||||
expect(server.requests[1]).toMatchObject({ thinking: { type: 'disabled' } })
|
||||
expect(server.requests[1]).not.toHaveProperty('reasoning_effort')
|
||||
})
|
||||
|
||||
it('sends a declared off value as the effort parameter instead of omitting it', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
models: [{
|
||||
id: 'acme-think',
|
||||
contextWindow: 65_536,
|
||||
maxTokens: 4096,
|
||||
reasoningEfforts: { off: 'none', high: 'high' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// The adapter strips a selected Off to "no reasoning option", and pi-ai's
|
||||
// dispatch reads thinkingLevelMap.off exactly then — so the declared value
|
||||
// still reaches the wire, which is the README's promise for `off: none`.
|
||||
await assemble(ctx, {
|
||||
provider: 'acme-gateway',
|
||||
model: 'acme-think',
|
||||
reasoningEffort: ReasoningEffortId('off'),
|
||||
messages: [],
|
||||
})
|
||||
expect(server.requests[0]).toMatchObject({ reasoning_effort: 'none' })
|
||||
})
|
||||
|
||||
it('holds back reasoning_effort when the endpoint cannot take it', async () => {
|
||||
vi.stubEnv('PI_TEST_KEY', 'test-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
const ctx = new Context()
|
||||
await ctx.plugin(LlmService)
|
||||
await ctx.plugin(LlmPiAi, {
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
apiKeyEnv: 'PI_TEST_KEY',
|
||||
api: 'openai-completions',
|
||||
baseURL: `${server.url}/v1`,
|
||||
compat: { supportsReasoningEffort: false },
|
||||
models: [{
|
||||
id: 'acme-think',
|
||||
contextWindow: 65_536,
|
||||
maxTokens: 4096,
|
||||
reasoningEfforts: { off: null, high: 'high' },
|
||||
}],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await assemble(ctx, {
|
||||
provider: 'acme-gateway',
|
||||
model: 'acme-think',
|
||||
reasoningEffort: ReasoningEffortId('high'),
|
||||
messages: [],
|
||||
})
|
||||
expect(server.requests[0]).not.toHaveProperty('reasoning_effort')
|
||||
})
|
||||
|
||||
it('accepts absent credentials for pi-ai ambient authentication', async () => {
|
||||
vi.stubEnv('DEEPSEEK_API_KEY', 'ambient-key')
|
||||
const server = await mockServer([{ events: textEvents }])
|
||||
|
||||
@@ -10,8 +10,8 @@ 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 } from '@earendil-works/pi-ai'
|
||||
import type { Api, Model, Provider } from '@earendil-works/pi-ai'
|
||||
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'
|
||||
@@ -484,6 +484,248 @@ describe('catalog routes with per-model configuration', () => {
|
||||
})
|
||||
})
|
||||
|
||||
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('reasoning-dispatch 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', () => {
|
||||
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 }])
|
||||
|
||||
32
packages/llm/llm-pi-ai/tests/config.spec.ts
Normal file
32
packages/llm/llm-pi-ai/tests/config.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Config } from '../src/config.ts'
|
||||
|
||||
describe('reasoning schema boundary', () => {
|
||||
const configWith = (model: Record<string, unknown>): (() => unknown) =>
|
||||
() => Config({
|
||||
providers: {
|
||||
'acme-gateway': {
|
||||
api: 'openai-completions',
|
||||
baseURL: 'https://acme.test',
|
||||
models: [{ id: 'm', ...model }],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
it('rejects a level pi-ai does not know at the write that produced it', () => {
|
||||
expect(configWith({ reasoningEfforts: { ultra: 'x' } })).toThrow(/"off"/)
|
||||
expect(configWith({ reasoningEfforts: { high: 42 } })).toThrow()
|
||||
})
|
||||
|
||||
it('keeps false distinguishable from an absent declaration', () => {
|
||||
type Materialized = { providers: Record<string, { models?: { reasoningEfforts?: unknown }[] }> }
|
||||
const withFalse = configWith({ reasoningEfforts: false })() as Materialized
|
||||
expect(withFalse.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBe(false)
|
||||
const absent = configWith({})() as Materialized
|
||||
expect(absent.providers['acme-gateway']?.models?.[0]?.reasoningEfforts).toBeUndefined()
|
||||
})
|
||||
|
||||
it('rejects a thinking format outside the offered set', () => {
|
||||
expect(configWith({ compat: { thinkingFormat: 'quantum' } })).toThrow(/expected/)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user