SLOPSHOPPER

creditos

Mostra, a cada prompt, quanto ele consumiu: tokens por modelo, custo estimado, contexto e limites do plano.

newbandcommand
★ 1v0.1.0MITupdated 2026-10-08enzoschitini/better-ai/.claude/skills/creditos
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · creditos
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM Créditos deste prompt: $0.0000 (estimado) · 42s - Nenhuma chamada ao modelo registrada - Sessão até agora: $0.420 · Contexto: 49% (97.4k de… › /creditos ⎿ creditos: Consumo da sessão: $0.420 (estimado, a preço de API) ⎿ creditos: ⎿ creditos: Tokens desde que o mod carregou (1 prompt): ⎿ creditos: ⎿ creditos: - Nenhuma chamada ao modelo registrada ainda ⎿ creditos: - Contexto: 49% (97.4k de 200.0k) Último prompt: $0.0000 · 0 chamadas · 0 tokens (0 do cache) · saída 0 [ Ocultar ] Sessão: $0.420 · contexto 49% · 5h 31% ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
Último prompt: $0.0000 · 0 chamadas · 0 tokens (0 do cache) · saída 0 [ Ocultar ] Sessão: $0.420 · contexto 49% · 5h 31%
README

BetterAI — Where Intelligence Finds Purpose

![BetterAI](<images/Frame 27346.png>)

Documentation

Múltiplos Modelos de IA, Um Único Back-end Unificado

A BetterAI é um back-end de IA modular, escrito em Python, que reúne atrás de uma única interface os provedores de modelos que normalmente vivem espalhados em SDKs incompatíveis entre si. O ModelGateway — construído sobre o framework Agno — cria modelos e agentes da OpenAI, Anthropic, Google Gemini e Groq a partir da mesma chamada, validando os parâmetros por introspecção do construtor de cada provedor para evitar erros silenciosos. Trocar de modelo passa a ser uma decisão de configuração, não uma refatoração.

Sobre essa base roda a Web Service Network, uma API FastAPI que orquestra os serviços de IA em produção. Os roteadores são descobertos dinamicamente, de modo que novos serviços entram sem alterar o bootstrap da aplicação, e toda resposta segue o mesmo envelope — job_id, status, result e um bloco de tempo com a duração da execução. O acesso é autenticado por X-API-Key.

O que a plataforma faz hoje

  • Parsing de documentos — arquivos txt, md, pdf e docx são convertidos em dados estruturados conforme um schema definido por quem chama. Os modelos Pydantic são gerados em tempo de execução a partir de metadados JSON, então cada extração declara o seu próprio contrato de saída.
  • Deep research — busca web profunda via Tavily, filtrada por score e devolvida como contexto em markdown, pronta para alimentar pipelines de RAG.
  • Base vetorial — ingestão, busca semântica e exclusão no Pinecone, com separação entre namespace local e global, escrita em lote e rollback automático quando parte de um lote falha.
  • Embeddings locais — chunking e recuperação em FAISS na memória, alternando entre OpenAI, HuggingFace ou um provedor fake para testes offline.
  • Geração de imagens — o serviço Da-Vinci gera e edita imagens a partir de texto e de referências visuais.

A infraestrutura por baixo

Cada operação passa por uma camada própria de tracing, com logs estruturados correlacionados por log_id e persistidos em MongoDB ou Supabase. O custo é calculado token a token e convertido de dólar para real pela cotação oficial do Banco Central, com fallback quando não há cotação no dia. Consumo de IA, aqui, é um número auditável e não uma estimativa.

Duas aplicações que rodam sobre isso

O Content Generator gera conteúdo alinhado à identidade de uma marca a partir de catálogos de produtos indexados no Pinecone, e cita as fontes recuperadas em cada texto — o que torna cada afirmação verificável em vez de plausível. O Acquarello faz geração de imagens em dois modos, texto-para-imagem e imagem-para-imagem, com controle de estilo visual, projetado para quem não quer lidar com prompts complexos.

Autor: Enzo Schitini

Guia de Inicialização

![alt text](<images/Gemini_Generated_Image_12swry12swry12sw (1).png>)

1. Atualize o Projeto

Primeiro, certifique-se de estar na branch correta e de que o seu código está atualizado:

git checkout production
git pull
git checkout -b nome-da-sua-branch

2. Crie o Ambiente Virtual

Requisito: Python 3.14 ou superior (versão fixada em .python-version).

Crie um ambiente virtual para isolar as dependências do projeto:

python -m venv .venv

3. Ative o Ambiente Virtual

PowerShell
.\.venv\Scripts\Activate.ps1
CMD
.venv\Scripts\activate.bat

Após a ativação, o seu terminal deve ficar parecido com isto:

(.venv) PS C:\Users\nome_do_usuario\better-ai>

4. Instale as Dependências

Você pode instalar as dependências de duas formas:

Usando uv (recomendado)
uv sync

5. Configure as Variáveis de Ambiente

Crie um arquivo .env na raiz do projeto com as seguintes variáveis:

VariávelDescrição
BETTERAI_API_KEYChave de API interna da BetterAI
OPENAI_API_KEYChave de API da OpenAI
OPENAI_EMBEDDING_MODELModelo de embeddings da OpenAI utilizado
GEMINI_API_KEYChave de API do Google Gemini
GROQ_API_KEYChave de API da Groq
ANTHROPIC_API_KEYChave de API da Anthropic (Claude)
TAVILY_API_KEYChave de API do Tavily (busca web)
FAL_API_KEYChave de API do Fal (geração de mídia)
EXCHANGE_RATE_API_KEYChave de API de cotação de câmbio
HUGGINGFACEHUB_API_TOKENToken de acesso ao Hugging Face Hub
PINECONE_API_KEYChave de API do Pinecone (vector store)
PINECONE_ENVIRONMENTRegião/ambiente do Pinecone
PINECONE_INDEX_NAMENome do índice vetorial no Pinecone
PINECONE_NAMESPACENamespace da base de conhecimento
PINECONE_GLOBAL_NAMESPACENamespace global da base de conhecimento
MONGO_URIString de conexão completa do MongoDB
MONGO_USERUsuário do MongoDB
MONGO_PASSWORDSenha do MongoDB
MONGO_HOSTHost do MongoDB
MONGO_PORTPorta do MongoDB
NOSQL_BACKENDBackend NoSQL utilizado (local ou remoto)
SUPABASE_URLURL do projeto Supabase
SUPABASE_SECRET_KEYChave secreta de serviço do Supabase
SUPABASE_DATABASE_URLString de conexão do banco Postgres do Supabase
SUPABASE_PROJECT_NAMENome do projeto Supabase
SUPABASE_PROJECT_HOSTHost do projeto Supabase
SUPABASE_DATABASE_PASSWORDSenha do banco de dados Supabase
SHOW_INFO_LOGSExibe logs informativos (true/false)
SHOW_METADATAExibe metadados nas respostas (true/false)
FORMAT_METADATAFormata os metadados exibidos (true/false)
SAVE_LOGSSalva logs de execução (true/false)
SAVE_MONGOSalva dados de execução no MongoDB (true/false)
LOCALFlag para execução local (true/false)
# BetterAI
BETTERAI_API_KEY=********************

# LLM's
OPENAI_API_KEY=********************
OPENAI_EMBEDDING_MODEL=text-embedding-3-large

GEMINI_API_KEY=********************
GROQ_API_KEY=********************
ANTHROPIC_API_KEY=********************

# Tools
TAVILY_API_KEY=********************
FAL_API_KEY=********************
EXCHANGE_RATE_API_KEY=********************
HUGGINGFACEHUB_API_TOKEN=********************

# Database

# Pinecone
PINECONE_API_KEY=********************
PINECONE_ENVIRONMENT=us-east-1
PINECONE_INDEX_NAME=backai-vectorstore
PINECONE_NAMESPACE=knowledge_base
PINECONE_GLOBAL_NAMESPACE=global_knowledge_base

# MongoDB
MONGO_URI=********************
MONGO_USER=********************
MONGO_PASSWORD=********************
MONGO_HOST=********************
MONGO_PORT=********************

NOSQL_BACKEND=local

# Supabase
SUPABASE_URL=********************
SUPABASE_SECRET_KEY=********************
SUPABASE_DATABASE_URL=********************
SUPABASE_PROJECT_NAME=better-ai-bucket-storage
SUPABASE_PROJECT_HOST=********************
SUPABASE_DATABASE_PASSWORD=********************

# Application Tracing Rules
SHOW_INFO_LOGS=false
SHOW_METADATA=false
FORMAT_METADATA=false
SAVE_LOGS=false
SAVE_MONGO=false

LOCAL=false

6. Execute a aplicação

O projeto expõe dois serviços independentes, que podem ser executados separadamente ou em paralelo (em terminais distintos):

ServiçoComandoPortaQuando usar
API (FastAPI)uvicorn web_services:app --reload8000Consumo programático dos módulos de IA via HTTP
Interface (Streamlit)streamlit run web_app.py8501Uso visual das aplicações (Acquarello, Content Generator)

Pré-requisitos: ambiente virtual ativado (passo 3), dependências instaladas (passo 4) e arquivo .env configurado (passo 5).


6.1. API (FastAPI + Uvicorn)
uvicorn web_services:app --reload

A flag --reload reinicia o servidor automaticamente a cada alteração no código — recomendada apenas em desenvolvimento.

INFO:     Will watch for changes in these directories: ['C:\\Users\\user_name\\better-ai']
INFO:     Uvicorn running on http://localhost:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [24960] using StatReload
INFO:     Router included: /agents
INFO:     Router included: /davinci
INFO:     Router included: /deep-research
INFO:     Router included: /parse-content
INFO:     Router included: /vector-store
INFO:     Started server process [4748]
INFO:     Waiting for application startup.
INFO:     

    ╔═══════════════════════════════════════════════════════════════════════╗

        ██████╗ ███████╗████████╗████████╗███████╗██████╗      █████╗ ██╗ ✦
        ██╔══██╗██╔════╝╚══██╔══╝╚══██╔══╝██╔════╝██╔══██╗    ██╔══██╗██║
        ██████╔╝█████╗     ██║      ██║   █████╗  ██████╔╝    ███████║██║
        ██╔══██╗██╔══╝     ██║      ██║   ██╔══╝  ██╔══██╗    ██╔══██║██║
        ██████╔╝███████╗   ██║      ██║   ███████╗██║  ██║    ██║  ██║██║
        ╚═════╝ ╚══════╝   ╚═╝      ╚═╝   ╚══════╝╚═╝  ╚═╝    ╚═╝  ╚═╝╚═╝

    ╚═══════════════════════════════════════════════════════════════════════╝

                        ✦ Where intelligence finds purpose. ✦
    
INFO:     BetterAI Web Service Network initialized successfully at 2026-08-17 08:55:20.
INFO:     Version: 1.0.0
INFO:     API_DOMAIN: http://localhost:8000
INFO:     Health check available at: http://localhost:8000/health
INFO:     Documentation available at: http://localhost:8000/docs
INFO:     Application startup complete.

Endereços disponíveis

RecursoURL
Base URLhttp://localhost:8000
Health checkhttp://localhost:8000/health
Documentação interativa (Swagger UI)http://localhost:8000/docs

Valide a inicialização

Confirme que a API respondeu corretamente antes de seguir:

curl -X GET "http://localhost:8000/health"

Endpoints principais

EndpointDescrição
GET /healthVerificação de disponibilidade do serviço
GET /health-authorizationVerificação de disponibilidade com autenticação
POST /deep-research/context-builderConstrução de contexto a partir de pesquisa profunda
POST /parse-content/document-parseExtração e estruturação de conteúdo de documentos
POST /davinci/image-generationGeração de imagens

Cada resposta segue um formato padronizado com job_id, status, result e métricas de tempo de execução. Os parâmetros e exemplos completos de request/response estão em [Web Service Network - API.ipynb](<doc/Modules/Web Service Network/Web Service Network - API.ipynb>).


6.2. Interface Streamlit
streamlit run web_app.py

A aplicação ficará disponível em http://localhost:8501, com as seguintes páginas:

AplicaçãoURLDocumentação
Acquarellohttp://localhost:8501/acquarello[Acquarello.ipynb](<doc/Streamlit Applications/Acquarello.ipynb>)
Content Generatorhttp://localhost:8501/content_generator[Content Generator.ipynb](<doc/Streamlit Applications/Content Generator.ipynb>)

6.3. Resolução de problemas
SintomaCausa provávelO que fazer
ModuleNotFoundError ao iniciarAmbiente virtual não ativado ou dependências ausentesReative o .venv (passo 3) e rode uv sync (passo 4)
Erro de autenticação nos provedores de IAChaves ausentes ou inválidas no .envRevise o passo 5 e confirme os valores das chaves
Falha de conexão com MongoDB/Supabase/PineconeCredenciais ou host incorretos no .envVerifique as variáveis de banco e a conectividade de rede
Porta já em usoOutra instância do serviço está ativaEncerre o processo anterior ou use outra porta (--port)

Se o ambiente virtual ficar inconsistente, recrie-o do zero:

deactivate
Remove-Item -Recurse -Force .\.venv
python -m venv .venv

alt text

BetterAI — Where Intelligence Finds Purpose.

Autor: Enzo Schitini

Source 3 files
hooks/register.tsx 134 lines
1import { atom, read, update } from 'claude-code'
2import type { Register } from 'claude-code'
3
4import type { Snapshot, Tally, TurnReport } from '../types'
5import { addUsage, describeSession, describeTurn, lastLine, mergeTallies, sessionLine, snapshot } from './format'
6
7const last = atom({ plugin: 'creditos', key: 'last' } as const, null)
8const live = atom({ plugin: 'creditos', key: 'live' } as const, null)
9const prompts = atom({ plugin: 'creditos', key: 'prompts' } as const, 0)
10const sessionMain = atom({ plugin: 'creditos', key: 'sessionMain' } as const, [])
11const sessionSubagents = atom({ plugin: 'creditos', key: 'sessionSubagents' } as const, [])
12const isHidden = atom({ plugin: 'creditos', key: 'isHidden' } as const, false)
13
14export const register: Register = on => {
15  // The prompt in flight: reset at each main-loop turn.start, reported at its turn.complete.
16  let main: Tally[] = []
17  let subagents: Tally[] = []
18  let before: Snapshot | null = null
19
20  on('session.start', async ($, e, next) => {
21    await $.command.register({
22      name: 'creditos',
23      description: 'Mostra o consumo de créditos da sessão, por modelo, e reexibe a faixa',
24    })
25    const now = snapshot(await $.session.usage())
26    await update($, live, () => now)
27
28    return next(e)
29  })
30
31  on('turn.start', async ($, e, next) => {
32    main = []
33    subagents = []
34    before = snapshot(await $.session.usage())
35
36    return next(e)
37  })
38
39  // Every model request, main loop and subagents alike, with what it cost.
40  on('turn.step', async function* ($, e, next) {
41    const result = yield* next(e)
42    if (result.usage !== null) {
43      if (e.agentId === undefined) {
44        main = addUsage(main, result.usage.model, result.usage)
45      } else {
46        subagents = addUsage(subagents, result.usage.model, result.usage)
47      }
48    }
49
50    return result
51  })
52
53  on('turn.complete', async ($, e, next) => {
54    const result = await next(e)
55    if (e.agentId !== undefined) {
56      return result
57    }
58
59    const now = snapshot(await $.session.usage())
60    const turn: TurnReport = {
61      durationMs: e.durationMs,
62      costUsd: now.usd !== null && before?.usd != null ? Math.max(0, now.usd - before.usd) : null,
63      main,
64      subagents,
65    }
66    const text = describeTurn(turn, now, before, await $.clock.now())
67
68    await update($, prompts, n => n + 1)
69    await update($, sessionMain, list => mergeTallies(list, turn.main))
70    await update($, sessionSubagents, list => mergeTallies(list, turn.subagents))
71    await update($, last, () => turn)
72    await update($, live, () => now)
73    main = []
74    subagents = []
75    before = null
76
77    return { ...result, text }
78  })
79
80  on('session.measure', async ($, e, next) => {
81    const now = snapshot(e)
82    await update($, live, () => now)
83
84    return next(e)
85  })
86
87  on('session.end', async ($, e, next) => {
88    if (e.reason === 'clear') {
89      await update($, prompts, () => 0)
90      await update($, sessionMain, () => [])
91      await update($, sessionSubagents, () => [])
92      await update($, last, () => null)
93    }
94
95    return next(e)
96  })
97
98  on('command.run', { command: 'creditos' }, async $ => {
99    await update($, isHidden, () => false)
100    const text = describeSession(
101      await read($, live),
102      await read($, prompts),
103      await read($, sessionMain),
104      await read($, sessionSubagents),
105      await $.clock.now(),
106    )
107
108    return { text }
109  })
110
111  on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
112    if (e.props.hasSurvey || (await read($, isHidden))) {
113      return next(e)
114    }
115    const turn = await read($, last)
116    const snap = await read($, live)
117    if (turn === null && snap === null) {
118      return next(e)
119    }
120
121    const { Box, Button, Text } = $.ui.resolve(e)
122
123    return (
124      <Box flexDirection="column">
125        <Box>
126          <Text dimColor>{turn === null ? 'Créditos: aguardando o primeiro prompt ' : `${lastLine(turn)} `}</Text>
127          <Button key="ocultar" label="Ocultar" onPress={() => update($, isHidden, () => true)} />
128        </Box>
129        {snap !== null && <Text dimColor>{sessionLine(snap)}</Text>}
130      </Box>
131    )
132  })
133}
134
hooks/format.ts 189 lines
1import type { ModelUsage, SessionUsage } from 'claude-code'
2
3import type { Limit, Snapshot, Tally, TurnReport } from '../types'
4
5const LIMIT_NAMES: Record<string, string> = {
6  five_hour: 'Limite 5h',
7  seven_day: 'Limite semanal',
8  seven_day_opus: 'Limite semanal (Opus)',
9  seven_day_sonnet: 'Limite semanal (Sonnet)',
10  spend_limit: 'Limite de gasto',
11}
12
13const SHORT_LIMIT_NAMES: Record<string, string> = {
14  five_hour: '5h',
15  seven_day: 'semanal',
16  seven_day_opus: 'semanal Opus',
17  seven_day_sonnet: 'semanal Sonnet',
18  spend_limit: 'gasto',
19}
20
21export const tokens = (n: number): string =>
22  n >= 1_000_000
23    ? `${(n / 1_000_000).toFixed(2)}M`
24    : n >= 1_000
25      ? `${(n / 1_000).toFixed(1)}k`
26      : String(n)
27
28export const dollars = (n: number): string =>
29  n >= 1 ? `$${n.toFixed(2)}` : n >= 0.01 ? `$${n.toFixed(3)}` : `$${n.toFixed(4)}`
30
31export const duration = (ms: number): string => {
32  const s = Math.round(ms / 1000)
33  if (s < 60) return `${s}s`
34  const m = Math.floor(s / 60)
35  return m < 60 ? `${m}min ${s % 60}s` : `${Math.floor(m / 60)}h ${m % 60}min`
36}
37
38const until = (iso: string, now: number): string | null => {
39  const ms = Date.parse(iso) - now
40  if (!Number.isFinite(ms) || ms <= 0) return null
41  const min = Math.ceil(ms / 60_000)
42  if (min < 60) return `${min}min`
43  const h = Math.floor(min / 60)
44  return h < 24 ? `${h}h ${min % 60}min` : `${Math.floor(h / 24)}d ${h % 24}h`
45}
46
47const percent = (n: number): string => `${Math.round(n * 10) / 10}%`
48
49const shortModel = (model: string): string => model.replace(/^claude-/, '')
50
51const plural = (n: number, one: string, many: string): string => `${n} ${n === 1 ? one : many}`
52
53export const emptyTally = (model: string): Tally => ({
54  model,
55  requests: 0,
56  input: 0,
57  output: 0,
58  cacheRead: 0,
59  cacheWrite: 0,
60})
61
62/** Adds one response's (or a summed tally's) usage to the model's row. */
63export const addUsage = (list: readonly Tally[], model: string, usage: ModelUsage, requests = 1): Tally[] => {
64  const base = list.find(t => t.model === model) ?? emptyTally(model)
65  const sum: Tally = {
66    model,
67    requests: base.requests + requests,
68    input: base.input + usage.input_tokens,
69    output: base.output + usage.output_tokens,
70    cacheRead: base.cacheRead + usage.cache_read_input_tokens,
71    cacheWrite: base.cacheWrite + usage.cache_creation_input_tokens,
72  }
73  return list.some(t => t.model === model) ? list.map(t => (t.model === model ? sum : t)) : [...list, sum]
74}
75
76export const mergeTallies = (into: readonly Tally[], from: readonly Tally[]): Tally[] =>
77  from.reduce<Tally[]>(
78    (acc, t) =>
79      addUsage(
80        acc,
81        t.model,
82        {
83          input_tokens: t.input,
84          output_tokens: t.output,
85          cache_read_input_tokens: t.cacheRead,
86          cache_creation_input_tokens: t.cacheWrite,
87        },
88        t.requests,
89      ),
90    [...into],
91  )
92
93export const snapshot = (u: Pick<SessionUsage, 'context' | 'rateLimits' | 'cost'>): Snapshot => ({
94  usd: u.cost?.usd ?? null,
95  contextPercent: u.context.percent ?? null,
96  contextTokens: u.context.tokens ?? null,
97  contextWindow: u.context.window,
98  limits: u.rateLimits.map(l =>
99    l.resetsAt === undefined
100      ? { kind: l.kind, percentUsed: l.percentUsed }
101      : { kind: l.kind, percentUsed: l.percentUsed, resetsAt: l.resetsAt },
102  ),
103})
104
105const tallyTokens = (list: readonly Tally[]): number =>
106  list.reduce((sum, t) => sum + t.input + t.output + t.cacheRead + t.cacheWrite, 0)
107
108const tallyRequests = (list: readonly Tally[]): number => list.reduce((sum, t) => sum + t.requests, 0)
109
110const tallyLine = (t: Tally): string =>
111  `${shortModel(t.model)}: ${plural(t.requests, 'chamada', 'chamadas')} · entrada ${tokens(t.input)} · ` +
112  `cache lido ${tokens(t.cacheRead)} · cache gravado ${tokens(t.cacheWrite)} · saída ${tokens(t.output)}`
113
114const contextLine = (snap: Snapshot): string =>
115  snap.contextPercent === null
116    ? `Contexto: janela de ${tokens(snap.contextWindow)}`
117    : `Contexto: ${percent(snap.contextPercent)} (${tokens(snap.contextTokens ?? 0)} de ${tokens(snap.contextWindow)})`
118
119const limitLine = (limit: Limit, now: number, before: Limit | undefined): string => {
120  let head = `${LIMIT_NAMES[limit.kind] ?? limit.kind}: ${percent(limit.percentUsed)}`
121  if (before !== undefined) {
122    const delta = Math.round((limit.percentUsed - before.percentUsed) * 10) / 10
123    head += delta > 0 ? ` (+${delta} pts neste prompt)` : delta < 0 ? ' (a janela reiniciou)' : ' (sem mudança)'
124  }
125  const left = limit.resetsAt === undefined ? null : until(limit.resetsAt, now)
126  return left === null ? head : `${head} · reseta em ${left}`
127}
128
129const tallyBullets = (main: readonly Tally[], subagents: readonly Tally[]): string[] => [
130  ...main.map(t => `- Principal · ${tallyLine(t)}`),
131  ...subagents.map(t => `- Subagentes · ${tallyLine(t)}`),
132]
133
134/** The report shown beneath each answer. */
135export const describeTurn = (turn: TurnReport, now: Snapshot, before: Snapshot | null, clock: number): string => {
136  const cost = turn.costUsd === null ? 'custo indisponível' : `${dollars(turn.costUsd)} (estimado)`
137  const bullets = tallyBullets(turn.main, turn.subagents)
138  const lines = [
139    `Créditos deste prompt: ${cost} · ${duration(turn.durationMs)}`,
140    '',
141    ...(bullets.length === 0 ? ['- Nenhuma chamada ao modelo registrada'] : bullets),
142    `- Sessão até agora: ${now.usd === null ? 'custo indisponível' : dollars(now.usd)} · ${contextLine(now)}`,
143    ...now.limits.map(l => `- ${limitLine(l, clock, before?.limits.find(b => b.kind === l.kind))}`),
144  ]
145  return lines.join('\n')
146}
147
148/** What /creditos answers: the session so far. */
149export const describeSession = (
150  snap: Snapshot | null,
151  prompts: number,
152  main: readonly Tally[],
153  subagents: readonly Tally[],
154  clock: number,
155): string => {
156  const cost = snap?.usd == null ? 'custo indisponível' : `${dollars(snap.usd)} (estimado, a preço de API)`
157  const bullets = tallyBullets(main, subagents)
158  const lines = [
159    `Consumo da sessão: ${cost}`,
160    '',
161    `Tokens desde que o mod carregou (${plural(prompts, 'prompt', 'prompts')}):`,
162    '',
163    ...(bullets.length === 0 ? ['- Nenhuma chamada ao modelo registrada ainda'] : bullets),
164    ...(snap === null ? [] : [`- ${contextLine(snap)}`, ...snap.limits.map(l => `- ${limitLine(l, clock, undefined)}`)]),
165  ]
166  return lines.join('\n')
167}
168
169/** The band's first line: the last prompt in one glance. */
170export const lastLine = (turn: TurnReport): string => {
171  const all = [...turn.main, ...turn.subagents]
172  const cache = all.reduce((sum, t) => sum + t.cacheRead, 0)
173  const cost = turn.costUsd === null ? '' : `${dollars(turn.costUsd)} · `
174  return (
175    `Último prompt: ${cost}${plural(tallyRequests(all), 'chamada', 'chamadas')} · ` +
176    `${tokens(tallyTokens(all))} tokens (${tokens(cache)} do cache) · saída ${tokens(all.reduce((s, t) => s + t.output, 0))}`
177  )
178}
179
180/** The band's second line: the session and the plan's limits. */
181export const sessionLine = (snap: Snapshot): string =>
182  [
183    `Sessão: ${snap.usd === null ? 'custo indisponível' : dollars(snap.usd)}`,
184    snap.contextPercent === null ? null : `contexto ${percent(snap.contextPercent)}`,
185    ...snap.limits.map(l => `${SHORT_LIMIT_NAMES[l.kind] ?? l.kind} ${percent(l.percentUsed)}`),
186  ]
187    .filter(part => part !== null)
188    .join(' · ')
189
types/index.d.ts 47 lines
1/** Tokens and requests one model spent, summed. */
2export type Tally = {
3  model: string
4  requests: number
5  input: number
6  output: number
7  cacheRead: number
8  cacheWrite: number
9}
10
11/** One rate-limit window as the last API response reported it. */
12export type Limit = {
13  kind: string
14  percentUsed: number
15  resetsAt?: string
16}
17
18/** The session's figures at one moment: cost, context fill, limits. */
19export type Snapshot = {
20  usd: number | null
21  contextPercent: number | null
22  contextTokens: number | null
23  contextWindow: number
24  limits: Limit[]
25}
26
27/** What one prompt (one main-loop turn) consumed. */
28export type TurnReport = {
29  durationMs: number
30  costUsd: number | null
31  main: Tally[]
32  subagents: Tally[]
33}
34
35declare module 'claude-code' {
36  interface PluginState {
37    creditos: {
38      last: TurnReport | null
39      live: Snapshot | null
40      prompts: number
41      sessionMain: Tally[]
42      sessionSubagents: Tally[]
43      isHidden: boolean
44    }
45  }
46}
47