# LLM Runtime Defaults — timeout, retry, strict, amostragem e o pin do SDK

> **Scope:** stacks=["dotnet"]
> **Layer:** 1 (on-keyword)
> **Keywords:** llm defaults, runtime defaults, timeout llm, retry llm, strict json schema, temperature reasoning, capability por alias, network timeout, pin de sdk
> **Read by Claude in:** implement (ao configurar cliente, alias ou opções de chamada) e troubleshoot (chamada pendurada, 400 recorrente, quebra binária)

**Verified against:** Microsoft.Extensions.AI 10.9.0 + Microsoft.Agents.AI 1.20.0 + OpenAI 2.13.0 (ai-pin 2026-09-08); provado por `templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs` e `.../Providers/ModelRegistry.cs` (alias, `api:`, e limite por alias com procedência obrigatória). A superfície do SDK citada aqui — `ChatOptions`, `ChatResponseFormatJson`, `ReasoningEffort`, `OpenAIClientOptions` — foi **medida por reflexão sobre as assemblies do pin** em 2026-09-08. O timeout é **provado por** `.../Agents/AgentSpecOptions.cs` e `.../Agents/AgentFactory.cs`, que o kit compila **por spec** (`TimeoutSeconds` vira um cliente de timeout no pipeline) — e **não** por alias: a allowlist de alias do registry não tem chave de timeout (medido). Os valores default do retry do `System.ClientModel` 1.15.0 são **verificação documental** — essa assembly está fora do conjunto que o probe carrega. Last-verified: 2026-09-08.

---

## Fronteira com o Model Registry

`ai-agents-providers-model-registry` é **Layer 0** (sempre carregado) e continua dono do **schema**:
o formato do documento, `defaultAlias`, o fallback e a tabela de providers. **Este** standard é dono
dos **valores e dos botões de runtime**: timeout, retry, strict, amostragem, esforço de raciocínio e
o pin de versão do SDK.

Sem esta fronteira escrita, o par vira **duas verdades sobre o mesmo JSON** — e a segunda verdade é
sempre a que alguém encontra primeiro.

Complemento: `ai-agents-agent-spec` é dono da **forma** do agente e da regra "nulo = não envia";
`ai-agents-cost-and-budget` é dono de `pricing`. Aqui não se repete nenhum dos três.

---

## Retry herdado

**Não escreva um loop de retry sobre chamada de LLM.** A pilha do `System.ClientModel` — a base dos
clientes gerados da OpenAI — já traz uma política de retry, e ela é aplicada por padrão.

O que a assembly `System.ClientModel` 1.15.0 (a restaurada junto com o pin) carrega nos metadados,
**medido por leitura de literal em 2026-09-08**: `ClientRetryPolicy`, `ClientPipelineOptions`,
`RetryPolicy`, `MaxRetries`, `DefaultMaxRetries`, `DefaultInitialDelay`, `ShouldRetry`,
`GetNextDelay`. Isto prova que os pontos de extensão existem e como se chamam.

**Verificação documental** (fonte primária: o fonte de `ClientRetryPolicy` em
`Azure/azure-sdk-for-net`, lido em 2026-09-08), e por isso escrita como valor a **conferir** antes de
depender dela: 3 tentativas de retry, atraso inicial de 0,8 s, backoff exponencial na forma
`(1 << (tryCount - 1)) * initialDelay`, e o cabeçalho de espera do servidor respeitado quando ele
pede mais tempo do que o backoff calculado.

**A regra:** herde. Se precisar de outro número, **substitua a política** (`RetryPolicy` nas opções
do cliente), não empilhe um `for` por cima. Duas camadas de retry multiplicam: 3 × 3 = 9 chamadas
pagas para um incidente que o operador vê como "uma".

Prática de campo alinhada, com a razão escrita no próprio código: *"não adicionamos loop próprio"*.

---

## Timeout explícito

**Retry sobre uma chamada que nunca expira é o pior dos dois mundos.** A política tenta de novo o
que ainda não desistiu, e o incidente aparece como latência infinita em vez de erro.

Dois fatos medidos por reflexão sobre as assemblies do pin, em 2026-09-08:

1. **`Microsoft.Extensions.AI.ChatOptions` não tem propriedade de timeout.** As 20 propriedades
   declaradas são de conteúdo da chamada (`Temperature`, `TopP`, `TopK`, `MaxOutputTokens`,
   `ResponseFormat`, `Tools`, `ToolMode`, `Reasoning`, `Seed`, `StopSequences`, `Instructions`,
   `ModelId`, `ConversationId`, `AdditionalProperties`, …). A abstração **não sabe expressar**
   timeout — logo, ele não é um botão por chamada nesse nível.
2. **`OpenAI.OpenAIClientOptions` (OpenAI 2.13.0) declara apenas** `Endpoint`, `OrganizationId`,
   `ProjectId` e `UserAgentApplicationId`. O botão de timeout **não é dele**: ele vem da base,
   `System.ClientModel.Primitives.ClientPipelineOptions`, cuja assembly carrega os nomes
   `NetworkTimeout` e `get_NetworkTimeout` (medido por literal na mesma data). A declaração está
   fora do conjunto de assemblies que o probe carrega, então este ponto é **documental**.

**A regra, em três partes:**

- **Configure o timeout explicitamente**, e escolha conscientemente a camada. O exemplar do kit o faz **por spec de agente** (`AgentSpecOptions.TimeoutSeconds` → um cliente de timeout no pipeline), porque a allowlist de alias do registry **não tem** chave de timeout — medido. Por alias exige abrir essa allowlist, o que é mudança de `.cs`. Alias
  de raciocínio pesado e alias de resposta curta não têm o mesmo tempo aceitável.
- **`CancellationToken` de turno não é timeout de chamada.** Ele cancela quando *alguém* desiste
  (o usuário fechou a aba, o job foi abortado). Uma chamada pendurada sem ninguém para desistir
  fica pendurada.
- **Timeout curto sem retry é fragilidade; retry sem timeout é vazamento.** Os dois andam juntos.

Medições que motivam a regra: um projeto do acervo tem **apenas** o `CancellationToken` do turno,
sem nenhum timeout de chamada. Outro tem uma biblioteca de resiliência declarada no `.csproj`
**sem aplicá-la à camada de IA**, e nenhum timeout de `HttpClient` nem handler de resiliência
padrão registrado.

> **Correção explícita de premissa.** A formulação "timeout é herdado do SDK, como o retry" é
> **falsa** nas duas pontas medidas: o retry é herdado, o timeout precisa ser **decidido**. Herdar
> o que não existe é como se descobre que uma chamada roda até o processo morrer.

---

## Strict JSON schema por alias

O identificador **`StrictJsonSchema` não existe**. Medição, 2026-09-08: uma varredura por reflexão
sobre as **14 assemblies** dos pacotes do pin e das pontes (`Microsoft.Agents.AI` e derivados,
`Microsoft.Extensions.AI` e derivados, `OpenAI`, `Google.GenAI`, `ModelContextProtocol` e derivados,
`Microsoft.Extensions.VectorData.Abstractions`) devolve **zero** tipos exportados cujo nome contenha
`Strict`.

E `Microsoft.Extensions.AI.ChatResponseFormatJson` declara exatamente três propriedades — `Schema`,
`SchemaDescription`, `SchemaName` — e **nenhum campo `strict`**, também medido por reflexão. A
abstração não sabe expressar strict.

O mecanismo real é **uma chave em `ChatOptions.AdditionalProperties`**, lida pela ponte do provider,
que transforma o schema antes de enviá-lo (proibir propriedades adicionais, converter schemas
booleanos). Isso significa três coisas para o desenho:

- **Strict é decisão por chamada / por alias**, porque é onde `ChatOptions` vive.
- **Suporte a strict é capacidade do modelo**, e o registry já sabe o provider e o modelo de cada
  alias. Logo o lugar do "este alias suporta strict" é o **alias**.
- **Nunca uma flag global de processo.**

### O anti-padrão, com o nome real do campo

Medido em produção no acervo: um par `StrictJsonSchemaSupported` (propriedade de configuração) +
`volatile bool _strictUnsupported` (campo estático de processo). Ao receber um `400` de qualquer
chamada, o campo é ligado e **todas as chamadas seguintes do processo** passam a ser enviadas sem
strict. A frase do próprio diagnóstico do projeto: *"um 400 em qualquer ponto degrada todas as
chamadas seguintes"*.

O efeito prático: um modelo local em desenvolvimento, que legitimamente não suporta strict, rebaixa
as chamadas de **produção** do mesmo processo. Capacidade é do alias; degradação global é um bug
disfarçado de resiliência.

O kit compila a forma correta dessa ideia: limites conhecidos de um alias vivem num record de
`Constraints` **por alias**, e um limite **sem procedência escrita é recusado na carga do
registry** — para que ninguém consiga gravar um "todo mundo sabe que" sem dizer de onde tirou.

---

## Temperature e reasoning

**Modelo de raciocínio e amostragem não se misturam.** A regra prática: `Temperature` só faz sentido
em modelo sem raciocínio; em modelo de raciocínio o botão é o **esforço**.

O que está **medido por reflexão** sobre `Microsoft.Extensions.AI.Abstractions` 10.9.0, em
2026-09-08:

- `ChatOptions.Temperature` (`float?`) e `ChatOptions.Reasoning` (`ReasoningOptions`) são
  propriedades **distintas** e independentes.
- `ReasoningOptions` declara `Effort` e `Output`.
- `ReasoningEffort` é um **enum de primeira classe** com exatamente cinco valores:
  `None = 0`, `Low = 1`, `Medium = 2`, `High = 3`, `ExtraHigh = 4`.

Esforço de raciocínio, portanto, é **tipado** — não é string, e não precisa de convenção de projeto.

A **medição de campo** que sustenta o limite prático (dois modelos de raciocínio que rejeitam
`Temperature != 1` quando há tools na chamada) é de `ai-agents-providers-model-registry` — Layer 0,
sempre carregado, e dono dela desde a re-verificação anterior. **Não é repetida aqui**: uma segunda
cópia da mesma medição é a segunda verdade que a seção de fronteira acima existe para evitar. O que
este standard acrescenta é onde o limite mora e o que ele governa.

**Contra-evidência, e ela é instrutiva:** outro projeto do acervo **não configura parâmetro de
amostragem nenhum** e roda no default do SDK. Não configurar é uma escolha válida, e mais honesta
que configurar errado. A regra final é: **configure quando você mediu que precisa; caso contrário,
não envie** (`ai-agents-agent-spec`, "nulo = não envia").

Onde o limite mora: no **alias**, como `Constraints` com procedência — a mesma forma que o kit
compila para `ToolCallingRequiresReasoningEffortNone` e `TemperatureUnsupported`. Um limite por
alias é verificável antes da chamada; um limite espalhado em `if`s só é verificável depois do 400.

---

## Pin de SDK e compatibilidade binária

**Faixa flutuante em SDK de IA custa dinheiro.** Não é uma preferência de estilo: é um incidente
medido.

O caso, do acervo: um projeto referenciava o SDK da OpenAI numa faixa `2.*`. O NuGet resolveu para
uma versão menor mais nova, um membro de conteúdo de resposta **mudou de assinatura**, e a chamada
estourou `MissingMethodException` **depois de a requisição já ter sido paga** — o dinheiro saiu, a
resposta não chegou ao domínio. A correção foi pinar a versão exata, com oito linhas de comentário
explicando por quê.

**As três regras:**

1. **Pin exato** (`[2.13.0]`, não `2.*`) para todo pacote de IA. Neste framework a fonte única é
   `framework/ai-pin.json`, e nenhum standard escreve versão à mão.
2. **A faixa e o pacote de ponte andam juntos.** `Microsoft.Extensions.AI.OpenAI` referencia uma
   versão do `OpenAI`; carregar outra em runtime é uma quebra binária esperando um caminho de código
   pouco exercitado.
3. **Teste de compatibilidade binária como rede de segurança** — comparar a versão do assembly
   carregado com a que a ponte referencia, e conferir a **assinatura** do membro que já quebrou uma
   vez. A técnica está em `ai-agents-testing-ai`.

---

## Anti-patterns

| Anti-pattern | Por quê é problema | Solução |
|---|---|---|
| Loop de retry próprio sobre a chamada de LLM | Multiplica com o retry do SDK: 3 × 3 = 9 chamadas pagas por incidente | Herde; para outro número, substitua a política |
| Chamada de LLM sem timeout | Retry sobre o que nunca expira; o incidente vira latência infinita | Timeout explícito — por spec de agente (o exemplar do kit) ou por alias, se a allowlist foi aberta |
| `CancellationToken` de turno usado como timeout | Só cancela quando alguém desiste; ninguém desiste de um job noturno | Timeout explícito, além do token |
| Flag global de processo para strict | Um 400 em dev rebaixa produção no mesmo processo | Capacidade por alias, em `Constraints` com procedência |
| `Temperature` em modelo de raciocínio | Erro do provider, ou pior: silenciosamente ignorado | Esforço tipado (`ReasoningEffort`) para raciocínio; amostragem só sem raciocínio |
| Parâmetro de amostragem configurado "por garantia" | Envia valor que ninguém mediu, e some no default do provider | Nulo = não envia |
| Faixa flutuante (`2.*`) em pacote de IA | Quebra binária depois da chamada paga | Pin exato, a partir de `framework/ai-pin.json` |
| Limite de alias escrito como regra universal em C# | Vira folclore sem procedência, e ninguém sabe revogá-lo | Campo do alias, com procedência obrigatória |

---

## Checklist (verifiable by morph-eval)

- [ ] Nenhum loop de retry escrito à mão em volta de uma chamada de LLM
- [ ] Há timeout explícito (por spec de agente, como o exemplar do kit, ou por alias se a allowlist foi aberta), e ele **não** é o `CancellationToken` do turno
- [ ] Nenhuma flag estática de processo governa strict ou qualquer outra capacidade
- [ ] Capacidade e limite são campos do **alias**, com procedência escrita
- [ ] Nenhuma ocorrência de um tipo chamado `StrictJsonSchema` (ele não existe em nenhum pacote do pin)
- [ ] Esforço de raciocínio usa o enum `ReasoningEffort`, não string
- [ ] `Temperature` não é enviada junto com esforço de raciocínio
- [ ] Todo pacote de IA está pinado em versão exata, derivada de `framework/ai-pin.json`
- [ ] Existe um teste de compatibilidade binária cobrindo o par SDK × ponte

---

## References

- `ai-agents-providers-model-registry` — Layer 0: o **schema** do documento, `defaultAlias`, fallback
  e a tabela de providers. Este standard não repete nada disso
- `ai-agents-agent-spec` — a forma do agente e a regra "nulo = não envia"
- `ai-agents-cost-and-budget` — `pricing` por alias, cálculo e teto de gasto
- `ai-agents-testing-ai` — o teste de compatibilidade binária, e como exercitar opções sem rede
- `ai-agents-structured-output` — o schema da saída, cujo strict este standard governa
- `framework/ai-pin.json` — a fonte única das versões; nenhuma versão é digitada num standard

---

*MORPH-SPEC by Polymorphism Tech — ai-agents/llm-runtime-defaults.md v1.0 (2026-09-08)*
