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
25 changes: 25 additions & 0 deletions packages/components/credentials/AstraflowApi.credential.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { INodeCredential, INodeParams } from '../src/Interface'

class AstraflowApi implements INodeCredential {
label: string
name: string
version: number
inputs: INodeParams[]

constructor() {
this.label = 'Astraflow API'
this.name = 'astraflowApi'
this.version = 1.0
this.inputs = [
{
label: 'Astraflow API Key',
name: 'astraflowApiKey',
type: 'password',
description:
'Astraflow (UCloud) API key. Use a Global key for https://api-us-ca.umodelverse.ai/v1 (ASTRAFLOW_API_KEY) or a China key for https://api.modelverse.cn/v1 (ASTRAFLOW_CN_API_KEY).'
}
]
}
}

module.exports = { credClass: AstraflowApi }
182 changes: 182 additions & 0 deletions packages/components/nodes/chatmodels/ChatAstraflow/ChatAstraflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import { BaseCache } from '@langchain/core/caches'
import { ChatOpenAI, ChatOpenAIFields } from '@langchain/openai'
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'

class ChatAstraflow_ChatModels implements INode {
label: string
name: string
version: number
type: string
icon: string
category: string
description: string
baseClasses: string[]
credential: INodeParams
inputs: INodeParams[]

constructor() {
this.label = 'ChatAstraflow'
this.name = 'chatAstraflow'
this.version = 1.0
this.type = 'ChatAstraflow'
this.icon = 'astraflow.svg'
this.category = 'Chat Models'
this.description = 'Wrapper around Astraflow (UCloud) large language models that use the Chat endpoint'
this.baseClasses = [this.type, ...getBaseClasses(ChatOpenAI)]
this.credential = {
label: 'Connect Credential',
name: 'credential',
type: 'credential',
credentialNames: ['astraflowApi']
}
this.inputs = [
{
label: 'Cache',
name: 'cache',
type: 'BaseCache',
optional: true
},
{
label: 'Base URL',
name: 'basePath',
type: 'string',
default: 'https://api-us-ca.umodelverse.ai/v1',
description:
'Astraflow API base URL. Use https://api-us-ca.umodelverse.ai/v1 for the Global endpoint or https://api.modelverse.cn/v1 for the China endpoint.'
},
{
label: 'Model Name',
name: 'modelName',
type: 'string',
placeholder: 'gpt-4o-mini',
description: 'Enter the model name supported by Astraflow (e.g., gpt-4o-mini, claude-3-5-sonnet, deepseek-chat)'
},
{
label: 'Temperature',
name: 'temperature',
type: 'number',
step: 0.1,
default: 0.7,
optional: true
},
{
label: 'Streaming',
name: 'streaming',
type: 'boolean',
default: true,
optional: true,
additionalParams: true
},
{
label: 'Max Tokens',
name: 'maxTokens',
type: 'number',
step: 1,
optional: true,
additionalParams: true
},
{
label: 'Top Probability',
name: 'topP',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
},
{
label: 'Frequency Penalty',
name: 'frequencyPenalty',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
},
{
label: 'Presence Penalty',
name: 'presencePenalty',
type: 'number',
step: 0.1,
optional: true,
additionalParams: true
},
{
label: 'Base Options',
name: 'baseOptions',
type: 'json',
optional: true,
additionalParams: true,
description: 'Additional options to pass to the Astraflow client. This should be a JSON object.'
}
]
}

async init(nodeData: INodeData, _: string, options: ICommonObject): Promise<any> {
const temperature = nodeData.inputs?.temperature as string
const modelName = nodeData.inputs?.modelName as string
const maxTokens = nodeData.inputs?.maxTokens as string
const topP = nodeData.inputs?.topP as string
const frequencyPenalty = nodeData.inputs?.frequencyPenalty as string
const presencePenalty = nodeData.inputs?.presencePenalty as string
const streaming = nodeData.inputs?.streaming as boolean
const baseOptions = nodeData.inputs?.baseOptions
const basePath = (nodeData.inputs?.basePath as string) || 'https://api-us-ca.umodelverse.ai/v1'

if (nodeData.inputs?.credentialId) {
nodeData.credential = nodeData.inputs?.credentialId
}
const credentialData = await getCredentialData(nodeData.credential ?? '', options)
const astraflowApiKey = getCredentialParam('astraflowApiKey', credentialData, nodeData)

if (!astraflowApiKey || astraflowApiKey.trim() === '') {
throw new Error(
'Astraflow API Key is missing or empty. Please provide a valid Astraflow API key in the credential configuration.'
)
}

if (!modelName || modelName.trim() === '') {
throw new Error('Model Name is required. Please enter a valid Astraflow model name (e.g., gpt-4o-mini).')
}

const cache = nodeData.inputs?.cache as BaseCache

const obj: ChatOpenAIFields = {
temperature: parseFloat(temperature),
modelName,
openAIApiKey: astraflowApiKey,
apiKey: astraflowApiKey,
streaming: streaming ?? true
}
Comment on lines +143 to +149
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

The temperature input is optional. If it is missing or empty, parseFloat(temperature) will result in NaN, which can cause issues during model initialization. It is safer to only assign it if a value is present.

Suggested change
const obj: ChatOpenAIFields = {
temperature: parseFloat(temperature),
modelName,
openAIApiKey: astraflowApiKey,
apiKey: astraflowApiKey,
streaming: streaming ?? true
}
const obj: ChatOpenAIFields = {
modelName,
openAIApiKey: astraflowApiKey,
apiKey: astraflowApiKey,
streaming: streaming ?? true
}
if (temperature) obj.temperature = parseFloat(temperature)


if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
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

For consistency with the main ChatOpenAI node and to align with newer versions of @langchain/openai, use maxCompletionTokens instead of maxTokens.

Suggested change
if (maxTokens) obj.maxTokens = parseInt(maxTokens, 10)
if (maxTokens) obj.maxCompletionTokens = parseInt(maxTokens, 10)

if (topP) obj.topP = parseFloat(topP)
if (frequencyPenalty) obj.frequencyPenalty = parseFloat(frequencyPenalty)
if (presencePenalty) obj.presencePenalty = parseFloat(presencePenalty)
if (cache) obj.cache = cache

let parsedBaseOptions: any | undefined = undefined

if (baseOptions) {
try {
parsedBaseOptions = typeof baseOptions === 'object' ? baseOptions : JSON.parse(baseOptions)
if (parsedBaseOptions.baseURL) {
console.warn("The 'baseURL' parameter is not allowed in baseOptions when using the ChatAstraflow node. Use the Base URL field instead.")
parsedBaseOptions.baseURL = undefined
}
} catch (exception) {
throw new Error('Invalid JSON in the BaseOptions: ' + exception)
}
}

const model = new ChatOpenAI({
...obj,
configuration: {
baseURL: basePath,
...parsedBaseOptions
}
Comment on lines +173 to +176
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.

high

The baseOptions should be passed as defaultHeaders within the configuration object to ensure custom headers are correctly handled by the underlying OpenAI client, maintaining consistency with the ChatOpenAI node implementation.

Suggested change
configuration: {
baseURL: basePath,
...parsedBaseOptions
}
configuration: {
baseURL: basePath,
defaultHeaders: parsedBaseOptions
}

})
return model
}
}

module.exports = { nodeClass: ChatAstraflow_ChatModels }
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.