openai / openai-node

The official Node.js / Typescript library for the OpenAI API
https://www.npmjs.com/package/openai
Apache License 2.0
7.65k stars 821 forks source link

ReferenceError: fetch is not defined in import OpenAI from 'openai' #304

Closed JackLazenbyZigzag closed 10 months ago

JackLazenbyZigzag commented 1 year ago

Hello there! There seems to have been a few issues around this that have been resolved recently, but I'm still getting it so thought I would share just in case it's something different.

Confirm this is a Node library issue and not an underlying OpenAI API issue

Describe the bug

When building and running the OpenAI library in our NestJS API, we can do so locally with no issue ; we can communicate with it and get responses etc. However, we have a Jest testing suite, and when trying to test, it fails with:

ReferenceError: fetch is not defined

  1 | import {Injectable} from "@nestjs/common";
  2 | import {ChatCompletionMessageParam} from "openai/resources/chat";
> 3 | import OpenAI from 'openai';

at Object. (../../../../node_modules/openai/_shims/fetch.js:8:17) at Object. (../../../../node_modules/openai/core.js:64:17) at Object. (../../../../node_modules/openai/src/index.ts:116:7)

To Reproduce

  1. Create a NestJS API
  2. Install the openai library via npm
  3. Create a controller that returns a response when a message is passed through via POST
  4. Create a test suite in Jest to test this endpoint
  5. Error occurs

Code snippets

No response

OS

macOS

Node version

Node v18.17.1

Library version

openai 4.6.0

rattrayalex commented 1 year ago

Thanks for reporting @JackLazenbyZigzag . Can you share your jest config, tsconfig, and package.json?

JackLazenbyZigzag commented 1 year ago

No problem at all @rattrayalex! We use Nx to manage our projects via a monorepo setup so I will share the jest and ts configs for the particular project that is failing.

Here they are:

package.json:

{
    "dependencies": {
        "@angular/animations": "^15.2.0",
        "@angular/common": "^15.2.0",
        "@angular/compiler": "^15.0.0",
        "@angular/core": "^15.2.0",
        "@angular/fire": "^7.6.0",
        "@angular/forms": "^15.2.0",
        "@angular/platform-browser": "^15.2.0",
        "@angular/platform-browser-dynamic": "^15.2.0",
        "@angular/router": "^15.2.0",
        "@awesome-cordova-plugins/adjust": "^6.4.0",
        "@awesome-cordova-plugins/core": "^6.4.0",
        "@awesome-cordova-plugins/mixpanel": "^6.4.0",
        "@awesome-cordova-plugins/purchases": "^6.4.0",
        "@awesome-cordova-plugins/social-sharing": "^6.4.0",
        "@awesome-cordova-plugins/status-bar": "^6.4.0",
        "@azure/identity": "^3.3.0",
        "@azure/keyvault-secrets": "^4.7.0",
        "@azure/service-bus": "^7.9.0",
        "@capacitor/app": "^5.0.6",
        "@capacitor/camera": "^5.0.6",
        "@capacitor/core": "^5.0.6",
        "@capacitor/device": "^5.0.6",
        "@capacitor/haptics": "^5.0.6",
        "@capacitor/network": "^5.0.6",
        "@capacitor/preferences": "^5.0.6",
        "@contentful/rich-text-html-renderer": "^16.1.1",
        "@ionic/angular": "^6.7.0",
        "@microsoft/applicationinsights-web": "^3.0.2",
        "@nestjs/axios": "^0.0.8",
        "@nestjs/common": "^9.4.0",
        "@nestjs/config": "^2.3.1",
        "@nestjs/core": "^9.2.0",
        "@nestjs/cqrs": "^9.0.3",
        "@nestjs/event-emitter": "^1.4.1",
        "@nestjs/jwt": "^10.0.3",
        "@nestjs/passport": "^9.0.3",
        "@nestjs/platform-express": "^9.4.0",
        "@nestjs/schedule": "^2.2.1",
        "@nestjs/typeorm": "^9.0.1",
        "@nestjs/websockets": "^9.4.0",
        "@ngneat/until-destroy": "^10.0.0",
        "@ngrx/effects": "13.2.0",
        "@ngrx/eslint-plugin": "^16.2.0",
        "@ngrx/router-store": "15.4.0",
        "@ngrx/store": "13.2.0",
        "@ngx-translate/core": "^14.0.0",
        "app-root-path": "^3.1.0",
        "applicationinsights": "^2.7.3",
        "argon2": "^0.29.1",
        "azure-devops-node-api": "^12.1.0",
        "cache-manager": "^5.2.3",
        "canvas-confetti": "^1.6.0",
        "capacitor-native-settings": "^5.0.1",
        "capacitor-rate-app": "^4.0.3",
        "class-transformer": "^0.5.1",
        "class-validator": "^0.14.0",
        "com.adjust.sdk": "^4.32.0",
        "compass-mixins": "^0.12.12",
        "configcat-js": "^8.1.1",
        "contentful": "^9.3.5",
        "csv-parse": "5.5.0",
        "currency-symbol-map": "^5.1.0",
        "es6-promise-plugin": "^4.2.2",
        "express": "^4.17.3",
        "firebase": "^9.23.0",
        "firebase-admin": "11.10.1",
        "json-rules-engine": "^6.4.2",
        "jwks-rsa": "^2.1.3",
        "minisearch": "^6.1.0",
        "mixpanel-browser": "^2.47.0",
        "mssql": "^10.0.0",
        "ng-circle-progress": "^1.7.1",
        "ngrx-forms": "^8.0.0",
        "ngx-sse-client": "^3.0.0",
        "node-ipinfo": "^3.4.2",
        "onesignal-cordova-plugin": "^3.3.1",
        "onesignal-node": "^3.4.0",
        "openai": "^4.6.0",
        "passport": "^0.6.0",
        "passport-azure-ad": "^4.3.5",
        "passport-headerapikey": "^1.2.2",
        "passport-jwt": "^4.0.1",
        "qs": "^6.11.2",
        "reflect-metadata": "^0.1.13",
        "rxjs": "^7.8.1",
        "seedrandom": "^3.0.5",
        "shallow-equal-object": "^1.1.1",
        "sqlite3": "^5.1.5",
        "stream-chat": "^8.11.0",
        "stream-chat-angular": "^4.35.0",
        "svg-path-properties": "^1.2.0",
        "swiper": "^8.4.7",
        "tedious": "^16.4.0",
        "ts-loader": "^9.4.4",
        "tslib": "^2.6.2",
        "typeorm": "^0.3.17",
        "webpack": "5.88.2",
        "yargs": "^17.3.0",
        "zone.js": "0.13.1"
    },
    "devDependencies": {
        "@angular-devkit/build-angular": "^15.2.0",
        "@angular-eslint/eslint-plugin": "^15.2.0",
        "@angular-eslint/eslint-plugin-template": "^15.2.0",
        "@angular-eslint/template-parser": "^15.2.0",
        "@angular/cli": "^15.2.0",
        "@angular/compiler": "^15.2.0",
        "@angular/compiler-cli": "^15.2.0",
        "@angular/language-service": "^15.2.0",
        "@azure/storage-blob": "^12.15.0",
        "@capacitor/android": "^5.0.0",
        "@capacitor/cli": "^5.0.0",
        "@capacitor/ios": "^5.0.0",
        "@commitlint/cli": "^17.7.1",
        "@commitlint/config-conventional": "^17.7.0",
        "@ionic/angular-toolkit": "^10.0.0",
        "@nestjs/schematics": "^9.0.3",
        "@nestjs/testing": "^9.2.0",
        "@ngneat/spectator": "^14.0.0",
        "@ngrx/schematics": "15.4.0",
        "@ngrx/store-devtools": "15.4.0",
        "@nrwl/cypress": "14.0.5",
        "@nrwl/eslint-plugin-nx": "14.0.5",
        "@nrwl/jest": "14.0.5",
        "@nrwl/linter": "14.0.5",
        "@nrwl/nest": "14.0.5",
        "@nrwl/node": "14.0.5",
        "@nrwl/nx-cloud": "14.0.3",
        "@nrwl/workspace": "^14.0.5",
        "@nxtend/capacitor": "13.0.0",
        "@nxtend/ionic-angular": "13.1.0",
        "@swc/cli": "^0.1.55",
        "@swc/core": "^1.3.83",
        "@swc/jest": "^0.2.29",
        "@types/app-root-path": "^1.2.5",
        "@types/bcryptjs": "^2.4.3",
        "@types/canvas-confetti": "^1.6.1",
        "@types/express": "^4.17.13",
        "@types/jest": "27.4.1",
        "@types/jest-when": "^3.5.2",
        "@types/mixpanel-browser": "^2.47.1",
        "@types/mssql": "^8.1.2",
        "@types/node": "^20.6.0",
        "@types/passport-azure-ad": "^4.0.8",
        "@types/passport-http": "^0.3.8",
        "@types/passport-jwt": "^3.0.9",
        "@types/supertest": "^2.0.12",
        "@types/uuid": "^9.0.3",
        "@types/yargs": "^17.0.7",
        "@typescript-eslint/eslint-plugin": "5.60.1",
        "@typescript-eslint/parser": "5.62.0",
        "axios": "^0.27.2",
        "babel-jest": "29.6.4",
        "contentful-cli": "^2.8.6",
        "contentful-management": "^10.45.0",
        "cypress": "^12.15.0",
        "cypress-localstorage-commands": "^2.2.4",
        "dotenv": "^16.3.1",
        "eslint": "8.48.0",
        "eslint-config-prettier": "9.0.0",
        "eslint-plugin-cypress": "^2.10.3",
        "eslint-plugin-jest": "^26.0.0",
        "husky": "^8.0.3",
        "jasmine-marbles": "0.9.2",
        "jest": "27.5.1",
        "jest-extended": "^2.0.0",
        "jest-mock-extended": "^3.0.4",
        "jest-preset-angular": "13.1.1",
        "jest-when": "^3.6.0",
        "lint-staged": "^14.0.1",
        "ng-mocks": "^14.11.0",
        "node-loader": "^2.0.0",
        "nodemon": "^3.0.1",
        "nx": "^14.1.7",
        "postcss": "^8.4.29",
        "prettier": "2.8.8",
        "supertest": "^6.3.3",
        "ts-jest": "27.1.4",
        "ts-node": "^10.7.0",
        "typescript": "4.9.5"
    }
}

jest.config.js:

module.exports = {
    displayName: 'app-backend-user-e2e',
    preset: '../../../../jest.preset.js',
    globals: {
        'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' },
    },
    transform: {
        '^.+\\.[tj]sx?$': 'ts-jest',
    },
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
    coverageDirectory: '../../../../coverage/libs/app/backend/user-e2e',
};

tsconfig:

{
    "extends": "../../../../tsconfig.base.json",
    "files": [],
    "include": [],
    "references": [
        {
            "path": "./tsconfig.lib.json"
        },
        {
            "path": "./tsconfig.spec.json"
        }
    ]
}

Thanks!

rattrayalex commented 1 year ago

Thanks! It looks like your jest and tsconfig extend base configs, would you mind sharing those as well?

rattrayalex commented 1 year ago

(I believe this may be the same bug mentioned here: https://github.com/openai/openai-node/issues/243#issuecomment-1709123367)

JackLazenbyZigzag commented 1 year ago

@rattrayalex of course! As an FYI, I have obfsucated some things that are business specific to our project, in case it looks a bit empty! :-) We also only extend from a base tsconfig, the jest side of things just uses the rules out of the box that jest suggests.

base tsconfig:

{
    "compileOnSave": false,
    "compilerOptions": {
        "baseUrl": ".",
        "outDir": "./dist/out-tsc",
        "sourceMap": true,
        "declaration": false,
        "module": "commonjs",
        "moduleResolution": "node",
        "emitDecoratorMetadata": true,
        "experimentalDecorators": true,
        "allowSyntheticDefaultImports": true,
        "importHelpers": true,
        "target": "es2015",
        "typeRoots": ["node_modules/@types"],
        "lib": ["es2019", "dom", "es2020.string"],
        "paths": {
            "@angular/*": ["./node_modules/@angular/*"],
        },
        "rootDir": ".",
        "skipLibCheck": true,
        "noUnusedLocals": true,
        "noUnusedParameters": true,
        "noImplicitReturns": true,
        "noImplicitAny": true
    },
    "angularCompilerOptions": {
        "fullTemplateTypeCheck": true,
        "strictInjectionParameters": true
    }
}
rattrayalex commented 1 year ago

great, thanks @JackLazenbyZigzag ! We'll try to take a look at this on Monday.

JackLazenbyZigzag commented 1 year ago

(I believe this may be the same bug mentioned here: #243 (comment))

@rattrayalex I had a look at that prior to creating this and it does look like it could be similar!

Thank you for your help, appreciate it. Have a great rest of your weekend!

jedwards1211 commented 1 year ago

@JackLazenbyZigzag what jest testEnvironment/@jest-environment are you running your tests in, is it jsdom or something else besides 'node'?

We weren't able to reproduce the issue when using the node environment, but we can reproduce using the jsdom environment because jsdom still doesn't polyfill fetch: https://github.com/jsdom/jsdom/issues/1724.

We were able to get chat completion requests working with the jsdom environment by adding import 'cross-fetch/polyfill' before any import from 'openai'. But warning: we couldn't get any file upload methods to work that way. Let me know if this works for you in the short term, we're still investigating if there are better solutions to this.

jedwards1211 commented 1 year ago

Okay we were able to get even file uploads to work in the jsdom environment by using

/**
 * @jest-environment jsdom
 */
import 'formdata-polyfill'
import 'whatwg-fetch'
import OpenAI, { toFile } from 'openai';

in that specific order.

JackLazenbyZigzag commented 1 year ago

Hi @jedwards1211 - thanks for this! I've added the import for cross-fetch/polyfill, and that resolved it for our existing project test suite (so not adding anything new test-wise). However, when trying to test our service specifically that uses the library, it still doesn't like it. I'm getting the below from the import for formdata-polyfill:

TS7016: Could not find a declaration file for module  formdata-polyfill .
/Users/jacklazenby/WebstormProjects/zigzag/node_modules/formdata-polyfill/formdata.min.js
implicitly has an  any  type.

and, if I ignore this and try to test, I get this:

Test suite failed to run

    ReferenceError: FormData is not defined

      at Object.<anonymous> (../../../../node_modules/openai/_shims/form-data.js:5:20)
      at Object.<anonymous> (../../../../node_modules/openai/src/uploads.ts:66:15)`

This only happens when I test a file that is directly using the openai library though. If I don't do that, the test suite now runs correctly.

jedwards1211 commented 1 year ago

@JackLazenbyZigzag would you be able to use whatwg-fetch? We weren't able to get file uploads to work with cross-fetch. Also it seems I was confused and we didn't actually need formdata-polyfill to do file uploads with whatwg-fetch.

JackLazenbyZigzag commented 1 year ago

@jedwards1211 at this point I'm not trying to do any file related things. I've just built a very basic test suite for a basic service.ts file that handles the openai library interaction, like so:

service ts file:

import 'cross-fetch/polyfill';
import 'whatwg-fetch';
import { Injectable } from '@nestjs/common';
import { ChatCompletionMessageParam } from 'openai/resources/chat';
import OpenAI from 'openai';
import { setupMessage } from '../../../domain/openai/openai';

const openai = new OpenAI({
    apiKey: 'someapikey',
});

@Injectable()
export class OpenAIService {
    async getResponse(message: string, userId: string) {
        const messageToSendChat: ChatCompletionMessageParam = {
            content: message,
            role: 'user',
        };

        const response = await openai.chat.completions.create({
            model: 'gpt-3.5-turbo',
            messages: [setupMessage, messageToSendChat],
            user: userId,
        });

        return response.choices[0].message.content;
    }
}

test ts file:


import {OpenAIService} from './openai.service';
import {Test, TestingModule} from '@nestjs/testing';

describe('OpenAIService', () => {
    let service: OpenAIService;

    beforeEach(async () => {
        const module: TestingModule = await Test.createTestingModule({
            providers: [
                OpenAIService,
            ],
        }).compile();

        service = module.get<OpenAIService>(OpenAIService);
    });

    it('should be defined', () => {
        expect(service).toBeDefined();
    });
});

And this gives me the error:

Test suite failed to run

    ReferenceError: FormData is not defined

      at Object.<anonymous> (../../../../node_modules/openai/_shims/form-data.js:5:20)
      at Object.<anonymous> (../../../../node_modules/openai/src/uploads.ts:66:15)
jedwards1211 commented 1 year ago

Huh, in whatever version of the jest jsdom environment I have, FormData is defined...I think we could rework our shims to where you only get an error like that if you try to do a file upload.

Btw you only need one of cross-fetch/polyfill and whatwg-fetch, they're both polyfills for the fetch API.

JackLazenbyZigzag commented 1 year ago

@jedwards1211 I have removed whatwg-fetch for now, and removed the test file. I can re-add it once a fix or workaround has been identified :-)

rattrayalex commented 1 year ago

@JackLazenbyZigzag did you install 'formdata-polyfill' before importing it?

Have you tried using the node jest environment instead of jsdom for your backend tests?

JackLazenbyZigzag commented 1 year ago

@rattrayalex yes, I have installed 'formdata-polyfill before importing it.

Also, just re-checked our jest.config for this test suite and it was always using node. Apologies for the mix up!

If I change it to jsdom, then it does pass. Obviously this isn't ideal as it could increase test run speed, and we're running this in a backend environment which shouldn't require jsdom for our needs.

jedwards1211 commented 1 year ago

If you can create a repository that reproduces the issue it would be the most helpful, otherwise, to make sure we're on the same page, you're able to reproduce the fetch is not defined error under all of the following conditions?

  1. jest.config has testEnvironment: 'node'
  2. the test file has
    /**
     * @jest-environment node
     */

    (or doesn't have a @jest-environment pragma)

  3. you're running in Node >= 16

Can you also share your jest.preset.js and any related files? I wonder if some jest plugin or config is affecting its module resolution.

JackLazenbyZigzag commented 1 year ago

@jedwards1211 I can get a test repo put together in the next day or so if needed, but in the meantime, in response to your questions regarding environment. In our case:

  1. jest.config has testEnvironment: 'node' for all of our configs. (we have since changed one to jsdom to allow these tests to pass for openai, but when it fails it is when it is set to node)
  2. the test file does not have any other specification of environment - it picks it up from the config
  3. We are running v18.17.1 of node.

Jest.preset is pretty small, but here it is:

const nxPreset = require('@nrwl/jest/preset');

module.exports = {
    ...nxPreset,
    testPathIgnorePatterns: ['<rootDir>/src/environments/environment.test.ts'],
};

jest.config.js for this library:

module.exports = {
    displayName: 'app-backend-user',
    preset: '../../../../jest.preset.js',
    globals: {
        'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json' },
    },
    testEnvironment: 'jsdom',
    transform: {
        '^.+\\.[tj]sx?$': 'ts-jest',
    },
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
    coverageDirectory: '../../../../coverage/libs/app/backend/user',
};
jedwards1211 commented 1 year ago

Okay I was able to repro on Jest 27, I was testing on Jest 29 and didn't notice that was different from your project until now. I think support for package.json exports must have been added to Jest in a later version. The solution we're working on is to have you import 'openai/shims/node' in cases like this.

rattrayalex commented 1 year ago

@dilizarov @JackLazenbyZigzag are you able to upgrade to Jest 29? If so, can you confirm the issue does not appear on that version?

JackLazenbyZigzag commented 1 year ago

@rattrayalex we have a lot of jest dependencies in our project that mean we can't upgrade to the latest version of Jest from 27 without a lot of extra work, which I can't quickly do to investigate this I'm afraid. I will see what I can do over the next few days, though.

rattrayalex commented 1 year ago

Ah ok, don't worry about it then. We hope to have a workaround out within a day or two.

ciryon commented 1 year ago

We got the same problem here in AWS Lambda NodeJS 16 runtime.

rattrayalex commented 1 year ago

@ciryon can you share a minimal repo that reproduces the issue you're seeing?

(Note that Node 16 is now EOL and we'll be dropping support for it soon)

ciryon commented 1 year ago

@rattrayalex not easily made into a minimal repo I'm afraid. The issue appeared when upgrading from version 3 of the library, and I've tried to rollback from latest all the way to 4.0.1 and the same problem still appears.

rattrayalex commented 1 year ago

Gotcha. And you only experience the problem on Lambda, not locally?

ciryon commented 1 year ago

Yes, it works locally!

ciryon commented 1 year ago

I can also report that it works if I use the Node 18 runtime on AWS Lambda.

rattrayalex commented 1 year ago

Ah, great. Thanks Christian! Glad you've found something that works – we'll be dropping support Node 16 support in the next release.

yardenGerecht commented 10 months ago

For me updating all jest libraries to the latest versions helped.