mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
* fix(sse): apply Azure request-param rules on the azure-ai wire path
Azure rejects several stock Chat Completions params on its newer deployments
and returns HTTP 400 rather than ignoring them:
max_tokens -> 'max_tokens' is not supported with this model.
Use 'max_completion_tokens' instead.
reasoning_effort -> Function tools with reasoning_effort are not supported.
Those rules lived inline in AzureOpenAIExecutor, so they only covered the
azure-openai provider. azure-ai (Azure AI Foundry) had no executor entry and
fell through to the bare DefaultExecutor, so the SAME Azure deployment
succeeded on one connection and 400'd on the other. Every agentic client sends
tools on every turn, so azure-ai failed on the first request.
Extract the rules to open-sse/executors/azureParamRules.ts, add an
AzureAiExecutor that inherits DefaultExecutor's azure-ai URL/header/apiType
handling unchanged and applies the shared rules, and register it for azure-ai.
Also widen the deployment pattern to cover gpt-chat-latest: it is a moving
alias that resolves to a GPT-5-era model and rejects max_tokens, but carries no
version number for the token-boundary pattern to key on. Verified against the
base regex - gpt-chat-latest did not match, which is exactly the observed 400.
Regression guard: tests/unit/azure-param-rules.test.ts, including an assertion
that getExecutor("azure-ai") no longer resolves to a bare DefaultExecutor.
* fix(sse): clamp Azure gpt-4o-mini completion tokens to its 16384 ceiling
Azure gpt-4o-mini deployments accept at most 16384 completion tokens and 400 on
anything larger:
max_tokens is too large: 32000. This model supports at most 16384 completion
tokens, whereas you provided 32000.
The 32000 is OmniRoute's own doing: adjustMaxTokens raises any smaller
max_tokens to DEFAULT_MIN_TOKENS (32000) whenever tools are present, to avoid
truncated tool arguments. That floor has no upper bound, so an agentic client
asking for far less still trips the model ceiling on its first turn.
Add scoped maxOutputCap rules in paramSupport.ts for both Azure wire paths.
PROVIDER_MAX_TOKENS is the wrong lever here - it is provider-wide, and the same
Azure resource also serves GPT-5 deployments with a much higher ceiling.
Regression guard: tests/unit/azure-max-output-clamp.test.ts, which also pins
that the clamp does not leak to gpt-5.1 or to gpt-4o-mini on other providers.
---------
Co-authored-by: Mihaly Bodo <michael@proton-quantum.com>
77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
/**
|
|
* Azure Chat Completions param rules, shared by every Azure wire path.
|
|
*
|
|
* Azure's newer deployments reject a handful of stock OpenAI Chat Completions
|
|
* params and return HTTP 400 rather than ignoring them:
|
|
*
|
|
* - `max_tokens` -> "Unsupported parameter: 'max_tokens' is not supported
|
|
* with this model. Use 'max_completion_tokens' instead."
|
|
* - `temperature` -> only the default (1) is accepted.
|
|
* - `reasoning_effort` -> "Function tools with reasoning_effort are not
|
|
* supported ... Please use /v1/responses instead."
|
|
*
|
|
* This logic previously lived inline in `AzureOpenAIExecutor`, so it only
|
|
* covered the `azure-openai` provider. `azure-ai` (Azure AI Foundry) routes
|
|
* through `DefaultExecutor` and inherited none of it, which meant an identical
|
|
* deployment 400'd on one connection and succeeded on the other. Extracted here
|
|
* so both executors apply exactly the same rules.
|
|
*/
|
|
|
|
/**
|
|
* 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
|
|
* 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.
|
|
*/
|
|
export const AZURE_COMPLETION_TOKEN_DEPLOYMENT =
|
|
/(?:^|[/_-])(?:gpt-5|o(?:1|3|4))(?:[._-]|$)|^gpt-chat-latest$/i;
|
|
|
|
/**
|
|
* Apply the Azure param rules to an already-translated Chat Completions body.
|
|
*
|
|
* `originalBody` is the pre-translation request, consulted only to recover a
|
|
* caller-supplied token budget that translation may have moved or dropped.
|
|
* Returns `transformed` untouched when the deployment is unaffected or the body
|
|
* is not a plain object, and never mutates either input.
|
|
*/
|
|
export function applyAzureParamRules(
|
|
model: string,
|
|
originalBody: unknown,
|
|
transformed: unknown
|
|
): unknown {
|
|
if (!AZURE_COMPLETION_TOKEN_DEPLOYMENT.test(model)) return transformed;
|
|
if (!transformed || typeof transformed !== "object" || Array.isArray(transformed)) {
|
|
return transformed;
|
|
}
|
|
|
|
const original =
|
|
originalBody && typeof originalBody === "object" && !Array.isArray(originalBody)
|
|
? (originalBody as Record<string, unknown>)
|
|
: null;
|
|
const normalized = { ...(transformed as Record<string, unknown>) };
|
|
|
|
if (original?.max_completion_tokens !== undefined) {
|
|
normalized.max_completion_tokens = original.max_completion_tokens;
|
|
} else if (normalized.max_completion_tokens === undefined && original?.max_tokens !== undefined) {
|
|
normalized.max_completion_tokens = original.max_tokens;
|
|
}
|
|
delete normalized.max_tokens;
|
|
|
|
if (normalized.temperature !== undefined && normalized.temperature !== 1) {
|
|
delete normalized.temperature;
|
|
}
|
|
|
|
// Azure 400s on reasoning_effort as soon as tools are present, which is every
|
|
// agentic client (Claude Code, Cursor agent) on every turn.
|
|
const hasTools = Array.isArray(normalized.tools) && normalized.tools.length > 0;
|
|
if (hasTools || normalized.reasoning_effort === "none") {
|
|
delete normalized.reasoning_effort;
|
|
}
|
|
|
|
return normalized;
|
|
}
|