Modelos e provedores
Este capítulo explica como um turno acaba conversando com um modelo específico, o que acontece quando esse modelo falha, e por que dois membros do mesmo deploy podem ver ferramentas diferentes no mesmo agente. Ele foi escrito para o harness ganglion, que é o que este livro ensina; veja harnesses para o outro.
Dois sistemas estão envolvidos e ajuda mantê-los separados. O proxy decide quais modelos um workspace tem — isso é o inventário e a cascata descritos no guia do administrador. O harness decide, turno a turno, qual desses modelos de fato responde. Este capítulo é sobre a segunda metade, mais o encanamento que leva as credenciais de um lado ao outro.
Como o harness fica sabendo dos modelos
Toda vez que o proxy garante que o container de um membro está pronto, ele resolve o modelo daquele workspace a partir do inventário e escreve um arquivo de configuração no diretório do membro: .ganglion-config.json. O arquivo é montado no container somente leitura em /data/.ganglion/config.json, e o GANGLION_CONFIG_FILE aponta o harness para ele (crab/crab-shell-proxy/internal/docker/ganglion_config.go).
Ele é somente leitura, e fica acima do único diretório em que o container pode escrever, por um motivo específico: uma lista de modelos que o agente pudesse editar deixaria uma ferramenta guiada por texto não confiável escolher o endpoint para onde as próprias chaves de API são enviadas.
O formato do arquivo é o config.json do picoclaw, de propósito, para que uma única tela de administração gerencie os dois harnesses. O harness lê model_list, agents.defaults.model_name, agents.defaults.model_fallbacks, agents.defaults.image_model, agents.defaults.image_gen_model, tools.web.* e tools.mcp, e ignora tudo o mais que encontra. Um arquivo ausente não é um erro: o harness sintetiza uma lista de uma entrada só, chamada default, a partir de três variáveis de ambiente e se comporta como se comportava antes de o arquivo existir (LoadRegistry em crab/crab-ganglion-harness/internal/config/file.go).
As três cadeias
Um turno pede ao registro uma lista ordenada de modelos candidatos. Qual lista ele recebe depende do tipo de turno (Kind no mesmo arquivo):
| Tipo | Configurado por | Cai para a cadeia de texto? |
|---|---|---|
| texto | agents.defaults.model_name + model_fallbacks | ela é a cadeia de texto |
| visão | agents.defaults.image_model + image_model_fallbacks | sim |
| geração de imagem | agents.defaults.image_gen_model + image_gen_model_fallbacks | não |
A cadeia de texto
O Chain monta a lista de candidatos assim. Se o turno nomeou um modelo que o registro realmente conhece, esse modelo vem primeiro. Caso contrário, a lista começa com model_name seguido de model_fallbacks. Depois — para cada entrada que já está na lista — os fallbacks daquela entrada são acrescentados, um nível de profundidade. Entradas desabilitadas ou desconhecidas são descartadas, e duplicatas são unificadas.
Um turno que nomeia um modelo que o registro não tem é o caso comum, não uma anomalia: o proxy preenche o campo de modelo do turno com um valor de espaço reservado. Então o harness o resolve silenciosamente para a cadeia padrão em vez de reportar qualquer coisa ao membro.
A regra de um nível merece atenção. Os fallbacks de um fallback não são percorridos recursivamente, e a expansão itera sobre um instantâneo, então um ciclo na configuração não consegue travar um turno.
Um administrador não edita este arquivo para mudar a cadeia. O model_fallbacks é escrito pelo proxy a partir da cadeia declarada no registro de inventário do modelo que foi resolvido, então a cadeia é editada na seção Modelo, no guia do administrador.
A cadeia de visão
Um turno é um turno de visão quando qualquer mensagem na janela de contexto carrega um anexo (hasAttachments em crab/crab-ganglion-harness/internal/runtime/loop.go).
Se o image_model nomeia uma entrada, essa cadeia responde. Se não nomeia, o registro devolve a cadeia de texto no lugar. Isso não é gentileza: se um modelo enxerga ou não é uma propriedade do modelo, não de um slot, então um deploy cujo único modelo por acaso é multimodal não precisa de uma segunda entrada e não deve ser obrigado a escrever uma.
A cadeia de geração de imagem
Esta não cai para outra. Se o image_gen_model não nomeia nada, a cadeia fica vazia. O motivo está dito no código: um modelo de texto a quem se pede uma imagem devolve um texto descrevendo uma, o que é pior do que uma ferramenta ausente porque parece sucesso.
A consequência é a próxima seção.
O renderizador do proxy escreve
model_name,model_fallbacks,model_list,tools.webetools.mcp— e nãoimage_modelouimage_gen_model(ganglionConfigDocemcrab/crab-shell-proxy/internal/docker/ganglion_config.go). Para um agente ganglion, essas duas chaves só chegam a um workspace se forem adicionadas à mão no editor de instância única da seção Config, que as guarda no overlay por instância. Nenhuma das duas é uma chave de propriedade do proxy, então o overlay as mantém e as reaplica a cada renderização. Uma cadeia de visão ou de geração de imagem é, portanto, por membro neste harness, não algo que um escopo consiga definir.
O que um fallback significa na hora do turno
Uma cadeia não é um laço de repetição em volta do turno inteiro. O completeWithFallback em loop.go diz a regra: um candidato só é abandonado enquanto nada chegou ao membro. Assim que um único byte de conteúdo é emitido, o turno está comprometido com aquele modelo e a falha dele aparece — recomeçar com outro modelo emendaria duas vozes numa mesma resposta, e o membro já leu a primeira metade da primeira.
Quando a cadeia acaba, o erro que o membro vê é o do último provedor, não um resumo sintético. Um operador precisa do motivo pelo qual a última tentativa falhou, e “todos os 3 modelos falharam” enterraria esse motivo.
Há uma degradação em cima disso, e ela é um recurso, não gentileza. Se a cadeia inteira falha num turno que carrega uma imagem, a imagem é descartada e o turno é tentado de novo uma vez, só com texto, contando ao modelo o que aconteceu. O membro vê um aviso de progresso dizendo que a imagem não pôde ser lida e recebe uma resposta degradada que diz isso.
Isso existe porque a alternativa é permanente. A referência da mídia continua no histórico da conversa, então um harness que simplesmente falhasse falharia do mesmo jeito em todos os turnos seguintes daquela conversa. Esta stack passou exatamente por isso em produção.
Quais ferramentas do agente existem
Esta é a pergunta que os membros de fato fazem — “por que meu agente não consegue pesquisar na web”, “por que ele não consegue fazer uma imagem para mim” — e a resposta honesta é que o registro de ferramentas é condicional. Uma ferramenta cujos pré-requisitos não estão configurados simplesmente não está lá. Ela não fica presente e falhando, porque uma ferramenta de que o modelo sabe e que nunca pode responder é pior do que nenhuma ferramenta: ela gasta um turno descobrindo a ausência.
O tools() em crab/crab-ganglion-harness/cmd/crab-ganglion/main.go é a decisão inteira:
| Ferramenta | Presente quando |
|---|---|
| shell | sempre |
load_image | sempre |
set_reasoning_depth | sempre |
web_search, web_fetch | pelo menos um provedor de busca está habilitado e pronto |
generate_image | a cadeia de geração de imagem tem pelo menos uma entrada com chave de API |
| despacho de subagente | o fan-out de subagentes está habilitado com orçamentos diferentes de zero |
research | o despacho de subagente existe e um provedor de busca está configurado |
| ferramentas do grafo de memória | o proxy gerou um token MCP para o workspace |
O load_image é incondicional de propósito: uma imagem no workspace é algo que qualquer deploy pode ter. Se um modelo consegue ver o resultado é decidido pela cadeia de visão na hora da conclusão, não aqui.
O research precisa de um despachante e de busca, porque sem busca ele é um modelo a quem se pede que lembre — que é justamente a falha que ele existe para substituir.
O harness registra cada uma dessas decisões no boot. Se um membro relata uma capacidade faltando, as primeiras linhas de log do container dizem quais ferramentas foram habilitadas e quais não foram.
Por que a busca, em especial, é fácil de errar
Duas listas de nomes de provedores precisam concordar, e elas não coincidem por completo.
O proxy aceita um segredo compartilhado native no slot web.<provider> para estes: brave, tavily, kagi, gemini, perplexity, glm_search e baidu_search (webProviders em crab/crab-shell-proxy/internal/docker/secrets.go). Todos eles são escritos na configuração do ganglion como tools.web.<name>: {enabled: true}, com a chave passada em separado.
O harness implementa quatro: brave, tavily, searxng e duckduckgo, nessa ordem de preferência quando tools.web.provider não está definido (WebProviderNames em file.go). Qualquer outra coisa no arquivo é ignorada.
Então, dos provedores que um administrador pode registrar pela seção Segredos, só brave e tavily de fato dão a um agente ganglion uma ferramenta de busca. Registrar uma chave kagi ou perplexity produz uma configuração que parece correta e não rende ferramenta nenhuma.
A prontidão também varia por provedor (providers.go): brave e tavily precisam de chave, searxng precisa de uma base_url e de nenhuma chave, e duckduckgo só precisa estar habilitado. Um bloco de provedor sem enabled: true está desligado — declarar "brave": {} o deixa presente e desabilitado, o que combina com os próprios exemplos do picoclaw.
Chaves de provedor
Credenciais nunca viajam no arquivo de configuração. O proxy separa estrutura de segredos do jeito que o picoclaw faz: o arquivo carrega endpoints e nomes, o ambiente carrega chaves, uma variável por modelo.
Há três lugares de onde uma chave pode vir.
A chave do próprio agente. Cada agente em crab/crab-shell-proxy/config.yaml nomeia um apiKeyEnv, e o proxy lê essa variável do próprio ambiente. Para um agente ganglion isso vira GANGLION_API_KEY, ao lado de GANGLION_MODEL e GANGLION_BASE_URL. Esse trio é o piso: é com ele que um workspace roda quando o inventário não resolve nada para ele.
Se essa variável não está definida, um agente ganglion é desabilitado na carga. O proxy registra o motivo, nomeando a variável, e as rotas do agente respondem 404 em vez de o proxy se recusar a subir. Um agente picoclaw, de propósito, não está sujeito a isso — a chave dele é escrita num arquivo por usuário, e uma chave vazia aparece como erro de autenticação na primeira chamada ao modelo.
A chave de um modelo do inventário. Quando a cascata resolve um modelo do inventário, a chave dele é passada como GANGLION_MODEL_KEY_<NAME>, uma variável por modelo da cadeia. O nome vem de colocar o nome do modelo em maiúsculas e trocar tudo que está fora de A-Z0-9 por um sublinhado. Os dois lados calculam isso de forma independente — ganglionModelKeyEnv no proxy e KeyEnvVar no harness — e o contrato inteiro é os dois concordarem. Dois nomes de modelo que diferem só na pontuação colidem, e isso é aceito: a alternativa é uma codificação que ninguém consegue ler na saída do docker inspect, que é onde essas coisas são depuradas.
A chave de um provedor de busca. Mesmo esquema, prefixo diferente: GANGLION_WEB_KEY_<PROVIDER>, vinda do segredo compartilhado native no slot web.<provider>.
O harness resolve uma chave primeiro pelo ambiente e só depois pelo arquivo. O ambiente ganha porque é o caminho que o proxy usa, e porque uma chave que nunca entra num arquivo não pode ser lida por nada que seja apontado para o arquivo por engano.
Um modelo da cadeia cuja chave está vazia simplesmente não é oferecido à ferramenta de geração de imagem — o imagegen.New filtra candidatos por chave não vazia e não devolve ferramenta nenhuma quando nenhum sobrevive. Na cadeia de texto o efeito é outro: o candidato é tentado e falha na hora da chamada, e a cadeia segue em frente.
As chaves também podem ser guardadas criptografadas. Um valor que começa com
enc://é resolvido dentro do container a partir de dois fatores que chegam por caminhos diferentes — uma senha emGANGLION_KEY_PASSPHRASEe um arquivo de chave montado somente leitura a partir do host. Eles são de tipos diferentes de propósito: os dois como variável de ambiente significaria que um únicodocker inspectentrega o texto puro.
De onde vem o endpoint de um modelo
O ganglion recebe uma URL base e posta nela; ele não tem uma tabela interna mapeando nome de provedor para endereço. O picoclaw tinha uma, então um agente migrado do picoclaw chega nomeando um provedor, sem nomear endpoint, e costumava falhar no primeiro turno com unsupported protocol scheme "".
O proxy preenche a lacuna a partir de três fontes, nesta ordem (resolveGanglionEndpoints em crab/crab-shell-proxy/internal/docker/ganglion.go):
- O
api_basedo próprio registro de inventário, que é definitivo. Um modelo customizado é customizado justamente porque o endpoint dele não é o padrão do provedor dele. - O
baseUrldo agente, vindo doconfig.yaml. - O padrão do provedor, de um catálogo embutido com cerca de trinta pares provedor/modelo (
ProviderEndpoint,model-catalog.json).
As entradas de fallback também são preenchidas, não só a principal — uma cadeia cuja segunda entrada não tem endpoint é uma cadeia que funciona até o dia em que ela é necessária. Uma entrada principal sem endpoint em lugar nenhum é recusada onde um operador consegue ver.
Duas ressalvas vindas de provider_endpoint.go. A entrada de catálogo do Azure é um molde para preencher, não um endereço, então ela fica de fora da tabela de fallback. E os runtimes locais — ollama, lmstudio, vllm, github-copilot — têm entradas de localhost, o que dentro de um container quer dizer o container, não o host; usar um deles significa definir um baseUrl explícito.
Profundidade de raciocínio
Uma entrada de modelo pode declarar thinking_level, um entre off, low, medium, high, xhigh ou adaptive. Declarar a chave é a declaração de capacidade: um modelo sem thinking_level nunca recebe um campo de profundidade por caminho nenhum, porque o harness não tem como descobrir se um endpoint aceita um, e o operador tem.
Um valor não reconhecido é tratado como ausente e gera um aviso, em vez de ser lido como off. Essa distinção importa: um erro de digitação que silenciosamente quisesse dizer “pense menos” pareceria uma configuração funcionando até a hora da fatura.
Se um modelo rejeita um campo de raciocínio no meio da cadeia, o campo é removido e o mesmo modelo é consultado mais uma vez, em vez de a cadeia queimar seu orçamento por causa de um campo que ninguém pediu.
Para onde ir agora
O guia do administrador trata de registrar modelos e escolher quem recebe qual. Criando um agente customizado mostra onde o modelo padrão de um agente é declarado. Solução de problemas reúne os sintomas que esses mecanismos produzem.