Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d6dd55b
feat: add dedicated Azure OpenAI provider using @ai-sdk/azure package
roomote Feb 1, 2026
91b0ff7
feat: add Azure provider UI component and translations
roomote Feb 1, 2026
0712696
feat: add Azure provider translations for all locales
roomote Feb 1, 2026
b2e2cc0
chore: add missing Azure placeholder translations
daniel-lxs Feb 1, 2026
1a73f4e
Delete .changeset/azure-ai-sdk-migration.md
mrubens Feb 2, 2026
7ac28ac
fix: add Azure provider validation for onboarding workflow
hannesrudolph Feb 3, 2026
926c23c
feat(azure): add model metadata, model picker, rename to Azure AI Fou…
hannesrudolph Feb 5, 2026
06dd495
fix(azure): add missing isAiSdkProvider() override for reasoning bloc…
hannesrudolph Feb 5, 2026
562e9ea
fix: upgrade @ai-sdk/azure to v3 and use deployment-based URLs
hannesrudolph Feb 6, 2026
d3fe52c
feat(azure): add all tool-calling models from models.dev/api.json
hannesrudolph Feb 6, 2026
4a8c037
refactor(azure): remove models with <128k context window
hannesrudolph Feb 6, 2026
47cbb06
refactor(azure): scope provider to Azure OpenAI models only
hannesrudolph Feb 6, 2026
723592d
refactor(azure): replace resourceName with baseURL and fix translations
hannesrudolph Feb 6, 2026
ae946fa
feat(azure): add URL auto-parser and improve settings UX
hannesrudolph Feb 7, 2026
e462255
fix(azure): stop auto-filling API version from pasted URL
hannesrudolph Feb 7, 2026
179ab88
refactor(azure): hide API version field in Welcome view
hannesrudolph Feb 7, 2026
f66fbff
feat(azure): change default model to gpt-5.2
hannesrudolph Feb 7, 2026
b331a14
fix(azure): coerce empty azureApiKey to undefined for env var fallback
roomote Feb 7, 2026
b7278ae
fix(settings): default reasoning effort from model definition
hannesrudolph Feb 7, 2026
d7be2b9
test: add 35 Azure provider verification tests
hannesrudolph Feb 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions packages/types/src/__tests__/azure-models.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type { ModelInfo } from "../model.js"
import { azureModels, azureDefaultModelId, azureDefaultModelInfo } from "../providers/azure.js"

// Object.entries loses the per-key literal types from `as const satisfies`,
// so we cast each value back to ModelInfo to access optional properties.
const modelEntries = Object.entries(azureModels) as [string, ModelInfo][]

describe("Azure model definitions", () => {
it("all models have required ModelInfo fields with valid values", () => {
for (const [id, info] of modelEntries) {
expect(info.maxTokens, `${id} maxTokens`).toBeGreaterThan(0)
expect(info.contextWindow, `${id} contextWindow`).toBeGreaterThan(0)
expect(typeof info.supportsImages, `${id} supportsImages`).toBe("boolean")
expect(typeof info.supportsPromptCache, `${id} supportsPromptCache`).toBe("boolean")
expect(info.inputPrice, `${id} inputPrice`).toBeGreaterThanOrEqual(0)
expect(info.outputPrice, `${id} outputPrice`).toBeGreaterThanOrEqual(0)
}
})

it("default model ID exists in model map", () => {
expect(azureModels[azureDefaultModelId]).toBeDefined()
})

it("default model info matches the default model ID entry", () => {
expect(azureDefaultModelInfo).toBe(azureModels[azureDefaultModelId])
})

it("models with supportsReasoningEffort have a valid reasoningEffort default", () => {
for (const [id, info] of modelEntries) {
if (Array.isArray(info.supportsReasoningEffort)) {
expect(info.reasoningEffort, `${id} missing reasoningEffort default`).toBeDefined()
expect(
info.supportsReasoningEffort,
`${id} reasoningEffort not in supportsReasoningEffort array`,
).toContain(info.reasoningEffort)
}
}
})

it("models claiming prompt cache support have cacheReadsPrice defined", () => {
for (const [id, info] of modelEntries) {
if (info.supportsPromptCache) {
// Azure models with cache support define cacheReadsPrice but not
// cacheWritesPrice — Azure does not charge separately for cache writes.
expect(info.cacheReadsPrice, `${id} supports cache but missing cacheReadsPrice`).toBeDefined()
}
}
})

it("maxTokens never exceeds contextWindow for any model", () => {
for (const [id, info] of modelEntries) {
expect(
info.maxTokens,
`${id} maxTokens (${info.maxTokens}) exceeds contextWindow (${info.contextWindow})`,
).toBeLessThanOrEqual(info.contextWindow)
}
})
})
33 changes: 32 additions & 1 deletion packages/types/src/__tests__/provider-settings.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getApiProtocol } from "../provider-settings.js"
import { getApiProtocol, providerSettingsSchemaDiscriminated } from "../provider-settings.js"

describe("getApiProtocol", () => {
describe("Anthropic-style providers", () => {
Expand Down Expand Up @@ -62,6 +62,37 @@ describe("getApiProtocol", () => {
})
})

describe("azure provider settings", () => {
it("accepts valid Azure config with all fields", () => {
const result = providerSettingsSchemaDiscriminated.safeParse({
apiProvider: "azure",
azureApiKey: "test-key-123",
azureBaseUrl: "https://my-resource.openai.azure.com/openai",
azureDeploymentName: "gpt-5.2",
azureApiVersion: "2024-10-21",
apiModelId: "gpt-5.2",
})
expect(result.success).toBe(true)
})

it("accepts Azure config without optional azureApiKey (managed identity)", () => {
const result = providerSettingsSchemaDiscriminated.safeParse({
apiProvider: "azure",
azureBaseUrl: "https://my-resource.openai.azure.com/openai",
azureDeploymentName: "gpt-4o",
})
expect(result.success).toBe(true)
})

it("rejects Azure config with invalid field types", () => {
const result = providerSettingsSchemaDiscriminated.safeParse({
apiProvider: "azure",
azureApiKey: 12345,
})
expect(result.success).toBe(false)
})
})

describe("Edge cases", () => {
it("should return 'openai' when provider is undefined", () => {
expect(getApiProtocol(undefined)).toBe("openai")
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export const SECRET_STATE_KEYS = [
"ioIntelligenceApiKey",
"vercelAiGatewayApiKey",
"basetenApiKey",
"azureApiKey",
] as const

// Global secrets that are part of GlobalSettings (not ProviderSettings)
Expand Down
17 changes: 17 additions & 0 deletions packages/types/src/provider-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export const providerNames = [
...customProviders,
...fauxProviders,
"anthropic",
"azure",
"bedrock",
"baseten",
"cerebras",
Expand Down Expand Up @@ -413,12 +414,20 @@ const basetenSchema = apiModelIdProviderModelSchema.extend({
basetenApiKey: z.string().optional(),
})

const azureSchema = apiModelIdProviderModelSchema.extend({
azureApiKey: z.string().optional(),
azureBaseUrl: z.string().optional(),
azureDeploymentName: z.string().optional(),
azureApiVersion: z.string().optional(),
})

const defaultSchema = z.object({
apiProvider: z.undefined(),
})

export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProvider", [
anthropicSchema.merge(z.object({ apiProvider: z.literal("anthropic") })),
azureSchema.merge(z.object({ apiProvider: z.literal("azure") })),
openRouterSchema.merge(z.object({ apiProvider: z.literal("openrouter") })),
bedrockSchema.merge(z.object({ apiProvider: z.literal("bedrock") })),
vertexSchema.merge(z.object({ apiProvider: z.literal("vertex") })),
Expand Down Expand Up @@ -460,6 +469,7 @@ export const providerSettingsSchemaDiscriminated = z.discriminatedUnion("apiProv
export const providerSettingsSchema = z.object({
apiProvider: providerNamesSchema.optional(),
...anthropicSchema.shape,
...azureSchema.shape,
...openRouterSchema.shape,
...bedrockSchema.shape,
...vertexSchema.shape,
Expand Down Expand Up @@ -548,6 +558,7 @@ export const isTypicalProvider = (key: unknown): key is TypicalProvider =>

export const modelIdKeysByProvider: Record<TypicalProvider, ModelIdKey> = {
anthropic: "apiModelId",
azure: "apiModelId",
openrouter: "openRouterModelId",
bedrock: "apiModelId",
vertex: "apiModelId",
Expand Down Expand Up @@ -624,6 +635,12 @@ export const MODELS_BY_PROVIDER: Record<
label: "Anthropic",
models: Object.keys(anthropicModels),
},
azure: {
id: "azure",
label: "Azure OpenAI",
// Azure uses deployment names configured by the user (not a fixed upstream model ID list)
models: [],
},
bedrock: {
id: "bedrock",
label: "Amazon Bedrock",
Expand Down
Loading
Loading