-
-
Notifications
You must be signed in to change notification settings - Fork 335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Better ways of storing and accessing API keys #13
Comments
Another pattern I'm seeing for configuring API keys is in the (excellent and free) DeepLearning.AI Short Courses. They rely on the dotenv module, and allow more flexible configuration, allowing other API keys to be set.
That reads the API keys from the So unfortunately you can't use the same file for both mechanisms. Note if you try to reuse the
I hope they provide a better error message via the discussion at openai/openai-python#464 Back to llm: I suggest supporting (or even switching to) the dotenv approach, since I dare say that people will want to specify multiple API keys for the burgeoning variety of LLMs and plugins out there. |
This is now blocking this issue, because I need to solve it for other API key providers too: |
I'm going to start storing keys in I think I'll leave people who want to use You can do |
I'm taking inspiration from how Vercel does this: https://til.simonwillison.net/macos/fs-usage
I'm going to use the same filename - I'll use different keys for the tokens though. Each command/provider will have a default key - "openai" for OpenAI, something else for the PaLM ones. When using
This design ensures people can over-ride their current environment variable if they want to by specifying |
I'm not going to have a |
How do users get their tokens into the system? They can edit
|
Now I'm torn on naming. It would be good if the filename and the commands and the concepts were all consistent with each other. Some options:
I worry that "tokens" are less obviously secret than the others.
Keys do at least have a clear implication that they should be protected.
I feel like secrets could be misunderstood to mean some other concept. |
What do you think of using I know you have And then having |
OpenAI call them "API keys". Google call them all sorts of things - I'm going to call them keys, and go with this:
|
Yes! That's a great reason not to use "tokens" to mean authentication tokens, since tokens already means something different in LLM space. |
I'm going with:
On macOS at least - that's generated using: def keys_path():
return os.path.join(user_dir(), "keys.json")
def user_dir():
return user_data_dir("io.datasette.llm", "Datasette") I plan to move the docs for this tool to |
Partly for unit testing convenience I'm going to allow the path to |
Prototype - this seems to work well: diff --git a/llm/cli.py b/llm/cli.py
index 37dd9ed..b7d7112 100644
--- a/llm/cli.py
+++ b/llm/cli.py
@@ -1,9 +1,13 @@
import click
from click_default_group import DefaultGroup
import datetime
+import getpass
import json
import openai
import os
+import pathlib
+from platformdirs import user_data_dir
+import requests
import sqlite_utils
import sys
import warnings
@@ -124,6 +128,50 @@ def init_db():
db.vacuum()
+@cli.group()
+def keys():
+ "Manage API keys for different models"
+
+
+@keys.command()
+def path():
+ "Output path to keys.json file"
+ click.echo(keys_path())
+
+
+def keys_path():
+ return os.path.join(user_dir(), "keys.json")
+
+
+def user_dir():
+ return user_data_dir("io.datasette.llm", "Datasette")
+
+
+@keys.command(name="set")
+@click.argument("name")
+def set_(name):
+ """
+ Save a key in keys.json
+
+ Example usage:
+
+ $ llm keys set openai
+ Enter key: ...
+ """
+ default = {"// Note": "This file stores secret API credentials. Do not share!"}
+ path = pathlib.Path(keys_path())
+ path.parent.mkdir(parents=True, exist_ok=True)
+ value = getpass.getpass("Enter key: ").strip()
+ if not path.exists():
+ path.write_text(json.dumps(default))
+ try:
+ current = json.loads(path.read_text())
+ except json.decoder.JSONDecodeError:
+ current = default
+ current[name] = value
+ path.write_text(json.dumps(current, indent=2) + "\n")
+
+
@cli.command()
@click.option(
"-n",
diff --git a/setup.py b/setup.py
index dc75536..b36718b 100644
--- a/setup.py
+++ b/setup.py
@@ -31,7 +31,13 @@ setup(
[console_scripts]
llm=llm.cli:cli
""",
- install_requires=["click", "openai", "click-default-group-wheel", "sqlite-utils"],
+ install_requires=[
+ "click",
+ "openai",
+ "click-default-group-wheel",
+ "sqlite-utils",
+ "platformdirs",
+ ],
extras_require={"test": ["pytest", "requests-mock"]},
python_requires=">=3.7",
) |
def keys_path():
return os.path.join(user_dir(), "keys.json") Since you're using pathlib later in the code, what do you think of making pathlib the default lib to handle it? The same result can be achieved with I suggested the generalization of pathlib in #19 😊. |
OK, I implemented these two commands:
|
Still needed:
|
Good call, I'll do that as part of cleaning up this code. |
This is implemented, next step is to write the docs for it - which is blocked on: |
Also documents new keys.json mechanism, closes #13
I'm not sure this is actually an issue, as I've developed a workaround, but I thought it was worth bringing up for discussion.
I prefer to keep keys like this in my password manager. Among other things, it allows secure access and consistent sync across machines. I already had a function to access the key, but I don't want to call it on every new shell session as it pops up a prompt in my password manager. I'd prefer to only do that when using the tool, and only the first time in the shell session.
So, I wrote a wrapper function to do that:
I use zsh on macOS.
I'm not aware of other patterns that
llm
could use to look for a key, and you already provide two reasonable ones...but if there was a third way that would obviate the need for my little wrapper, that would be cool! Otherwise, maybe someone else finds this helpful.The text was updated successfully, but these errors were encountered: