#!/usr/bin/env python3 """ Debug script to investigate cost API data for specific user and date. Uses raw metadata files from output/usage/raw/ directory. Usage: python debug_cost_api.py 2025-10-18 python debug_cost_api.py 2025-10-18 --email someone@example.com """ import argparse import json import os import sys import urllib.parse import urllib.request from datetime import datetime, timedelta from pathlib import Path def fetch_api_data(endpoint: str, params: dict, admin_key: str) -> dict: """Fetch data from Claude Admin API with pagination.""" base_url = 'https://api.anthropic.com' # Build query string query_parts = [] for key, value in params.items(): if isinstance(value, list): for item in value: query_parts.append(f'{urllib.parse.quote(key)}={urllib.parse.quote(str(item))}') else: query_parts.append(f'{urllib.parse.quote(key)}={urllib.parse.quote(str(value))}') query_string = '&'.join(query_parts) url = f'{base_url}{endpoint}?{query_string}' headers = {'anthropic-version': '2023-06-01', 'x-api-key': admin_key} all_data = None page = 1 while url: print(f'Fetching page {page}...', file=sys.stderr) req = urllib.request.Request(url, headers=headers) try: with urllib.request.urlopen(req) as response: data = json.loads(response.read().decode()) if all_data is None: all_data = data else: # Merge paginated data if 'data' in data: all_data['data'].extend(data['data']) all_data['has_more'] = data.get('has_more', False) if 'next_page' in data: all_data['next_page'] = data.get('next_page') if 'last_id' in data: all_data['last_id'] = data.get('last_id') # Check for next page if not data.get('has_more'): url = None elif data.get('next_page'): next_page = data['next_page'] base_url_only = url.split('?')[0] if '?' in url else url separator = '&' if query_string else '' url = f'{base_url_only}?{query_string}{separator}page={urllib.parse.quote(next_page)}' elif data.get('last_id'): last_id = data['last_id'] base_url_only = url.split('?')[0] if '?' in url else url separator = '&' if query_string else '' url = f'{base_url_only}?{query_string}{separator}after_id={urllib.parse.quote(last_id)}' else: url = None page += 1 except urllib.error.HTTPError as e: error_body = e.read().decode() if e.fp else 'No error details' print(f'API Error {e.code}: {error_body}', file=sys.stderr) raise return all_data def load_raw_metadata(filename: str) -> dict: """Load metadata from raw JSON file.""" raw_dir = Path('output/usage/raw') filepath = raw_dir / filename if not filepath.exists(): print(f'ERROR: Raw metadata file not found: {filepath}', file=sys.stderr) print('Please run: make fetch-metadata', file=sys.stderr) sys.exit(1) with open(filepath) as f: return json.load(f) def fetch_usage_data(date_str: str, admin_key: str) -> dict: """Fetch usage data directly from API for the given date.""" dt = datetime.fromisoformat(date_str) next_day = dt + timedelta(days=1) starting_at = f'{date_str}T00:00:00Z' ending_at = next_day.strftime('%Y-%m-%dT00:00:00Z') print(f' Fetching from API: {starting_at} to {ending_at}') return fetch_api_data( '/v1/organizations/usage_report/messages', { 'starting_at': starting_at, 'ending_at': ending_at, 'bucket_width': '1d', 'group_by[]': ['api_key_id', 'model'], }, admin_key, ) def main(): parser = argparse.ArgumentParser( description='Debug cost API data for a specific user and date', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python debug_cost_api.py 2025-10-18 python debug_cost_api.py 2025-10-18 --email john@example.com """, ) parser.add_argument('date', help='Target date in YYYY-MM-DD format (e.g., 2025-10-18)') parser.add_argument('--email', default='ratoui@sonymusic-pde.com', help='User email address to investigate (default: ratoui@sonymusic-pde.com)') args = parser.parse_args() # Validate date format try: datetime.fromisoformat(args.date) except ValueError: print(f'ERROR: Invalid date format: {args.date}', file=sys.stderr) print('Expected format: YYYY-MM-DD (e.g., 2025-10-18)', file=sys.stderr) sys.exit(1) # Get admin key admin_key = os.getenv('CLAUDE_ADMIN_KEY') if not admin_key: print('ERROR: CLAUDE_ADMIN_KEY environment variable not set', file=sys.stderr) sys.exit(1) target_email = args.email target_date = args.date print(f'\n=== Investigating cost data for {target_email} on {target_date} ===\n') # 1. Load users from raw metadata and build lookup map print('Step 1: Loading users metadata from raw file...') users_response = load_raw_metadata('users.json') users = users_response.get('data', []) print(f'Found {len(users)} users total') # Build user_id -> user data map users_by_id = {user['id']: user for user in users} target_user = None for user in users: if user.get('email') == target_email: target_user = user break if target_user: print('\n✓ Found user:') print(f' ID: {target_user.get("id")}') print(f' Name: {target_user.get("name")}') print(f' Email: {target_user.get("email")}') print(f' Role: {target_user.get("role")}') print(f' Added: {target_user.get("added_at")}') else: print(f'\n✗ User {target_email} NOT found in organization') print('\nAvailable users with similar email:') search_term = target_email.split('@')[0] if '@' in target_email else target_email for user in users: if search_term.lower() in user.get('email', '').lower(): print(f' - {user.get("email")} ({user.get("name")})') return # 2. Load API keys from raw metadata and match by user ID print('\n\nStep 2: Loading API keys from raw file...') api_keys_response = load_raw_metadata('api_keys.json') api_keys = api_keys_response.get('data', []) print(f'Found {len(api_keys)} API keys total') target_user_id = target_user['id'] user_api_keys = [] for key in api_keys: created_by = key.get('created_by', {}) if created_by.get('id') == target_user_id: user_api_keys.append(key) if user_api_keys: print(f'\n✓ Found {len(user_api_keys)} API key(s) created by {target_email}:') for key in user_api_keys: created_by = key.get('created_by', {}) creator_id = created_by.get('id') creator = users_by_id.get(creator_id, {}) creator_name = creator.get('name', 'Unknown') print(f'\n API Key: {key.get("name")}') print(f' ID: {key.get("id")}') print(f' Status: {key.get("status")}') print(f' Created: {key.get("created_at")}') print(f' Created by: {creator_name} ({creator_id})') print(f' Hint: {key.get("partial_key_hint")}') else: print(f'\n✗ No API keys found for {target_email}') return # 3. Fetch usage data from API for the target date print(f'\n\nStep 3: Fetching usage data for {target_date}...') usage_response = fetch_usage_data(target_date, admin_key) # Parse usage data - format is data[0]['results'] usage_data = [] data_array = usage_response.get('data', []) if data_array and isinstance(data_array, list) and len(data_array) > 0: usage_data = data_array[0].get('results', []) print(f'Found {len(usage_data)} usage records for {target_date}') # 4. Check if any usage records match the user's API keys user_key_ids = {key['id'] for key in user_api_keys} matching_records = [] for record in usage_data: if record.get('api_key_id') in user_key_ids: matching_records.append(record) if matching_records: print(f'\n✓ Found {len(matching_records)} usage record(s) for {target_email} on {target_date}:') for record in matching_records: # Parse cache creation tokens cache_creation = record.get('cache_creation', {}) ephemeral_1h = cache_creation.get('ephemeral_1h_input_tokens', 0) ephemeral_5m = cache_creation.get('ephemeral_5m_input_tokens', 0) total_cache_creation = ephemeral_1h + ephemeral_5m # Find the API key name key_id = record.get('api_key_id') key_name = 'Unknown' for key in user_api_keys: if key['id'] == key_id: key_name = key['name'] break print('\n Record:') print(f' API Key: {key_name} ({key_id})') print(f' Model: {record.get("model")}') print(f' Uncached Input: {record.get("uncached_input_tokens", 0):,}') print(f' Cache Creation: {total_cache_creation:,} (1h: {ephemeral_1h:,}, 5m: {ephemeral_5m:,})') print(f' Cache Read: {record.get("cache_read_input_tokens", 0):,}') print(f' Output: {record.get("output_tokens", 0):,}') else: print(f'\n✗ No usage records found for {target_email} on {target_date}') print('\nSearching for nearby dates...') # Check a few days before and after dt = datetime.fromisoformat(target_date) for offset in [-3, -2, -1, 1, 2, 3]: check_date = dt + timedelta(days=offset) check_date_str = check_date.strftime('%Y-%m-%d') print(f'\nChecking {check_date_str}...') check_response = fetch_usage_data(check_date_str, admin_key) # Parse usage data - format is data[0]['results'] check_data = [] data_array = check_response.get('data', []) if data_array and isinstance(data_array, list) and len(data_array) > 0: check_data = data_array[0].get('results', []) check_matches = [r for r in check_data if r.get('api_key_id') in user_key_ids] if check_matches: print(f' ✓ Found {len(check_matches)} record(s) on {check_date_str}') for record in check_matches: cache_creation = record.get('cache_creation', {}) cache_tokens = cache_creation.get('ephemeral_1h_input_tokens', 0) + cache_creation.get('ephemeral_5m_input_tokens', 0) tokens = record.get('uncached_input_tokens', 0) + record.get('output_tokens', 0) + cache_tokens + record.get('cache_read_input_tokens', 0) print(f' - {record.get("model")}: {tokens:,} total tokens') else: print(f' ✗ No records on {check_date_str}') # 5. Summary print('\n\n=== Summary ===') print(f'Target User: {target_email}') print(f'Target Date: {target_date}') print(f'API Keys Found: {len(user_api_keys)}') print(f'Usage Records on Target Date: {len(matching_records)}') if not matching_records: print('\n⚠️ No usage data found for this user on the target date.') print('Possible reasons:') print(' 1. The user did not use Claude Code on this date') print(' 2. The API key was not active on this date') print(' 3. There is a delay in usage data availability') print(' 4. The usage data is grouped differently than expected') if __name__ == '__main__': main()