29 de maio de 202627 min de leitura

Migrando para GPT-5.x sem quebrar o GPT-4: Um guia prático de compatibilidade retroativa

sourav_sahu__

Azure

Banner - Migrando para GPT-5.x sem quebrar o GPT-4: Um guia prático de compatibilidade retroativa

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-4o falhará na primeira chamada contra gpt-5.1 com HTTP 400.
  • Uma suíte de testes que passa no gpt-5.1 falhará em todo deployment legado gpt-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 vinculam stop=[...] 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:

  1. 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 de 4096 que 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.

  2. O parâmetro stop é o assassino silencioso. Qualquer helper que chame llm.bind(stop=[...]) — e há vários no langchain — 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.

Diagrama de estratégia de compatibilidade

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=50 saí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-large e text-embedding-ada-002 nã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. tiktoken ainda funciona, mas atualize para 0.8.0+ para que o novo nome de modelo resolva para o encoding correto em vez de cair silenciosamente para cl100k_base.

Próximos passos

Se você fizer apenas quatro coisas deste post, faça estas — em ordem:

  1. 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.
  2. Coloque model_compat.py e langchain_compat.py no seu projeto (Seções 4 e 5). Substitua toda construção AzureChatOpenAI(...) por ReasoningSafeAzureChatOpenAI e roteie todos os literais de kwargs através dos builders.
  3. 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.
  4. 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

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.

Gostou? Compartilhe:
Precisa de ajuda?Fale com nossos especialistas 👋
Avatar Walcew - Headset