about summary refs log tree commit diff
path: root/kaakaa/openai.scm
blob: 08a7254df63f21eba32c5883e83d4d8a93893907 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
;;; kaakaa --- Tiny, security-focused AI agent in Guile
;;; Copyright © 2026 Arun Isaac <arunisaac@systemreboot.net>
;;;
;;; This file is part of kaakaa.
;;;
;;; kaakaa is free software: you can redistribute it and/or modify it
;;; under the terms of the GNU General Public License as published by
;;; the Free Software Foundation, either version 3 of the License, or
;;; (at your option) any later version.
;;;
;;; kaakaa is distributed in the hope that it will be useful, but
;;; WITHOUT ANY WARRANTY; without even the implied warranty of
;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
;;; General Public License for more details.
;;;
;;; You should have received a copy of the GNU General Public License
;;; along with kaakaa.  If not, see <https://www.gnu.org/licenses/>.

(define-module (kaakaa openai)
  #:use-module (rnrs conditions)
  #:use-module (rnrs exceptions)
  #:use-module (srfi srfi-11)
  #:use-module (web client)
  #:use-module (web http)
  #:use-module (web response)
  #:use-module (json)
  #:export (get-api-key
            openai-query))

;; TODO: URIs must not be operated on using string operations. Replace with a
;; more principled implementation involving (web uri).
(define (uri-join base uri)
  (string-append base uri))

(define* (json-post url #:key headers json)
  "Send a POST request to @var{url} with @var{json} body and additional
@var{headers}. The @samp{Content-Type} header is set to @samp{application/json}
and need not be specified in @var{headers}. Return JSON response."
  (let-values (((response body)
                (http-post url
                           #:headers `((content-type application/json)
                                       ,@headers)
                           #:body (scm->json-string json)
                           #:streaming? #t)))
    ;; Guile does not consider application/json responses as textual, and does
    ;; not automatically set the port encoding to UTF-8.
    (set-port-encoding! body "UTF-8")
    (case (quotient (response-code response)
                    100)
      ((2) (json->scm body))
      ((4)
       (raise-exception
        (condition (make-violation)
                   (make-message-condition
                    (string-append "JSON API request failed with client error code "
                                   (number->string (response-code response)))))))
      (else
       (raise-exception
        (condition (make-error)
                   (make-message-condition
                    (string-append "JSON API request failed with code "
                                   (number->string (response-code response))))))))))

;; Declare the Authorization header as opaque so that Guile doesn't try to mess
;; with it.
(declare-opaque-header! "Authorization")

(define (openai-query base-uri api-key model messages tools)
  "Send a request to the OpenAI completions API and return the JSON response.
@var{base-uri} is the base URI of the OpenAI-compatible service. @var{api-key}
is the API key for authentication. @var{model} is a supported LLM model.
@var{messages} and @var{tools} are respectively lists of JSON messages and tools
compatible with the OpenAI API specification."
  (json-post (uri-join base-uri "/api/v1/chat/completions")
             #:headers `((authorization
                          . ,(string-append "Bearer " api-key)))
             #:json `(("model" . ,model)
                      ("messages" . ,(list->vector messages))
                      ("tools" . ,(list->vector tools)))))