AUR-596: provedores de LLM conhecidos por configuração
Medido antes (base dff46828)
- O Aurum falava um único dialeto: Chat Completions compatível com OpenAI em
internal/llm/provider/litellm, comAuthorization: Bearer,max_tokenseresponse_format(json_schemaoujson_object), escolhido só porLLM_API_KEY+LLM_BASE_URL. A URL era sempre<base>/chat/completions. - Um erro do provedor virava
LiteLLM API error (status N): <corpo cru>, sem o nome do provedor, sem resumo e sem redação da chave ecoada no corpo. - Uma resposta JSON sem lista de achados (
{"answer":"ok"}) era decodificada como lista vazia: uma aprovação que o modelo nunca deu. - Diretórios vazios
internal/llm/provider/{anthropic,openai,ollama}sem uso.
O que mudou
internal/llm/provider/profiles/profiles.yml(embutido porgo:embed): o catálogo declarativo de perfis —openai-compatible(padrão),openai,azure-openai,anthropic,google,bedrock,litellm,openrouter,opencode,ollama. Campos:base_url(com{VAR}e{VAR:-padrão}),key_env,auth(bearer|header|none),auth_header,path,query,token_field(max_tokens|max_completion_tokens),structured_output(json_schema|json_object|none).profiles/catalog.go: carrega e valida o catálogo (valor fora da lista falha ao carregar, nunca no fio);Mergeaplica o arquivo do operador (LLM_PROVIDERS_FILE), que acrescenta ou substitui um perfil inteiro.profiles/resolve.go:FromEnvlêLLM_PROVIDER; ausente, devolve "não selecionado" e o caminho antigo segue intacto. Perfil desconhecido, chave ausente,{VAR}sem valor ou URL ausente são erro (nunca recaída silenciosa). Provedor, URL e chave vêm só do ambiente do operador, nunca da configuração do repositório revisado (por isso não hállm.providerno config do repositório).litellm/dialect.go: oDialectaplicado pelo motor único (URL + path + query, autenticação, campo do limite,response_format).DefaultDialectreproduz o dialeto antigo;NewProvidero usa, então semLLM_PROVIDERa requisição é byte a byte a de antes.litellm/errors.go: erro não-200 resumido à mensagem do envelope do provedor (OpenAI/Azure/OpenRouter/LiteLLM{"error":{...}}, Anthropic{"type":"error","error":{...}}, Google{"error":{"status"...}}ou lista, Bedrock{"message":...}), redigido (a chave e formas conhecidas de segredo) antes de truncar em 300 caracteres, com o nome do perfil.internal/prompt: resposta semissues/line_commentsé erro de validaçãomissing_issues(revisão inconclusiva), para todo provedor.cmd/aurumcode/main.go:providerFromEnvconsulta o catálogo antes do caminho antigo.review_cache.go: a URL resolvida pelo perfil entra na chave do cache quandoLLM_BASE_URLestá vazia (dois deployments Azure não compartilham entrada)..github/workflows/providers-smoke.yml: sóworkflow_dispatch; o jobdetectarlista provedores com secrets; matrizsmokesó para esses; os demais aparecem como "não verificado"; sem nenhum secret a execução falha.actions/checkoutpinado por SHA.actionlintlimpo.- Docs:
docs/provedores.md(nova, no nav), links emconfiguration.mdegetting-started.md, seção do servidor MCP do Aurum no OpenCode (opencode.json, chavemcp,type: local,command: ["aurumcode","mcp"]). Tutorialdemo/tutoriais/provedores+docs/tutorials/provedores.md. - Removidos os diretórios vazios
internal/llm/provider/{anthropic,openai,ollama}.
Fontes dos valores do catálogo (conferidas em 2026-10-07)
| Perfil | Valor | Fonte |
|---|---|---|
| openai | https://api.openai.com/v1, Bearer, max_completion_tokens, json_schema |
https://platform.openai.com/docs/api-reference/chat/create |
| azure-openai | https://{recurso}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version=..., header api-key; 2024-10-21 é release GA do data plane |
https://learn.microsoft.com/en-us/azure/ai-foundry/openai/reference |
| anthropic | https://api.anthropic.com/v1/chat/completions, Bearer; response_format ignorado |
https://docs.anthropic.com/en/api/openai-sdk |
https://generativelanguage.googleapis.com/v1beta/openai/, Bearer; JSON Schema restrito (perfil usa json_object) |
https://ai.google.dev/gemini-api/docs/openai | |
| bedrock | https://bedrock-runtime.{region}.amazonaws.com/openai/v1/chat/completions, chave de API Bedrock como Bearer (AWS_BEARER_TOKEN_BEDROCK); SigV4 fora de escopo (via proxy LiteLLM) |
https://docs.aws.amazon.com/bedrock/latest/userguide/inference-chat-completions.html |
| litellm | proxy em http://localhost:4000, chave mestra/virtual Bearer |
https://docs.litellm.ai/docs/proxy/quick_start |
| openrouter | https://openrouter.ai/api/v1, Bearer |
https://openrouter.ai/docs/api-reference/overview |
| opencode | https://opencode.ai/zen/v1/chat/completions para os modelos servidos em Chat Completions (os demais usam /responses ou /messages, fora deste motor) |
https://opencode.ai/docs/zen/ |
| ollama | http://localhost:11434/v1, chave ignorada |
https://docs.ollama.com/api/openai-compatibility |
| OpenCode MCP | opencode.json, mcp.<nome>.type: local, command em lista, environment, {env:VAR} |
https://opencode.ai/docs/mcp-servers/ e https://opencode.ai/docs/config/ |
Prova
Aceite: tests/acceptance/AUR-596.sh (seletores all, AC-001..AC-006,
MUT-001..MUT-003; copia cmd internal pkg go.mod go.sum inteiros).
| Item | Como foi provado | Testes |
|---|---|---|
| AC-001 | o DefaultDialect envia o mesmo corpo, path e headers de antes; sem LLM_PROVIDER o catálogo não é selecionado |
TestDefaultDialectRequestIsUnchanged, TestUnsetProviderKeepsTheLegacySelection |
| AC-002 | para cada perfil, um httptest com o contrato escrito independente do yml (path, query, auth, campo de tokens, response_format, URL padrão) recebe a requisição; uma revisão real (runReview) conclui com exit 0 em todos os perfis; todo perfil tem LLM_PROVIDER=<nome> em docs/provedores.md |
TestEveryProfileSpeaksItsDocumentedDialect, TestAUR596EveryProfileConcludesAReview |
| AC-003 | max_completion_tokens sem max_tokens; Azure com api-key e api-version, sem Authorization |
TestDialectSendsOnlyItsTokenFieldAndAuth, contrato acima |
| AC-004 | resposta sem lista de achados é missing_issues; na CLI exit 1 validation_failed; no gate do agente (aurumcode mcp) inconclusive |
TestAnswerWithoutFindingsListIsInconclusive, TestAUR596OutOfSchemaAnswerIsInconclusive, TestAUR596OutOfSchemaAnswerIsInconclusiveForTheAgentGate |
| AC-005 | envelopes OpenAI/Anthropic/Google/Bedrock resumidos, chave redigida, nome do perfil | TestProviderErrorIsSummarisedRedactedAndNamed, TestAUR596ProviderErrorIsRedactedAndNamed |
| AC-006 | LLM_PROVIDER=nao-existe sai diferente de 0 listando os 10 perfis |
TestUnknownProfileListsTheValidOnes, TestAUR596UnknownProfileFailsListingTheValidOnes |
| MUT-001 | replyCap ignora o perfil: vermelho por asserção (max_completion_tokens = null, want 64) |
|
| MUT-002 | hasFindingsList desligado: vermelho ({"answer":"looks fine to me"}: ... err=<nil>, exit=0) |
|
| MUT-003 | Authorization junto do api-key: vermelho (Authorization sent together with api-key) |
Execução (2026-10-07, container compartilhado go-shared):
$ bash tests/acceptance/AUR-596.sh all
AUR-596/AC-001/pass
AUR-596/AC-002/pass
AUR-596/AC-003/pass
AUR-596/AC-004/pass
AUR-596/AC-005/pass
AUR-596/AC-006/pass
AUR-596/MUT-001/rejected
AUR-596/MUT-002/rejected
AUR-596/MUT-003/rejected
AUR-596/all/pass
Selado: ./.board/bin/oci-run --profile go-unit-offline-v1 --card AUR-596 →
exit_code: 0, stdout AUR-596/all/pass.
Suíte: go vet ./... && go test -p 1 ./... → exit 0 (47 pacotes ok).
Tutoriais: imagem aurum-tutoriais:aur596 construída desta árvore; os 20
tutoriais reexecutados (run.sh all) e --check em todos CHECK OK;
scripts/docs/capturas.sh (251 imagens) e scripts/docs/build.sh sem
warnings; AUR-561..564, AUR-586 e AUR-588 passam.
Tutorial provedores (provedor falso dentro do container, rede none): sem
LLM_PROVIDER o provedor recebe authorization=Bearer <chave> e
response_format=json_object; com azure-openai,
query=api-version=2024-10-21 authorization=ausente api-key=<chave>; com
anthropic, response_format=ausente; resposta fora do schema → exit 1
could not understand the model's response (validation_failed); erro 401 →
anthropic API error (status 401): authentication_error: invalid x-api-key [REDACTED].
Decisões e pendências
- Nenhum SDK de fornecedor e nenhuma dependência nova; Bedrock sem chave de API (SigV4) vai pelo proxy LiteLLM, documentado.
- A revisão hoje não envia limite de resposta;
token_fieldvale para todo chamador que envia (provado nos testes de contrato, AC-003/MUT-001). - Modelos do OpenCode Zen servidos só em
/responsesou/messagesnão são cobertos pelo perfilopencode(o motor fala Chat Completions). - Nenhum provedor foi exercitado ao vivo neste card: o smoke é manual e depende de secrets do repositório.
- O workflow reutilizável (
.github/workflows/review.yml) ainda exige e repassa sóLLM_API_KEYeLLM_BASE_URL; repassarLLM_PROVIDERe as variáveis de cada perfil fica para um card com esse arquivo nospaths. Pela CLI, pela imagem e pelo servidor MCP o perfil já vale. docs/agentes.md(página dos agentes MCP) está fora dospathsdo card: o OpenCode aparece emdocs/provedores.md, que aponta para ela.