Skip to content

Commit fc865a6

Browse files
author
tomfrenken
committed
add resilience tests
1 parent 2ebb412 commit fc865a6

File tree

3 files changed

+124
-14
lines changed

3 files changed

+124
-14
lines changed
Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,98 @@
1-
// test timeout & maxRetries with mocked target
1+
import nock from 'nock';
2+
import { constructCompletionPostRequest } from '@sap-ai-sdk/orchestration/internal.js';
3+
import {
4+
mockClientCredentialsGrantCall,
5+
mockDeploymentsList,
6+
mockInference,
7+
parseMockResponse
8+
} from '../../../../test-util/mock-http.js';
9+
import { OrchestrationClient } from './client.js';
10+
import type {
11+
CompletionPostResponse,
12+
OrchestrationModuleConfig
13+
} from '@sap-ai-sdk/orchestration';
14+
15+
describe('orchestration service client', () => {
16+
beforeEach(() => {
17+
mockClientCredentialsGrantCall();
18+
mockDeploymentsList({ scenarioId: 'orchestration' }, { id: '1234' });
19+
});
20+
21+
afterEach(() => {
22+
nock.cleanAll();
23+
});
24+
25+
it('calls chatCompletion with minimum configuration', async () => {
26+
const config: OrchestrationModuleConfig = {
27+
llm: {
28+
model_name: 'gpt-35-turbo-16k',
29+
model_params: { max_tokens: 50, temperature: 0.1 }
30+
},
31+
templating: {
32+
template: [{ role: 'user', content: 'Hello!' }]
33+
}
34+
};
35+
36+
const mockResponse = await parseMockResponse<CompletionPostResponse>(
37+
'orchestration',
38+
'orchestration-chat-completion-success-response.json'
39+
);
40+
41+
mockInference(
42+
{
43+
data: constructCompletionPostRequest(config, { messagesHistory: [] })
44+
},
45+
{
46+
data: mockResponse,
47+
status: 200
48+
},
49+
{
50+
url: 'inference/deployments/1234/completion'
51+
},
52+
{ retry: 3 }
53+
);
54+
const response = await new OrchestrationClient(config, {
55+
maxRetries: 5
56+
}).invoke([], { timeout: 1 });
57+
58+
// expect(nock.isDone()).toBe(true);
59+
expect(response).toMatchInlineSnapshot(`
60+
{
61+
"id": [
62+
"langchain_core",
63+
"messages",
64+
"OrchestrationMessage",
65+
],
66+
"kwargs": {
67+
"additional_kwargs": {
68+
"finish_reason": "stop",
69+
"function_call": undefined,
70+
"index": 0,
71+
"tool_calls": undefined,
72+
},
73+
"content": "Hello! How can I assist you today?",
74+
"invalid_tool_calls": [],
75+
"response_metadata": {
76+
"created": 172,
77+
"finish_reason": "stop",
78+
"function_call": undefined,
79+
"id": "orchestration-id",
80+
"index": 0,
81+
"model": "gpt-35-turbo",
82+
"object": "chat.completion",
83+
"system_fingerprint": undefined,
84+
"tokenUsage": {
85+
"completionTokens": 9,
86+
"promptTokens": 9,
87+
"totalTokens": 18,
88+
},
89+
"tool_calls": undefined,
90+
},
91+
"tool_calls": [],
92+
},
93+
"lc": 1,
94+
"type": "constructor",
95+
}
96+
`);
97+
});
98+
});

packages/langchain/src/orchestration/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ export class OrchestrationClient extends BaseChatModel<
5454
Exclude<NewRunOutput, Error>,
5555
OrchestrationCallOptions
5656
> {
57-
// Delegate to the superclass pipe method and narrow the type.
5857
return super.pipe(coerceable) as Runnable<
5958
BaseLanguageModelInput,
6059
Exclude<NewRunOutput, Error>,

test-util/mock-http.ts

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,18 @@ import { readFile } from 'node:fs/promises';
22
import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import nock from 'nock';
5-
import { type EndpointOptions } from '@sap-ai-sdk/core';
6-
import {
7-
type FoundationModel,
8-
type DeploymentResolutionOptions
9-
} from '@sap-ai-sdk/ai-api/internal.js';
10-
import { dummyToken } from './mock-jwt.js';
115
import {
126
registerDestination,
137
type DestinationAuthToken,
148
type HttpDestination,
159
type ServiceCredentials
1610
} from '@sap-cloud-sdk/connectivity';
11+
import { type EndpointOptions } from '@sap-ai-sdk/core';
12+
import {
13+
type FoundationModel,
14+
type DeploymentResolutionOptions
15+
} from '@sap-ai-sdk/ai-api/internal.js';
16+
import { dummyToken } from './mock-jwt.js';
1717

1818
// Get the directory of this file
1919
const __filename = fileURLToPath(import.meta.url);
@@ -98,19 +98,33 @@ export function mockInference(
9898
data: any;
9999
status?: number;
100100
},
101-
endpoint: EndpointOptions = mockEndpoint
101+
endpoint: EndpointOptions = mockEndpoint,
102+
resilienceOptions?: {
103+
delay?: number;
104+
retry?: number;
105+
}
102106
): nock.Scope {
103107
const { url, apiVersion, resourceGroup = 'default' } = endpoint;
104108
const destination = getMockedAiCoreDestination();
105-
return nock(destination.url, {
109+
const scope = nock(destination.url, {
106110
reqheaders: {
107111
'ai-resource-group': resourceGroup,
108112
authorization: `Bearer ${destination.authTokens?.[0].value}`
109113
}
110-
})
111-
.post(`/v2/${url}`, request.data)
112-
.query(apiVersion ? { 'api-version': apiVersion } : {})
113-
.reply(response.status, response.data);
114+
});
115+
116+
let interceptor = scope.post(`/v2/${url}`, request.data).query(apiVersion ? { 'api-version': apiVersion } : {});
117+
118+
if (resilienceOptions?.retry) {
119+
interceptor.times(resilienceOptions.retry).reply(500);
120+
interceptor = scope.post(`/v2/${url}`, request.data).query(apiVersion ? { 'api-version': apiVersion } : {});
121+
}
122+
123+
if (resilienceOptions?.delay) {
124+
interceptor = interceptor.delay(resilienceOptions.delay);
125+
}
126+
127+
return interceptor.reply(response.status, response.data);
114128
}
115129

116130
/**

0 commit comments

Comments
 (0)