From 840b85887b67f28a72f6747fe7ddbe83dffbbb6b Mon Sep 17 00:00:00 2001 From: Grail Finder Date: Mon, 23 Jun 2025 12:05:20 +0300 Subject: [PATCH] Enha: add openrouter parser --- llmapi/main.go | 57 ++------------------------------ llmapi/models.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ llmapi/parser.go | 37 +++++++++++++++++++++ 3 files changed, 125 insertions(+), 55 deletions(-) create mode 100644 llmapi/models.go diff --git a/llmapi/main.go b/llmapi/main.go index 210d2ab..8b67ca1 100644 --- a/llmapi/main.go +++ b/llmapi/main.go @@ -27,61 +27,6 @@ var ( GuesserSimplePrompt = `we are playing game of alias;\n you were given a clue: \"%s\";\nplease return your guess and words that could be meant by the clue, but you do not wish to open yet, in json like:\n{\n\"guess\": \"most_relevant_word_to_the_clue\",\n\"could_be\": [\"this\", \"that\", ...]\n}\nhere is the words that left:\n%s` ) -type DSResp struct { - ID string `json:"id"` - Choices []struct { - Text string `json:"text"` - Index int `json:"index"` - FinishReason string `json:"finish_reason"` - } `json:"choices"` - Created int `json:"created"` - Model string `json:"model"` - SystemFingerprint string `json:"system_fingerprint"` - Object string `json:"object"` -} - -type LLMResp struct { - Index int `json:"index"` - Content string `json:"content"` - Tokens []any `json:"tokens"` - IDSlot int `json:"id_slot"` - Stop bool `json:"stop"` - Model string `json:"model"` - TokensPredicted int `json:"tokens_predicted"` - TokensEvaluated int `json:"tokens_evaluated"` - Prompt string `json:"prompt"` - HasNewLine bool `json:"has_new_line"` - Truncated bool `json:"truncated"` - StopType string `json:"stop_type"` - StoppingWord string `json:"stopping_word"` - TokensCached int `json:"tokens_cached"` -} - -type MimeResp struct { - Clue string `json:"clue"` - Number string `json:"number"` - Answer []string `json:"words_I_mean_my_team_to_open"` -} - -type GusserResp struct { - Guesses []string `json:"guesses"` - CouldBe []string `json:"could_be"` -} - -type Bot struct { - Role string `json:"role"` - Team string `json:"team"` - cfg *config.Config `json:"-"` - RoomID string `json:"room_id"` // can we get a room from here? - BotName string `json:"bot_name"` - log *slog.Logger `json:"-"` - LLMParser RespParser `json:"-"` - // channels for communicaton - // channels are not serializable - // SignalsCh chan bool - // DoneCh chan bool -} - func convertToSliceOfStrings(value any) ([]string, error) { switch v := value.(type) { case []string: @@ -299,6 +244,8 @@ func NewBot(role, team, name, roomID string, cfg *config.Config, recovery bool) bot.LLMParser = NewLCPRespParser(bot.log) if strings.Contains(cfg.LLMConfig.URL, "api.deepseek.com") { bot.LLMParser = NewDeepSeekParser(bot.log) + } else if strings.Contains(cfg.LLMConfig.URL, "openrouter.ai") { + bot.LLMParser = NewOpenRouterParser(bot.log) } // add to room room, err := getRoomByID(bot.RoomID) diff --git a/llmapi/models.go b/llmapi/models.go new file mode 100644 index 0000000..1c80f0e --- /dev/null +++ b/llmapi/models.go @@ -0,0 +1,86 @@ +package llmapi + +import ( + "gralias/config" + "log/slog" +) + +type OpenRouterResp struct { + ID string `json:"id"` + Provider string `json:"provider"` + Model string `json:"model"` + Object string `json:"object"` + Created int `json:"created"` + Choices []struct { + Logprobs any `json:"logprobs"` + FinishReason string `json:"finish_reason"` + NativeFinishReason string `json:"native_finish_reason"` + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + Refusal any `json:"refusal"` + Reasoning any `json:"reasoning"` + } `json:"message"` + } `json:"choices"` + Usage struct { + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` + } `json:"usage"` +} + +type DSResp struct { + ID string `json:"id"` + Choices []struct { + Text string `json:"text"` + Index int `json:"index"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Created int `json:"created"` + Model string `json:"model"` + SystemFingerprint string `json:"system_fingerprint"` + Object string `json:"object"` +} + +type LLMResp struct { + Index int `json:"index"` + Content string `json:"content"` + Tokens []any `json:"tokens"` + IDSlot int `json:"id_slot"` + Stop bool `json:"stop"` + Model string `json:"model"` + TokensPredicted int `json:"tokens_predicted"` + TokensEvaluated int `json:"tokens_evaluated"` + Prompt string `json:"prompt"` + HasNewLine bool `json:"has_new_line"` + Truncated bool `json:"truncated"` + StopType string `json:"stop_type"` + StoppingWord string `json:"stopping_word"` + TokensCached int `json:"tokens_cached"` +} + +type MimeResp struct { + Clue string `json:"clue"` + Number string `json:"number"` + Answer []string `json:"words_I_mean_my_team_to_open"` +} + +type GusserResp struct { + Guesses []string `json:"guesses"` + CouldBe []string `json:"could_be"` +} + +type Bot struct { + Role string `json:"role"` + Team string `json:"team"` + cfg *config.Config `json:"-"` + RoomID string `json:"room_id"` // can we get a room from here? + BotName string `json:"bot_name"` + log *slog.Logger `json:"-"` + LLMParser RespParser `json:"-"` + // channels for communicaton + // channels are not serializable + // SignalsCh chan bool + // DoneCh chan bool +} diff --git a/llmapi/parser.go b/llmapi/parser.go index 5ee9003..8f41659 100644 --- a/llmapi/parser.go +++ b/llmapi/parser.go @@ -88,3 +88,40 @@ func (p *lcpRespParser) ParseBytes(body []byte) (map[string]any, error) { } return respMap, nil } + +type openRouterParser struct { + log *slog.Logger +} + +func NewOpenRouterParser(log *slog.Logger) *openRouterParser { + return &openRouterParser{log: log} +} + +func (p *openRouterParser) ParseBytes(body []byte) (map[string]any, error) { + // parsing logic here + resp := OpenRouterResp{} + if err := json.Unmarshal(body, &resp); err != nil { + p.log.Error("failed to unmarshal", "error", err) + return nil, err + } + if len(resp.Choices) == 0 { + p.log.Error("empty choices", "resp", resp) + err := errors.New("empty choices in resp") + return nil, err + } + text := resp.Choices[0].Message.Content + li := strings.Index(text, "{") + ri := strings.LastIndex(text, "}") + if li < 0 || ri < 1 { + p.log.Error("not a json", "msg", text) + err := fmt.Errorf("fn: ParseBytes, not a json; data: %s", text) + return nil, err + } + sj := text[li : ri+1] + respMap := make(map[string]any) + if err := json.Unmarshal([]byte(sj), &respMap); err != nil { + p.log.Error("failed to unmarshal response", "error", err, "string-json", sj) + return nil, err + } + return respMap, nil +}