@alpic80/rivet-core 1.19.1-aidon.3 → 1.24.0-aidon.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/README.md +4 -0
  2. package/dist/cjs/bundle.cjs +4512 -1240
  3. package/dist/cjs/bundle.cjs.map +4 -4
  4. package/dist/esm/api/createProcessor.js +8 -17
  5. package/dist/esm/api/looseDataValue.js +16 -0
  6. package/dist/esm/exports.js +2 -0
  7. package/dist/esm/integrations/CodeRunner.js +36 -0
  8. package/dist/esm/integrations/DatasetProvider.js +1 -1
  9. package/dist/esm/integrations/GptTokenizerTokenizer.js +7 -4
  10. package/dist/esm/integrations/openai/OpenAIEmbeddingGenerator.js +1 -1
  11. package/dist/esm/model/DataValue.js +14 -2
  12. package/dist/esm/model/GraphProcessor.js +276 -107
  13. package/dist/esm/model/NodeBase.js +11 -1
  14. package/dist/esm/model/NodeImpl.js +8 -0
  15. package/dist/esm/model/Nodes.js +31 -4
  16. package/dist/esm/model/ProjectReferenceLoader.js +1 -0
  17. package/dist/esm/model/nodes/AssembleMessageNode.js +12 -2
  18. package/dist/esm/model/nodes/AssemblePromptNode.js +22 -0
  19. package/dist/esm/model/nodes/CallGraphNode.js +3 -4
  20. package/dist/esm/model/nodes/ChatLoopNode.js +150 -0
  21. package/dist/esm/model/nodes/ChatNode.js +7 -934
  22. package/dist/esm/model/nodes/ChatNodeBase.js +1277 -0
  23. package/dist/esm/model/nodes/ChunkNode.js +2 -2
  24. package/dist/esm/model/nodes/CodeNode.js +40 -5
  25. package/dist/esm/model/nodes/CronNode.js +248 -0
  26. package/dist/esm/model/nodes/DelegateFunctionCallNode.js +37 -12
  27. package/dist/esm/model/nodes/DestructureNode.js +1 -1
  28. package/dist/esm/model/nodes/DocumentNode.js +183 -0
  29. package/dist/esm/model/nodes/ExtractJsonNode.js +4 -4
  30. package/dist/esm/model/nodes/ExtractRegexNode.js +10 -11
  31. package/dist/esm/model/nodes/GetAllDatasetsNode.js +1 -1
  32. package/dist/esm/model/nodes/GetEmbeddingNode.js +1 -1
  33. package/dist/esm/model/nodes/HttpCallNode.js +3 -1
  34. package/dist/esm/model/nodes/IfNode.js +5 -0
  35. package/dist/esm/model/nodes/LoopControllerNode.js +1 -1
  36. package/dist/esm/model/nodes/LoopUntilNode.js +214 -0
  37. package/dist/esm/model/nodes/ObjectNode.js +1 -1
  38. package/dist/esm/model/nodes/PromptNode.js +29 -6
  39. package/dist/esm/model/nodes/RaceInputsNode.js +1 -2
  40. package/dist/esm/model/nodes/ReadAllFilesNode.js +210 -0
  41. package/dist/esm/model/nodes/ReadDirectoryNode.js +31 -25
  42. package/dist/esm/model/nodes/ReferencedGraphAliasNode.js +199 -0
  43. package/dist/esm/model/nodes/ReplaceDatasetNode.js +1 -1
  44. package/dist/esm/model/nodes/SliceNode.js +0 -1
  45. package/dist/esm/model/nodes/SplitNode.js +1 -1
  46. package/dist/esm/model/nodes/SubGraphNode.js +0 -1
  47. package/dist/esm/model/nodes/TextNode.js +9 -4
  48. package/dist/esm/model/nodes/ToMarkdownTableNode.js +119 -0
  49. package/dist/esm/model/nodes/ToTreeNode.js +133 -0
  50. package/dist/esm/model/nodes/{GptFunctionNode.js → ToolNode.js} +10 -10
  51. package/dist/esm/model/nodes/UserInputNode.js +10 -12
  52. package/dist/esm/model/nodes/WriteFileNode.js +147 -0
  53. package/dist/esm/native/BrowserNativeApi.js +16 -1
  54. package/dist/esm/plugins/aidon/nodes/ChatAidonNode.js +5 -5
  55. package/dist/esm/plugins/anthropic/anthropic.js +29 -14
  56. package/dist/esm/plugins/anthropic/fetchEventSource.js +3 -2
  57. package/dist/esm/plugins/anthropic/nodes/ChatAnthropicNode.js +264 -147
  58. package/dist/esm/plugins/anthropic/plugin.js +9 -1
  59. package/dist/esm/plugins/assemblyAi/LemurQaNode.js +1 -1
  60. package/dist/esm/plugins/assemblyAi/LemurSummaryNode.js +1 -1
  61. package/dist/esm/plugins/gentrace/plugin.js +6 -6
  62. package/dist/esm/plugins/google/google.js +120 -6
  63. package/dist/esm/plugins/google/nodes/ChatGoogleNode.js +219 -56
  64. package/dist/esm/plugins/google/plugin.js +13 -6
  65. package/dist/esm/plugins/openai/nodes/RunThreadNode.js +2 -2
  66. package/dist/esm/plugins/openai/nodes/ThreadMessageNode.js +1 -1
  67. package/dist/esm/recording/ExecutionRecorder.js +59 -4
  68. package/dist/esm/utils/base64.js +13 -0
  69. package/dist/esm/utils/chatMessageToOpenAIChatCompletionMessage.js +15 -2
  70. package/dist/esm/utils/coerceType.js +4 -1
  71. package/dist/esm/utils/fetchEventSource.js +1 -1
  72. package/dist/esm/utils/interpolation.js +108 -3
  73. package/dist/esm/utils/openai.js +106 -50
  74. package/dist/esm/utils/paths.js +80 -0
  75. package/dist/esm/utils/serialization/serialization_v4.js +5 -0
  76. package/dist/types/api/createProcessor.d.ts +11 -5
  77. package/dist/types/api/looseDataValue.d.ts +4 -0
  78. package/dist/types/api/streaming.d.ts +1 -1
  79. package/dist/types/exports.d.ts +2 -0
  80. package/dist/types/integrations/CodeRunner.d.ts +18 -0
  81. package/dist/types/integrations/DatasetProvider.d.ts +1 -1
  82. package/dist/types/model/DataValue.d.ts +29 -6
  83. package/dist/types/model/EditorDefinition.d.ts +6 -1
  84. package/dist/types/model/GraphProcessor.d.ts +14 -7
  85. package/dist/types/model/NodeBase.d.ts +4 -0
  86. package/dist/types/model/NodeImpl.d.ts +5 -4
  87. package/dist/types/model/Nodes.d.ts +13 -4
  88. package/dist/types/model/ProcessContext.d.ts +16 -1
  89. package/dist/types/model/Project.d.ts +19 -7
  90. package/dist/types/model/ProjectReferenceLoader.d.ts +5 -0
  91. package/dist/types/model/RivetPlugin.d.ts +6 -0
  92. package/dist/types/model/RivetUIContext.d.ts +5 -1
  93. package/dist/types/model/Settings.d.ts +1 -0
  94. package/dist/types/model/nodes/AssemblePromptNode.d.ts +4 -1
  95. package/dist/types/model/nodes/ChatLoopNode.d.ts +21 -0
  96. package/dist/types/model/nodes/ChatNode.d.ts +2 -62
  97. package/dist/types/model/nodes/ChatNodeBase.d.ts +85 -0
  98. package/dist/types/model/nodes/CodeNode.d.ts +8 -2
  99. package/dist/types/model/nodes/CronNode.d.ts +34 -0
  100. package/dist/types/model/nodes/DelegateFunctionCallNode.d.ts +1 -0
  101. package/dist/types/model/nodes/DocumentNode.d.ts +28 -0
  102. package/dist/types/model/nodes/GetAllDatasetsNode.d.ts +2 -2
  103. package/dist/types/model/nodes/LoopUntilNode.d.ts +32 -0
  104. package/dist/types/model/nodes/ObjectNode.d.ts +2 -2
  105. package/dist/types/model/nodes/PromptNode.d.ts +2 -0
  106. package/dist/types/model/nodes/RaceInputsNode.d.ts +1 -2
  107. package/dist/types/model/nodes/ReadAllFilesNode.d.ts +30 -0
  108. package/dist/types/model/nodes/ReadDirectoryNode.d.ts +1 -1
  109. package/dist/types/model/nodes/ReferencedGraphAliasNode.d.ts +31 -0
  110. package/dist/types/model/nodes/SplitNode.d.ts +2 -2
  111. package/dist/types/model/nodes/ToMarkdownTableNode.d.ts +19 -0
  112. package/dist/types/model/nodes/ToTreeNode.d.ts +21 -0
  113. package/dist/types/model/nodes/UserInputNode.d.ts +2 -3
  114. package/dist/types/model/nodes/WriteFileNode.d.ts +23 -0
  115. package/dist/types/native/BrowserNativeApi.d.ts +8 -5
  116. package/dist/types/native/NativeApi.d.ts +12 -1
  117. package/dist/types/plugins/anthropic/anthropic.d.ts +94 -13
  118. package/dist/types/plugins/anthropic/nodes/ChatAnthropicNode.d.ts +7 -2
  119. package/dist/types/plugins/google/google.d.ts +101 -18
  120. package/dist/types/plugins/google/nodes/ChatGoogleNode.d.ts +3 -2
  121. package/dist/types/recording/RecordedEvents.d.ts +3 -0
  122. package/dist/types/utils/base64.d.ts +2 -1
  123. package/dist/types/utils/chatMessageToOpenAIChatCompletionMessage.d.ts +3 -1
  124. package/dist/types/utils/interpolation.d.ts +3 -0
  125. package/dist/types/utils/openai.d.ts +127 -21
  126. package/dist/types/utils/paths.d.ts +8 -0
  127. package/dist/types/utils/serialization/serialization_v3.d.ts +1 -0
  128. package/package.json +15 -11
  129. /package/dist/types/model/nodes/{GptFunctionNode.d.ts → ToolNode.d.ts} +0 -0
@@ -0,0 +1,1277 @@
1
+ import { match } from 'ts-pattern';
2
+ import { coerceType, coerceTypeOptional } from '../../utils/coerceType.js';
3
+ import { getError } from '../../utils/errors.js';
4
+ import { dedent } from '../../utils/misc.js';
5
+ import { OpenAIError, openAiModelOptions, openaiModels, chatCompletions, streamChatCompletions, defaultOpenaiSupported, } from '../../utils/openai.js';
6
+ import { isArrayDataValue, getScalarTypeOf } from '../DataValue.js';
7
+ import { cleanHeaders, getInputOrData } from '../../utils/inputs.js';
8
+ import { chatMessageToOpenAIChatCompletionMessage } from '../../utils/chatMessageToOpenAIChatCompletionMessage.js';
9
+ import { DEFAULT_CHAT_ENDPOINT } from '../../utils/defaults.js';
10
+ import { addWarning } from '../../utils/outputs.js';
11
+ import retry from 'p-retry';
12
+ import { base64ToUint8Array } from '../../utils/base64.js';
13
+ // Temporary
14
+ const cache = new Map();
15
+ export const ChatNodeBase = {
16
+ defaultData: () => ({
17
+ model: 'gpt-4o-mini',
18
+ useModelInput: false,
19
+ temperature: 0.5,
20
+ useTemperatureInput: false,
21
+ top_p: 1,
22
+ useTopPInput: false,
23
+ useTopP: false,
24
+ useUseTopPInput: false,
25
+ maxTokens: 1024,
26
+ useMaxTokensInput: false,
27
+ useStop: false,
28
+ stop: '',
29
+ useStopInput: false,
30
+ presencePenalty: undefined,
31
+ usePresencePenaltyInput: false,
32
+ frequencyPenalty: undefined,
33
+ useFrequencyPenaltyInput: false,
34
+ user: undefined,
35
+ useUserInput: false,
36
+ enableFunctionUse: false,
37
+ cache: false,
38
+ useAsGraphPartialOutput: true,
39
+ parallelFunctionCalling: true,
40
+ additionalParameters: [],
41
+ useAdditionalParametersInput: false,
42
+ useServerTokenCalculation: true,
43
+ outputUsage: false,
44
+ usePredictedOutput: false,
45
+ modalitiesIncludeAudio: false,
46
+ modalitiesIncludeText: false,
47
+ reasoningEffort: '',
48
+ useReasoningEffortInput: false,
49
+ }),
50
+ getInputDefinitions: (data) => {
51
+ const inputs = [];
52
+ if (data.useEndpointInput) {
53
+ inputs.push({
54
+ dataType: 'string',
55
+ id: 'endpoint',
56
+ title: 'Endpoint',
57
+ description: 'The endpoint to use for the OpenAI API. You can use this to replace with any OpenAI-compatible API. Leave blank for the default: https://api.openai.com/api/v1/chat/completions',
58
+ });
59
+ }
60
+ inputs.push({
61
+ id: 'systemPrompt',
62
+ title: 'System Prompt',
63
+ dataType: 'string',
64
+ required: false,
65
+ description: 'The system prompt to send to the model.',
66
+ coerced: true,
67
+ });
68
+ if (data.useModelInput) {
69
+ inputs.push({
70
+ id: 'model',
71
+ title: 'Model',
72
+ dataType: 'string',
73
+ required: false,
74
+ description: 'The model to use for the chat.',
75
+ });
76
+ }
77
+ if (data.useTemperatureInput) {
78
+ inputs.push({
79
+ dataType: 'number',
80
+ id: 'temperature',
81
+ title: 'Temperature',
82
+ description: 'What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.',
83
+ });
84
+ }
85
+ if (data.useTopPInput) {
86
+ inputs.push({
87
+ dataType: 'number',
88
+ id: 'top_p',
89
+ title: 'Top P',
90
+ description: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.',
91
+ });
92
+ }
93
+ if (data.useUseTopPInput) {
94
+ inputs.push({
95
+ dataType: 'boolean',
96
+ id: 'useTopP',
97
+ title: 'Use Top P',
98
+ description: 'Whether to use top p sampling, or temperature sampling.',
99
+ });
100
+ }
101
+ if (data.useMaxTokensInput) {
102
+ inputs.push({
103
+ dataType: 'number',
104
+ id: 'maxTokens',
105
+ title: 'Max Tokens',
106
+ description: 'The maximum number of tokens to generate in the chat completion.',
107
+ });
108
+ }
109
+ if (data.useStopInput) {
110
+ inputs.push({
111
+ dataType: 'string',
112
+ id: 'stop',
113
+ title: 'Stop',
114
+ description: 'A sequence where the API will stop generating further tokens.',
115
+ });
116
+ }
117
+ if (data.usePresencePenaltyInput) {
118
+ inputs.push({
119
+ dataType: 'number',
120
+ id: 'presencePenalty',
121
+ title: 'Presence Penalty',
122
+ description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`,
123
+ });
124
+ }
125
+ if (data.useFrequencyPenaltyInput) {
126
+ inputs.push({
127
+ dataType: 'number',
128
+ id: 'frequencyPenalty',
129
+ title: 'Frequency Penalty',
130
+ description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`,
131
+ });
132
+ }
133
+ if (data.useUserInput) {
134
+ inputs.push({
135
+ dataType: 'string',
136
+ id: 'user',
137
+ title: 'User',
138
+ description: 'A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.',
139
+ });
140
+ }
141
+ if (data.useNumberOfChoicesInput) {
142
+ inputs.push({
143
+ dataType: 'number',
144
+ id: 'numberOfChoices',
145
+ title: 'Number of Choices',
146
+ description: 'If greater than 1, the model will return multiple choices and the response will be an array.',
147
+ });
148
+ }
149
+ if (data.useHeadersInput) {
150
+ inputs.push({
151
+ dataType: 'object',
152
+ id: 'headers',
153
+ title: 'Headers',
154
+ description: 'Additional headers to send to the API.',
155
+ });
156
+ }
157
+ inputs.push({
158
+ dataType: ['chat-message', 'chat-message[]'],
159
+ id: 'prompt',
160
+ title: 'Prompt',
161
+ description: 'The prompt message or messages to send to the model.',
162
+ coerced: true,
163
+ });
164
+ if (data.enableFunctionUse) {
165
+ inputs.push({
166
+ dataType: ['gpt-function', 'gpt-function[]'],
167
+ id: 'functions',
168
+ title: 'Functions',
169
+ description: 'Functions to use in the model. To connect multiple functions, use an Array node.',
170
+ coerced: false,
171
+ });
172
+ }
173
+ if (data.useSeedInput) {
174
+ inputs.push({
175
+ dataType: 'number',
176
+ id: 'seed',
177
+ title: 'Seed',
178
+ coerced: true,
179
+ description: 'If specified, OpenAI will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result.',
180
+ });
181
+ }
182
+ if (data.useToolChoiceInput) {
183
+ inputs.push({
184
+ dataType: 'string',
185
+ id: 'toolChoice',
186
+ title: 'Tool Choice',
187
+ coerced: true,
188
+ description: 'Controls which (if any) function is called by the model. `none` is the default when no functions are present. `auto` is the default if functions are present. `function` forces the model to call a function.',
189
+ });
190
+ }
191
+ if (data.useToolChoiceInput || data.useToolChoiceFunctionInput) {
192
+ inputs.push({
193
+ dataType: 'string',
194
+ id: 'toolChoiceFunction',
195
+ title: 'Tool Choice Function',
196
+ coerced: true,
197
+ description: 'The name of the function to force the model to call.',
198
+ });
199
+ }
200
+ if (data.useResponseFormatInput) {
201
+ inputs.push({
202
+ dataType: 'string',
203
+ id: 'responseFormat',
204
+ title: 'Response Format',
205
+ coerced: true,
206
+ description: 'The format to force the model to reply in.',
207
+ });
208
+ }
209
+ if (data.useAdditionalParametersInput) {
210
+ inputs.push({
211
+ dataType: 'object',
212
+ id: 'additionalParameters',
213
+ title: 'Additional Parameters',
214
+ description: 'Additional chat completion parameters to send to the API.',
215
+ });
216
+ }
217
+ if (data.responseFormat === 'json_schema') {
218
+ inputs.push({
219
+ dataType: 'object',
220
+ id: 'responseSchema',
221
+ title: 'Response Schema',
222
+ description: 'The JSON schema that the response will adhere to (Structured Outputs).',
223
+ required: true,
224
+ });
225
+ if (data.useResponseSchemaNameInput) {
226
+ inputs.push({
227
+ dataType: 'string',
228
+ id: 'responseSchemaName',
229
+ title: 'Response Schema Name',
230
+ description: 'The name of the JSON schema that the response will adhere to (Structured Outputs).',
231
+ required: false,
232
+ });
233
+ }
234
+ }
235
+ if (data.usePredictedOutput) {
236
+ inputs.push({
237
+ dataType: 'string[]',
238
+ id: 'predictedOutput',
239
+ title: 'Predicted Output',
240
+ description: 'The predicted output from the model.',
241
+ coerced: true,
242
+ });
243
+ }
244
+ if (data.useAudioVoiceInput) {
245
+ inputs.push({
246
+ dataType: 'string',
247
+ id: 'audioVoice',
248
+ title: 'Audio Voice',
249
+ description: 'The voice to use for audio responses. See your model for supported voices.',
250
+ });
251
+ }
252
+ if (data.useAudioFormatInput) {
253
+ inputs.push({
254
+ dataType: 'string',
255
+ id: 'audioFormat',
256
+ title: 'Audio Format',
257
+ description: 'The format to use for audio responses.',
258
+ });
259
+ }
260
+ return inputs;
261
+ },
262
+ getOutputDefinitions: (data) => {
263
+ const outputs = [];
264
+ if (data.useNumberOfChoicesInput || (data.numberOfChoices ?? 1) > 1) {
265
+ outputs.push({
266
+ dataType: 'string[]',
267
+ id: 'response',
268
+ title: 'Responses',
269
+ description: 'All responses from the model.',
270
+ });
271
+ }
272
+ else {
273
+ outputs.push({
274
+ dataType: 'string',
275
+ id: 'response',
276
+ title: 'Response',
277
+ description: 'The textual response from the model.',
278
+ });
279
+ }
280
+ if (data.enableFunctionUse) {
281
+ if (data.parallelFunctionCalling) {
282
+ outputs.push({
283
+ dataType: 'object[]',
284
+ id: 'function-calls',
285
+ title: 'Function Calls',
286
+ description: 'The function calls that were made, if any.',
287
+ });
288
+ }
289
+ else {
290
+ outputs.push({
291
+ dataType: 'object',
292
+ id: 'function-call',
293
+ title: 'Function Call',
294
+ description: 'The function call that was made, if any.',
295
+ });
296
+ }
297
+ }
298
+ outputs.push({
299
+ dataType: 'chat-message[]',
300
+ id: 'in-messages',
301
+ title: 'Messages Sent',
302
+ description: 'All messages sent to the model.',
303
+ });
304
+ if (!(data.useNumberOfChoicesInput || (data.numberOfChoices ?? 1) > 1)) {
305
+ outputs.push({
306
+ dataType: 'chat-message[]',
307
+ id: 'all-messages',
308
+ title: 'All Messages',
309
+ description: 'All messages, with the response appended.',
310
+ });
311
+ }
312
+ outputs.push({
313
+ dataType: 'number',
314
+ id: 'responseTokens',
315
+ title: 'Response Tokens',
316
+ description: 'The number of tokens in the response from the LLM. For a multi-response, this is the sum.',
317
+ });
318
+ if (data.outputUsage) {
319
+ outputs.push({
320
+ dataType: 'object',
321
+ id: 'usage',
322
+ title: 'Usage',
323
+ description: 'Usage statistics for the model.',
324
+ });
325
+ }
326
+ if (data.modalitiesIncludeAudio) {
327
+ outputs.push({
328
+ dataType: 'audio',
329
+ id: 'audio',
330
+ title: 'Audio',
331
+ description: 'The audio response from the model.',
332
+ });
333
+ outputs.push({
334
+ dataType: 'string',
335
+ id: 'audioTranscript',
336
+ title: 'Transcript',
337
+ description: 'The transcript of the audio response.',
338
+ });
339
+ }
340
+ return outputs;
341
+ },
342
+ getEditors: () => {
343
+ return [
344
+ {
345
+ type: 'dropdown',
346
+ label: 'GPT Model',
347
+ dataKey: 'model',
348
+ useInputToggleDataKey: 'useModelInput',
349
+ options: openAiModelOptions,
350
+ disableIf: (data) => {
351
+ return !!data.overrideModel?.trim();
352
+ },
353
+ helperMessage: (data) => {
354
+ if (data.overrideModel?.trim()) {
355
+ return `Model overridden to: ${data.overrideModel}`;
356
+ }
357
+ if (data.model === 'local-model') {
358
+ return 'Local model is an indicator for your own convenience, it does not affect the local LLM used.';
359
+ }
360
+ },
361
+ },
362
+ {
363
+ type: 'group',
364
+ label: 'Parameters',
365
+ editors: [
366
+ {
367
+ type: 'number',
368
+ label: 'Temperature',
369
+ dataKey: 'temperature',
370
+ useInputToggleDataKey: 'useTemperatureInput',
371
+ min: 0,
372
+ max: 2,
373
+ step: 0.1,
374
+ helperMessage: 'What sampling temperature to use, between 0 and 2. Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic.',
375
+ },
376
+ {
377
+ type: 'number',
378
+ label: 'Top P',
379
+ dataKey: 'top_p',
380
+ useInputToggleDataKey: 'useTopPInput',
381
+ min: 0,
382
+ max: 1,
383
+ step: 0.1,
384
+ helperMessage: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.',
385
+ },
386
+ {
387
+ type: 'toggle',
388
+ label: 'Use Top P',
389
+ dataKey: 'useTopP',
390
+ useInputToggleDataKey: 'useUseTopPInput',
391
+ helperMessage: 'Whether to use top p sampling, or temperature sampling.',
392
+ },
393
+ {
394
+ type: 'number',
395
+ label: 'Max Tokens',
396
+ dataKey: 'maxTokens',
397
+ useInputToggleDataKey: 'useMaxTokensInput',
398
+ min: 0,
399
+ max: Number.MAX_SAFE_INTEGER,
400
+ step: 1,
401
+ helperMessage: 'The maximum number of tokens to generate in the chat completion.',
402
+ },
403
+ {
404
+ type: 'string',
405
+ label: 'Stop',
406
+ dataKey: 'stop',
407
+ useInputToggleDataKey: 'useStopInput',
408
+ helperMessage: 'A sequence where the API will stop generating further tokens.',
409
+ },
410
+ {
411
+ type: 'number',
412
+ label: 'Presence Penalty',
413
+ dataKey: 'presencePenalty',
414
+ useInputToggleDataKey: 'usePresencePenaltyInput',
415
+ min: 0,
416
+ max: 2,
417
+ step: 0.1,
418
+ allowEmpty: true,
419
+ helperMessage: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`,
420
+ },
421
+ {
422
+ type: 'number',
423
+ label: 'Frequency Penalty',
424
+ dataKey: 'frequencyPenalty',
425
+ useInputToggleDataKey: 'useFrequencyPenaltyInput',
426
+ min: 0,
427
+ max: 2,
428
+ step: 0.1,
429
+ allowEmpty: true,
430
+ helperMessage: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`,
431
+ },
432
+ {
433
+ type: 'dropdown',
434
+ label: 'Reasoning Effort',
435
+ dataKey: 'reasoningEffort',
436
+ useInputToggleDataKey: 'useReasoningEffortInput',
437
+ options: [
438
+ { value: '', label: 'Unset' },
439
+ { value: 'low', label: 'Low' },
440
+ { value: 'medium', label: 'Medium' },
441
+ { value: 'high', label: 'High' },
442
+ ],
443
+ defaultValue: '',
444
+ helperMessage: 'Adjust the level of reasoning depth the model should apply. Only applies to reasoning models such as o3-mini.',
445
+ },
446
+ {
447
+ type: 'dropdown',
448
+ label: 'Response Format',
449
+ dataKey: 'responseFormat',
450
+ useInputToggleDataKey: 'useResponseFormatInput',
451
+ options: [
452
+ { value: '', label: 'Default' },
453
+ { value: 'text', label: 'Text' },
454
+ { value: 'json', label: 'JSON Object' },
455
+ { value: 'json_schema', label: 'JSON Schema' },
456
+ ],
457
+ defaultValue: '',
458
+ helperMessage: 'The format to force the model to reply in.',
459
+ },
460
+ {
461
+ type: 'string',
462
+ label: 'Response Schema Name',
463
+ dataKey: 'responseSchemaName',
464
+ useInputToggleDataKey: 'useResponseSchemaNameInput',
465
+ helperMessage: 'The name of the JSON schema that the response will adhere to (Structured Outputs). Defaults to response_schema',
466
+ hideIf: (data) => data.responseFormat !== 'json_schema',
467
+ },
468
+ {
469
+ type: 'number',
470
+ label: 'Seed',
471
+ dataKey: 'seed',
472
+ useInputToggleDataKey: 'useSeedInput',
473
+ step: 1,
474
+ allowEmpty: true,
475
+ helperMessage: 'If specified, OpenAI will make a best effort to sample deterministically, such that repeated requests with the same `seed` and parameters should return the same result.',
476
+ },
477
+ ],
478
+ },
479
+ {
480
+ type: 'group',
481
+ label: 'GPT Tools',
482
+ editors: [
483
+ {
484
+ type: 'toggle',
485
+ label: 'Enable Function Use',
486
+ dataKey: 'enableFunctionUse',
487
+ },
488
+ {
489
+ type: 'toggle',
490
+ label: 'Enable Parallel Function Calling',
491
+ dataKey: 'parallelFunctionCalling',
492
+ hideIf: (data) => !data.enableFunctionUse,
493
+ },
494
+ {
495
+ type: 'dropdown',
496
+ label: 'Tool Choice',
497
+ dataKey: 'toolChoice',
498
+ useInputToggleDataKey: 'useToolChoiceInput',
499
+ options: [
500
+ { value: '', label: 'Default' },
501
+ { value: 'none', label: 'None' },
502
+ { value: 'auto', label: 'Auto' },
503
+ { value: 'function', label: 'Function' },
504
+ { value: 'required', label: 'Required' },
505
+ ],
506
+ defaultValue: '',
507
+ helperMessage: 'Controls which (if any) function is called by the model. None is the default when no functions are present. Auto is the default if functions are present.',
508
+ hideIf: (data) => !data.enableFunctionUse,
509
+ },
510
+ {
511
+ type: 'string',
512
+ label: 'Tool Choice Function',
513
+ dataKey: 'toolChoiceFunction',
514
+ useInputToggleDataKey: 'useToolChoiceFunctionInput',
515
+ helperMessage: 'The name of the function to force the model to call.',
516
+ hideIf: (data) => data.toolChoice !== 'function' || !data.enableFunctionUse,
517
+ },
518
+ ],
519
+ },
520
+ {
521
+ type: 'group',
522
+ label: 'Features',
523
+ editors: [
524
+ {
525
+ type: 'toggle',
526
+ label: 'Enable Predicted Output',
527
+ dataKey: 'usePredictedOutput',
528
+ helperMessage: 'If on, enables an input port for the predicted output from the model, when many of the output tokens are known ahead of time.',
529
+ },
530
+ {
531
+ type: 'toggle',
532
+ label: 'Modalities: Text',
533
+ dataKey: 'modalitiesIncludeText',
534
+ helperMessage: 'If on, the model will include text in its responses. Only relevant for multimodal models.',
535
+ },
536
+ {
537
+ type: 'toggle',
538
+ label: 'Modalities: Audio',
539
+ dataKey: 'modalitiesIncludeAudio',
540
+ helperMessage: 'If on, the model will include audio in its responses. Only relevant for multimodal models.',
541
+ },
542
+ {
543
+ type: 'string',
544
+ label: 'Audio Voice',
545
+ dataKey: 'audioVoice',
546
+ useInputToggleDataKey: 'useAudioVoiceInput',
547
+ helperMessage: 'The voice to use for audio responses. See your model for supported voices. OpenAI voices are: alloy, ash, coral, echo, fable, onyx, nova, sage, and shimmer.',
548
+ hideIf: (data) => !data.modalitiesIncludeAudio,
549
+ },
550
+ {
551
+ type: 'dropdown',
552
+ label: 'Audio Format',
553
+ dataKey: 'audioFormat',
554
+ useInputToggleDataKey: 'useAudioFormatInput',
555
+ options: [
556
+ { value: 'wav', label: 'WAV' },
557
+ { value: 'mp3', label: 'MP3' },
558
+ { value: 'flac', label: 'FLAC' },
559
+ { value: 'opus', label: 'OPUS' },
560
+ { value: 'pcm16', label: 'PCM16' },
561
+ ],
562
+ defaultValue: 'wav',
563
+ hideIf: (data) => !data.modalitiesIncludeAudio,
564
+ },
565
+ ],
566
+ },
567
+ {
568
+ type: 'group',
569
+ label: 'Advanced',
570
+ editors: [
571
+ {
572
+ type: 'toggle',
573
+ label: 'Use Server Token Calculation',
574
+ dataKey: 'useServerTokenCalculation',
575
+ helperMessage: 'If on, do not calculate token counts on the client side, and rely on the server providing the token count.',
576
+ },
577
+ {
578
+ type: 'toggle',
579
+ label: 'Output Usage Statistics',
580
+ dataKey: 'outputUsage',
581
+ helperMessage: 'If on, output usage statistics for the model, such as token counts and cost.',
582
+ },
583
+ {
584
+ type: 'string',
585
+ label: 'User',
586
+ dataKey: 'user',
587
+ useInputToggleDataKey: 'useUserInput',
588
+ helperMessage: 'A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.',
589
+ },
590
+ {
591
+ type: 'number',
592
+ label: 'Number of Choices',
593
+ dataKey: 'numberOfChoices',
594
+ useInputToggleDataKey: 'useNumberOfChoicesInput',
595
+ min: 1,
596
+ max: 10,
597
+ step: 1,
598
+ defaultValue: 1,
599
+ helperMessage: 'If greater than 1, the model will return multiple choices and the response will be an array.',
600
+ },
601
+ {
602
+ type: 'string',
603
+ label: 'Endpoint',
604
+ dataKey: 'endpoint',
605
+ useInputToggleDataKey: 'useEndpointInput',
606
+ helperMessage: 'The endpoint to use for the OpenAI API. You can use this to replace with any OpenAI-compatible API. Leave blank for the default: https://api.openai.com/api/v1/chat/completions',
607
+ },
608
+ {
609
+ type: 'string',
610
+ label: 'Custom Model',
611
+ dataKey: 'overrideModel',
612
+ helperMessage: 'Overrides the model selected above with a custom string for the model.',
613
+ },
614
+ {
615
+ type: 'number',
616
+ label: 'Custom Max Tokens',
617
+ dataKey: 'overrideMaxTokens',
618
+ allowEmpty: true,
619
+ helperMessage: 'Overrides the max number of tokens a model can support. Leave blank for preconfigured token limits.',
620
+ },
621
+ {
622
+ type: 'keyValuePair',
623
+ label: 'Headers',
624
+ dataKey: 'headers',
625
+ useInputToggleDataKey: 'useHeadersInput',
626
+ keyPlaceholder: 'Header',
627
+ helperMessage: 'Additional headers to send to the API.',
628
+ },
629
+ {
630
+ type: 'toggle',
631
+ label: 'Cache In Rivet',
632
+ dataKey: 'cache',
633
+ helperMessage: 'If on, requests with the same parameters and messages will be cached in Rivet, for immediate responses without an API call.',
634
+ },
635
+ {
636
+ type: 'toggle',
637
+ label: 'Use for subgraph partial output',
638
+ dataKey: 'useAsGraphPartialOutput',
639
+ helperMessage: 'If on, streaming responses from this node will be shown in Subgraph nodes that call this graph.',
640
+ },
641
+ {
642
+ type: 'keyValuePair',
643
+ label: 'Additional Parameters',
644
+ dataKey: 'additionalParameters',
645
+ useInputToggleDataKey: 'useAdditionalParametersInput',
646
+ keyPlaceholder: 'Parameter',
647
+ valuePlaceholder: 'Value',
648
+ helperMessage: 'Additional chat completion parameters to send to the API. If the value appears to be a number, it will be sent as a number.',
649
+ },
650
+ ],
651
+ },
652
+ ];
653
+ },
654
+ getBody: (data) => {
655
+ return dedent `
656
+ ${data.endpoint ? `${data.endpoint}` : ''}
657
+ ${data.useMaxTokensInput ? 'Max Tokens: (Using Input)' : `${data.maxTokens} tokens`}
658
+ Model: ${data.useModelInput ? '(Using Input)' : data.overrideModel || data.model}
659
+ ${data.useTopP ? 'Top P' : 'Temperature'}:
660
+ ${data.useTopP
661
+ ? data.useTopPInput
662
+ ? '(Using Input)'
663
+ : data.top_p
664
+ : data.useTemperatureInput
665
+ ? '(Using Input)'
666
+ : data.temperature}
667
+ ${data.useStop ? `Stop: ${data.useStopInput ? '(Using Input)' : data.stop}` : ''}
668
+ ${(data.frequencyPenalty ?? 0) !== 0
669
+ ? `Frequency Penalty: ${data.useFrequencyPenaltyInput ? '(Using Input)' : data.frequencyPenalty}`
670
+ : ''}
671
+ ${(data.presencePenalty ?? 0) !== 0
672
+ ? `Presence Penalty: ${data.usePresencePenaltyInput ? '(Using Input)' : data.presencePenalty}`
673
+ : ''}
674
+ `.trim();
675
+ },
676
+ process: async (data, node, inputs, context) => {
677
+ const output = {};
678
+ const model = getInputOrData(data, inputs, 'model');
679
+ const temperature = getInputOrData(data, inputs, 'temperature', 'number');
680
+ const topP = data.useTopPInput ? coerceTypeOptional(inputs['top_p'], 'number') ?? data.top_p : data.top_p;
681
+ const useTopP = getInputOrData(data, inputs, 'useTopP', 'boolean');
682
+ const stop = data.useStopInput
683
+ ? data.useStop
684
+ ? coerceTypeOptional(inputs['stop'], 'string') ?? data.stop
685
+ : undefined
686
+ : data.stop;
687
+ const presencePenalty = getInputOrData(data, inputs, 'presencePenalty', 'number');
688
+ const frequencyPenalty = getInputOrData(data, inputs, 'frequencyPenalty', 'number');
689
+ const numberOfChoices = getInputOrData(data, inputs, 'numberOfChoices', 'number');
690
+ const endpoint = getInputOrData(data, inputs, 'endpoint');
691
+ const overrideModel = getInputOrData(data, inputs, 'overrideModel');
692
+ const seed = getInputOrData(data, inputs, 'seed', 'number');
693
+ const responseFormat = getInputOrData(data, inputs, 'responseFormat');
694
+ const toolChoiceMode = getInputOrData(data, inputs, 'toolChoice', 'string');
695
+ const parallelFunctionCalling = getInputOrData(data, inputs, 'parallelFunctionCalling', 'boolean');
696
+ const predictedOutput = data.usePredictedOutput
697
+ ? coerceTypeOptional(inputs['predictedOutput'], 'string[]')
698
+ : undefined;
699
+ const toolChoice = !toolChoiceMode || !data.enableFunctionUse
700
+ ? undefined
701
+ : toolChoiceMode === 'function'
702
+ ? {
703
+ type: 'function',
704
+ function: {
705
+ name: getInputOrData(data, inputs, 'toolChoiceFunction', 'string'),
706
+ },
707
+ }
708
+ : toolChoiceMode;
709
+ let responseSchema;
710
+ const responseSchemaInput = inputs['responseSchema'];
711
+ if (responseSchemaInput?.type === 'gpt-function') {
712
+ responseSchema = responseSchemaInput.value.parameters;
713
+ }
714
+ else if (responseSchemaInput != null) {
715
+ responseSchema = coerceType(responseSchemaInput, 'object');
716
+ }
717
+ const openaiResponseFormat = !responseFormat?.trim()
718
+ ? undefined
719
+ : responseFormat === 'json'
720
+ ? {
721
+ type: 'json_object',
722
+ }
723
+ : responseFormat === 'json_schema'
724
+ ? {
725
+ type: 'json_schema',
726
+ json_schema: {
727
+ name: getInputOrData(data, inputs, 'responseSchemaName', 'string') || 'response_schema',
728
+ strict: true,
729
+ schema: responseSchema ?? {},
730
+ },
731
+ }
732
+ : {
733
+ type: 'text',
734
+ };
735
+ const headersFromData = (data.headers ?? []).reduce((acc, header) => {
736
+ acc[header.key] = header.value;
737
+ return acc;
738
+ }, {});
739
+ const additionalHeaders = data.useHeadersInput
740
+ ? coerceTypeOptional(inputs['headers'], 'object') ??
741
+ headersFromData
742
+ : headersFromData;
743
+ const additionalParametersFromData = (data.additionalParameters ?? []).reduce((acc, param) => {
744
+ acc[param.key] = Number.isNaN(parseFloat(param.value)) ? param.value : parseFloat(param.value);
745
+ return acc;
746
+ }, {});
747
+ const additionalParameters = data.useAdditionalParametersInput
748
+ ? coerceTypeOptional(inputs['additionalParameters'], 'object') ?? additionalParametersFromData
749
+ : additionalParametersFromData;
750
+ // If using a model input, that's priority, otherwise override > main
751
+ const finalModel = data.useModelInput && inputs['model'] != null ? model : overrideModel || model;
752
+ const functions = coerceTypeOptional(inputs['functions'], 'gpt-function[]');
753
+ const tools = (functions ?? []).map((fn) => ({
754
+ function: fn,
755
+ type: 'function',
756
+ }));
757
+ const { messages } = getChatNodeMessages(inputs);
758
+ const isReasoningModel = finalModel.startsWith('o1') || finalModel.startsWith('o3');
759
+ const completionMessages = await Promise.all(messages.map((message) => chatMessageToOpenAIChatCompletionMessage(message, { isReasoningModel })));
760
+ let { maxTokens } = data;
761
+ const openaiModel = {
762
+ ...(openaiModels[model] ?? {
763
+ maxTokens: data.overrideMaxTokens ?? 8192,
764
+ cost: {
765
+ completion: 0,
766
+ prompt: 0,
767
+ },
768
+ displayName: 'Custom Model',
769
+ }),
770
+ };
771
+ if (data.overrideMaxTokens) {
772
+ openaiModel.maxTokens = data.overrideMaxTokens;
773
+ }
774
+ const isMultiResponse = data.useNumberOfChoicesInput || (data.numberOfChoices ?? 1) > 1;
775
+ // Resolve to final endpoint if configured in ProcessContext
776
+ const configuredEndpoint = endpoint || context.settings.openAiEndpoint || DEFAULT_CHAT_ENDPOINT;
777
+ const resolvedEndpointAndHeaders = context.getChatNodeEndpoint
778
+ ? await context.getChatNodeEndpoint(configuredEndpoint, finalModel)
779
+ : {
780
+ endpoint: configuredEndpoint,
781
+ headers: {},
782
+ };
783
+ const allAdditionalHeaders = cleanHeaders({
784
+ ...context.settings.chatNodeHeaders,
785
+ ...additionalHeaders,
786
+ ...resolvedEndpointAndHeaders.headers,
787
+ });
788
+ let inputTokenCount = 0;
789
+ const tokenizerInfo = {
790
+ node,
791
+ model: finalModel,
792
+ endpoint: resolvedEndpointAndHeaders.endpoint,
793
+ };
794
+ if (!data.useServerTokenCalculation) {
795
+ inputTokenCount = await context.tokenizer.getTokenCountForMessages(messages, functions, tokenizerInfo);
796
+ if (inputTokenCount >= openaiModel.maxTokens) {
797
+ throw new Error(`The model ${model} can only handle ${openaiModel.maxTokens} tokens, but ${inputTokenCount} were provided in the prompts alone.`);
798
+ }
799
+ if (inputTokenCount + maxTokens > openaiModel.maxTokens) {
800
+ const message = `The model can only handle a maximum of ${openaiModel.maxTokens} tokens, but the prompts and max tokens together exceed this limit. The max tokens has been reduced to ${openaiModel.maxTokens - inputTokenCount}.`;
801
+ addWarning(output, message);
802
+ maxTokens = Math.floor((openaiModel.maxTokens - inputTokenCount) * 0.95); // reduce max tokens by 5% to be safe, calculation is a little wrong.
803
+ }
804
+ }
805
+ const predictionObject = predictedOutput
806
+ ? predictedOutput.length === 1
807
+ ? { type: 'content', content: predictedOutput[0] }
808
+ : { type: 'content', content: predictedOutput.map((part) => ({ type: 'text', text: part })) }
809
+ : undefined;
810
+ const voice = getInputOrData(data, inputs, 'audioVoice');
811
+ let modalities = [];
812
+ if (data.modalitiesIncludeText) {
813
+ modalities.push('text');
814
+ }
815
+ if (data.modalitiesIncludeAudio) {
816
+ modalities.push('audio');
817
+ if (!voice) {
818
+ throw new Error('Audio voice must be specified if audio is enabled.');
819
+ }
820
+ }
821
+ // Errors happen if modalities isn't supported, so omit it if it's empty
822
+ if (modalities.length === 0) {
823
+ modalities = undefined;
824
+ }
825
+ const audio = modalities?.includes('audio')
826
+ ? {
827
+ voice,
828
+ format: getInputOrData(data, inputs, 'audioFormat') ??
829
+ 'wav',
830
+ }
831
+ : undefined;
832
+ const reasoningEffort = getInputOrData(data, inputs, 'reasoningEffort');
833
+ const supported = openaiModels[finalModel]?.supported ??
834
+ defaultOpenaiSupported;
835
+ try {
836
+ return await retry(async () => {
837
+ const options = {
838
+ messages: completionMessages,
839
+ model: finalModel,
840
+ top_p: useTopP ? topP : undefined,
841
+ n: numberOfChoices,
842
+ frequency_penalty: frequencyPenalty,
843
+ presence_penalty: presencePenalty,
844
+ stop: stop || undefined,
845
+ tools: tools.length > 0 ? tools : undefined,
846
+ endpoint: resolvedEndpointAndHeaders.endpoint,
847
+ seed,
848
+ response_format: openaiResponseFormat,
849
+ tool_choice: toolChoice,
850
+ parallel_tool_calls: tools.length > 0 && supported.parallelFunctionCalls ? parallelFunctionCalling : undefined,
851
+ prediction: predictionObject,
852
+ modalities,
853
+ audio,
854
+ reasoning_effort: reasoningEffort || undefined,
855
+ ...additionalParameters,
856
+ };
857
+ const isO1Beta = finalModel.startsWith('o1-preview') || finalModel.startsWith('o1-mini');
858
+ if (isReasoningModel) {
859
+ options.max_completion_tokens = maxTokens;
860
+ }
861
+ else {
862
+ options.temperature = useTopP ? undefined : temperature; // Not supported in o1-preview
863
+ options.max_tokens = maxTokens;
864
+ }
865
+ const cacheKey = JSON.stringify(options);
866
+ if (data.cache) {
867
+ const cached = cache.get(cacheKey);
868
+ if (cached) {
869
+ return cached;
870
+ }
871
+ }
872
+ const startTime = Date.now();
873
+ // Non-streaming APIs
874
+ if (isO1Beta || audio) {
875
+ const response = await chatCompletions({
876
+ auth: {
877
+ apiKey: context.settings.openAiKey ?? '',
878
+ organization: context.settings.openAiOrganization,
879
+ },
880
+ headers: allAdditionalHeaders,
881
+ signal: context.signal,
882
+ timeout: context.settings.chatNodeTimeout,
883
+ ...options,
884
+ });
885
+ if ('error' in response) {
886
+ throw new OpenAIError(400, response.error);
887
+ }
888
+ if (isMultiResponse) {
889
+ output['response'] = {
890
+ type: 'string[]',
891
+ value: response.choices.map((c) => c.message.content),
892
+ };
893
+ }
894
+ else {
895
+ output['response'] = {
896
+ type: 'string',
897
+ value: response.choices[0].message.content ?? '',
898
+ };
899
+ }
900
+ if (!isMultiResponse) {
901
+ output['all-messages'] = {
902
+ type: 'chat-message[]',
903
+ value: [
904
+ ...messages,
905
+ {
906
+ type: 'assistant',
907
+ message: response.choices[0].message.content ?? '',
908
+ function_calls: undefined,
909
+ isCacheBreakpoint: false,
910
+ function_call: undefined,
911
+ },
912
+ ],
913
+ };
914
+ }
915
+ if (modalities?.includes('audio')) {
916
+ const audioData = response.choices[0].message.audio;
917
+ output['audio'] = {
918
+ type: 'audio',
919
+ value: {
920
+ data: base64ToUint8Array(audioData.data),
921
+ mediaType: audioFormatToMediaType(audio.format),
922
+ },
923
+ };
924
+ output['audioTranscript'] = {
925
+ type: 'string',
926
+ value: response.choices[0].message.audio.transcript,
927
+ };
928
+ }
929
+ output['duration'] = { type: 'number', value: Date.now() - startTime };
930
+ if (response.usage) {
931
+ output['usage'] = {
932
+ type: 'object',
933
+ value: response.usage,
934
+ };
935
+ const costs = finalModel in openaiModels ? openaiModels[finalModel].cost : undefined;
936
+ const promptCostPerThousand = costs?.prompt ?? 0;
937
+ const completionCostPerThousand = costs?.completion ?? 0;
938
+ const audioPromptCostPerThousand = costs
939
+ ? 'audioPrompt' in costs
940
+ ? costs.audioPrompt
941
+ : 0
942
+ : 0;
943
+ const audioCompletionCostPerThousand = costs
944
+ ? 'audioCompletion' in costs
945
+ ? costs.audioCompletion
946
+ : 0
947
+ : 0;
948
+ const promptCost = getCostForTokens(response.usage.prompt_tokens_details.text_tokens, promptCostPerThousand);
949
+ const completionCost = getCostForTokens(response.usage.completion_tokens_details.text_tokens, completionCostPerThousand);
950
+ const audioPromptCost = getCostForTokens(response.usage.prompt_tokens_details.audio_tokens, audioPromptCostPerThousand);
951
+ const audioCompletionCost = getCostForTokens(response.usage.completion_tokens_details.audio_tokens, audioCompletionCostPerThousand);
952
+ output['cost'] = {
953
+ type: 'number',
954
+ value: promptCost + completionCost + audioPromptCost + audioCompletionCost,
955
+ };
956
+ }
957
+ Object.freeze(output);
958
+ cache.set(cacheKey, output);
959
+ return output;
960
+ }
961
+ const chunks = streamChatCompletions({
962
+ auth: {
963
+ apiKey: context.settings.openAiKey ?? '',
964
+ organization: context.settings.openAiOrganization,
965
+ },
966
+ headers: allAdditionalHeaders,
967
+ signal: context.signal,
968
+ timeout: context.settings.chatNodeTimeout,
969
+ ...options,
970
+ });
971
+ const responseChoicesParts = [];
972
+ // First array is the function calls per choice, inner array is the functions calls inside the choice
973
+ const functionCalls = [];
974
+ let usage;
975
+ let throttleLastCalledTime = Date.now();
976
+ const onPartialOutput = (output) => {
977
+ const now = Date.now();
978
+ if (now - throttleLastCalledTime > (context.settings.throttleChatNode ?? 100)) {
979
+ context.onPartialOutputs?.(output);
980
+ throttleLastCalledTime = now;
981
+ }
982
+ };
983
+ for await (const chunk of chunks) {
984
+ if (chunk.usage) {
985
+ usage = chunk.usage;
986
+ }
987
+ if (!chunk.choices) {
988
+ // Could be error for some reason 🤷‍♂️ but ignoring has worked for me so far.
989
+ continue;
990
+ }
991
+ for (const { delta, index } of chunk.choices) {
992
+ if (delta.content != null) {
993
+ responseChoicesParts[index] ??= [];
994
+ responseChoicesParts[index].push(delta.content);
995
+ }
996
+ if (delta.tool_calls) {
997
+ // Are we sure that tool_calls will always be full and not a bunch of deltas?
998
+ functionCalls[index] ??= [];
999
+ for (const toolCall of delta.tool_calls) {
1000
+ functionCalls[index][toolCall.index] ??= {
1001
+ type: 'function',
1002
+ arguments: '',
1003
+ lastParsedArguments: undefined,
1004
+ name: '',
1005
+ id: '',
1006
+ };
1007
+ if (toolCall.id) {
1008
+ functionCalls[index][toolCall.index].id = toolCall.id;
1009
+ }
1010
+ if (toolCall.function.name) {
1011
+ functionCalls[index][toolCall.index].name += toolCall.function.name;
1012
+ }
1013
+ if (toolCall.function.arguments) {
1014
+ functionCalls[index][toolCall.index].arguments += toolCall.function.arguments;
1015
+ try {
1016
+ functionCalls[index][toolCall.index].lastParsedArguments = JSON.parse(functionCalls[index][toolCall.index].arguments);
1017
+ }
1018
+ catch (error) {
1019
+ // Ignore
1020
+ }
1021
+ }
1022
+ }
1023
+ }
1024
+ }
1025
+ if (isMultiResponse) {
1026
+ output['response'] = {
1027
+ type: 'string[]',
1028
+ value: responseChoicesParts.map((parts) => parts.join('')),
1029
+ };
1030
+ }
1031
+ else {
1032
+ output['response'] = {
1033
+ type: 'string',
1034
+ value: responseChoicesParts[0]?.join('') ?? '',
1035
+ };
1036
+ }
1037
+ if (functionCalls.length > 0) {
1038
+ if (isMultiResponse) {
1039
+ output['function-call'] = {
1040
+ type: 'object[]',
1041
+ value: functionCalls.map((functionCalls) => ({
1042
+ name: functionCalls[0]?.name,
1043
+ arguments: functionCalls[0]?.lastParsedArguments,
1044
+ id: functionCalls[0]?.id,
1045
+ })),
1046
+ };
1047
+ }
1048
+ else {
1049
+ if (data.parallelFunctionCalling) {
1050
+ output['function-calls'] = {
1051
+ type: 'object[]',
1052
+ value: functionCalls[0].map((functionCall) => ({
1053
+ name: functionCall.name,
1054
+ arguments: functionCall.lastParsedArguments,
1055
+ id: functionCall.id,
1056
+ })),
1057
+ };
1058
+ }
1059
+ else {
1060
+ output['function-call'] = {
1061
+ type: 'object',
1062
+ value: {
1063
+ name: functionCalls[0][0]?.name,
1064
+ arguments: functionCalls[0][0]?.lastParsedArguments,
1065
+ id: functionCalls[0][0]?.id,
1066
+ },
1067
+ };
1068
+ }
1069
+ }
1070
+ }
1071
+ onPartialOutput(output);
1072
+ }
1073
+ // Call one last time manually to ensure the last output is sent
1074
+ context.onPartialOutputs?.(output);
1075
+ if (!isMultiResponse) {
1076
+ output['all-messages'] = {
1077
+ type: 'chat-message[]',
1078
+ value: [
1079
+ ...messages,
1080
+ {
1081
+ type: 'assistant',
1082
+ message: responseChoicesParts[0]?.join('') ?? '',
1083
+ function_call: functionCalls[0]
1084
+ ? {
1085
+ name: functionCalls[0][0].name,
1086
+ arguments: functionCalls[0][0].arguments, // Needs the stringified one here in chat list
1087
+ id: functionCalls[0][0].id,
1088
+ }
1089
+ : undefined,
1090
+ function_calls: functionCalls[0]
1091
+ ? functionCalls[0].map((fc) => ({
1092
+ name: fc.name,
1093
+ arguments: fc.arguments,
1094
+ id: fc.id,
1095
+ }))
1096
+ : undefined,
1097
+ },
1098
+ ],
1099
+ };
1100
+ }
1101
+ const endTime = Date.now();
1102
+ if (responseChoicesParts.length === 0 && functionCalls.length === 0) {
1103
+ throw new Error('No response from OpenAI');
1104
+ }
1105
+ let outputTokenCount = 0;
1106
+ if (usage) {
1107
+ inputTokenCount = usage.prompt_tokens;
1108
+ outputTokenCount = usage.completion_tokens;
1109
+ }
1110
+ output['in-messages'] = { type: 'chat-message[]', value: messages };
1111
+ output['requestTokens'] = { type: 'number', value: inputTokenCount * (numberOfChoices ?? 1) };
1112
+ if (!data.useServerTokenCalculation) {
1113
+ let responseTokenCount = 0;
1114
+ for (const choiceParts of responseChoicesParts) {
1115
+ responseTokenCount += await context.tokenizer.getTokenCountForString(choiceParts.join(), tokenizerInfo);
1116
+ }
1117
+ outputTokenCount = responseTokenCount;
1118
+ }
1119
+ output['responseTokens'] = { type: 'number', value: outputTokenCount };
1120
+ const outputTokensForCostCalculation = usage?.completion_tokens_details
1121
+ ? usage.completion_tokens_details.rejected_prediction_tokens > 0
1122
+ ? usage.completion_tokens_details.rejected_prediction_tokens
1123
+ : usage.completion_tokens
1124
+ : outputTokenCount;
1125
+ const promptCostPerThousand = model in openaiModels ? openaiModels[model].cost.prompt : 0;
1126
+ const completionCostPerThousand = model in openaiModels ? openaiModels[model].cost.completion : 0;
1127
+ const promptCost = getCostForTokens(inputTokenCount, promptCostPerThousand);
1128
+ const completionCost = getCostForTokens(outputTokensForCostCalculation, completionCostPerThousand);
1129
+ const cost = promptCost + completionCost;
1130
+ if (usage) {
1131
+ output['usage'] = {
1132
+ type: 'object',
1133
+ value: {
1134
+ ...usage,
1135
+ prompt_cost: promptCost,
1136
+ completion_cost: completionCost,
1137
+ total_cost: cost,
1138
+ },
1139
+ };
1140
+ }
1141
+ else {
1142
+ output['usage'] = {
1143
+ type: 'object',
1144
+ value: {
1145
+ prompt_tokens: inputTokenCount,
1146
+ completion_tokens: outputTokenCount,
1147
+ },
1148
+ };
1149
+ }
1150
+ output['cost'] = { type: 'number', value: cost };
1151
+ output['__hidden_token_count'] = { type: 'number', value: inputTokenCount + outputTokenCount };
1152
+ const duration = endTime - startTime;
1153
+ output['duration'] = { type: 'number', value: duration };
1154
+ Object.freeze(output);
1155
+ cache.set(cacheKey, output);
1156
+ return output;
1157
+ }, {
1158
+ forever: true,
1159
+ retries: 10000,
1160
+ maxRetryTime: 1000 * 60 * 5,
1161
+ factor: 2.5,
1162
+ minTimeout: 500,
1163
+ maxTimeout: 5000,
1164
+ randomize: true,
1165
+ signal: context.signal,
1166
+ onFailedAttempt(originalError) {
1167
+ let err = originalError;
1168
+ if (originalError.toString().includes('fetch failed') && originalError.cause) {
1169
+ const cause = getError(originalError.cause) instanceof AggregateError
1170
+ ? originalError.cause.errors[0]
1171
+ : getError(originalError.cause);
1172
+ err = cause;
1173
+ }
1174
+ if (context.signal.aborted) {
1175
+ throw new Error('Aborted');
1176
+ }
1177
+ context.trace(`ChatNode failed, retrying: ${err.toString()}`);
1178
+ const { retriesLeft } = err;
1179
+ // Retry network errors
1180
+ if (err.toString().includes('terminated') ||
1181
+ originalError.toString().includes('terminated') ||
1182
+ err.toString().includes('fetch failed')) {
1183
+ return;
1184
+ }
1185
+ if (!(err instanceof OpenAIError)) {
1186
+ if ('code' in err) {
1187
+ throw err;
1188
+ }
1189
+ return; // Just retry?
1190
+ }
1191
+ if (err.status === 429) {
1192
+ if (retriesLeft) {
1193
+ context.onPartialOutputs?.({
1194
+ ['response']: {
1195
+ type: 'string',
1196
+ value: 'OpenAI API rate limit exceeded, retrying...',
1197
+ },
1198
+ });
1199
+ return;
1200
+ }
1201
+ }
1202
+ if (err.status === 408) {
1203
+ if (retriesLeft) {
1204
+ context.onPartialOutputs?.({
1205
+ ['response']: {
1206
+ type: 'string',
1207
+ value: 'OpenAI API timed out, retrying...',
1208
+ },
1209
+ });
1210
+ return;
1211
+ }
1212
+ }
1213
+ // We did something wrong (besides rate limit)
1214
+ if (err.status >= 400 && err.status < 500) {
1215
+ throw new Error(err.message);
1216
+ }
1217
+ },
1218
+ });
1219
+ }
1220
+ catch (error) {
1221
+ context.trace(getError(error).stack ?? 'Missing stack');
1222
+ throw new Error(`Error processing ChatNode: ${error.message}`, { cause: error });
1223
+ }
1224
+ },
1225
+ };
1226
+ export function getChatNodeMessages(inputs) {
1227
+ const prompt = inputs['prompt'];
1228
+ let messages = match(prompt)
1229
+ .with({ type: 'chat-message' }, (p) => [p.value])
1230
+ .with({ type: 'chat-message[]' }, (p) => p.value)
1231
+ .with({ type: 'string' }, (p) => [{ type: 'user', message: p.value }])
1232
+ .with({ type: 'string[]' }, (p) => p.value.map((v) => ({ type: 'user', message: v })))
1233
+ .otherwise((p) => {
1234
+ if (!p) {
1235
+ return [];
1236
+ }
1237
+ if (isArrayDataValue(p)) {
1238
+ const stringValues = p.value.map((v) => coerceType({
1239
+ type: getScalarTypeOf(p.type),
1240
+ value: v,
1241
+ }, 'string'));
1242
+ return stringValues.filter((v) => v != null).map((v) => ({ type: 'user', message: v }));
1243
+ }
1244
+ const coercedMessage = coerceTypeOptional(p, 'chat-message');
1245
+ if (coercedMessage != null) {
1246
+ return [coercedMessage];
1247
+ }
1248
+ const coercedString = coerceTypeOptional(p, 'string');
1249
+ return coercedString != null ? [{ type: 'user', message: coerceType(p, 'string') }] : [];
1250
+ });
1251
+ const systemPrompt = inputs['systemPrompt'];
1252
+ if (systemPrompt) {
1253
+ if (messages.length > 0 && messages.at(0).type === 'system') {
1254
+ // Delete the first system message if it's already there
1255
+ messages.splice(0, 1);
1256
+ }
1257
+ messages = [{ type: 'system', message: coerceType(systemPrompt, 'string') }, ...messages];
1258
+ }
1259
+ return { messages, systemPrompt };
1260
+ }
1261
+ export function getCostForTokens(tokenCount, costPerThousand) {
1262
+ return (tokenCount / 1000) * costPerThousand;
1263
+ }
1264
+ function audioFormatToMediaType(format) {
1265
+ switch (format) {
1266
+ case 'wav':
1267
+ return 'audio/wav';
1268
+ case 'mp3':
1269
+ return 'audio/mpeg';
1270
+ case 'flac':
1271
+ return 'audio/flac';
1272
+ case 'opus':
1273
+ return 'audio/opus';
1274
+ case 'pcm16':
1275
+ return 'audio/wav';
1276
+ }
1277
+ }