340 lines
14 KiB
TypeScript
340 lines
14 KiB
TypeScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
|
||
|
|
import {
|
||
|
|
applyResponsesApiStatelessStoreOption,
|
||
|
|
buildCattyReasoningProviderOptions,
|
||
|
|
cattyReasoningLevelsForSelection,
|
||
|
|
estimateReasoningOutputReserve,
|
||
|
|
openaiModelLikelySupportsReasoning,
|
||
|
|
openaiModelSupportsNoneReasoning,
|
||
|
|
resolveVisibleCattyThinkingLevel,
|
||
|
|
} from './cattyReasoning';
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions is omitted when effort is off', () => {
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'off'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, undefined),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions maps OpenAI-compatible effort', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'deepseek' }, 'high'),
|
||
|
|
{ openai: { reasoningEffort: 'high' } },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions omits reasoningEffort for non-reasoning OpenAI models', () => {
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'high', 'gpt-4o'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'high', 'gpt-5.5'),
|
||
|
|
{ openai: { reasoningEffort: 'high' } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'off', 'o3-mini'),
|
||
|
|
{ openai: { reasoningEffort: 'low' } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'off', 'gpt-5'),
|
||
|
|
{ openai: { reasoningEffort: 'minimal' } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'off', 'gpt-5.5'),
|
||
|
|
{ openai: { reasoningEffort: 'none' } },
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, undefined, 'gpt-5.5'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(openaiModelSupportsNoneReasoning('o4-mini'), false);
|
||
|
|
assert.equal(openaiModelSupportsNoneReasoning('gpt-5.1-codex'), true);
|
||
|
|
assert.equal(openaiModelSupportsNoneReasoning('gpt-5.1-chat-latest'), false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('cattyReasoningLevelsForSelection hides the chip unless the model can take effort', () => {
|
||
|
|
assert.equal(openaiModelLikelySupportsReasoning('gpt-4o'), false);
|
||
|
|
assert.equal(openaiModelLikelySupportsReasoning('gpt-5-chat-latest'), false);
|
||
|
|
assert.equal(openaiModelLikelySupportsReasoning('gpt-5.1-chat-latest'), false);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'high', 'gpt-5-chat-latest'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'off', 'gpt-5.1-chat-latest'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(openaiModelLikelySupportsReasoning('gpt-5.5'), true);
|
||
|
|
assert.deepEqual(cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-4o'), []);
|
||
|
|
assert.ok(cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-5.5').includes('high'));
|
||
|
|
assert.ok(cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-5.5').includes('off'));
|
||
|
|
assert.equal(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-5.5'),
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-5.6'),
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'gpt-5'),
|
||
|
|
['minimal', 'low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'openai' }, 'o3-mini'),
|
||
|
|
['low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-1.5-flash'), []);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3-flash'),
|
||
|
|
['minimal', 'low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.ok(cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3-flash').includes('minimal'));
|
||
|
|
assert.ok(!cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3-flash').includes('off'));
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3.7-flash'),
|
||
|
|
['low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3.1-flash-lite-image'),
|
||
|
|
['minimal', 'high'],
|
||
|
|
);
|
||
|
|
assert.ok(cattyReasoningLevelsForSelection({ providerId: 'anthropic' }, 'claude-opus-4-6').includes('high'));
|
||
|
|
assert.ok(cattyReasoningLevelsForSelection({ providerId: 'anthropic' }, 'claude-sonnet-5').includes('high'));
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'anthropic' }, 'claude-fable-5'),
|
||
|
|
['low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'anthropic' }, 'claude-3-haiku-20240307'),
|
||
|
|
[],
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'high', 'claude-3-haiku-20240307'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3-pro'),
|
||
|
|
['low', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-3.1-pro-preview'),
|
||
|
|
['low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
cattyReasoningLevelsForSelection({ providerId: 'google' }, 'gemini-2.5-pro'),
|
||
|
|
['low', 'medium', 'high'],
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('resolveVisibleCattyThinkingLevel drops stale levels after a model switch', () => {
|
||
|
|
assert.equal(
|
||
|
|
resolveVisibleCattyThinkingLevel(['low', 'medium', 'high'], 'minimal'),
|
||
|
|
'low',
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveVisibleCattyThinkingLevel(['minimal', 'low', 'medium', 'high'], 'off'),
|
||
|
|
'minimal',
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveVisibleCattyThinkingLevel(['off', 'low', 'medium', 'high'], 'high'),
|
||
|
|
'high',
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveVisibleCattyThinkingLevel(['low', 'high'], 'medium'),
|
||
|
|
'high',
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
resolveVisibleCattyThinkingLevel(['low', 'medium', 'high'], 'off'),
|
||
|
|
'low',
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('estimateReasoningOutputReserve folds thinking budgets into the output reserve', () => {
|
||
|
|
assert.equal(estimateReasoningOutputReserve(undefined), 0);
|
||
|
|
assert.equal(
|
||
|
|
estimateReasoningOutputReserve(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'high', 'claude-sonnet-4-5'),
|
||
|
|
),
|
||
|
|
20_000,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
estimateReasoningOutputReserve(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'medium', 'claude-3-7-sonnet-20250219'),
|
||
|
|
),
|
||
|
|
10_000,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
estimateReasoningOutputReserve(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'high', 'claude-opus-4-6'),
|
||
|
|
),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
estimateReasoningOutputReserve(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'high', 'gemini-2.5-pro'),
|
||
|
|
),
|
||
|
|
16_384,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
estimateReasoningOutputReserve(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'openai' }, 'high', 'gpt-5.5'),
|
||
|
|
),
|
||
|
|
0,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions maps Anthropic thinking budgets', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'medium'),
|
||
|
|
{ anthropic: { thinking: { type: 'enabled', budgetTokens: 10_000 } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'medium', 'claude-sonnet-4-5'),
|
||
|
|
{ anthropic: { thinking: { type: 'enabled', budgetTokens: 10_000 } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'high', 'claude-opus-4-20250514'),
|
||
|
|
{ anthropic: { thinking: { type: 'enabled', budgetTokens: 20_000 } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'high', 'claude-opus-4-6'),
|
||
|
|
{ anthropic: { thinking: { type: 'adaptive' }, effort: 'high' } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'low', 'claude-sonnet-5'),
|
||
|
|
{ anthropic: { thinking: { type: 'adaptive' }, effort: 'low' } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'off', 'claude-sonnet-5'),
|
||
|
|
{ anthropic: { thinking: { type: 'disabled' } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'off', 'claude-fable-5'),
|
||
|
|
{ anthropic: { thinking: { type: 'adaptive' }, effort: 'low' } },
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'anthropic' }, 'off', 'claude-sonnet-4-5'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions respects an explicit style override', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'custom', style: 'openai' }, 'low'),
|
||
|
|
{ openai: { reasoningEffort: 'low' } },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('buildCattyReasoningProviderOptions maps Gemini thinking levels', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'high', 'gemini-3-pro'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'high', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'off', 'gemini-3-pro'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'low', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'medium', 'gemini-3-pro'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'high', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'medium', 'gemini-3.1-pro-preview'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'medium', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'off', 'gemini-3-flash'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'minimal', includeThoughts: false } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'minimal', 'gemini-3.7-flash'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'low', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'off', 'gemini-3.7-flash'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'low', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'low', 'gemini-3.1-flash-lite-image'),
|
||
|
|
{ google: { thinkingConfig: { thinkingLevel: 'high', includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, undefined, 'gemini-3-flash'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'high', 'gemini-2.5-pro'),
|
||
|
|
{ google: { thinkingConfig: { thinkingBudget: 16_384, includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'off', 'gemini-2.5-pro'),
|
||
|
|
{ google: { thinkingConfig: { thinkingBudget: 1_024, includeThoughts: true } } },
|
||
|
|
);
|
||
|
|
assert.deepEqual(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'off', 'gemini-2.5-flash'),
|
||
|
|
{ google: { thinkingConfig: { thinkingBudget: 0, includeThoughts: false } } },
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, undefined, 'gemini-2.5-flash'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'high'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
assert.equal(
|
||
|
|
buildCattyReasoningProviderOptions({ providerId: 'google' }, 'high', 'gemini-1.5-flash'),
|
||
|
|
undefined,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyResponsesApiStatelessStoreOption sets store:false for OpenAI Responses providers', () => {
|
||
|
|
const responsesProvider = { providerId: 'openai' as const, openaiApi: 'responses' as const };
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption(responsesProvider, { openai: { reasoningEffort: 'high' } }),
|
||
|
|
{ openai: { reasoningEffort: 'high', store: false, include: ['reasoning.encrypted_content'] } },
|
||
|
|
);
|
||
|
|
// Even with no reasoning options, Responses turns stay stateless
|
||
|
|
// and request replayable (encrypted) reasoning.
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption(responsesProvider, undefined),
|
||
|
|
{ openai: { store: false, include: ['reasoning.encrypted_content'] } },
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyResponsesApiStatelessStoreOption is a no-op for Chat Completions and other styles', () => {
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption({ providerId: 'openai' as const }, { openai: { reasoningEffort: 'high' } }),
|
||
|
|
{ openai: { reasoningEffort: 'high' } },
|
||
|
|
);
|
||
|
|
const chatProvider = { providerId: 'openai' as const, openaiApi: 'chat' as const };
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption(chatProvider, { openai: { reasoningEffort: 'low' } }),
|
||
|
|
{ openai: { reasoningEffort: 'low' } },
|
||
|
|
);
|
||
|
|
const anthropicProvider = { providerId: 'openai' as const, openaiApi: 'responses' as const, style: 'anthropic' as const };
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption(anthropicProvider, { anthropic: { thinking: { type: 'adaptive' } } }),
|
||
|
|
{ anthropic: { thinking: { type: 'adaptive' } } },
|
||
|
|
);
|
||
|
|
assert.equal(applyResponsesApiStatelessStoreOption(undefined, undefined), undefined);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('applyResponsesApiStatelessStoreOption always requests encrypted reasoning on stateless Responses turns', () => {
|
||
|
|
const responsesProvider = { providerId: 'custom' as const, openaiApi: 'responses' as const };
|
||
|
|
// Every stateless Responses turn gets the include, covering known reasoner
|
||
|
|
// IDs (`deepseek-r1`, `gpt-oss-120b`, `grok-4`, `o3`, `gpt-5.1`) as before,
|
||
|
|
// always-thinking relay models whose IDs match no classifier (e.g.
|
||
|
|
// DeepSeek's default `deepseek-v4-flash`), and plain chat models — for the
|
||
|
|
// latter the include is a no-op (no reasoning items to encrypt).
|
||
|
|
assert.deepEqual(
|
||
|
|
applyResponsesApiStatelessStoreOption(responsesProvider, undefined),
|
||
|
|
{ openai: { store: false, include: ['reasoning.encrypted_content'] } },
|
||
|
|
'deepseek-v4-flash',
|
||
|
|
);
|
||
|
|
});
|