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


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.
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.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.
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
.png>)
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
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
.\.venv\Scripts\Activate.ps1
.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>
Você pode instalar as dependências de duas formas:
uv sync
Crie um arquivo .env na raiz do projeto com as seguintes variáveis:
| Variável | Descrição |
|---|---|
BETTERAI_API_KEY | Chave de API interna da BetterAI |
OPENAI_API_KEY | Chave de API da OpenAI |
OPENAI_EMBEDDING_MODEL | Modelo de embeddings da OpenAI utilizado |
GEMINI_API_KEY | Chave de API do Google Gemini |
GROQ_API_KEY | Chave de API da Groq |
ANTHROPIC_API_KEY | Chave de API da Anthropic (Claude) |
TAVILY_API_KEY | Chave de API do Tavily (busca web) |
FAL_API_KEY | Chave de API do Fal (geração de mídia) |
EXCHANGE_RATE_API_KEY | Chave de API de cotação de câmbio |
HUGGINGFACEHUB_API_TOKEN | Token de acesso ao Hugging Face Hub |
PINECONE_API_KEY | Chave de API do Pinecone (vector store) |
PINECONE_ENVIRONMENT | Região/ambiente do Pinecone |
PINECONE_INDEX_NAME | Nome do índice vetorial no Pinecone |
PINECONE_NAMESPACE | Namespace da base de conhecimento |
PINECONE_GLOBAL_NAMESPACE | Namespace global da base de conhecimento |
MONGO_URI | String de conexão completa do MongoDB |
MONGO_USER | Usuário do MongoDB |
MONGO_PASSWORD | Senha do MongoDB |
MONGO_HOST | Host do MongoDB |
MONGO_PORT | Porta do MongoDB |
NOSQL_BACKEND | Backend NoSQL utilizado (local ou remoto) |
SUPABASE_URL | URL do projeto Supabase |
SUPABASE_SECRET_KEY | Chave secreta de serviço do Supabase |
SUPABASE_DATABASE_URL | String de conexão do banco Postgres do Supabase |
SUPABASE_PROJECT_NAME | Nome do projeto Supabase |
SUPABASE_PROJECT_HOST | Host do projeto Supabase |
SUPABASE_DATABASE_PASSWORD | Senha do banco de dados Supabase |
SHOW_INFO_LOGS | Exibe logs informativos (true/false) |
SHOW_METADATA | Exibe metadados nas respostas (true/false) |
FORMAT_METADATA | Formata os metadados exibidos (true/false) |
SAVE_LOGS | Salva logs de execução (true/false) |
SAVE_MONGO | Salva dados de execução no MongoDB (true/false) |
LOCAL | Flag 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
O projeto expõe dois serviços independentes, que podem ser executados separadamente ou em paralelo (em terminais distintos):
| Serviço | Comando | Porta | Quando usar |
|---|---|---|---|
| API (FastAPI) | uvicorn web_services:app --reload | 8000 | Consumo programático dos módulos de IA via HTTP |
| Interface (Streamlit) | streamlit run web_app.py | 8501 | Uso visual das aplicações (Acquarello, Content Generator) |
Pré-requisitos: ambiente virtual ativado (passo 3), dependências instaladas (passo 4) e arquivo
.envconfigurado (passo 5).
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
| Recurso | URL |
|---|---|
| Base URL | http://localhost:8000 |
| Health check | http://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
| Endpoint | Descrição |
|---|---|
GET /health | Verificação de disponibilidade do serviço |
GET /health-authorization | Verificação de disponibilidade com autenticação |
POST /deep-research/context-builder | Construção de contexto a partir de pesquisa profunda |
POST /parse-content/document-parse | Extração e estruturação de conteúdo de documentos |
POST /davinci/image-generation | Geraçã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>).
streamlit run web_app.py
A aplicação ficará disponível em http://localhost:8501, com as seguintes páginas:
| Aplicação | URL | Documentação |
|---|---|---|
| Acquarello | http://localhost:8501/acquarello | [Acquarello.ipynb](<doc/Streamlit Applications/Acquarello.ipynb>) |
| Content Generator | http://localhost:8501/content_generator | [Content Generator.ipynb](<doc/Streamlit Applications/Content Generator.ipynb>) |
| Sintoma | Causa provável | O que fazer |
|---|---|---|
ModuleNotFoundError ao iniciar | Ambiente virtual não ativado ou dependências ausentes | Reative o .venv (passo 3) e rode uv sync (passo 4) |
| Erro de autenticação nos provedores de IA | Chaves ausentes ou inválidas no .env | Revise o passo 5 e confirme os valores das chaves |
| Falha de conexão com MongoDB/Supabase/Pinecone | Credenciais ou host incorretos no .env | Verifique as variáveis de banco e a conectividade de rede |
| Porta já em uso | Outra instância do serviço está ativa | Encerre 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

Autor: Enzo Schitini
hooks/register.tsx 134 lines1import { 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}
134hooks/format.ts 189 lines1import 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(' · ')
189types/index.d.ts 47 lines1/** 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