Skip to content

Commit dc803d8

Browse files
authored
Merge pull request #6023 from continuedev/pe/watsonx-vi
feat: use correct deployment for azure
2 parents fae5c5d + 9f92372 commit dc803d8

File tree

8 files changed

+267
-121
lines changed

8 files changed

+267
-121
lines changed

.github/workflows/pr_checks.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ jobs:
435435
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
436436
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
437437
AZURE_FOUNDRY_API_KEY: ${{ secrets.AZURE_FOUNDRY_API_KEY }}
438+
AZURE_FOUNDRY_MISTRAL_SMALL_API_KEY: ${{ secrets.AZURE_FOUNDRY_MISTRAL_SMALL_API_KEY }}
439+
AZURE_OPENAI_GPT41_API_KEY: ${{ secrets.AZURE_OPENAI_GPT41_API_KEY }}
438440

439441
package-tests:
440442
runs-on: ubuntu-latest
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { OpenAI } from "openai/index";
2+
import {
3+
ChatCompletionChunk,
4+
ChatCompletionCreateParams,
5+
ChatCompletionCreateParamsStreaming,
6+
} from "openai/resources/index";
7+
import { z } from "zod";
8+
import { AzureConfigSchema } from "../types.js";
9+
import { customFetch } from "../util.js";
10+
import { OpenAIApi } from "./OpenAI.js";
11+
12+
export class AzureApi extends OpenAIApi {
13+
constructor(private azureConfig: z.infer<typeof AzureConfigSchema>) {
14+
super({
15+
...azureConfig,
16+
provider: "openai",
17+
});
18+
19+
this.openai = new OpenAI({
20+
apiKey: azureConfig.apiKey,
21+
baseURL: this._getAzureBaseURL(azureConfig),
22+
defaultQuery: azureConfig.env?.apiVersion
23+
? { "api-version": azureConfig.env.apiVersion }
24+
: {},
25+
fetch: customFetch(azureConfig.requestOptions),
26+
});
27+
}
28+
29+
/**
30+
* Default is `azure-openai`, but previously was `azure`
31+
* @param apiType
32+
* @returns
33+
*/
34+
private _isAzureOpenAI(apiType?: string): boolean {
35+
return apiType === "azure-openai" || apiType === "azure";
36+
}
37+
38+
private _getAzureBaseURL(config: z.infer<typeof AzureConfigSchema>): string {
39+
const baseURL = new URL(this.apiBase).toString().replace(/\/$/, "");
40+
41+
// Default is `azure-openai` in docs, but previously was `azure`
42+
if (this._isAzureOpenAI(config.env?.apiType)) {
43+
if (!config.env?.deployment) {
44+
throw new Error(
45+
"Azure deployment is required if `apiType` is `azure-openai` or `azure`",
46+
);
47+
}
48+
49+
return `${baseURL}/openai/deployments/${config.env.deployment}`;
50+
}
51+
52+
return baseURL;
53+
}
54+
55+
/**
56+
* Filters out empty text content parts from messages.
57+
*
58+
* Azure models may not support empty content parts, which can cause issues.
59+
* This function removes any text content parts that are empty or contain only whitespace.
60+
*/
61+
private _filterEmptyContentParts<T extends ChatCompletionCreateParams>(
62+
body: T,
63+
): T {
64+
const result = { ...body };
65+
66+
result.messages = result.messages.map((message: any) => {
67+
if (Array.isArray(message.content)) {
68+
const filteredContent = message.content.filter((part: any) => {
69+
return !(
70+
part.type === "text" &&
71+
(!part.text || part.text.trim() === "")
72+
);
73+
});
74+
return {
75+
...message,
76+
content:
77+
filteredContent.length > 0 ? filteredContent : message.content,
78+
};
79+
}
80+
return message;
81+
}) as any;
82+
83+
return result;
84+
}
85+
86+
modifyChatBody<T extends ChatCompletionCreateParams>(body: T): T {
87+
let modifiedBody = super.modifyChatBody(body);
88+
modifiedBody = this._filterEmptyContentParts(modifiedBody);
89+
return modifiedBody;
90+
}
91+
92+
async *chatCompletionStream(
93+
body: ChatCompletionCreateParamsStreaming,
94+
signal: AbortSignal,
95+
): AsyncGenerator<ChatCompletionChunk, any, unknown> {
96+
const response = await this.openai.chat.completions.create(
97+
this.modifyChatBody(body),
98+
{ signal },
99+
);
100+
101+
for await (const result of response) {
102+
// Skip chunks with no choices (common with Azure content filtering)
103+
if (result.choices && result.choices.length > 0) {
104+
yield result;
105+
}
106+
}
107+
}
108+
}

packages/openai-adapters/src/apis/OpenAI.ts

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
Model,
1313
} from "openai/resources/index";
1414
import { z } from "zod";
15-
import { AzureConfigSchema, OpenAIConfigSchema } from "../types.js";
15+
import { OpenAIConfigSchema } from "../types.js";
1616
import { customFetch } from "../util.js";
1717
import {
1818
BaseLlmApi,
@@ -25,19 +25,14 @@ export class OpenAIApi implements BaseLlmApi {
2525
openai: OpenAI;
2626
apiBase: string = "https://api.openai.com/v1/";
2727

28-
constructor(
29-
protected config: z.infer<
30-
typeof OpenAIConfigSchema | typeof AzureConfigSchema
31-
>,
32-
) {
28+
constructor(protected config: z.infer<typeof OpenAIConfigSchema>) {
3329
this.apiBase = config.apiBase ?? this.apiBase;
3430
this.openai = new OpenAI({
3531
apiKey: config.apiKey,
3632
baseURL: this.apiBase,
3733
fetch: customFetch(config.requestOptions),
3834
});
3935
}
40-
4136
modifyChatBody<T extends ChatCompletionCreateParams>(body: T): T {
4237
// o-series models - only apply for official OpenAI API
4338
const isOfficialOpenAIAPI = this.apiBase === "https://api.openai.com/v1/";
@@ -74,6 +69,7 @@ export class OpenAIApi implements BaseLlmApi {
7469
);
7570
return response;
7671
}
72+
7773
async *chatCompletionStream(
7874
body: ChatCompletionCreateParamsStreaming,
7975
signal: AbortSignal,

packages/openai-adapters/src/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import dotenv from "dotenv";
22
import { z } from "zod";
33
import { AnthropicApi } from "./apis/Anthropic.js";
4+
import { AzureApi } from "./apis/Azure.js";
45
import { CohereApi } from "./apis/Cohere.js";
56
import { DeepSeekApi } from "./apis/DeepSeek.js";
67
import { GeminiApi } from "./apis/Gemini.js";
@@ -29,8 +30,9 @@ function openAICompatible(
2930
export function constructLlmApi(config: LLMConfig): BaseLlmApi | undefined {
3031
switch (config.provider) {
3132
case "openai":
32-
case "azure":
3333
return new OpenAIApi(config);
34+
case "azure":
35+
return new AzureApi(config);
3436
case "cohere":
3537
return new CohereApi(config);
3638
case "anthropic":

packages/openai-adapters/src/test/main.test.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import { DEEPSEEK_API_BASE } from "../apis/DeepSeek.js";
44
import { INCEPTION_API_BASE } from "../apis/Inception.js";
55
import { OpenAIApi } from "../apis/OpenAI.js";
66
import { constructLlmApi } from "../index.js";
7-
import { getLlmApi, testChat, testCompletion, testEmbed } from "./util.js";
7+
import { getLlmApi, testChat, testEmbed } from "./util.js";
88

99
dotenv.config();
1010

11-
function testConfig(config: ModelConfig) {
11+
export interface TestConfigOptions {
12+
skipTools: boolean;
13+
}
14+
15+
function testConfig(_config: ModelConfig & { options?: TestConfigOptions }) {
16+
const { options, ...config } = _config;
1217
const model = config.model;
1318
const api = getLlmApi({
1419
provider: config.provider as any,
@@ -17,16 +22,12 @@ function testConfig(config: ModelConfig) {
1722
env: config.env,
1823
});
1924

20-
if (false) {
21-
testCompletion(api, model);
22-
}
23-
2425
if (
2526
["chat", "summarize", "edit", "apply"].some((role) =>
2627
config.roles?.includes(role as any),
2728
)
2829
) {
29-
testChat(api, model);
30+
testChat(api, model, options);
3031
}
3132

3233
if (config.roles?.includes("embed")) {
@@ -38,7 +39,7 @@ function testConfig(config: ModelConfig) {
3839
}
3940
}
4041

41-
const TESTS: Omit<ModelConfig, "name">[] = [
42+
const TESTS: Omit<ModelConfig & { options?: TestConfigOptions }, "name">[] = [
4243
{
4344
provider: "openai",
4445
model: "gpt-4o",
@@ -106,6 +107,26 @@ const TESTS: Omit<ModelConfig, "name">[] = [
106107
// apiKey: process.env.COHERE_API_KEY!,
107108
// roles: ["rerank"],
108109
// },
110+
{
111+
provider: "azure",
112+
model: "gpt-4.1",
113+
apiBase: "https://continue-openai.openai.azure.com",
114+
apiKey: process.env.AZURE_OPENAI_GPT41_API_KEY,
115+
env: {
116+
deployment: "gpt-4.1",
117+
apiVersion: "2024-02-15-preview",
118+
apiType: "azure-openai",
119+
},
120+
roles: ["chat"],
121+
},
122+
{
123+
provider: "azure",
124+
model: "mistral-small-2503",
125+
apiBase: "https://nate-0276-resource.services.ai.azure.com/models",
126+
apiKey: process.env.AZURE_FOUNDRY_MISTRAL_SMALL_API_KEY,
127+
roles: ["chat"],
128+
options: { skipTools: true },
129+
},
109130
];
110131

111132
TESTS.forEach((config) => {

0 commit comments

Comments
 (0)