feat(ai): support Azure Foundry Chat Completions deployments (#9714)

* feat(ai): support Azure Foundry Chat Completions deployments

The Azure provider only implemented the Responses API, so Foundry
deployments that speak Chat Completions could not be used. On Chat
Completions the deepseek/deepseek-v4-pro model sends `thinking` and
`prompt_cache_key`, both of which Azure rejects with 400.

The provider now dispatches openai-completions alongside
azure-openai-responses. A stream wrapper resolves the Azure endpoint
onto the model first, since the shared openai-completions
implementation reads it off the model.

The request-shape differences are catalog compat rather than api code:
reasoning_effort instead of DeepSeek's thinking field, no prompt cache
parameters, reasoning_content kept on assistant turns so the prefix
Foundry cached stays byte-identical, and effort clamped to the
low/medium/high the deployment accepts.

The provider is renamed from azure-openai-responses to azure, since it
now serves more than the Responses API. The azure-openai-responses api
id is unchanged. Breaking: auth.json and models.json entries keyed on
azure-openai-responses must move to azure, and existing sessions see a
different provider on their next turn.

closes #9645

* feat(ai): apply the Azure deployment name map to Chat Completions

The Responses path already sent the deployment name from
AZURE_OPENAI_DEPLOYMENT_NAME_MAP. The Chat Completions path sent the
catalog model id, so any deployment not named after the model 404'd.

The Azure provider now swaps the deployment name into the request via
onPayload before dispatch, then runs the caller's own onPayload on it.
model.id stays the catalog id. The map helpers move to
azure-openai-config.ts so the provider can share them without
importing the SDK.

* docs(ai,coding-agent): add changelog entries for the Azure provider rename

Breaking: the azure-openai-responses provider is now azure. Users rename
the key in auth.json, models.json, and settings.json; SDK users switch to
getModel("azure", ...) and providers/azure.

Also note Azure Foundry Chat Completions support (DeepSeek V4 Pro).

---------

Co-authored-by: David Brailovsky <davidbrai@gmail.com>
This commit is contained in:
Justin Santer 2026-10-05 03:46:48 -04:00 • committed by GitHub
parent b2b5c42f61
commit a37306d437
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 559 additions and 188 deletions

View file

@ -290,6 +290,15 @@ const DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP = {
...DEEPSEEK_V4_THINKING_LEVEL_MAP,
low: "low",
} as const;
// Azure Foundry rejects DeepSeek's own max effort.
const AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP = {
minimal: null,
low: "low",
medium: "medium",
high: "high",
xhigh: null,
max: null,
} as const;
// Verified against Fireworks Messages raw_output on 2026-09-10 (#9323).
// Fall back to verified support when models.dev omits effort metadata; this is
// not an allowlist. Any Fireworks Messages model advertising effort uses adaptive thinking.
@ -870,7 +879,7 @@ function applyStrictToolCompatMetadata(model: Model<Api>): void {
const OPENAI_GRAMMAR_TOOL_PROVIDERS = new Set([
"openai",
"openai-codex",
"azure-openai-responses",
"azure",
"github-copilot",
"opencode",
"cloudflare-ai-gateway",
@ -1121,10 +1130,12 @@ function applyThinkingLevelMetadata(model: Model<any>): void {
model,
model.provider === "openrouter"
? { ...DEEPSEEK_V4_THINKING_LEVEL_MAP, xhigh: "xhigh", max: null }
: (model.provider === "deepseek" || model.provider === "opencode" || model.provider === "opencode-go") &&
model.id.includes("deepseek-v4-flash")
? DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP
: DEEPSEEK_V4_THINKING_LEVEL_MAP,
: model.provider === "azure"
? AZURE_DEEPSEEK_V4_THINKING_LEVEL_MAP
: (model.provider === "deepseek" || model.provider === "opencode" || model.provider === "opencode-go") &&
model.id.includes("deepseek-v4-flash")
? DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP
: DEEPSEEK_V4_THINKING_LEVEL_MAP,
);
}
if (model.provider === "groq" && model.id === "qwen/qwen3.6-27b") {
@ -3427,7 +3438,7 @@ async function generateModels() {
.map((model) => ({
...model,
api: "azure-openai-responses",
provider: "azure-openai-responses",
provider: "azure",
baseUrl: "",
cost: {
input: model.cost.input,
@ -3439,6 +3450,28 @@ async function generateModels() {
}));
allModels.push(...azureOpenAiModels);
// Azure resells DeepSeek at its own rates. US data zone, checked 2026-09-16.
// https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/deepseek/
const AZURE_DEEPSEEK_V4_PRO_COST: ModelCost = { input: 1.925, output: 3.828, cacheRead: 0.165, cacheWrite: 0 };
// Azure 400s on DeepSeek's `thinking` field and on every prompt cache parameter, discards a
// `developer` system message unbilled once reasoning_effort is set, and honours mid-convo ones (#9645).
const azureDeepSeekModels: Model<Api>[] = allModels
.filter((model) => model.provider === "deepseek" && model.id === "deepseek-v4-pro")
.map((model) => ({
...model,
provider: "azure",
baseUrl: "",
cost: AZURE_DEEPSEEK_V4_PRO_COST,
compat: {
...(model.compat as OpenAICompletionsCompat),
supportsDeveloperRole: false,
supportsMidConvoSystemMessages: true,
thinkingFormat: "openai",
supportsLongCacheRetention: false,
},
}));
allModels.push(...azureDeepSeekModels);
for (const model of allModels) {
applyOpenAICompletionsCompatMetadata(model);
applyAnthropicMessagesCompatMetadata(model);