Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
jest.mock('@langchain/openai', () => ({
ChatOpenAI: jest.fn().mockImplementation((fields) => ({ fields }))
}))

jest.mock('../../../src/utils', () => ({
getBaseClasses: jest.fn().mockReturnValue(['BaseChatModel']),
getCredentialData: jest.fn(),
getCredentialParam: jest.fn()
}))

import { getCredentialData, getCredentialParam } from '../../../src/utils'

const { nodeClass: ChatOpenAICustom } = require('./ChatOpenAICustom')

describe('ChatOpenAICustom', () => {
beforeEach(() => {
jest.clearAllMocks()
;(getCredentialData as jest.Mock).mockResolvedValue({ openAIApiKey: 'test-api-key' })
;(getCredentialParam as jest.Mock).mockImplementation((key, credentialData) => credentialData[key])
})

it('exposes stopSequence as an additional parameter', () => {
const node = new ChatOpenAICustom()

expect(node.inputs).toEqual(
expect.arrayContaining([
expect.objectContaining({
label: 'Stop Sequence',
name: 'stopSequence',
additionalParams: true
})
])
)
})

it('passes comma-separated stop sequences to ChatOpenAI', async () => {
const node = new ChatOpenAICustom()
const model = await node.init(
{
credential: 'cred-1',
inputs: {
modelName: 'custom-model',
temperature: '0.3',
stopSequence: '<|im_end|>, END',
streaming: false
}
},
'',
{}
)

expect(model.fields).toMatchObject({
modelName: 'custom-model',
openAIApiKey: 'test-api-key',
apiKey: 'test-api-key',
temperature: 0.3,
streaming: false,
stop: ['<|im_end|>', 'END']
})
})

it('ignores empty stop sequence entries', async () => {
const node = new ChatOpenAICustom()
const model = await node.init(
{
credential: 'cred-1',
inputs: {
modelName: 'custom-model',
temperature: '0.3',
stopSequence: 'foo,, bar, ',
streaming: false
}
},
'',
{}
)

expect(model.fields.stop).toEqual(['foo', 'bar'])
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class ChatOpenAICustom_ChatModels implements INode {
constructor() {
this.label = 'OpenAI Custom Model'
this.name = 'chatOpenAICustom'
this.version = 4.0
this.version = 4.1
this.type = 'ChatOpenAI-Custom'
this.icon = 'openai.svg'
this.category = 'Chat Models'
Expand Down Expand Up @@ -92,6 +92,15 @@ class ChatOpenAICustom_ChatModels implements INode {
optional: true,
additionalParams: true
},
{
label: 'Stop Sequence',
name: 'stopSequence',
type: 'string',
rows: 4,
optional: true,
description: 'List of stop words to use when generating. Use comma to separate multiple stop words.',
additionalParams: true
},
{
label: 'Timeout',
name: 'timeout',
Expand Down Expand Up @@ -126,6 +135,7 @@ class ChatOpenAICustom_ChatModels implements INode {
const topP = nodeData.inputs?.topP as string
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
const presencePenalty = nodeData.inputs?.presencePenalty as string
const stopSequence = nodeData.inputs?.stopSequence as string
const timeout = nodeData.inputs?.timeout as string
const streaming = nodeData.inputs?.streaming as boolean
const basePath = nodeData.inputs?.basepath as string
Expand All @@ -147,6 +157,13 @@ class ChatOpenAICustom_ChatModels implements INode {
if (topP) obj.topP = parseFloat(topP)
if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty)
if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty)
if (stopSequence) {
const stopSequenceArray = stopSequence
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
if (stopSequenceArray.length > 0) obj.stop = stopSequenceArray
}
Comment on lines +160 to +166
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the stopSequence contains empty segments (e.g., trailing commas or multiple consecutive commas like foo,,bar), splitting and mapping will result in empty strings in the stop array. The OpenAI API will reject requests containing empty strings in the stop parameter with a 400 Bad Request error. Filtering out empty strings using simple, chained operations ensures robustness and maintains readability.

Suggested change
if (stopSequence) {
const stopSequenceArray = stopSequence.split(',').map((item) => item.trim())
obj.stop = stopSequenceArray
}
if (stopSequence) {
const stopSequenceArray = stopSequence
.split(',')
.map((item) => item.trim())
.filter((item) => item !== '')
if (stopSequenceArray.length > 0) {
obj.stop = stopSequenceArray
}
}
References
  1. Prioritize code readability and understandability over conciseness. A series of simple, chained operations can be preferable to a single, more complex one if it improves understandability and reduces the potential for future errors.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. I’ve addressed this in 0c594c8 by filtering empty stop sequence entries and added a regression test for consecutive/trailing commas.

if (timeout) obj.timeout = parseInt(timeout, 10)
if (cache) obj.cache = cache

Expand Down