Skip to content

Commit f7f626f

Browse files
authored
Merge branch 'main' into docs
2 parents 49a8311 + 1650766 commit f7f626f

File tree

7 files changed

+156
-9
lines changed

7 files changed

+156
-9
lines changed

.github/workflows/run_tests.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Run Gateway tests
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize, reopened]
6+
7+
jobs:
8+
gateway-tests:
9+
runs-on: ubuntu-latest
10+
environment: production
11+
steps:
12+
- name: Checkout head
13+
uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0
16+
- name: Install dependencies
17+
run: npm ci
18+
- name: Build
19+
run: npm run build
20+
- name: Start gateway and run tests
21+
run: |
22+
npm run build/start-server.js &
23+
echo "Waiting for gateway to start..."
24+
while ! curl -s http://localhost:8787 > /dev/null; do
25+
sleep 1
26+
done
27+
echo "Gateway is ready. Running tests..."
28+
npm run test:gateway
29+
30+
env:
31+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
32+
COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@
3434
"start:node": "node build/start-server.js",
3535
"format": "prettier --write \"./**/*.{js,jsx,ts,tsx,json}\"",
3636
"format:check": "prettier --check \"./**/*.{js,jsx,ts,tsx,json}\"",
37-
"test": "ts-jest",
37+
"test:gateway": "jest src/",
38+
"test:plugins": "jest plugins/",
3839
"pre-push": "npm run build && node start-test.js",
3940
"prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){process.exit(1)} \" || husky"
4041
},

src/handlers/handlerUtils.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,13 @@ export function constructConfigFromRequestHeaders(
10131013
resourceName: requestHeaders[`x-${POWERED_BY}-azure-resource-name`],
10141014
deploymentId: requestHeaders[`x-${POWERED_BY}-azure-deployment-id`],
10151015
apiVersion: requestHeaders[`x-${POWERED_BY}-azure-api-version`],
1016+
azureAuthMode: requestHeaders[`x-${POWERED_BY}-azure-auth-mode`],
1017+
azureManagedClientId:
1018+
requestHeaders[`x-${POWERED_BY}-azure-managed-client-id`],
1019+
azureEntraClientId: requestHeaders[`x-${POWERED_BY}-azure-entra-client-id`],
1020+
azureEntraClientSecret:
1021+
requestHeaders[`x-${POWERED_BY}-azure-entra-client-secret`],
1022+
azureEntraTenantId: requestHeaders[`x-${POWERED_BY}-azure-entra-tenant-id`],
10161023
azureModelName: requestHeaders[`x-${POWERED_BY}-azure-model-name`],
10171024
};
10181025

src/providers/azure-openai/api.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,46 @@
11
import { ProviderAPIConfig } from '../types';
2+
import {
3+
getAccessTokenFromEntraId,
4+
getAzureManagedIdentityToken,
5+
} from './utils';
26

37
const AzureOpenAIAPIConfig: ProviderAPIConfig = {
48
getBaseURL: ({ providerOptions }) => {
59
const { resourceName, deploymentId } = providerOptions;
610
return `https://${resourceName}.openai.azure.com/openai/deployments/${deploymentId}`;
711
},
8-
headers: ({ providerOptions, fn }) => {
12+
headers: async ({ providerOptions, fn }) => {
13+
const { apiKey, azureAuthMode } = providerOptions;
14+
15+
if (azureAuthMode === 'entra') {
16+
const { azureEntraTenantId, azureEntraClientId, azureEntraClientSecret } =
17+
providerOptions;
18+
if (azureEntraTenantId && azureEntraClientId && azureEntraClientSecret) {
19+
const scope = 'https://cognitiveservices.azure.com/.default';
20+
const accessToken = await getAccessTokenFromEntraId(
21+
azureEntraTenantId,
22+
azureEntraClientId,
23+
azureEntraClientSecret,
24+
scope
25+
);
26+
return {
27+
Authorization: `Bearer ${accessToken}`,
28+
};
29+
}
30+
}
31+
if (azureAuthMode === 'managed') {
32+
const { azureManagedClientId } = providerOptions;
33+
const resource = 'https://cognitiveservices.azure.com/';
34+
const accessToken = await getAzureManagedIdentityToken(
35+
resource,
36+
azureManagedClientId
37+
);
38+
return {
39+
Authorization: `Bearer ${accessToken}`,
40+
};
41+
}
942
const headersObj: Record<string, string> = {
10-
'api-key': `${providerOptions.apiKey}`,
43+
'api-key': `${apiKey}`,
1144
};
1245
if (fn === 'createTranscription' || fn === 'createTranslation')
1346
headersObj['Content-Type'] = 'multipart/form-data';
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
export async function getAccessTokenFromEntraId(
2+
tenantId: string,
3+
clientId: string,
4+
clientSecret: string,
5+
scope = 'https://cognitiveservices.azure.com/.default'
6+
) {
7+
try {
8+
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
9+
const params = new URLSearchParams({
10+
client_id: clientId,
11+
client_secret: clientSecret,
12+
scope: scope,
13+
grant_type: 'client_credentials',
14+
});
15+
16+
const response = await fetch(url, {
17+
method: 'POST',
18+
headers: {
19+
'Content-Type': 'application/x-www-form-urlencoded',
20+
},
21+
body: params,
22+
});
23+
24+
if (!response.ok) {
25+
const errorMessage = await response.text();
26+
console.log({ message: `Error from Entra ${errorMessage}` });
27+
return undefined;
28+
}
29+
const data: { access_token: string } = await response.json();
30+
return data.access_token;
31+
} catch (error) {
32+
console.log(error);
33+
}
34+
}
35+
36+
export async function getAzureManagedIdentityToken(
37+
resource: string,
38+
clientId?: string
39+
) {
40+
try {
41+
const response = await fetch(
42+
`http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=${encodeURIComponent(resource)}${clientId ? `&client_id=${encodeURIComponent(clientId)}` : ''}`,
43+
{
44+
method: 'GET',
45+
headers: {
46+
Metadata: 'true',
47+
},
48+
}
49+
);
50+
if (!response.ok) {
51+
const errorMessage = await response.text();
52+
console.log({ message: `Error from Managed ${errorMessage}` });
53+
return undefined;
54+
}
55+
const data: { access_token: string } = await response.json();
56+
return data.access_token;
57+
} catch (error) {
58+
console.log({ error });
59+
}
60+
}

src/tests/routeSpecificTestFunctions.ts/chatCompletion.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,20 @@ export const executeChatCompletionEndpointTests: (
2121
}
2222

2323
test(`${providerName} /chat/completions test message strings`, async () => {
24-
const request = new Request(CHAT_COMPLETIONS_ENDPOINT, {
24+
const res = await fetch(CHAT_COMPLETIONS_ENDPOINT, {
2525
method: 'POST',
2626
headers: createDefaultHeaders(providerName, apiKey),
2727
body: getChatCompleteWithMessageStringRequest(model),
2828
});
29-
const res = await app.fetch(request);
30-
expect(res.status).toBe(200);
29+
expect(res.status).toEqual(200);
3130
});
3231

3332
test(`${providerName} /chat/completions test message content arrays`, async () => {
34-
const request = new Request(CHAT_COMPLETIONS_ENDPOINT, {
33+
const res = await fetch(CHAT_COMPLETIONS_ENDPOINT, {
3534
method: 'POST',
3635
headers: createDefaultHeaders(providerName, apiKey),
3736
body: getChatCompleteWithMessageContentArraysRequest(model),
3837
});
39-
const res = await app.fetch(request);
40-
expect(res.status).toBe(200);
38+
expect(res.status).toEqual(200);
4139
});
4240
};

src/types/requestBody.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,11 @@ export interface Options {
6060
apiVersion?: string;
6161
adAuth?: string;
6262
azureModelName?: string;
63+
azureAuthMode?: string; // can be entra or managed
64+
azureManagedClientId?: string;
65+
azureEntraClientId?: string;
66+
azureEntraClientSecret?: string;
67+
azureEntraTenantId?: string;
6368
/** Workers AI specific */
6469
workersAiAccountId?: string;
6570
/** The parameter to set custom base url */
@@ -143,6 +148,12 @@ export interface Targets {
143148
deploymentId?: string;
144149
apiVersion?: string;
145150
adAuth?: string;
151+
azureAuthMode?: string;
152+
azureManagedClientId?: string;
153+
azureEntraClientId?: string;
154+
azureEntraClientSecret?: string;
155+
azureEntraTenantId?: string;
156+
azureModelName?: string;
146157
/** provider option index picked based on weight in loadbalance mode */
147158
index?: number;
148159
cache?: CacheSettings | string;
@@ -356,6 +367,11 @@ export interface ShortConfig {
356367
azureModelName?: string;
357368
workersAiAccountId?: string;
358369
apiVersion?: string;
370+
azureAuthMode?: string;
371+
azureManagedClientId?: string;
372+
azureEntraClientId?: string;
373+
azureEntraClientSecret?: string;
374+
azureEntraTenantId?: string;
359375
customHost?: string;
360376
// Google Vertex AI specific
361377
vertexRegion?: string;

0 commit comments

Comments
 (0)