GPT-5.x é o primeiro bump subtractivo no Azure OpenAI: parâmetros como temperature, top_p e stop retornam 400. A migração exige um módulo de compatibilidade que detecta a família do modelo e ajusta os kwargs, além de uma subclasse para LangChain que remove o stop para modelos reasoning. O artigo fornece um módulo copy-paste (model_compat.py), uma subclasse ReasoningSafeAzureChatOpenAI e um checklist de 10 passos para rollout seguro, permitindo que o mesmo código funcione com GPT-4 e GPT-5.x.
Por que esta migração é diferente?
Toda atualização anterior do Azure OpenAI — 3.5 → 4, 4 → 4o, 4o → 4o-mini — foi aditiva: você trocava engine="gpt-4o" e tudo continuava funcionando.
O GPT-5.x é a primeira geração subtrativa: parâmetros que você enviava agora retornam 400 Unsupported parameter. O protocolo wire mudou porque o GPT-5 é um modelo reasoning — ele gasta tokens pensando internamente antes de responder, então os parâmetros que controlavam o pipeline de sampling antigo (temperature, top_p, presence_penalty, frequency_penalty) não existem mais no schema da requisição.
O que isso significa para código em produção:
- Uma suíte de testes que passa no
gpt-4ofalhará na primeira chamada contragpt-5.1com HTTP 400. - Uma suíte de testes que passa no
gpt-5.1falhará em todo deployment legadogpt-4*porque os novos controles de reasoning (reasoning_effort,verbosity) não são reconhecidos. - Helpers do LangChain que funcionaram sem modificação por dois anos (notadamente
create_sql_query_chain) silenciosamente vinculamstop=[...]ao seu LLM e disparam o mesmo 400. Um source-grep não encontrará a linha ofensiva porque ela vive dentro da biblioteca.
A boa notícia: a divergência é mecânica. Com um helper de detecção, um construtor de parâmetros e uma pequena subclasse do LangChain, você pode executar o mesmo código contra ambas as famílias.
A matriz de breaking changes
| Concern | GPT-4 / GPT-4o (legacy) | GPT-5.x / o1 / o3 (reasoning) |
|---|---|---|
| Output budget | max_tokens |
max_completion_tokens (rejeita max_tokens) |
| temperature | 0.0–1.0 | Apenas o default (1) é aceito — omita |
| top_p | Suportado | Rejeitado |
| presence_penalty, frequency_penalty | Suportado | Rejeitado |
| logprobs, logit_bias | Suportado | Rejeitado |
| stop sequences | Suportado | Rejeitado na maioria dos deployments reasoning |
| reasoning_effort | Rejeitado | Novo: minimal | low | medium | high |
| verbosity | Rejeitado | Novo: low | medium | high (às vezes via extra_body) |
| System instruction role | system |
developer recomendado; system funciona como alias |
| Output token cost | Apenas output tokens | Output + reasoning tokens contam contra seu cap |
| Recommended API version | 2024-12-01-preview ou anterior |
2025-03-01-preview ou posterior |
Duas consequências fáceis de perder:
-
max_completion_tokensé um orçamento compartilhado. O GPT-5.1 pode queimar 2–4× mais tokens internamente antes de emitir o primeiro token de resposta. Um cap de4096que confortavelmente segurava uma SQL query no GPT-4o agora trunca silenciosamente a resposta no meio do token no GPT-5.1. Multiplique seus budgets legados por ~2.5× e adicione um floor (ex.: 4096) antes de enviar. -
O parâmetro
stopé o assassino silencioso. Qualquer helper que chamellm.bind(stop=[...])— e há vários nolangchain— transformará um caminho de código funcional em 400 no momento em que você trocar os deployments.
Estratégia de compatibilidade: detecte, não bifurque
A tentação é bifurcar: um branch para GPT-4, outro para GPT-5. Não faça isso. A unidade de abstração correta é uma função que classifica o deployment em uma family, e uma função que constrói um dicionário de kwargs que o SDK aceitará para aquela família.
Cada ponto de chamada — SDK, LangChain, HTTP puro — drena para o mesmo builder de kwargs. Quando você eventualmente retirar o GPT-4, deleta o branch legado em um arquivo, não em cinquenta.
O módulo de compatibilidade independente de plataforma
Coloque o seguinte arquivo no seu projeto. Ele não tem imports de Azure / OpenAI / LangChain no momento do carregamento do módulo, então o mesmo arquivo funciona em um web service, uma serverless function, um notebook ou uma ferramenta CLI.
4.1 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"
4.2 O que isso proporciona
Toda chamada direta ao SDK se reduz a duas linhas:
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, # automaticamente vira max_completion_tokens para GPT-5
temperature=0.2, # automaticamente descartado para GPT-5
reasoning_effort="low", # automaticamente descartado para GPT-4
)
response = client.chat.completions.create(
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": user_input},
],
**kwargs,
)
O mesmo ponto de chamada agora mira corretamente gpt-5.1, gpt-4o, gpt-4-32k, o3-mini ou qualquer deployment futuro cujo nome incorpore a família — e você pode sobrescrever com a variável de ambiente OPENAI_MODEL_FAMILY quando o alias do deployment for opaco.
4.3 Chamadas HTTP puras
Alguns caminhos de código legados bypassam o SDK e fazem POST de JSON diretamente. O mesmo builder funciona lá:
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`` está codificado na URL para Azure
payload.pop("extra_body", None) # já está no 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()
LangChain: o parâmetro stop oculto
langchain.chains.sql_database.query.create_sql_query_chain chama llm.bind(stop=["\nSQLResult:"]) internamente para terminar a saída do modelo antes do bloco de exemplo no prompt. Esse valor stop é repassado ao SDK em toda invocação. O GPT-5.1 o rejeita:
openai.BadRequestError: Error code: 400 - {'error': {
'message': "Unsupported parameter: 'stop' is not supported with this model.",
'type': 'invalid_request_error',
'param': 'stop',
}}
Você não pode acessar a chain para desabilitá-lo. A correção limpa é uma subclasse fina de AzureChatOpenAI que remove stop apenas para modelos reasoning:
5.1 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 para 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)
Use como substituto direto:
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,
)
Essa única substituição faz create_sql_query_chain, SQLDatabaseChain e os helpers RAG baseados em ChatOpenAI funcionarem contra GPT-5.1 sem nenhuma outra alteração.
O segundo gotcha do LangChain: prosa onde SQL deveria estar
create_sql_query_chain está documentado para retornar a string literal "I don't know" (ou um fallback similar) quando o LLM não consegue formar uma query. O caminho de código padrão pega a saída da chain e a executa contra o banco:
sql = chain.invoke({...}) # -> "I don't know"
result = db.run(sql) # -> envia "I don't know" para pyodbc
O banco retorna fielmente:
[42000] Unclosed quotation mark after the character string 't know'. (105)
Que aparece para o usuário final como um enganoso "erro de sintaxe SQL". A mitigação é um guard de uma linha que valida se a saída da chain parece SQL antes da execução:
import re
_SQL_START_RE = re.compile(
r"^\s*(?:WITH|SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|MERGE|EXEC|EXECUTE|TRUNCATE)\b",
re.IGNORECASE,
)
def looks_like_sql(text: str) -> bool:
"""True only if ``text`` starts with a recognised SQL DML/DDL keyword."""
if not text or not text.strip():
return False
return bool(_SQL_START_RE.match(text))
sql = extract_sql_query(chain.invoke({...}))
if not looks_like_sql(sql):
logging.warning("SQL chain returned a non-SQL response: %r", sql[:200])
return (
"I couldn't form a SQL query for that question. "
"Please rephrase or add more context."
)
result = db.run(sql)
Isso não é específico do GPT-5.1 — é boa higiene para qualquer LLM que alimente um agente SQL — mas o modo de falha se torna muito mais frequente em modelos reasoning porque eles são melhores em recusar.
Limpando Markdown da saída do create_sql_query_chain
Modelos reasoning gostam de envolver sua resposta em fences markdown e anexar um parágrafo "Note:" ou "Explanation:". Nada disso sobrevive a db.run(). Um extract_sql_query defensivo lida com todas as variantes:
import re
def extract_sql_query(text: str) -> str:
"""Strip markdown fences, leading prose, and trailing explanations."""
# 1) Prefer SQL inside a markdown code fence.
m = re.search(r"```(?:sql|SQL|Sql)?\s*\n(.*?)\n```", text, re.DOTALL)
if m:
text = m.group(1)
text = text.strip()
# 2) Drop any prose *before* the SQL by jumping to the first SQL keyword.
m = re.search(
r"(?im)^\s*(WITH|SELECT|INSERT|UPDATE|DELETE|CREATE|DROP|ALTER|MERGE|EXEC|EXECUTE|TRUNCATE)\b",
text,
)
if m:
text = text[m.start(1):]
# 3) Cut at the first "Explanation:" / "Note:" / "This query..." marker.
m = re.compile(
r"(?im)^\s*(?:Explanation|Note|Notes|Here(?:'|\u2019)?s|"
r"This\s+(?:query|SQL|statement|returns|counts|selects|will|gets|finds)|"
r"The\s+(?:query|SQL|above|result|statement)|"
r"Result|Results|Description|Output|Answer)\b[^\n]*"
).search(text)
if m:
text = text[: m.start()].rstrip()
# 4) Drop any trailing fence that survived step 1.
if text.endswith("```"):
text = text[:-3].rstrip()
return text.strip()
Versionamento de pacotes
O mínimo que seu requirements.txt / environment.yml precisa:
| Package | Last GPT-4-only version | First GPT-5.x-safe version | Notes |
|---|---|---|---|
| openai | 1.55.x | 1.65.x (recommend 1.65.4+) | Versões anteriores rejeitam max_completion_tokens e reasoning_effort como kwargs desconhecidos |
| langchain-openai | 0.2.14 | 0.3.7+ | A linha 0.3.x expõe azure_deployment e encaminha model_kwargs corretamente para o novo SDK |
| langchain | 0.3.14 | 0.3.21+ | Pine junto com langchain-openai e langchain-core |
| langchain-core | 0.3.29 | 0.3.49+ | Atualize em lockstep com os outros |
| langchain-community | 0.3.14 | 0.3.20+ | Principalmente transitivo; necessário para helpers SQLDatabase |
| tiktoken | 0.7.x | 0.8.0+ | Encodings para GPT-5.1 estão no 0.8.0; versões antigas caem para cl100k_base em modelos desconhecidos |
| tokencost (opcional) | 0.1.16 | 0.1.20+ | Atualize para as tabelas de preços do GPT-5.x |
| Azure OpenAI API version | 2024-12-01-preview | 2025-03-01-preview | Primeira versão que inclui reasoning_effort e o roteamento GPT-5.x |
Fixar versões exatas após testar — LangChain tem o hábito de mover re-exports públicos entre releases minor.
requirements.txt snippet:
openai==1.65.4
langchain==0.3.21
langchain-core==0.3.49
langchain-openai==0.3.7
langchain-community==0.3.20
tiktoken==0.8.0
Novos knobs do GPT-5.x que valem a pena usar
Uma vez em um deployment reasoning, dois novos parâmetros se tornam disponíveis. Ambos são opcionais, ambos têm um valor padrão sensato, e ambos são removidos pelo builder de kwargs acima quando o alvo é um modelo legado.
reasoning_effort
minimal— one-shot lookups, classificação.low— saída estruturada determinística (SQL, extração JSON-schema, reescritas baseadas em regras). Menor custo adicional.medium(default) — RAG, sumarização, Q&A normal.high— raciocínio analítico multi-step, síntese de código complexa.
Um padrão útil é escolher o nível por perfil de tarefa em vez de no ponto de chamada:
TASK_EFFORT = {
"sql": "low",
"structured_extract": "low",
"kg_cleaning": "low",
"rag_qa": "medium",
"vision": "medium",
"analytical": "high",
}
verbosity
low | medium | high. Controla o comprimento da resposta, não seu conteúdo. Útil para grounding em UIs de chat onde você quer respostas concisas — defina low para endpoints /answer e high para painéis "explique como um engenheiro sênior".
Nota: no openai-python <= 1.65.x, verbosity ainda não é um argumento de keyword de alto nível; passe-o via extra_body (o builder acima já faz isso).
Função developer
O GPT-5.x prefere {"role": "developer", "content": "..."} para instruções que antes usavam system. A mudança é não-breaking no lado do Azure — system ainda é aceito como alias — mas alguns templates de prompt do LangChain são anteriores ao role e o rejeitarão na construção. Trate developer como opt-in (OPENAI_USE_DEVELOPER_ROLE=1) por enquanto; mude o default depois que a versão do seu template de prompt for conhecida como boa.
Auditando seus prompts existentes
Quando a migração no nível wire estiver concluída, seu serviço conversará com o GPT-5.x — mas isso não significa que ele diz a coisa certa. Modelos reasoning leem prompts de maneiras diferentes que não aparecerão como 400:
- Eles levam instruções mais ao pé da letra. Um prompt que funcionava quando o GPT-4o arredondava as arestas pode agora retornar cada edge case verbatim.
- Eles recusam com mais frequência. "I don't know" / "I cannot help with that" são mais frequentes porque modelos reasoning são menos dispostos a confabular.
- Eles ignoram "be concise" / "be terse". Use o novo knob
verbosity. - Instruções step-by-step / chain-of-thought se tornam redundantes. O modelo já raciocina internamente; prosa extra "think before you answer" compete com sua própria cadeia de pensamento e frequentemente prejudica a qualidade da saída.
- Instruções negativas podem sair pela culatra. Prompts "Never output X" ocasionalmente causam recusas onde você preferiria uma solução alternativa.
10.1 Construa um harness de regressão de prompts
Capture todos os prompts system+user que seu serviço emite em um CSV, depois replique cada um contra ambos os deployments e faça o diff da saída. O diff é o artefato mais útil que você pode produzir antes do cutover:
# prompt_audit.py - minimal differential tester
import csv
from openai import AzureOpenAI
from model_compat import build_openai_chat_kwargs
LEGACY = "gpt-4o"
REASONING = "gpt-5.1"
client = AzureOpenAI(
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
api_version=os.environ["OPENAI_API_VERSION"],
api_key=os.environ["AZURE_OPENAI_API_KEY"],
)
def run(model: str, system: str, user: str) -> str:
kw = build_openai_chat_kwargs(
model=model,
max_tokens=4096,
temperature=0.2, # auto-dropped for reasoning
reasoning_effort="medium", # auto-dropped for legacy
)
resp = client.chat.completions.create(
messages=[
{"role": "system", "content": system},
{"role": "user", "content": user},
],
**kw,
)
return resp.choices[0].message.content or ""
with open("prompts.csv") as f_in, open("diff.tsv", "w", newline="") as f_out:
writer = csv.writer(f_out, delimiter="\t")
writer.writerow(["id", "legacy_first80", "reasoning_first80",
"len_legacy", "len_new", "identical"])
for row in csv.DictReader(f_in):
legacy = run(LEGACY, row["system"], row["user"])
new = run(REASONING, row["system"], row["user"])
writer.writerow([
row["id"],
legacy[:80].replace("\n", " "),
new[:80].replace("\n", " "),
len(legacy), len(new),
legacy.strip() == new.strip(),
])
Capture três sinais por prompt — são suficientes para triar 95% dos desvios:
- Conformidade de formato. A saída ainda foi parseada como o JSON / YAML / Markdown / SQL esperado? Execute seu parser downstream existente em ambas as colunas.
- Delta de custo de tokens. Modelos reasoning tendem a ser mais verbosos por default. Qualquer coisa além de +20% é candidata ao knob
verbosity="low". - Desvio semântico. Spot-check de 5–10% das linhas manualmente. Você está procurando mudanças na intenção, não mudanças na redação.
10.2 Reescrevias comuns para tornar prompts agnósticos a modelo
O objetivo não é escrever dois prompts. É escrever um prompt que produza saída correta em ambas as famílias, movendo as restrições para fora do corpo em linguagem natural e para dentro da forma da requisição.
10.2a. Restrições de formato pertencem a response_format, não à prosa
Não faça:
Output ONLY a JSON object with keys `name` and `score`. Do not include any explanation. Do not wrap in markdown. Do not say anything else.
Faça:
resp = client.chat.completions.create(
messages=[...],
response_format={
"type": "json_schema",
"json_schema": {
"name": "scored_entity",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"score": {"type": "number"},
},
"required": ["name", "score"],
"additionalProperties": False,
},
"strict": True,
},
},
**kw,
)
response_format é honrado tanto por gpt-4o (>= 2024-08-06) quanto por toda a linha GPT-5.x. O prompt perde três linhas de restrições frágeis em linguagem natural e você ganha saída validada por schema gratuitamente.
10.2b. Substitua "think step by step" por reasoning_effort
Não faça:
Let's think step by step. First identify the entity. Then find the category. Then compute the score. Then format the answer.
Faça:
Delete a prosa e passe reasoning_effort="medium" (ou "high") para deployments reasoning. O builder de kwargs descarta o parâmetro automaticamente para modelos GPT-4, então o mesmo prompt agora produz:
- raciocínio step-by-step internamente no GPT-5.x (custo de output token mais baixo),
- a mesma resposta final no GPT-4o que o prompt verboso costumava eliciar.
10.2c. Substitua variedade baseada em temperature por amostragem n
Se seu código dependia de temperature=0.9 para obter completions diversas, o GPT-5.x retornará aproximadamente a mesma resposta todas as vezes. Gere variedade da maneira explícita:
resp = client.chat.completions.create(messages=[...], n=5, **kw)
candidates = [c.message.content for c in resp.choices]
Ou chame o modelo N vezes com pequenos variações de framing. Ambos os padrões funcionam contra qualquer família sem mais alterações de código.
10.2d. Mova instruções procedurais para o papel developer
Para workflows multi-step, o novo papel developer dá uma separação mais clara entre o que o sistema impõe e o que o usuário está perguntando:
messages = [
{"role": get_system_role(deployment), "content": role_card_for_assistant},
{"role": "developer", "content": procedural_instructions},
{"role": "user", "content": user_question},
]
get_system_role retorna "system" para modelos legados e "developer" para modelos reasoning optados via OPENAI_USE_DEVELOPER_ROLE=1. Depois que seus templates LangChain suportarem o novo papel, você pode trocar o default.
10.2e. Adicione um header de execução literal para formatos estritos
Para prompts onde a forma exata da saída importa (geração de tabelas, SQL com ordem de colunas fixa, relatórios de incidente estruturados), prependa um header explícito de execução literal para que modelos reasoning não derivem para "melhorias úteis":
LITERAL_EXECUTION_HEADER = (
"Execution mode: follow the instructions below literally and in order. "
"Do not infer intent, skip, reorder, merge, or add steps. Honour the "
"exact formatting, tone, and verbosity specified. If a step is "
"ambiguous, respond with the literal interpretation and flag the "
"ambiguity instead of guessing."
)
def apply_literal_execution(prompt: str) -> str:
if LITERAL_EXECUTION_HEADER in prompt:
return prompt
return f"{LITERAL_EXECUTION_HEADER}\n\n{prompt}"
É um no-op no GPT-4o (os modelos antigos já seguem instruções literalmente o suficiente) e uma guard rail significativa no GPT-5.1. Coloque por trás de uma flag OPENAI_LITERAL_EXECUTION para que você possa desabilitá-la sem redeploy.
10.3 Um checklist em formato de prompt
Execute cada prompt que seu serviço emite por estas perguntas:
| Pergunta | Ação |
|---|---|
| Especifica formato de saída em prosa? | Mova para response_format (10.2a) |
| Inclui "think step by step"? | Remova; defina reasoning_effort (10.2b) |
| Define restrições de tom ("be concise")? | Use verbosity |
| Usa instruções negativas ("never X")? | Adicione alternativa positiva ("do Y instead") |
| Incorpora exemplos com valores que mudariam? | Substitua valores concretos por placeholders (<VALUE>) |
Depende de temperature > 0 para variedade? |
Use amostragem n=K (10.2c) |
| Prompt do sistema tem > 2k tokens? | Divida em role-card (system) + procedimento (developer) |
| Ordem da saída importa? | Adicione header de execução literal (10.2e) |
10.4 Pontue antes de enviar
Não aprove um prompt reescrito olhando para um exemplo. Pontue-o:
- Taxa de conformidade de formato. Percentual de
N=50saídas que passam seu parser downstream / validação JSON schema. - Delta de custo de tokens. Cap de regressão em +20% versus o baseline legado. Além disso, reduza
verbosity="low"ou aperte o prompt. - Delta de latência p50 / p95. Modelos reasoning adicionam tail latency. Se seu SLA é apertado, defina
reasoning_effort="low"para o caminho ou mova-o para uma fila em background.
Um prompt que regride em qualquer um desses em mais do que sua janela de tolerância deve ser enviado por trás de uma feature flag com rollback ativado.
Estratégia de testes
Duas camadas de teste capturam >90% das regressões:
Testes de classificação de família
import pytest
from model_compat import get_model_family, build_openai_chat_kwargs
@pytest.mark.parametrize("name,expected", [
("gpt-5.1", "reasoning"),
("gpt5", "reasoning"),
("gpt-5-prod-eu", "reasoning"),
("o3-mini", "reasoning"),
("o1", "reasoning"),
("gpt-4o", "legacy"),
("gpt-4", "legacy"),
("gpt-4-32k", "legacy"),
("gpt-35-turbo", "legacy"),
("", "legacy"), # unknown -> fail closed to legacy
(None, "legacy"),
])
def test_family(name, expected):
assert get_model_family(name) == expected
def test_kwargs_for_reasoning_drops_temperature():
kw = build_openai_chat_kwargs(
model="gpt-5.1", max_tokens=1000, temperature=0.2, top_p=0.9,
reasoning_effort="low",
)
assert "temperature" not in kw
assert "top_p" not in kw
assert kw["max_completion_tokens"] >= 4096 # floor applied
assert kw["reasoning_effort"] == "low"
def test_kwargs_for_legacy_keeps_temperature():
kw = build_openai_chat_kwargs(
model="gpt-4o", max_tokens=1000, temperature=0.2, top_p=0.9,
)
assert kw["max_tokens"] == 1000
assert kw["temperature"] == 0.2
assert kw["top_p"] == 0.9
assert "reasoning_effort" not in kw
Testes smoke de nível wire
Para cada ponto de chamada LLM que você mantém, escreva um único teste de integração que exercite a chain contra um endpoint real (ou mockado) e afirme:
- HTTP 200,
- conteúdo não vazio,
finish_reason != "length"(para capturar truncamento silencioso),- (opcional) asserções estilo classificador contra uma saída golden.
Execute esses testes uma vez contra o deployment legado e outra contra o novo — mesmo código de teste, dois valores de OPENAI_ENGINE.
Coisas que não mudam
É fácil over-correct. Várias peças de plumbing continuam funcionando sem modificação:
- Autenticação. Provedores de token AAD, managed identity e chaves de API permanecem inalterados.
- Embeddings.
text-embedding-3-small,text-embedding-3-largeetext-embedding-ada-002não fazem parte da geração reasoning; a forma da chamada de embeddings é idêntica. - Function calling / tool use. Mesmo JSON schema, mesma forma de resposta.
- Streaming. Formato SSE permanece inalterado.
- Contadores de tokens.
tiktokenainda funciona, mas atualize para0.8.0+para que o novo nome de modelo resolva para o encoding correto em vez de cair silenciosamente paracl100k_base.
Próximos passos
Se você fizer apenas quatro coisas deste post, faça estas — em ordem:
- Implante um modelo GPT-5.1 lado a lado com seu deployment GPT-4 atual no Microsoft Foundry. Mantenha o deployment GPT-4 ativo; você precisará de ambos para o período de execução paralela.
- Coloque
model_compat.pyelangchain_compat.pyno seu projeto (Seções 4 e 5). Substitua toda construçãoAzureChatOpenAI(...)porReasoningSafeAzureChatOpenAIe roteie todos os literais de kwargs através dos builders. - Execute o harness de auditoria de prompts (Seção 10.1) contra seus 50 prompts mais frequentemente invocados. Trie o diff com o checklist em 10.3.
- Faça rollout por trás de uma flag baseada em percentual. Comece com 5% do tráfego por 24 horas, compare telemetria de qualidade e custo contra o baseline GPT-4o, depois aumente.
Material de referência
- Azure OpenAI in Microsoft Foundry - model overview
- Azure OpenAI model retirements and deprecations
- Reasoning models in Azure OpenAI
- Structured Outputs in Azure OpenAI
- openai-python SDK changelog
- langchain-openai release notes
Fale conosco
Abra uma issue no repositório de amostras GitHub do Microsoft Foundry se encontrar uma lacuna que este post não cobriu.
Compartilhe sua história de migração ou números nos comentários abaixo — dados de campo são a maneira mais rápida de melhorar este guia para o próximo time.
Se você opera uma workload regulada (finanças, saúde, setor público) e precisa de ajuda para sequenciar o rollout com seus prazos de aposentadoria de modelo, entre em contato com sua equipe de conta Microsoft ou um parceiro Microsoft Foundry.
GPT-5.x é o primeiro grande bump de modelo em dois anos que requer mudanças de código — mas as mudanças se resumem a um pequeno módulo de compatibilidade e uma subclasse de uma linha do LangChain. Com eles em vigor, seu código é forward-compatible (funciona em modelos reasoning hoje) e backward-compatible (ainda funciona em todo deployment GPT-4 que você ainda não migrou). O investimento paga um dividendo recorrente: quando o próximo bump reasoning chegar, o único arquivo que precisará de atualização é model_compat.py.
Apêndice A - Template minimal .env
# Endpoint and auth (unchanged between families)
AZURE_OPENAI_ENDPOINT=https://<resource>.openai.azure.com
AZURE_OPENAI_API_KEY=<key>
# The deployment name decides the family. The classifier reads it.
OPENAI_ENGINE=gpt-5.1
OPENAI_API_VERSION=2025-03-01-preview
# Optional override for opaque deployment names
# OPENAI_MODEL_FAMILY=reasoning # or "legacy"
# Optional reasoning controls (ignored for legacy deployments)
OPENAI_REASONING_EFFORT=medium
OPENAI_VERBOSITY=medium
OPENAI_REASONING_TOKEN_SCALE=2.5
OPENAI_REASONING_TOKEN_FLOOR=4096
# Flip when your LangChain templates support it
# OPENAI_USE_DEVELOPER_ROLE=1
Apêndice B - Verificações de sanidade em uma linha
# Does a deployment name classify correctly?
python -c "from model_compat import get_model_family; print(get_model_family('gpt-5.1'))"
# -> reasoning
# Does the LangChain LLM strip ``stop`` when the deployment is GPT-5.1?
python -c "
from langchain_compat import ReasoningSafeAzureChatOpenAI
import inspect; print(inspect.getsource(ReasoningSafeAzureChatOpenAI._generate))
"
Companion repository: coloque model_compat.py e langchain_compat.py lado a lado no seu pacote utils/. Eles têm zero dependência no import, então você pode vendê-los em qualquer serviço — web, function, batch job — sem arrastar Azure SDK ou LangChain para o module-load.
Perguntas Frequentes
-
Por que a migração do GPT-4 para o GPT-5.x é considerada 'subtrativa'?
Diferente dos bumps anteriores, que adicionavam parâmetros sem quebrar os existentes, o GPT-5.x remove parâmetros da API (temperature, top_p, stop, etc.) por ser um modelo de reasoning. Qualquer chamada que envie esses parâmetros recebe HTTP 400, mesmo se eles vierem de bibliotecas como LangChain. -
O que preciso mudar no meu código para usar GPT-5.x sem quebrar o GPT-4?
Adicione o módulo model_compat.py ao projeto, que classifica o deployment (reasoning ou legacy) e constrói os kwargs corretos. Substitua AzureChatOpenAI por ReasoningSafeAzureChatOpenAI para remover automaticamente o parâmetro stop. Use os builders build_openai_chat_kwargs ou build_langchain_chat_kwargs para unificar as chamadas. -
O parâmetro 'stop' causa problemas com o GPT-5.1 em LangChain? Como resolver?
Sim, funções como create_sql_query_chain chamam llm.bind(stop=[...]) internamente, o que é rejeitado pelo GPT-5.1. A solução é usar a subclasse ReasoningSafeAzureChatOpenAI fornecida no artigo, que detecta modelos reasoning e define stop=None automaticamente. -
Quais versões de pacotes são necessárias para suportar GPT-5.x?
OpenAI >= 1.65.4, langchain-openai >= 0.3.7, langchain >= 0.3.21, langchain-core >= 0.3.49, langchain-community >= 0.3.20 e tiktoken >= 0.8.0. A API version do Azure OpenAI deve ser 2025-03-01-preview ou superior. -
Como testar se meus prompts funcionam corretamente com modelos reasoning?
Use o prompt regression harness fornecido (Seção 10.1) que captura prompts em um CSV, executa contra GPT-4o e GPT-5.1, e produz um diff. Analise formato, custo de tokens e drift semântico. Além disso, aplique o checklist da Seção 10.3 para adaptar prompts.
Artigo originalmente publicado por sourav_sahu__ (Microsoft) em Azure Updates - Latest from Azure Charts.