From 3a8506ad06533d7c5c4633a65fd59949f186c215 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 8 Aug 2026 23:07:12 -0300 Subject: [PATCH 1/2] =?UTF-8?q?chore(ai-chat):=20Sollinha=20via=20Deep=20I?= =?UTF-8?q?nfra=20=E2=80=94=20mesma=20API=20e=20chave=20dos=20agentes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Troca o provider da Sollinha de @ai-sdk/deepseek (api.deepseek.com) para @ai-sdk/deepinfra (API OpenAI-compatível da Deep Infra), mantendo o mesmo modelo deepseek-ai/DeepSeek-V4-Flash e as mesmas tools. Uma chave só (DEEPINFRA_API_KEY), a mesma dos agentes; rollback = git revert. Closes #449 --- .env.example | 6 +- docs/CHANGELOG-AGENTS.md | 2 + docs/plans/sollinha-no-deepinfra-impl.md | 95 +++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 25 +++-- .../(campaign)/campanha/api/ai-chat/route.ts | 4 +- 6 files changed, 122 insertions(+), 12 deletions(-) create mode 100644 docs/plans/sollinha-no-deepinfra-impl.md diff --git a/.env.example b/.env.example index 78e1c567..ee78a22a 100644 --- a/.env.example +++ b/.env.example @@ -36,6 +36,6 @@ CAMPAIGN_EMAIL_FROM_NAME=Campanha Jorge Solla # Note: the automated test suite ignores this file and reads .env.test instead, # and only ever runs against a database whose name ends with "_test". -# DeepSeek API key for the AI chat feature (Sollinha). -# Get one at https://platform.deepseek.com/api_keys -DEEPSEEK_API_KEY=YOUR_DEEPSEEK_API_KEY_HERE +# Deep Infra API key for the AI chat feature (Sollinha) — same provider/key the +# agents use (one provider, one key, one invoice). Get one at https://deepinfra.com +DEEPINFRA_API_KEY=YOUR_DEEPINFRA_API_KEY_HERE diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 3265ee58..c3d639a0 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**Recently resolved (2026-08-08):** **B174** (Issue #449) — a Sollinha (`/campanha/api/ai-chat`) trocou o provider de `@ai-sdk/deepseek` (API oficial `api.deepseek.com`) pelo **`@ai-sdk/deepinfra`** no caminho OpenAI-compatível da Deep Infra — **mesmo modelo** (`deepseek-ai/DeepSeek-V4-Flash`), **mesmas tools** e **mesma chave** que os agentes do time usam (um provedor, uma chave, uma fatura). Swap de 2 linhas no `route.ts`; dep `@ai-sdk/deepinfra@3.0.20` pinada na mesma ferroviária do `ai@7.0.47` (provider 4.0.4 / provider-utils 5.0.18, `pnpm why` confirma cópia única — nada de 4.0.7 duplicado), `@ai-sdk/deepseek` removido (dead dep, knip); `.env.example` `DEEPSEEK_API_KEY` → `DEEPINFRA_API_KEY` (B173 STT/Whisper reusa a mesma chave). Sem migration, access, UI ou teste novo — e2e já mocka o SSE (sem chave real no CI); rollback documentado = `git revert` (restaura provider/baseURL antiga); smoke de deploy = 2–3 perguntas forçando tool calling real antes de confiar o flip. Intenção [`sollinha-no-deepinfra.md`](plans/sollinha-no-deepinfra.md); impl [`sollinha-no-deepinfra-impl.md`](plans/sollinha-no-deepinfra-impl.md). + **Recently resolved (2026-08-08):** **`pnpm worktree plan [bag] [--go] [--no-migrate]`** — worktree de **planejamento** para rodar a skill `/plan-issue` sem ocupar o main: **cada invocação cria UM DIFERENTE**, para sessões de planejamento paralelas — com `bag`, branch `plans/plan-issue-` (sufixo `-2`/`-3` se o nome já estiver vivo); sem `bag`, o próximo sequencial `plans/plan-issue-` livre (`PLAN_BRANCH_PREFIX`/`planBranchName`, `scripts/lib/worktree.mjs`). Prefixo minúsculo `plans/…` nunca colide com um `next` posterior em branch nem em slot (slot do plano = hash do branch, registrado via marcador e bumpado pelos vivos). Mesmo provisionamento isolado do `next` (porta `3100+slot`, bancos `teqo_wt`/`teqo_wt_test`, migrations; sem Docker degrada ao compartilhado). `kill` cobre; comment das envs distingue `gerado por pnpm worktree plan`. Sem migration. Naming unit-testada. **Recently resolved (2026-08-08):** **B168** (Issue #422) — desfeito o **encadeamento automático** das ações rápidas do wizard (B98 era o precedente que este item inverte; evidência de campo: o coordenador rejeitou a tela avançando sozinha). Ao salvar **qualquer** ação de ajuste (votos, tendência, atualização, liderança), o usuário volta à origem (`from` allowlist B110 mantido; Início sem `from`) com toast — nunca avança para outro wizard. Removidos a fila/pipeline `wizardActionChain.ts` (fundido `wizardReturnHref` + `wizardPreviousHref` só com passos internos em `campaignActionRoutes.ts`), o param `entry`, os skip resolvers/`Pular` (`WizardSkipTrailing`/`WizardTrendSkipTrailing` deletados, chrome mobile sempre “X”) e o prefill de nota sem produtor (`notePrefill`/`voteFrom`/`voteTo`). Tendência: pós-save o RSC refresha com status já aplicado e o **client** é o único dono da navegação (toast + retorno) — o redirect servidor engolia o toast (B97 corrigido no caminho). Liderança: botão flutuante pós-save passou de “Continuar” (avançava) para **“Concluir”** (retorna à origem). Sem migration. Intenção [`desfazer-encadeamento-acoes-wizard.md`](plans/desfazer-encadeamento-acoes-wizard.md); impl [`desfazer-encadeamento-acoes-wizard-impl.md`](plans/desfazer-encadeamento-acoes-wizard-impl.md). diff --git a/docs/plans/sollinha-no-deepinfra-impl.md b/docs/plans/sollinha-no-deepinfra-impl.md new file mode 100644 index 00000000..bf23d03c --- /dev/null +++ b/docs/plans/sollinha-no-deepinfra-impl.md @@ -0,0 +1,95 @@ +# Impl: B174 — Sollinha no Deep Infra (LLM) — mesma API e chave dos agentes + +Status: aprovado +Atualizado em: 2026-08-08 +Issue: #449 +Intenção: docs/plans/sollinha-no-deepinfra.md +Appetite restante: ~0,5 dia (herdado) +Impeccable: A — sem UI + +## Leitura da intenção + +- **Outcome:** A Sollinha (endpoint `/campanha/api/ai-chat`) passa a ser servida pela **Deep Infra** no caminho OpenAI-compatível, usando o **mesmo modelo** (DeepSeek-V4-Flash), as **mesmas tools** e a **mesma chave** que os agentes do time já usam — um provedor, uma chave, uma fatura. Sem mudança visível para o assessor. +- **O que NÃO negociar:** modelo permanece `deepseek-v4-flash` (equivalente Deep Infra `deepseek-ai/DeepSeek-V4-Flash`); tools/sistema da Sollinha intactos; nenhuma UI; rollback barato e documentado (reverter baseURL/chave volta ao estado atual); env/e2e com endpoint mockado continua sem chave real no CI. +- **O que reavaliar:** a hipótese de "área provável = `route.ts` + env `DEEPINFRA_API_KEY`" está correta; o desenho exato do provider (pacote oficial `@ai-sdk/deepinfra` vs. baseURL OpenAI-compatível crua) é a decisão de engenharia aqui. + +## Abordagem recomendada + +```mermaid +flowchart LR + subgraph client [Chat UI] + useChat + end + subgraph route [route.ts] + streamText + deepInfra + end + subgraph dl [Deep Infra] + openai[API OpenAI-compatível] + model[deepseek-ai/DeepSeek-V4-Flash] + end + subgraph tools [Tools Sollinha] + buildAITools + end + useChat --> route --> deepInfra --> openai --> model + streamText --> tools +``` + +**Opções consideradas:** +- **A — `@ai-sdk/deepinfra`** (provider oficial do AI SDK p/ Deep Infra; versão **3.0.20** que está na mesma "ferroviária" que o `ai@7.0.47` instalado — `@ai-sdk/provider@4.0.4` + `@ai-sdk/provider-utils@5.0.18`). Troca de 2 linhas: `deepSeek('deepseek-v4-flash')` → `deepInfra('deepseek-ai/DeepSeek-V4-Flash')`. O provider **lê `DEEPINFRA_API_KEY` do env sozinho** (source confirma `environmentVariableName: "DEEPINFRA_API_KEY"`). Seu default baseURL é `https://api.deepinfra.com/v1` + `/openai/...` = exatamente o endpoint OpenAI-compatível que a documentação da Deep Infra prescreve. +- **B — `@ai-sdk/openai-compatible` cru** com `baseURL: 'https://api.deepinfra.com/v1/openai'` manual + `loadApiKey` próprio. Funciona, mas reimplementa à mão o que o pacote A já encapsula (nome do provider, env var, headers/user-agent, schema de erro). +- **C — continuar com `@ai-sdk/deepseek` e só sobrescrever `baseURL`** p/ a Deep Infra. Hacky: provider com nome/contrato "DeepSeek oficial" apontando para outra API; model naming e tratamento de reasoning divergem. + +**Recomendação: A** — porque é o provider first-party do AI SDK para Deep Infra (mesma linha de código que o `deepSeek` já usa), é a menor superfície de diff (2 linhas no `route.ts` + env), e a Deep Infra serve V4-Flash no schema OpenAI-compatível que esse provider já serializa. C só funcionaria "por acaso"; B é parafuso de ouro onde A resolve com um martelinho. + +**Rejeitadas:** B (encapsulado por A, sem ganho); C (mal-nomeado, fragilidade de contrato desnecessária). + +### Componentes / mudanças + +- **`src/app/(campaign)/campanha/api/ai-chat/route.ts`**: `import { deepSeek } from '@ai-sdk/deepseek'` → `import { deepInfra } from '@ai-sdk/deepinfra'`; `model: deepSeek('deepseek-v4-flash')` → `model: deepInfra('deepseek-ai/DeepSeek-V4-Flash')`. Nada mais muda: `system`, `messages`, `tools: buildAITools`, `stopWhen: stepCountIs(10)`, rate limit, auth, streaming ficam idênticos. +- **`package.json`**: remove `@ai-sdk/deepseek`; adiciona `@ai-sdk/deepinfra@3.0.20` (pino explícito — `^3.0.20` flutuaria para 3.0.27 que puxa `@ai-sdk/provider@4.0.7`/`provider-utils@5.0.25`, criando uma SEGUNDA cópia de provider ao lado do `ai@7.0.47` que permanece em 4.0.4/5.0.18; pinar na mesma ferroviária evita isso — conferir com `pnpm why @ai-sdk/provider` após instalar). + - `@ai-sdk/deepseek` vira dependência morta → knip (CI-blocking) exige a remoção; rollback é `git revert` do commit + re-set `DEEPSEEK_API_KEY`, documentado abaixo. +- **`.env.example`**: substitui o bloco `DEEPSEEK_API_KEY` pelo bloco `DEEPINFRA_API_KEY` (com URL de geração da chave na Deep Infra). Nome `DEEPINFRA_API_KEY` é o que o provider A lê automaticamente E o mesmo que B173 (STT/Whisper na Deep Infra) reusa. +- **Migration:** sem migration (nenhum schema/collection). +- **Access / Consent:** intocados (auth/session do chat e RBAC das tools já existem; provider não muda async no RBAC). +- **UI:** Impeccable A — nenhuma mudança. +- **Docs:** atalho no `docs/plans/sollinha-no-deepinfra-impl.md` (este) + registro curto no `docs/CHANGELOG-AGENTS.md`. + +### Dados → forma + +N/A — sem apresentação de dados nova. (O "smoke das tools" abaixo é uma checagem de runtime, não uma forma de dados.) + +## Fases verificáveis + +1. **Dependência** — `pnpm add -E @ai-sdk/deepinfra@3.0.20` + `pnpm remove @ai-sdk/deepseek`; conferir `pnpm why @ai-sdk/provider` (uma cópia 4.0.4, sem 4.0.7 duplicado). +2. **Code** — swap de 2 linhas no `route.ts`. +3. **Env** — `.env.example` troca `DEEPSEEK_API_KEY` → `DEEPINFRA_API_KEY`. +4. **Gates** — `pnpm gate:fast` (lint + typecheck + unit) na iteração; entrega via `pnpm push` (inclui `gate:ci`: format:check + knip + madge). + +## Smoke (deploy-time; passo de runbook no merge, não no CI) + +Sollinha já está em produção e o routing de tools é o músculo dela. Antes/logo após o flip: + +1. Garantir `DEEPINFRA_API_KEY` na env de produção da Vercel (mesma chave usada pelos agentes; remover/ignorar a `DEEPSEEK_API_KEY` antiga — deixá-la definida não quebra, mas o objetivo é uma chave só). +2. Pós-deploy, disparar pelo chat real 2–3 perguntas que forcem **tool calling de dados** ("Quantos votos tivemos em Ilhéus em 2022?", "Quem é o deputado mais votado em Feira?", "Quais dobradinhas temos em Salvador?") e conferir resposta com dados reais (não alucinação margem). +3. Se qualquer tool falhar → **rollback**: `git revert` o commit do provider (restaura `@ai-sdk/deepseek` + `baseURL` oficial) OU simplesmente re-apontar a baseURL/chave antiga; redeploy. A documentação acima é o "rollback barato e documentado" do aceite. + +## Rabbit holes / Não escopo (engenharia) + +- **"Aproveita e sobe o `ai`/providers para as últimas versões"** — explodiria o diff e o risco de compatibilidade com `ai@7.0.47`/`react` em uso; 3.0.20 alinha a ferroviária atual. Reavaliar provider bump isoladamente se/quando o `ai` subir. +- **Teste unitário do provider** — sem ganho para um chore de infra: o endpoint (route) não tem unit test hoje, e o e2e mocka o SSE. O guard de regressão real é o e2e mockado (não requer chave) + o smoke de deploy. +- **Guard de código** ("model id fixo") — over-engineering para um chore; o diff de 2 linhas é o próprio contrato. + +## Riscos e mitigação + +- **Tool calling se comporta diferente via Deep Infra (V4-Flash no schema OpenAI-compatível)** → é exatamente o que o smoke cobre; se falhar, rollback de 1 commit. A Deep Infra documenta tool calling no endpoint OpenAI-compatível; o `streamText` do AI SDK abstrai as SSE. +- **Versão de provider duplicada** (3.0.27 puxando provider 4.0.7) → mitigado pinando `@ai-sdk/deepinfra@3.0.20` exact; verificar com `pnpm why`. +- **Chave errada/ausente em prod** → provider falha com 401 no request (não no boot); o e2e mockado e a UI continuam funcionando (erro aparece por stream como hoje). Smoke pega isso na primeira pergunta. + +## Aceite de engenharia + +- [x] Aceite de produto da intenção ainda coberto (mesmo modelo, mesmas tools, sem UI, rollback documentado, e2e mockado sem chave no CI) +- [x] Invariantes AGENTS/engineering-standards (dead code morre: `@ai-sdk/deepseek` removido; knip verde) +- [ ] Testes de domínio previstos (unit/int) onde access/write paths mudam — **nenhum path de access/write muda** (chore de provider); e2e existente (`campaignAiChatResize`, `campaignSollinhaWidth`) segue passando sem chave real, cobrindo o contrato. + +Self-score decision-quality: 5/5 — decisões caras (escolha de provider) têm rejeitadas; cabe no appetite; rabbit holes nomeados; depth check (não cria módulo novo além do swap; reusa o existing route); aceite de produto intacto. diff --git a/package.json b/package.json index 5eb1917f..58c31813 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,7 @@ "issue": "node scripts/issue.mjs" }, "dependencies": { - "@ai-sdk/deepseek": "^3.0.19", + "@ai-sdk/deepinfra": "3.0.20", "@ai-sdk/react": "^4.0.50", "@base-ui/react": "^1.6.0", "@fullcalendar/react": "7.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0850df1..59aad99d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,9 @@ importers: .: dependencies: - '@ai-sdk/deepseek': - specifier: ^3.0.19 - version: 3.0.19(zod@4.3.6) + '@ai-sdk/deepinfra': + specifier: 3.0.20 + version: 3.0.20(zod@4.3.6) '@ai-sdk/react': specifier: ^4.0.50 version: 4.0.50(react@19.2.4)(zod@4.3.6) @@ -288,8 +288,8 @@ packages: '@acemir/cssom@0.9.31': resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} - '@ai-sdk/deepseek@3.0.19': - resolution: {integrity: sha512-APzL/IpDSLhVlKQXo1gKCvTDLeli98ITzKnMq/eUNVWZ/UzWKYYG/0mCsmJRwTC37YE0ZzpxwDBw3J8xQT3lxg==} + '@ai-sdk/deepinfra@3.0.20': + resolution: {integrity: sha512-LqBoRPuSiU9R8hJF/q3/ujDnK5niDcQfbbSHoUrJNmSKOLT0Gwi6sL6l2XxxoJXLlt+1MtxDuZvO9GVbvmODLA==} engines: {node: '>=22'} peerDependencies: zod: ^3.25.76 || ^4.1.8 @@ -306,6 +306,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/openai-compatible@3.0.20': + resolution: {integrity: sha512-RsPg+HilsKc/d+jy4zItG7v7RJQm+caUu+iU0a5bx3eyyX5tkayMGDRklXMMMAPa4l34luzQwT3t4cmeHfdiWw==} + engines: {node: '>=22'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + '@ai-sdk/provider-utils@5.0.18': resolution: {integrity: sha512-UBNCrkxS5llgN2/RXLBRkjCFTIuL6YB1Goq5c+yRecnznhtX4XPjTwLHz2hrsTltTVZ0rqVegtnwFXdPBkjDHA==} engines: {node: '>=22'} @@ -7777,8 +7783,9 @@ snapshots: '@acemir/cssom@0.9.31': {} - '@ai-sdk/deepseek@3.0.19(zod@4.3.6)': + '@ai-sdk/deepinfra@3.0.20(zod@4.3.6)': dependencies: + '@ai-sdk/openai-compatible': 3.0.20(zod@4.3.6) '@ai-sdk/provider': 4.0.4 '@ai-sdk/provider-utils': 5.0.18(zod@4.3.6) zod: 4.3.6 @@ -7797,6 +7804,12 @@ snapshots: pkce-challenge: 5.0.1 zod: 4.3.6 + '@ai-sdk/openai-compatible@3.0.20(zod@4.3.6)': + dependencies: + '@ai-sdk/provider': 4.0.4 + '@ai-sdk/provider-utils': 5.0.18(zod@4.3.6) + zod: 4.3.6 + '@ai-sdk/provider-utils@5.0.18(zod@4.3.6)': dependencies: '@ai-sdk/provider': 4.0.4 diff --git a/src/app/(campaign)/campanha/api/ai-chat/route.ts b/src/app/(campaign)/campanha/api/ai-chat/route.ts index ccb88f52..8513ec55 100644 --- a/src/app/(campaign)/campanha/api/ai-chat/route.ts +++ b/src/app/(campaign)/campanha/api/ai-chat/route.ts @@ -1,4 +1,4 @@ -import { deepSeek } from '@ai-sdk/deepseek' +import { deepInfra } from '@ai-sdk/deepinfra' import configPromise from '@payload-config' import { convertToModelMessages, @@ -38,7 +38,7 @@ export async function POST(req: Request) { const { messages } = await req.json() const result = streamText({ - model: deepSeek('deepseek-v4-flash'), + model: deepInfra('deepseek-ai/DeepSeek-V4-Flash'), system: AI_SYSTEM_PROMPT, messages: await convertToModelMessages(messages), tools: buildAITools({ user, payload }), From 9d827fbd0db309a277a3b0e0b2c58e612dfc40e3 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Sat, 8 Aug 2026 23:07:49 -0300 Subject: [PATCH 2/2] style(docs): formato prettier no impl plan B174 --- docs/plans/sollinha-no-deepinfra-impl.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/plans/sollinha-no-deepinfra-impl.md b/docs/plans/sollinha-no-deepinfra-impl.md index bf23d03c..3a12d9d7 100644 --- a/docs/plans/sollinha-no-deepinfra-impl.md +++ b/docs/plans/sollinha-no-deepinfra-impl.md @@ -36,6 +36,7 @@ flowchart LR ``` **Opções consideradas:** + - **A — `@ai-sdk/deepinfra`** (provider oficial do AI SDK p/ Deep Infra; versão **3.0.20** que está na mesma "ferroviária" que o `ai@7.0.47` instalado — `@ai-sdk/provider@4.0.4` + `@ai-sdk/provider-utils@5.0.18`). Troca de 2 linhas: `deepSeek('deepseek-v4-flash')` → `deepInfra('deepseek-ai/DeepSeek-V4-Flash')`. O provider **lê `DEEPINFRA_API_KEY` do env sozinho** (source confirma `environmentVariableName: "DEEPINFRA_API_KEY"`). Seu default baseURL é `https://api.deepinfra.com/v1` + `/openai/...` = exatamente o endpoint OpenAI-compatível que a documentação da Deep Infra prescreve. - **B — `@ai-sdk/openai-compatible` cru** com `baseURL: 'https://api.deepinfra.com/v1/openai'` manual + `loadApiKey` próprio. Funciona, mas reimplementa à mão o que o pacote A já encapsula (nome do provider, env var, headers/user-agent, schema de erro). - **C — continuar com `@ai-sdk/deepseek` e só sobrescrever `baseURL`** p/ a Deep Infra. Hacky: provider com nome/contrato "DeepSeek oficial" apontando para outra API; model naming e tratamento de reasoning divergem.