mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(Anthropic Chat Model Node): Add adaptive thinking mode for Claude Opus 4.7+ (#29467)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import { ChatAnthropic, type ChatAnthropicInput } from '@langchain/anthropic';
|
||||
import type { LLMResult } from '@langchain/core/outputs';
|
||||
import {
|
||||
getProxyAgent,
|
||||
@@ -21,7 +21,7 @@ import { searchModels } from './methods/searchModels';
|
||||
|
||||
const ANTHROPIC_MODEL_BUILDER_HINT = {
|
||||
message:
|
||||
'Default to claude-sonnet-4-6 (latest Sonnet); use claude-opus-4-7 when the user needs the most capable model. Never use Claude Sonnet 4.5, Claude 3.x, Claude 2, or LEGACY options — those are superseded and are not valid choices.',
|
||||
'Default to claude-sonnet-4-6 (latest Sonnet); use claude-opus-4-7 when the user needs the most capable model. Never use Claude Sonnet 4.5, Claude 3.x, Claude 2, or LEGACY options — those are superseded and are not valid choices. When extended thinking is needed on Opus 4.7+, set Thinking Mode to Adaptive and choose an Effort level. The legacy Manual thinking mode is rejected by Opus 4.7.',
|
||||
};
|
||||
|
||||
const modelField: INodeProperties = {
|
||||
@@ -92,8 +92,8 @@ export class LmChatAnthropic implements INodeType {
|
||||
name: 'lmChatAnthropic',
|
||||
icon: 'file:anthropic.svg',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4],
|
||||
defaultVersion: 1.4,
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5],
|
||||
defaultVersion: 1.5,
|
||||
description: 'Language Model Anthropic',
|
||||
defaults: {
|
||||
name: 'Anthropic Chat Model',
|
||||
@@ -225,7 +225,44 @@ export class LmChatAnthropic implements INodeType {
|
||||
'The model. Choose from the list, or specify an ID. <a href="https://docs.anthropic.com/claude/docs/models-overview">Learn more</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.4 } }],
|
||||
'@version': [1.4],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Model',
|
||||
name: 'model',
|
||||
type: 'resourceLocator',
|
||||
default: {
|
||||
mode: 'list',
|
||||
value: 'claude-sonnet-4-6',
|
||||
cachedResultName: 'Claude Sonnet 4.6',
|
||||
},
|
||||
builderHint: ANTHROPIC_MODEL_BUILDER_HINT,
|
||||
required: true,
|
||||
modes: [
|
||||
{
|
||||
displayName: 'From List',
|
||||
name: 'list',
|
||||
type: 'list',
|
||||
placeholder: 'Select a model...',
|
||||
typeOptions: {
|
||||
searchListMethod: 'searchModels',
|
||||
searchable: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: 'Claude Sonnet',
|
||||
},
|
||||
],
|
||||
description:
|
||||
'The model. Choose from the list, or specify an ID. <a href="https://docs.anthropic.com/claude/docs/models-overview">Learn more</a>.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -255,6 +292,7 @@ export class LmChatAnthropic implements INodeType {
|
||||
displayOptions: {
|
||||
hide: {
|
||||
thinking: [true],
|
||||
thinkingMode: ['adaptive', 'manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -269,6 +307,7 @@ export class LmChatAnthropic implements INodeType {
|
||||
displayOptions: {
|
||||
hide: {
|
||||
thinking: [true],
|
||||
thinkingMode: ['adaptive', 'manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -283,6 +322,7 @@ export class LmChatAnthropic implements INodeType {
|
||||
displayOptions: {
|
||||
hide: {
|
||||
thinking: [true],
|
||||
thinkingMode: ['adaptive', 'manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -292,6 +332,11 @@ export class LmChatAnthropic implements INodeType {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description: 'Whether to enable thinking mode for the model',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lte: 1.4 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Thinking Budget (Tokens)',
|
||||
@@ -301,10 +346,94 @@ export class LmChatAnthropic implements INodeType {
|
||||
description: 'The maximum number of tokens to use for thinking',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { lte: 1.4 } }],
|
||||
thinking: [true],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Thinking Mode',
|
||||
name: 'thinkingMode',
|
||||
type: 'options',
|
||||
default: 'disabled',
|
||||
description: 'How extended thinking should be configured for the model',
|
||||
options: [
|
||||
{
|
||||
name: 'Disabled',
|
||||
value: 'disabled',
|
||||
description: 'No extended thinking',
|
||||
},
|
||||
{
|
||||
name: 'Adaptive (Recommended)',
|
||||
value: 'adaptive',
|
||||
description: 'Claude decides how much to think; control with Effort',
|
||||
},
|
||||
{
|
||||
name: 'Manual (Deprecated)',
|
||||
value: 'manual',
|
||||
description: 'Legacy fixed-budget mode; rejected by Opus 4.7+',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Effort',
|
||||
name: 'effort',
|
||||
type: 'options',
|
||||
default: 'medium',
|
||||
description: 'Effort level for adaptive thinking',
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
{ name: 'X-High', value: 'xhigh' },
|
||||
{ name: 'Max', value: 'max' },
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
thinkingMode: ['adaptive'],
|
||||
'/model.value': [{ _cnd: { includes: 'opus' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Effort',
|
||||
name: 'effort',
|
||||
type: 'options',
|
||||
default: 'medium',
|
||||
description: 'Effort level for adaptive thinking',
|
||||
options: [
|
||||
{ name: 'Low', value: 'low' },
|
||||
{ name: 'Medium', value: 'medium' },
|
||||
{ name: 'High', value: 'high' },
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
thinkingMode: ['adaptive'],
|
||||
'/model.value': [{ _cnd: { regex: '^(?!.*opus).*' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Thinking Budget (Tokens)',
|
||||
name: 'thinkingBudget',
|
||||
type: 'number',
|
||||
default: MIN_THINKING_BUDGET,
|
||||
description: 'Maximum tokens used for thinking. Manual mode is rejected by Opus 4.7+.',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [{ _cnd: { gte: 1.5 } }],
|
||||
thinkingMode: ['manual'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
@@ -333,30 +462,42 @@ export class LmChatAnthropic implements INodeType {
|
||||
|
||||
const options = this.getNodeParameter('options', itemIndex, {}) as {
|
||||
maxTokensToSample?: number;
|
||||
temperature: number;
|
||||
temperature?: number;
|
||||
topK?: number;
|
||||
topP?: number;
|
||||
thinking?: boolean;
|
||||
thinkingBudget?: number;
|
||||
};
|
||||
let invocationKwargs = {};
|
||||
|
||||
const tokensUsageParser = (result: LLMResult) => {
|
||||
const usage = (result?.llmOutput?.usage as {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
}) ?? {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
};
|
||||
return {
|
||||
completionTokens: usage.output_tokens,
|
||||
promptTokens: usage.input_tokens,
|
||||
totalTokens: usage.input_tokens + usage.output_tokens,
|
||||
};
|
||||
thinkingMode?: 'disabled' | 'adaptive' | 'manual';
|
||||
effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
||||
};
|
||||
|
||||
if (options.thinking) {
|
||||
const isOpus47Model = modelName.startsWith('claude-opus-4-7');
|
||||
const thinkingMode: 'disabled' | 'adaptive' | 'manual' =
|
||||
version >= 1.5
|
||||
? (options.thinkingMode ?? 'disabled')
|
||||
: options.thinking
|
||||
? 'manual'
|
||||
: 'disabled';
|
||||
|
||||
if (thinkingMode === 'manual' && isOpus47Model) {
|
||||
throw new NodeOperationError(
|
||||
this.getNode(),
|
||||
`Manual thinking mode is not supported on "${modelName}". Use Thinking Mode = Adaptive (with Effort) instead.`,
|
||||
{ itemIndex },
|
||||
);
|
||||
}
|
||||
|
||||
let invocationKwargs: Record<string, unknown> = {};
|
||||
if (thinkingMode === 'adaptive') {
|
||||
invocationKwargs = {
|
||||
thinking: { type: 'adaptive' },
|
||||
output_config: { effort: options.effort ?? 'medium' },
|
||||
max_tokens: options.maxTokensToSample ?? DEFAULT_MAX_TOKENS,
|
||||
top_k: undefined,
|
||||
top_p: undefined,
|
||||
temperature: undefined,
|
||||
};
|
||||
} else if (thinkingMode === 'manual') {
|
||||
invocationKwargs = {
|
||||
thinking: {
|
||||
type: 'enabled',
|
||||
@@ -376,6 +517,21 @@ export class LmChatAnthropic implements INodeType {
|
||||
};
|
||||
}
|
||||
|
||||
const tokensUsageParser = (result: LLMResult) => {
|
||||
const usage = (result?.llmOutput?.usage as {
|
||||
input_tokens: number;
|
||||
output_tokens: number;
|
||||
}) ?? {
|
||||
input_tokens: 0,
|
||||
output_tokens: 0,
|
||||
};
|
||||
return {
|
||||
completionTokens: usage.output_tokens,
|
||||
promptTokens: usage.input_tokens,
|
||||
totalTokens: usage.input_tokens + usage.output_tokens,
|
||||
};
|
||||
};
|
||||
|
||||
const clientOptions: {
|
||||
fetchOptions?: { dispatcher: ReturnType<typeof getProxyAgent> };
|
||||
defaultHeaders?: Record<string, string>;
|
||||
@@ -412,19 +568,25 @@ export class LmChatAnthropic implements INodeType {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const model = new ChatAnthropic({
|
||||
const chatAnthropicParams: ChatAnthropicInput = {
|
||||
anthropicApiKey: credentials.apiKey,
|
||||
model: modelName,
|
||||
anthropicApiUrl: baseURL,
|
||||
maxTokens: options.maxTokensToSample,
|
||||
temperature: options.temperature,
|
||||
topK: options.topK,
|
||||
topP: options.topP,
|
||||
callbacks: [new N8nLlmTracing(this, { tokensUsageParser })],
|
||||
onFailedAttempt: makeN8nLlmFailedAttemptHandler(this, gatewayErrorHandler),
|
||||
invocationKwargs,
|
||||
clientOptions,
|
||||
});
|
||||
};
|
||||
|
||||
// Opus 4.7 rejects temperature/topK/topP at the SDK layer regardless of thinking mode
|
||||
if (!isOpus47Model) {
|
||||
chatAnthropicParams.temperature = options.temperature;
|
||||
chatAnthropicParams.topK = options.topK;
|
||||
chatAnthropicParams.topP = options.topP;
|
||||
}
|
||||
|
||||
const model = new ChatAnthropic(chatAnthropicParams);
|
||||
|
||||
// Some Anthropic models do not support Langchain default of -1 for topP so we need to unset it
|
||||
if (options.topP === undefined) {
|
||||
|
||||
+240
-5
@@ -4,7 +4,7 @@
|
||||
import { ChatAnthropic } from '@langchain/anthropic';
|
||||
import { makeN8nLlmFailedAttemptHandler, N8nLlmTracing, getProxyAgent } from '@n8n/ai-utilities';
|
||||
import { createMockExecuteFunction } from 'n8n-nodes-base/test/nodes/Helpers';
|
||||
import type { INode, ISupplyDataFunctions } from 'n8n-workflow';
|
||||
import type { INode, INodeProperties, ISupplyDataFunctions } from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import { LmChatAnthropic } from '../LmChatAnthropic.node';
|
||||
@@ -79,7 +79,7 @@ describe('LmChatAnthropic', () => {
|
||||
displayName: 'Anthropic Chat Model',
|
||||
name: 'lmChatAnthropic',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5],
|
||||
description: 'Language Model Anthropic',
|
||||
});
|
||||
});
|
||||
@@ -570,13 +570,12 @@ describe('LmChatAnthropic', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should have Claude Sonnet 4.6 as default for v1.4+ resource locator', () => {
|
||||
it('should have Claude Sonnet 4.6 as default for v1.4 resource locator', () => {
|
||||
const v14ModelField = lmChatAnthropic.description.properties.find(
|
||||
(p) =>
|
||||
p.name === 'model' &&
|
||||
p.type === 'resourceLocator' &&
|
||||
(p.displayOptions?.show?.['@version']?.[0] as { _cnd?: { gte?: number } })?._cnd?.gte ===
|
||||
1.4,
|
||||
p.displayOptions?.show?.['@version']?.[0] === 1.4,
|
||||
);
|
||||
|
||||
expect(v14ModelField).toBeDefined();
|
||||
@@ -588,6 +587,242 @@ describe('LmChatAnthropic', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('thinking modes (v1.5)', () => {
|
||||
it('should not set thinking-related invocationKwargs when thinkingMode is disabled', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-sonnet-4-6';
|
||||
if (paramName === 'options')
|
||||
return { thinkingMode: 'disabled', temperature: 0.5, topK: 10, topP: 0.8 };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'claude-sonnet-4-6',
|
||||
temperature: 0.5,
|
||||
topK: 10,
|
||||
topP: 0.8,
|
||||
invocationKwargs: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should configure adaptive thinking with default effort (medium)', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-sonnet-4-6';
|
||||
if (paramName === 'options') return { thinkingMode: 'adaptive' };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'claude-sonnet-4-6',
|
||||
invocationKwargs: {
|
||||
thinking: { type: 'adaptive' },
|
||||
output_config: { effort: 'medium' },
|
||||
max_tokens: 4096,
|
||||
top_k: undefined,
|
||||
top_p: undefined,
|
||||
temperature: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['low', 'medium', 'high', 'xhigh', 'max'] as const)(
|
||||
'should forward effort=%s for adaptive mode',
|
||||
async (effort) => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-opus-4-7-20251101';
|
||||
if (paramName === 'options') return { thinkingMode: 'adaptive', effort };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocationKwargs: expect.objectContaining({
|
||||
thinking: { type: 'adaptive' },
|
||||
output_config: { effort },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('should keep legacy enabled+budget payload for manual thinkingMode on Sonnet 4.6', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-sonnet-4-6';
|
||||
if (paramName === 'options')
|
||||
return { thinkingMode: 'manual', thinkingBudget: 2048, maxTokensToSample: 4096 };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
model: 'claude-sonnet-4-6',
|
||||
invocationKwargs: {
|
||||
thinking: { type: 'enabled', budget_tokens: 2048 },
|
||||
max_tokens: 4096,
|
||||
top_k: undefined,
|
||||
top_p: undefined,
|
||||
temperature: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should strip temperature/topK/topP from constructor when model is Opus 4.7 (disabled mode)', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-opus-4-7-20251101';
|
||||
if (paramName === 'options')
|
||||
return { thinkingMode: 'disabled', temperature: 0.5, topK: 40, topP: 0.9 };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
const callArgs = MockedChatAnthropic.mock.calls[0][0]!;
|
||||
expect(callArgs.model).toBe('claude-opus-4-7-20251101');
|
||||
expect(callArgs).not.toHaveProperty('temperature');
|
||||
expect(callArgs).not.toHaveProperty('topK');
|
||||
expect(callArgs).not.toHaveProperty('topP');
|
||||
});
|
||||
|
||||
it('should throw NodeOperationError when manual mode is selected on Opus 4.7', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.5 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-opus-4-7-20251101';
|
||||
if (paramName === 'options') return { thinkingMode: 'manual', thinkingBudget: 2048 };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await expect(lmChatAnthropic.supplyData.call(mockContext, 0)).rejects.toThrow(
|
||||
NodeOperationError,
|
||||
);
|
||||
expect(MockedChatAnthropic).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should still emit legacy thinking payload when thinking=true on v1.4', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.4 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-sonnet-4-6';
|
||||
if (paramName === 'options')
|
||||
return { thinking: true, thinkingBudget: 1500, maxTokensToSample: 4096 };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
invocationKwargs: {
|
||||
thinking: { type: 'enabled', budget_tokens: 1500 },
|
||||
max_tokens: 4096,
|
||||
top_k: undefined,
|
||||
top_p: undefined,
|
||||
temperature: undefined,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit empty invocationKwargs when thinking=false on v1.4', async () => {
|
||||
const mockContext = setupMockContext({ typeVersion: 1.4 });
|
||||
|
||||
mockContext.getNodeParameter = jest.fn().mockImplementation((paramName: string) => {
|
||||
if (paramName === 'model.value') return 'claude-sonnet-4-6';
|
||||
if (paramName === 'options') return { thinking: false };
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await lmChatAnthropic.supplyData.call(mockContext, 0);
|
||||
|
||||
expect(MockedChatAnthropic).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ invocationKwargs: {} }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should describe v1.5 model field, thinkingMode, and gated effort fields', () => {
|
||||
const properties = lmChatAnthropic.description.properties;
|
||||
|
||||
const v15ModelField = properties.find(
|
||||
(p) =>
|
||||
p.name === 'model' &&
|
||||
p.type === 'resourceLocator' &&
|
||||
(p.displayOptions?.show?.['@version']?.[0] as { _cnd?: { gte?: number } })?._cnd?.gte ===
|
||||
1.5,
|
||||
);
|
||||
expect(v15ModelField).toBeDefined();
|
||||
expect(v15ModelField!.default).toEqual({
|
||||
mode: 'list',
|
||||
value: 'claude-sonnet-4-6',
|
||||
cachedResultName: 'Claude Sonnet 4.6',
|
||||
});
|
||||
|
||||
const optionsField = properties.find((p) => p.name === 'options' && p.type === 'collection');
|
||||
expect(optionsField).toBeDefined();
|
||||
|
||||
const innerOptions = (optionsField as { options: INodeProperties[] }).options;
|
||||
|
||||
const thinkingMode = innerOptions.find((o) => o.name === 'thinkingMode');
|
||||
expect(thinkingMode).toBeDefined();
|
||||
expect(thinkingMode!.type).toBe('options');
|
||||
const modeValues = (thinkingMode as { options: Array<{ value: string }> }).options.map(
|
||||
(o) => o.value,
|
||||
);
|
||||
expect(modeValues).toEqual(['disabled', 'adaptive', 'manual']);
|
||||
|
||||
const effortFields = innerOptions.filter((o) => o.name === 'effort');
|
||||
expect(effortFields).toHaveLength(2);
|
||||
|
||||
const opusEffort = effortFields.find((f) => {
|
||||
const cnd = (
|
||||
f.displayOptions?.show?.['/model.value']?.[0] as {
|
||||
_cnd?: { includes?: string };
|
||||
}
|
||||
)?._cnd;
|
||||
return cnd?.includes === 'opus';
|
||||
});
|
||||
expect(opusEffort).toBeDefined();
|
||||
expect(
|
||||
(opusEffort as { options: Array<{ value: string }> }).options.map((o) => o.value),
|
||||
).toEqual(['low', 'medium', 'high', 'xhigh', 'max']);
|
||||
|
||||
const nonOpusEffort = effortFields.find((f) => {
|
||||
const cnd = (
|
||||
f.displayOptions?.show?.['/model.value']?.[0] as {
|
||||
_cnd?: { regex?: string };
|
||||
}
|
||||
)?._cnd;
|
||||
return typeof cnd?.regex === 'string';
|
||||
});
|
||||
expect(nonOpusEffort).toBeDefined();
|
||||
expect(
|
||||
(nonOpusEffort as { options: Array<{ value: string }> }).options.map((o) => o.value),
|
||||
).toEqual(['low', 'medium', 'high']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
it('should have searchModels method', () => {
|
||||
expect(lmChatAnthropic.methods).toEqual({
|
||||
|
||||
@@ -64,7 +64,7 @@ describe('LmChatAnthropic', () => {
|
||||
displayName: 'Anthropic Chat Model',
|
||||
name: 'lmChatAnthropic',
|
||||
group: ['transform'],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4],
|
||||
version: [1, 1.1, 1.2, 1.3, 1.4, 1.5],
|
||||
description: 'Language Model Anthropic',
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user