Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions packages/core/src/plugin/provider/azure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { define } from "../internal"
import { ProviderV2 } from "../../provider"

function selectLanguage(sdk: any, modelID: string, useChat: boolean) {
// Azure DeepSeek deployments need the DeepSeek-specific adapter for reasoning
// effort and reasoning_content across agentic turns; it is chat-completions
// based, so it also applies when useCompletionUrls is set.
// Note: this predicate (any "deepseek" deployment) deliberately differs from
// the "deepseek-v4" gate in transform.ts reasoningVariants, which only
// exposes a max effort for v4 models.
// Keep in sync with packages/opencode/src/provider/provider.ts selectAzureLanguageModel.
if (modelID.toLowerCase().includes("deepseek") && sdk.deepseek) return sdk.deepseek(modelID)
if (useChat && sdk.chat) return sdk.chat(modelID)
if (sdk.responses) return sdk.responses(modelID)
if (sdk.messages) return sdk.messages(modelID)
Expand Down
59 changes: 59 additions & 0 deletions packages/core/test/plugin/provider-azure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ function fakeSelectorSdk(calls: string[]) {
responses: make("responses"),
messages: make("messages"),
chat: make("chat"),
deepseek: make("deepseek"),
languageModel: make("languageModel"),
}
}
Expand Down Expand Up @@ -261,6 +262,64 @@ describe("AzurePlugin", () => {
}),
)

it.effect("selects deepseek adapter for DeepSeek deployments", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
yield* addPlugin()
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deepseek-v4-pro")),
api: { id: ModelV2.ID.make("deepseek-v4-pro"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: {},
})
expect(calls).toEqual(["deepseek:deepseek-v4-pro"])
}),
)

it.effect("keeps the deepseek adapter when useCompletionUrls is set", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
yield* addPlugin()
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deepseek-v4-pro")),
api: { id: ModelV2.ID.make("deepseek-v4-pro"), type: "aisdk", package: "test-provider" },
}),
sdk: fakeSelectorSdk(calls),
options: { useCompletionUrls: true },
})
expect(calls).toEqual(["deepseek:deepseek-v4-pro"])
}),
)

it.effect("falls back to responses when the sdk has no deepseek adapter", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
const aisdk = yield* AISDK.Service
const calls: string[] = []
const make = (method: string) => (id: string) => {
calls.push(`${method}:${id}`)
return { modelId: id, provider: method, specificationVersion: "v3" }
}
yield* addPlugin()
yield* aisdk.runLanguage({
model: ModelV2.Info.make({
...ModelV2.Info.empty(ProviderV2.ID.azure, ModelV2.ID.make("deepseek-v4-pro")),
api: { id: ModelV2.ID.make("deepseek-v4-pro"), type: "aisdk", package: "test-provider" },
}),
sdk: { responses: make("responses"), chat: make("chat"), languageModel: make("languageModel") },
options: {},
})
expect(calls).toEqual(["responses:deepseek-v4-pro"])
}),
)

it.effect("falls back through the legacy Azure selector order", () =>
Effect.gen(function* () {
const plugin = yield* PluginV2.Service
Expand Down
8 changes: 8 additions & 0 deletions packages/opencode/src/provider/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ type CustomDep = {
}

function selectAzureLanguageModel(sdk: any, modelID: string, useChat: boolean) {
// Azure DeepSeek deployments need the DeepSeek-specific adapter for reasoning
// effort and reasoning_content across agentic turns; it is chat-completions
// based, so it also applies when useCompletionUrls is set.
// Note: this predicate (any "deepseek" deployment) deliberately differs from
// the "deepseek-v4" gate in transform.ts reasoningVariants, which only
// exposes a max effort for v4 models.
// Keep in sync with packages/core/src/plugin/provider/azure.ts selectLanguage.
if (modelID.toLowerCase().includes("deepseek") && sdk.deepseek) return sdk.deepseek(modelID)
if (useChat && sdk.chat) return sdk.chat(modelID)
if (sdk.responses) return sdk.responses(modelID)
if (sdk.messages) return sdk.messages(modelID)
Expand Down
7 changes: 6 additions & 1 deletion packages/opencode/src/provider/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,8 +941,13 @@ export function variants(model: Provider.Model): Record<string, Record<string, a
case "@ai-sdk/azure":
// https://v5.ai-sdk.dev/providers/ai-sdk-providers/azure
if (id === "o1-mini") return {}
const azureEfforts = [...openaiReasoningEfforts(id, model.release_date)]
// Only deepseek-v4 deployments expose a max effort; the adapter
// selection in the azure loaders gates on any "deepseek" deployment,
// which is deliberately broader.
if (model.api.id.toLowerCase().includes("deepseek-v4") && !azureEfforts.includes("max")) azureEfforts.push("max")
return Object.fromEntries(
openaiReasoningEfforts(id, model.release_date).map((effort) => [
azureEfforts.map((effort) => [
effort,
{
reasoningEffort: effort,
Expand Down
65 changes: 65 additions & 0 deletions packages/opencode/test/provider/provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,71 @@ it.instance("getSmallModel skips inferred models for Azure Cognitive Services",
}),
)

const azureTestConfig = {
provider: {
azure: {
npm: "@ai-sdk/azure",
options: { resourceName: "test-resource", apiKey: "test-key" },
models: {
"deepseek-v4-pro": { name: "DeepSeek V4 Pro" },
"gpt-5": { name: "GPT-5" },
},
},
},
}

it.instance(
"azure deepseek deployments select the DeepSeek adapter",
Effect.gen(function* () {
yield* set("AZURE_RESOURCE_NAME", "test-resource")
yield* set("AZURE_API_KEY", "test-key")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.azure, ModelV2.ID.make("deepseek-v4-pro"))
const language = yield* provider.getLanguage(model)
expect(language.provider).toBe("azure.deepseek")
}),
{ config: azureTestConfig },
)

it.instance(
"azure deepseek deployments keep the DeepSeek adapter with useCompletionUrls",
Effect.gen(function* () {
yield* set("AZURE_RESOURCE_NAME", "test-resource")
yield* set("AZURE_API_KEY", "test-key")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.azure, ModelV2.ID.make("deepseek-v4-pro"))
const language = yield* provider.getLanguage({ ...model, options: { ...model.options, useCompletionUrls: true } })
expect(language.provider).toBe("azure.deepseek")
}),
{ config: azureTestConfig },
)

it.instance(
"azure non-deepseek deployments keep responses routing",
Effect.gen(function* () {
yield* set("AZURE_RESOURCE_NAME", "test-resource")
yield* set("AZURE_API_KEY", "test-key")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.azure, ModelV2.ID.make("gpt-5"))
const language = yield* provider.getLanguage(model)
expect(language.provider).toBe("azure.responses")
}),
{ config: azureTestConfig },
)

it.instance(
"azure non-deepseek deployments keep chat routing with useCompletionUrls",
Effect.gen(function* () {
yield* set("AZURE_RESOURCE_NAME", "test-resource")
yield* set("AZURE_API_KEY", "test-key")
const provider = yield* Provider.Service
const model = yield* provider.getModel(ProviderV2.ID.azure, ModelV2.ID.make("gpt-5"))
const language = yield* provider.getLanguage({ ...model, options: { ...model.options, useCompletionUrls: true } })
expect(language.provider).toBe("azure.chat")
}),
{ config: azureTestConfig },
)

it.instance(
"getSmallModel respects config small_model override",
Effect.gen(function* () {
Expand Down
29 changes: 29 additions & 0 deletions packages/opencode/test/provider/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3702,6 +3702,35 @@ describe("ProviderTransform.variants", () => {
expect(result).toEqual({})
})

test("azure deepseek-v4 deployments include a max reasoning effort variant", () => {
const model = createMockModel({
id: "azure/deepseek-v4-pro",
providerID: "azure",
api: {
id: "deepseek-v4-pro",
url: "https://azure.com",
npm: "@ai-sdk/azure",
},
})
const result = ProviderTransform.variants(model)
expect(Object.keys(result)).toEqual(["low", "medium", "high", "max"])
expect(result.max).toMatchObject({ reasoningEffort: "max" })
})

test("azure non-deepseek deployments do not include a max reasoning effort variant", () => {
const model = createMockModel({
id: "azure/gpt-5",
providerID: "azure",
api: {
id: "gpt-5",
url: "https://azure.com",
npm: "@ai-sdk/azure",
},
})
const result = ProviderTransform.variants(model)
expect(result.max).toBeUndefined()
})

test("minimax returns empty object", () => {
const model = createMockModel({
id: "minimax/minimax-model",
Expand Down
Loading