Skip to content

Commit

Permalink
Add OpenRouter insights integration
Browse files Browse the repository at this point in the history
  • Loading branch information
pijng committed Feb 26, 2025
1 parent 3d6d9a7 commit b9d90cc
Show file tree
Hide file tree
Showing 6 changed files with 162 additions and 25 deletions.
4 changes: 2 additions & 2 deletions internal/adapters/insights/gemini.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

// TODO: Consider if this should be customizable
const geminiModel = "gemini-1.5-flash"
const baseURI = "https://generativelanguage.googleapis.com/v1beta/models/"
const geminiURI = "https://generativelanguage.googleapis.com/v1beta/models/"
const action = "generateContent"
const requestMethod = http.MethodPost
const contentTypeKey = "Content-Type"
Expand Down Expand Up @@ -42,7 +42,7 @@ type GeminiCandidate struct {
}

func (g *GeminiInsightsAdapter) GenerateContent(ctx context.Context, prompt string, httpClient *http.Client) (string, error) {
req, err := g.buildRequest(ctx, baseURI, geminiModel, g.token, action, requestMethod, contentTypeKey, contentTypeValue, prompt)
req, err := g.buildRequest(ctx, geminiURI, geminiModel, g.token, action, requestMethod, contentTypeKey, contentTypeValue, prompt)
if err != nil {
return "", fmt.Errorf("preparing request: %w", err)
}
Expand Down
126 changes: 126 additions & 0 deletions internal/adapters/insights/openrouter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package insights

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"moonlogs/internal/lib/serialize"
"net/http"
"net/url"
"strings"
)

const openRouterUri = "https://openrouter.ai/api/v1/chat/completions"
const acceptKey = "Accept"
const authKey = "Authorization"
const bearerPrefix = "Bearer "

type OpenRouterInsightsAdapter struct {
token string
model string
}

func NewOpenRouterInsightsAdapter(token string, model string) *OpenRouterInsightsAdapter {
return &OpenRouterInsightsAdapter{token: token, model: model}
}

type OpenRouterResponse struct {
Choices []*OpenRouterChoice `json:"choices"`
}

type OpenRouterChoice struct {
Message *OpenRouterContentMessage `json:"message"`
}

type OpenRouterContentMessage struct {
Content string `json:"content"`
}

func (a *OpenRouterInsightsAdapter) GenerateContent(ctx context.Context, prompt string, httpClient *http.Client) (string, error) {
req, err := a.buildRequest(ctx, openRouterUri, a.model, a.token, requestMethod, contentTypeKey, contentTypeValue, prompt)
if err != nil {
return "", fmt.Errorf("preparing request: %w", err)
}

resp, err := httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("sending request: %w", err)
}
defer resp.Body.Close()

b, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("reading response body: %w", err)
}

// OpenRouter returns strange responses with a lot of new lines at the beginning of response body.
// Trim leading spaces.
respStr := string(b)
respStr = strings.TrimSpace(respStr)

var openRouterResp OpenRouterResponse
err = serialize.NewJSONDecoder(bytes.NewBufferString(respStr)).Decode(&openRouterResp)
if err != nil {
return "", fmt.Errorf("decoding open router response: %w", err)
}

if len(openRouterResp.Choices) == 0 {
return "", errors.New("empty choices")
}

choice := openRouterResp.Choices[0]
if choice.Message == nil {
return "", errors.New("empty choice message")
}

content := choice.Message.Content
if len(content) == 0 {
return "", errors.New("empty choice message content")
}

return content, nil
}

type OpenRouterRequest struct {
Model string `json:"model"`
Messages []OpenRouterMessage `json:"messages"`
}

type OpenRouterMessage struct {
Role string `json:"role"`
Content []OpenRouterContent `json:"content"`
}

type OpenRouterContent struct {
Type string `json:"type"`
Text string `json:"text"`
}

func (a *OpenRouterInsightsAdapter) buildRequest(ctx context.Context, baseURI, modelName, token, requestMethod, contentTypeKey, contentTypeValue, propmt string) (*http.Request, error) {
_, err := url.Parse(baseURI)
if err != nil {
return nil, fmt.Errorf("parsing base uri: %w", err)
}

content := OpenRouterContent{Text: propmt, Type: "text"}
message := OpenRouterMessage{Role: "user", Content: []OpenRouterContent{content}}
payload := OpenRouterRequest{Model: modelName, Messages: []OpenRouterMessage{message}}

jsonData, err := serialize.JSONMarshal(payload)
if err != nil {
return nil, fmt.Errorf("marshalling payload body: %w", err)
}

req, err := http.NewRequestWithContext(ctx, requestMethod, baseURI, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("building request with timeout: %w", err)
}

req.Header.Set(contentTypeKey, contentTypeValue)
req.Header.Set(acceptKey, contentTypeValue)
req.Header.Set(authKey, bearerPrefix+token)

return req, nil
}
2 changes: 1 addition & 1 deletion internal/api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ info:
license:
name: Apache 2.0
url: http://www.apache.org/licenses/LICENSE-2.0.html
version: 1.22.0
version: 1.23.0
externalDocs:
description: Find out more about spec
url: https://moonlogs.pages.dev
Expand Down
50 changes: 29 additions & 21 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (
DB_PATH = "/var/lib/moonlogs/database.sqlite"
CONFIG_PATH = "/etc/moonlogs/config.yaml"
GEMINI_TOKEN = ""
OPEN_ROUTER_TOKEN = ""
OPEN_ROUTER_MODEL = ""
INSIGHTS_PROXY_USER = ""
INSIGHTS_PROXY_PASS = ""
INSIGHTS_PROXY_HOST = ""
Expand All @@ -29,16 +31,18 @@ const (
var config *Config

type Config struct {
Port int `yaml:"port"`
DBPath string `yaml:"db_path"`
DBAdapter string `yaml:"db_adapter"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
GeminiToken string `yaml:"gemini_token"`
ProxyUser string `yaml:"proxy_user"`
ProxyPass string `yaml:"proxy_pass"`
ProxyHost string `yaml:"proxy_host"`
ProxyPort string `yaml:"proxy_port"`
Port int `yaml:"port"`
DBPath string `yaml:"db_path"`
DBAdapter string `yaml:"db_adapter"`
ReadTimeout time.Duration `yaml:"read_timeout"`
WriteTimeout time.Duration `yaml:"write_timeout"`
GeminiToken string `yaml:"gemini_token"`
OpenRouterToken string `yaml:"open_router_token"`
OpenRouterModel string `yaml:"open_router_model"`
ProxyUser string `yaml:"insights_proxy_user"`
ProxyPass string `yaml:"insights_proxy_pass"`
ProxyHost string `yaml:"insights_proxy_host"`
ProxyPort string `yaml:"insights_proxy_port"`
}

func Load() (*Config, error) {
Expand Down Expand Up @@ -106,17 +110,19 @@ write_timeout: %s
}

type args struct {
Config string
Port int
DBPath string
DBAdapter string
ReadTimeout time.Duration
WriteTimeout time.Duration
GeminiToken string
ProxyUser string
ProxyPass string
ProxyHost string
ProxyPort string
Config string
Port int
DBPath string
DBAdapter string
ReadTimeout time.Duration
WriteTimeout time.Duration
GeminiToken string
OpenRouterToken string
OpenRouterModel string
ProxyUser string
ProxyPass string
ProxyHost string
ProxyPort string
}

func processArgs() (args, error) {
Expand All @@ -130,6 +136,8 @@ func processArgs() (args, error) {
f.DurationVar(&a.WriteTimeout, "write-timeout", WRITE_TIMEOUT, "write timeout duration")
f.DurationVar(&a.ReadTimeout, "read-timeout", READ_TIMEOUT, "read timeout duration")
f.StringVar(&a.GeminiToken, "gemini-token", GEMINI_TOKEN, "token to access Gemini API")
f.StringVar(&a.OpenRouterToken, "open-router-token", OPEN_ROUTER_TOKEN, "token to access OpenRouter API")
f.StringVar(&a.OpenRouterModel, "open-router-model", OPEN_ROUTER_MODEL, "model of OpenRouter API")
f.StringVar(&a.ProxyUser, "insights-proxy-user", INSIGHTS_PROXY_USER, "proxy user for insights API")
f.StringVar(&a.ProxyPass, "insights-proxy-pass", INSIGHTS_PROXY_PASS, "proxy pass for insights API")
f.StringVar(&a.ProxyHost, "insights-proxy-host", INSIGHTS_PROXY_HOST, "proxy host for insights API")
Expand Down
3 changes: 3 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ func main() {
if cfg.GeminiToken != "" {
insightsAdapter = insights.NewGeminiInsightsAdapter(cfg.GeminiToken)
}
if cfg.OpenRouterToken != "" && cfg.OpenRouterModel != "" {
insightsAdapter = insights.NewOpenRouterInsightsAdapter(cfg.OpenRouterToken, cfg.OpenRouterModel)
}

proxyCfg := usecases.ProxyCfg{
ProxyUser: cfg.ProxyUser,
Expand Down
2 changes: 1 addition & 1 deletion web/src/features/insights-builder/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const summarizeLogsAiFx = createEffect(({ lang, logs }: { lang: string; logs: Lo
})
.join("\n");

const prompt = `Write short summary of these logs in '${lang}' language, try to highlight problems if there are any error level logs, use only new lines as markup:\n\n${logsText}`;
const prompt = `Write really short summary of these logs in '${lang}' language, try to highlight problems if there are any error level logs, dont use markdown markup, only new lines:\n\n${logsText}`;

return generateContent(prompt);
});
Expand Down

0 comments on commit b9d90cc

Please sign in to comment.