first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
import { shouldIncludeModel } from '../modelFiltering';
|
||||
|
||||
describe('shouldIncludeModel', () => {
|
||||
const testCases: Array<{ modelId: string; officialAPI: boolean }> = [
|
||||
// Excluded model types
|
||||
{ modelId: 'babbage-002', officialAPI: false },
|
||||
{ modelId: 'davinci-002', officialAPI: false },
|
||||
{ modelId: 'computer-use-preview', officialAPI: false },
|
||||
{ modelId: 'dall-e-3', officialAPI: false },
|
||||
{ modelId: 'text-embedding-ada-002', officialAPI: false },
|
||||
{ modelId: 'tts-1', officialAPI: false },
|
||||
{ modelId: 'whisper-1', officialAPI: false },
|
||||
{ modelId: 'omni-moderation-latest', officialAPI: false },
|
||||
{ modelId: 'sora-1', officialAPI: false },
|
||||
{ modelId: 'gpt-4o-realtime-preview', officialAPI: false }, // infix check for -realtime
|
||||
{ modelId: 'gpt-3.5-turbo-instruct', officialAPI: false }, // gpt-* with instruct
|
||||
|
||||
// Included models (standard chat models)
|
||||
{ modelId: 'gpt-4', officialAPI: true },
|
||||
{ modelId: 'gpt-4o', officialAPI: true },
|
||||
{ modelId: 'o1-preview', officialAPI: true },
|
||||
{ modelId: 'ft:gpt-3.5-turbo', officialAPI: true }, // fine-tuned models
|
||||
|
||||
// Edge cases
|
||||
{ modelId: 'llama-3-70b-instruct', officialAPI: true }, // non-gpt instruct is allowed
|
||||
{ modelId: 'custom-model', officialAPI: true }, // arbitrary custom model names
|
||||
];
|
||||
|
||||
describe('Custom API behavior', () => {
|
||||
it.each(testCases)('should include "$modelId"', ({ modelId }) => {
|
||||
expect(shouldIncludeModel(modelId, true)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Official OpenAI API filtering', () => {
|
||||
const testCasesWithAction = testCases.map((tc) => ({
|
||||
...tc,
|
||||
action: tc.officialAPI ? 'include' : 'exclude',
|
||||
}));
|
||||
|
||||
it.each(testCasesWithAction)('should $action "$modelId"', ({ modelId, officialAPI }) => {
|
||||
expect(shouldIncludeModel(modelId, false)).toBe(officialAPI);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { IBinaryData, IExecuteFunctions } from 'n8n-workflow';
|
||||
|
||||
/** Chunk size to use for streaming. 256Kb */
|
||||
const CHUNK_SIZE = 256 * 1024;
|
||||
|
||||
/**
|
||||
* Gets the binary data file for the given item index and given property name.
|
||||
* Returns the file name, content type and the file content. Uses streaming
|
||||
* when possible.
|
||||
*/
|
||||
export async function getBinaryDataFile(
|
||||
ctx: IExecuteFunctions,
|
||||
itemIdx: number,
|
||||
binaryPropertyData: string | IBinaryData,
|
||||
) {
|
||||
const binaryData = ctx.helpers.assertBinaryData(itemIdx, binaryPropertyData);
|
||||
|
||||
const fileContent = binaryData.id
|
||||
? await ctx.helpers.getBinaryStream(binaryData.id, CHUNK_SIZE)
|
||||
: await ctx.helpers.getBinaryDataBuffer(itemIdx, binaryPropertyData);
|
||||
|
||||
return {
|
||||
filename: binaryData.fileName,
|
||||
contentType: binaryData.mimeType,
|
||||
fileContent,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export const MODELS_NOT_SUPPORT_FUNCTION_CALLS = [
|
||||
'gpt-3.5-turbo-16k-0613',
|
||||
'dall-e-3',
|
||||
'text-embedding-3-large',
|
||||
'dall-e-2',
|
||||
'whisper-1',
|
||||
'tts-1-hd-1106',
|
||||
'tts-1-hd',
|
||||
'gpt-4-0314',
|
||||
'text-embedding-3-small',
|
||||
'gpt-4-32k-0314',
|
||||
'gpt-3.5-turbo-0301',
|
||||
'gpt-4-vision-preview',
|
||||
'gpt-3.5-turbo-16k',
|
||||
'gpt-3.5-turbo-instruct-0914',
|
||||
'tts-1',
|
||||
'davinci-002',
|
||||
'gpt-3.5-turbo-instruct',
|
||||
'babbage-002',
|
||||
'tts-1-1106',
|
||||
'text-embedding-ada-002',
|
||||
];
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { INodeInputConfiguration } from 'n8n-workflow';
|
||||
|
||||
export const prettifyOperation = (resource: string, operation: string) => {
|
||||
if (operation === 'deleteAssistant') {
|
||||
return 'Delete Assistant';
|
||||
}
|
||||
|
||||
if (operation === 'deleteFile') {
|
||||
return 'Delete File';
|
||||
}
|
||||
|
||||
if (operation === 'classify') {
|
||||
return 'Classify Text';
|
||||
}
|
||||
|
||||
if (operation === 'message' && resource === 'text') {
|
||||
return 'Message Model';
|
||||
}
|
||||
|
||||
const capitalize = (str: string) => {
|
||||
const chars = str.split('');
|
||||
chars[0] = chars[0].toUpperCase();
|
||||
return chars.join('');
|
||||
};
|
||||
|
||||
if (['transcribe', 'translate'].includes(operation)) {
|
||||
resource = 'recording';
|
||||
}
|
||||
|
||||
if (operation === 'list') {
|
||||
resource = resource + 's';
|
||||
}
|
||||
|
||||
return `${capitalize(operation)} ${capitalize(resource)}`;
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
export const configureNodeInputs = (
|
||||
resource: string,
|
||||
operation: string,
|
||||
hideTools: string,
|
||||
memory: string | undefined,
|
||||
) => {
|
||||
if (resource === 'assistant' && operation === 'message') {
|
||||
const inputs: INodeInputConfiguration[] = [
|
||||
{ type: 'main' },
|
||||
{ type: 'ai_tool', displayName: 'Tools' },
|
||||
];
|
||||
if (memory !== 'threadId') {
|
||||
inputs.push({ type: 'ai_memory', displayName: 'Memory', maxConnections: 1 });
|
||||
}
|
||||
return inputs;
|
||||
}
|
||||
if (resource === 'text' && (operation === 'message' || operation === 'response')) {
|
||||
if (hideTools === 'hide') {
|
||||
return ['main'];
|
||||
}
|
||||
return [{ type: 'main' }, { type: 'ai_tool', displayName: 'Tools' }];
|
||||
}
|
||||
|
||||
return ['main'];
|
||||
};
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import { RateLimitError } from 'openai';
|
||||
import { OpenAIError } from 'openai/error';
|
||||
|
||||
import { openAiFailedAttemptHandler, getCustomErrorMessage, isOpenAiError } from './error-handling';
|
||||
|
||||
describe('error-handling', () => {
|
||||
describe('getCustomErrorMessage', () => {
|
||||
it('should return the correct custom error message for known error codes', () => {
|
||||
expect(getCustomErrorMessage('insufficient_quota')).toBe(
|
||||
'Insufficient quota detected. <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-langchain.openai/common-issues/#insufficient-quota" target="_blank">Learn more</a> about resolving this issue',
|
||||
);
|
||||
expect(getCustomErrorMessage('rate_limit_exceeded')).toBe('OpenAI: Rate limit reached');
|
||||
});
|
||||
|
||||
it('should return undefined for unknown error codes', () => {
|
||||
expect(getCustomErrorMessage('unknown_error_code')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOpenAiError', () => {
|
||||
it('should return true if the error is an instance of OpenAIError', () => {
|
||||
const error = new OpenAIError('Test error');
|
||||
expect(isOpenAiError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if the error is not an instance of OpenAIError', () => {
|
||||
const error = new Error('Test error');
|
||||
expect(isOpenAiError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('openAiFailedAttemptHandler', () => {
|
||||
it('should handle RateLimitError and modify the error message', () => {
|
||||
const error = new RateLimitError(
|
||||
429,
|
||||
{ code: 'rate_limit_exceeded' },
|
||||
'Rate limit exceeded',
|
||||
new Headers(),
|
||||
);
|
||||
|
||||
try {
|
||||
openAiFailedAttemptHandler(error);
|
||||
} catch (e) {
|
||||
expect(e).toBeInstanceOf(OperationalError);
|
||||
expect(e.level).toBe('warning');
|
||||
expect(e.cause).toBe(error);
|
||||
expect(e.message).toBe('OpenAI: Rate limit reached');
|
||||
}
|
||||
});
|
||||
|
||||
it('should throw the error if it is not a RateLimitError', () => {
|
||||
const error = new Error('Test error');
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).not.toThrow();
|
||||
});
|
||||
|
||||
describe('non-chat model error handling', () => {
|
||||
it('should throw helpful error when model requires Responses API', () => {
|
||||
const error = {
|
||||
status: 404,
|
||||
type: 'invalid_request_error',
|
||||
param: 'model',
|
||||
message:
|
||||
'This is not a chat model and thus not supported in the v1/chat/completions endpoint. Did you mean to use v1/completions?',
|
||||
code: null,
|
||||
};
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).toThrow(OperationalError);
|
||||
expect(() => openAiFailedAttemptHandler(error)).toThrow(
|
||||
'This model requires the Responses API. Enable "Use Responses API" in the OpenAI Chat Model node options to use this model.',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw for 404 errors with different type', () => {
|
||||
const error = {
|
||||
status: 404,
|
||||
type: 'not_found_error',
|
||||
param: 'model',
|
||||
message: 'This is not a chat model',
|
||||
};
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for 404 errors with different param', () => {
|
||||
const error = {
|
||||
status: 404,
|
||||
type: 'invalid_request_error',
|
||||
param: 'api_key',
|
||||
message: 'This is not a chat model',
|
||||
};
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for 404 errors with different message', () => {
|
||||
const error = {
|
||||
status: 404,
|
||||
type: 'invalid_request_error',
|
||||
param: 'model',
|
||||
message: 'Model not found',
|
||||
};
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for errors with different status', () => {
|
||||
const error = {
|
||||
status: 400,
|
||||
type: 'invalid_request_error',
|
||||
param: 'model',
|
||||
message: 'This is not a chat model',
|
||||
};
|
||||
|
||||
expect(() => openAiFailedAttemptHandler(error)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for null or undefined errors', () => {
|
||||
expect(() => openAiFailedAttemptHandler(null)).not.toThrow();
|
||||
expect(() => openAiFailedAttemptHandler(undefined)).not.toThrow();
|
||||
});
|
||||
|
||||
it('should not throw for non-object errors', () => {
|
||||
expect(() => openAiFailedAttemptHandler('string error')).not.toThrow();
|
||||
expect(() => openAiFailedAttemptHandler(123)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { OperationalError } from 'n8n-workflow';
|
||||
import { RateLimitError } from 'openai';
|
||||
import { OpenAIError } from 'openai/error';
|
||||
|
||||
const errorMap: Record<string, string> = {
|
||||
insufficient_quota:
|
||||
'Insufficient quota detected. <a href="https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-langchain.openai/common-issues/#insufficient-quota" target="_blank">Learn more</a> about resolving this issue',
|
||||
rate_limit_exceeded: 'OpenAI: Rate limit reached',
|
||||
};
|
||||
|
||||
export function getCustomErrorMessage(errorCode: string): string | undefined {
|
||||
return errorMap[errorCode];
|
||||
}
|
||||
|
||||
export function isOpenAiError(error: any): error is OpenAIError {
|
||||
return error instanceof OpenAIError;
|
||||
}
|
||||
|
||||
function isNonChatModelError(error: unknown): boolean {
|
||||
if (typeof error !== 'object' || error === null) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
'status' in error &&
|
||||
error.status === 404 &&
|
||||
'type' in error &&
|
||||
error.type === 'invalid_request_error' &&
|
||||
'param' in error &&
|
||||
error.param === 'model' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string' &&
|
||||
error.message.includes('not a chat model')
|
||||
);
|
||||
}
|
||||
|
||||
export const openAiFailedAttemptHandler = (error: unknown) => {
|
||||
if (isNonChatModelError(error)) {
|
||||
throw new OperationalError(
|
||||
'This model requires the Responses API. Enable "Use Responses API" in the OpenAI Chat Model node options to use this model.',
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
if (error instanceof RateLimitError) {
|
||||
// If the error is a rate limit error, we want to handle it differently
|
||||
// because OpenAI has multiple different rate limit errors
|
||||
const errorCode = error?.code;
|
||||
const errorMessage =
|
||||
getCustomErrorMessage(errorCode ?? 'rate_limit_exceeded') ?? errorMap.rate_limit_exceeded;
|
||||
throw new OperationalError(errorMessage, { cause: error });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { type OpenAIClient } from '@langchain/openai';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import type {
|
||||
ComputerTool,
|
||||
CustomTool,
|
||||
FileSearchTool,
|
||||
FunctionTool,
|
||||
ResponseInputContent,
|
||||
ResponseInputItem,
|
||||
Tool,
|
||||
WebSearchTool as OpenAIChatWebSearchTool,
|
||||
} from 'openai/resources/responses/responses';
|
||||
|
||||
export type ChatResponse = OpenAIClient.Responses.Response;
|
||||
export type ChatContent = ResponseInputContent[];
|
||||
export type ChatInputItem = OpenAIClient.Responses.ResponseInputItem.Message;
|
||||
|
||||
// FIXME: remove these overrides, when langchain-openai is updated with the new types
|
||||
export type WebSearchTool = Omit<OpenAIChatWebSearchTool, 'type'> & {
|
||||
type: 'web_search';
|
||||
filters?: {
|
||||
allowed_domains?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ChatTool =
|
||||
| FunctionTool
|
||||
| FileSearchTool
|
||||
| WebSearchTool
|
||||
| ComputerTool
|
||||
| Tool.CodeInterpreter
|
||||
| Tool.ImageGeneration
|
||||
| Tool.LocalShell
|
||||
| CustomTool;
|
||||
|
||||
export type ChatResponseRequest = Omit<
|
||||
OpenAIClient.Responses.ResponseCreateParamsNonStreaming,
|
||||
'input'
|
||||
> & {
|
||||
max_tool_calls?: number;
|
||||
conversation?:
|
||||
| string
|
||||
| {
|
||||
id: string;
|
||||
};
|
||||
input: ResponseInputItem[];
|
||||
top_logprobs?: number;
|
||||
tools?: ChatTool[];
|
||||
};
|
||||
|
||||
export type ChatCompletion = {
|
||||
id: string;
|
||||
object: string;
|
||||
created: number;
|
||||
model: string;
|
||||
choices: Array<{
|
||||
index: number;
|
||||
message: {
|
||||
role: string;
|
||||
content: string;
|
||||
tool_calls?: Array<{
|
||||
id: string;
|
||||
type: 'function';
|
||||
function: {
|
||||
name: string;
|
||||
arguments: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
finish_reason?: 'tool_calls';
|
||||
}>;
|
||||
usage: {
|
||||
prompt_tokens: number;
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
system_fingerprint: string;
|
||||
};
|
||||
|
||||
export type ThreadMessage = {
|
||||
id: string;
|
||||
object: string;
|
||||
created_at: number;
|
||||
thread_id: string;
|
||||
role: string;
|
||||
content: Array<{
|
||||
type: string;
|
||||
text: {
|
||||
value: string;
|
||||
annotations: string[];
|
||||
};
|
||||
}>;
|
||||
file_ids: string[];
|
||||
assistant_id: string;
|
||||
run_id: string;
|
||||
metadata: IDataObject;
|
||||
};
|
||||
|
||||
export type ExternalApiCallOptions = {
|
||||
callExternalApi: boolean;
|
||||
url: string;
|
||||
path: string;
|
||||
method: string;
|
||||
requestOptions: IDataObject;
|
||||
sendParametersIn: string;
|
||||
};
|
||||
|
||||
export type VideoJob = {
|
||||
id: string;
|
||||
completed_at?: number;
|
||||
created_at: number;
|
||||
error?: {
|
||||
code: string;
|
||||
message: string;
|
||||
};
|
||||
expires_at?: number;
|
||||
model: string;
|
||||
object: 'video';
|
||||
progress?: number;
|
||||
remixed_from_video_id?: string;
|
||||
seconds: string;
|
||||
size: string;
|
||||
status: 'completed' | 'queued' | 'in_progress';
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Determines whether a model should be included in the model list based on
|
||||
* whether it's a custom API and the model's ID.
|
||||
*
|
||||
* @param modelId - The ID of the model to check
|
||||
* @param isCustomAPI - Whether this is a custom API (not official OpenAI)
|
||||
* @returns true if the model should be included, false otherwise
|
||||
*/
|
||||
export function shouldIncludeModel(modelId: string, isCustomAPI: boolean): boolean {
|
||||
// For custom APIs, include all models
|
||||
if (isCustomAPI) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// For official OpenAI API, exclude certain model types
|
||||
return !(
|
||||
modelId.startsWith('babbage') ||
|
||||
modelId.startsWith('davinci') ||
|
||||
modelId.startsWith('computer-use') ||
|
||||
modelId.startsWith('dall-e') ||
|
||||
modelId.startsWith('text-embedding') ||
|
||||
modelId.startsWith('tts') ||
|
||||
modelId.includes('-tts') ||
|
||||
modelId.startsWith('whisper') ||
|
||||
modelId.startsWith('omni-moderation') ||
|
||||
modelId.startsWith('sora') ||
|
||||
modelId.includes('-realtime') ||
|
||||
(modelId.startsWith('gpt-') && modelId.includes('instruct'))
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
export async function pollUntilAvailable<TResponse>(
|
||||
ctx: IExecuteFunctions,
|
||||
request: () => Promise<TResponse>,
|
||||
check: (response: TResponse) => boolean,
|
||||
timeoutSeconds: number,
|
||||
intervalSeconds = 5,
|
||||
): Promise<TResponse> {
|
||||
const abortSignal = ctx.getExecutionCancelSignal();
|
||||
let response: TResponse | undefined;
|
||||
const startTime = Date.now();
|
||||
|
||||
while (!response || !check(response)) {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
if (elapsedTime >= timeoutSeconds * 1000) {
|
||||
throw new NodeApiError(ctx.getNode(), {
|
||||
message: 'Timeout reached',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
|
||||
if (abortSignal?.aborted) {
|
||||
throw new NodeApiError(ctx.getNode(), {
|
||||
message: 'Execution was cancelled',
|
||||
code: 500,
|
||||
});
|
||||
}
|
||||
|
||||
response = await request();
|
||||
|
||||
// Wait before the next polling attempt
|
||||
await new Promise((resolve) => setTimeout(resolve, intervalSeconds * 1000));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { BaseMessage } from '@langchain/core/messages';
|
||||
import type { Tool } from '@langchain/core/tools';
|
||||
import type { OpenAIClient } from '@langchain/openai';
|
||||
import type { BufferWindowMemory } from '@langchain/classic/memory';
|
||||
import { isObjectEmpty } from 'n8n-workflow';
|
||||
import { zodToJsonSchema } from 'zod-to-json-schema';
|
||||
|
||||
// Copied from langchain(`langchain/src/tools/convert_to_openai.ts`)
|
||||
// since these functions are not exported
|
||||
|
||||
/**
|
||||
* Formats a `Tool` instance into a format that is compatible
|
||||
* with OpenAI's ChatCompletionFunctions. It uses the `zodToJsonSchema`
|
||||
* function to convert the schema of the tool into a JSON
|
||||
* schema, which is then used as the parameters for the OpenAI function.
|
||||
*/
|
||||
export function formatToOpenAIFunction(
|
||||
tool: Tool,
|
||||
): OpenAIClient.Chat.ChatCompletionCreateParams.Function {
|
||||
return {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: zodToJsonSchema(tool.schema),
|
||||
};
|
||||
}
|
||||
|
||||
export function formatToOpenAITool(tool: Tool): OpenAIClient.Chat.ChatCompletionTool {
|
||||
const schema = zodToJsonSchema(tool.schema);
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: schema,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function formatToOpenAIAssistantTool(tool: Tool): OpenAIClient.Beta.AssistantTool {
|
||||
return {
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
parameters: zodToJsonSchema(tool.schema),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const requireStrict = (schema: any) => {
|
||||
if (!schema.required) {
|
||||
return false;
|
||||
}
|
||||
// when strict:true, Responses API requires `required` to be present and all properties to be included
|
||||
if (schema.properties) {
|
||||
const propertyNames = Object.keys(schema.properties);
|
||||
const somePropertyMissingFromRequired = propertyNames.some(
|
||||
(propertyName) => !schema.required.includes(propertyName),
|
||||
);
|
||||
const requireStrict = !somePropertyMissingFromRequired;
|
||||
return requireStrict;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export function formatToOpenAIResponsesTool(tool: Tool): OpenAIClient.Responses.FunctionTool {
|
||||
const schema = zodToJsonSchema(tool.schema) as any;
|
||||
const strict = requireStrict(schema);
|
||||
|
||||
// when strict:true, Responses API requires `additionalProperties` either to be true/false or an object with properties
|
||||
const isAdditionalPropertiesEmpty =
|
||||
schema.additionalProperties &&
|
||||
typeof schema.additionalProperties === 'object' &&
|
||||
isObjectEmpty(schema.additionalProperties);
|
||||
if (isAdditionalPropertiesEmpty && strict) {
|
||||
schema.additionalProperties = false;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'function',
|
||||
name: tool.name,
|
||||
parameters: schema,
|
||||
strict,
|
||||
description: tool.description,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getChatMessages(memory: BufferWindowMemory): Promise<BaseMessage[]> {
|
||||
return (await memory.loadMemoryVariables({}))[memory.memoryKey] as BaseMessage[];
|
||||
}
|
||||
Reference in New Issue
Block a user