diff --git a/gleam.toml b/gleam.toml index bfba93d9bbb4a2865f89342601ab925022662bd6..2870f6a6bb1f359b21710f0714ae1f3a017b1c66 100644 --- a/gleam.toml +++ b/gleam.toml @@ -28,7 +28,7 @@ glint = ">= 1.2.1 and < 2.0.0" gleam_httpc = ">= 5.0.0 and < 6.0.0" gleam_http = ">= 4.3.0 and < 5.0.0" gleam_json = ">= 3.1.0 and < 4.0.0" -starlet = ">= 1.0.1 and < 2.0.0" +starlet = { git = "https://github.com/amolith/starlet", ref = "openai-completions" } envoy = ">= 1.1.0 and < 2.0.0" tom = ">= 2.0.0 and < 3.0.0" simplifile = ">= 2.3.2 and < 3.0.0" diff --git a/manifest.toml b/manifest.toml index c3ae6e916d350958015306780ae24b2b0b1e6b7e..29ecfcc849e259a30d892e58c9b469813d16d25c 100644 --- a/manifest.toml +++ b/manifest.toml @@ -1,7 +1,3 @@ -# SPDX-FileCopyrightText: Amolith -# -# SPDX-License-Identifier: CC0-1.0 - # This file was generated by Gleam # You typically do not need to edit this file @@ -9,15 +5,15 @@ packages = [ { name = "argv", version = "1.0.2", build_tools = ["gleam"], requirements = [], otp_app = "argv", source = "hex", outer_checksum = "BA1FF0929525DEBA1CE67256E5ADF77A7CDDFE729E3E3F57A5BDCAA031DED09D" }, { name = "envoy", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "envoy", source = "hex", outer_checksum = "850DA9D29D2E5987735872A2B5C81035146D7FE19EFC486129E44440D03FD832" }, { name = "filepath", version = "1.1.2", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "filepath", source = "hex", outer_checksum = "B06A9AF0BF10E51401D64B98E4B627F1D2E48C154967DA7AF4D0914780A6D40A" }, - { name = "gleam_community_ansi", version = "1.4.3", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "8A62AE9CC6EA65BEA630D95016D6C07E4F9973565FA3D0DE68DC4200D8E0DD27" }, - { name = "gleam_community_colour", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "E34DD2C896AC3792151EDA939DA435FF3B69922F33415ED3C4406C932FBE9634" }, + { name = "gleam_community_ansi", version = "1.4.4", build_tools = ["gleam"], requirements = ["gleam_community_colour", "gleam_regexp", "gleam_stdlib"], otp_app = "gleam_community_ansi", source = "hex", outer_checksum = "1B3AEA6074AB34D5F0674744F36DDC7290303A03295507E2DEC61EDD6F5777FE" }, + { name = "gleam_community_colour", version = "2.0.4", build_tools = ["gleam"], requirements = ["gleam_json", "gleam_stdlib"], otp_app = "gleam_community_colour", source = "hex", outer_checksum = "6DB4665555D7D2B27F0EA32EF47E8BEBC4303821765F9C73D483F38EE24894F0" }, { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, { name = "gleam_httpc", version = "5.0.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_http", "gleam_stdlib"], otp_app = "gleam_httpc", source = "hex", outer_checksum = "C545172618D07811494E97AAA4A0FB34DA6F6D0061FDC8041C2F8E3BE2B2E48F" }, { name = "gleam_json", version = "3.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_json", source = "hex", outer_checksum = "44FDAA8847BE8FC48CA7A1C089706BD54BADCC4C45B237A992EDDF9F2CDB2836" }, { name = "gleam_regexp", version = "1.1.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_regexp", source = "hex", outer_checksum = "9C215C6CA84A5B35BB934A9B61A9A306EC743153BE2B0425A0D032E477B062A9" }, - { name = "gleam_stdlib", version = "0.68.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "EEC7E7A18B8A53B7A28B7F0A2198CE53BAFF05D45479E4806C387EDF26DA842D" }, - { name = "gleam_time", version = "1.6.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "0DF3834D20193F0A38D0EB21F0A78D48F2EC276C285969131B86DF8D4EF9E762" }, + { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, + { name = "gleam_time", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_time", source = "hex", outer_checksum = "56DB0EF9433826D3B99DB0B4AF7A2BFED13D09755EC64B1DAAB46F804A9AD47D" }, { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, { name = "gleescript", version = "1.5.2", build_tools = ["gleam"], requirements = ["argv", "filepath", "gleam_erlang", "gleam_stdlib", "simplifile", "snag", "tom"], otp_app = "gleescript", source = "hex", outer_checksum = "27AC58481742ED29D9B37C506F78958A8AD798750A79ED08C8F8AFBA8F23563B" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, @@ -26,9 +22,9 @@ packages = [ { name = "shellout", version = "1.7.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "shellout", source = "hex", outer_checksum = "1BDC03438FEB97A6AF3E396F4ABEB32BECF20DF2452EC9A8C0ACEB7BDDF70B14" }, { name = "simplifile", version = "2.3.2", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "E049B4DACD4D206D87843BCF4C775A50AE0F50A52031A2FFB40C9ED07D6EC70A" }, { name = "snag", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "snag", source = "hex", outer_checksum = "274F41D6C3ECF99F7686FDCE54183333E41D2C1CA5A3A673F9A8B2C7A4401077" }, - { name = "starlet", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "jscheam"], otp_app = "starlet", source = "hex", outer_checksum = "4E288CB970EFF9BE11EB476A0449A7A9BFAA993BE25CAA7B7E8A0A5B3A76AE5C" }, + { name = "starlet", version = "1.0.1", build_tools = ["gleam"], requirements = ["gleam_http", "gleam_httpc", "gleam_json", "gleam_stdlib", "jscheam"], source = "git", repo = "https://github.com/amolith/starlet", commit = "581594be3c51e5134abe7e0ac462b3f48bd0eb6e" }, { name = "stdin", version = "2.0.2", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_yielder"], otp_app = "stdin", source = "hex", outer_checksum = "437084939CE094E06D32D07EB19B74473D08895AB817C03550CEDFDBF0DFDC19" }, - { name = "tom", version = "2.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "74D0C5A3761F7A7D06994755D4D5AD854122EF8E9F9F76A3E7547606D8C77091" }, + { name = "tom", version = "2.0.1", build_tools = ["gleam"], requirements = ["gleam_stdlib", "gleam_time"], otp_app = "tom", source = "hex", outer_checksum = "90791DA4AACE637E30081FE77049B8DB850FBC8CACC31515376BCC4E59BE1DD2" }, ] [requirements] @@ -45,6 +41,6 @@ gleeunit = { version = ">= 1.0.0 and < 2.0.0" } glint = { version = ">= 1.2.1 and < 2.0.0" } shellout = { version = ">= 1.7.0 and < 2.0.0" } simplifile = { version = ">= 2.3.2 and < 3.0.0" } -starlet = { version = ">= 1.0.1 and < 2.0.0" } +starlet = { git = "https://github.com/amolith/starlet", ref = "openai-completions" } stdin = { version = ">= 2.0.2 and < 3.0.0" } tom = { version = ">= 2.0.0 and < 3.0.0" } diff --git a/src/clients.gleam b/src/clients.gleam index 0464bf81af5d9d47be10e11070edb3be394e1b54..70bc389e19aef8d680f72fd569852e5a4466603a 100644 --- a/src/clients.gleam +++ b/src/clients.gleam @@ -12,6 +12,8 @@ import starlet/anthropic import starlet/gemini import starlet/ollama import starlet/openai +import starlet/openai_compat +import starlet/openai_compat/thinking pub fn send_openai( api_key: String, @@ -128,6 +130,36 @@ pub fn send_gemini( |> result.map_error(format_starlet_error) } +pub fn send_openai_compat( + api_key: String, + base_url: String, + model: String, + system_prompt: String, + user_prompt: String, + reasoning: Option(config.ReasoningEffort), +) -> Result(String, String) { + // Default to Generic dialect - this is the most widely compatible option + // Provider-specific dialects can be configured via garble's config file + let client = openai_compat.new(base_url, api_key, thinking.Generic) + + let chat = + starlet.chat(client, model) + |> starlet.system(system_prompt) + |> starlet.user(user_prompt) + + let chat = case reasoning { + Some(config.ReasoningLow) -> openai_compat.with_reasoning(chat, thinking.EffortLow) + Some(config.ReasoningMedium) -> openai_compat.with_reasoning(chat, thinking.EffortMedium) + Some(config.ReasoningHigh) -> openai_compat.with_reasoning(chat, thinking.EffortHigh) + None -> chat + } + + chat + |> starlet.send() + |> result.map(fn(resp) { starlet.text(resp.1) }) + |> result.map_error(format_starlet_error) +} + pub fn send_ollama( endpoint: String, model: String, diff --git a/src/garble.gleam b/src/garble.gleam index 95b00ed248ead42e05b2b2afa2d87aa732630b32..b53b3018b221a18f3286c7febfed7f68e1246068 100644 --- a/src/garble.gleam +++ b/src/garble.gleam @@ -12,7 +12,6 @@ import gleam/result import gleam/string import gleam/yielder import glint -import openai_compat import prompts import providers.{type Provider} import stdin @@ -160,7 +159,7 @@ fn send_request( "openai-compat" -> { case provider.api_endpoint { Some(endpoint) -> - openai_compat.send(endpoint, key, cfg.model, system, user_prompt, reasoning) + clients.send_openai_compat(key, endpoint, cfg.model, system, user_prompt, reasoning) None -> Error("No endpoint configured for " <> provider.id) } } diff --git a/src/openai_compat.gleam b/src/openai_compat.gleam deleted file mode 100644 index d3041d10916cbe6e723b668d635756185f172771..0000000000000000000000000000000000000000 --- a/src/openai_compat.gleam +++ /dev/null @@ -1,95 +0,0 @@ -// SPDX-FileCopyrightText: Amolith -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -import config.{type ReasoningEffort} -import gleam/dynamic/decode -import gleam/http -import gleam/http/request -import gleam/httpc -import gleam/json -import gleam/list -import gleam/option.{type Option, None, Some} - -pub fn send( - endpoint: String, - api_key: String, - model: String, - system_prompt: String, - user_prompt: String, - reasoning: Option(ReasoningEffort), -) -> Result(String, String) { - let messages = build_messages(system_prompt, user_prompt) - let base_fields = [ - #("model", json.string(model)), - #("messages", json.array(messages, fn(m) { m })), - ] - let fields = case reasoning { - Some(effort) -> - list.append(base_fields, [ - #("reasoning_effort", json.string(config.reasoning_to_string(effort))), - ]) - None -> base_fields - } - let body = json.object(fields) |> json.to_string - - let url = endpoint <> "/chat/completions" - - case request.to(url) { - Error(_) -> Error("Invalid endpoint URL: " <> endpoint) - Ok(req) -> { - let req = - req - |> request.set_method(http.Post) - |> request.set_header("content-type", "application/json") - |> request.set_header("authorization", "Bearer " <> api_key) - |> request.set_body(body) - - case httpc.send(req) { - Error(_) -> Error("Network error") - Ok(resp) if resp.status >= 200 && resp.status < 300 -> - parse_response(resp.body) - Ok(resp) -> Error("HTTP " <> resp.body) - } - } - } -} - -fn build_messages(system_prompt: String, user_prompt: String) -> List(json.Json) { - case system_prompt { - "" -> [user_message(user_prompt)] - sys -> [system_message(sys), user_message(user_prompt)] - } -} - -fn system_message(content: String) -> json.Json { - json.object([ - #("role", json.string("system")), - #("content", json.string(content)), - ]) -} - -fn user_message(content: String) -> json.Json { - json.object([ - #("role", json.string("user")), - #("content", json.string(content)), - ]) -} - -fn parse_response(body: String) -> Result(String, String) { - let choice_decoder = decode.at(["message", "content"], decode.string) - - let response_decoder = { - use choices <- decode.field("choices", decode.list(choice_decoder)) - decode.success(choices) - } - - case json.parse(body, response_decoder) { - Error(_) -> Error("Failed to parse response") - Ok(choices) -> - case list.first(choices) { - Ok(content) -> Ok(content) - Error(_) -> Error("No response content") - } - } -}