ia-tracker-qca 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ia_cost_tracker/__init__.py +27 -0
- ia_cost_tracker/database.py +290 -0
- ia_cost_tracker/exceptions.py +26 -0
- ia_cost_tracker/models.py +72 -0
- ia_cost_tracker/providers/__init__.py +11 -0
- ia_cost_tracker/providers/anthropic.py +81 -0
- ia_cost_tracker/providers/base.py +55 -0
- ia_cost_tracker/providers/maritaca.py +142 -0
- ia_cost_tracker/tracker.py +357 -0
- ia_tracker_qca-0.1.0.dist-info/METADATA +140 -0
- ia_tracker_qca-0.1.0.dist-info/RECORD +14 -0
- ia_tracker_qca-0.1.0.dist-info/WHEEL +5 -0
- ia_tracker_qca-0.1.0.dist-info/licenses/LICENCE +21 -0
- ia_tracker_qca-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
IA Cost Tracker - Biblioteca para rastreamento de custos de APIs de IA.
|
|
3
|
+
|
|
4
|
+
Esta biblioteca permite rastrear e auditar custos de chamadas a APIs de IA
|
|
5
|
+
como Maritaca e Anthropic, salvando informações detalhadas em banco de dados.
|
|
6
|
+
|
|
7
|
+
Para Maritaca: sincroniza preços via API
|
|
8
|
+
Para Anthropic: usa preços mantidos no banco de dados
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
|
|
13
|
+
from .tracker import IATracker
|
|
14
|
+
from .exceptions import (
|
|
15
|
+
IATrackerError,
|
|
16
|
+
DatabaseError,
|
|
17
|
+
ProviderError,
|
|
18
|
+
ModelNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"IATracker",
|
|
23
|
+
"IATrackerError",
|
|
24
|
+
"DatabaseError",
|
|
25
|
+
"ProviderError",
|
|
26
|
+
"ModelNotFoundError",
|
|
27
|
+
]
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Gerenciador de conexões e operações com banco de dados."""
|
|
2
|
+
|
|
3
|
+
import psycopg2
|
|
4
|
+
from psycopg2.extras import RealDictCursor
|
|
5
|
+
from typing import List, Optional, Dict, Any
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from decimal import Decimal
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .models import ModeloIA, UsoToken
|
|
11
|
+
from .exceptions import DatabaseError, ModelNotFoundError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DatabaseManager:
|
|
17
|
+
"""
|
|
18
|
+
Gerenciador de conexões e operações com PostgreSQL.
|
|
19
|
+
|
|
20
|
+
Esta classe gerencia todas as interações com o banco de dados,
|
|
21
|
+
incluindo operações CRUD para modelos e registros de uso.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, connection_string: str):
|
|
25
|
+
"""
|
|
26
|
+
Inicializa o gerenciador de banco de dados.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
connection_string: String de conexão PostgreSQL
|
|
30
|
+
(ex: "postgresql://user:pass@localhost/dbname")
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
DatabaseError: Se houver erro na conexão com o banco
|
|
34
|
+
"""
|
|
35
|
+
self.connection_string = connection_string
|
|
36
|
+
self._test_connection()
|
|
37
|
+
|
|
38
|
+
def _test_connection(self) -> None:
|
|
39
|
+
"""Testa a conexão com o banco de dados."""
|
|
40
|
+
try:
|
|
41
|
+
with self._get_connection() as conn:
|
|
42
|
+
with conn.cursor() as cur:
|
|
43
|
+
cur.execute("SELECT 1")
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise DatabaseError(f"Erro ao conectar ao banco de dados: {str(e)}")
|
|
46
|
+
|
|
47
|
+
@contextmanager
|
|
48
|
+
def _get_connection(self):
|
|
49
|
+
"""
|
|
50
|
+
Context manager para conexões com o banco.
|
|
51
|
+
|
|
52
|
+
Yields:
|
|
53
|
+
psycopg2.connection: Conexão com o banco de dados
|
|
54
|
+
"""
|
|
55
|
+
conn = None
|
|
56
|
+
try:
|
|
57
|
+
conn = psycopg2.connect(self.connection_string)
|
|
58
|
+
yield conn
|
|
59
|
+
conn.commit()
|
|
60
|
+
except Exception as e:
|
|
61
|
+
if conn:
|
|
62
|
+
conn.rollback()
|
|
63
|
+
raise DatabaseError(f"Erro na operação do banco: {str(e)}")
|
|
64
|
+
finally:
|
|
65
|
+
if conn:
|
|
66
|
+
conn.close()
|
|
67
|
+
|
|
68
|
+
@staticmethod
|
|
69
|
+
def _convert_decimal_to_float(row_dict: Dict) -> Dict:
|
|
70
|
+
"""
|
|
71
|
+
Converte valores Decimal para float no dicionário.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
row_dict: Dicionário com dados do banco
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Dicionário com Decimals convertidos para float
|
|
78
|
+
"""
|
|
79
|
+
converted = {}
|
|
80
|
+
for key, value in row_dict.items():
|
|
81
|
+
if isinstance(value, Decimal):
|
|
82
|
+
converted[key] = float(value)
|
|
83
|
+
else:
|
|
84
|
+
converted[key] = value
|
|
85
|
+
return converted
|
|
86
|
+
|
|
87
|
+
def buscar_modelo(self, provedor: str, nome_modelo: str) -> Optional[ModeloIA]:
|
|
88
|
+
"""
|
|
89
|
+
Busca um modelo específico no banco de dados.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
provedor: Nome do provedor (ex: 'maritaca', 'anthropic')
|
|
93
|
+
nome_modelo: Nome do modelo
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
ModeloIA se encontrado, None caso contrário
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
DatabaseError: Se houver erro na consulta
|
|
100
|
+
"""
|
|
101
|
+
query = """
|
|
102
|
+
SELECT id_modelo, provedor, nome_modelo, custo_input_por_1k,
|
|
103
|
+
custo_output_por_1k, ativo, criado_em
|
|
104
|
+
FROM dim_modelo_ia
|
|
105
|
+
WHERE provedor = %s AND nome_modelo = %s AND ativo = true
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
with self._get_connection() as conn:
|
|
109
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
110
|
+
cur.execute(query, (provedor, nome_modelo))
|
|
111
|
+
row = cur.fetchone()
|
|
112
|
+
|
|
113
|
+
if row:
|
|
114
|
+
row_dict = self._convert_decimal_to_float(dict(row))
|
|
115
|
+
return ModeloIA(**row_dict)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def listar_modelos(self, provedor: Optional[str] = None,
|
|
119
|
+
apenas_ativos: bool = True) -> List[ModeloIA]:
|
|
120
|
+
"""
|
|
121
|
+
Lista modelos do banco de dados.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
provedor: Filtrar por provedor específico (opcional)
|
|
125
|
+
apenas_ativos: Se True, retorna apenas modelos ativos
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Lista de ModeloIA
|
|
129
|
+
|
|
130
|
+
Raises:
|
|
131
|
+
DatabaseError: Se houver erro na consulta
|
|
132
|
+
"""
|
|
133
|
+
query = """
|
|
134
|
+
SELECT id_modelo, provedor, nome_modelo, custo_input_por_1k,
|
|
135
|
+
custo_output_por_1k, ativo, criado_em
|
|
136
|
+
FROM dim_modelo_ia
|
|
137
|
+
WHERE 1=1
|
|
138
|
+
"""
|
|
139
|
+
params = []
|
|
140
|
+
|
|
141
|
+
if provedor:
|
|
142
|
+
query += " AND provedor = %s"
|
|
143
|
+
params.append(provedor)
|
|
144
|
+
|
|
145
|
+
if apenas_ativos:
|
|
146
|
+
query += " AND ativo = true"
|
|
147
|
+
|
|
148
|
+
query += " ORDER BY provedor, nome_modelo"
|
|
149
|
+
|
|
150
|
+
with self._get_connection() as conn:
|
|
151
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
152
|
+
cur.execute(query, params)
|
|
153
|
+
rows = cur.fetchall()
|
|
154
|
+
modelos = []
|
|
155
|
+
for row in rows:
|
|
156
|
+
row_dict = self._convert_decimal_to_float(dict(row))
|
|
157
|
+
modelos.append(ModeloIA(**row_dict))
|
|
158
|
+
return modelos
|
|
159
|
+
|
|
160
|
+
def inserir_ou_atualizar_modelo(self, modelo: ModeloIA) -> int:
|
|
161
|
+
"""
|
|
162
|
+
Insere um novo modelo ou atualiza se já existir.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
modelo: Objeto ModeloIA a ser inserido/atualizado
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
ID do modelo inserido/atualizado
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
DatabaseError: Se houver erro na operação
|
|
172
|
+
"""
|
|
173
|
+
query = """
|
|
174
|
+
INSERT INTO dim_modelo_ia
|
|
175
|
+
(provedor, nome_modelo, custo_input_por_1k, custo_output_por_1k, ativo)
|
|
176
|
+
VALUES (%s, %s, %s, %s, %s)
|
|
177
|
+
ON CONFLICT (provedor, nome_modelo)
|
|
178
|
+
DO UPDATE SET
|
|
179
|
+
custo_input_por_1k = EXCLUDED.custo_input_por_1k,
|
|
180
|
+
custo_output_por_1k = EXCLUDED.custo_output_por_1k,
|
|
181
|
+
ativo = EXCLUDED.ativo
|
|
182
|
+
RETURNING id_modelo
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
with self._get_connection() as conn:
|
|
186
|
+
with conn.cursor() as cur:
|
|
187
|
+
cur.execute(query, (
|
|
188
|
+
modelo.provedor,
|
|
189
|
+
modelo.nome_modelo,
|
|
190
|
+
modelo.custo_input_por_1k,
|
|
191
|
+
modelo.custo_output_por_1k,
|
|
192
|
+
modelo.ativo
|
|
193
|
+
))
|
|
194
|
+
id_modelo = cur.fetchone()[0]
|
|
195
|
+
logger.info(f"Modelo {modelo.provedor}/{modelo.nome_modelo} "
|
|
196
|
+
f"inserido/atualizado com ID {id_modelo}")
|
|
197
|
+
return id_modelo
|
|
198
|
+
|
|
199
|
+
def registrar_uso(self, uso: UsoToken) -> int:
|
|
200
|
+
"""
|
|
201
|
+
Registra um uso de tokens no banco de dados.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
uso: Objeto UsoToken com os dados do uso
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
ID do registro de uso criado
|
|
208
|
+
|
|
209
|
+
Raises:
|
|
210
|
+
DatabaseError: Se houver erro na inserção
|
|
211
|
+
"""
|
|
212
|
+
query = """
|
|
213
|
+
INSERT INTO fato_uso_tokens (
|
|
214
|
+
id_modelo, tokens_input, tokens_output,
|
|
215
|
+
custo_input, custo_output, tag_funcionalidade,
|
|
216
|
+
aplicacao, usuario, tempo_resposta_ms,
|
|
217
|
+
erro, mensagem_erro, timestamp_uso
|
|
218
|
+
) VALUES (
|
|
219
|
+
%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s
|
|
220
|
+
) RETURNING id_uso
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
with self._get_connection() as conn:
|
|
224
|
+
with conn.cursor() as cur:
|
|
225
|
+
cur.execute(query, (
|
|
226
|
+
uso.id_modelo,
|
|
227
|
+
uso.tokens_input,
|
|
228
|
+
uso.tokens_output,
|
|
229
|
+
uso.custo_input,
|
|
230
|
+
uso.custo_output,
|
|
231
|
+
uso.tag_funcionalidade,
|
|
232
|
+
uso.aplicacao,
|
|
233
|
+
uso.usuario,
|
|
234
|
+
uso.tempo_resposta_ms,
|
|
235
|
+
uso.erro,
|
|
236
|
+
uso.mensagem_erro,
|
|
237
|
+
uso.timestamp_uso
|
|
238
|
+
))
|
|
239
|
+
id_uso = cur.fetchone()[0]
|
|
240
|
+
logger.info(f"Uso registrado com ID {id_uso} - "
|
|
241
|
+
f"Custo total: R$ {uso.custo_total:.6f}")
|
|
242
|
+
return id_uso
|
|
243
|
+
|
|
244
|
+
def buscar_custos_por_aplicacao(self, aplicacao: str,
|
|
245
|
+
data_inicio: Optional[Any] = None,
|
|
246
|
+
data_fim: Optional[Any] = None) -> Dict[str, Any]:
|
|
247
|
+
"""
|
|
248
|
+
Busca estatísticas de custo para uma aplicação específica.
|
|
249
|
+
|
|
250
|
+
Args:
|
|
251
|
+
aplicacao: Nome da aplicação
|
|
252
|
+
data_inicio: Data inicial do período (opcional)
|
|
253
|
+
data_fim: Data final do período (opcional)
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Dicionário com estatísticas de uso e custo
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
DatabaseError: Se houver erro na consulta
|
|
260
|
+
"""
|
|
261
|
+
query = """
|
|
262
|
+
SELECT
|
|
263
|
+
COUNT(*) as total_chamadas,
|
|
264
|
+
SUM(tokens_input) as total_tokens_input,
|
|
265
|
+
SUM(tokens_output) as total_tokens_output,
|
|
266
|
+
SUM(tokens_total) as total_tokens,
|
|
267
|
+
SUM(custo_total) as custo_total,
|
|
268
|
+
AVG(custo_total) as custo_medio,
|
|
269
|
+
AVG(tempo_resposta_ms) as tempo_medio_resposta_ms,
|
|
270
|
+
SUM(CASE WHEN erro = true THEN 1 ELSE 0 END) as total_erros
|
|
271
|
+
FROM fato_uso_tokens
|
|
272
|
+
WHERE aplicacao = %s
|
|
273
|
+
"""
|
|
274
|
+
params = [aplicacao]
|
|
275
|
+
|
|
276
|
+
if data_inicio:
|
|
277
|
+
query += " AND timestamp_uso >= %s"
|
|
278
|
+
params.append(data_inicio)
|
|
279
|
+
|
|
280
|
+
if data_fim:
|
|
281
|
+
query += " AND timestamp_uso <= %s"
|
|
282
|
+
params.append(data_fim)
|
|
283
|
+
|
|
284
|
+
with self._get_connection() as conn:
|
|
285
|
+
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
286
|
+
cur.execute(query, params)
|
|
287
|
+
resultado = cur.fetchone()
|
|
288
|
+
if resultado:
|
|
289
|
+
return self._convert_decimal_to_float(dict(resultado))
|
|
290
|
+
return {}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Exceções personalizadas para o IA Cost Tracker."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IATrackerError(Exception):
|
|
5
|
+
"""Exceção base para erros do IA Cost Tracker."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseError(IATrackerError):
|
|
10
|
+
"""Exceção para erros relacionados ao banco de dados."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProviderError(IATrackerError):
|
|
15
|
+
"""Exceção para erros relacionados aos provedores de IA."""
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ModelNotFoundError(IATrackerError):
|
|
20
|
+
"""Exceção quando um modelo não é encontrado no banco de dados."""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigurationError(IATrackerError):
|
|
25
|
+
"""Exceção para erros de configuração."""
|
|
26
|
+
pass
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Modelos de dados para o IA Cost Tracker."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ModeloIA:
|
|
10
|
+
"""
|
|
11
|
+
Representa um modelo de IA com seus custos.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
id_modelo: ID do modelo no banco de dados
|
|
15
|
+
provedor: Nome do provedor (maritaca, anthropic, etc)
|
|
16
|
+
nome_modelo: Nome do modelo
|
|
17
|
+
custo_input_por_1k: Custo por 1000 tokens de entrada
|
|
18
|
+
custo_output_por_1k: Custo por 1000 tokens de saída
|
|
19
|
+
ativo: Se o modelo está ativo
|
|
20
|
+
criado_em: Data de criação do registro
|
|
21
|
+
"""
|
|
22
|
+
provedor: str
|
|
23
|
+
nome_modelo: str
|
|
24
|
+
custo_input_por_1k: float
|
|
25
|
+
custo_output_por_1k: float
|
|
26
|
+
id_modelo: Optional[int] = None
|
|
27
|
+
ativo: bool = True
|
|
28
|
+
criado_em: Optional[datetime] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class UsoToken:
|
|
33
|
+
"""
|
|
34
|
+
Representa um registro de uso de tokens.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
id_modelo: ID do modelo utilizado
|
|
38
|
+
tokens_input: Quantidade de tokens de entrada
|
|
39
|
+
tokens_output: Quantidade de tokens de saída
|
|
40
|
+
custo_input: Custo total da entrada
|
|
41
|
+
custo_output: Custo total da saída
|
|
42
|
+
tag_funcionalidade: Tag para identificar a funcionalidade
|
|
43
|
+
aplicacao: Nome da aplicação que fez a chamada
|
|
44
|
+
usuario: Usuário que realizou a chamada
|
|
45
|
+
tempo_resposta_ms: Tempo de resposta em milissegundos
|
|
46
|
+
erro: Se houve erro na chamada
|
|
47
|
+
mensagem_erro: Mensagem de erro, se houver
|
|
48
|
+
id_uso: ID do registro (gerado pelo banco)
|
|
49
|
+
timestamp_uso: Timestamp do uso
|
|
50
|
+
tokens_total: Total de tokens (calculado)
|
|
51
|
+
custo_total: Custo total (calculado)
|
|
52
|
+
"""
|
|
53
|
+
id_modelo: int
|
|
54
|
+
tokens_input: int
|
|
55
|
+
tokens_output: int
|
|
56
|
+
custo_input: float
|
|
57
|
+
custo_output: float
|
|
58
|
+
tag_funcionalidade: Optional[str] = None
|
|
59
|
+
aplicacao: Optional[str] = None
|
|
60
|
+
usuario: Optional[str] = None
|
|
61
|
+
tempo_resposta_ms: Optional[int] = None
|
|
62
|
+
erro: bool = False
|
|
63
|
+
mensagem_erro: Optional[str] = None
|
|
64
|
+
id_uso: Optional[int] = None
|
|
65
|
+
timestamp_uso: Optional[datetime] = field(default_factory=datetime.now)
|
|
66
|
+
tokens_total: Optional[int] = field(init=False)
|
|
67
|
+
custo_total: Optional[float] = field(init=False)
|
|
68
|
+
|
|
69
|
+
def __post_init__(self):
|
|
70
|
+
"""Calcula campos derivados."""
|
|
71
|
+
self.tokens_total = self.tokens_input + self.tokens_output
|
|
72
|
+
self.custo_total = self.custo_input + self.custo_output
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Provider para Anthropic (Claude)."""
|
|
2
|
+
|
|
3
|
+
from typing import List, Dict, Optional
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from .base import BaseProvider
|
|
7
|
+
from ..models import ModeloIA
|
|
8
|
+
from ..exceptions import ProviderError
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnthropicProvider(BaseProvider):
|
|
14
|
+
"""
|
|
15
|
+
Provider para integração com Anthropic (Claude).
|
|
16
|
+
|
|
17
|
+
Busca modelos e preços diretamente do banco de dados,
|
|
18
|
+
já que a Anthropic não possui API pública para consulta de preços.
|
|
19
|
+
|
|
20
|
+
Os preços devem ser mantidos atualizados manualmente no banco.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, db_manager=None):
|
|
24
|
+
"""
|
|
25
|
+
Inicializa o provider da Anthropic.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
db_manager: Instância do DatabaseManager para buscar preços do banco
|
|
29
|
+
"""
|
|
30
|
+
super().__init__("anthropic")
|
|
31
|
+
self.db_manager = db_manager
|
|
32
|
+
|
|
33
|
+
def buscar_modelos_disponiveis(self) -> List[ModeloIA]:
|
|
34
|
+
"""
|
|
35
|
+
Busca modelos disponíveis da Anthropic do banco de dados.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Lista de ModeloIA com modelos e preços
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ProviderError: Se o DatabaseManager não estiver configurado
|
|
42
|
+
"""
|
|
43
|
+
if not self.db_manager:
|
|
44
|
+
raise ProviderError(
|
|
45
|
+
"DatabaseManager não configurado para AnthropicProvider. "
|
|
46
|
+
"Os modelos da Anthropic são gerenciados diretamente no banco de dados."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
modelos = self.db_manager.listar_modelos(
|
|
50
|
+
provedor=self.provider_name,
|
|
51
|
+
apenas_ativos=True
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
logger.info(f"Buscados {len(modelos)} modelos da Anthropic do banco de dados")
|
|
55
|
+
return modelos
|
|
56
|
+
|
|
57
|
+
def calcular_custo(self, nome_modelo: str, tokens_input: int,
|
|
58
|
+
tokens_output: int, custo_input_1k: float,
|
|
59
|
+
custo_output_1k: float) -> Dict[str, float]:
|
|
60
|
+
"""
|
|
61
|
+
Calcula o custo de uma chamada à Anthropic.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
nome_modelo: Nome do modelo utilizado
|
|
65
|
+
tokens_input: Quantidade de tokens de entrada
|
|
66
|
+
tokens_output: Quantidade de tokens de saída
|
|
67
|
+
custo_input_1k: Custo por 1000 tokens de entrada
|
|
68
|
+
custo_output_1k: Custo por 1000 tokens de saída
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dicionário com custos calculados
|
|
72
|
+
"""
|
|
73
|
+
custo_input = (tokens_input / 1000) * custo_input_1k
|
|
74
|
+
custo_output = (tokens_output / 1000) * custo_output_1k
|
|
75
|
+
custo_total = custo_input + custo_output
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
'custo_input': round(custo_input, 6),
|
|
79
|
+
'custo_output': round(custo_output, 6),
|
|
80
|
+
'custo_total': round(custo_total, 6)
|
|
81
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Classe base para provedores de IA."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Dict, List
|
|
5
|
+
from ..models import ModeloIA
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BaseProvider(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Classe base abstrata para provedores de IA.
|
|
11
|
+
|
|
12
|
+
Cada provedor (Maritaca, Anthropic, etc) deve herdar desta classe
|
|
13
|
+
e implementar os métodos abstratos.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, provider_name: str):
|
|
17
|
+
"""
|
|
18
|
+
Inicializa o provedor base.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
provider_name: Nome do provedor
|
|
22
|
+
"""
|
|
23
|
+
self.provider_name = provider_name
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def buscar_modelos_disponiveis(self) -> List[ModeloIA]:
|
|
27
|
+
"""
|
|
28
|
+
Busca a lista de modelos disponíveis do provedor.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Lista de objetos ModeloIA com os modelos e preços
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
ProviderError: Se houver erro ao buscar os modelos
|
|
35
|
+
"""
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def calcular_custo(self, nome_modelo: str, tokens_input: int,
|
|
40
|
+
tokens_output: int, custo_input_1k: float,
|
|
41
|
+
custo_output_1k: float) -> Dict[str, float]:
|
|
42
|
+
"""
|
|
43
|
+
Calcula o custo de uma chamada.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
nome_modelo: Nome do modelo utilizado
|
|
47
|
+
tokens_input: Quantidade de tokens de entrada
|
|
48
|
+
tokens_output: Quantidade de tokens de saída
|
|
49
|
+
custo_input_1k: Custo por 1000 tokens de entrada
|
|
50
|
+
custo_output_1k: Custo por 1000 tokens de saída
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Dicionário com 'custo_input', 'custo_output' e 'custo_total'
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Provider para Maritaca AI."""
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
from .base import BaseProvider
|
|
8
|
+
from ..models import ModeloIA
|
|
9
|
+
from ..exceptions import ProviderError
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaritacaProvider(BaseProvider):
|
|
15
|
+
"""
|
|
16
|
+
Provider para integração com Maritaca AI.
|
|
17
|
+
|
|
18
|
+
Busca modelos e preços através da API da Maritaca.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, api_key: str, base_url: str = "https://chat.maritaca.ai"):
|
|
22
|
+
"""
|
|
23
|
+
Inicializa o provider da Maritaca.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
api_key: Chave de API da Maritaca
|
|
27
|
+
base_url: URL base da API (padrão: https://chat.maritaca.ai)
|
|
28
|
+
"""
|
|
29
|
+
super().__init__("maritaca")
|
|
30
|
+
self.api_key = api_key
|
|
31
|
+
self.base_url = base_url.rstrip('/')
|
|
32
|
+
|
|
33
|
+
def buscar_modelos_disponiveis(self) -> List[ModeloIA]:
|
|
34
|
+
"""
|
|
35
|
+
Busca modelos disponíveis da API da Maritaca.
|
|
36
|
+
|
|
37
|
+
A API retorna um dicionário com:
|
|
38
|
+
- models: dict com modelos ativos (chave = nome do modelo)
|
|
39
|
+
- deprecated_models: dict com modelos deprecados
|
|
40
|
+
|
|
41
|
+
Cada modelo contém:
|
|
42
|
+
- input_price: preço por token de entrada
|
|
43
|
+
- output_price: preço por token de saída
|
|
44
|
+
- offpeak_input_price: preço reduzido em horário de pico
|
|
45
|
+
- offpeak_output_price: preço reduzido em horário de pico
|
|
46
|
+
- batch_input_price: preço para lote
|
|
47
|
+
- batch_output_price: preço para lote
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Lista de ModeloIA com modelos e preços
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
ProviderError: Se houver erro ao buscar os modelos
|
|
54
|
+
"""
|
|
55
|
+
url = f"{self.base_url}/api/chat/models"
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
58
|
+
"Content-Type": "application/json"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
response = requests.get(url, headers=headers, timeout=30)
|
|
63
|
+
response.raise_for_status()
|
|
64
|
+
|
|
65
|
+
data = response.json()
|
|
66
|
+
modelos = []
|
|
67
|
+
|
|
68
|
+
# Processar modelos ativos
|
|
69
|
+
if 'models' in data and isinstance(data['models'], dict):
|
|
70
|
+
for nome_modelo, detalhes in data['models'].items():
|
|
71
|
+
# Preços vêm por token, convertemos para por 1k tokens
|
|
72
|
+
input_price_per_token = detalhes.get('input_price', 0)
|
|
73
|
+
output_price_per_token = detalhes.get('output_price', 0)
|
|
74
|
+
|
|
75
|
+
# Converter para preço por 1k tokens
|
|
76
|
+
custo_input_por_1k = input_price_per_token * 1000
|
|
77
|
+
custo_output_por_1k = output_price_per_token * 1000
|
|
78
|
+
|
|
79
|
+
modelo = ModeloIA(
|
|
80
|
+
provedor=self.provider_name,
|
|
81
|
+
nome_modelo=nome_modelo,
|
|
82
|
+
custo_input_por_1k=custo_input_por_1k,
|
|
83
|
+
custo_output_por_1k=custo_output_por_1k,
|
|
84
|
+
ativo=True
|
|
85
|
+
)
|
|
86
|
+
modelos.append(modelo)
|
|
87
|
+
|
|
88
|
+
logger.debug(
|
|
89
|
+
f"Modelo {nome_modelo}: "
|
|
90
|
+
f"input={custo_input_por_1k:.6f}, "
|
|
91
|
+
f"output={custo_output_por_1k:.6f} por 1k tokens"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Opcional: Processar modelos deprecados (marcados como inativos)
|
|
95
|
+
if 'deprecated_models' in data and isinstance(data['deprecated_models'], dict):
|
|
96
|
+
for nome_modelo, detalhes in data['deprecated_models'].items():
|
|
97
|
+
# Modelos deprecados podem não ter preços, usar 0
|
|
98
|
+
modelo = ModeloIA(
|
|
99
|
+
provedor=self.provider_name,
|
|
100
|
+
nome_modelo=nome_modelo,
|
|
101
|
+
custo_input_por_1k=0.0,
|
|
102
|
+
custo_output_por_1k=0.0,
|
|
103
|
+
ativo=False
|
|
104
|
+
)
|
|
105
|
+
modelos.append(modelo)
|
|
106
|
+
|
|
107
|
+
logger.debug(f"Modelo deprecado: {nome_modelo}")
|
|
108
|
+
|
|
109
|
+
logger.info(f"Buscados {len(modelos)} modelos da Maritaca "
|
|
110
|
+
f"({sum(1 for m in modelos if m.ativo)} ativos)")
|
|
111
|
+
return modelos
|
|
112
|
+
|
|
113
|
+
except requests.RequestException as e:
|
|
114
|
+
raise ProviderError(f"Erro ao buscar modelos da Maritaca: {str(e)}")
|
|
115
|
+
except (KeyError, ValueError, TypeError) as e:
|
|
116
|
+
raise ProviderError(f"Erro ao processar resposta da Maritaca: {str(e)}")
|
|
117
|
+
|
|
118
|
+
def calcular_custo(self, nome_modelo: str, tokens_input: int,
|
|
119
|
+
tokens_output: int, custo_input_1k: float,
|
|
120
|
+
custo_output_1k: float) -> Dict[str, float]:
|
|
121
|
+
"""
|
|
122
|
+
Calcula o custo de uma chamada à Maritaca.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
nome_modelo: Nome do modelo utilizado
|
|
126
|
+
tokens_input: Quantidade de tokens de entrada
|
|
127
|
+
tokens_output: Quantidade de tokens de saída
|
|
128
|
+
custo_input_1k: Custo por 1000 tokens de entrada
|
|
129
|
+
custo_output_1k: Custo por 1000 tokens de saída
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dicionário com custos calculados
|
|
133
|
+
"""
|
|
134
|
+
custo_input = (tokens_input / 1000) * custo_input_1k
|
|
135
|
+
custo_output = (tokens_output / 1000) * custo_output_1k
|
|
136
|
+
custo_total = custo_input + custo_output
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
'custo_input': round(custo_input, 6),
|
|
140
|
+
'custo_output': round(custo_output, 6),
|
|
141
|
+
'custo_total': round(custo_total, 6)
|
|
142
|
+
}
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Classe principal do IA Cost Tracker."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional, Dict, Any, List
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from .database import DatabaseManager
|
|
9
|
+
from .models import ModeloIA, UsoToken
|
|
10
|
+
from .providers import MaritacaProvider, AnthropicProvider
|
|
11
|
+
from .exceptions import ModelNotFoundError, ConfigurationError
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class IATracker:
|
|
17
|
+
"""
|
|
18
|
+
Classe principal para rastreamento de custos de IA.
|
|
19
|
+
|
|
20
|
+
Esta classe gerencia o rastreamento de uso e custos de chamadas
|
|
21
|
+
a APIs de IA, incluindo sincronização de preços e registro de uso.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> tracker = IATracker(
|
|
25
|
+
... db_connection_string="postgresql://user:pass@localhost/db",
|
|
26
|
+
... maritaca_api_key="sua-chave"
|
|
27
|
+
... )
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Sincronizar preços da Maritaca
|
|
30
|
+
>>> tracker.sincronizar_precos_maritaca()
|
|
31
|
+
>>>
|
|
32
|
+
>>> # Registrar uso (Anthropic busca preços do banco automaticamente)
|
|
33
|
+
>>> tracker.registrar_chamada(
|
|
34
|
+
... provedor="anthropic",
|
|
35
|
+
... modelo="claude-3-5-sonnet-20241022",
|
|
36
|
+
... tokens_input=100,
|
|
37
|
+
... tokens_output=50,
|
|
38
|
+
... aplicacao="meu-app",
|
|
39
|
+
... tag_funcionalidade="chat"
|
|
40
|
+
... )
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self,
|
|
44
|
+
db_connection_string: str,
|
|
45
|
+
maritaca_api_key: Optional[str] = None,
|
|
46
|
+
maritaca_base_url: str = "https://chat.maritaca.ai"):
|
|
47
|
+
"""
|
|
48
|
+
Inicializa o IA Cost Tracker.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
db_connection_string: String de conexão PostgreSQL
|
|
52
|
+
maritaca_api_key: Chave de API da Maritaca (opcional)
|
|
53
|
+
maritaca_base_url: URL base da API Maritaca
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
ConfigurationError: Se a configuração for inválida
|
|
57
|
+
"""
|
|
58
|
+
self.db = DatabaseManager(db_connection_string)
|
|
59
|
+
|
|
60
|
+
# Maritaca Provider (precisa de API key)
|
|
61
|
+
self.maritaca = None
|
|
62
|
+
if maritaca_api_key:
|
|
63
|
+
self.maritaca = MaritacaProvider(maritaca_api_key, maritaca_base_url)
|
|
64
|
+
|
|
65
|
+
# Anthropic Provider (usa o banco de dados)
|
|
66
|
+
self.anthropic = AnthropicProvider(db_manager=self.db)
|
|
67
|
+
|
|
68
|
+
logger.info("IATracker inicializado com sucesso")
|
|
69
|
+
|
|
70
|
+
def sincronizar_precos_maritaca(self) -> int:
|
|
71
|
+
"""
|
|
72
|
+
Sincroniza os preços dos modelos da Maritaca.
|
|
73
|
+
|
|
74
|
+
Busca os modelos e preços da API da Maritaca e
|
|
75
|
+
atualiza o banco de dados.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Número de modelos sincronizados
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ConfigurationError: Se a Maritaca não estiver configurada
|
|
82
|
+
ProviderError: Se houver erro ao buscar os modelos
|
|
83
|
+
"""
|
|
84
|
+
if not self.maritaca:
|
|
85
|
+
raise ConfigurationError(
|
|
86
|
+
"Maritaca não configurada. Forneça maritaca_api_key na inicialização."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
modelos = self.maritaca.buscar_modelos_disponiveis()
|
|
90
|
+
|
|
91
|
+
count = 0
|
|
92
|
+
for modelo in modelos:
|
|
93
|
+
self.db.inserir_ou_atualizar_modelo(modelo)
|
|
94
|
+
count += 1
|
|
95
|
+
|
|
96
|
+
logger.info(f"{count} modelos da Maritaca sincronizados")
|
|
97
|
+
return count
|
|
98
|
+
|
|
99
|
+
def listar_modelos_anthropic(self) -> List[ModeloIA]:
|
|
100
|
+
"""
|
|
101
|
+
Lista os modelos da Anthropic disponíveis no banco de dados.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Lista de ModeloIA da Anthropic
|
|
105
|
+
|
|
106
|
+
Note:
|
|
107
|
+
Os preços da Anthropic devem ser mantidos manualmente no banco,
|
|
108
|
+
pois não há API pública para consulta.
|
|
109
|
+
"""
|
|
110
|
+
modelos = self.anthropic.buscar_modelos_disponiveis()
|
|
111
|
+
logger.info(f"{len(modelos)} modelos da Anthropic encontrados no banco")
|
|
112
|
+
return modelos
|
|
113
|
+
|
|
114
|
+
def registrar_chamada(self,
|
|
115
|
+
provedor: str,
|
|
116
|
+
modelo: str,
|
|
117
|
+
tokens_input: int,
|
|
118
|
+
tokens_output: int,
|
|
119
|
+
aplicacao: Optional[str] = None,
|
|
120
|
+
tag_funcionalidade: Optional[str] = None,
|
|
121
|
+
usuario: Optional[str] = None,
|
|
122
|
+
tempo_resposta_ms: Optional[int] = None,
|
|
123
|
+
erro: bool = False,
|
|
124
|
+
mensagem_erro: Optional[str] = None) -> int:
|
|
125
|
+
"""
|
|
126
|
+
Registra uma chamada de IA no banco de dados.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
provedor: Nome do provedor (maritaca, anthropic)
|
|
130
|
+
modelo: Nome do modelo utilizado
|
|
131
|
+
tokens_input: Quantidade de tokens de entrada
|
|
132
|
+
tokens_output: Quantidade de tokens de saída
|
|
133
|
+
aplicacao: Nome da aplicação que fez a chamada
|
|
134
|
+
tag_funcionalidade: Tag para identificar a funcionalidade
|
|
135
|
+
usuario: Usuário que realizou a chamada
|
|
136
|
+
tempo_resposta_ms: Tempo de resposta em milissegundos
|
|
137
|
+
erro: Se houve erro na chamada
|
|
138
|
+
mensagem_erro: Mensagem de erro, se houver
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
ID do registro de uso criado
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
ModelNotFoundError: Se o modelo não for encontrado no banco
|
|
145
|
+
|
|
146
|
+
Example:
|
|
147
|
+
>>> tracker.registrar_chamada(
|
|
148
|
+
... provedor="anthropic",
|
|
149
|
+
... modelo="claude-3-5-sonnet-20241022",
|
|
150
|
+
... tokens_input=150,
|
|
151
|
+
... tokens_output=75,
|
|
152
|
+
... aplicacao="chat-app",
|
|
153
|
+
... tag_funcionalidade="suporte"
|
|
154
|
+
... )
|
|
155
|
+
"""
|
|
156
|
+
# Buscar modelo no banco
|
|
157
|
+
modelo_db = self.db.buscar_modelo(provedor, modelo)
|
|
158
|
+
|
|
159
|
+
if not modelo_db:
|
|
160
|
+
raise ModelNotFoundError(
|
|
161
|
+
f"Modelo {provedor}/{modelo} não encontrado no banco de dados. "
|
|
162
|
+
f"Para Maritaca, execute sincronizar_precos_maritaca(). "
|
|
163
|
+
f"Para Anthropic, adicione o modelo manualmente no banco."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# Calcular custos
|
|
167
|
+
provider = self._get_provider(provedor)
|
|
168
|
+
custos = provider.calcular_custo(
|
|
169
|
+
modelo,
|
|
170
|
+
tokens_input,
|
|
171
|
+
tokens_output,
|
|
172
|
+
modelo_db.custo_input_por_1k,
|
|
173
|
+
modelo_db.custo_output_por_1k
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Criar registro de uso
|
|
177
|
+
uso = UsoToken(
|
|
178
|
+
id_modelo=modelo_db.id_modelo,
|
|
179
|
+
tokens_input=tokens_input,
|
|
180
|
+
tokens_output=tokens_output,
|
|
181
|
+
custo_input=custos['custo_input'],
|
|
182
|
+
custo_output=custos['custo_output'],
|
|
183
|
+
aplicacao=aplicacao,
|
|
184
|
+
tag_funcionalidade=tag_funcionalidade,
|
|
185
|
+
usuario=usuario,
|
|
186
|
+
tempo_resposta_ms=tempo_resposta_ms,
|
|
187
|
+
erro=erro,
|
|
188
|
+
mensagem_erro=mensagem_erro
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return self.db.registrar_uso(uso)
|
|
192
|
+
|
|
193
|
+
def _get_provider(self, provedor: str):
|
|
194
|
+
"""
|
|
195
|
+
Retorna o provider correspondente ao nome.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
provedor: Nome do provedor
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Instância do provider
|
|
202
|
+
|
|
203
|
+
Raises:
|
|
204
|
+
ConfigurationError: Se o provedor não for suportado
|
|
205
|
+
"""
|
|
206
|
+
provedor_lower = provedor.lower()
|
|
207
|
+
|
|
208
|
+
if provedor_lower == "maritaca":
|
|
209
|
+
if not self.maritaca:
|
|
210
|
+
raise ConfigurationError(
|
|
211
|
+
"Maritaca não configurada. Forneça maritaca_api_key na inicialização."
|
|
212
|
+
)
|
|
213
|
+
return self.maritaca
|
|
214
|
+
elif provedor_lower == "anthropic":
|
|
215
|
+
return self.anthropic
|
|
216
|
+
else:
|
|
217
|
+
raise ConfigurationError(
|
|
218
|
+
f"Provedor desconhecido: {provedor}. "
|
|
219
|
+
f"Provedores suportados: maritaca, anthropic"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
def track_call(self,
|
|
223
|
+
provedor: str,
|
|
224
|
+
modelo: str,
|
|
225
|
+
aplicacao: Optional[str] = None,
|
|
226
|
+
tag_funcionalidade: Optional[str] = None,
|
|
227
|
+
usuario: Optional[str] = None):
|
|
228
|
+
"""
|
|
229
|
+
Decorator para rastrear automaticamente chamadas de IA.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
provedor: Nome do provedor (maritaca, anthropic)
|
|
233
|
+
modelo: Nome do modelo utilizado
|
|
234
|
+
aplicacao: Nome da aplicação
|
|
235
|
+
tag_funcionalidade: Tag para identificar a funcionalidade
|
|
236
|
+
usuario: Usuário que realizou a chamada
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Decorator function
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
>>> @tracker.track_call(
|
|
243
|
+
... provedor="anthropic",
|
|
244
|
+
... modelo="claude-3-5-sonnet-20241022",
|
|
245
|
+
... aplicacao="chat-app"
|
|
246
|
+
... )
|
|
247
|
+
... def processar_mensagem(mensagem):
|
|
248
|
+
... response = client.messages.create(
|
|
249
|
+
... model="claude-3-5-sonnet-20241022",
|
|
250
|
+
... messages=[{"role": "user", "content": mensagem}]
|
|
251
|
+
... )
|
|
252
|
+
... return response
|
|
253
|
+
"""
|
|
254
|
+
def decorator(func):
|
|
255
|
+
def wrapper(*args, **kwargs):
|
|
256
|
+
start_time = time.time()
|
|
257
|
+
erro = False
|
|
258
|
+
mensagem_erro = None
|
|
259
|
+
result = None
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
result = func(*args, **kwargs)
|
|
263
|
+
return result
|
|
264
|
+
except Exception as e:
|
|
265
|
+
erro = True
|
|
266
|
+
mensagem_erro = str(e)
|
|
267
|
+
raise
|
|
268
|
+
finally:
|
|
269
|
+
tempo_resposta_ms = int((time.time() - start_time) * 1000)
|
|
270
|
+
|
|
271
|
+
# Extrair tokens da resposta
|
|
272
|
+
tokens_input = 0
|
|
273
|
+
tokens_output = 0
|
|
274
|
+
|
|
275
|
+
# Tentar diferentes formatos de resposta
|
|
276
|
+
if result:
|
|
277
|
+
# Formato Anthropic
|
|
278
|
+
if hasattr(result, 'usage'):
|
|
279
|
+
tokens_input = getattr(result.usage, 'input_tokens', 0)
|
|
280
|
+
tokens_output = getattr(result.usage, 'output_tokens', 0)
|
|
281
|
+
# Formato OpenAI-like (Maritaca pode usar)
|
|
282
|
+
elif isinstance(result, dict) and 'usage' in result:
|
|
283
|
+
usage = result['usage']
|
|
284
|
+
tokens_input = usage.get('prompt_tokens', 0)
|
|
285
|
+
tokens_output = usage.get('completion_tokens', 0)
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
if tokens_input > 0 or tokens_output > 0:
|
|
289
|
+
self.registrar_chamada(
|
|
290
|
+
provedor=provedor,
|
|
291
|
+
modelo=modelo,
|
|
292
|
+
tokens_input=tokens_input,
|
|
293
|
+
tokens_output=tokens_output,
|
|
294
|
+
aplicacao=aplicacao,
|
|
295
|
+
tag_funcionalidade=tag_funcionalidade,
|
|
296
|
+
usuario=usuario,
|
|
297
|
+
tempo_resposta_ms=tempo_resposta_ms,
|
|
298
|
+
erro=erro,
|
|
299
|
+
mensagem_erro=mensagem_erro
|
|
300
|
+
)
|
|
301
|
+
else:
|
|
302
|
+
logger.warning(
|
|
303
|
+
"Não foi possível extrair tokens da resposta. "
|
|
304
|
+
"Registro não criado."
|
|
305
|
+
)
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Erro ao registrar chamada: {str(e)}")
|
|
308
|
+
|
|
309
|
+
return wrapper
|
|
310
|
+
return decorator
|
|
311
|
+
|
|
312
|
+
def obter_estatisticas_aplicacao(self,
|
|
313
|
+
aplicacao: str,
|
|
314
|
+
data_inicio: Optional[datetime] = None,
|
|
315
|
+
data_fim: Optional[datetime] = None) -> Dict[str, Any]:
|
|
316
|
+
"""
|
|
317
|
+
Obtém estatísticas de uso para uma aplicação.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
aplicacao: Nome da aplicação
|
|
321
|
+
data_inicio: Data inicial do período
|
|
322
|
+
data_fim: Data final do período
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
Dicionário com estatísticas de uso e custo
|
|
326
|
+
|
|
327
|
+
Example:
|
|
328
|
+
>>> from datetime import datetime, timedelta
|
|
329
|
+
>>> data_inicio = datetime.now() - timedelta(days=7)
|
|
330
|
+
>>> stats = tracker.obter_estatisticas_aplicacao(
|
|
331
|
+
... "meu-app",
|
|
332
|
+
... data_inicio=data_inicio
|
|
333
|
+
... )
|
|
334
|
+
>>> print(f"Custo total: R$ {stats['custo_total']:.2f}")
|
|
335
|
+
"""
|
|
336
|
+
return self.db.buscar_custos_por_aplicacao(aplicacao, data_inicio, data_fim)
|
|
337
|
+
|
|
338
|
+
def listar_modelos(self, provedor: Optional[str] = None) -> List[ModeloIA]:
|
|
339
|
+
"""
|
|
340
|
+
Lista modelos disponíveis no banco de dados.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
provedor: Filtrar por provedor específico (opcional)
|
|
344
|
+
|
|
345
|
+
Returns:
|
|
346
|
+
Lista de ModeloIA
|
|
347
|
+
|
|
348
|
+
Example:
|
|
349
|
+
>>> # Listar todos os modelos
|
|
350
|
+
>>> todos = tracker.listar_modelos()
|
|
351
|
+
>>>
|
|
352
|
+
>>> # Listar apenas Anthropic
|
|
353
|
+
>>> anthropic = tracker.listar_modelos(provedor="anthropic")
|
|
354
|
+
>>> for m in anthropic:
|
|
355
|
+
... print(f"{m.nome_modelo}: R$ {m.custo_input_por_1k}/1k")
|
|
356
|
+
"""
|
|
357
|
+
return self.db.listar_modelos(provedor=provedor)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ia_tracker_qca
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Biblioteca para rastreamento e auditoria de custos de APIs de IA
|
|
5
|
+
Author-email: Sua Equipe <contato@suaempresa.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/suaempresa/ia-cost-tracker
|
|
8
|
+
Project-URL: Documentation, https://github.com/suaempresa/ia-cost-tracker#readme
|
|
9
|
+
Project-URL: Repository, https://github.com/suaempresa/ia-cost-tracker
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Requires-Python: >=3.8
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENCE
|
|
21
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
22
|
+
Requires-Dist: requests>=2.28.0
|
|
23
|
+
Requires-Dist: python-dotenv>=0.19.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-cov>=3.0.0; extra == "dev"
|
|
27
|
+
Requires-Dist: black>=22.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: isort>=5.10.0; extra == "dev"
|
|
29
|
+
Requires-Dist: flake8>=4.0.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# IA Cost Tracker
|
|
33
|
+
|
|
34
|
+
[](https://badge.fury.io/py/ia-cost-tracker)
|
|
35
|
+
[](https://pypi.org/project/ia-cost-tracker/)
|
|
36
|
+
[](https://opensource.org/licenses/MIT)
|
|
37
|
+
[](https://pepy.tech/project/ia-cost-tracker)
|
|
38
|
+
|
|
39
|
+
Rastreie custos de APIs de IA (Maritaca, Anthropic) automaticamente.
|
|
40
|
+
|
|
41
|
+
Biblioteca Python para rastreamento e auditoria de custos de APIs de IA (Maritaca, Anthropic).
|
|
42
|
+
|
|
43
|
+
## Instalação
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pip install ia_tracker_qca
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Uso básico (não recomendado pois demora mais por causa que busca os valores atualizados na hora!):
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ia_cost_tracker import IATracker
|
|
54
|
+
|
|
55
|
+
# Inicializar
|
|
56
|
+
tracker = IATracker(
|
|
57
|
+
db_connection_string="postgresql://user:pass@localhost/db",
|
|
58
|
+
maritaca_api_key="sua-chave",
|
|
59
|
+
anthropic_api_key="sua-chave"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Sincronizar preços dos modelos
|
|
63
|
+
tracker.sincronizar_precos_maritaca() #essa chamada a api demora uns 10segs as vezes.
|
|
64
|
+
tracker.sincronizar_precos_anthropic()
|
|
65
|
+
|
|
66
|
+
# Registrar uma chamada
|
|
67
|
+
tracker.registrar_chamada(
|
|
68
|
+
provedor="maritaca",
|
|
69
|
+
modelo="sabia-2-medium",
|
|
70
|
+
tokens_input=150,
|
|
71
|
+
tokens_output=75,
|
|
72
|
+
aplicacao="meu-app",
|
|
73
|
+
tag_funcionalidade="chat",
|
|
74
|
+
usuario="usuario@email.com"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Obter estatísticas
|
|
78
|
+
stats = tracker.obter_estatisticas_aplicacao("meu-app")
|
|
79
|
+
print(f"Custo total: R$ {stats['custo_total']}")
|
|
80
|
+
print(f"Total de chamadas: {stats['total_chamadas']}")
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
Exemplo de uso em uma aplicação em produção:
|
|
86
|
+
```python
|
|
87
|
+
from ia_cost_tracker import IATracker
|
|
88
|
+
|
|
89
|
+
tracker = IATracker(db_connection_string=os.getenv("DB_CONNECTION_STRING"))
|
|
90
|
+
|
|
91
|
+
# Após chamar a IA
|
|
92
|
+
tracker.registrar_chamada(
|
|
93
|
+
provedor="maritaca", # ou "anthropic"
|
|
94
|
+
modelo="sabia-4",
|
|
95
|
+
tokens_input=150,
|
|
96
|
+
tokens_output=75,
|
|
97
|
+
aplicacao="meu-app",
|
|
98
|
+
tag_funcionalidade="chat",
|
|
99
|
+
usuario="user@email.com" # opcional
|
|
100
|
+
)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Desenvolvimento:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Clonar repositório
|
|
107
|
+
git clone https://github.com/suaempresa/ia-cost-tracker
|
|
108
|
+
cd ia-cost-tracker
|
|
109
|
+
|
|
110
|
+
# Instalar em modo desenvolvimento
|
|
111
|
+
pip install -e ".[dev]"
|
|
112
|
+
|
|
113
|
+
# Rodar testes
|
|
114
|
+
pytest
|
|
115
|
+
|
|
116
|
+
# Formatar código
|
|
117
|
+
black src/
|
|
118
|
+
isort src/
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Consultar Custos:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from datetime import datetime, timedelta
|
|
125
|
+
|
|
126
|
+
# Últimos 7 dias
|
|
127
|
+
data_inicio = datetime.now() - timedelta(days=7)
|
|
128
|
+
stats = tracker.obter_estatisticas_aplicacao(
|
|
129
|
+
aplicacao="meu-app",
|
|
130
|
+
data_inicio=data_inicio
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Listar modelos disponíveis
|
|
134
|
+
modelos = tracker.listar_modelos(provedor="maritaca")
|
|
135
|
+
for modelo in modelos:
|
|
136
|
+
print(f"{modelo.nome_modelo}: "
|
|
137
|
+
f"R$ {modelo.custo_input_por_1k}/1k input, "
|
|
138
|
+
f"R$ {modelo.custo_output_por_1k}/1k output")
|
|
139
|
+
|
|
140
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
ia_cost_tracker/__init__.py,sha256=zxycoNe5hNjk7UVI0wIisyzsVatDIJhYofZY1oIODW8,646
|
|
2
|
+
ia_cost_tracker/database.py,sha256=11JEiBjAEDzrlJzZXz7SKCDyvCK54GJPzjQ4-5bHWiM,10282
|
|
3
|
+
ia_cost_tracker/exceptions.py,sha256=vm032KW8XPVQCCASzsRU0GPK7qYgc40ds15KO0QLs3E,636
|
|
4
|
+
ia_cost_tracker/models.py,sha256=pGc3bbgUx6VerlfG9-yG_bgyM8NLs0Eef-5OOglZSeM,2500
|
|
5
|
+
ia_cost_tracker/tracker.py,sha256=1fNqav0dlSBZsqLV5WZIF3bjL7IzcgYufMwHXumFTL0,13717
|
|
6
|
+
ia_cost_tracker/providers/__init__.py,sha256=XRL0u59YpikkjmgDfAd-e6fX-_3rsJHyqmJBPeo7q5U,240
|
|
7
|
+
ia_cost_tracker/providers/anthropic.py,sha256=S8utx3aR0o1Hmt8G5QI7zcNwJJCob-oEIqUeyKVB-BQ,2795
|
|
8
|
+
ia_cost_tracker/providers/base.py,sha256=V-8s8qUHVxJSOo3difQO30Q3IGN6e1yt5sxUCsPOMQM,1687
|
|
9
|
+
ia_cost_tracker/providers/maritaca.py,sha256=JnRwt0naBRnPH0FHdNVkZ9ZfwpSJwHpmFYBv6rl3P88,5735
|
|
10
|
+
ia_tracker_qca-0.1.0.dist-info/licenses/LICENCE,sha256=Xpa1tEk-XW4tkTokBTU-bngiw8cbfGtqBoNVBjKnFAo,1096
|
|
11
|
+
ia_tracker_qca-0.1.0.dist-info/METADATA,sha256=F-CIvLcAvWNRSHZ7ok40DsLFB64OzcfGl1Md9suffIU,4198
|
|
12
|
+
ia_tracker_qca-0.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
ia_tracker_qca-0.1.0.dist-info/top_level.txt,sha256=OUDlog_CDGtGd2Xc1EQuCg-hiYQfGcsf91c5Ob4NelQ,16
|
|
14
|
+
ia_tracker_qca-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Elison Felipe Santos
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ia_cost_tracker
|