A simple, local, and secure way to manage your AI API keys.
The ASTRAI API Key Manager is a lightweight desktop application built with Python's Tkinter, designed to help you securely store and quickly access your various AI API keys.
It's particularly useful for developers, AI enthusiasts, and anyone working with multiple AI services (like OpenAI, Google Gemini, Claude, etc.) who needs a convenient way to keep their API keys organized and ready for use.
This Python script creates a simple GUI where you can:
api_keys.json file, ensuring they never leave your machine.
To use the API Key Manager, you will need Python installed on your system. You'll also need the pyperclip library for clipboard functionality.
pip install pyperclip.py file (e.g., api_key_manager.py).python api_key_manager.pyThe manager will automatically create an api_keys.json file in the same directory where the script is run, to store your keys.
import tkinter as tk
from tkinter import simpledialog, messagebox, ttk
import json
import os
import pyperclip
class APIKeyManager:
def __init__(self, root):
self.root = root
self.root.title("API Key Manager")
self.root.geometry("550x560")
self.root.resizable(False, False)
self.style_dark_mode()
self.keys_file = "api_keys.json"
self.api_keys = self.load_keys()
main_frame = ttk.Frame(root, padding="20 20 20 20")
main_frame.pack(fill=tk.BOTH, expand=True)
ascii_logo = """
_____ _______ _____ _____ _____ _ _ _______
/\ / ____|__ __| __ \ /\ |_ _/ ____| | | | /\|__ __|
/ \ | (___ | | | |__) | / \ | || | | |__| | / \ | |
/ /\ \ \___ \ | | | _ / / /\ \ | || | | __ | / /\ \ | |
/ ____ \ ____) | | | | | \ \ / ____ \ _| || |____| | | |/ ____ \| |
/_/ \_\_____/ |_| |_| \_\/_/ \_\_____\_____|_| |_/_/ \_\_|
"""
logo_label = tk.Label(
main_frame,
text=ascii_logo,
font=('Consolas', 8),
bg='#1e1e1e',
fg="#7f1dc0"
)
logo_label.pack(pady=(0, 10))
title_label = ttk.Label(main_frame, text="API Key Manager",
font=('Arial', 18, 'bold'))
title_label.pack(pady=(0, 20))
listbox_frame = ttk.Frame(main_frame)
listbox_frame.pack(pady=10, fill=tk.X)
self.keys_listbox = tk.Listbox(
listbox_frame,
height=12,
bg='#2c2c2c',
fg='#ffffff',
selectbackground='#007acc',
selectforeground='#ffffff',
font=('Consolas', 10),
bd=0,
highlightthickness=0 #
)
self.keys_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar = ttk.Scrollbar(listbox_frame, orient=tk.VERTICAL, command=self.keys_listbox.yview)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.keys_listbox.config(yscrollcommand=scrollbar.set)
self.update_keys_listbox()
button_frame = ttk.Frame(main_frame)
button_frame.pack(pady=15)
add_button = ttk.Button(
button_frame,
text="Add Key",
command=self.add_key
)
add_button.pack(side=tk.LEFT, padx=5)
copy_button = ttk.Button(
button_frame,
text="Copy Selected Key",
command=self.copy_key
)
copy_button.pack(side=tk.LEFT, padx=5)
delete_button = ttk.Button(
button_frame,
text="Delete Key",
command=self.delete_key
)
delete_button.pack(side=tk.LEFT, padx=5)
self.status_label = ttk.Label(
main_frame,
text="",
font=('Arial', 10)
)
self.status_label.pack(pady=10)
instruction_label = ttk.Label(
main_frame,
text="Tip: Press 1-9 to quickly copy the corresponding key.",
font=('Arial', 9, 'italic'),
foreground='#aaaaaa'
)
instruction_label.pack(pady=(0, 5))
for i in range(1, 10):
self.root.bind(f"", self._on_number_key_press)
def style_dark_mode(self):
"""Applies a dark theme to the Tkinter window and widgets."""
self.root.tk_setPalette(background='#1e1e1e', foreground='white',
activeBackground='#4a4a4a', activeForeground='white')
style = ttk.Style()
style.theme_use('clam')
style.configure('.', font=('Arial', 10), background='#1e1e1e', foreground='white')
style.configure('TFrame', background='#1e1e1e')
style.configure('TLabel', background='#1e1e1e', foreground='white')
style.configure('Title.TLabel', font=('Arial', 18, 'bold'))
style.configure('Status.TLabel', foreground='#00ff00')
style.configure('TButton',
background='#3c3f41',
foreground='white',
font=('Arial', 10, 'bold'),
borderwidth=0,
focusthickness=3,
focuscolor='#007acc')
style.map('TButton',
background=[('active', '#007acc'), ('!active', '#3c3f41')],
foreground=[('active', 'white'), ('!active', 'white')])
style.configure("Vertical.TScrollbar", troughcolor='#1e1e1e', background='#555555',
gripcount=0, borderwidth=0)
style.map("Vertical.TScrollbar",
background=[('active', '#777777')])
def show_status(self, message, color='#00ff00'):
"""Displays a temporary status message."""
self.status_label.config(text=message, foreground=color)
# Reset color to default (white) after some time
self.root.after(2000, lambda: self.status_label.config(text="", foreground='white'))
def load_keys(self):
"""Loads API keys from a JSON file."""
if os.path.exists(self.keys_file):
try:
with open(self.keys_file, 'r') as f:
return json.load(f)
except json.JSONDecodeError:
messagebox.showerror("Error", "Could not load API keys. The file might be corrupted.")
return {}
return {}
def save_keys(self):
"""Saves API keys to a JSON file."""
try:
with open(self.keys_file, 'w') as f:
json.dump(self.api_keys, f, indent=4)
except Exception as e:
messagebox.showerror("Error", f"Failed to save API keys: {e}")
def update_keys_listbox(self):
"""Updates the listbox with current API keys."""
self.keys_listbox.delete(0, tk.END)
if not self.api_keys:
self.keys_listbox.insert(tk.END, "No API keys stored. Add one using 'Add Key'.")
self.keys_listbox.itemconfig(0, {'fg': '#888888'}) # Gray out the placeholder text
return
for i, (name, key) in enumerate(self.api_keys.items()):
# Mask the key for display
masked_key = key[:5] + '*' * (len(key) - 10) + key[-5:] if len(key) > 10 else '*' * len(key)
self.keys_listbox.insert(tk.END, f"{i+1}. {name}: {masked_key}")
def add_key(self):
"""Adds a new API key."""
name = simpledialog.askstring(
"Add API Key",
"Enter key name (e.g., 'OpenAI', 'Google'):",
parent=self.root
)
if name:
name = name.strip()
if not name:
self.show_status("Key name cannot be empty.", color='#ffcc00')
return
if name in self.api_keys:
messagebox.showwarning("Warning", f"A key with the name '{name}' already exists. Please choose a different name or delete the existing key.")
return
key = simpledialog.askstring(
"Add API Key",
"Enter API key:",
parent=self.root
)
if key:
key = key.strip()
if not key:
self.show_status("API key cannot be empty.", color='#ffcc00')
return
self.api_keys[name] = key
self.save_keys()
self.update_keys_listbox()
self.show_status("Key added successfully.", color='#00ff00')
else:
self.show_status("Key addition cancelled.", color='#ffcc00')
else:
self.show_status("Key addition cancelled.", color='#ffcc00')
def copy_key(self, index=None):
"""Copies the selected key to the clipboard.
If `index` is provided, copies the key at that index.
Otherwise, copies the currently selected key in the listbox.
"""
try:
if index is None:
selected_indices = self.keys_listbox.curselection()
if not selected_indices:
self.show_status("Please select a key to copy.", color='#ffcc00')
return
index = selected_indices[0]
if 0 <= index < len(self.api_keys):
key = list(self.api_keys.values())[index]
pyperclip.copy(key)
self.show_status(f"Copied key '{list(self.api_keys.keys())[index]}'.", color='#00ff00')
self.keys_listbox.selection_clear(0, tk.END)
self.keys_listbox.selection_set(index)
self.keys_listbox.activate(index)
else:
self.show_status("Invalid key index.", color='#ff0000')
except IndexError:
self.show_status("Please select a key to copy.", color='#ffcc00')
except Exception as e:
self.show_status(f"Error copying key: {e}", color='#ff0000')
def delete_key(self):
"""Deletes the selected API key."""
try:
selected_index = self.keys_listbox.curselection()
if not selected_index:
self.show_status("Please select a key to delete.", color='#ffcc00')
return
index = selected_index[0]
key_name_to_delete = list(self.api_keys.keys())[index]
if messagebox.askyesno("Confirm Deletion", f"Are you sure you want to delete the key '{key_name_to_delete}'?"):
del self.api_keys[key_name_to_delete]
self.save_keys()
self.update_keys_listbox()
self.show_status(f"Key '{key_name_to_delete}' deleted.", color='#00ff00')
else:
self.show_status("Deletion cancelled.", color='#ffcc00')
except IndexError:
self.show_status("Please select a key to delete.", color='#ffcc00')
except Exception as e:
self.show_status(f"Error deleting key: {e}", color='#ff0000')
def _on_number_key_press(self, event):
"""Handles numeric key presses (1-9) to copy a key."""
try:
key_number = int(event.char)
if 1 <= key_number <= 9:
self.copy_key(key_number - 1)
except ValueError:
pass
except Exception as e:
self.show_status(f"Key press error: {e}", color='#ff0000')
def main():
root = tk.Tk()
app = APIKeyManager(root)
root.mainloop()
if __name__ == "__main__":
main()