code-meter 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.
code_meter/cli.py ADDED
@@ -0,0 +1,334 @@
1
+
2
+ """Typer CLI interface for code-meter."""
3
+
4
+ import sys
5
+ from datetime import datetime, timedelta, timezone
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from code_meter.analytics.reports import ReportGenerator
13
+ from code_meter.config import get_config_file_path, load_config, save_config
14
+ from code_meter.pricing.engine import PricingEngine
15
+ from code_meter.providers.antigravity import AntigravityProvider
16
+ from code_meter.providers.claude_code import ClaudeCodeProvider
17
+ from code_meter.providers.codex import CodexProvider
18
+ from code_meter.storage.database import Database
19
+ from code_meter.storage.repository import UsageRepository
20
+ from code_meter.ui.dashboard import render_dashboard
21
+ from code_meter.ui.tables import (
22
+ render_daily_table,
23
+ render_models_table,
24
+ render_projects_table,
25
+ render_sessions_table,
26
+ render_status_panel,
27
+ )
28
+ from code_meter.watcher import run_watch_loop
29
+
30
+ app = typer.Typer(
31
+ name="code-meter",
32
+ help="Local AI coding token usage tracker and cost estimator.",
33
+ add_completion=False,
34
+ invoke_without_command=True,
35
+ )
36
+
37
+ console = Console()
38
+
39
+
40
+ def _get_services():
41
+ """Helper to initialize config, database, repository, pricing engine, and providers."""
42
+ config = load_config()
43
+ db_path = config.get_db_path()
44
+ claude_dir = config.get_claude_dir()
45
+ codex_dir = config.get_codex_dir()
46
+ antigravity_dir = config.get_antigravity_dir()
47
+
48
+ db = Database(db_path)
49
+ repository = UsageRepository(db)
50
+ pricing_engine = PricingEngine(repository)
51
+
52
+ providers = [
53
+ ClaudeCodeProvider(claude_dir),
54
+ CodexProvider(codex_dir),
55
+ AntigravityProvider(antigravity_dir),
56
+ ]
57
+
58
+ report_gen = ReportGenerator(pricing_engine)
59
+
60
+ return config, repository, pricing_engine, providers, report_gen
61
+
62
+
63
+ def _perform_scan(quiet: bool = False) -> int:
64
+ """Execute scan across all AI providers and return number of new records saved."""
65
+ config, repo, pricing_engine, providers, _ = _get_services()
66
+ existing_states = repo.get_scan_states()
67
+
68
+ total_new_records = 0
69
+ total_files_scanned = 0
70
+
71
+ for provider in providers:
72
+ result = provider.scan(existing_states)
73
+ new_records_count = repo.save_records(result.records)
74
+ repo.save_prompts(result.prompts)
75
+ repo.save_code_changes(result.code_changes)
76
+ repo.update_scan_states(result.updated_states)
77
+
78
+ total_new_records += new_records_count
79
+ total_files_scanned += result.files_scanned
80
+
81
+ if not quiet:
82
+ console.print(
83
+ f"[bold green]Scan complete:[/bold green] Scanned {total_files_scanned} files across "
84
+ f"{len(providers)} providers, added {total_new_records} new records."
85
+ )
86
+
87
+ return total_new_records
88
+
89
+
90
+ @app.callback()
91
+ def main_callback(ctx: typer.Context):
92
+ """Default action when running `code-meter` without subcommands."""
93
+ if ctx.invoked_subcommand is None:
94
+ _perform_scan(quiet=True)
95
+ config, repo, pricing_engine, providers, report_gen = _get_services()
96
+
97
+ records = repo.query_usage_events()
98
+ if not records:
99
+ console.print("[yellow]No usage data available yet.[/yellow]")
100
+ console.print("[dim]Scanned providers: Claude Code, Codex, Antigravity[/dim]")
101
+ return
102
+
103
+ summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
104
+ console.clear()
105
+ render_dashboard(summary, title_suffix="Overview", console=console)
106
+
107
+
108
+ @app.command("scan")
109
+ def scan_cmd():
110
+ """Scan local Claude Code directories for new token usage records."""
111
+ _perform_scan(quiet=False)
112
+
113
+
114
+ @app.command("today")
115
+ def today_cmd():
116
+ """Display usage statistics for today."""
117
+ _perform_scan(quiet=True)
118
+ _, repo, _, _, report_gen = _get_services()
119
+
120
+ today_start = datetime.now(timezone.utc).replace(hour=0, minute=0, second=0, microsecond=0)
121
+ records = repo.query_usage_events(start_date=today_start)
122
+
123
+ if not records:
124
+ console.print("[yellow]No usage recorded today.[/yellow]")
125
+ return
126
+
127
+ summary = report_gen.generate_summary(records, filter_date_str=today_start.strftime("%Y-%m-%d"))
128
+ render_dashboard(summary, title_suffix="Today", console=console)
129
+
130
+
131
+ @app.command("week")
132
+ def week_cmd():
133
+ """Display usage statistics for the past 7 days."""
134
+ _perform_scan(quiet=True)
135
+ _, repo, _, _, report_gen = _get_services()
136
+
137
+ week_start = datetime.now(timezone.utc) - timedelta(days=7)
138
+ records = repo.query_usage_events(start_date=week_start)
139
+
140
+ if not records:
141
+ console.print("[yellow]No usage recorded in the last 7 days.[/yellow]")
142
+ return
143
+
144
+ summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
145
+ render_dashboard(summary, title_suffix="Last 7 Days", console=console)
146
+
147
+
148
+ @app.command("month")
149
+ def month_cmd():
150
+ """Display usage statistics for the past 30 days."""
151
+ _perform_scan(quiet=True)
152
+ _, repo, _, _, report_gen = _get_services()
153
+
154
+ month_start = datetime.now(timezone.utc) - timedelta(days=30)
155
+ records = repo.query_usage_events(start_date=month_start)
156
+
157
+ if not records:
158
+ console.print("[yellow]No usage recorded in the last 30 days.[/yellow]")
159
+ return
160
+
161
+ summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
162
+ render_dashboard(summary, title_suffix="Last 30 Days", console=console)
163
+
164
+
165
+ @app.command("status")
166
+ def status_cmd():
167
+ """Display system status (DB path, file counts, record totals)."""
168
+ _perform_scan(quiet=True)
169
+ _, repo, _, _, _ = _get_services()
170
+ summary = repo.get_status_summary()
171
+ render_status_panel(summary, console=console)
172
+
173
+
174
+ @app.command("report")
175
+ def report_cmd(
176
+ model: Optional[str] = typer.Option(None, "--model", help="Filter by model name"),
177
+ project: Optional[str] = typer.Option(None, "--project", help="Filter by project ID or path"),
178
+ provider: Optional[str] = typer.Option(None, "--provider", help="Filter by provider"),
179
+ days: Optional[int] = typer.Option(None, "--days", help="Number of days to include"),
180
+ ):
181
+ """Display detailed aggregated report with optional filters."""
182
+ _perform_scan(quiet=True)
183
+ _, repo, pricing_engine, _, report_gen = _get_services()
184
+
185
+ start_date = datetime.now(timezone.utc) - timedelta(days=days) if days else None
186
+ records = repo.query_usage_events(
187
+ provider=provider,
188
+ model=model,
189
+ project_id=project,
190
+ start_date=start_date,
191
+ )
192
+
193
+ if not records:
194
+ console.print("[yellow]No matching usage records found.[/yellow]")
195
+ return
196
+
197
+ record_costs_list = [
198
+ pricing_engine.calculate_cost(
199
+ r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
200
+ )
201
+ for r in records
202
+ ]
203
+
204
+ daily_stats = report_gen.aggregate_by_day(records, record_costs_list)
205
+ render_daily_table(daily_stats, console=console)
206
+
207
+
208
+ @app.command("models")
209
+ def models_cmd():
210
+ """Display token usage and cost aggregated by model."""
211
+ _perform_scan(quiet=True)
212
+ _, repo, pricing_engine, _, report_gen = _get_services()
213
+ records = repo.query_usage_events()
214
+ if not records:
215
+ console.print("[yellow]No usage records found.[/yellow]")
216
+ return
217
+
218
+ costs = [
219
+ pricing_engine.calculate_cost(
220
+ r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
221
+ )
222
+ for r in records
223
+ ]
224
+ model_stats = report_gen.aggregate_by_model(records, costs)
225
+ render_models_table(model_stats, console=console)
226
+
227
+
228
+ @app.command("projects")
229
+ def projects_cmd():
230
+ """Display token usage and cost aggregated by project/directory."""
231
+ _perform_scan(quiet=True)
232
+ _, repo, pricing_engine, _, report_gen = _get_services()
233
+ records = repo.query_usage_events()
234
+ if not records:
235
+ console.print("[yellow]No usage records found.[/yellow]")
236
+ return
237
+
238
+ costs = [
239
+ pricing_engine.calculate_cost(
240
+ r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
241
+ )
242
+ for r in records
243
+ ]
244
+ project_stats = report_gen.aggregate_by_project(records, costs)
245
+ render_projects_table(project_stats, console=console)
246
+
247
+
248
+ @app.command("sessions")
249
+ def sessions_cmd():
250
+ """Display token usage and cost aggregated by session ID."""
251
+ _perform_scan(quiet=True)
252
+ _, repo, pricing_engine, _, report_gen = _get_services()
253
+ records = repo.query_usage_events()
254
+ if not records:
255
+ console.print("[yellow]No usage records found.[/yellow]")
256
+ return
257
+
258
+ costs = [
259
+ pricing_engine.calculate_cost(
260
+ r.provider, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, r.cache_write_tokens, r.timestamp
261
+ )
262
+ for r in records
263
+ ]
264
+ session_stats = report_gen.aggregate_by_session(records, costs)
265
+ render_sessions_table(session_stats, console=console)
266
+
267
+
268
+ @app.command("export")
269
+ def export_cmd(
270
+ format: str = typer.Option("csv", "--format", "-f", help="Export format: csv or json"),
271
+ output: Optional[Path] = typer.Option(None, "--output", "-o", help="Output file path"),
272
+ days: Optional[int] = typer.Option(None, "--days", help="Number of days to include"),
273
+ model: Optional[str] = typer.Option(None, "--model", help="Filter by model"),
274
+ project: Optional[str] = typer.Option(None, "--project", help="Filter by project"),
275
+ ):
276
+ """Export usage records to CSV or JSON format."""
277
+ _perform_scan(quiet=True)
278
+ _, repo, _, _, report_gen = _get_services()
279
+
280
+ start_date = datetime.now(timezone.utc) - timedelta(days=days) if days else None
281
+ records = repo.query_usage_events(model=model, project_id=project, start_date=start_date)
282
+
283
+ if not records:
284
+ console.print("[yellow]No matching records to export.[/yellow]")
285
+ return
286
+
287
+ content = report_gen.export_records(records, format)
288
+
289
+ if output:
290
+ output = Path(output).resolve()
291
+ output.parent.mkdir(parents=True, exist_ok=True)
292
+ output.write_text(content, encoding="utf-8")
293
+ console.print(f"[bold green]Exported {len(records)} records to:[/bold green] {output}")
294
+ else:
295
+ sys.stdout.write(content)
296
+
297
+
298
+ @app.command("config")
299
+ def config_cmd():
300
+ """Display current configuration file path and contents."""
301
+ config_file = get_config_file_path()
302
+ config = load_config()
303
+ console.print(f"[bold cyan]Configuration File:[/bold cyan] {config_file}\n")
304
+ console.print(f"[bold white]General DB Path:[/bold white] {config.get_db_path()}")
305
+ console.print(f"[bold white]Claude Directory:[/bold white] {config.get_claude_dir()}")
306
+ console.print(f"[bold white]Codex Directory:[/bold white] {config.get_codex_dir()}")
307
+ console.print(f"[bold white]Antigravity Directory:[/bold white] {config.get_antigravity_dir()}")
308
+ console.print(f"[bold white]Refresh Seconds:[/bold white] {config.get_refresh_seconds()}")
309
+ console.print(f"[bold white]Currency:[/bold white] {config.pricing.currency}")
310
+
311
+
312
+ @app.command("watch")
313
+ def watch_cmd():
314
+ """Monitor local session log files in real-time and auto-update metrics."""
315
+ config, repo, pricing_engine, providers, report_gen = _get_services()
316
+
317
+ def update_and_render():
318
+ _perform_scan(quiet=True)
319
+ records = repo.query_usage_events()
320
+ summary = report_gen.generate_summary(records, filter_date_str=datetime.now(timezone.utc).strftime("%Y-%m-%d"))
321
+ console.clear()
322
+ render_dashboard(summary, title_suffix="Watch Mode (Live)", console=console)
323
+
324
+ target_dir = providers[0].claude_dir if providers else config.get_claude_dir()
325
+ run_watch_loop(
326
+ target_dir=target_dir,
327
+ on_change_callback=update_and_render,
328
+ refresh_seconds=config.get_refresh_seconds(),
329
+ console=console,
330
+ )
331
+
332
+
333
+ if __name__ == "__main__":
334
+ app()
code_meter/config.py ADDED
@@ -0,0 +1,116 @@
1
+ """Configuration management for code-meter."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Any, Dict, Optional
6
+ from pydantic import BaseModel, Field
7
+
8
+ try:
9
+ import tomllib
10
+ except ImportError:
11
+ import tomli as tomllib # type: ignore
12
+
13
+ import tomli_w
14
+
15
+
16
+ class GeneralConfig(BaseModel):
17
+ database: str = "~/.local/share/code-meter/usage.db"
18
+
19
+
20
+ class ClaudeCodeConfig(BaseModel):
21
+ directory: str = "~/.claude"
22
+
23
+
24
+ class CodexConfig(BaseModel):
25
+ directory: str = "~/.codex"
26
+
27
+
28
+ class AntigravityConfig(BaseModel):
29
+ directory: str = "~/.gemini/antigravity-ide"
30
+
31
+
32
+ class UIConfig(BaseModel):
33
+ refresh_seconds: int = 2
34
+
35
+
36
+ class PricingConfig(BaseModel):
37
+ currency: str = "USD"
38
+
39
+
40
+ class Config(BaseModel):
41
+ general: GeneralConfig = Field(default_factory=GeneralConfig)
42
+ claude_code: ClaudeCodeConfig = Field(default_factory=ClaudeCodeConfig)
43
+ codex: CodexConfig = Field(default_factory=CodexConfig)
44
+ antigravity: AntigravityConfig = Field(default_factory=AntigravityConfig)
45
+ ui: UIConfig = Field(default_factory=UIConfig)
46
+ pricing: PricingConfig = Field(default_factory=PricingConfig)
47
+
48
+ def get_db_path(self) -> Path:
49
+ raw_db = os.environ.get("CODE_METER_DB", os.environ.get("CLAUDE_METER_DB", self.general.database))
50
+ path = Path(os.path.expanduser(raw_db)).resolve()
51
+ path.parent.mkdir(parents=True, exist_ok=True)
52
+ return path
53
+
54
+ def get_claude_dir(self) -> Path:
55
+ raw_dir = os.environ.get("CODE_METER_CLAUDE_DIR", os.environ.get("CLAUDE_METER_CLAUDE_DIR", self.claude_code.directory))
56
+ return Path(os.path.expanduser(raw_dir)).resolve()
57
+
58
+ def get_codex_dir(self) -> Path:
59
+ raw_dir = os.environ.get("CODE_METER_CODEX_DIR", self.codex.directory)
60
+ return Path(os.path.expanduser(raw_dir)).resolve()
61
+
62
+ def get_antigravity_dir(self) -> Path:
63
+ raw_dir = os.environ.get("CODE_METER_ANTIGRAVITY_DIR", self.antigravity.directory)
64
+ return Path(os.path.expanduser(raw_dir)).resolve()
65
+
66
+ def get_refresh_seconds(self) -> int:
67
+ env_val = os.environ.get("CODE_METER_REFRESH", os.environ.get("CLAUDE_METER_REFRESH"))
68
+ if env_val:
69
+ try:
70
+ return int(env_val)
71
+ except ValueError:
72
+ pass
73
+ return self.ui.refresh_seconds
74
+
75
+
76
+ def get_config_dir() -> Path:
77
+ """Get platform-appropriate config directory (~/.config/code-meter or %APPDATA%/code-meter)."""
78
+ if os.name == "nt":
79
+ base = os.environ.get("APPDATA")
80
+ if base:
81
+ path = Path(base) / "code-meter"
82
+ else:
83
+ path = Path.home() / ".config" / "code-meter"
84
+ else:
85
+ path = Path.home() / ".config" / "code-meter"
86
+ path.mkdir(parents=True, exist_ok=True)
87
+ return path
88
+
89
+
90
+ def get_config_file_path() -> Path:
91
+ return get_config_dir() / "config.toml"
92
+
93
+
94
+ def load_config() -> Config:
95
+ """Load configuration from config.toml, returning default config if not present."""
96
+ config_file = get_config_file_path()
97
+ if not config_file.exists():
98
+ config = Config()
99
+ save_config(config)
100
+ return config
101
+
102
+ try:
103
+ with open(config_file, "rb") as f:
104
+ data = tomllib.load(f)
105
+ return Config(**data)
106
+ except Exception:
107
+ return Config()
108
+
109
+
110
+ def save_config(config: Config) -> Path:
111
+ """Save configuration to config.toml."""
112
+ config_file = get_config_file_path()
113
+ data = config.model_dump()
114
+ with open(config_file, "wb") as f:
115
+ tomli_w.dump(data, f)
116
+ return config_file
@@ -0,0 +1,13 @@
1
+ """Data models for code-meter."""
2
+
3
+ from code_meter.models.usage import UsageRecord
4
+ from code_meter.models.project import ProjectStats, ModelStats, SessionStats, DailyStats, AggregateSummary
5
+
6
+ __all__ = [
7
+ "UsageRecord",
8
+ "ProjectStats",
9
+ "ModelStats",
10
+ "SessionStats",
11
+ "DailyStats",
12
+ "AggregateSummary",
13
+ ]
@@ -0,0 +1,85 @@
1
+ """Analytics summary models for reporting and UI."""
2
+
3
+ from typing import List, Optional
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class ModelStats(BaseModel):
8
+ """Token usage and cost aggregated by model."""
9
+
10
+ model: str
11
+ request_count: int = 0
12
+ input_tokens: int = 0
13
+ output_tokens: int = 0
14
+ cache_read_tokens: int = 0
15
+ cache_write_tokens: int = 0
16
+ total_tokens: int = 0
17
+ estimated_cost_usd: Optional[float] = None
18
+
19
+
20
+ class ProjectStats(BaseModel):
21
+ """Token usage and cost aggregated by project/directory."""
22
+
23
+ project_name: str
24
+ project_path: Optional[str] = None
25
+ request_count: int = 0
26
+ input_tokens: int = 0
27
+ output_tokens: int = 0
28
+ cache_read_tokens: int = 0
29
+ cache_write_tokens: int = 0
30
+ total_tokens: int = 0
31
+ estimated_cost_usd: Optional[float] = None
32
+
33
+
34
+ class SessionStats(BaseModel):
35
+ """Token usage and cost aggregated by session ID."""
36
+
37
+ session_id: str
38
+ project_name: str = "Unknown"
39
+ project_path: Optional[str] = None
40
+ primary_model: str = "Unknown"
41
+ request_count: int = 0
42
+ input_tokens: int = 0
43
+ output_tokens: int = 0
44
+ cache_read_tokens: int = 0
45
+ cache_write_tokens: int = 0
46
+ total_tokens: int = 0
47
+ estimated_cost_usd: Optional[float] = None
48
+ first_seen: Optional[str] = None
49
+ last_seen: Optional[str] = None
50
+
51
+
52
+ class DailyStats(BaseModel):
53
+ """Token usage and cost aggregated by date (YYYY-MM-DD)."""
54
+
55
+ date_str: str
56
+ request_count: int = 0
57
+ input_tokens: int = 0
58
+ output_tokens: int = 0
59
+ cache_read_tokens: int = 0
60
+ cache_write_tokens: int = 0
61
+ total_tokens: int = 0
62
+ estimated_cost_usd: Optional[float] = None
63
+
64
+
65
+ class AggregateSummary(BaseModel):
66
+ """Overall token usage, cost, and burn rate summary."""
67
+
68
+ total_requests: int = 0
69
+ input_tokens: int = 0
70
+ output_tokens: int = 0
71
+ cache_read_tokens: int = 0
72
+ cache_write_tokens: int = 0
73
+ total_tokens: int = 0
74
+
75
+ estimated_cost_usd: Optional[float] = None
76
+ unpriced_requests: int = 0
77
+
78
+ tokens_per_hour: Optional[float] = None
79
+ tokens_per_day: Optional[float] = None
80
+ cost_per_hour: Optional[float] = None
81
+ cost_per_day: Optional[float] = None
82
+
83
+ by_model: List[ModelStats] = Field(default_factory=list)
84
+ by_project: List[ProjectStats] = Field(default_factory=list)
85
+ today_summary: Optional[DailyStats] = None
@@ -0,0 +1,61 @@
1
+ """Normalized UsageRecord, SessionPrompt, and SessionCodeChange models."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class UsageRecord(BaseModel):
9
+ """Normalized usage event from any AI provider."""
10
+
11
+ provider: str
12
+ request_id: Optional[str] = None
13
+ session_id: Optional[str] = None
14
+ timestamp: datetime
15
+
16
+ user_id: Optional[str] = None
17
+ project_id: Optional[str] = None
18
+ project_path: Optional[str] = None
19
+
20
+ model: str
21
+
22
+ input_tokens: int = 0
23
+ output_tokens: int = 0
24
+
25
+ cache_read_tokens: int = 0
26
+ cache_write_tokens: int = 0
27
+
28
+ metadata: Dict[str, Any] = Field(default_factory=dict)
29
+
30
+ # Agent-level tracking readiness
31
+ agent_id: Optional[str] = None
32
+ task_id: Optional[str] = None
33
+ tool_name: Optional[str] = None
34
+
35
+ @property
36
+ def total_tokens(self) -> int:
37
+ return self.input_tokens + self.output_tokens + self.cache_read_tokens + self.cache_write_tokens
38
+
39
+
40
+ class SessionPrompt(BaseModel):
41
+ """Record of a user prompt asked during an AI session."""
42
+
43
+ provider: str
44
+ session_id: str
45
+ prompt_id: str
46
+ timestamp: datetime
47
+ prompt_text: str
48
+ project_id: Optional[str] = None
49
+
50
+
51
+ class SessionCodeChange(BaseModel):
52
+ """Record of code changes / diffs made during an AI session."""
53
+
54
+ provider: str
55
+ session_id: str
56
+ change_id: str
57
+ timestamp: datetime
58
+ file_path: str
59
+ change_type: str # e.g., 'edit', 'write', 'snapshot', 'tool_call'
60
+ diff_summary: str
61
+ project_id: Optional[str] = None
@@ -0,0 +1,14 @@
1
+ """Pricing engine package."""
2
+
3
+ from code_meter.pricing.anthropic import DEFAULT_ANTHROPIC_PRICING
4
+ from code_meter.pricing.engine import DEFAULT_PRICING, PricingEngine
5
+ from code_meter.pricing.google import DEFAULT_GOOGLE_PRICING
6
+ from code_meter.pricing.openai import DEFAULT_OPENAI_PRICING
7
+
8
+ __all__ = [
9
+ "PricingEngine",
10
+ "DEFAULT_PRICING",
11
+ "DEFAULT_ANTHROPIC_PRICING",
12
+ "DEFAULT_OPENAI_PRICING",
13
+ "DEFAULT_GOOGLE_PRICING",
14
+ ]