- Дата публикации
Как перейти на GPT-5.1 и не уронить прод: подробный гид по совместимости с GPT-4o
Что нового
Microsoft на площадке Azure OpenAI (Microsoft Foundry) фактически вводит два разных семейства моделей под общим брендом:
- GPT‑4 / GPT‑4o / GPT‑4‑32k / GPT‑3.5‑turbo — «наследные» чат‑модели.
- GPT‑5.x, o1, o3, o4 — «reasoning‑модели», которые тратят часть токенов на внутреннее рассуждение.
Главные технические изменения для продакшена:
-
Новый протокол параметров для GPT‑5.x / o‑серии
- Вместо
max_tokensтеперь используетсяmax_completion_tokens. - Параметры
temperature,top_p,presence_penalty,frequency_penaltyбольше не поддерживаются — запрос с ними вернётся с HTTP 400Unsupported parameter. - Новые параметры управления рассуждением:
reasoning_effort:minimal | low | medium | high.verbosity:low | medium | high(часто передаётся черезextra_body).
- Вместо
-
Общий бюджет токенов ответа
- GPT‑5.1 тратит в 2–4 раза больше токенов на внутреннее «мышление», прежде чем вернуть первый токен ответа.
- Старый лимит, который комфортно жил на GPT‑4o, теперь режет ответы:
- Пример: лимит 4096 токенов, который спокойно держал SQL‑ответ на GPT‑4o, на GPT‑5.1 может обрывать ответ посередине токена.
- Рекомендация из гайда:
- умножать старые бюджеты примерно на 2,5×;
- ввести минимальный порог, например 4096 токенов.
-
Стоимость по токенам
- Для GPT‑4 / GPT‑4o в лимит и биллинг обычно попадают только выходные токены.
- Для GPT‑5.x / o‑серии в счёт идут и выходные, и reasoning‑токены.
-
Поведение API и версии
- Наследные модели: ожидаемый API‑путь с параметрами
max_tokens,temperature,top_pи т.д. - Reasoning‑модели: отклоняют старые sampling‑параметры и
stop(на многих деплойментах). - Рекомендуемые версии API:
- Для GPT‑4 / GPT‑4o:
2024-12-01-previewили старше. - Для GPT‑5.x / o‑серии:
2025-03-01-previewили новее.
- Для GPT‑4 / GPT‑4o:
- Наследные модели: ожидаемый API‑путь с параметрами
-
Роль system / developer
- В reasoning‑семействе появляется новая роль
developer. - Старый
systemпродолжает работать как алиас, но часть инструментов (например, LangChain) пока жёстко завязаны наsystem. - В совместимом слое это настраивается через переменную окружения
OPENAI_USE_DEVELOPER_ROLE.
- В reasoning‑семействе появляется новая роль
-
Жёсткий breaking change по
stop- Многие reasoning‑деплойменты возвращают 400, если в запросе есть
stop. - LangChain‑цепочки (например,
create_sql_query_chain) автоматически подставляютstop=["\nSQLResult:"], даже если вы этого нигде не писали. - В результате первый же запрос к GPT‑5.1 из старого кода падает с 400, хотя с GPT‑4o всё было зелёным.
- Многие reasoning‑деплойменты возвращают 400, если в запросе есть
-
Готовый модуль совместимости Пост предлагает положить в проект один файл
model_compat.py, который:- определяет, к какому семейству относится деплоймент (legacy vs reasoning);
- собирает корректный набор аргументов для SDK OpenAI / Azure OpenAI;
- масштабирует
max_tokensдля reasoning‑моделей; - аккуратно прокидывает
reasoning_effortиverbosity; - даёт функцию выбора роли
system/developer.
Отдельно идёт маленький файл langchain_compat.py с подклассом ReasoningSafeAzureChatOpenAI, который автоматически выбрасывает stop для reasoning‑деплойментов, но оставляет поведение бит‑в‑бит таким же для GPT‑4 / GPT‑3.5.
Как это работает
1. Два семейства моделей и детектор
Вся логика завязана на определение «семейства» по имени деплоймента или модели.
В model_compat.py задаются два набора регулярных выражений:
_REASONING_PATTERNS = (
# gpt-5, gpt5, gpt-5.1, gpt_5, GPT 5, gpt5mini-prod-eu, ...
re.compile(r"(?i)(^|[^a-z0-9])gpt[-_ ]?5(\.\d+)?([^0-9]|$)"),
# o1, o3, o4, o1-mini, o3-preview ...
re.compile(r"(?i)(^|[^a-z0-9])o[134](-mini|-preview)?([^a-z0-9]|$)"),
)
_LEGACY_PATTERNS = (
re.compile(r"(?i)gpt[-_ ]?4o"),
re.compile(r"(?i)gpt[-_ ]?4(?!\d)"),
re.compile(r"(?i)gpt[-_ ]?4[-_ ]?32k"),
re.compile(r"(?i)gpt[-_ ]?3\.?5"),
re.compile(r"(?i)gpt[-_ ]?35"),
)
Функция get_model_family():
- сначала смотрит на переменную окружения
OPENAI_MODEL_FAMILY— это нужно, если деплоймент называется, например,prod-defaultи из имени нельзя понять семейство; - если оверрайда нет, прогоняет имя через паттерны:
- если совпал
_REASONING_PATTERNS— возвращает"reasoning"; - если совпал
_LEGACY_PATTERNS—"legacy"; - иначе — по умолчанию
"legacy".
- если совпал
Важно: автор сознательно «ошибается в сторону legacy». Если reasoning‑деплоймент случайно классифицировать как legacy, вы получите 400 с Unsupported parameter и сразу увидите проблему. Если сделать наоборот, то часть нужных параметров тихо потеряется.
2. Управление рассуждением: reasoning_effort и verbosity
В модуле есть два хелпера, которые берут значения либо из аргументов, либо из переменных окружения:
_VALID_REASONING_EFFORT = {"minimal", "low", "medium", "high"}
_VALID_VERBOSITY = {"low", "medium", "high"}
def get_reasoning_effort(override: Optional[str] = None) -> Optional[str]:
return _coerce_choice(
override if override is not None else os.getenv("OPENAI_REASONING_EFFORT"),
_VALID_REASONING_EFFORT,
)
def get_verbosity(override: Optional[str] = None) -> Optional[str]:
return _coerce_choice(
override if override is not None else os.getenv("OPENAI_VERBOSITY"),
_VALID_VERBOSITY,
)
Функция _coerce_choice:
- нормализует строку к lower‑case;
- проверяет, входит ли значение в допустимый набор;
- если нет — пишет
logging.warningи возвращаетNone.
Так вы не сломаете прод из‑за опечатки в переменной окружения — параметр просто не уйдёт в запрос.
3. Масштабирование max_tokens для reasoning‑моделей
Ключевая часть — функция scale_max_tokens_for_reasoning():
def _reasoning_token_scale() -> float:
try:
scale = float(os.getenv("OPENAI_REASONING_TOKEN_SCALE", "2.5"))
except (TypeError, ValueError):
scale = 2.5
return scale if scale > 0 else 1.0
def _reasoning_token_floor() -> int:
try:
floor = int(os.getenv("OPENAI_REASONING_TOKEN_FLOOR", "4096"))
except (TypeError, ValueError):
floor = 4096
return floor if floor > 0 else 4096
def scale_max_tokens_for_reasoning(max_tokens: Optional[int]) -> Optional[int]:
"""Scale a legacy ``max_tokens`` budget up for reasoning models.
``None`` and ``-1`` ("no explicit cap") are passed through.
"""
if max_tokens is None:
return None
if max_tokens == -1:
return -1
return max(int(round(max_tokens * _reasoning_token_scale())), _reasoning_token_floor())
Что происходит:
- вы продолжаете в коде оперировать старым
max_tokens; - для reasoning‑моделей он автоматически умножается на коэффициент из
OPENAI_REASONING_TOKEN_SCALE(по умолчанию 2.5); - применяется нижний порог из
OPENAI_REASONING_TOKEN_FLOOR(по умолчанию 4096 токенов); - если вы явно передали
-1, это трактуется как «без явного лимита» и не трогается.
4. Сборка аргументов для SDK OpenAI / Azure OpenAI
Главная функция:
def build_openai_chat_kwargs(
model: str,
*,
max_tokens: Optional[int] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
presence_penalty: Optional[float] = None,
frequency_penalty: Optional[float] = None,
reasoning_effort: Optional[str] = None,
verbosity: Optional[str] = None,
extra: Optional[Mapping[str, Any]] = None,
) -> Dict[str, Any]:
"""Build kwargs for ``openai.OpenAI / AzureOpenAI .chat.completions.create``.
Splat the result directly: ``client.chat.completions.create(**kwargs)``.
Unsupported parameters are silently omitted for reasoning models; legacy
deployments retain the historical behaviour.
"""
family = get_model_family(model)
kwargs: Dict[str, Any] = {"model": model}
# ---- output budget ----
if max_tokens is not None and max_tokens != -1:
if family == "reasoning":
kwargs["max_completion_tokens"] = scale_max_tokens_for_reasoning(int(max_tokens))
else:
kwargs["max_tokens"] = int(max_tokens)
# ---- sampling ----
if family == "legacy":
kwargs.update(_drop_none({
"temperature": temperature,
"top_p": top_p,
"presence_penalty": presence_penalty,
"frequency_penalty": frequency_penalty,
}))
else:
for key, value in (
("temperature", temperature), ("top_p", top_p),
("presence_penalty", presence_penalty), ("frequency_penalty", frequency_penalty),
):
if value is not None:
logging.debug(
"Dropping unsupported parameter '%s' for reasoning model '%s'",
key, model,
)
# ---- reasoning controls ----
if family == "reasoning":
effort = get_reasoning_effort(reasoning_effort)
if effort is not None:
kwargs["reasoning_effort"] = effort
verb = get_verbosity(verbosity)
if verb is not None:
# ``verbosity`` is not a top-level kwarg in openai-python <= 1.65.x;
# route it via ``extra_body`` so it lands in the JSON without a
# TypeError from the SDK.
kwargs.setdefault("extra_body", {})["verbosity"] = verb
# ---- caller-supplied extras (already filtered) ----
if extra:
for key, value in extra.items():
if value is None:
continue
if family == "reasoning" and key in _SAMPLING_KEYS:
continue
kwargs[key] = value
return kwargs
Что делает функция:
- для legacy‑моделей:
- передаёт
max_tokens,temperature,top_p,presence_penalty,frequency_penaltyкак раньше;
- передаёт
- для reasoning‑моделей:
- заменяет
max_tokensнаmax_completion_tokensс масштабированием; - тихо отбрасывает все sampling‑параметры, которые модель не понимает;
- добавляет
reasoning_effortиverbosity(черезextra_body, чтобы SDK не упал из‑за неизвестного аргумента);
- заменяет
- любые дополнительные параметры из
extraпроходят фильтрацию: если ключ попадает в список_SAMPLING_KEYS, он не уходит reasoning‑модели.
5. LangChain: отдельный билдер аргументов
Для LangChain‑классов (AzureChatOpenAI, ChatOpenAI) часть параметров передаётся не напрямую, а через model_kwargs. Отсюда вторая функция:
def build_langchain_chat_kwargs(
deployment_name: str,
*,
max_tokens: Optional[int] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
reasoning_effort: Optional[str] = None,
verbosity: Optional[str] = None,
) -> Dict[str, Any]:
"""Build kwargs for ``langchain_openai.AzureChatOpenAI`` / ``ChatOpenAI``.
Older ``langchain-openai`` releases don't expose ``max_completion_tokens``
as a top-level kwarg, so we forward it through ``model_kwargs`` (which
langchain passes straight to the SDK).
"""
family = get_model_family(deployment_name)
kwargs: Dict[str, Any] = {}
model_kwargs: Dict[str, Any] = {}
if max_tokens is not None and max_tokens != -1:
if family == "reasoning":
model_kwargs["max_completion_tokens"] = scale_max_tokens_for_reasoning(int(max_tokens))
else:
kwargs["max_tokens"] = int(max_tokens)
if family == "reasoning":
effort = get_reasoning_effort(reasoning_effort)
if effort is not None:
model_kwargs["reasoning_effort"] = effort
verb = get_verbosity(verbosity)
if verb is not None:
model_kwargs.setdefault("extra_body", {})["verbosity"] = verb
else:
if temperature is not None:
kwargs["temperature"] = temperature
if top_p is not None:
kwargs["top_p"] = top_p
if model_kwargs:
kwargs["model_kwargs"] = model_kwargs
return kwargs
Здесь логика та же, но адаптирована под интерфейс LangChain.
6. Роли system / developer
Функция get_system_role() даёт единое место, где вы решаете, какую роль использовать:
def get_system_role(model_or_deployment: Optional[str] = None) -> str:
"""Return ``"developer"`` for reasoning models when opted in, ``"system"`` otherwise.
Defaulting to ``"system"`` preserves compatibility with LangChain prompt
templates and SDK helpers that don't yet recognise the new role. Opt in
with ``OPENAI_USE_DEVELOPER_ROLE=1`` once your stack supports it.
"""
if not is_reasoning_model(model_or_deployment):
return "system"
raw = os.getenv("OPENAI_USE_DEVELOPER_ROLE", "")
return "developer" if raw.strip().lower() in {"1", "true", "yes", "on"} else "system"
По умолчанию всё остаётся на system, чтобы не ломать существующие промпты и шаблоны. Как только стек готов к developer, вы включаете его флагом.
7. LangChain и скрытый stop
Главный «подводный камень»: многие цепочки LangChain сами добавляют stop. Например, langchain.chains.sql_database.query.create_sql_query_chain всегда вызывает llm.bind(stop=["\nSQLResult:"]).
GPT‑5.1 и другие reasoning‑модели на Azure OpenAI отвечают на такой запрос так:
openai.BadRequestError: Error code: 400 - {'error': {
'message': "Unsupported parameter: 'stop' is not supported with this model.",
'type': 'invalid_request_error',
'param': 'stop',
}}
Вы не можете из внешнего кода запретить этой цепочке добавлять stop. Поэтому автор предлагает аккуратный шорткат — подкласс ReasoningSafeAzureChatOpenAI:
"""LangChain-side compatibility shim for reasoning-class deployments."""
from __future__ import annotations
from typing import Any, List, Optional
from langchain_core.callbacks.manager import (
AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun,
)
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatResult
from langchain_openai import AzureChatOpenAI # use ChatOpenAI for non-Azure
from model_compat import is_reasoning_model
class ReasoningSafeAzureChatOpenAI(AzureChatOpenAI):
"""``AzureChatOpenAI`` variant that hides parameters reasoning models reject.
Reasoning models (GPT-5.x, o1/o3/o4) return HTTP 400 when a request
payload carries ``stop``. LangChain's SQL helpers unconditionally bind it,
so the unsupported parameter reaches the SDK regardless of how the caller
configured the LLM. This subclass strips ``stop`` for reasoning
deployments while forwarding it unchanged for legacy GPT-4 / GPT-3.5
deployments - the behaviour is byte-identical to upstream LangChain
for those models.
"""
def _deployment_id(self) -> str:
# ``langchain-openai`` >= 0.2 exposes ``azure_deployment``; older
# releases use ``deployment_name``. Either may be set by the caller.
return (
getattr(self, "azure_deployment", None)
or getattr(self, "deployment_name", None)
or ""
)
def _generate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> ChatResult:
if is_reasoning_model(self._deployment_id()):
stop = None
return super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
async def _agenerate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> ChatResult:
if is_reasoning_model(self._deployment_id()):
stop = None
return await super()._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs)
Класс:
- для GPT‑4 / GPT‑3.5 ведёт себя точно так же, как обычный
AzureChatOpenAI; - для GPT‑5.x / o‑серии обнуляет
stopперед вызовом родительской реализации.
Этого достаточно, чтобы create_sql_query_chain, SQLDatabaseChain и RAG‑хелперы, построенные на ChatOpenAI, начали работать с GPT‑5.1 без переписывания промптов и цепочек.
Что это значит для вас
Если вы держите продакшен‑сервис на Azure OpenAI или OpenAI API и хотите подключить GPT‑5.x / o‑серию, но при этом не выключать GPT‑4o, для вас здесь несколько прямых выводов.
1. Нельзя просто заменить имя модели в конфиге
Сценарий «поменяли engine="gpt-4o" на "gpt-5.1" и поехали» больше не работает.
- Первый же запрос к GPT‑5.1 вернёт HTTP 400, потому что:
- в запросе есть
temperature,top_pили штрафы; - LangChain незаметно добавил
stop; - вы продолжаете слать
max_tokensвместоmax_completion_tokens.
- в запросе есть
Обратная сторона тоже жёсткая:
- Если вы настроите код под GPT‑5.1 (новые параметры, без sampling‑контролей), запросы к старым GPT‑4 деплойментам начнут падать*, потому что те не знают
reasoning_effortиverbosity.
2. Не плодите ветки кода под каждую модель
Интуитивное решение — сделать два пути:
- один код для GPT‑4;
- другой для GPT‑5.
Через полгода таких веток станет десяток, и вы начнёте забывать, где какой параметр меняли.
Гайд предлагает другой подход:
- одна функция, которая определяет семейство (legacy / reasoning);
- одна функция‑билдер, которая собирает валидный
kwargsдля этого семейства; - все вызовы SDK, LangChain и raw HTTP сходятся в этот билдер.
Когда вы окончательно выведите GPT‑4 из продакшена, вы меняете один файл, а не 50 мест по коду.
3. Пересчитайте лимиты токенов
Если вы сейчас держите жёсткие лимиты max_tokens для GPT‑4o, при переходе на GPT‑5.1:
- увеличьте их примерно в 2,5 раза;
- введите минимальный порог 4096;
- закладывайте, что в счёт идут и reasoning‑токены.
Иначе GPT‑5.1 будет обрывать ответы, особенно в длинных задачах: SQL, сложная аналитика, генерация кода.
4. Перепроверьте LangChain‑цепочки
Если вы используете:
create_sql_query_chain;SQLDatabaseChain;- RAG‑цепочки на
ChatOpenAI;
скорее всего, где‑то внутри LangChain уже живёт stop, о котором вы не знаете.
Решение из гайда:
- заменить
AzureChatOpenAIнаReasoningSafeAzureChatOpenAI(или аналог дляChatOpenAI); - генерировать
llm_kwargsчерезbuild_langchain_chat_kwargs().
Это минимальное изменение, которое возвращает зелёные тесты и не требует правок в промптах или цепочках.
5. Где GPT‑5.x / o‑серия особенно полезны
С учётом того, что reasoning‑модели тратят больше токенов на внутреннее рассуждение, они особенно полезны в задачах, где важна цепочка логики:
- сложные SQL‑запросы и аналитика по базе данных;
- многошаговые бизнес‑правила, где GPT‑4o часто «срезает углы»;
- генерация кода и ревью с длинным контекстом, где модель должна «подумать» перед ответом.
Но есть и минусы:
- больший расход токенов за счёт reasoning‑части;
- жёсткие требования к протоколу запросов (нельзя просто «как раньше, только новую модель»);
- необходимость пройтись по обвязке — SDK, LangChain, сырые HTTP‑клиенты.
6. Где лучше пока остаться на GPT‑4o
Если задача:
- простая генерация текста;
- короткие ответы без сложной логики;
- критичен бюджет токенов;
можно оставить GPT‑4o как «рабочую лошадку», а GPT‑5.1 подключать точечно там, где нужен выигрыш в качестве рассуждений.
7. Доступность и инфраструктура
Речь идёт о Azure OpenAI в Microsoft Foundry и официальном OpenAI API. Для работы из России чаще всего потребуется VPN и инфраструктура за пределами страны. Это нужно учитывать при планировании продакшена и выборе региона деплоймента.
Место на рынке
Гайд сфокусирован на миграции внутри линейки OpenAI: GPT‑4 / GPT‑4o → GPT‑5.1 / o‑серия. Внутри этой экосистемы:
-
GPT‑5.1 и o‑серия:
- дают более сложное рассуждение за счёт внутреннего reasoning;
- потребляют 2–4× больше токенов на «думание» в тех же задачах;
- требуют новых параметров (
max_completion_tokens,reasoning_effort,verbosity).
-
GPT‑4 / GPT‑4o:
- проще в интеграции, так как используют старый протокол (
max_tokens,temperature,top_pи т.д.); - дешевле по токенам в сценариях, где reasoning‑запас не нужен;
- безболезненно работают с существующими LangChain‑цепочками.
- проще в интеграции, так как используют старый протокол (
Чётких чисел по скорости, ценам за 1K токенов или сравнению с другими вендорами (например, Claude) в материале нет. Основной акцент — на том, как не сломать прод при одновременной работе GPT‑4o и GPT‑5.1.
Установка
Специальной установки нет: вы продолжаете использовать стандартные SDK OpenAI / Azure OpenAI и LangChain. Всё, что нужно — добавить в проект два файла: model_compat.py и langchain_compat.py.
Ниже — полный код из гайда.
model_compat.py
"""
Model compatibility helper for GPT-5.x with GPT-4 backward compatibility.
This module centralises the parameter translation needed to talk to the
"reasoning" generation of OpenAI / Azure OpenAI models (GPT-5, GPT-5.1,
""""o1, o3, o4) while keeping older deployments (gpt-4, gpt-4o, gpt-4-32k,
gpt-3.5-turbo, etc.) working unchanged.
"""
from __future__ import annotations
import logging
import os
import re
from typing import Any, Dict, Iterable, Mapping, Optional
# ---------------------------------------------------------------------------
# Family detection
# ---------------------------------------------------------------------------
_REASONING_PATTERNS = (
# gpt-5, gpt5, gpt-5.1, gpt_5, GPT 5, gpt5mini-prod-eu, ...
re.compile(r"(?i)(^|[^a-z0-9])gpt[-_ ]?5(\.\d+)?([^0-9]|$)"),
# o1, o3, o4, o1-mini, o3-preview ...
re.compile(r"(?i)(^|[^a-z0-9])o[134](-mini|-preview)?([^a-z0-9]|$)"),
)
_LEGACY_PATTERNS = (
re.compile(r"(?i)gpt[-_ ]?4o"),
re.compile(r"(?i)gpt[-_ ]?4(?!\d)"),
re.compile(r"(?i)gpt[-_ ]?4[-_ ]?32k"),
re.compile(r"(?i)gpt[-_ ]?3\.?5"),
re.compile(r"(?i)gpt[-_ ]?35"),
)
def get_model_family(model_or_deployment: Optional[str]) -> str:
"""Return ``"reasoning"`` for GPT-5.x / o-series, ``"legacy"`` otherwise.
Honours an ``OPENAI_MODEL_FAMILY`` env-var override for deployments whose
user-defined name does not embed the model family (e.g. ``prod-default``).
"""
override = (os.getenv("OPENAI_MODEL_FAMILY") or "").strip().lower()
if override in {"reasoning", "gpt-5", "gpt5", "gpt-5.1", "o-series", "o1", "o3"}:
return "reasoning"
if override in {"legacy", "gpt-4", "gpt4", "gpt-3.5", "gpt35", "chat"}:
return "legacy"
name = (model_or_deployment or "").strip()
if not name:
# Fail closed: when we don't know, assume legacy so old code keeps
# working. Misclassifying a reasoning deployment as legacy fails fast
# with a clear "Unsupported parameter" 400; the reverse silently
# drops parameters the caller expected.
return "legacy"
for pat in _REASONING_PATTERNS:
if pat.search(name):
return "reasoning"
for pat in _LEGACY_PATTERNS:
if pat.search(name):
return "legacy"
return "legacy"
def is_reasoning_model(model_or_deployment: Optional[str]) -> bool:
return get_model_family(model_or_deployment) == "reasoning"
# ---------------------------------------------------------------------------
# Reasoning controls
# ---------------------------------------------------------------------------
_VALID_REASONING_EFFORT = {"minimal", "low", "medium", "high"}
_VALID_VERBOSITY = {"low", "medium", "high"}
def _coerce_choice(raw: Optional[str], valid: Iterable[str]) -> Optional[str]:
if raw is None:
return None
value = str(raw).strip().lower()
if not value:
return None
if value not in set(valid):
logging.warning(
"Ignoring unsupported value '%s'; expected one of %s",
raw, sorted(valid),
)
return None
return value
def get_reasoning_effort(override: Optional[str] = None) -> Optional[str]:
return _coerce_choice(
override if override is not None else os.getenv("OPENAI_REASONING_EFFORT"),
_VALID_REASONING_EFFORT,
)
def get_verbosity(override: Optional[str] = None) -> Optional[str]:
return _coerce_choice(
override if override is not None else os.getenv("OPENAI_VERBOSITY"),
_VALID_VERBOSITY,
)
# ---------------------------------------------------------------------------
# max_completion_tokens scaling
# ---------------------------------------------------------------------------
def _reasoning_token_scale() -> float:
"""Multiplier applied to legacy ``max_tokens`` when targeting a reasoning model."""
try:
scale = float(os.getenv("OPENAI_REASONING_TOKEN_SCALE", "2.5"))
except (TypeError, ValueError):
scale = 2.5
return scale if scale > 0 else 1.0
def _reasoning_token_floor() -> int:
try:
floor = int(os.getenv("OPENAI_REASONING_TOKEN_FLOOR", "4096"))
except (TypeError, ValueError):
floor = 4096
return floor if floor > 0 else 4096
def scale_max_tokens_for_reasoning(max_tokens: Optional[int]) -> Optional[int]:
"""Scale a legacy ``max_tokens`` budget up for reasoning models.
``None`` and ``-1`` ("no explicit cap") are passed through.
"""
if max_tokens is None:
return None
if max_tokens == -1:
return -1
return max(int(round(max_tokens * _reasoning_token_scale())), _reasoning_token_floor())
# ---------------------------------------------------------------------------
# Kwargs builders
# ---------------------------------------------------------------------------
_SAMPLING_KEYS = ("temperature", "top_p", "presence_penalty", "frequency_penalty")
def _drop_none(mapping: Mapping[str, Any]) -> Dict[str, Any]:
return {k: v for k, v in mapping.items() if v is not None}
def build_openai_chat_kwargs(
model: str,
*,
max_tokens: Optional[int] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
presence_penalty: Optional[float] = None,
frequency_penalty: Optional[float] = None,
reasoning_effort: Optional[str] = None,
verbosity: Optional[str] = None,
extra: Optional[Mapping[str, Any]] = None,
) -> Dict[str, Any]:
"""Build kwargs for ``openai.OpenAI / AzureOpenAI .chat.completions.create``.
Splat the result directly: ``client.chat.completions.create(**kwargs)``.
Unsupported parameters are silently omitted for reasoning models; legacy
deployments retain the historical behaviour.
"""
family = get_model_family(model)
kwargs: Dict[str, Any] = {"model": model}
# ---- output budget ----
if max_tokens is not None and max_tokens != -1:
if family == "reasoning":
kwargs["max_completion_tokens"] = scale_max_tokens_for_reasoning(int(max_tokens))
else:
kwargs["max_tokens"] = int(max_tokens)
# ---- sampling ----
if family == "legacy":
kwargs.update(_drop_none({
"temperature": temperature,
"top_p": top_p,
"presence_penalty": presence_penalty,
"frequency_penalty": frequency_penalty,
}))
else:
for key, value in (
("temperature", temperature), ("top_p", top_p),
("presence_penalty", presence_penalty), ("frequency_penalty", frequency_penalty),
):
if value is not None:
logging.debug(
"Dropping unsupported parameter '%s' for reasoning model '%s'",
key, model,
)
# ---- reasoning controls ----
if family == "reasoning":
effort = get_reasoning_effort(reasoning_effort)
if effort is not None:
kwargs["reasoning_effort"] = effort
verb = get_verbosity(verbosity)
if verb is not None:
# ``verbosity`` is not a top-level kwarg in openai-python <= 1.65.x;
# route it via ``extra_body`` so it lands in the JSON without a
# TypeError from the SDK.
kwargs.setdefault("extra_body", {})["verbosity"] = verb
# ---- caller-supplied extras (already filtered) ----
if extra:
for key, value in extra.items():
if value is None:
continue
if family == "reasoning" and key in _SAMPLING_KEYS:
continue
kwargs[key] = value
return kwargs
def build_langchain_chat_kwargs(
deployment_name: str,
*,
max_tokens: Optional[int] = None,
temperature: Optional[float] = None,
top_p: Optional[float] = None,
reasoning_effort: Optional[str] = None,
verbosity: Optional[str] = None,
) -> Dict[str, Any]:
"""Build kwargs for ``langchain_openai.AzureChatOpenAI`` / ``ChatOpenAI``.
Older ``langchain-openai`` releases don't expose ``max_completion_tokens``
as a top-level kwarg, so we forward it through ``model_kwargs`` (which
langchain passes straight to the SDK).
"""
family = get_model_family(deployment_name)
kwargs: Dict[str, Any] = {}
model_kwargs: Dict[str, Any] = {}
if max_tokens is not None and max_tokens != -1:
if family == "reasoning":
model_kwargs["max_completion_tokens"] = scale_max_tokens_for_reasoning(int(max_tokens))
else:
kwargs["max_tokens"] = int(max_tokens)
if family == "reasoning":
effort = get_reasoning_effort(reasoning_effort)
if effort is not None:
model_kwargs["reasoning_effort"] = effort
verb = get_verbosity(verbosity)
if verb is not None:
model_kwargs.setdefault("extra_body", {})["verbosity"] = verb
else:
if temperature is not None:
kwargs["temperature"] = temperature
if top_p is not None:
kwargs["top_p"] = top_p
if model_kwargs:
kwargs["model_kwargs"] = model_kwargs
return kwargs
def get_system_role(model_or_deployment: Optional[str] = None) -> str:
"""Return ``"developer"`` for reasoning models when opted in, ``"system"`` otherwise.
Defaulting to ``"system"`` preserves compatibility with LangChain prompt
templates and SDK helpers that don't yet recognise the new role. Opt in
with ``OPENAI_USE_DEVELOPER_ROLE=1`` once your stack supports it.
"""
if not is_reasoning_model(model_or_deployment):
return "system"
raw = os.getenv("OPENAI_USE_DEVELOPER_ROLE", "")
return "developer" if raw.strip().lower() in {"1", "true", "yes", "on"} else "system"
langchain_compat.py
"""LangChain-side compatibility shim for reasoning-class deployments."""
from __future__ import annotations
from typing import Any, List, Optional
from langchain_core.callbacks.manager import (
AsyncCallbackManagerForLLMRun, CallbackManagerForLLMRun,
)
from langchain_core.messages import BaseMessage
from langchain_core.outputs import ChatResult
from langchain_openai import AzureChatOpenAI # use ChatOpenAI for non-Azure
from model_compat import is_reasoning_model
class ReasoningSafeAzureChatOpenAI(AzureChatOpenAI):
"""``AzureChatOpenAI`` variant that hides parameters reasoning models reject.
Reasoning models (GPT-5.x, o1/o3/o4) return HTTP 400 when a request
payload carries ``stop``. LangChain's SQL helpers unconditionally bind it,
so the unsupported parameter reaches the SDK regardless of how the caller
configured the LLM. This subclass strips ``stop`` for reasoning
deployments while forwarding it unchanged for legacy GPT-4 / GPT-3.5
deployments - the behaviour is byte-identical to upstream LangChain
for those models.
"""
def _deployment_id(self) -> str:
# ``langchain-openai`` >= 0.2 exposes ``azure_deployment``; older
# releases use ``deployment_name``. Either may be set by the caller.
return (
getattr(self, "azure_deployment", None)
or getattr(self, "deployment_name", None)
or ""
)
def _generate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[CallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> ChatResult:
if is_reasoning_model(self._deployment_id()):
stop = None
return super()._generate(messages, stop=stop, run_manager=run_manager, **kwargs)
async def _agenerate(
self,
messages: List[BaseMessage],
stop: Optional[List[str]] = None,
run_manager: Optional[AsyncCallbackManagerForLLMRun] = None,
**kwargs: Any,
) -> ChatResult:
if is_reasoning_model(self._deployment_id()):
stop = None
return await super()._agenerate(messages, stop=stop, run_manager=run_manager, **kwargs)
Как запустить
1. Прямой вызов SDK Azure OpenAI / OpenAI
Пример для Azure OpenAI:
from openai import AzureOpenAI
from model_compat import build_openai_chat_kwargs
client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version=os.environ["OPENAI_API_VERSION"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
kwargs = build_openai_chat_kwargs(
model=os.environ["OPENAI_ENGINE"],
max_tokens=4096, # автоматически станет max_completion_tokens для GPT-5
temperature=0.2, # автоматически выкинется для GPT-5
reasoning_effort="low", # автоматически выкинется для GPT-4
)
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_input},
],
**kwargs,
)
Один и тот же вызов корректно работает с:
gpt-5.1;gpt-4o;gpt-4-32k;o3-mini;- любым будущим деплойментом, в имени которого зашито семейство.
Если деплоймент называется абстрактно (prod-default), вы можете принудительно указать семейство через OPENAI_MODEL_FAMILY.
2. Raw HTTP‑вызовы к Azure OpenAI
Если часть кода обходит SDK и шлёт HTTP‑запросы напрямую, тот же билдер можно использовать и там:
import json
import requests
from model_compat import build_openai_chat_kwargs, get_system_role
deployment = os.environ["OPENAI_ENGINE"]
api_version = os.environ["OPENAI_API_VERSION"]
endpoint = (
f"{os.environ['AZURE_OPENAI_ENDPOINT']}/openai/deployments/{deployment}"
f"/chat/completions?api-version={api_version}"
)
payload = {
"messages": [
{"role": get_system_role(deployment), "content": system_prompt},
{"role": "user", "content": user_prompt},
],
}
# Splat the kwargs into the payload, then strip the SDK-only ``model`` key.
payload.update(build_openai_chat_kwargs(
model=deployment,
max_tokens=800,
temperature=0.7,
top_p=0.95,
reasoning_effort="low",
))
payload.pop("model", None) # ``model`` is encoded in the URL for Azure
payload.pop("extra_body", None) # already on the payload root
resp = requests.post(
endpoint,
headers={"Content-Type": "application/json", "api-key": api_key},
data=json.dumps(payload),
timeout=60,
)
resp.raise_for_status()
Тот же подход: один билдер, который знает, с каким семейством вы говорите.
3. LangChain + Azure OpenAI
Пример инициализации LLM для LangChain:
from langchain_compat import ReasoningSafeAzureChatOpenAI
from model_compat import build_langchain_chat_kwargs
llm_kwargs = build_langchain_chat_kwargs(
deployment_name=os.environ["OPENAI_ENGINE"],
max_tokens=6000,
temperature=0,
reasoning_effort="low",
)
llm = ReasoningSafeAzureChatOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
azure_deployment=os.environ["OPENAI_ENGINE"],
openai_api_version=os.environ["OPENAI_API_VERSION"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
**llm_kwargs,
)
После этой замены:
create_sql_query_chain;SQLDatabaseChain;- RAG‑цепочки на
ChatOpenAI;
начинают корректно работать с GPT‑5.1 и reasoning‑деплойментами, не ломая существующий код под GPT‑4o.
Читайте также
- Как честно тестировать GPT‑5.5 и других «агентов»: OpenAI предлагает общий протокол
- Microsoft собрала стартовый набор для наблюдаемости AI‑агентов в Foundry: трассировка, оценки качества и red‑team за один запуск
- Azure SRE Agent подключили к MCP: как один бэкенд работает и для людей, и для ИИ-агентов