From 8bde567cfa26a42cfe89bbf8c6ed1356a6ad1892 Mon Sep 17 00:00:00 2001 From: Deeksha Sinha <88374536+deekshas8@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:54:07 +0200 Subject: [PATCH] chore: Restructure packages (#61) * restructure * lint * test restructure+workflows * lint * add temporary workaround * remove log * change tsconfig setup * mock env in global setup * fix tsconfig.test * fix test * add core to canary release * add ignored packages to config.json * Update test-util/mock-http.ts Co-authored-by: Marika Marszalkowski <868536+marikaner@users.noreply.github.com> * Update packages/core/src/context.test.ts Co-authored-by: Marika Marszalkowski <868536+marikaner@users.noreply.github.com> * review * fix: Changes from lint * update docs * docs * fix: Changes from lint --------- Co-authored-by: Tom Frenken Co-authored-by: Marika Marszalkowski <868536+marikaner@users.noreply.github.com> Co-authored-by: cloud-sdk-js --- .changeset/config.json | 2 +- .github/workflows/build.yml | 2 +- .github/workflows/e2e-test.yaml | 4 +- .github/workflows/publish-canary.yml | 1 + eslint.config.js | 5 + global-test-setup.ts | 14 ++ global-test-teardown.ts | 6 + jest.config.mjs | 5 +- package.json | 20 +-- packages/ai-core/jest.config.mjs | 4 +- packages/core/README.md | 18 +++ packages/core/jest.config.mjs | 5 + packages/core/package.json | 33 +++++ packages/core/src/context.test.ts | 22 +++ .../src/core => core/src}/context.ts | 10 +- .../src/core => core/src}/http-client.test.ts | 31 ++--- .../src/core => core/src}/http-client.ts | 0 packages/core/src/index.ts | 7 + packages/core/tsconfig.json | 11 ++ packages/gen-ai-hub/package.json | 1 + packages/gen-ai-hub/src/client/interface.ts | 2 +- .../src/client/openai/openai-client.test.ts | 72 ++++------ .../src/client/openai/openai-client.ts | 2 +- .../src/client/openai/openai-types.ts | 2 +- packages/gen-ai-hub/src/core/context.test.ts | 37 ------ packages/gen-ai-hub/src/core/index.ts | 2 - packages/gen-ai-hub/src/index.ts | 5 +- .../orchestration-client.test.ts | 45 +++---- .../src/orchestration/orchestration-client.ts | 2 +- .../src/orchestration/orchestration-types.ts | 2 +- .../gen-ai-hub/src/test-util/mock-context.ts | 79 ----------- .../gen-ai-hub/src/test-util/mock-http.ts | 57 -------- ...enai-chat-completion-success-response.json | 0 .../openai-embeddings-success-response.json | 0 .../openai/openai-error-response.json | 0 ...enaihub-chat-completion-filter-config.json | 0 ...aihub-chat-completion-message-history.json | 0 ...ihub-chat-completion-success-response.json | 0 packages/gen-ai-hub/tsconfig.json | 3 +- pnpm-lock.yaml | 57 +++++++- pnpm-workspace.yaml | 5 +- sample-code/src/aiservice.ts | 1 - scripts/update-imports.ts | 68 ---------- test-util/mock-http.ts | 125 ++++++++++++++++++ test-util/mock-jwt.ts | 23 ++++ tests/e2e-tests/package.json | 5 +- tests/e2e-tests/src/ai-core.test.ts | 2 +- tests/type-tests/package.json | 20 +++ tests/type-tests/{ => test}/context.test-d.ts | 2 +- .../{ => test}/http-client.test-d.ts | 2 +- tests/type-tests/{ => test}/openai.test-d.ts | 6 +- .../{ => test}/orchestration.test-d.ts | 3 +- tsconfig.test.json | 9 ++ 53 files changed, 453 insertions(+), 386 deletions(-) create mode 100644 global-test-setup.ts create mode 100644 global-test-teardown.ts create mode 100644 packages/core/README.md create mode 100644 packages/core/jest.config.mjs create mode 100644 packages/core/package.json create mode 100644 packages/core/src/context.test.ts rename packages/{gen-ai-hub/src/core => core/src}/context.ts (89%) rename packages/{gen-ai-hub/src/core => core/src}/http-client.test.ts (59%) rename packages/{gen-ai-hub/src/core => core/src}/http-client.ts (100%) create mode 100644 packages/core/src/index.ts create mode 100644 packages/core/tsconfig.json delete mode 100644 packages/gen-ai-hub/src/core/context.test.ts delete mode 100644 packages/gen-ai-hub/src/core/index.ts delete mode 100644 packages/gen-ai-hub/src/test-util/mock-context.ts delete mode 100644 packages/gen-ai-hub/src/test-util/mock-http.ts rename packages/gen-ai-hub/{src/test-util/mock-data => test}/openai/openai-chat-completion-success-response.json (100%) rename packages/gen-ai-hub/{src/test-util/mock-data => test}/openai/openai-embeddings-success-response.json (100%) rename packages/gen-ai-hub/{src/test-util/mock-data => test}/openai/openai-error-response.json (100%) rename packages/gen-ai-hub/{src/test-util/mock-data => test}/orchestration/genaihub-chat-completion-filter-config.json (100%) rename packages/gen-ai-hub/{src/test-util/mock-data => test}/orchestration/genaihub-chat-completion-message-history.json (100%) rename packages/gen-ai-hub/{src/test-util/mock-data => test}/orchestration/genaihub-chat-completion-success-response.json (100%) delete mode 100644 scripts/update-imports.ts create mode 100644 test-util/mock-http.ts create mode 100644 test-util/mock-jwt.ts create mode 100644 tests/type-tests/package.json rename tests/type-tests/{ => test}/context.test-d.ts (63%) rename tests/type-tests/{ => test}/http-client.test-d.ts (88%) rename tests/type-tests/{ => test}/openai.test-d.ts (78%) rename tests/type-tests/{ => test}/orchestration.test-d.ts (92%) create mode 100644 tsconfig.test.json diff --git a/.changeset/config.json b/.changeset/config.json index 31ec3812..6b628ff7 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -7,5 +7,5 @@ "access": "public", "baseBranch": "main", "updateInternalDependencies": "patch", - "ignore": [] + "ignore": ["@sap-ai-sdk/sample-code", "@sap-ai-sdk/e2e-tests", "@sap-ai-sdk/type-tests"] } diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c666762..9d07827f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: node-version: ${{ matrix.node-version }} cache: 'pnpm' - run: pnpm i --frozen-lockfile - - run: pnpm test + - run: pnpm test:unit - run: pnpm test:type checks: diff --git a/.github/workflows/e2e-test.yaml b/.github/workflows/e2e-test.yaml index 3f75b219..2011b156 100644 --- a/.github/workflows/e2e-test.yaml +++ b/.github/workflows/e2e-test.yaml @@ -52,9 +52,7 @@ jobs: done wget -qO- -S --content-on-error localhost:8080 - name: "Execute E2E Tests" - working-directory: ./tests/e2e-tests - run: | - pnpm run e2e-test + run: pnpm test:e2e - name: "Slack Notification" if: failure() uses: slackapi/slack-github-action@v1.26.0 diff --git a/.github/workflows/publish-canary.yml b/.github/workflows/publish-canary.yml index 20ab2bcf..3e9896d1 100644 --- a/.github/workflows/publish-canary.yml +++ b/.github/workflows/publish-canary.yml @@ -35,6 +35,7 @@ jobs: cat <> .changeset/canary-release-changeset.md --- '@sap-ai-sdk/ai-core': patch + '@sap-ai-sdk/core': patch '@sap-ai-sdk/gen-ai-hub': patch --- diff --git a/eslint.config.js b/eslint.config.js index ccd444fd..a5371107 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,6 +2,11 @@ import flatConfig from '@sap-cloud-sdk/eslint-config/flat-config.js'; export default [ ...flatConfig, + { + // Estlint flat config is not supported by eslint-plugin-import. + // https://github.com/import-js/eslint-plugin-import/issues/2556 + rules: { 'import/namespace': 'off'} + }, { ignores: ['**/dist/**/*', '**/coverage/**/*', 'packages/ai-core/src/client/**/*'], }, diff --git a/global-test-setup.ts b/global-test-setup.ts new file mode 100644 index 00000000..8c14b538 --- /dev/null +++ b/global-test-setup.ts @@ -0,0 +1,14 @@ +/** + * This file is used to mock the environment variables that are required for the tests. + */ +export default async function mockAiCoreEnvVariable(): Promise { + const aiCoreServiceCredentials = { + clientid: 'clientid', + clientsecret: 'clientsecret', + url: 'https://example.authentication.eu12.hana.ondemand.com', + serviceurls: { + AI_API_URL: 'https://api.ai.ml.hana.ondemand.com' + } + }; + process.env['aicore'] = JSON.stringify(aiCoreServiceCredentials); +} diff --git a/global-test-teardown.ts b/global-test-teardown.ts new file mode 100644 index 00000000..42e8c459 --- /dev/null +++ b/global-test-teardown.ts @@ -0,0 +1,6 @@ +/** + * This file is used to run code after all tests have been run. + */ +export default async function tearDown(): Promise { + delete process.env.aicore; +} diff --git a/jest.config.mjs b/jest.config.mjs index 2bca7c35..6fa0d469 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -54,10 +54,10 @@ const config = { // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites - // globalSetup: undefined, + globalSetup: '../../global-test-setup.ts', // A path to a module which exports an async function that is triggered once after all test suites - // globalTeardown: undefined, + globalTeardown: '../../global-test-teardown.ts', // A set of global variables that need to be available in all test environments // globals: {}, @@ -174,6 +174,7 @@ const config = { 'ts-jest', { useESM: true, + tsconfig: '../../tsconfig.test.json', }, ], }, diff --git a/package.json b/package.json index 53b902ce..24d97603 100644 --- a/package.json +++ b/package.json @@ -7,24 +7,25 @@ "repository": "github:SAP/ai-sdk-js", "private": true, "type": "module", - "types": "tests/type-tests", - "tsd": { - "directory": "tests/type-tests" - }, "scripts": { "postinstall": "pnpm compile", "compile": "pnpm -r -w=false run compile", - "test": "NODE_OPTIONS=--experimental-vm-modules pnpm -r run test", - "test:type": "tsd", + "test:unit": "pnpm -r -F=./packages/** test", + "test:type": "pnpm -F=@sap-ai-sdk/type-tests test", + "test:e2e": "pnpm -F=@sap-ai-sdk/e2e-tests test", "lint": "pnpm -r run lint", "lint:fix": "pnpm -r run lint:fix", - "generate": "pnpm -r run generate" + "generate": "pnpm -r run generate", + "ai-core": "pnpm -F=@sap-ai-sdk/ai-core", + "gen-ai-hub": "pnpm -F=@sap-ai-sdk/gen-ai-hub", + "core": "pnpm -F=@sap-ai-sdk/core" }, "devDependencies": { "@changesets/cli": "^2.27.7", "@sap-cloud-sdk/eslint-config": "^3.18.0", "@sap-cloud-sdk/connectivity": "^3.18.0", "@sap-cloud-sdk/http-client": "^3.18.0", + "@sap-ai-sdk/core": "workspace:^", "@types/jest": "^29.5.12", "@types/node": "^20.14.15", "@jest/globals": "^29.5.12", @@ -34,7 +35,8 @@ "prettier": "^3.3.3", "ts-jest": "^29.2.4", "ts-node": "^10.9.2", - "tsd": "^0.31.0", - "typescript": "^5.5.4" + "typescript": "^5.5.4", + "jsonwebtoken": "^9.0.2", + "@types/jsonwebtoken": "^9.0.2" } } diff --git a/packages/ai-core/jest.config.mjs b/packages/ai-core/jest.config.mjs index 3d5aa2fa..596d4a8d 100644 --- a/packages/ai-core/jest.config.mjs +++ b/packages/ai-core/jest.config.mjs @@ -1,7 +1,5 @@ import config from '../../jest.config.mjs'; -const aiCoreConfig = { +export default { ...config, displayName: 'ai-core', }; - -export default aiCoreConfig; diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..8ed23e4b --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,18 @@ +# @sap-ai-sdk/core + +This package contains core utility functions that we reuse in the SDK to set the context and execute HTTP requests. +They are primarily designed for internal usage. + +### Installation + +``` +$ npm install @sap-ai-sdk/core +``` + +### Usage + +The core package is not intended for direct usage. + +## License + +The SAP AI SDK is released under the [Apache License Version 2.0.](http://www.apache.org/licenses/) diff --git a/packages/core/jest.config.mjs b/packages/core/jest.config.mjs new file mode 100644 index 00000000..daae3e2f --- /dev/null +++ b/packages/core/jest.config.mjs @@ -0,0 +1,5 @@ +import config from '../../jest.config.mjs'; +export default { + ...config, + displayName: 'core', +}; diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 00000000..6f9e378a --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,33 @@ +{ + "name": "@sap-ai-sdk/core", + "version": "0.0.0", + "description": "", + "license": "Apache-2.0", + "keywords": [ + "sap-ai-sdk", + "ai-core" + ], + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/**/*.js", + "dist/**/*.js.map", + "dist/**/*.d.ts", + "dist/**/*.d.ts.map" + ], + "scripts": { + "compile": "tsc", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", + "lint": "eslint . && prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -c", + "lint:fix": "eslint . --fix && prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -w --log-level error" + }, + "dependencies": { + "@sap-cloud-sdk/http-client": "^3.18.0", + "@sap-cloud-sdk/connectivity": "^3.18.0", + "@sap-cloud-sdk/util": "^3.18.0" + }, + "devDependencies": { + "typescript": "^5.5.4" + } +} diff --git a/packages/core/src/context.test.ts b/packages/core/src/context.test.ts new file mode 100644 index 00000000..00ceb833 --- /dev/null +++ b/packages/core/src/context.test.ts @@ -0,0 +1,22 @@ +import nock from 'nock'; +import { mockClientCredentialsGrantCall } from '../../../test-util/mock-http.js'; +import { getAiCoreDestination } from './context.js'; + +describe('context', () => { + afterEach(() => { + nock.cleanAll(); + }); + + it('should throw if client credentials are not fetched', async () => { + mockClientCredentialsGrantCall( + { + error: 'unauthorized', + error_description: 'Bad credentials' + }, + 401 + ); + await expect(getAiCoreDestination()).rejects.toThrow( + /Could not fetch client credentials token for service of type "aicore"/ + ); + }); +}); diff --git a/packages/gen-ai-hub/src/core/context.ts b/packages/core/src/context.ts similarity index 89% rename from packages/gen-ai-hub/src/core/context.ts rename to packages/core/src/context.ts index 42dc3281..35b3740a 100644 --- a/packages/gen-ai-hub/src/core/context.ts +++ b/packages/core/src/context.ts @@ -2,12 +2,13 @@ import { createLogger } from '@sap-cloud-sdk/util'; import { Destination, Service, + ServiceCredentials, getServiceBinding, transformServiceBindingToDestination } from '@sap-cloud-sdk/connectivity'; const logger = createLogger({ - package: 'gen-ai-hub', + package: 'core', messageContext: 'context' }); @@ -31,7 +32,8 @@ export async function getAiCoreDestination(): Promise { const aiCoreDestination = await transformServiceBindingToDestination( aiCoreServiceBinding, { - useCache: true + useCache: true, + jwt: { zid: 'dummy-tenant' } } ); return aiCoreDestination; @@ -52,7 +54,9 @@ function getAiCoreServiceKeyFromEnv(): Service | undefined { } } -function parseServiceKeyFromEnv(aiCoreEnv: string | undefined) { +function parseServiceKeyFromEnv( + aiCoreEnv: string | undefined +): ServiceCredentials | undefined { if (aiCoreEnv) { try { return JSON.parse(aiCoreEnv); diff --git a/packages/gen-ai-hub/src/core/http-client.test.ts b/packages/core/src/http-client.test.ts similarity index 59% rename from packages/gen-ai-hub/src/core/http-client.test.ts rename to packages/core/src/http-client.test.ts index 5dd5cfb0..bf608740 100644 --- a/packages/gen-ai-hub/src/core/http-client.test.ts +++ b/packages/core/src/http-client.test.ts @@ -1,26 +1,18 @@ -import { jest } from '@jest/globals'; -import { HttpDestination } from '@sap-cloud-sdk/connectivity'; -import { mockGetAiCoreDestination } from '../test-util/mock-context.js'; -import { mockInference } from '../test-util/mock-http.js'; - -jest.unstable_mockModule('./context.js', () => ({ - getAiCoreDestination: jest.fn(() => - Promise.resolve(mockGetAiCoreDestination()) - ) -})); -const { executeRequest } = await import('./http-client.js'); +import nock from 'nock'; +import { + mockClientCredentialsGrantCall, + mockInference +} from '../../../test-util/mock-http.js'; +import { dummyToken } from '../../../test-util/mock-jwt.js'; +import { executeRequest } from './http-client.js'; describe('http-client', () => { - let destination: HttpDestination; - beforeAll(() => { - destination = mockGetAiCoreDestination(); + mockClientCredentialsGrantCall({ access_token: dummyToken }, 200); }); - - afterAll(() => { - jest.restoreAllMocks(); + afterEach(() => { + nock.cleanAll(); }); - it('should execute a request to the AI Core service', async () => { const mockPrompt = { prompt: 'some test prompt' }; const mockPromptResponse = { completion: 'some test completion' }; @@ -35,8 +27,7 @@ describe('http-client', () => { { data: mockPromptResponse, status: 200 - }, - destination + } ); const res = await executeRequest( { url: '/mock-endpoint', apiVersion: 'mock-api-version' }, diff --git a/packages/gen-ai-hub/src/core/http-client.ts b/packages/core/src/http-client.ts similarity index 100% rename from packages/gen-ai-hub/src/core/http-client.ts rename to packages/core/src/http-client.ts diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..99fb69b0 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,7 @@ +export { + executeRequest, + BaseLlmParametersWithDeploymentId, + BaseLlmParameters, + CustomRequestConfig +} from './http-client.js'; +export { getAiCoreDestination } from './context.js'; diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..d7b4b5b2 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "tsBuildInfoFile": "./dist/.tsbuildinfo", + "composite": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist/**/*", "**/*.test.ts", "node_modules/**/*"] +} diff --git a/packages/gen-ai-hub/package.json b/packages/gen-ai-hub/package.json index 6603bbfe..35bdd898 100644 --- a/packages/gen-ai-hub/package.json +++ b/packages/gen-ai-hub/package.json @@ -27,6 +27,7 @@ "generate:orchestration": "openapi-generator --overwrite --generateESM -i ./src/orchestration/spec/api.yaml -o ./src/orchestration/client && pnpm lint:fix" }, "dependencies": { + "@sap-ai-sdk/core": "workspace:^", "@sap-cloud-sdk/http-client": "^3.18.0", "@sap-cloud-sdk/connectivity": "^3.18.0", "@sap-cloud-sdk/util": "^3.18.0", diff --git a/packages/gen-ai-hub/src/client/interface.ts b/packages/gen-ai-hub/src/client/interface.ts index 75c30350..d67e82de 100644 --- a/packages/gen-ai-hub/src/client/interface.ts +++ b/packages/gen-ai-hub/src/client/interface.ts @@ -1,4 +1,4 @@ -import { BaseLlmParameters, CustomRequestConfig } from '../core/http-client.js'; +import { BaseLlmParameters, CustomRequestConfig } from '@sap-ai-sdk/core'; import { BaseLlmOutput } from './types.js'; /** diff --git a/packages/gen-ai-hub/src/client/openai/openai-client.test.ts b/packages/gen-ai-hub/src/client/openai/openai-client.test.ts index 3d179dda..89d9faad 100644 --- a/packages/gen-ai-hub/src/client/openai/openai-client.test.ts +++ b/packages/gen-ai-hub/src/client/openai/openai-client.test.ts @@ -1,12 +1,11 @@ import nock from 'nock'; -import { jest } from '@jest/globals'; -import { HttpDestination } from '@sap-cloud-sdk/connectivity'; -import { mockGetAiCoreDestination } from '../../test-util/mock-context.js'; +import { BaseLlmParametersWithDeploymentId } from '@sap-ai-sdk/core'; import { - BaseLlmParametersWithDeploymentId, - EndpointOptions -} from '../../core/http-client.js'; -import { mockInference, parseMockResponse } from '../../test-util/mock-http.js'; + mockClientCredentialsGrantCall, + mockInference, + parseMockResponse +} from '../../../../../test-util/mock-http.js'; +import { dummyToken } from '../../../../../test-util/mock-jwt.js'; import { OpenAiChatCompletionOutput, OpenAiChatCompletionParameters, @@ -14,41 +13,29 @@ import { OpenAiEmbeddingOutput, OpenAiEmbeddingParameters } from './openai-types.js'; -jest.unstable_mockModule('../../core/context.js', () => ({ - getAiCoreDestination: jest.fn(() => - Promise.resolve(mockGetAiCoreDestination()) - ) -})); -const { OpenAiClient } = await import('./openai-client.js'); +import { OpenAiClient } from './openai-client.js'; describe('openai client', () => { - let destination: HttpDestination; const deploymentConfiguration: BaseLlmParametersWithDeploymentId = { deploymentId: 'deployment-id' }; - let chatCompletionEndpoint: EndpointOptions; - let embeddingsEndpoint: EndpointOptions; - - beforeAll(() => { - destination = mockGetAiCoreDestination(); - - chatCompletionEndpoint = { - url: 'chat/completions', - apiVersion: '2024-02-01' - }; + const chatCompletionEndpoint = { + url: 'chat/completions', + apiVersion: '2024-02-01' + }; + const embeddingsEndpoint = { + url: 'embeddings', + apiVersion: '2024-02-01' + }; - embeddingsEndpoint = { - url: 'embeddings', - apiVersion: '2024-02-01' - }; - }); + const client = new OpenAiClient(); - afterEach(() => { - nock.cleanAll(); + beforeAll(() => { + mockClientCredentialsGrantCall({ access_token: dummyToken }, 200); }); afterAll(() => { - jest.restoreAllMocks(); + nock.cleanAll(); }); describe('chatCompletion', () => { @@ -78,13 +65,11 @@ describe('openai client', () => { data: mockResponse, status: 200 }, - destination, chatCompletionEndpoint ); - expect(new OpenAiClient().chatCompletion(request)).resolves.toEqual( - mockResponse - ); + const response = await client.chatCompletion(request); + expect(response).toEqual(mockResponse); }); it('throws on bad request', async () => { @@ -106,13 +91,10 @@ describe('openai client', () => { data: mockResponse, status: 400 }, - destination, chatCompletionEndpoint ); - await expect( - new OpenAiClient().chatCompletion(request) - ).rejects.toThrow(); + expect(client.chatCompletion(request)).rejects.toThrow(); }); }); @@ -136,13 +118,10 @@ describe('openai client', () => { data: mockResponse, status: 200 }, - destination, embeddingsEndpoint ); - - expect(new OpenAiClient().embeddings(request)).resolves.toEqual( - mockResponse - ); + const response = await client.embeddings(request); + expect(response).toEqual(mockResponse); }); it('throws on bad request', async () => { @@ -164,11 +143,10 @@ describe('openai client', () => { data: mockResponse, status: 400 }, - destination, embeddingsEndpoint ); - expect(new OpenAiClient().embeddings(request)).rejects.toThrow(); + expect(client.embeddings(request)).rejects.toThrow(); }); }); }); diff --git a/packages/gen-ai-hub/src/client/openai/openai-client.ts b/packages/gen-ai-hub/src/client/openai/openai-client.ts index 8cae3ce7..c1eb9180 100644 --- a/packages/gen-ai-hub/src/client/openai/openai-client.ts +++ b/packages/gen-ai-hub/src/client/openai/openai-client.ts @@ -2,7 +2,7 @@ import { BaseLlmParameters, CustomRequestConfig, executeRequest -} from '../../core/index.js'; +} from '@sap-ai-sdk/core'; import { BaseClient } from '../interface.js'; import { OpenAiChatCompletionParameters, diff --git a/packages/gen-ai-hub/src/client/openai/openai-types.ts b/packages/gen-ai-hub/src/client/openai/openai-types.ts index dbbbcd15..06e04b0c 100644 --- a/packages/gen-ai-hub/src/client/openai/openai-types.ts +++ b/packages/gen-ai-hub/src/client/openai/openai-types.ts @@ -1,4 +1,4 @@ -import { BaseLlmParameters } from '../../core/http-client.js'; +import { BaseLlmParameters } from '@sap-ai-sdk/core'; /** * OpenAI system message. diff --git a/packages/gen-ai-hub/src/core/context.test.ts b/packages/gen-ai-hub/src/core/context.test.ts deleted file mode 100644 index 6a5c7c9a..00000000 --- a/packages/gen-ai-hub/src/core/context.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - mockAiCoreEnvVariable, - mockClientCredentialsGrantCall -} from '../test-util/mock-context.js'; -import { getAiCoreDestination } from './context.js'; - -describe('context', () => { - it('should throw if ai-core binding is not found', async () => { - await expect(getAiCoreDestination()).rejects.toThrow( - 'Could not find service credentials for AI Core. Please check the service binding.' - ); - }); - - it('should throw for ill formatted JSON', async () => { - process.env.aicore = 'Improper JSON string'; - - await expect(getAiCoreDestination()).rejects - .toThrowErrorMatchingInlineSnapshot(` - "Error in parsing service key from the "aicore" environment variable. - Cause: Unexpected token 'I', "Improper JSON string" is not valid JSON" - `); - }); - - it('should throw if client credentials are not fetched', async () => { - mockAiCoreEnvVariable(); - mockClientCredentialsGrantCall( - { - error: 'unauthorized', - error_description: 'Bad credentials' - }, - 401 - ); - await expect(getAiCoreDestination()).rejects.toThrow( - /Could not fetch client credentials token for service of type "aicore"/ - ); - }); -}); diff --git a/packages/gen-ai-hub/src/core/index.ts b/packages/gen-ai-hub/src/core/index.ts deleted file mode 100644 index daa75097..00000000 --- a/packages/gen-ai-hub/src/core/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './http-client.js'; -export * from './context.js'; diff --git a/packages/gen-ai-hub/src/index.ts b/packages/gen-ai-hub/src/index.ts index 96aed6c6..73e14135 100644 --- a/packages/gen-ai-hub/src/index.ts +++ b/packages/gen-ai-hub/src/index.ts @@ -5,13 +5,12 @@ export { OpenAiEmbeddingOutput, OpenAiChatCompletionOutput } from './client/index.js'; -export { CustomRequestConfig, BaseLlmParameters } from './core/index.js'; export { GenAiHubClient, GenAiHubCompletionParameters, GenAiHubCompletionResponse, PromptConfig, LlmConfig, - ChatMessages + ChatMessages, + CompletionPostResponse } from './orchestration/index.js'; -export { getAiCoreDestination } from './core/index.js'; diff --git a/packages/gen-ai-hub/src/orchestration/orchestration-client.test.ts b/packages/gen-ai-hub/src/orchestration/orchestration-client.test.ts index 2c067c06..adbc9c63 100644 --- a/packages/gen-ai-hub/src/orchestration/orchestration-client.test.ts +++ b/packages/gen-ai-hub/src/orchestration/orchestration-client.test.ts @@ -1,38 +1,30 @@ import nock from 'nock'; -import { jest } from '@jest/globals'; -import { HttpDestination } from '@sap-cloud-sdk/connectivity'; -import { mockGetAiCoreDestination } from '../test-util/mock-context.js'; -import { mockInference, parseMockResponse } from '../test-util/mock-http.js'; -import { BaseLlmParametersWithDeploymentId } from '../core/index.js'; +import { BaseLlmParametersWithDeploymentId } from '@sap-ai-sdk/core'; +import { + mockClientCredentialsGrantCall, + mockInference, + parseMockResponse +} from '../../../../test-util/mock-http.js'; +import { dummyToken } from '../../../../test-util/mock-jwt.js'; import { CompletionPostResponse } from './client/api/index.js'; import { GenAiHubCompletionParameters } from './orchestration-types.js'; +import { + GenAiHubClient, + constructCompletionPostRequest +} from './orchestration-client.js'; import { azureContentFilter } from './orchestration-filter-utility.js'; -jest.unstable_mockModule('../core/context.js', () => ({ - getAiCoreDestination: jest.fn(() => - Promise.resolve(mockGetAiCoreDestination()) - ) -})); - -const { GenAiHubClient, constructCompletionPostRequest } = await import( - './orchestration-client.js' -); describe('GenAiHubClient', () => { - let destination: HttpDestination; const client = new GenAiHubClient(); const deploymentConfiguration: BaseLlmParametersWithDeploymentId = { deploymentId: 'deployment-id' }; beforeAll(() => { - destination = mockGetAiCoreDestination(); - }); - - afterEach(() => { - nock.cleanAll(); + mockClientCredentialsGrantCall({ access_token: dummyToken }, 200); }); afterAll(() => { - jest.restoreAllMocks(); + nock.cleanAll(); }); it('calls chatCompletion with minimum configuration', async () => { @@ -63,12 +55,12 @@ describe('GenAiHubClient', () => { data: mockResponse, status: 200 }, - destination, { url: 'completion' } ); - expect(client.chatCompletion(request)).resolves.toEqual(mockResponse); + const response = await client.chatCompletion(request); + expect(response).toEqual(mockResponse); }); it('calls chatCompletion with filter configuration supplied using convenience function', async () => { @@ -105,7 +97,6 @@ describe('GenAiHubClient', () => { data: mockResponse, status: 200 }, - destination, { url: 'completion' } @@ -167,7 +158,6 @@ describe('GenAiHubClient', () => { data: mockResponse, status: 200 }, - destination, { url: 'completion' } @@ -217,12 +207,11 @@ describe('GenAiHubClient', () => { data: mockResponse, status: 200 }, - destination, { url: 'completion' } ); - - expect(client.chatCompletion(request)).resolves.toEqual(mockResponse); + const response = await client.chatCompletion(request); + expect(response).toEqual(mockResponse); }); }); diff --git a/packages/gen-ai-hub/src/orchestration/orchestration-client.ts b/packages/gen-ai-hub/src/orchestration/orchestration-client.ts index 78c5ed57..e9c64df8 100644 --- a/packages/gen-ai-hub/src/orchestration/orchestration-client.ts +++ b/packages/gen-ai-hub/src/orchestration/orchestration-client.ts @@ -1,4 +1,4 @@ -import { executeRequest, CustomRequestConfig } from '../core/index.js'; +import { executeRequest, CustomRequestConfig } from '@sap-ai-sdk/core'; import { CompletionPostRequest } from './client/api/schema/index.js'; import { GenAiHubCompletionParameters, diff --git a/packages/gen-ai-hub/src/orchestration/orchestration-types.ts b/packages/gen-ai-hub/src/orchestration/orchestration-types.ts index 3e18b366..74c012c0 100644 --- a/packages/gen-ai-hub/src/orchestration/orchestration-types.ts +++ b/packages/gen-ai-hub/src/orchestration/orchestration-types.ts @@ -1,4 +1,4 @@ -import { BaseLlmParameters } from '../core/index.js'; +import { BaseLlmParameters } from '@sap-ai-sdk/core'; import { ChatMessages, CompletionPostResponse, diff --git a/packages/gen-ai-hub/src/test-util/mock-context.ts b/packages/gen-ai-hub/src/test-util/mock-context.ts deleted file mode 100644 index 6148aebe..00000000 --- a/packages/gen-ai-hub/src/test-util/mock-context.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { - DestinationAuthToken, - HttpDestination, - ServiceCredentials -} from '@sap-cloud-sdk/connectivity'; -import nock from 'nock'; - -export const aiCoreServiceBinding = { - label: 'aicore', - credentials: { - clientid: 'clientid', - clientsecret: 'clientsecret', - url: 'https://example.authentication.eu12.hana.ondemand.com', - identityzone: 'examplezone', - identityzoneid: 'examplezoneid', - appname: 'appname', - serviceurls: { - AI_API_URL: 'https://api.ai.ml.hana.ondemand.com' - } - } -}; - -export const aiCoreDestination = { - url: 'https://api.ai.ml.hana.ondemand.com' -}; - -export function createDestinationTokens( - token: string = 'mock-token', - expiresIn?: string -): { authTokens: DestinationAuthToken[] } { - return { - authTokens: [ - { - value: token, - type: 'bearer', - expiresIn, - http_header: { key: 'Authorization', value: `Bearer ${token}` }, - error: null - } - ] - }; -} - -export function mockAiCoreEnvVariable(): void { - process.env['aicore'] = JSON.stringify(aiCoreServiceBinding.credentials); -} - -export function mockGetAiCoreDestination( - destination = aiCoreDestination -): HttpDestination { - const mockDestination: HttpDestination = { - ...destination, - authentication: 'OAuth2ClientCredentials', - ...createDestinationTokens() - }; - return mockDestination; -} - -export function mockClientCredentialsGrantCall( - response: any, - responseCode: number, - serviceCredentials: ServiceCredentials = aiCoreServiceBinding.credentials, - delay = 0 -): nock.Scope { - return nock(serviceCredentials.url, { - reqheaders: { - 'Content-Type': 'application/x-www-form-urlencoded', - Accept: 'application/json' - } - }) - .post('/oauth/token', { - grant_type: 'client_credentials', - client_id: serviceCredentials.clientid, - client_secret: serviceCredentials.clientsecret, - response_type: 'token' - }) - .delay(delay) - .reply(responseCode, response); -} diff --git a/packages/gen-ai-hub/src/test-util/mock-http.ts b/packages/gen-ai-hub/src/test-util/mock-http.ts deleted file mode 100644 index b40aed58..00000000 --- a/packages/gen-ai-hub/src/test-util/mock-http.ts +++ /dev/null @@ -1,57 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { HttpDestination } from '@sap-cloud-sdk/connectivity'; -import nock from 'nock'; -import { - BaseLlmParameters, - CustomRequestConfig, - EndpointOptions -} from '../core/http-client.js'; - -// Get the directory of this file -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const mockEndpoint: EndpointOptions = { - url: 'mock-endpoint', - apiVersion: 'mock-api-version' -}; - -export function mockInference( - request: { - data: D; - requestConfig?: CustomRequestConfig; - }, - response: { - data: any; - status?: number; - }, - destination: HttpDestination, - endpoint: EndpointOptions = mockEndpoint -): nock.Scope { - const { deploymentConfiguration, ...body } = request.data; - const { url, apiVersion } = endpoint; - - return nock(destination.url, { - reqheaders: { - 'ai-resource-group': 'default', - authorization: `Bearer ${destination.authTokens?.[0].value}` - } - }) - .post( - `/v2/inference/deployments/${deploymentConfiguration.deploymentId}/${url}`, - body as any - ) - .query(apiVersion ? { 'api-version': apiVersion } : {}) - .reply(response.status, response.data); -} - -export function parseMockResponse(client: string, fileName: string): T { - const fileContent = fs.readFileSync( - path.join(__dirname, 'mock-data', client, fileName), - 'utf-8' - ); - - return JSON.parse(fileContent); -} diff --git a/packages/gen-ai-hub/src/test-util/mock-data/openai/openai-chat-completion-success-response.json b/packages/gen-ai-hub/test/openai/openai-chat-completion-success-response.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/openai/openai-chat-completion-success-response.json rename to packages/gen-ai-hub/test/openai/openai-chat-completion-success-response.json diff --git a/packages/gen-ai-hub/src/test-util/mock-data/openai/openai-embeddings-success-response.json b/packages/gen-ai-hub/test/openai/openai-embeddings-success-response.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/openai/openai-embeddings-success-response.json rename to packages/gen-ai-hub/test/openai/openai-embeddings-success-response.json diff --git a/packages/gen-ai-hub/src/test-util/mock-data/openai/openai-error-response.json b/packages/gen-ai-hub/test/openai/openai-error-response.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/openai/openai-error-response.json rename to packages/gen-ai-hub/test/openai/openai-error-response.json diff --git a/packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-filter-config.json b/packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-filter-config.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-filter-config.json rename to packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-filter-config.json diff --git a/packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-message-history.json b/packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-message-history.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-message-history.json rename to packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-message-history.json diff --git a/packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-success-response.json b/packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-success-response.json similarity index 100% rename from packages/gen-ai-hub/src/test-util/mock-data/orchestration/genaihub-chat-completion-success-response.json rename to packages/gen-ai-hub/test/orchestration/genaihub-chat-completion-success-response.json diff --git a/packages/gen-ai-hub/tsconfig.json b/packages/gen-ai-hub/tsconfig.json index d7b4b5b2..5c6b23ab 100644 --- a/packages/gen-ai-hub/tsconfig.json +++ b/packages/gen-ai-hub/tsconfig.json @@ -7,5 +7,6 @@ "composite": true }, "include": ["src/**/*.ts"], - "exclude": ["dist/**/*", "**/*.test.ts", "node_modules/**/*"] + "exclude": ["dist/**/*", "test/**/*", "**/*.test.ts", "node_modules/**/*"], + "references": [{ "path": "../core" }] } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6a6dd70e..6949ac77 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,9 @@ importers: '@jest/globals': specifier: ^29.5.12 version: 29.7.0 + '@sap-ai-sdk/core': + specifier: workspace:^ + version: link:packages/core '@sap-cloud-sdk/connectivity': specifier: ^3.18.0 version: 3.18.0 @@ -26,6 +29,9 @@ importers: '@types/jest': specifier: ^29.5.12 version: 29.5.12 + '@types/jsonwebtoken': + specifier: ^9.0.2 + version: 9.0.6 '@types/node': specifier: ^20.14.15 version: 20.14.15 @@ -35,6 +41,9 @@ importers: jest: specifier: ^30.0.0-alpha.6 version: 30.0.0-alpha.6(@types/node@20.14.15)(ts-node@10.9.2(@types/node@20.14.15)(typescript@5.5.4)) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 nock: specifier: ^13.5.4 version: 13.5.4 @@ -47,9 +56,6 @@ importers: ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@20.14.15)(typescript@5.5.4) - tsd: - specifier: ^0.31.0 - version: 0.31.1 typescript: specifier: ^5.5.4 version: 5.5.4 @@ -67,8 +73,27 @@ importers: specifier: ^5.5.4 version: 5.5.4 + packages/core: + dependencies: + '@sap-cloud-sdk/connectivity': + specifier: ^3.18.0 + version: 3.18.0 + '@sap-cloud-sdk/http-client': + specifier: ^3.18.0 + version: 3.18.0 + '@sap-cloud-sdk/util': + specifier: ^3.18.0 + version: 3.18.0 + devDependencies: + typescript: + specifier: ^5.5.4 + version: 5.5.4 + packages/gen-ai-hub: dependencies: + '@sap-ai-sdk/core': + specifier: workspace:^ + version: link:../core '@sap-cloud-sdk/connectivity': specifier: ^3.18.0 version: 3.18.0 @@ -109,6 +134,9 @@ importers: '@sap-ai-sdk/ai-core': specifier: workspace:^ version: link:../../packages/ai-core + '@sap-ai-sdk/core': + specifier: workspace:^ + version: link:../../packages/core '@sap-ai-sdk/gen-ai-hub': specifier: workspace:^ version: link:../../packages/gen-ai-hub @@ -120,6 +148,18 @@ importers: specifier: ^16.4.5 version: 16.4.5 + tests/type-tests: + devDependencies: + '@sap-ai-sdk/core': + specifier: workspace:^ + version: link:../../packages/core + '@sap-ai-sdk/gen-ai-hub': + specifier: workspace:^ + version: link:../../packages/gen-ai-hub + tsd: + specifier: ^0.31.0 + version: 0.31.1 + packages: '@ampproject/remapping@2.3.0': @@ -727,6 +767,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/jsonwebtoken@9.0.6': + resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} + '@types/mime@1.3.5': resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} @@ -4302,7 +4345,7 @@ snapshots: '@types/connect@3.4.38': dependencies: - '@types/node': 20.14.14 + '@types/node': 20.14.15 '@types/eslint@7.29.0': dependencies: @@ -4356,6 +4399,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/jsonwebtoken@9.0.6': + dependencies: + '@types/node': 20.14.15 + '@types/mime@1.3.5': {} '@types/minimist@1.2.5': {} @@ -4381,7 +4428,7 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.14.14 + '@types/node': 20.14.15 '@types/serve-static@1.15.7': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4787eced..7d9348b8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,9 +2,12 @@ packages: # all packages in direct subdirs of packages/ - 'packages/ai-core' - 'packages/gen-ai-hub' + - 'packages/core' # sample code - 'sample-code' # e2e tests - 'tests/e2e-tests' + # type tests + - 'tests/type-tests' # exclude packages that are inside test directories - - '!tests/type-tests' + diff --git a/sample-code/src/aiservice.ts b/sample-code/src/aiservice.ts index 7b79492a..36f0543f 100644 --- a/sample-code/src/aiservice.ts +++ b/sample-code/src/aiservice.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line import/namespace import { OpenAiClient } from '@sap-ai-sdk/gen-ai-hub'; const openAiClient = new OpenAiClient(); diff --git a/scripts/update-imports.ts b/scripts/update-imports.ts deleted file mode 100644 index 4e6ead01..00000000 --- a/scripts/update-imports.ts +++ /dev/null @@ -1,68 +0,0 @@ -/* eslint-disable no-console */ -import * as fs from 'fs/promises'; -import * as path from 'path'; - -// Function to read a file and return its contents -async function readFile(filePath: string): Promise { - return fs.readFile(filePath, 'utf-8'); -} - -// Function to write data to a file -async function writeFile(filePath: string, data: string): Promise { - await fs.writeFile(filePath, data, 'utf-8'); -} - -// Function to process index.ts files and update export statements -async function processIndexFile(filePath: string): Promise { - const content = await readFile(filePath); - const updatedContent = content.replace(/export \* from '\.\/([^']*)';/g, "export * from './$1.js';"); - await writeFile(filePath, updatedContent); -} - -// Function to process root-level files and update import statements for the schema folder -async function processRootFile(filePath: string): Promise { - const content = await readFile(filePath); - const updatedContent = content.replace(/import type \{ ([^}]*) \} from '\.\/schema';/g, "import type { $1 } from './schema/index.js';"); - await writeFile(filePath, updatedContent); -} - -// Function to process schema-level files and update import statements -async function processSchemaFile(filePath: string): Promise { - const content = await readFile(filePath); - const updatedContent = content.replace(/import type \{ ([^}]*) \} from '\.\/([^']*)';/g, "import type { $1 } from './$2.js';"); - await writeFile(filePath, updatedContent); -} - -// Function to recursively traverse the directory and apply transformations -async function traverseDirectory(dirPath: string): Promise { - const files = await fs.readdir(dirPath); - - for (const file of files) { - const filePath = path.join(dirPath, file); - const stat = await fs.stat(filePath); - - if (stat.isDirectory()) { - await traverseDirectory(filePath); - } else if (stat.isFile()) { - if (file === 'index.ts') { - await processIndexFile(filePath); - } else if (dirPath === path.resolve(rootDir)) { - await processRootFile(filePath); - } else if (path.basename(dirPath) === 'schema') { - await processSchemaFile(filePath); - } - } - } -} - -// Entry point: Get the root directory from command-line arguments -const rootDir = process.argv[2]; - -if (!rootDir) { - console.error('Please provide the root directory as an argument.'); - process.exit(1); -} - -traverseDirectory(path.resolve(rootDir)) - .then(() => console.log('All files processed successfully.')) - .catch(err => console.error('Error processing files:', err)); diff --git a/test-util/mock-http.ts b/test-util/mock-http.ts new file mode 100644 index 00000000..bd1efc1b --- /dev/null +++ b/test-util/mock-http.ts @@ -0,0 +1,125 @@ +import fs from 'fs'; +import path from 'path'; +import { DestinationAuthToken, HttpDestination, ServiceCredentials } from '@sap-cloud-sdk/connectivity'; +import nock from 'nock'; +import { + BaseLlmParameters, + CustomRequestConfig +} from '@sap-ai-sdk/core'; +import { EndpointOptions } from '@sap-ai-sdk/core/src/http-client.js'; +import { dummyToken } from './mock-jwt.js'; + +export const aiCoreDestination = { + url: 'https://api.ai.ml.hana.ondemand.com' +}; + +export const aiCoreServiceBinding = { + label: 'aicore', + credentials: { + clientid: 'clientid', + clientsecret: 'clientsecret', + url: 'https://example.authentication.eu12.hana.ondemand.com', + identityzone: 'examplezone', + identityzoneid: 'examplezoneid', + appname: 'appname', + serviceurls: { + AI_API_URL: aiCoreDestination.url + } + } +}; + +const mockEndpoint: EndpointOptions = { + url: 'mock-endpoint', + apiVersion: 'mock-api-version' +}; + +export function mockAiCoreEnvVariable(): void { + process.env['aicore'] = JSON.stringify(aiCoreServiceBinding.credentials); +} + +export function createDestinationTokens( + token: string = dummyToken, + expiresIn?: string +): { authTokens: DestinationAuthToken[] } { + return { + authTokens: [ + { + value: token, + type: 'bearer', + expiresIn, + http_header: { key: 'authorization', value: `Bearer ${token}` }, + error: null + } + ] + }; +} + +/** + * @internal + */ +export function getMockedAiCoreDestination( + destination = aiCoreDestination +): HttpDestination { + const mockDestination: HttpDestination = { + ...destination, + authentication: 'OAuth2ClientCredentials', + ...createDestinationTokens() + }; + return mockDestination; +} + +export function mockClientCredentialsGrantCall( + response: any, + responseCode: number, + serviceCredentials: ServiceCredentials = aiCoreServiceBinding.credentials, + delay = 0 +): nock.Scope { + return nock(serviceCredentials.url) + .post('/oauth/token', { + grant_type: 'client_credentials', + client_id: serviceCredentials.clientid, + client_secret: serviceCredentials.clientsecret + }) + .delay(delay) + .reply(responseCode, response); +} + +export function mockInference( + request: { + data: D; + requestConfig?: CustomRequestConfig; + }, + response: { + data: any; + status?: number; + }, + endpoint: EndpointOptions = mockEndpoint +): nock.Scope { + const { deploymentConfiguration, ...body } = request.data; + const { url, apiVersion } = endpoint; + const destination = getMockedAiCoreDestination(); + return nock(destination.url, { + reqheaders: { + 'ai-resource-group': 'default', + authorization: `Bearer ${destination.authTokens?.[0].value}` + } + }) + .post( + `/v2/inference/deployments/${deploymentConfiguration.deploymentId}/${url}`, + body as any + ) + .query(apiVersion ? { 'api-version': apiVersion } : {}) + .reply(response.status, response.data); +} + +/** + * @internal + */ +export function parseMockResponse(client: string, fileName: string): T { + const fileContent = fs.readFileSync( + path.join('test', client, fileName), + 'utf-8' + ); + + return JSON.parse(fileContent); +} diff --git a/test-util/mock-jwt.ts b/test-util/mock-jwt.ts new file mode 100644 index 00000000..909857fb --- /dev/null +++ b/test-util/mock-jwt.ts @@ -0,0 +1,23 @@ +import jwt from 'jsonwebtoken'; +import { generateKeyPairSync } from 'node:crypto'; + +export const { publicKey, privateKey } = generateKeyPairSync('rsa', { + modulusLength: 4096, + publicKeyEncoding: { + type: 'pkcs1', + format: 'pem' + }, + privateKeyEncoding: { + type: 'pkcs1', + format: 'pem' + } + }); + + export const dummyToken = jwt.sign( + { dummy: 'content' }, + privateKey, + { + algorithm: 'RS512' + } + ); + \ No newline at end of file diff --git a/tests/e2e-tests/package.json b/tests/e2e-tests/package.json index 0b6ca9c4..743c79c3 100644 --- a/tests/e2e-tests/package.json +++ b/tests/e2e-tests/package.json @@ -14,11 +14,12 @@ "dependencies": { "@sap-ai-sdk/sample-code": "workspace:^", "@sap-ai-sdk/gen-ai-hub": "workspace:^", - "@sap-ai-sdk/ai-core": "workspace:^" + "@sap-ai-sdk/ai-core": "workspace:^", + "@sap-ai-sdk/core": "workspace:^" }, "scripts": { "compile": "tsc", - "e2e-test": "NODE_OPTIONS=--experimental-vm-modules jest", + "test": "NODE_OPTIONS=--experimental-vm-modules jest", "lint": "prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -c", "lint:fix": "prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -w --log-level error" }, diff --git a/tests/e2e-tests/src/ai-core.test.ts b/tests/e2e-tests/src/ai-core.test.ts index 640ee719..7d2b3107 100644 --- a/tests/e2e-tests/src/ai-core.test.ts +++ b/tests/e2e-tests/src/ai-core.test.ts @@ -1,5 +1,5 @@ import { DeploymentApi } from '@sap-ai-sdk/ai-core'; -import { getAiCoreDestination } from '@sap-ai-sdk/gen-ai-hub'; +import { getAiCoreDestination } from '@sap-ai-sdk/core'; import { HttpDestination } from '@sap-cloud-sdk/connectivity'; import 'dotenv/config'; diff --git a/tests/type-tests/package.json b/tests/type-tests/package.json new file mode 100644 index 00000000..a2fa84d7 --- /dev/null +++ b/tests/type-tests/package.json @@ -0,0 +1,20 @@ +{ + "name": "@sap-ai-sdk/type-tests", + "version": "0.0.0", + "description": "Tests to ensure correct types in the SAP AI SDK for JavaScript.", + "type": "module", + "private": true, + "types": "test", + "scripts": { + "test": "pnpm tsd", + "lint:fix": "prettier . --config ../../.prettierrc --ignore-path ../../.prettierignore -w --log-level error" + }, + "tsd": { + "directory": "test" + }, + "devDependencies": { + "@sap-ai-sdk/gen-ai-hub": "workspace:^", + "@sap-ai-sdk/core": "workspace:^", + "tsd": "^0.31.0" + } +} diff --git a/tests/type-tests/context.test-d.ts b/tests/type-tests/test/context.test-d.ts similarity index 63% rename from tests/type-tests/context.test-d.ts rename to tests/type-tests/test/context.test-d.ts index 98029060..aa967764 100644 --- a/tests/type-tests/context.test-d.ts +++ b/tests/type-tests/test/context.test-d.ts @@ -1,5 +1,5 @@ import { Destination } from '@sap-cloud-sdk/connectivity'; import { expectType } from 'tsd'; -import { getAiCoreDestination } from '../../packages/gen-ai-hub/src/core/context.js'; +import { getAiCoreDestination } from '@sap-ai-sdk/core'; expectType>(getAiCoreDestination()); diff --git a/tests/type-tests/http-client.test-d.ts b/tests/type-tests/test/http-client.test-d.ts similarity index 88% rename from tests/type-tests/http-client.test-d.ts rename to tests/type-tests/test/http-client.test-d.ts index 8b85cdc0..918a9f65 100644 --- a/tests/type-tests/http-client.test-d.ts +++ b/tests/type-tests/test/http-client.test-d.ts @@ -1,6 +1,6 @@ import { HttpResponse } from '@sap-cloud-sdk/http-client'; import { expectError, expectType } from 'tsd'; -import { executeRequest } from '../../packages/gen-ai-hub/src/core/http-client.js'; +import { executeRequest } from '@sap-ai-sdk/core'; expectType>( executeRequest( diff --git a/tests/type-tests/openai.test-d.ts b/tests/type-tests/test/openai.test-d.ts similarity index 78% rename from tests/type-tests/openai.test-d.ts rename to tests/type-tests/test/openai.test-d.ts index 53050c22..443662e6 100644 --- a/tests/type-tests/openai.test-d.ts +++ b/tests/type-tests/test/openai.test-d.ts @@ -1,12 +1,12 @@ import { expectError, expectType } from 'tsd'; -import { OpenAiClient } from '../../packages/gen-ai-hub/src/client/openai/openai-client.js'; import { + OpenAiClient, OpenAiChatCompletionOutput, OpenAiEmbeddingOutput -} from '../../packages/gen-ai-hub/src/client/openai/openai-types.js' +} from '@sap-ai-sdk/gen-ai-hub'; const client = new OpenAiClient(); -expectType(client ); +expectType(client); /** * Chat Completion. diff --git a/tests/type-tests/orchestration.test-d.ts b/tests/type-tests/test/orchestration.test-d.ts similarity index 92% rename from tests/type-tests/orchestration.test-d.ts rename to tests/type-tests/test/orchestration.test-d.ts index a2eab494..82cb0dd1 100644 --- a/tests/type-tests/orchestration.test-d.ts +++ b/tests/type-tests/test/orchestration.test-d.ts @@ -1,6 +1,5 @@ import { expectError, expectType } from 'tsd'; -import { GenAiHubClient } from '../../packages/gen-ai-hub/src/orchestration/orchestration-client.js'; -import { CompletionPostResponse } from '../../packages/gen-ai-hub/src/orchestration/index.js'; +import { GenAiHubClient, CompletionPostResponse } from '@sap-ai-sdk/gen-ai-hub'; const client = new GenAiHubClient(); expectType(client); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 00000000..24573e6a --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["packages/**/*.ts", "test-util/**/*.ts"], + "exclude": ["dist/**/*", "node_modules/**/*"] +}