diff --git a/README.md b/README.md index 17d8c55..67bafb9 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,14 @@ You will need to set up your API key before you can use this library. (setq openai-key "[YOUR API KEY]") ``` +Alternatively you can configure a function to retrieve the key from some +external source. A function, `openai-key-auth-source` is provided to retrieve +the key from an auth-source entry under the `:host` key `api.openai.com` + +```elisp +(setq openai-key 'openai-key-auth-source) +``` + For requests that need your user identifier, ```elisp diff --git a/openai-audio.el b/openai-audio.el index 16b0371..341ecea 100644 --- a/openai-audio.el +++ b/openai-audio.el @@ -34,7 +34,6 @@ ;;;###autoload (cl-defun openai-audio-create-transcription ( file callback &key - (key openai-key) (model "whisper-1") prompt response-format @@ -54,8 +53,6 @@ for more information. Arguments here refer to MODEL PROMPT, RESPONSE-FORMAT, TEMPERATURE, and LANGUAGE." (openai-request "https://api.openai.com/v1/audio/transcriptions" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("file" . ,file) @@ -63,7 +60,6 @@ TEMPERATURE, and LANGUAGE." ("response_format" . ,response-format) ("temperature" . ,temperature) ("language" . ,language))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) @@ -71,7 +67,6 @@ TEMPERATURE, and LANGUAGE." ;;;###autoload (cl-defun openai-audio-create-translation ( file callback &key - (key openai-key) (model "whisper-1") prompt response-format @@ -89,15 +84,12 @@ for more information. Arguments here refer to MODEL PROMPT, RESPONSE-FORMAT, and TEMPERATURE." (openai-request "https://api.openai.com/v1/audio/transcriptions" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("file" . ,file) ("prompt" . ,prompt) ("response_format" . ,response-format) ("temperature" . ,temperature))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-chat.el b/openai-chat.el index c2c84e5..a8b11ae 100644 --- a/openai-chat.el +++ b/openai-chat.el @@ -34,7 +34,6 @@ ;;;###autoload (cl-defun openai-chat ( messages callback &key - (key openai-key) (model "gpt-3.5-turbo") temperature top-p @@ -59,8 +58,6 @@ for more information. Arguments here refer to MODEL, TEMPERATURE, TOP-P, N, STREAM, STOP, MAX-TOKENS, PRESENCE-PENALTY, FREQUENCY-PENALTY, and LOGIT-BIAS." (openai-request "https://api.openai.com/v1/chat/completions" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("messages" . ,messages) @@ -74,7 +71,6 @@ STREAM, STOP, MAX-TOKENS, PRESENCE-PENALTY, FREQUENCY-PENALTY, and LOGIT-BIAS." ("frequency_penalty" . ,frequency-penalty) ("logit_bias" . ,logit-bias) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-completion.el b/openai-completion.el index 80db56e..929a095 100644 --- a/openai-completion.el +++ b/openai-completion.el @@ -34,7 +34,6 @@ ;;;###autoload (cl-defun openai-completion ( prompt callback &key - (key openai-key) (model "text-davinci-003") suffix max-tokens @@ -65,8 +64,6 @@ TEMPERATURE, TOP-P, N, STREAM, LOGPROBS, ECHO, STOP, PRESENCE-PENALTY, FREQUENCY-PENALTY, BEST-OF, and LOGIT-BIAS." (openai-request "https://api.openai.com/v1/completions" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("prompt" . ,prompt) @@ -84,7 +81,6 @@ FREQUENCY-PENALTY, BEST-OF, and LOGIT-BIAS." ("best_of" . ,best-of) ("logit_bias" . ,logit-bias) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-edit.el b/openai-edit.el index ce7b220..765e331 100644 --- a/openai-edit.el +++ b/openai-edit.el @@ -34,7 +34,6 @@ (cl-defun openai-edit-create ( input instruction callback &key - (key openai-key) (model "text-davinci-edit-001") temperature top-p @@ -53,8 +52,6 @@ The rest of the arugments are optional, please see OpenAI API reference page for more information. Arguments here refer to TEMPERATURE, TOP-P, and N." (openai-request "https://api.openai.com/v1/edits" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("input" . ,input) @@ -62,7 +59,6 @@ for more information. Arguments here refer to TEMPERATURE, TOP-P, and N." ("temperature" . ,temperature) ("top_p" . ,top-p) ("n" . ,n))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-embedding.el b/openai-embedding.el index 1ebd828..3d4e3ab 100644 --- a/openai-embedding.el +++ b/openai-embedding.el @@ -34,7 +34,6 @@ (cl-defun openai-embedding-create ( input callback &key - (key openai-key) (model "text-embedding-ada-002") (user openai-user)) "Creates an embedding vector representing the input text. @@ -53,13 +52,10 @@ The rest of the arugments are optional, please see OpenAI API reference page for more information. Arguments here refer to MODEL." (openai-request "https://api.openai.com/v1/embeddings" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("input" . ,input) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-engine.el b/openai-engine.el index 759d6bc..70d906f 100644 --- a/openai-engine.el +++ b/openai-engine.el @@ -34,9 +34,7 @@ ;; ;;; API -(cl-defun openai-engine-list ( callback - &key - (key openai-key)) +(cl-defun openai-engine-list (callback) "Lists the currently available (non-finetuned) models, and provides basic information about each one such as the owner and availability. @@ -46,16 +44,11 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/engines" :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-engine-retrieve ( engine-id callback - &key - (key openai-key)) +(cl-defun openai-engine-retrieve (engine-id callback) "Retrieves a model instance, providing basic information about it such as the owner and availability. @@ -67,9 +60,6 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request (format "https://api.openai.com/v1/engines/%s" engine-id) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-file.el b/openai-file.el index 9844460..f0c017d 100644 --- a/openai-file.el +++ b/openai-file.el @@ -32,9 +32,7 @@ ;; ;;; API -(cl-defun openai-file-list ( callback - &key - (key openai-key)) +(cl-defun openai-file-list (callback) "Return a list of files that belong to the user's organization. The argument CALLBACK is execuated after request is made. @@ -43,16 +41,11 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/files" :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-file-upload ( file purpose callback - &key - (key openai-key)) +(cl-defun openai-file-upload (file purpose callback) "Upload a file that contain document(s) to be used across various endpoints/features. @@ -72,19 +65,14 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/files" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("file" . ,file) ("purpose" . ,purpose))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-file-delete ( file-id callback - &key - (key openai-key)) +(cl-defun openai-file-delete (file-id callback) "Delete a file. The arument FILE-ID is id of the file to use for this request. @@ -95,18 +83,13 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/files" :type "DELETE" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("file_id" . ,file-id))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-file-retrieve ( file-id callback - &key - (key openai-key)) +(cl-defun openai-file-retrieve (file-id callback) "Return information about a specific file. The arument FILE-ID is id of the file to use for this request. @@ -117,18 +100,13 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request (format "https://api.openai.com/v1/files/%s" file-id) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("file_id" . ,file-id))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-file-retrieve-content ( file-id callback - &key - (key openai-key)) +(cl-defun openai-file-retrieve-content (file-id callback) "Return the contents of the specified file The arument FILE-ID is id of the file to use for this request. @@ -139,11 +117,8 @@ Arguments KEY is global option; however, you can overwrite the value by passing it in." (openai-request (format "https://api.openai.com/v1/files/%s/content" file-id) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("file_id" . ,file-id))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-fine-tune.el b/openai-fine-tune.el index a426a41..4daeb89 100644 --- a/openai-fine-tune.el +++ b/openai-fine-tune.el @@ -33,7 +33,6 @@ (cl-defun openai-fine-tune-create ( training-file callback &key - (key openai-key) (model "curie") validation-file n-epochs @@ -65,8 +64,6 @@ COMPUTE-CLASSIFICATION-METRICS, CLASSIFICATION-N-CLASSES, CLASSIFICATION-POSITIVE-CLASS, CLASSIFICATION-BETAS, and SUFFIX" (openai-request "https://api.openai.com/v1/fine-tunes" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("training_file" . ,training-file) @@ -80,29 +77,21 @@ CLASSIFICATION-POSITIVE-CLASS, CLASSIFICATION-BETAS, and SUFFIX" ("classification_positive_class" . ,classification-positive-class) ("classification_betas" . ,classification-betas) ("suffix" . ,suffix))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-fine-tune-list ( callback - &key - (key openai-key)) +(cl-defun openai-fine-tune-list (callback) "List your organization's fine-tuning jobs. The argument CALLBACK is execuated after request is made." (openai-request "https://api.openai.com/v1/fine-tunes" :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-fine-tune-retrieve ( fine-tune-id callback - &key - (key openai-key)) +(cl-defun openai-fine-tune-retrieve (fine-tune-id callback) "Gets info about the fine-tune job. The FINE-TUNE-ID of the fine-tune job. @@ -110,16 +99,11 @@ The FINE-TUNE-ID of the fine-tune job. The argument CALLBACK is execuated after request is made." (openai-request (format "https://api.openai.com/v1/fine-tunes/%s" fine-tune-id) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-fine-tune-cancel ( fine-tune-id callback - &key - (key openai-key)) +(cl-defun openai-fine-tune-cancel (fine-tune-id callback) "Immediately cancel a fine-tune job. The FINE-TUNE-ID of the fine-tune job to cancel. @@ -127,16 +111,11 @@ The FINE-TUNE-ID of the fine-tune job to cancel. The argument CALLBACK is execuated after request is made." (openai-request (format "https://api.openai.com/v1/fine-tunes/%s/cancel" fine-tune-id) :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-fine-tune-list-events ( fine-tune-id callback - &key - (key openai-key)) +(cl-defun openai-fine-tune-list-events (fine-tune-id callback) "Get fine-grained status updates for a fine-tune job. The FINE-TUNE-ID of the fine-tune job to get events for. @@ -144,16 +123,11 @@ The FINE-TUNE-ID of the fine-tune job to get events for. The argument CALLBACK is execuated after request is made." (openai-request (format "https://api.openai.com/v1/fine-tunes/%s/events" fine-tune-id) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-fine-tune-delete ( model callback - &key - (key openai-key)) +(cl-defun openai-fine-tune-delete (model callback) "Delete a fine-tuned model. You must have the Owner role in your organization. The MODEL to delete. @@ -161,9 +135,6 @@ The MODEL to delete. The argument CALLBACK is execuated after request is made." (openai-request (format "https://api.openai.com/v1/models/%s" model) :type "DELETE" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-image.el b/openai-image.el index 2f7a95d..29fadd3 100644 --- a/openai-image.el +++ b/openai-image.el @@ -33,7 +33,6 @@ (cl-defun openai-image ( prompt callback &key - (key openai-key) n size response-format @@ -51,22 +50,18 @@ The rest of the arugments are optional, please see OpenAI API reference page for more information. Arguments here refer to N, SIZE, and RESPONSE-FORMAT." (openai-request "https://api.openai.com/v1/images/generations" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("prompt" . ,prompt) ("n" . ,n) ("size" . ,size) ("response_format" . ,response-format) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) (cl-defun openai-image-edit ( image prompt callback &key - (key openai-key) mask n size @@ -86,7 +81,6 @@ for more information. Arguments here refer to MASK, N, SIZE, and RESPONSE-FORMAT." (openai-request "https://api.openai.com/v1/images/edits" :type "POST" - :headers `(("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("image" . ,image) ("prompt" . ,prompt) @@ -95,14 +89,12 @@ RESPONSE-FORMAT." ("size" . ,size) ("response_format" . ,response-format) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) (cl-defun openai-image-variation ( image callback &key - (key openai-key) mask n size @@ -121,7 +113,6 @@ for more information. Arguments here refer to MASK, N, SIZE, and RESPONSE-FORMAT." (openai-request "https://api.openai.com/v1/images/variations" :type "POST" - :headers `(("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("image" . ,image) ("mask" . ,mask) @@ -129,7 +120,6 @@ RESPONSE-FORMAT." ("size" . ,size) ("response_format" . ,response-format) ("user" . ,user))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-model.el b/openai-model.el index 14533f0..9e820bf 100644 --- a/openai-model.el +++ b/openai-model.el @@ -29,34 +29,24 @@ ;; ;;; API -(cl-defun openai-models ( callback - &key - (key openai-key)) +(cl-defun openai-models (callback) "Return models data and execute the CALLBACK. Arguments KEY is global options; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/models" :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) -(cl-defun openai-model ( model callback - &key - (key openai-key)) +(cl-defun openai-model (model callback) "Return MODEL data and execute the CALLBACK. Arguments KEY is global options; however, you can overwrite the value by passing it in." (openai-request (format "https://api.openai.com/v1/models/%s" model) :type "GET" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai-moderation.el b/openai-moderation.el index ce5b6d1..b42bb68 100644 --- a/openai-moderation.el +++ b/openai-moderation.el @@ -34,7 +34,6 @@ (cl-defun openai-moderation-create ( input callback &key - (key openai-key) (model "text-moderation-latest")) "Classifies if text violates OpenAI's Content Policy. @@ -46,12 +45,9 @@ Arguments KEY is global options; however, you can overwrite the value by passing it in." (openai-request "https://api.openai.com/v1/embeddings" :type "POST" - :headers `(("Content-Type" . "application/json") - ("Authorization" . ,(concat "Bearer " key))) :data (openai--json-encode `(("model" . ,model) ("input" . ,input))) - :parser 'json-read :complete (cl-function (lambda (&key data &allow-other-keys) (funcall callback data))))) diff --git a/openai.el b/openai.el index b50cd95..28f83b6 100644 --- a/openai.el +++ b/openai.el @@ -31,6 +31,7 @@ ;;; Code: +(require 'auth-source) (require 'cl-lib) (require 'let-alist) (require 'pcase) @@ -60,13 +61,37 @@ ;; ;;; Request +;;;###autoload +(defun openai-key-auth-source () + "Retrieve the OpenAI API key from auth-source." + (if-let ((auth-info (auth-source-search :max 1 + :host "api.openai.com" + :require '(:user :secret)))) + (funcall (plist-get (car auth-info) :secret)) + (error "OpenAI API key not found in auth-source"))) + + + (defvar openai-key "" - "Generated API key.") + "Variable storing the openai key or a function name to retrieve it. + +The function should take no arguments and return a string containing the key. + +A function, `openai-key-auth-source', that retrieves the key from +auth-source is provided for convenience.") (defvar openai-user "" "A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse.") +(defun openai--resolve-key (key) + "If the given KEY is a function call it and return the result, +otherwise return KEY." + (cond + ((functionp key) (funcall key)) + ((not (string-empty-p key)) key) + (t (user-error "[INFO] Invalid API key, please set it to the correct value: %s" openai-key)))) + (defun openai--json-encode (object) "Wrapper for function `json-encode' but it removes `nil' value before constructing JSON data. @@ -99,23 +124,37 @@ See https://beta.openai.com/docs/guides/error-codes/api-errors." (defvar openai-error nil "Records for the last error.") +(defun openai--add-missing-defaults (p) + "Add Content-Type and Authorization headers to BODY if not present." + + (let* ((headers (plist-get p :headers)) + (headers (if (assoc-string "Content-Type" headers) headers + (cons `("Content-Type" . "application/json") headers))) + (headers (if (assoc-string "Authorization" headers) headers + (cons `("Authorization" . ,(concat "Bearer " (openai--resolve-key openai-key))) headers))) + (parser (if-let ((parser (plist-get p :parser))) parser + 'json-read))) + (thread-first p + (plist-put :parser parser) + (plist-put :headers headers)))) + (defmacro openai-request (url &rest body) "Wrapper for `request' function. The URL is the url for `request' function; then BODY is the arguments for rest." (declare (indent 1)) - `(if (string-empty-p openai-key) - (user-error "[INFO] Invalid API key, please set it to the correct value: %s" openai-key) + `(let ((args (plist-put + (list ,@body) + :error (cl-function + (lambda (&key response &allow-other-keys) + (setq openai-error response) + (openai--handle-error response)))))) (setq openai-error nil) - (request ,url - :error (cl-function - (lambda (&key response &allow-other-keys) - (setq openai-error response) - (openai--handle-error response))) - ,@body))) + (apply (symbol-function 'request) ,url + (openai--add-missing-defaults args)))) ;; -;;; Util +;;; d (defcustom openai-annotation-ratio 2.5 "Ratio align from the right to display `completin-read' annotation."