├── .gitignore ├── src ├── openai.gleam ├── openai │ ├── config.gleam │ ├── models.gleam │ └── completion │ │ ├── message.gleam │ │ └── chat.gleam ├── backend.gleam └── helper.gleam ├── README.md ├── gleam.toml ├── test └── openai_test.gleam └── manifest.toml /.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | /build 4 | erl_crash.dump 5 | .env 6 | -------------------------------------------------------------------------------- /src/openai.gleam: -------------------------------------------------------------------------------- 1 | import gleam/io 2 | 3 | pub fn main() { 4 | io.println("hellow orld") 5 | } 6 | -------------------------------------------------------------------------------- /src/openai/config.gleam: -------------------------------------------------------------------------------- 1 | import gleam/uri 2 | 3 | pub const version = "v1" 4 | 5 | pub type OpenAIConfig { 6 | OpenAIConfig(api_key: String, base_url: uri.Uri) 7 | } 8 | 9 | pub fn new(api_key: String) { 10 | let assert Ok(base_url) = uri.parse("https://api.openai.com/v1/") 11 | OpenAIConfig(api_key:, base_url:) 12 | } 13 | 14 | pub fn set_base_url(c: OpenAIConfig, url: String) { 15 | let assert Ok(base_url) = uri.parse(url) 16 | OpenAIConfig(..c, base_url:) 17 | } 18 | -------------------------------------------------------------------------------- /src/backend.gleam: -------------------------------------------------------------------------------- 1 | import gleam/http/request 2 | import gleam/httpc 3 | import gleam/result 4 | 5 | pub fn send(req: request.Request(String)) { 6 | let config = httpc.configure() 7 | httpc.dispatch(config, req) 8 | |> result.map_error(fn(e) { 9 | case e { 10 | httpc.FailedToConnect(_, _) -> 11 | "failed to request with httpc: failed to connect" 12 | httpc.InvalidUtf8Response -> 13 | "failed to request with httpc: invalid utf8 response" 14 | } 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenAI SDK for Gleam 2 | Wrapper around OpenAI API for Gleam types. 3 | 4 | ## Usage 5 | 6 | ### 1. List Models 7 | ```gleam 8 | let assert Ok(models) = config.new("your openai api key") 9 | |> models.list_models 10 | ``` 11 | 12 | ### 2. Chat Completion 13 | ```gleam 14 | let config = config.new("your api key") 15 | let assert Ok(res) = 16 | chat.new_params("gpt-4.1", [ 17 | message.user("hello, tell me about the Gleam languge") 18 | ]) 19 | |> chat.create(config) 20 | 21 | let assert Ok(choice) = res.choices |> list.first() 22 | io.println(choice.message.content) 23 | ``` 24 | -------------------------------------------------------------------------------- /gleam.toml: -------------------------------------------------------------------------------- 1 | name = "openai" 2 | version = "1.0.0" 3 | 4 | # Fill out these fields if you intend to generate HTML documentation or publish 5 | # your project to the Hex package manager. 6 | # 7 | # description = "" 8 | # licences = ["Apache-2.0"] 9 | # repository = { type = "github", user = "", repo = "" } 10 | # links = [{ title = "Website", href = "" }] 11 | # 12 | # For a full reference of all the available options, you can have a look at 13 | # https://gleam.run/writing-gleam/gleam-toml/. 14 | 15 | [dependencies] 16 | gleam_stdlib = ">= 0.44.0 and < 2.0.0" 17 | gleam_http = ">= 4.0.0 and < 5.0.0" 18 | gleam_httpc = ">= 4.1.1 and < 5.0.0" 19 | gleam_json = ">= 3.0.1 and < 4.0.0" 20 | dotenv = ">= 3.1.0 and < 4.0.0" 21 | dotenv_gleam = ">= 2.0.0 and < 3.0.0" 22 | envoy = ">= 1.0.2 and < 2.0.0" 23 | gleam_hackney = ">= 1.3.1 and < 2.0.0" 24 | 25 | [dev-dependencies] 26 | gleeunit = ">= 1.0.0 and < 2.0.0" 27 | -------------------------------------------------------------------------------- /src/helper.gleam: -------------------------------------------------------------------------------- 1 | import dotenv_gleam 2 | import gleam/http 3 | import gleam/http/request 4 | import gleam/http/response 5 | import gleam/option 6 | import gleam/string 7 | 8 | pub fn must_ok(r: Result(a, b)) -> a { 9 | let assert Ok(ok) = r 10 | ok 11 | } 12 | 13 | pub fn must_some(r: option.Option(a)) -> a { 14 | let assert option.Some(some) = r 15 | some 16 | } 17 | 18 | pub fn dot_env() { 19 | dotenv_gleam.config() 20 | } 21 | 22 | pub fn handle_positive( 23 | r: response.Response(String), 24 | cb: fn(response.Response(String)) -> Result(b, String), 25 | ) { 26 | case r.status { 27 | a if a >= 200 && a < 300 -> cb(r) 28 | _ -> { 29 | Error("nagative status code < 300, body: " <> r.body) 30 | } 31 | } 32 | } 33 | 34 | pub fn new_https_request() { 35 | request.new() 36 | |> request.set_scheme( 37 | http.scheme_from_string("https") 38 | |> must_ok, 39 | ) 40 | } 41 | 42 | pub fn do(s, f: fn(s) -> d) { 43 | f(s) 44 | } 45 | 46 | pub fn delete_last_slash(from: String) { 47 | case string.ends_with(from, "/") { 48 | False -> from 49 | True -> string.drop_end(from, 1) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/openai/models.gleam: -------------------------------------------------------------------------------- 1 | import backend 2 | import gleam/dynamic/decode 3 | import gleam/http 4 | import gleam/http/request 5 | import gleam/json 6 | import gleam/result 7 | import gleam/string 8 | import helper 9 | import openai/config 10 | 11 | pub type Models = 12 | List(String) 13 | 14 | /// Returns list of avaliable models 15 | pub fn list_models(c: config.OpenAIConfig) -> Result(Models, String) { 16 | let item_decoder = { 17 | use id <- decode.field("id", decode.string) 18 | decode.success(id) 19 | } 20 | let list_decoder = { 21 | use data <- decode.field("data", decode.list(item_decoder)) 22 | decode.success(data) 23 | } 24 | 25 | let req = 26 | request.new() 27 | |> request.set_scheme( 28 | http.scheme_from_string(c.base_url.scheme |> helper.must_some) 29 | |> helper.must_ok, 30 | ) 31 | |> request.set_host(c.base_url.host |> helper.must_some) 32 | |> request.set_path( 33 | [c.base_url.path |> helper.delete_last_slash, "models"] 34 | |> string.join("/"), 35 | ) 36 | |> request.set_header("Authorization", "Bearer " <> c.api_key) 37 | |> backend.send() 38 | 39 | use res <- result.try(req) 40 | use res <- helper.handle_positive(res) 41 | res.body 42 | // |> echo 43 | |> json.parse(list_decoder) 44 | |> result.map_error(fn(_) { "failed to parse models" }) 45 | } 46 | -------------------------------------------------------------------------------- /test/openai_test.gleam: -------------------------------------------------------------------------------- 1 | import envoy 2 | import gleam/hackney 3 | import gleam/http 4 | import gleam/http/request 5 | import gleam/httpc 6 | import gleam/json 7 | import gleam/list 8 | import gleam/result 9 | import gleam/uri 10 | import gleeunit 11 | import gleeunit/should 12 | import helper 13 | import openai/completion/chat 14 | import openai/completion/message 15 | import openai/config 16 | import openai/models 17 | 18 | pub fn main() -> Nil { 19 | gleeunit.main() 20 | } 21 | 22 | // gleeunit test functions end in `_test` 23 | pub fn base_url_test() { 24 | config.new("token") 25 | |> should.equal(config.OpenAIConfig( 26 | "token", 27 | uri.parse("https://api.openai.com/v1/") |> helper.must_ok, 28 | )) 29 | 30 | config.new("token") 31 | |> config.set_base_url("https://another.api.com/") 32 | |> should.equal(config.OpenAIConfig( 33 | "token", 34 | uri.parse("https://another.api.com/") |> helper.must_ok, 35 | )) 36 | } 37 | 38 | // pub fn models_list_test() { 39 | // helper.dot_env() |> helper.must_ok 40 | // 41 | // let key = envoy.get("OPENAI_KEY") |> helper.must_ok 42 | // 43 | // config.new(key) 44 | // |> models.list_models 45 | // |> helper.must_ok 46 | // |> list.length 47 | // |> should.not_equal(0) 48 | // } 49 | 50 | pub fn delete_last_test() { 51 | helper.delete_last_slash("hello/") 52 | |> should.equal("hello") 53 | } 54 | 55 | pub fn tool_call_decoder_test() { 56 | let data = 57 | "{\"function\":{\"arguments\":\"{}\",\"name\":\"list_tasks\"},\"id\":\"\",\"type\":\"function\"}" 58 | 59 | json.parse(data, message.tool_call_decoder()) 60 | |> result.map_error(fn(a) { echo a }) 61 | |> should.be_ok 62 | } 63 | 64 | // pub fn chat_with_tools_test() { 65 | // // create a config 66 | // let assert Ok(_) = helper.dot_env() 67 | // let assert Ok(key) = envoy.get("OPENAI_KEY") 68 | // let cfg = config.new(key) 69 | // 70 | // let history = [ 71 | // message.system("Be consise and blut. Answer short"), 72 | // message.user("What is my tasks"), 73 | // ] 74 | // 75 | // // first request to get tool call 76 | // let assert Ok(res) = 77 | // chat.new_params("gpt-4.1-nano", history) 78 | // |> chat.with_tool( 79 | // "list_tasks", 80 | // "use this tool if need to get all user tasks", 81 | // [], 82 | // ) 83 | // |> chat.create(cfg) 84 | // 85 | // let assert Ok(choice) = res.choices |> list.first 86 | // 87 | // // append message from api 88 | // let history = history |> list.append([message.Assistant(choice.message)]) 89 | // 90 | // // append the results of the tool calls 91 | // let history = 92 | // list.append( 93 | // history, 94 | // list.map(choice.message.tool_calls, fn(t) { 95 | // // here will be your real world tool handling 96 | // case t.name { 97 | // "list_tasks" -> 98 | // message.Tool(content: "User has no tasks", tool_call_id: t.id) 99 | // name -> 100 | // message.Tool( 101 | // content: "Invalid task name: " <> name, 102 | // tool_call_id: t.id, 103 | // ) 104 | // } 105 | // }), 106 | // ) 107 | // 108 | // // final request with tool call results 109 | // let assert Ok(res) = 110 | // chat.new_params("gpt-4.1-nano", history) 111 | // |> chat.with_tool( 112 | // "list_tasks", 113 | // "use this tool if need to get all user tasks", 114 | // [], 115 | // ) 116 | // |> chat.create(cfg) 117 | // 118 | // // we should get something like: You dont have tasks 119 | // let assert Ok(choice) = res.choices |> list.first 120 | // choice.message.content 121 | // |> should.not_equal("") 122 | // } 123 | 124 | pub fn sse_test() { 125 | let res = 126 | request.new() 127 | |> request.set_host("sse.dev") 128 | |> request.set_scheme(http.scheme_from_string("https") |> helper.must_ok()) 129 | |> request.set_path("test") 130 | |> httpc.send() 131 | |> helper.must_ok() 132 | echo res 133 | } 134 | -------------------------------------------------------------------------------- /manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "certifi", version = "2.15.0", build_tools = ["rebar3"], requirements = [], otp_app = "certifi", source = "hex", outer_checksum = "B147ED22CE71D72EAFDAD94F055165C1C182F61A2FF49DF28BCC71D1D5B94A60" }, 6 | { name = "dotenv", version = "3.1.0", build_tools = ["mix"], requirements = [], otp_app = "dotenv", source = "hex", outer_checksum = "01BED84D21BEDD8739AEBAD16489A3CE12D19C2D59AF87377DA65EBB361980D3" }, 7 | { name = "dotenv_gleam", version = "2.0.0", build_tools = ["gleam"], requirements = ["envoy", "gleam_erlang", "gleam_stdlib", "simplifile"], otp_app = "dotenv_gleam", source = "hex", outer_checksum = "CA2BB70169AC11CFE52D5C8884BCFCFFAFACD7F5283E5A5E64AD188A823E1D05" }, 8 | { name = "envoy", version = "1.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "95FD059345AA982E89A0B6E2A3BF1CF43E17A7048DCD85B5B65D3B9E4E39D359" }, 9 | { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, 10 | { name = "gleam_erlang", version = "0.34.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "0C38F2A128BAA0CEF17C3000BD2097EB80634E239CE31A86400C4416A5D0FDCC" }, 11 | { name = "gleam_hackney", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_stdlib", "hackney"], otp_app = "gleam_hackney", source = "hex", outer_checksum = "0449AADBEBF3E979509A4079EE34B92EEE4162C5A0DC94F3DA2787E4777F6B45" }, 12 | { name = "gleam_http", version = "4.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "0A62451FC85B98062E0907659D92E6A89F5F3C0FBE4AB8046C99936BF6F91DBC" }, 13 | { name = "gleam_httpc", version = "4.1.1", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C670EBD46FC1472AD5F1F74F1D3938D1D0AC1C7531895ED1D4DDCB6F07279F43" }, 14 | { name = "gleam_json", version = "3.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "5BA154440B22D9800955B1AB854282FA37B97F30F409D76B0824D0A60C934188" }, 15 | { name = "gleam_stdlib", version = "0.60.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "621D600BB134BC239CB2537630899817B1A42E60A1D46C5E9F3FAE39F88C800B" }, 16 | { name = "gleeunit", version = "1.3.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "A7DD6C07B7DA49A6E28796058AA89E651D233B357D5607006D70619CD89DAAAB" }, 17 | { name = "hackney", version = "1.24.1", build_tools = ["rebar3"], requirements = ["certifi", "idna", "metrics", "mimerl", "parse_trans", "ssl_verify_fun", "unicode_util_compat"], otp_app = "hackney", source = "hex", outer_checksum = "F4A7392A0B53D8BBC3EB855BDCC919CD677358E65B2AFD3840B5B3690C4C8A39" }, 18 | { name = "idna", version = "6.1.1", build_tools = ["rebar3"], requirements = ["unicode_util_compat"], otp_app = "idna", source = "hex", outer_checksum = "92376EB7894412ED19AC475E4A86F7B413C1B9FBB5BD16DCCD57934157944CEA" }, 19 | { name = "metrics", version = "1.0.1", build_tools = ["rebar3"], requirements = [], otp_app = "metrics", source = "hex", outer_checksum = "69B09ADDDC4F74A40716AE54D140F93BEB0FB8978D8636EADED0C31B6F099F16" }, 20 | { name = "mimerl", version = "1.4.0", build_tools = ["rebar3"], requirements = [], otp_app = "mimerl", source = "hex", outer_checksum = "13AF15F9F68C65884ECCA3A3891D50A7B57D82152792F3E19D88650AA126B144" }, 21 | { name = "parse_trans", version = "3.4.1", build_tools = ["rebar3"], requirements = [], otp_app = "parse_trans", source = "hex", outer_checksum = "620A406CE75DADA827B82E453C19CF06776BE266F5A67CFF34E1EF2CBB60E49A" }, 22 | { name = "simplifile", version = "2.2.1", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "C88E0EE2D509F6D86EB55161D631657675AA7684DAB83822F7E59EB93D9A60E3" }, 23 | { name = "ssl_verify_fun", version = "1.1.7", build_tools = ["mix", "rebar3", "make"], requirements = [], otp_app = "ssl_verify_fun", source = "hex", outer_checksum = "FE4C190E8F37401D30167C8C405EDA19469F34577987C76DDE613E838BBC67F8" }, 24 | { name = "unicode_util_compat", version = "0.7.1", build_tools = ["rebar3"], requirements = [], otp_app = "unicode_util_compat", source = "hex", outer_checksum = "B3A917854CE3AE233619744AD1E0102E05673136776FB2FA76234F3E03B23642" }, 25 | ] 26 | 27 | [requirements] 28 | dotenv = { version = ">= 3.1.0 and < 4.0.0" } 29 | dotenv_gleam = { version = ">= 2.0.0 and < 3.0.0" } 30 | envoy = { version = ">= 1.0.2 and < 2.0.0" } 31 | gleam_hackney = { version = ">= 1.3.1 and < 2.0.0" } 32 | gleam_http = { version = ">= 4.0.0 and < 5.0.0" } 33 | gleam_httpc = { version = ">= 4.1.1 and < 5.0.0" } 34 | gleam_json = { version = ">= 3.0.1 and < 4.0.0" } 35 | gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } 36 | gleeunit = { version = ">= 1.0.0 and < 2.0.0" } 37 | -------------------------------------------------------------------------------- /src/openai/completion/message.gleam: -------------------------------------------------------------------------------- 1 | import gleam/dynamic 2 | import gleam/dynamic/decode.{type Decoder} 3 | import gleam/json 4 | import gleam/list 5 | import gleam/option.{type Option} 6 | import gleam/result 7 | 8 | pub type CompletionMessage { 9 | Developer(content: Content) 10 | System(content: Content) 11 | User(content: Content) 12 | Assistant(CompletionAssistantMessage) 13 | Tool(tool_call_id: String, content: Content) 14 | } 15 | 16 | pub type CompletionAssistantMessage { 17 | CompletionAssistantMessage( 18 | content: Content, 19 | audio_id: Option(String), 20 | refusal: Option(String), 21 | tool_calls: List(ToolCall), 22 | ) 23 | } 24 | 25 | pub fn completion_assistant_message_decoder() -> Decoder( 26 | CompletionAssistantMessage, 27 | ) { 28 | use content <- decode.optional_field( 29 | "content", 30 | dynamic.string(""), 31 | decode.dynamic, 32 | ) 33 | use audio_id <- decode.optional_field("audio_id", "", decode.string) 34 | use refusal <- decode.optional_field( 35 | "refusal", 36 | dynamic.string(""), 37 | decode.dynamic, 38 | ) 39 | use tool_calls <- decode.optional_field( 40 | "tool_calls", 41 | [], 42 | decode.list(tool_call_decoder()), 43 | ) 44 | decode.success(CompletionAssistantMessage( 45 | content: { 46 | let res = decode.run(content, decode.string) 47 | res |> result.unwrap("") 48 | }, 49 | audio_id: case audio_id == "" { 50 | False -> option.Some(audio_id) 51 | True -> option.None 52 | }, 53 | refusal: case refusal == dynamic.string("") { 54 | False -> 55 | option.Some({ 56 | let res = decode.run(refusal, decode.string) 57 | res |> result.unwrap("") 58 | }) 59 | True -> option.None 60 | }, 61 | tool_calls:, 62 | )) 63 | } 64 | 65 | pub type Content = 66 | String 67 | 68 | pub type ToolCall { 69 | ToolCall(id: String, name: String, arguments: String) 70 | } 71 | 72 | pub fn encode_completion_message( 73 | completion_message: CompletionMessage, 74 | ) -> json.Json { 75 | case completion_message { 76 | Developer(content:) -> 77 | json.object([ 78 | #("role", json.string("developer")), 79 | #("content", json.string(content)), 80 | ]) 81 | System(content:) -> 82 | json.object([ 83 | #("role", json.string("system")), 84 | #("content", json.string(content)), 85 | ]) 86 | User(content:) -> 87 | json.object([ 88 | #("role", json.string("user")), 89 | #("content", json.string(content)), 90 | ]) 91 | Assistant(CompletionAssistantMessage( 92 | content:, 93 | audio_id:, 94 | refusal:, 95 | tool_calls:, 96 | )) -> { 97 | let arr = [ 98 | #("role", json.string("assistant")), 99 | #("content", json.string(content)), 100 | ] 101 | let arr = 102 | arr 103 | |> list.append(case audio_id { 104 | option.None -> [] 105 | option.Some(value) -> [#("audio_Id", json.string(value))] 106 | }) 107 | let arr = 108 | arr 109 | |> list.append(case refusal { 110 | option.None -> [] 111 | option.Some(value) -> [#("refusal", json.string(value))] 112 | }) 113 | let arr = 114 | arr 115 | |> list.append(case tool_calls |> list.length { 116 | 0 -> [] 117 | _ -> [#("tool_calls", json.array(tool_calls, encode_tool_call))] 118 | }) 119 | json.object(arr) 120 | } 121 | 122 | Tool(tool_call_id:, content:) -> 123 | json.object([ 124 | #("role", json.string("tool")), 125 | #("tool_call_id", json.string(tool_call_id)), 126 | #("content", json.string(content)), 127 | ]) 128 | } 129 | } 130 | 131 | pub fn completion_message_decoder() -> Decoder(CompletionMessage) { 132 | use variant <- decode.field("role", decode.string) 133 | case variant { 134 | "developer" -> { 135 | use content <- decode.field("content", decode.string) 136 | decode.success(Developer(content:)) 137 | } 138 | "system" -> { 139 | use content <- decode.field("content", decode.string) 140 | decode.success(System(content:)) 141 | } 142 | "user" -> { 143 | use content <- decode.field("content", decode.string) 144 | decode.success(User(content:)) 145 | } 146 | "assistant" -> { 147 | decode.map(completion_assistant_message_decoder(), fn(a) { Assistant(a) }) 148 | } 149 | "tool" -> { 150 | use tool_call_id <- decode.field("tool_call_id", decode.string) 151 | use content <- decode.field("content", decode.string) 152 | decode.success(Tool(tool_call_id:, content:)) 153 | } 154 | _ -> decode.failure(User(""), "CompletionMessage") 155 | } 156 | } 157 | 158 | fn encode_tool_call(tool_call: ToolCall) -> json.Json { 159 | let ToolCall(id:, name:, arguments:) = tool_call 160 | json.object([ 161 | #("id", json.string(id)), 162 | #("type", json.string("function")), 163 | #( 164 | "function", 165 | json.object([ 166 | #("name", json.string(name)), 167 | #("arguments", json.string(arguments)), 168 | ]), 169 | ), 170 | ]) 171 | } 172 | 173 | pub fn tool_call_decoder() -> Decoder(ToolCall) { 174 | use function <- decode.field("function", { 175 | use name <- decode.field("name", decode.string) 176 | use arguments <- decode.field("arguments", decode.string) 177 | decode.success(#(name, arguments)) 178 | }) 179 | use id <- decode.field("id", decode.string) 180 | decode.success(ToolCall(id, function.0, function.1)) 181 | } 182 | 183 | pub fn user(c: String) { 184 | User(c) 185 | } 186 | 187 | pub fn assistant(c: String) { 188 | Assistant(CompletionAssistantMessage(c, option.None, option.None, [])) 189 | } 190 | 191 | pub fn system(c: String) -> CompletionMessage { 192 | System(c) 193 | } 194 | 195 | pub fn developer(c: String) -> CompletionMessage { 196 | Developer(c) 197 | } 198 | -------------------------------------------------------------------------------- /src/openai/completion/chat.gleam: -------------------------------------------------------------------------------- 1 | import backend 2 | import gleam/dynamic/decode 3 | import gleam/http 4 | import gleam/http/request 5 | import gleam/json 6 | import gleam/list 7 | import gleam/option.{type Option, None, Some} 8 | import gleam/result 9 | import gleam/string 10 | import helper 11 | import openai/completion/message 12 | import openai/config 13 | 14 | pub type ChatCompletionParams { 15 | ChatCompletionParams( 16 | model: String, 17 | messages: List(message.CompletionMessage), 18 | json_schema: Option(JsonSchema), 19 | max_completion_tokens: Option(Int), 20 | audio: Option(Audio), 21 | frequency_penalty: Option(Float), 22 | tools: Option(List(CompletionTool)), 23 | n: Option(Int), 24 | temperature: Option(Float), 25 | tool_choice: Option(String), 26 | top_p: Option(Float), 27 | ) 28 | } 29 | 30 | pub type JsonSchema { 31 | JsonSchema(name: String, description: String, scheme: json.Json) 32 | } 33 | 34 | pub type Audio { 35 | Audio(format: String, voice: String) 36 | } 37 | 38 | pub type CompletionTool { 39 | CompletionTool(name: String, description: String, parameters: json.Json) 40 | } 41 | 42 | /// Creates new params for chat completions api. 43 | /// In order to use other params look at functions 44 | /// starts with `with` from this package 45 | pub fn new_params(model model, messages messages) { 46 | ChatCompletionParams( 47 | model, 48 | messages, 49 | None, 50 | None, 51 | None, 52 | None, 53 | None, 54 | None, 55 | None, 56 | None, 57 | None, 58 | ) 59 | } 60 | 61 | pub fn with_max_tokens(c: ChatCompletionParams, max: Int) { 62 | ChatCompletionParams(..c, max_completion_tokens: Some(max)) 63 | } 64 | 65 | pub fn with_audio(c: ChatCompletionParams, format: String, voice: String) { 66 | ChatCompletionParams(..c, audio: Some(Audio(format, voice))) 67 | } 68 | 69 | pub fn with_n(c: ChatCompletionParams, n: Int) { 70 | ChatCompletionParams(..c, n: Some(n)) 71 | } 72 | 73 | pub fn with_temperature(c: ChatCompletionParams, temperature: Float) { 74 | ChatCompletionParams(..c, temperature: Some(temperature)) 75 | } 76 | 77 | pub fn with_top_p(c: ChatCompletionParams, top_p: Float) { 78 | ChatCompletionParams(..c, top_p: Some(top_p)) 79 | } 80 | 81 | pub fn with_tool_choice(c: ChatCompletionParams, tool_choice: String) { 82 | ChatCompletionParams(..c, tool_choice: Some(tool_choice)) 83 | } 84 | 85 | pub fn with_frequency_penalty(c: ChatCompletionParams, frequency_penalty: Float) { 86 | ChatCompletionParams(..c, frequency_penalty: Some(frequency_penalty)) 87 | } 88 | 89 | pub fn with_structed_output( 90 | c: ChatCompletionParams, 91 | name name: String, 92 | description description: String, 93 | properties properties: List(#(String, json.Json)), 94 | ) { 95 | let schema = 96 | json.object([ 97 | #("type", json.string("object")), 98 | #("properties", json.object(properties)), 99 | #("additionalProperties", json.bool(False)), 100 | #( 101 | "required", 102 | json.array(list.map(properties, fn(a) { a.0 }), of: json.string), 103 | ), 104 | ]) 105 | ChatCompletionParams( 106 | ..c, 107 | json_schema: Some(JsonSchema( 108 | name: name, 109 | description: description, 110 | scheme: schema, 111 | )), 112 | ) 113 | } 114 | 115 | pub fn with_tool( 116 | c: ChatCompletionParams, 117 | name: String, 118 | description: String, 119 | args: List(#(String, json.Json)), 120 | ) { 121 | let new_tool = 122 | CompletionTool( 123 | name: name, 124 | description: description, 125 | parameters: json.object([ 126 | #("type", json.string("object")), 127 | #("properties", json.object(args)), 128 | #("additionalProperties", json.bool(False)), 129 | #( 130 | "required", 131 | json.array(list.map(args, fn(a) { a.0 }), of: json.string), 132 | ), 133 | ]), 134 | ) 135 | ChatCompletionParams(..c, tools: case c.tools { 136 | None -> [new_tool] |> Some 137 | Some(tools) -> tools |> list.append([new_tool]) |> Some 138 | }) 139 | } 140 | 141 | fn encode_chat_completion_params( 142 | chat_completion_params: ChatCompletionParams, 143 | ) -> json.Json { 144 | let ChatCompletionParams( 145 | model:, 146 | messages:, 147 | json_schema:, 148 | max_completion_tokens:, 149 | audio:, 150 | frequency_penalty:, 151 | tools:, 152 | n:, 153 | temperature:, 154 | tool_choice:, 155 | top_p:, 156 | ) = chat_completion_params 157 | [ 158 | #("model", json.string(model)), 159 | #("messages", json.array(messages, message.encode_completion_message)), 160 | ] 161 | |> add_if_some(max_completion_tokens, fn(a) { 162 | #("max_completion_tokens", json.int(a)) 163 | }) 164 | |> add_if_some(audio, fn(a) { #("audio", encode_audio(a)) }) 165 | |> add_if_some(n, fn(a) { #("n", json.int(a)) }) 166 | |> add_if_some(top_p, fn(a) { #("top_p", json.float(a)) }) 167 | |> add_if_some(temperature, fn(a) { #("temperature", json.float(a)) }) 168 | |> add_if_some(tool_choice, fn(a) { #("tool_choice", json.string(a)) }) 169 | |> add_if_some(frequency_penalty, fn(a) { 170 | #("frequency_penalty", json.float(a)) 171 | }) 172 | |> add_if_some(tools, fn(a) { 173 | #("tools", json.array(a, encode_completion_tool)) 174 | }) 175 | |> add_if_some(json_schema, fn(a) { 176 | #( 177 | "response_format", 178 | json.object([ 179 | #("type", json.string("json_schema")), 180 | #( 181 | "json_schema", 182 | json.object([ 183 | #("name", json.string(a.name)), 184 | #("description", json.string(a.description)), 185 | #("schema", a.scheme), 186 | #("strict", json.bool(True)), 187 | ]), 188 | ), 189 | ]), 190 | ) 191 | }) 192 | |> json.object 193 | } 194 | 195 | fn add_if_some(arr: List(a), some: Option(b), cb: fn(b) -> a) { 196 | case some { 197 | None -> arr 198 | Some(s) -> arr |> list.append([cb(s)]) 199 | } 200 | } 201 | 202 | fn encode_audio(audio: Audio) -> json.Json { 203 | let Audio(format:, voice:) = audio 204 | json.object([#("format", json.string(format)), #("voice", json.string(voice))]) 205 | } 206 | 207 | fn encode_completion_tool(completion_tool: CompletionTool) -> json.Json { 208 | let CompletionTool(name:, description:, parameters:) = completion_tool 209 | json.object([ 210 | #("type", json.string("function")), 211 | #( 212 | "function", 213 | json.object([ 214 | #("name", json.string(name)), 215 | #("description", json.string(description)), 216 | #("parameters", parameters), 217 | #("strict", json.bool(True)), 218 | ]), 219 | ), 220 | ]) 221 | } 222 | 223 | pub fn create(p: ChatCompletionParams, c: config.OpenAIConfig) { 224 | let body = encode_chat_completion_params(p) |> json.to_string 225 | 226 | let req = 227 | helper.new_https_request() 228 | |> request.set_host(c.base_url.host |> helper.must_some) 229 | |> request.set_path( 230 | [c.base_url.path |> helper.delete_last_slash, "chat", "completions"] 231 | |> string.join("/"), 232 | ) 233 | |> request.set_method(http.Post) 234 | |> request.set_header("Content-Type", "application/json") 235 | |> request.set_header("Authorization", "Bearer " <> c.api_key) 236 | |> request.set_body(body) 237 | |> backend.send() 238 | 239 | use res <- result.try(req) 240 | use res <- helper.handle_positive(res) 241 | res.body 242 | |> json.parse(chat_completion_response_decoder()) 243 | |> result.map_error(fn(_) { "failed to parse chat_completion_response" }) 244 | } 245 | 246 | pub type ChatCompletionResponse { 247 | ChatCompletionResponse( 248 | choices: List(CompletionChoice), 249 | usage: CompletionUsage, 250 | ) 251 | } 252 | 253 | fn chat_completion_response_decoder() -> decode.Decoder(ChatCompletionResponse) { 254 | use choices <- decode.field( 255 | "choices", 256 | decode.list(completion_choice_decoder()), 257 | ) 258 | use usage <- decode.field("usage", completion_usage_decoder()) 259 | decode.success(ChatCompletionResponse(choices:, usage:)) 260 | } 261 | 262 | pub type CompletionChoice { 263 | CompletionChoice( 264 | finish_reason: String, 265 | index: Int, 266 | message: message.CompletionAssistantMessage, 267 | ) 268 | } 269 | 270 | pub type CompletionUsage { 271 | CompletionUsage(completion_tokens: Int, prompt_tokens: Int, total_tokens: Int) 272 | } 273 | 274 | fn completion_usage_decoder() -> decode.Decoder(CompletionUsage) { 275 | use completion_tokens <- decode.field("completion_tokens", decode.int) 276 | use prompt_tokens <- decode.field("prompt_tokens", decode.int) 277 | use total_tokens <- decode.field("total_tokens", decode.int) 278 | decode.success(CompletionUsage( 279 | completion_tokens:, 280 | prompt_tokens:, 281 | total_tokens:, 282 | )) 283 | } 284 | 285 | fn completion_choice_decoder() -> decode.Decoder(CompletionChoice) { 286 | use finish_reason <- decode.field("finish_reason", decode.string) 287 | use index <- decode.field("index", decode.int) 288 | use msg <- decode.field( 289 | "message", 290 | message.completion_assistant_message_decoder(), 291 | ) 292 | decode.success(CompletionChoice(finish_reason:, index:, message: msg)) 293 | } 294 | --------------------------------------------------------------------------------