diff --git a/.changeset/stupid-ligers-pretend.md b/.changeset/stupid-ligers-pretend.md new file mode 100644 index 000000000..e27b423cd --- /dev/null +++ b/.changeset/stupid-ligers-pretend.md @@ -0,0 +1,9 @@ +--- +'@sap-ai-sdk/foundation-models': minor +'@sap-ai-sdk/orchestration': minor +'@sap-ai-sdk/langchain': minor +'@sap-ai-sdk/ai-api': minor +'@sap-ai-sdk/core': minor +--- + +[New Functionality] Add support for providing custom destination for AI Core besides using environment variable and service binding. diff --git a/packages/ai-api/README.md b/packages/ai-api/README.md index 5f1b7f367..9a9b4154c 100644 --- a/packages/ai-api/README.md +++ b/packages/ai-api/README.md @@ -11,9 +11,8 @@ This package provides tools to manage scenarios and workflows in SAP AI Core. We maintain a list of [currently available and tested AI Core APIs](https://github.com/SAP/ai-sdk-js/blob/main/docs/list-tested-APIs.md) -## Table of Contents +### Table of Contents -- [Table of Contents](#table-of-contents) - [Installation](#installation) - [Version Management](#version-management) - [Prerequisites](#prerequisites) @@ -22,6 +21,7 @@ We maintain a list of [currently available and tested AI Core APIs](https://gith - [Create a Configuration](#create-a-configuration) - [Create a Deployment](#create-a-deployment) - [Delete a Deployment](#delete-a-deployment) + - [Custom Destination](#custom-destination) - [Local Testing](#local-testing) - [Support, Feedback, Contribution](#support-feedback-contribution) - [License](#license) @@ -64,60 +64,61 @@ In addition to the examples below, you can find more **sample code** [here](http ### Create an Artifact -```TypeScript +```ts async function createArtifact() { - - const requestBody: ArtifactPostData = { - name: 'training-test-dataset', - kind: 'dataset', - url: 'https://ai.example.com', - scenarioId: 'foundation-models' - } - - try { - const responseData: ArtifactCreationResponse = await ArtifactApi - .artifactCreate(requestBody, {'AI-Resource-Group': 'default'}) - .execute(); - return responseData; - } catch (errorData) { - const apiError = errorData.response.data.error as ApiError; - console.error('Status code:', errorData.response.status); - throw new Error(`Artifact creation failed: ${apiError.message}`); - } + const requestBody: ArtifactPostData = { + name: 'training-test-dataset', + kind: 'dataset', + url: 'https://ai.example.com', + scenarioId: 'foundation-models' + }; + + try { + const responseData: ArtifactCreationResponse = + await ArtifactApi.artifactCreate(requestBody, { + 'AI-Resource-Group': 'default' + }).execute(); + return responseData; + } catch (errorData) { + const apiError = errorData.response.data.error as ApiError; + console.error('Status code:', errorData.response.status); + throw new Error(`Artifact creation failed: ${apiError.message}`); + } } ``` ### Create a Configuration -```TypeScript +```ts async function createConfiguration() { - const requestBody: ConfigurationBaseData = { - name: 'gpt-35-turbo', - executableId: 'azure-openai', - scenarioId: 'foundation-models', - parameterBindings: [ - { - "key": "modelName", - "value": "gpt-35-turbo" - }, - { - "key": "modelVersion", - "value": "latest" - } - ], - inputArtifactBindings: [] - } - - try { - const responseData: ConfigurationCreationResponse = await ConfigurationApi - .configurationCreate(requestBody, {'AI-Resource-Group': 'default'}) - .execute(); - return responseData; - } catch (errorData) { - const apiError = errorData.response.data.error as ApiError; - console.error('Status code:', errorData.response.status); - throw new Error(`Configuration creation failed: ${apiError.message}`); - } + const requestBody: ConfigurationBaseData = { + name: 'gpt-35-turbo', + executableId: 'azure-openai', + scenarioId: 'foundation-models', + parameterBindings: [ + { + key: 'modelName', + value: 'gpt-35-turbo' + }, + { + key: 'modelVersion', + value: 'latest' + } + ], + inputArtifactBindings: [] + }; + + try { + const responseData: ConfigurationCreationResponse = + await ConfigurationApi.configurationCreate(requestBody, { + 'AI-Resource-Group': 'default' + }).execute(); + return responseData; + } catch (errorData) { + const apiError = errorData.response.data.error as ApiError; + console.error('Status code:', errorData.response.status); + throw new Error(`Configuration creation failed: ${apiError.message}`); + } } ``` @@ -148,42 +149,65 @@ async function createDeployment() { Only deployments with `targetStatus: STOPPED` can be deleted. Thus, a modification request must be sent before deletion can occur. -```TypeScript +```ts async function modifyDeployment() { + let deploymentId: string = '0a1b2c3d4e5f'; + + const deployment: DeploymentResponseWithDetails = + await DeploymentApi.deploymentGet( + deploymentId, + {}, + { 'AI-Resource-Group': 'default' } + ).execute(); + + if (deployment.targetStatus === 'RUNNING') { + // Only RUNNING deployments can be STOPPED. + const requestBody: DeploymentModificationRequest = { + targetStatus: 'STOPPED' + }; - let deploymentId: string = '0a1b2c3d4e5f'; - - const deployment: DeploymentResponseWithDetails = await DeploymentApi - .deploymentGet(deploymentId, {}, {'AI-Resource-Group': 'default'}) - .execute(); - - if(deployment.targetStatus === 'RUNNING') { - // Only RUNNING deployments can be STOPPED. - const requestBody: DeploymentModificationRequest = { - targetStatus: 'STOPPED', - }; - - try { - await DeploymentApi - .deploymentModify(deploymentId, requestBody, {'AI-Resource-Group': 'default'}) - .execute(); - } catch (errorData) { - const apiError = errorData.response.data.error as ApiError; - console.error('Status code:', errorData.response.status); - throw new Error(`Deployment modification failed: ${apiError.message}`); - } - } - // Wait a few seconds for the deployment to stop try { - return DeploymentApi.deploymentDelete(deploymentId, { 'AI-Resource-Group': 'default' }).execute(); + await DeploymentApi.deploymentModify(deploymentId, requestBody, { + 'AI-Resource-Group': 'default' + }).execute(); } catch (errorData) { - const apiError = errorData.response.data.error as ApiError; - console.error('Status code:', errorData.response.status); - throw new Error(`Deployment deletion failed: ${apiError.message}`); + const apiError = errorData.response.data.error as ApiError; + console.error('Status code:', errorData.response.status); + throw new Error(`Deployment modification failed: ${apiError.message}`); } + } + // Wait a few seconds for the deployment to stop + try { + return DeploymentApi.deploymentDelete(deploymentId, { + 'AI-Resource-Group': 'default' + }).execute(); + } catch (errorData) { + const apiError = errorData.response.data.error as ApiError; + console.error('Status code:', errorData.response.status); + throw new Error(`Deployment deletion failed: ${apiError.message}`); + } } ``` +### Custom Destination + +When calling the `execute()` method, it is possible to provide a custom destination. +For example, when querying deployments targeting a destination with the name `my-destination`, the following code can be used: + +```ts +const queryParams = status ? { status } : {}; +return DeploymentApi.deploymentQuery(queryParams, { + 'AI-Resource-Group': resourceGroup +}).execute({ + destinationName: 'my-destination' +}); +``` + +By default, the fetched destination is cached. +To disable caching, set the `useCache` parameter to `false` together with the `destinationName` parameter. + +```ts + ## Local Testing For local testing instructions, refer to this [section](https://github.com/SAP/ai-sdk-js/blob/main/README.md#local-testing). @@ -198,3 +222,4 @@ For more information about how to contribute, the project structure, as well as ## License The SAP Cloud SDK for AI is released under the [Apache License Version 2.0.](http://www.apache.org/licenses/). +``` diff --git a/packages/ai-api/src/tests/deployment-api.test.ts b/packages/ai-api/src/tests/deployment-api.test.ts index e9a283ab1..faf286db3 100644 --- a/packages/ai-api/src/tests/deployment-api.test.ts +++ b/packages/ai-api/src/tests/deployment-api.test.ts @@ -2,7 +2,8 @@ import nock from 'nock'; import { DeploymentApi } from '../client/AI_CORE_API'; import { aiCoreDestination, - mockClientCredentialsGrantCall + mockClientCredentialsGrantCall, + mockDestination } from '../../../../test-util/mock-http.js'; import type { AiDeploymentCreationRequest, @@ -171,4 +172,34 @@ describe('deployment', () => { expect(result).toEqual(expectedResponse); }); + + it('parses a successful response for delete request with custom destination', async () => { + mockDestination(); + + const deploymentId = '4e5f6g7h'; + const expectedResponse: AiDeploymentDeletionResponse = { + id: '4e5f6g7h', + message: 'Deletion scheduled', + targetStatus: 'DELETED' + }; + + nock('http://example.com', { + reqheaders: { + 'AI-Resource-Group': 'default' + } + }) + .delete(`/v2/lm/deployments/${deploymentId}`) + .reply(200, expectedResponse, { + 'Content-Type': 'application/json' + }); + + const result: AiDeploymentDeletionResponse = + await DeploymentApi.deploymentDelete(deploymentId, { + 'AI-Resource-Group': 'default' + }).execute({ + destinationName: 'aicore' + }); + + expect(result).toEqual(expectedResponse); + }); }); diff --git a/packages/core/src/context.ts b/packages/core/src/context.ts index 52cf7adde..0a2def8ef 100644 --- a/packages/core/src/context.ts +++ b/packages/core/src/context.ts @@ -1,10 +1,13 @@ import { createLogger } from '@sap-cloud-sdk/util'; import { + assertHttpDestination, getServiceBinding, - transformServiceBindingToDestination + transformServiceBindingToDestination, + useOrFetchDestination } from '@sap-cloud-sdk/connectivity'; import type { HttpDestination, + HttpDestinationOrFetchOptions, Service, ServiceCredentials } from '@sap-cloud-sdk/connectivity'; @@ -17,10 +20,32 @@ const logger = createLogger({ let aiCoreServiceBinding: Service | undefined; /** - * Returns a destination object from AI Core service binding. + * Returns a destination object. + * @param destination - The destination to use for the request. * @returns The destination object. */ -export async function getAiCoreDestination(): Promise { +export async function getAiCoreDestination( + destination?: HttpDestinationOrFetchOptions +): Promise { + // If Destination is provided, get the destination and return it. + if (destination) { + // If fetch options provided, by default cache the destination. + if ( + destination.destinationName !== undefined && + destination.useCache === undefined + ) { + destination.useCache = true; + } + + const resolvedDestination = await useOrFetchDestination(destination); + if (!resolvedDestination) { + throw new Error('Could not resolve destination.'); + } + assertHttpDestination(resolvedDestination); + return resolvedDestination; + } + + // Otherwise, get the destination from env or service binding with default service name "aicore". if (!aiCoreServiceBinding) { aiCoreServiceBinding = getAiCoreServiceKeyFromEnv() || getServiceBinding('aicore'); diff --git a/packages/core/src/http-client.test.ts b/packages/core/src/http-client.test.ts index 24048108c..e4e5bad1f 100644 --- a/packages/core/src/http-client.test.ts +++ b/packages/core/src/http-client.test.ts @@ -1,9 +1,10 @@ import nock from 'nock'; import { mockClientCredentialsGrantCall, - aiCoreDestination + aiCoreDestination, + mockDestination } from '../../../test-util/mock-http.js'; -import { executeRequest } from './http-client.js'; +import { executeRequest, getTargetUrl } from './http-client.js'; describe('http-client', () => { beforeEach(() => { @@ -35,7 +36,7 @@ describe('http-client', () => { expect(scope.isDone()).toBe(true); expect(res.status).toBe(200); expect(res.data).toEqual(mockPromptResponse); - }, 10000); + }); it('should execute a request to the AI Core service with a custom resource group', async () => { const mockPrompt = { prompt: 'some test prompt' }; @@ -63,4 +64,48 @@ describe('http-client', () => { expect(res.status).toBe(200); expect(res.data).toEqual(mockPromptResponse); }); + + it('should execute a request using custom destination', async () => { + mockDestination(); + + const mockPrompt = { prompt: 'some test prompt' }; + const mockPromptResponse = { completion: 'some test completion' }; + + const scope = nock('http://example.com', { + reqheaders: { + 'ai-resource-group': 'default', + 'ai-client-type': 'AI SDK JavaScript' + } + }) + .post('/v2/some/endpoint', mockPrompt) + .query({ 'api-version': 'mock-api-version' }) + .reply(200, mockPromptResponse); + + const res = await executeRequest( + { url: '/some/endpoint', apiVersion: 'mock-api-version' }, + mockPrompt, + {}, + { + destinationName: 'aicore' + } + ); + expect(scope.isDone()).toBe(true); + expect(res.status).toBe(200); + expect(res.data).toEqual(mockPromptResponse); + }); + + it('should get correct target url', async () => { + expect(getTargetUrl('http://example.com', '/some/endpoint')).toBe( + 'http://example.com/v2/some/endpoint' + ); + expect(getTargetUrl('http://example.com/', '/some/endpoint')).toBe( + 'http://example.com/v2/some/endpoint' + ); + expect(getTargetUrl('http://example.com/abc', '/some/endpoint')).toBe( + 'http://example.com/abc/some/endpoint' + ); + expect(getTargetUrl('http://example.com/abc/', '/some/endpoint')).toBe( + 'http://example.com/abc/some/endpoint' + ); + }); }); diff --git a/packages/core/src/http-client.ts b/packages/core/src/http-client.ts index 0eb63e6c4..0737d42c2 100644 --- a/packages/core/src/http-client.ts +++ b/packages/core/src/http-client.ts @@ -1,6 +1,11 @@ -import { mergeIgnoreCase, removeLeadingSlashes } from '@sap-cloud-sdk/util'; +import { + mergeIgnoreCase, + removeLeadingSlashes, + removeTrailingSlashes +} from '@sap-cloud-sdk/util'; import { executeHttpRequest } from '@sap-cloud-sdk/http-client'; import { getAiCoreDestination } from './context.js'; +import type { HttpDestinationOrFetchOptions } from '@sap-cloud-sdk/connectivity'; import type { HttpRequestConfig, HttpResponse @@ -44,14 +49,16 @@ export interface EndpointOptions { * @param endpointOptions - The options to call an endpoint. * @param data - The input parameters for the request. * @param requestConfig - The request configuration. + * @param destination - The destination to use for the request. * @returns The {@link HttpResponse} from the AI Core service. */ export async function executeRequest( endpointOptions: EndpointOptions, data: any, - requestConfig?: CustomRequestConfig + requestConfig?: CustomRequestConfig, + destination?: HttpDestinationOrFetchOptions ): Promise { - const aiCoreDestination = await getAiCoreDestination(); + const aiCoreDestination = await getAiCoreDestination(destination); const { url, apiVersion, resourceGroup = 'default' } = endpointOptions; const mergedRequestConfig = { @@ -59,10 +66,8 @@ export async function executeRequest( data: JSON.stringify(data) }; - const targetUrl = aiCoreDestination.url + `/v2/${removeLeadingSlashes(url)}`; - return executeHttpRequest( - { ...aiCoreDestination, url: targetUrl }, + { ...aiCoreDestination, url: getTargetUrl(aiCoreDestination.url, url) }, mergedRequestConfig, { fetchCsrfToken: false @@ -91,3 +96,24 @@ function mergeWithDefaultRequestConfig( params: mergeIgnoreCase(defaultConfig.params, requestConfig?.params) }; } + +/** + * Get target url with endpoint path appended. + * Append path `v2` if the url contains empty pathname `/`. + * @param url - The url, e.g., `http://example.com` or `http://example.com:8000/abc`. + * @param endpointPath - The path to the endpoint, e.g., `/some/endpoint`. + * @returns Target url combining the url and endpoint path. + * @internal + */ +export function getTargetUrl(url: string, endpointPath: string): string { + // Remove the last trailing slash + url = removeTrailingSlashes(url); + // Remove the first leading slashes + endpointPath = removeLeadingSlashes(endpointPath); + + const urlObj = new URL(url); + if (urlObj.pathname === '/') { + return url + '/v2/' + endpointPath; + } + return url + '/' + endpointPath; +} diff --git a/packages/core/src/openapi-request-builder.ts b/packages/core/src/openapi-request-builder.ts index be1cdc107..62b8bd58e 100644 --- a/packages/core/src/openapi-request-builder.ts +++ b/packages/core/src/openapi-request-builder.ts @@ -1,5 +1,6 @@ import { OpenApiRequestBuilder as CloudSDKOpenApiRequestBuilder } from '@sap-cloud-sdk/openapi'; import { executeRequest } from './http-client.js'; +import type { HttpDestinationOrFetchOptions } from '@sap-cloud-sdk/connectivity'; import type { OpenApiRequestParameters } from '@sap-cloud-sdk/openapi'; import type { HttpResponse, Method } from '@sap-cloud-sdk/http-client'; @@ -20,30 +21,41 @@ export class OpenApiRequestBuilder< /** * Execute request and get the response data. Use this to conveniently access the data of a service without technical information about the response. + * @param destination - The destination to execute the request against. * @returns A promise resolving to an HttpResponse. */ - async executeRaw(): Promise { + async executeRaw( + destination?: HttpDestinationOrFetchOptions + ): Promise { const { url, data, ...rest } = await this.requestConfig(); // TODO: Remove explicit url! once we updated the type in the Cloud SDK, since url is always defined. - return executeRequest({ url: url! }, data, { - ...rest, - headers: { - ...rest.headers?.requestConfig, - ...rest.headers?.custom + return executeRequest( + { url: url! }, + data, + { + ...rest, + headers: { + ...rest.headers?.requestConfig, + ...rest.headers?.custom + }, + params: { + ...rest.params?.requestConfig, + ...rest.params?.custom + } }, - params: { - ...rest.params?.requestConfig, - ...rest.params?.custom - } - }); + destination + ); } /** * Execute request and get the response data. Use this to conveniently access the data of a service without technical information about the response. + * @param destination - The destination to execute the request against. * @returns A promise resolving to the requested return type. */ - async execute(): Promise { - const response = await this.executeRaw(); + async execute( + destination?: HttpDestinationOrFetchOptions + ): Promise { + const response = await this.executeRaw(destination); if ('data' in response) { return response.data; } diff --git a/packages/foundation-models/README.md b/packages/foundation-models/README.md index 9a4342b99..6b621f897 100644 --- a/packages/foundation-models/README.md +++ b/packages/foundation-models/README.md @@ -4,9 +4,8 @@ SAP Cloud SDK for AI is the official Software Development Kit (SDK) for **SAP AI This package incorporates generative AI foundation models into your AI activities in SAP AI Core and SAP AI Launchpad. -## Table of Contents +### Table of Contents -- [Table of Contents](#table-of-contents) - [Installation](#installation) - [Prerequisites](#prerequisites) - [Relationship between Models and Deployment ID](#relationship-between-models-and-deployment-id) @@ -15,6 +14,7 @@ This package incorporates generative AI foundation models into your AI activitie - [Azure OpenAI Chat Client](#azure-openai-chat-client) - [Azure OpenAI Embedding Client](#azure-openai-embedding-client) - [Custom Request Configuration](#custom-request-configuration) + - [Custom Destination](#custom-destination) - [Local Testing](#local-testing) - [Support, Feedback, Contribution](#support-feedback-contribution) - [License](#license) @@ -261,6 +261,20 @@ const response = await client.run( ); ``` +### Custom Destination + +When initializing the `AzureOpenAiChatClient` and `AzureOpenAiEmbeddingClient` clients, it is possible to provide a custom destination. +For example, when targeting a destination with the name `my-destination`, the following code can be used: + +```ts +const client = await new AzureOpenAiChatClient('gpt-35-turbo', { + destinationName: 'my-destination' +}); +``` + +By default, the fetched destination is cached. +To disable caching, set the `useCache` parameter to `false` together with the `destinationName` parameter. + ## Local Testing For local testing instructions, refer to this [section](https://github.com/SAP/ai-sdk-js/blob/main/README.md#local-testing). diff --git a/packages/foundation-models/package.json b/packages/foundation-models/package.json index 94574a96c..b79ade43a 100644 --- a/packages/foundation-models/package.json +++ b/packages/foundation-models/package.json @@ -33,6 +33,7 @@ "@sap-ai-sdk/ai-api": "workspace:^", "@sap-ai-sdk/core": "workspace:^", "@sap-cloud-sdk/http-client": "^3.24.0", - "@sap-cloud-sdk/util": "^3.24.0" + "@sap-cloud-sdk/util": "^3.24.0", + "@sap-cloud-sdk/connectivity": "^3.24.0" } } diff --git a/packages/foundation-models/src/azure-openai/azure-openai-chat-client.ts b/packages/foundation-models/src/azure-openai/azure-openai-chat-client.ts index 6fffeeeaf..8107f0227 100644 --- a/packages/foundation-models/src/azure-openai/azure-openai-chat-client.ts +++ b/packages/foundation-models/src/azure-openai/azure-openai-chat-client.ts @@ -11,6 +11,7 @@ import { AzureOpenAiChatCompletionStream } from './azure-openai-chat-completion- import type { AzureOpenAiChatCompletionStreamChunkResponse } from './azure-openai-chat-completion-stream-chunk-response.js'; import type { HttpResponse } from '@sap-cloud-sdk/http-client'; import type { AzureOpenAiCreateChatCompletionRequest } from './client/inference/schema/index.js'; +import type { HttpDestinationOrFetchOptions } from '@sap-cloud-sdk/connectivity'; /** * Azure OpenAI client for chat completion. @@ -19,8 +20,12 @@ export class AzureOpenAiChatClient { /** * Creates an instance of the Azure OpenAI chat client. * @param modelDeployment - This configuration is used to retrieve a deployment. Depending on the configuration use either the given deployment ID or the model name to retrieve matching deployments. If model and deployment ID are given, the model is verified against the deployment. + * @param destination - The destination to use for the request. */ - constructor(private modelDeployment: ModelDeployment) {} + constructor( + private modelDeployment: ModelDeployment, + private destination?: HttpDestinationOrFetchOptions + ) {} /** * Creates a completion for the chat messages. @@ -75,7 +80,8 @@ export class AzureOpenAiChatClient { resourceGroup }, data, - requestConfig + requestConfig, + this.destination ); } diff --git a/packages/foundation-models/src/azure-openai/azure-openai-embedding-client.ts b/packages/foundation-models/src/azure-openai/azure-openai-embedding-client.ts index 0281a04ce..ae38c338c 100644 --- a/packages/foundation-models/src/azure-openai/azure-openai-embedding-client.ts +++ b/packages/foundation-models/src/azure-openai/azure-openai-embedding-client.ts @@ -7,6 +7,7 @@ import { import { AzureOpenAiEmbeddingResponse } from './azure-openai-embedding-response.js'; import { apiVersion, type AzureOpenAiEmbeddingModel } from './model-types.js'; import type { AzureOpenAiEmbeddingParameters } from './azure-openai-embedding-types.js'; +import type { HttpDestinationOrFetchOptions } from '@sap-cloud-sdk/connectivity'; /** * Azure OpenAI client for embeddings. @@ -18,7 +19,8 @@ export class AzureOpenAiEmbeddingClient { */ constructor( - private modelDeployment: ModelDeployment + private modelDeployment: ModelDeployment, + private destination?: HttpDestinationOrFetchOptions ) {} /** @@ -43,7 +45,8 @@ export class AzureOpenAiEmbeddingClient { resourceGroup }, data, - requestConfig + requestConfig, + this.destination ); return new AzureOpenAiEmbeddingResponse(response); } diff --git a/packages/langchain/README.md b/packages/langchain/README.md index 691ecccfa..869a99ce0 100644 --- a/packages/langchain/README.md +++ b/packages/langchain/README.md @@ -4,24 +4,18 @@ SAP Cloud SDK for AI is the official Software Development Kit (SDK) for **SAP AI This package provides LangChain model clients built on top of the foundation model clients of the SAP Cloud SDK for AI. -## Table of Contents - -- [@sap-ai-sdk/langchain](#sap-ai-sdklangchain) - - [Table of Contents](#table-of-contents) - - [Installation](#installation) - - [Prerequisites](#prerequisites) - - [Relationship between Models and Deployment ID](#relationship-between-models-and-deployment-id) - - [Usage](#usage) - - [Client Initialization](#client-initialization) - - [Chat Client](#chat-client) - - [Advanced Example with Templating and Output Parsing](#advanced-example-with-templating-and-output-parsing) - - [Embedding Client](#embedding-client) - - [Embed Text](#embed-text) - - [Embed Document Chunks](#embed-document-chunks) - - [Preprocess, embed, and store documents](#preprocess-embed-and-store-documents) - - [Local Testing](#local-testing) - - [Support, Feedback, Contribution](#support-feedback-contribution) - - [License](#license) +### Table of Contents + +- [Installation](#installation) +- [Prerequisites](#prerequisites) +- [Relationship between Models and Deployment ID](#relationship-between-models-and-deployment-id) +- [Usage](#usage) + - [Client Initialization](#client-initialization) + - [Chat Client](#chat-client) + - [Embedding Client](#embedding-client) +- [Local Testing](#local-testing) +- [Support, Feedback, Contribution](#support-feedback-contribution) +- [License](#license) ## Installation @@ -103,6 +97,27 @@ const embeddingClient = new AzureOpenAiEmbeddingClient({ }); ``` +#### Custom Destination + +When initializing the `AzureOpenAiChatClient` and `AzureOpenAiEmbeddingClient` clients, it is possible to provide a custom destination. +For example, when targeting a destination with the name `my-destination`, the following code can be used: + +```ts +const chatClient = new AzureOpenAiChatClient( + { + modelName: 'gpt-4o', + modelVersion: '24-07-2021', + resourceGroup: 'my-resource-group' + }, + { + destinationName: 'my-destination' + } +); +``` + +By default, the fetched destination is cached. +To disable caching, set the `useCache` parameter to `false` together with the `destinationName` parameter. + ### Chat Client The chat client allows you to interact with Azure OpenAI chat models, accessible via the generative AI hub of SAP AI Core. diff --git a/packages/langchain/package.json b/packages/langchain/package.json index 8004e9cc4..1c4ee390e 100644 --- a/packages/langchain/package.json +++ b/packages/langchain/package.json @@ -29,6 +29,7 @@ "@sap-ai-sdk/ai-api": "workspace:^", "@sap-ai-sdk/core": "workspace:^", "@sap-ai-sdk/foundation-models": "workspace:^", + "@sap-cloud-sdk/connectivity": "^3.24.0", "@langchain/core": "0.3.23", "zod-to-json-schema": "^3.24.1" } diff --git a/packages/langchain/src/openai/chat.ts b/packages/langchain/src/openai/chat.ts index 90d82c344..eaf3fa987 100644 --- a/packages/langchain/src/openai/chat.ts +++ b/packages/langchain/src/openai/chat.ts @@ -8,6 +8,7 @@ import type { AzureOpenAiChatCallOptions, AzureOpenAiChatModelParams } from './types.js'; +import type { HttpDestinationOrFetchOptions } from '@sap-cloud-sdk/connectivity'; /** * LangChain chat client for Azure OpenAI consumption on SAP BTP. @@ -23,9 +24,12 @@ export class AzureOpenAiChatClient extends BaseChatModel=18'} + '@langchain/core@0.3.23': resolution: {integrity: sha512-Aut43dEJYH/ibccSErFOLQzymkBG4emlN16P0OHWwx02bDosOR9ilZly4JJiCSYcprn2X2H8nee6P/4VMg1oQA==} engines: {node: '>=18'} @@ -1005,20 +1018,20 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sap-ai-sdk/ai-api@1.4.1-20241212013157.0': - resolution: {integrity: sha512-RNnv84lWwAKuQzGxLzUkxd0I5d5eVyq7iryZSUjtTPSvUhF5TZbQh8RPO3Uv/LMUQGGw69R5/YY0bHU2vZ3CDw==} + '@sap-ai-sdk/ai-api@1.3.1-20241206013137.0': + resolution: {integrity: sha512-GVZmkAKJ44z1rkqhMhAJFswjBJMwCK0iVex4t/rTEVzGB4FqgQtXDjya3BDTmAzh65chWXETPGKsDqBpPGKT0Q==} - '@sap-ai-sdk/core@1.4.1-20241212013157.0': - resolution: {integrity: sha512-0YJ7n02w4f42I4zOpLDsJAr7uVzWvzfgUpYpCoz3gWGQMfVeEhb5uDXobqywyQTfBkQmR8QF4QeULhCMIcGFqw==} + '@sap-ai-sdk/core@1.3.1-20241206013137.0': + resolution: {integrity: sha512-B2NwcnUvdXDWd305Xp2lNlZKQg1CAJjkhX+dhoxob0/R2PFO/goetMKk66S2l/2pnBC+WFQkFw/Mm7mVyhf+bg==} - '@sap-ai-sdk/foundation-models@1.4.1-20241212013157.0': - resolution: {integrity: sha512-VY17ujCwYWg9Z0pDBhn+CGzX40uA5/w4iKzVKapVuSXUWu3J5rsNm0/K8WZ6WtHa45QLiluyKeNu6B2mob/XiQ==} + '@sap-ai-sdk/foundation-models@1.3.1-20241206013137.0': + resolution: {integrity: sha512-oLbvF2podOdmnjy6thxrOaLszxFk1w48FN87lyzLnx7LMtt7OG+foTPMOoqIyUS6czoiVt+lxBlHTDBhR/OO1Q==} - '@sap-ai-sdk/langchain@1.4.1-20241212013157.0': - resolution: {integrity: sha512-yJEeiWuGy62O3uGZdQCDy8l+jyqkDyRi+LdD9bfoMMlcdxX7rnFGNzsNtELJN7Hd/YDQEyKFnxn/OGZI3heF1Q==} + '@sap-ai-sdk/langchain@1.3.1-20241206013137.0': + resolution: {integrity: sha512-qicT30oOdfJxAEvw70506YDLDu5b5YAbLqfRTZP1hFHwqrlDffqZxc4+JDu/XLBXA+nscvZGtsucKHjbB+OSRA==} - '@sap-ai-sdk/orchestration@1.4.1-20241212013157.0': - resolution: {integrity: sha512-aihqtM9WPr6leRrjejeZGuDQbRhs8Ub1B5gV8RNmmhUyAtTPLJvxQmtwTBI7QpiEq5bZiSxhSjJANL3XcDD57A==} + '@sap-ai-sdk/orchestration@1.3.1-20241206013137.0': + resolution: {integrity: sha512-qysBA5nVdSVB0Zb2WxGUDmiG5P5Qpf1iowobmZweX6PWAJkI2gOObBJm+cb336LmyU6VePHr+r4kb2QJJ8UGBw==} '@sap-cloud-sdk/connectivity@3.24.0': resolution: {integrity: sha512-yi9JxZEOzwqIOwwhcVnE7PUk7gvU6fY952SaGv0x12E1MVwGIw+A/mg4qp74GPt9mvn/36y75XSajlIVu4IReA==} @@ -1246,9 +1259,6 @@ packages: '@types/node@20.17.10': resolution: {integrity: sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==} - '@types/node@20.17.9': - resolution: {integrity: sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==} - '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -4819,25 +4829,25 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@cap-js/asyncapi@1.0.2(@sap/cds@8.5.1(express@4.21.2))': + '@cap-js/asyncapi@1.0.2(@sap/cds@8.5.1(express@4.21.1))': dependencies: - '@sap/cds': 8.5.1(express@4.21.2) + '@sap/cds': 8.5.1(express@4.21.1) - '@cap-js/db-service@1.15.0(@sap/cds@8.5.1(express@4.21.2))': + '@cap-js/db-service@1.15.0(@sap/cds@8.5.1(express@4.21.1))': dependencies: - '@sap/cds': 8.5.1(express@4.21.2) + '@sap/cds': 8.5.1(express@4.21.1) generic-pool: 3.9.0 optional: true - '@cap-js/openapi@1.0.7(@sap/cds@8.5.1(express@4.21.2))': + '@cap-js/openapi@1.0.7(@sap/cds@8.5.1(express@4.21.1))': dependencies: - '@sap/cds': 8.5.1(express@4.21.2) + '@sap/cds': 8.5.1(express@4.21.1) pluralize: 8.0.0 - '@cap-js/sqlite@1.7.7(@sap/cds@8.5.1(express@4.21.2))': + '@cap-js/sqlite@1.7.7(@sap/cds@8.5.1(express@4.21.1))': dependencies: - '@cap-js/db-service': 1.15.0(@sap/cds@8.5.1(express@4.21.2)) - '@sap/cds': 8.5.1(express@4.21.2) + '@cap-js/db-service': 1.15.0(@sap/cds@8.5.1(express@4.21.1)) + '@sap/cds': 8.5.1(express@4.21.1) better-sqlite3: 11.5.0 optional: true @@ -5422,6 +5432,22 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@langchain/core@0.3.22(openai@4.61.1(zod@3.24.1))': + dependencies: + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.14 + langsmith: 0.2.8(openai@4.61.1(zod@3.24.1)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.24.1 + zod-to-json-schema: 3.24.1(zod@3.24.1) + transitivePeerDependencies: + - openai + '@langchain/core@0.3.23(openai@4.61.1(zod@3.24.1))': dependencies: '@cfworker/json-schema': 4.0.3 @@ -5511,15 +5537,15 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@sap-ai-sdk/ai-api@1.4.1-20241212013157.0': + '@sap-ai-sdk/ai-api@1.3.1-20241206013137.0': dependencies: - '@sap-ai-sdk/core': 1.4.1-20241212013157.0 + '@sap-ai-sdk/core': 1.3.1-20241206013137.0 '@sap-cloud-sdk/connectivity': 3.24.0 transitivePeerDependencies: - debug - supports-color - '@sap-ai-sdk/core@1.4.1-20241212013157.0': + '@sap-ai-sdk/core@1.3.1-20241206013137.0': dependencies: '@sap-cloud-sdk/connectivity': 3.24.0 '@sap-cloud-sdk/http-client': 3.24.0 @@ -5529,22 +5555,22 @@ snapshots: - debug - supports-color - '@sap-ai-sdk/foundation-models@1.4.1-20241212013157.0': + '@sap-ai-sdk/foundation-models@1.3.1-20241206013137.0': dependencies: - '@sap-ai-sdk/ai-api': 1.4.1-20241212013157.0 - '@sap-ai-sdk/core': 1.4.1-20241212013157.0 + '@sap-ai-sdk/ai-api': 1.3.1-20241206013137.0 + '@sap-ai-sdk/core': 1.3.1-20241206013137.0 '@sap-cloud-sdk/http-client': 3.24.0 '@sap-cloud-sdk/util': 3.24.0 transitivePeerDependencies: - debug - supports-color - '@sap-ai-sdk/langchain@1.4.1-20241212013157.0(openai@4.61.1(zod@3.24.1))(zod@3.24.1)': + '@sap-ai-sdk/langchain@1.3.1-20241206013137.0(openai@4.61.1(zod@3.24.1))(zod@3.24.1)': dependencies: - '@langchain/core': 0.3.23(openai@4.61.1(zod@3.24.1)) - '@sap-ai-sdk/ai-api': 1.4.1-20241212013157.0 - '@sap-ai-sdk/core': 1.4.1-20241212013157.0 - '@sap-ai-sdk/foundation-models': 1.4.1-20241212013157.0 + '@langchain/core': 0.3.22(openai@4.61.1(zod@3.24.1)) + '@sap-ai-sdk/ai-api': 1.3.1-20241206013137.0 + '@sap-ai-sdk/core': 1.3.1-20241206013137.0 + '@sap-ai-sdk/foundation-models': 1.3.1-20241206013137.0 zod-to-json-schema: 3.24.1(zod@3.24.1) transitivePeerDependencies: - debug @@ -5552,10 +5578,10 @@ snapshots: - supports-color - zod - '@sap-ai-sdk/orchestration@1.4.1-20241212013157.0': + '@sap-ai-sdk/orchestration@1.3.1-20241206013137.0': dependencies: - '@sap-ai-sdk/ai-api': 1.4.1-20241212013157.0 - '@sap-ai-sdk/core': 1.4.1-20241212013157.0 + '@sap-ai-sdk/ai-api': 1.3.1-20241206013137.0 + '@sap-ai-sdk/core': 1.3.1-20241206013137.0 '@sap-cloud-sdk/http-client': 3.24.0 transitivePeerDependencies: - debug @@ -5582,7 +5608,7 @@ snapshots: eslint: 9.16.0 eslint-config-prettier: 9.1.0(eslint@9.16.0) eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0))(eslint@9.16.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0))(eslint@9.16.0))(eslint@9.16.0) eslint-plugin-jsdoc: 50.6.0(eslint@9.16.0) eslint-plugin-prettier: 5.2.1(@types/eslint@8.56.10)(eslint-config-prettier@9.1.0(eslint@9.16.0))(eslint@9.16.0)(prettier@3.4.2) eslint-plugin-regex: 1.10.0(eslint@9.16.0) @@ -5672,8 +5698,8 @@ snapshots: '@sap/cds-dk@8.5.1': dependencies: - '@cap-js/asyncapi': 1.0.2(@sap/cds@8.5.1(express@4.21.2)) - '@cap-js/openapi': 1.0.7(@sap/cds@8.5.1(express@4.21.2)) + '@cap-js/asyncapi': 1.0.2(@sap/cds@8.5.1(express@4.21.1)) + '@cap-js/openapi': 1.0.7(@sap/cds@8.5.1(express@4.21.1)) '@sap/cds': 8.5.1(express@4.21.2) '@sap/cds-foss': 5.0.1 '@sap/cds-mtxs': 2.3.1(hdb@0.19.10) @@ -5689,7 +5715,7 @@ snapshots: ws: 8.18.0 xml-js: 1.6.11 optionalDependencies: - '@cap-js/sqlite': 1.7.7(@sap/cds@8.5.1(express@4.21.2)) + '@cap-js/sqlite': 1.7.7(@sap/cds@8.5.1(express@4.21.1)) transitivePeerDependencies: - '@sap/hana-client' - bufferutil @@ -5703,9 +5729,9 @@ snapshots: '@sap/cds': 8.5.1(express@4.21.1) express: 4.21.1 - '@sap/cds-fiori@1.2.7(@sap/cds@8.5.1(express@4.21.2))(express@4.21.2)': + '@sap/cds-fiori@1.2.7(@sap/cds@8.5.1(express@4.21.1))(express@4.21.2)': dependencies: - '@sap/cds': 8.5.1(express@4.21.2) + '@sap/cds': 8.5.1(express@4.21.1) express: 4.21.2 '@sap/cds-foss@5.0.1': @@ -5736,7 +5762,7 @@ snapshots: '@sap/cds@8.5.1(express@4.21.2)': dependencies: '@sap/cds-compiler': 5.3.2 - '@sap/cds-fiori': 1.2.7(@sap/cds@8.5.1(express@4.21.2))(express@4.21.2) + '@sap/cds-fiori': 1.2.7(@sap/cds@8.5.1(express@4.21.1))(express@4.21.2) '@sap/cds-foss': 5.0.1 optionalDependencies: express: 4.21.2 @@ -5856,11 +5882,11 @@ snapshots: '@types/body-parser@1.19.5': dependencies: '@types/connect': 3.4.38 - '@types/node': 20.17.9 + '@types/node': 20.17.10 '@types/connect@3.4.38': dependencies: - '@types/node': 20.17.9 + '@types/node': 20.17.10 '@types/eslint@7.29.0': dependencies: @@ -5877,7 +5903,7 @@ snapshots: '@types/express-serve-static-core@5.0.0': dependencies: - '@types/node': 20.17.9 + '@types/node': 20.17.10 '@types/qs': 6.9.15 '@types/range-parser': 1.2.7 '@types/send': 0.17.4 @@ -5943,10 +5969,6 @@ snapshots: dependencies: undici-types: 6.19.6 - '@types/node@20.17.9': - dependencies: - undici-types: 6.19.6 - '@types/normalize-package-data@2.4.4': {} '@types/parse-json@4.0.2': {} @@ -5962,12 +5984,12 @@ snapshots: '@types/send@0.17.4': dependencies: '@types/mime': 1.3.5 - '@types/node': 20.17.9 + '@types/node': 20.17.10 '@types/serve-static@1.15.7': dependencies: '@types/http-errors': 2.0.4 - '@types/node': 20.17.9 + '@types/node': 20.17.10 '@types/send': 0.17.4 '@types/stack-utils@2.0.3': {} @@ -6985,7 +7007,7 @@ snapshots: is-bun-module: 1.3.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0))(eslint@9.16.0))(eslint@9.16.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -7003,7 +7025,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3)(eslint@9.16.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.16.0(eslint@9.16.0)(typescript@5.7.2))(eslint@9.16.0))(eslint@9.16.0))(eslint@9.16.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 diff --git a/sample-code/README.md b/sample-code/README.md index 43fbfaf27..471abaa48 100644 --- a/sample-code/README.md +++ b/sample-code/README.md @@ -33,6 +33,13 @@ Parts of the sample code are also used in E2E tests. Get all deployments in resource group `default`. +#### Get all Deployments with Custom Destination + +`GET /ai-api/deployments-with-destination` + +Get all deployments targeting a custom destination. +Provide a destination when calling the `execute()` method. + #### Create a Deployment `POST /ai-api/deployment/create` @@ -83,6 +90,13 @@ Get all foundation models in resource group `default`. Get chat completion response. +#### Chat Completion with Custom Destination + +`GET /azure-openai/chat-completion-with-destination` + +Get chat completion response targeting a custom destination. +Provide a destination when initializing the `AzureOpenAiChatClient`. + #### Chat Completion Streaming `GET /azure-openai/chat-completion-stream` diff --git a/sample-code/src/ai-api/deployment-api.ts b/sample-code/src/ai-api/deployment-api.ts index f89c44414..980406e4c 100644 --- a/sample-code/src/ai-api/deployment-api.ts +++ b/sample-code/src/ai-api/deployment-api.ts @@ -25,6 +25,25 @@ export async function getDeployments( }).execute(); } +/** + * Get all deployments filtered by status with destination. + * @param resourceGroup - AI-Resource-Group where the resources are available. + * @param status - Optional query parameter to filter deployments by status. + * @returns List of deployments. + */ +export async function getDeploymentsWithDestination( + resourceGroup: string, + status?: AiDeploymentStatus +): Promise { + // check for optional query parameters. + const queryParams = status ? { status } : {}; + return DeploymentApi.deploymentQuery(queryParams, { + 'AI-Resource-Group': resourceGroup + }).execute({ + destinationName: 'e2e-aicore' + }); +} + /** * Create a deployment using the configuration specified by configurationId. * @param configurationId - ID of the configuration to be used. diff --git a/sample-code/src/foundation-models/azure-openai.ts b/sample-code/src/foundation-models/azure-openai.ts index 0ea5ea065..416cb3b9c 100644 --- a/sample-code/src/foundation-models/azure-openai.ts +++ b/sample-code/src/foundation-models/azure-openai.ts @@ -70,3 +70,20 @@ export async function computeEmbedding(): Promise return response; } + +/** + * Use custom destination to ask Azure OpenAI model about the capital of France. + * @returns The response from Azure OpenAI containing the response content. + */ +export async function chatCompletionWithDestination(): Promise { + const response = await new AzureOpenAiChatClient('gpt-35-turbo', { + destinationName: 'e2e-aicore' + }).run({ + messages: [{ role: 'user', content: 'What is the capital of France?' }] + }); + + // Use getContent() to access the content responded by LLM. + logger.info(response.getContent()); + + return response; +} diff --git a/sample-code/src/index.ts b/sample-code/src/index.ts index 2edd67143..90b6fa797 100644 --- a/sample-code/src/index.ts +++ b/sample-code/src/index.ts @@ -1,7 +1,8 @@ // exported for e2e tests export { chatCompletion, - computeEmbedding + computeEmbedding, + chatCompletionWithDestination // eslint-disable-next-line import/no-internal-modules } from './foundation-models/azure-openai.js'; export { @@ -19,6 +20,7 @@ export { } from './langchain-azure-openai.js'; export { getDeployments, + getDeploymentsWithDestination, createDeployment, stopDeployments, deleteDeployments diff --git a/sample-code/src/server.ts b/sample-code/src/server.ts index 595abc0e0..2c212b806 100644 --- a/sample-code/src/server.ts +++ b/sample-code/src/server.ts @@ -3,6 +3,7 @@ import express from 'express'; import { chatCompletion, chatCompletionStream, + chatCompletionWithDestination, computeEmbedding // eslint-disable-next-line import/no-internal-modules } from './foundation-models/azure-openai.js'; @@ -15,6 +16,7 @@ import { } from './orchestration.js'; import { getDeployments, + getDeploymentsWithDestination, createDeployment, stopDeployments, deleteDeployments @@ -59,6 +61,23 @@ app.get('/ai-api/deployments', async (req, res) => { } }); +app.get('/ai-api/deployments-with-destination', async (req, res) => { + try { + res.send( + await getDeploymentsWithDestination( + 'default', + req.query.status as AiDeploymentStatus + ) + ); + } catch (error: any) { + console.error(error); + const apiError = error.response.data.error as AiApiError; + res + .status(error.response.status) + .send('Yikes, vibes are off apparently 😬 -> ' + apiError.message); + } +}); + app.post('/ai-api/deployment/create', express.json(), async (req, res) => { try { res.send(await createDeployment(req.body.configurationId, 'default')); @@ -136,6 +155,18 @@ app.get('/azure-openai/chat-completion', async (req, res) => { } }); +app.get('/azure-openai/chat-completion-with-destination', async (req, res) => { + try { + const response = await chatCompletionWithDestination(); + res.send(response.getContent()); + } catch (error: any) { + console.error(error); + res + .status(500) + .send('Yikes, vibes are off apparently 😬 -> ' + error.message); + } +}); + app.get('/azure-openai/chat-completion-stream', async (req, res) => { const controller = new AbortController(); try { diff --git a/test-util/mock-http.ts b/test-util/mock-http.ts index b49ed2d05..ff01e4a73 100644 --- a/test-util/mock-http.ts +++ b/test-util/mock-http.ts @@ -8,10 +8,11 @@ import { type DeploymentResolutionOptions } from '@sap-ai-sdk/ai-api/internal.js'; import { dummyToken } from './mock-jwt.js'; -import type { - DestinationAuthToken, - HttpDestination, - ServiceCredentials +import { + registerDestination, + type DestinationAuthToken, + type HttpDestination, + type ServiceCredentials } from '@sap-cloud-sdk/connectivity'; // Get the directory of this file @@ -167,3 +168,13 @@ export async function parseMockResponse( ); return JSON.parse(fileContent); } + +/** + * @internal + */ +export async function mockDestination() { + registerDestination({ + name: 'aicore', + url: 'http://example.com' + }); +} diff --git a/tests/smoke-tests/test/smoke.test.ts b/tests/smoke-tests/test/smoke.test.ts index 693ee4ccf..39f3e7d40 100644 --- a/tests/smoke-tests/test/smoke.test.ts +++ b/tests/smoke-tests/test/smoke.test.ts @@ -7,6 +7,12 @@ describe('Smoke Test', () => { ).resolves.toHaveProperty('status', 200); }); + it('aicore client retrieves a list of deployments with custom destination', async () => { + await expect( + fetch(`${smokeTestRoute}/ai-api/deployments-with-destination`) + ).resolves.toHaveProperty('status', 200); + }); + it('orchestration client retrieves completion results', async () => { await expect( fetch(`${smokeTestRoute}/orchestration/simple`) @@ -18,4 +24,10 @@ describe('Smoke Test', () => { fetch(`${smokeTestRoute}/langchain/invoke`) ).resolves.toHaveProperty('status', 200); }); + + it('azure-openai client retrieves completion results with custom destination', async () => { + await expect( + fetch(`${smokeTestRoute}/azure-openai/chat-completion-with-destination`) + ).resolves.toHaveProperty('status', 200); + }); }); diff --git a/tests/type-tests/test/azure-openai.test-d.ts b/tests/type-tests/test/azure-openai.test-d.ts index 17733723b..a7f93abe0 100644 --- a/tests/type-tests/test/azure-openai.test-d.ts +++ b/tests/type-tests/test/azure-openai.test-d.ts @@ -62,6 +62,15 @@ expectType( ).getTokenUsage() ); +expectType>( + new AzureOpenAiChatClient('gpt-4', { + destinationName: 'destinationName', + useCache: false + }).run({ + messages: [{ role: 'user', content: 'test prompt' }] + }) +); + /** * Chat completion with optional parameters. */ @@ -134,6 +143,15 @@ expectType>( ) ); +expectType>( + new AzureOpenAiEmbeddingClient('text-embedding-ada-002', { + destinationName: 'destinationName', + useCache: false + }).run({ + input: 'test input' + }) +); + expect('custom-model'); expect('gpt-4-32k'); diff --git a/tests/type-tests/test/http-client.test-d.ts b/tests/type-tests/test/http-client.test-d.ts index 15a88dbbb..69f9669ca 100644 --- a/tests/type-tests/test/http-client.test-d.ts +++ b/tests/type-tests/test/http-client.test-d.ts @@ -1,17 +1,40 @@ -import { HttpResponse } from '@sap-cloud-sdk/http-client'; import { expectError, expectType } from 'tsd'; import { executeRequest } from '@sap-ai-sdk/core'; +import type { HttpResponse } from '@sap-cloud-sdk/http-client'; expectType>( - executeRequest({ url: 'https://example.com', apiVersion: 'v1' }, {}) + executeRequest({ url: 'https://example.com', apiVersion: '2024-10-21' }, {}) ); expectError(executeRequest({}, { prompt: 'test prompt' })); expectType>( executeRequest( - { url: 'https://example.com', apiVersion: 'v1' }, + { url: 'https://example.com', apiVersion: '2024-10-21' }, {}, { headers: { 'Content-Type': 'application/json' } } ) ); + +expectType>( + executeRequest( + { url: '/some-path', apiVersion: '2024-10-21' }, + {}, + { headers: { 'Content-Type': 'application/json' } }, + { + destinationName: 'my-aicore-destination', + useCache: false + } + ) +); + +expectType>( + executeRequest( + { url: 'https://example.com', apiVersion: '2024-10-21' }, + {}, + { headers: { 'Content-Type': 'application/json' } }, + { + url: 'http://example.com' + } + ) +); diff --git a/tests/type-tests/test/orchestration.test-d.ts b/tests/type-tests/test/orchestration.test-d.ts index 0dbfd84bd..2fc1282eb 100644 --- a/tests/type-tests/test/orchestration.test-d.ts +++ b/tests/type-tests/test/orchestration.test-d.ts @@ -78,6 +78,27 @@ expectType( ).getTokenUsage() ); +expectType>( + new OrchestrationClient( + { + templating: { + template: [{ role: 'user', content: 'Hello!' }] + }, + llm: { + model_name: 'gpt-35-turbo-16k', + model_params: {} + } + }, + { + resourceGroup: 'resourceGroup' + }, + { + destinationName: 'destinationName', + useCache: false + } + ).chatCompletion() +); + /** * Chat Completion with optional parameters. */