mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-07 08:15:31 +00:00
title generation
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
config.yml
|
||||
debug.json
|
||||
/*.json
|
@@ -12,6 +12,7 @@ whiskr is a private, self-hosted web chat interface for interacting with AI mode
|
||||
- Edit, delete, or copy any message
|
||||
- Persistent settings for model, temperature, and other parameters
|
||||
- Full conversation control including clearing and modifying messages
|
||||
- Title generation (and refresh)
|
||||
- Smooth UI updates with [morphdom](https://github.com/patrick-steele-idem/morphdom), selections, images, and other state are preserved during updates
|
||||
- Easy model selection:
|
||||
- Tags indicate if a model supports **tools**, **vision**, or **reasoning**
|
||||
|
8
chat.go
8
chat.go
@@ -140,7 +140,9 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
request.Messages = append(request.Messages, openrouter.SystemMessage(InternalToolsPrompt))
|
||||
}
|
||||
|
||||
for index, message := range r.Messages {
|
||||
for _, message := range r.Messages {
|
||||
message.Text = strings.ReplaceAll(message.Text, "\r", "")
|
||||
|
||||
switch message.Role {
|
||||
case "system":
|
||||
request.Messages = append(request.Messages, openrouter.ChatCompletionMessage{
|
||||
@@ -211,8 +213,6 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
}
|
||||
|
||||
request.Messages = append(request.Messages, msg)
|
||||
default:
|
||||
return nil, fmt.Errorf("[%d] invalid role: %q", index+1, message.Role)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
request.Messages = append(request.Messages, openrouter.SystemMessage("You have reached the maximum number of tool calls for this conversation. Provide your final response based on the information you have gathered."))
|
||||
}
|
||||
|
||||
dump("debug.json", request)
|
||||
dump("chat.json", request)
|
||||
|
||||
tool, message, err := RunCompletion(ctx, response, request)
|
||||
if err != nil {
|
||||
|
16
env.go
16
env.go
@@ -19,7 +19,8 @@ type EnvTokens struct {
|
||||
}
|
||||
|
||||
type EnvSettings struct {
|
||||
CleanContent bool `json:"cleanup"`
|
||||
CleanContent bool `json:"cleanup"`
|
||||
TitleModel string `json:"title-model"`
|
||||
}
|
||||
|
||||
type EnvUser struct {
|
||||
@@ -97,6 +98,16 @@ func (e *Environment) Init() error {
|
||||
log.Warning("Missing token.exa, web search unavailable")
|
||||
}
|
||||
|
||||
// check if github token is set
|
||||
if e.Tokens.GitHub == "" {
|
||||
log.Warning("Missing token.github, limited api requests")
|
||||
}
|
||||
|
||||
// default title model
|
||||
if e.Settings.TitleModel == "" {
|
||||
e.Settings.TitleModel = "google/gemini-2.5-flash-lite"
|
||||
}
|
||||
|
||||
// create user lookup map
|
||||
e.Authentication.lookup = make(map[string]*EnvUser)
|
||||
|
||||
@@ -122,7 +133,8 @@ func (e *Environment) Store() error {
|
||||
"$.tokens.exa": {yaml.HeadComment(" exa search api token (optional; used by search tools)")},
|
||||
"$.tokens.github": {yaml.HeadComment(" github api token (optional; used by search tools)")},
|
||||
|
||||
"$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")},
|
||||
"$.settings.cleanup": {yaml.HeadComment(" normalize unicode in assistant output (optional; default: true)")},
|
||||
"$.settings.title-model": {yaml.HeadComment(" model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)")},
|
||||
|
||||
"$.authentication.enabled": {yaml.HeadComment(" require login with username and password")},
|
||||
"$.authentication.users": {yaml.HeadComment(" list of users with bcrypt password hashes")},
|
||||
|
@@ -14,6 +14,8 @@ tokens:
|
||||
settings:
|
||||
# normalize unicode in assistant output (optional; default: true)
|
||||
cleanup: true
|
||||
# model used to generate titles (needs to have structured output support; default: google/gemini-2.5-flash-lite)
|
||||
title-model: google/gemini-2.5-flash-lite
|
||||
|
||||
authentication:
|
||||
# require login with username and password
|
||||
|
24
internal/title.txt
Normal file
24
internal/title.txt
Normal file
@@ -0,0 +1,24 @@
|
||||
You are a title generator for chat conversations. Your task is to create a concise, descriptive title that captures the main topic or purpose of the conversation.
|
||||
|
||||
Guidelines:
|
||||
- Create a title that is 3-8 words long
|
||||
- Focus on the primary topic, question, or task being discussed
|
||||
- Be specific rather than generic (avoid titles like "General Discussion" or "Chat")
|
||||
- If the conversation covers multiple topics, focus on the most prominent or recent one
|
||||
- Use clear, natural language that would help someone quickly understand what the chat is about
|
||||
|
||||
{{if .Title}}
|
||||
Important: The current title is "{{.Title}}". Generate a DIFFERENT title that:
|
||||
- Captures a different aspect or angle of the conversation
|
||||
- May focus on more recent developments in the chat
|
||||
- Uses different wording and phrasing
|
||||
- Do NOT simply rephrase or slightly modify the existing title
|
||||
{{end}}
|
||||
|
||||
Analyze the conversation below and generate an appropriate title. The conversation may contain system messages (instructions), user messages, and assistant responses. Focus on the actual conversation content, not the system instructions.
|
||||
|
||||
Respond with a JSON object containing only the title, matching this format:
|
||||
|
||||
{"title": "string"}
|
||||
|
||||
ONLY respond with the json object, nothing else. Do not include extra formatting or markdown.
|
1
main.go
1
main.go
@@ -52,6 +52,7 @@ func main() {
|
||||
gr.Use(Authenticate)
|
||||
|
||||
gr.Get("/-/stats/{id}", HandleStats)
|
||||
gr.Post("/-/title", HandleTitle)
|
||||
gr.Post("/-/chat", HandleChat)
|
||||
})
|
||||
|
||||
|
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/revrost/go-openrouter"
|
||||
)
|
||||
@@ -28,7 +29,16 @@ func OpenRouterStartStream(ctx context.Context, request openrouter.ChatCompletio
|
||||
func OpenRouterRun(ctx context.Context, request openrouter.ChatCompletionRequest) (openrouter.ChatCompletionResponse, error) {
|
||||
client := OpenRouterClient()
|
||||
|
||||
return client.CreateChatCompletion(ctx, request)
|
||||
response, err := client.CreateChatCompletion(ctx, request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if len(response.Choices) == 0 {
|
||||
return response, errors.New("no choices")
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func OpenRouterGetGeneration(ctx context.Context, id string) (openrouter.Generation, error) {
|
||||
|
@@ -31,11 +31,18 @@ var (
|
||||
//go:embed internal/tools.txt
|
||||
InternalToolsPrompt string
|
||||
|
||||
//go:embed internal/title.txt
|
||||
InternalTitlePrompt string
|
||||
|
||||
InternalTitleTmpl *template.Template
|
||||
|
||||
Prompts []Prompt
|
||||
Templates = make(map[string]*template.Template)
|
||||
)
|
||||
|
||||
func init() {
|
||||
InternalTitleTmpl = NewTemplate("internal-title", InternalTitlePrompt)
|
||||
|
||||
var err error
|
||||
|
||||
Prompts, err = LoadPrompts()
|
||||
@@ -43,6 +50,8 @@ func init() {
|
||||
}
|
||||
|
||||
func NewTemplate(name, text string) *template.Template {
|
||||
text = strings.ReplaceAll(text, "\r", "")
|
||||
|
||||
return template.Must(template.New(name).Parse(text))
|
||||
}
|
||||
|
||||
|
@@ -133,12 +133,13 @@ body:not(.loading) #loading {
|
||||
gap: 5px;
|
||||
background: #1e2030;
|
||||
margin: auto;
|
||||
margin-top: 30px;
|
||||
margin-top: 40px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
height: calc(100% - 30px);
|
||||
height: calc(100% - 40px);
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
@@ -150,6 +151,40 @@ body:not(.loading) #loading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: absolute;
|
||||
top: -22px;
|
||||
left: -4px;
|
||||
font-style: italic;
|
||||
padding-left: 22px;
|
||||
transition: 150ms opacity;
|
||||
}
|
||||
|
||||
#title-text {
|
||||
transition: 150ms;
|
||||
}
|
||||
|
||||
#title #title-refresh {
|
||||
background-image: url(icons/refresh.svg);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
#title.refreshing #title-refresh {
|
||||
animation: rotating-y 1.2s linear infinite;
|
||||
}
|
||||
|
||||
#title.refreshing #title-text {
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
#messages {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -717,6 +752,7 @@ select {
|
||||
}
|
||||
|
||||
body.loading #version,
|
||||
#title-refresh,
|
||||
#loading .inner::after,
|
||||
.modal.loading .content::after,
|
||||
.reasoning .toggle::before,
|
||||
|
7
static/css/icons/refresh.svg
Normal file
7
static/css/icons/refresh.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Transformed by: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"/>
|
||||
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"/>
|
After Width: | Height: | Size: 879 B |
@@ -26,7 +26,13 @@
|
||||
</div>
|
||||
|
||||
<div id="page">
|
||||
<div id="title" class="hidden">
|
||||
<button id="title-refresh"></button>
|
||||
<div id="title-text"></div>
|
||||
</div>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div id="chat">
|
||||
<button id="top" class="hidden" title="Scroll to top"></button>
|
||||
<button id="bottom" class="hidden" title="Scroll to bottom"></button>
|
||||
|
@@ -1,6 +1,9 @@
|
||||
(() => {
|
||||
const $version = document.getElementById("version"),
|
||||
$total = document.getElementById("total"),
|
||||
$title = document.getElementById("title"),
|
||||
$titleRefresh = document.getElementById("title-refresh"),
|
||||
$titleText = document.getElementById("title-text"),
|
||||
$messages = document.getElementById("messages"),
|
||||
$chat = document.getElementById("chat"),
|
||||
$message = document.getElementById("message"),
|
||||
@@ -37,7 +40,8 @@
|
||||
|
||||
let autoScrolling = false,
|
||||
jsonMode = false,
|
||||
searchTool = false;
|
||||
searchTool = false,
|
||||
chatTitle = false;
|
||||
|
||||
let searchAvailable = false,
|
||||
activeMessage = null,
|
||||
@@ -51,6 +55,14 @@
|
||||
$total.textContent = formatMoney(totalCost);
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
$title.classList.toggle("hidden", !messages.length);
|
||||
|
||||
$titleText.textContent = chatTitle || (messages.length ? "New Chat" : "");
|
||||
|
||||
storeValue("title", chatTitle);
|
||||
}
|
||||
|
||||
function updateScrollButton() {
|
||||
const bottom = $messages.scrollHeight - ($messages.scrollTop + $messages.offsetHeight);
|
||||
|
||||
@@ -266,6 +278,8 @@
|
||||
let timeout;
|
||||
|
||||
_optCopy.addEventListener("click", () => {
|
||||
this.stopEdit();
|
||||
|
||||
clearTimeout(timeout);
|
||||
|
||||
navigator.clipboard.writeText(this.#text);
|
||||
@@ -304,6 +318,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopEdit();
|
||||
|
||||
while (messages.length > index) {
|
||||
messages[messages.length - 1].delete();
|
||||
}
|
||||
@@ -759,8 +775,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
let controller;
|
||||
|
||||
async function json(url) {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
@@ -778,6 +792,8 @@
|
||||
}
|
||||
|
||||
async function stream(url, options, callback) {
|
||||
let aborted;
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
@@ -834,29 +850,32 @@
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name !== "AbortError") {
|
||||
callback({
|
||||
type: "error",
|
||||
text: err.message,
|
||||
});
|
||||
if (err.name === "AbortError") {
|
||||
aborted = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
callback({
|
||||
type: "error",
|
||||
text: err.message,
|
||||
});
|
||||
} finally {
|
||||
callback(false);
|
||||
callback(aborted ? "aborted" : "done");
|
||||
}
|
||||
}
|
||||
|
||||
let chatController;
|
||||
|
||||
function generate(cancel = false) {
|
||||
if (controller) {
|
||||
controller.abort();
|
||||
if (chatController) {
|
||||
chatController.abort();
|
||||
|
||||
if (cancel) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$temperature.value) {
|
||||
}
|
||||
|
||||
let temperature = parseFloat($temperature.value);
|
||||
|
||||
if (Number.isNaN(temperature) || temperature < 0 || temperature > 2) {
|
||||
@@ -888,7 +907,7 @@
|
||||
|
||||
pushMessage();
|
||||
|
||||
controller = new AbortController();
|
||||
chatController = new AbortController();
|
||||
|
||||
$chat.classList.add("completing");
|
||||
|
||||
@@ -908,14 +927,16 @@
|
||||
|
||||
let message, generationID;
|
||||
|
||||
function finish() {
|
||||
function finish(aborted = false) {
|
||||
if (!message) {
|
||||
return;
|
||||
}
|
||||
|
||||
message.setState(false);
|
||||
|
||||
setTimeout(message.loadGenerationData.bind(message), 750, generationID);
|
||||
if (!aborted) {
|
||||
setTimeout(message.loadGenerationData.bind(message), 750, generationID);
|
||||
}
|
||||
|
||||
message = null;
|
||||
generationID = null;
|
||||
@@ -945,16 +966,24 @@
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
signal: chatController.signal,
|
||||
},
|
||||
chunk => {
|
||||
if (!chunk) {
|
||||
controller = null;
|
||||
if (chunk === "aborted") {
|
||||
finish(true);
|
||||
|
||||
return;
|
||||
} else if (chunk === "done") {
|
||||
chatController = null;
|
||||
|
||||
finish();
|
||||
|
||||
$chat.classList.remove("completing");
|
||||
|
||||
if (!chatTitle && !titleController) {
|
||||
refreshTitle();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -999,6 +1028,65 @@
|
||||
);
|
||||
}
|
||||
|
||||
let titleController;
|
||||
|
||||
async function refreshTitle() {
|
||||
if (titleController) {
|
||||
titleController.abort();
|
||||
}
|
||||
|
||||
titleController = new AbortController();
|
||||
|
||||
const body = {
|
||||
title: chatTitle || null,
|
||||
messages: messages.map(message => message.getData()).filter(Boolean),
|
||||
};
|
||||
|
||||
if (!body.messages.length) {
|
||||
updateTitle();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$title.classList.add("refreshing");
|
||||
|
||||
try {
|
||||
const response = await fetch("/-/title", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
signal: titleController.signal,
|
||||
}),
|
||||
result = await response.json();
|
||||
|
||||
if (result.cost) {
|
||||
totalCost += result.cost;
|
||||
|
||||
updateTotalCost();
|
||||
}
|
||||
|
||||
if (!response.ok || !result?.title) {
|
||||
throw new Error(result?.error || response.statusText);
|
||||
}
|
||||
|
||||
chatTitle = result.title;
|
||||
} catch (err) {
|
||||
if (err.name === "AbortError") {
|
||||
return;
|
||||
}
|
||||
|
||||
alert(err.message);
|
||||
}
|
||||
|
||||
titleController = null;
|
||||
|
||||
updateTitle();
|
||||
|
||||
$title.classList.remove("refreshing");
|
||||
}
|
||||
|
||||
async function login() {
|
||||
const username = $username.value.trim(),
|
||||
password = $password.value.trim();
|
||||
@@ -1147,6 +1235,10 @@
|
||||
}
|
||||
});
|
||||
|
||||
chatTitle = loadValue("title");
|
||||
|
||||
updateTitle();
|
||||
|
||||
scroll();
|
||||
|
||||
// small fix, sometimes when hard reloading we don't scroll all the way
|
||||
@@ -1235,11 +1327,12 @@
|
||||
const message = new Message($role.value, "", text, attachments);
|
||||
|
||||
clearAttachments();
|
||||
updateTitle();
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
$total.addEventListener("auxclick", (event) => {
|
||||
$total.addEventListener("auxclick", event => {
|
||||
if (event.button !== 1) {
|
||||
return;
|
||||
}
|
||||
@@ -1249,6 +1342,10 @@
|
||||
updateTotalCost();
|
||||
});
|
||||
|
||||
$titleRefresh.addEventListener("click", () => {
|
||||
refreshTitle();
|
||||
});
|
||||
|
||||
$messages.addEventListener("scroll", () => {
|
||||
updateScrollButton();
|
||||
});
|
||||
@@ -1417,6 +1514,10 @@
|
||||
}
|
||||
|
||||
clearMessages();
|
||||
|
||||
chatTitle = false;
|
||||
|
||||
updateTitle();
|
||||
});
|
||||
|
||||
$export.addEventListener("click", () => {
|
||||
|
152
title.go
Normal file
152
title.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/revrost/go-openrouter"
|
||||
"github.com/revrost/go-openrouter/jsonschema"
|
||||
)
|
||||
|
||||
type TitleRequest struct {
|
||||
Title *string `json:"title"`
|
||||
Messages []Message `json:"messages"`
|
||||
}
|
||||
|
||||
type TitleResponse struct {
|
||||
Title string `json:"title"`
|
||||
Cost float64 `json:"cost,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
titleReplacer = strings.NewReplacer(
|
||||
"\r", "",
|
||||
"\n", "\\n",
|
||||
"\t", "\\t",
|
||||
)
|
||||
|
||||
titleSchema, _ = jsonschema.GenerateSchema[TitleResponse]()
|
||||
)
|
||||
|
||||
func HandleTitle(w http.ResponseWriter, r *http.Request) {
|
||||
debug("parsing title")
|
||||
|
||||
var raw TitleRequest
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
|
||||
RespondJson(w, http.StatusBadRequest, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
debug("preparing request")
|
||||
|
||||
messages := make([]string, 0, len(raw.Messages))
|
||||
|
||||
for _, message := range raw.Messages {
|
||||
switch message.Role {
|
||||
case "system", "assistant", "user":
|
||||
text := message.Text
|
||||
|
||||
if len(message.Files) != 0 {
|
||||
if text != "" {
|
||||
text += "\n"
|
||||
}
|
||||
|
||||
files := make([]string, len(message.Files))
|
||||
|
||||
for i, file := range message.Files {
|
||||
files[i] = file.Name
|
||||
}
|
||||
|
||||
text += fmt.Sprintf("FILES: %s", strings.Join(files, ", "))
|
||||
}
|
||||
|
||||
if text != "" {
|
||||
text = strings.TrimSpace(text)
|
||||
text = titleReplacer.Replace(text)
|
||||
|
||||
messages = append(messages, fmt.Sprintf("%s: %s", strings.ToUpper(message.Role), text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(messages) == 0 {
|
||||
RespondJson(w, http.StatusBadRequest, map[string]any{
|
||||
"error": "no valid messages",
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var prompt strings.Builder
|
||||
|
||||
if err := InternalTitleTmpl.Execute(&prompt, raw); err != nil {
|
||||
RespondJson(w, http.StatusInternalServerError, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
request := openrouter.ChatCompletionRequest{
|
||||
Model: env.Settings.TitleModel,
|
||||
Messages: []openrouter.ChatCompletionMessage{
|
||||
openrouter.SystemMessage(prompt.String()),
|
||||
openrouter.UserMessage(strings.Join(messages, "\n")),
|
||||
},
|
||||
Temperature: 0.25,
|
||||
MaxTokens: 100,
|
||||
ResponseFormat: &openrouter.ChatCompletionResponseFormat{
|
||||
Type: openrouter.ChatCompletionResponseFormatTypeJSONSchema,
|
||||
JSONSchema: &openrouter.ChatCompletionResponseFormatJSONSchema{
|
||||
Name: "chat_title",
|
||||
Schema: titleSchema,
|
||||
Strict: true,
|
||||
},
|
||||
},
|
||||
Usage: &openrouter.IncludeUsage{
|
||||
Include: true,
|
||||
},
|
||||
}
|
||||
|
||||
if raw.Title != nil {
|
||||
request.Temperature = 0.4
|
||||
}
|
||||
|
||||
dump("title.json", request)
|
||||
|
||||
debug("generating title")
|
||||
|
||||
response, err := OpenRouterRun(r.Context(), request)
|
||||
if err != nil {
|
||||
RespondJson(w, http.StatusInternalServerError, map[string]any{
|
||||
"error": err.Error(),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
choice := response.Choices[0].Message.Content.Text
|
||||
cost := response.Usage.Cost
|
||||
|
||||
var result TitleResponse
|
||||
|
||||
err = json.Unmarshal([]byte(choice), &result)
|
||||
if err != nil {
|
||||
RespondJson(w, http.StatusInternalServerError, map[string]any{
|
||||
"error": err.Error(),
|
||||
"cost": cost,
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
result.Cost = cost
|
||||
|
||||
RespondJson(w, http.StatusOK, result)
|
||||
}
|
Reference in New Issue
Block a user