-
-
Notifications
You must be signed in to change notification settings - Fork 347
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
Python API support for passing an API key to a model #744
Comments
This is going to be a fiddle. Here's how Lines 639 to 659 in f67c215
Also relevant: Lines 386 to 398 in f67c215
So I have two options here - I could stash the optional key on the Either way, the Lines 567 to 591 in f67c215
But now we need that mechanism to have visibility into either the |
New idea! What if I use dependency injection here? If your plugin provides a That way I can upgrade plugins for the new mechanism. |
... in which case the underlying code implementation will be for that |
I have a working prototype for this now. Need to document it, test it and also decide what to do about conversations as opposed to just straight up |
Here's that prototype sketch, it's making diff --git a/llm/default_plugins/openai_models.py b/llm/default_plugins/openai_models.py
index 0a9dab2..177660f 100644
--- a/llm/default_plugins/openai_models.py
+++ b/llm/default_plugins/openai_models.py
@@ -474,7 +474,7 @@ class _Shared:
input=input_tokens, output=output_tokens, details=simplify_usage_dict(usage)
)
- def get_client(self, async_=False):
+ def get_client(self, *, async_=False, key=None):
kwargs = {}
if self.api_base:
kwargs["base_url"] = self.api_base
@@ -485,7 +485,7 @@ class _Shared:
if self.api_engine:
kwargs["engine"] = self.api_engine
if self.needs_key:
- kwargs["api_key"] = self.get_key()
+ kwargs["api_key"] = self.get_key(key)
else:
# OpenAI-compatible models don't need a key, but the
# openai client library requires one
@@ -522,12 +522,12 @@ class Chat(_Shared, Model):
default=None,
)
- def execute(self, prompt, stream, response, conversation=None):
+ def execute(self, prompt, stream, response, conversation=None, key=None):
if prompt.system and not self.allows_system_prompt:
raise NotImplementedError("Model does not support system prompts")
messages = self.build_messages(prompt, conversation)
kwargs = self.build_kwargs(prompt, stream)
- client = self.get_client()
+ client = self.get_client(key=key)
usage = None
if stream:
completion = client.chat.completions.create(
@@ -574,13 +574,13 @@ class AsyncChat(_Shared, AsyncModel):
)
async def execute(
- self, prompt, stream, response, conversation=None
+ self, prompt, stream, response, conversation=None, key=None
) -> AsyncGenerator[str, None]:
if prompt.system and not self.allows_system_prompt:
raise NotImplementedError("Model does not support system prompts")
messages = self.build_messages(prompt, conversation)
kwargs = self.build_kwargs(prompt, stream)
- client = self.get_client(async_=True)
+ client = self.get_client(async_=True, key=key)
usage = None
if stream:
completion = await client.chat.completions.create(
diff --git a/llm/models.py b/llm/models.py
index 2bed85e..25b23a9 100644
--- a/llm/models.py
+++ b/llm/models.py
@@ -5,6 +5,7 @@ import datetime
from .errors import NeedsKeyException
import hashlib
import httpx
+import inspect
from itertools import islice
import re
import time
@@ -204,11 +205,13 @@ class _BaseResponse:
model: "_BaseModel",
stream: bool,
conversation: Optional[_BaseConversation] = None,
+ key: Optional[str] = None,
):
self.prompt = prompt
self._prompt_json = None
self.model = model
self.stream = stream
+ self._key = key
self._chunks: List[str] = []
self._done = False
self.response_json = None
@@ -390,12 +393,15 @@ class Response(_BaseResponse):
yield from self._chunks
return
- for chunk in self.model.execute(
- self.prompt,
- stream=self.stream,
- response=self,
- conversation=self.conversation,
- ):
+ kwargs = {
+ "stream": self.stream,
+ "response": self,
+ "conversation": self.conversation,
+ }
+ if _accepts_parameter(self.model.execute, "key"):
+ kwargs["key"] = self.model.get_key(self._key)
+
+ for chunk in self.model.execute(self.prompt, **kwargs):
yield chunk
self._chunks.append(chunk)
@@ -447,12 +453,14 @@ class AsyncResponse(_BaseResponse):
return chunk
if not hasattr(self, "_generator"):
- self._generator = self.model.execute(
- self.prompt,
- stream=self.stream,
- response=self,
- conversation=self.conversation,
- )
+ kwargs = {
+ "stream": self.stream,
+ "response": self,
+ "conversation": self.conversation,
+ }
+ if _accepts_parameter(self.model.execute, "key"):
+ kwargs["key"] = self.model.get_key(self._key)
+ self._generator = self.model.execute(self.prompt, **kwargs)
try:
chunk = await self._generator.__anext__()
@@ -564,7 +572,7 @@ _Options = Options
class _get_key_mixin:
- def get_key(self):
+ def get_key(self, explicit_key: Optional[str] = None) -> Optional[str]:
from llm import get_key
if self.needs_key is None:
@@ -577,7 +585,9 @@ class _get_key_mixin:
# Attempt to load a key using llm.get_key()
key = get_key(
- explicit_key=None, key_alias=self.needs_key, env_var=self.key_env_var
+ explicit_key=explicit_key,
+ key_alias=self.needs_key,
+ env_var=self.key_env_var,
)
if key:
return key
@@ -633,6 +643,7 @@ class Model(_BaseModel):
stream: bool,
response: Response,
conversation: Optional[Conversation],
+ key: Optional[str] = None,
) -> Iterator[str]:
pass
@@ -643,6 +654,7 @@ class Model(_BaseModel):
attachments: Optional[List[Attachment]] = None,
system: Optional[str] = None,
stream: bool = True,
+ key: Optional[str] = None,
**options,
) -> Response:
self._validate_attachments(attachments)
@@ -656,6 +668,7 @@ class Model(_BaseModel):
),
self,
stream,
+ key=key,
)
@@ -670,6 +683,7 @@ class AsyncModel(_BaseModel):
stream: bool,
response: AsyncResponse,
conversation: Optional[AsyncConversation],
+ key: Optional[str] = None,
) -> AsyncGenerator[str, None]:
yield ""
@@ -680,6 +694,7 @@ class AsyncModel(_BaseModel):
attachments: Optional[List[Attachment]] = None,
system: Optional[str] = None,
stream: bool = True,
+ key: Optional[str] = None,
**options,
) -> AsyncResponse:
self._validate_attachments(attachments)
@@ -693,6 +708,7 @@ class AsyncModel(_BaseModel):
),
self,
stream,
+ key=key,
)
@@ -780,3 +796,7 @@ def _conversation_name(text):
if len(text) <= CONVERSATION_NAME_LENGTH:
return text
return text[: CONVERSATION_NAME_LENGTH - 1] + "…"
+
+
+def _accepts_parameter(callable: Callable, parameter: str) -> bool:
+ return parameter in inspect.signature(callable).parameters |
Also need to consider if this design should apply to embedding methods too. It probably should. |
Documentation for this will go in advanced model plugins: https://llm.datasette.io/en/stable/plugins/advanced-model-plugins.html |
After much digging I don't think it's possible to define an ABC with a Options:
|
This seems to work though: from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def execute(self, **kwargs):
print(kwargs)
class One(Base):
def execute(self, **kwargs):
print("One", kwargs)
class Two(Base):
def execute(self, a, b):
print("Two", a, b)
if __name__ == "__main__":
one = One()
one.execute(a=1, b=2)
two = Two()
two.execute(a=1, b=2)
|
I don't understand why this file passes from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def execute(self, a: int, b: int):
print(a, b)
class Two(Base):
def execute(self, a, b, d):
print("Two", a, b, d)
if __name__ == "__main__":
two = Two()
two.execute(a=1, b=2, d=4)
|
... figured that out. If I explicitly tell it that from abc import ABC, abstractmethod
class Base(ABC):
@abstractmethod
def execute(self, a: int, b: int):
print(a, b)
class Two(Base):
def execute(self, a, b, d):
print("Two", a, b, d)
if __name__ == "__main__":
two: Base = Two()
two.execute(a=1, b=2, d=4)
|
I tried to solve this with |
The best option at this point may be to abandon |
Or... how about if I have a Then I could have the code that calls |
For some applications it may be useful to provide API keys for models at runtime when a prompt is executed. That's not very easy right now - you can set
model.key = "x"
but that's then shared across all uses of that model instance. This is bad for multi-user environments like web applications, plus there's no easy way to create new model instances -llm.get_model(model_id)
returns a single shared object.I don't want to break existing code here, so I'm going to make this a new optional argument to
model.prompt("prompt", key=...)
.The text was updated successfully, but these errors were encountered: