diff --git a/packages/core/src/plugin/provider/azure.ts b/packages/core/src/plugin/provider/azure.ts index 2e1f9d9b48f4..2388c032e36a 100644 --- a/packages/core/src/plugin/provider/azure.ts +++ b/packages/core/src/plugin/provider/azure.ts @@ -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) diff --git a/packages/core/test/plugin/provider-azure.test.ts b/packages/core/test/plugin/provider-azure.test.ts index 1d8172854cff..990c6069f978 100644 --- a/packages/core/test/plugin/provider-azure.test.ts +++ b/packages/core/test/plugin/provider-azure.test.ts @@ -55,6 +55,7 @@ function fakeSelectorSdk(calls: string[]) { responses: make("responses"), messages: make("messages"), chat: make("chat"), + deepseek: make("deepseek"), languageModel: make("languageModel"), } } @@ -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 diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index dba9cbece552..fc678952878c 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -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) diff --git a/packages/opencode/src/provider/transform.ts b/packages/opencode/src/provider/transform.ts index 0667fc2eb098..9153ddee145e 100644 --- a/packages/opencode/src/provider/transform.ts +++ b/packages/opencode/src/provider/transform.ts @@ -941,8 +941,13 @@ export function variants(model: Provider.Model): Record [ + azureEfforts.map((effort) => [ effort, { reasoningEffort: effort, diff --git a/packages/opencode/test/provider/provider.test.ts b/packages/opencode/test/provider/provider.test.ts index 32d4e3a39b10..df236f657cfe 100644 --- a/packages/opencode/test/provider/provider.test.ts +++ b/packages/opencode/test/provider/provider.test.ts @@ -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* () { diff --git a/packages/opencode/test/provider/transform.test.ts b/packages/opencode/test/provider/transform.test.ts index 97f0de281483..864e5be4da33 100644 --- a/packages/opencode/test/provider/transform.test.ts +++ b/packages/opencode/test/provider/transform.test.ts @@ -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",