Pular para conteúdo

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, com Authorization: Bearer, max_tokens e response_format (json_schema ou json_object), escolhido só por LLM_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 por go: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); Merge aplica o arquivo do operador (LLM_PROVIDERS_FILE), que acrescenta ou substitui um perfil inteiro.
  • profiles/resolve.go: FromEnv lê 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.provider no config do repositório).
  • litellm/dialect.go: o Dialect aplicado pelo motor único (URL + path + query, autenticação, campo do limite, response_format). DefaultDialect reproduz o dialeto antigo; NewProvider o usa, então sem LLM_PROVIDER a 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 sem issues/line_comments é erro de validação missing_issues (revisão inconclusiva), para todo provedor.
  • cmd/aurumcode/main.go: providerFromEnv consulta o catálogo antes do caminho antigo. review_cache.go: a URL resolvida pelo perfil entra na chave do cache quando LLM_BASE_URL está vazia (dois deployments Azure não compartilham entrada).
  • .github/workflows/providers-smoke.yml: só workflow_dispatch; o job detectar lista provedores com secrets; matriz smoke só para esses; os demais aparecem como "não verificado"; sem nenhum secret a execução falha. actions/checkout pinado por SHA. actionlint limpo.
  • Docs: docs/provedores.md (nova, no nav), links em configuration.md e getting-started.md, seção do servidor MCP do Aurum no OpenCode (opencode.json, chave mcp, type: local, command: ["aurumcode","mcp"]). Tutorial demo/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
google 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_field vale para todo chamador que envia (provado nos testes de contrato, AC-003/MUT-001).
  • Modelos do OpenCode Zen servidos só em /responses ou /messages não são cobertos pelo perfil opencode (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_KEY e LLM_BASE_URL; repassar LLM_PROVIDER e as variáveis de cada perfil fica para um card com esse arquivo nos paths. Pela CLI, pela imagem e pelo servidor MCP o perfil já vale.
  • docs/agentes.md (página dos agentes MCP) está fora dos paths do card: o OpenCode aparece em docs/provedores.md, que aponta para ela.