fix(azure): match the generation, not one release, for max_completion_tokens (#13007)

Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
This commit is contained in:
Nguyen Thanh Dat
2026-09-11 04:12:57 +07:00
committed by GitHub
parent a6f28210de
commit 4edc3d57d0
3 changed files with 43 additions and 2 deletions

View File

@@ -0,0 +1 @@
- **fix(azure):** Deployments from GPT-6 onward now send `max_completion_tokens` instead of `max_tokens`, which Azure rejects with HTTP 400. The rule matched a literal `gpt-5`, so each new generation arrived broken; it now matches the generation range, while `gpt-35-turbo` still keeps `max_tokens`.

View File

@@ -20,15 +20,24 @@
/**
* Deployments that require `max_completion_tokens` instead of `max_tokens`.
*
* Matches the GPT-5 family and the o1/o3/o4 reasoning series at a token
* Matches GPT-5 and later, and the o1/o3/o4 reasoning series, at a token
* boundary, so a deployment named `my-gpt-5-prod` matches while an unrelated
* `piston-o4-legacy`-style name does not match by accident. `gpt-chat-latest`
* is listed explicitly: it is a moving alias that currently resolves to a
* GPT-5-era model and rejects `max_tokens`, but carries no version number for
* the boundary pattern to key on.
*
* The generation is a range rather than a literal `gpt-5`, because the rule is
* a property of the generation and not of one release: `gpt-6-astra` rejects
* `max_tokens` for exactly the reason `gpt-5` does, and pinning the literal
* meant every new family arrived broken (#12981).
*
* It is a range and not `\d+` on purpose. Azure's own name for GPT-3.5 is
* `gpt-35-turbo`, which takes `max_tokens` and would be caught by a digit-run.
* `1\d` keeps a future `gpt-10` working without letting `gpt-35` in.
*/
export const AZURE_COMPLETION_TOKEN_DEPLOYMENT =
/(?:^|[/_-])(?:gpt-5|o(?:1|3|4))(?:[._-]|$)|^gpt-chat-latest$/i;
/(?:^|[/_-])(?:gpt-(?:[5-9]|1\d)|o(?:1|3|4))(?:[._-]|$)|^gpt-chat-latest$/i;
/**
* Apply the Azure param rules to an already-translated Chat Completions body.

View File

@@ -46,6 +46,37 @@ test("gpt-5 family converts max_tokens too", () => {
}
});
test("generations after GPT-5 convert max_tokens too (#12981)", () => {
// The rule belongs to the generation, not to one release. gpt-6-astra is the
// deployment from the report; the rest are the next names Azure will use.
for (const model of ["gpt-6-astra", "gpt-6", "azure/gpt-7-mini", "gpt-9.1", "gpt-10-turbo"]) {
const out = applyAzureParamRules(model, { max_tokens: 100 }, { max_tokens: 100 }) as Record<
string,
unknown
>;
assert.equal(out.max_tokens, undefined, `${model} should drop max_tokens`);
assert.equal(out.max_completion_tokens, 100, `${model} should set max_completion_tokens`);
}
});
test("gpt-35-turbo is not a GPT-3.5 deployment caught by the generation range", () => {
// Azure's own name for GPT-3.5 has no dot, so a digit-run like `gpt-\d+`
// would match it and strip the max_tokens it actually requires. This is why
// the pattern is a range and stops at 19.
for (const model of ["gpt-35-turbo", "gpt-35-turbo-16k", "azure/gpt-35"]) {
assert.equal(
AZURE_COMPLETION_TOKEN_DEPLOYMENT.test(model),
false,
`${model} must keep max_tokens`
);
const out = applyAzureParamRules(model, { max_tokens: 100 }, { max_tokens: 100 }) as Record<
string,
unknown
>;
assert.equal(out.max_tokens, 100, `${model} should pass through untouched`);
}
});
test("reasoning_effort is dropped when tools are present", () => {
const out = applyAzureParamRules(
"gpt-5.1",