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.
@@ -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,11 @@
1
+ """Módulo de provedores de IA."""
2
+
3
+ from .base import BaseProvider
4
+ from .maritaca import MaritacaProvider
5
+ from .anthropic import AnthropicProvider
6
+
7
+ __all__ = [
8
+ "BaseProvider",
9
+ "MaritacaProvider",
10
+ "AnthropicProvider",
11
+ ]
@@ -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
+ [![PyPI version](https://badge.fury.io/py/ia-cost-tracker.svg)](https://badge.fury.io/py/ia-cost-tracker)
35
+ [![Python Versions](https://img.shields.io/pypi/pyversions/ia-cost-tracker.svg)](https://pypi.org/project/ia-cost-tracker/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
37
+ [![Downloads](https://pepy.tech/badge/ia-cost-tracker)](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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.10.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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