openai call

This commit is contained in:
lehel 2025-10-08 22:18:40 +02:00
parent edc9d3d667
commit 1484b519d7
No known key found for this signature in database
GPG Key ID: 9C4F9D6111EE5CFA
1 changed files with 116 additions and 8 deletions

View File

@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -90,12 +91,25 @@ func (llm *OpenAIClient) openAICompletion(ctx context.Context, prompt string, fo
if apiURL == "" { if apiURL == "" {
apiURL = "https://api.openai.com/v1/chat/completions" apiURL = "https://api.openai.com/v1/chat/completions"
} }
isOpenAIStyle := strings.Contains(apiURL, "openrouter.ai") || strings.Contains(apiURL, "/v1/")
// Helper to stringify the expected JSON schema for instructions // Helper to stringify the expected JSON schema for instructions
schemaDesc := func() string { schemaDesc := func() string {
b, _ := json.MarshalIndent(format, "", " ") b, _ := json.MarshalIndent(format, "", " ")
return string(b) return string(b)
} }
body := map[string]interface{}{
truncate := func(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "...<truncated>"
}
buildBody := func() map[string]interface{} {
if isOpenAIStyle {
return map[string]interface{}{
"model": llm.Model, "model": llm.Model,
"messages": []map[string]string{ "messages": []map[string]string{
{"role": "system", "content": "You are a strict JSON generator. ONLY output valid JSON matching this schema: " + schemaDesc() + " Do not add explanations."}, {"role": "system", "content": "You are a strict JSON generator. ONLY output valid JSON matching this schema: " + schemaDesc() + " Do not add explanations."},
@ -103,7 +117,30 @@ func (llm *OpenAIClient) openAICompletion(ctx context.Context, prompt string, fo
}, },
"response_format": map[string]interface{}{"type": "json_object"}, "response_format": map[string]interface{}{"type": "json_object"},
} }
}
// This should never be reached in OpenAI client but keeping for safety
return map[string]interface{}{
"model": llm.Model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"stream": false,
"format": format,
}
}
body := buildBody()
// Enhanced logging similar to the unified client
jsonBody, _ := json.Marshal(body) jsonBody, _ := json.Marshal(body)
bodySize := len(jsonBody)
logrus.WithFields(logrus.Fields{
"event": "llm_request",
"api_url": apiURL,
"model": llm.Model,
"is_openai_style": isOpenAIStyle,
"prompt_len": len(prompt),
"body_size": bodySize,
}).Info("[LLM] sending request")
req, _ := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewBuffer(jsonBody)) req, _ := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewBuffer(jsonBody))
if llm.APIKey != "" { if llm.APIKey != "" {
req.Header.Set("Authorization", "Bearer "+llm.APIKey) req.Header.Set("Authorization", "Bearer "+llm.APIKey)
@ -114,16 +151,39 @@ func (llm *OpenAIClient) openAICompletion(ctx context.Context, prompt string, fo
req.Header.Set("Referer", "https://github.com/") req.Header.Set("Referer", "https://github.com/")
req.Header.Set("X-Title", "vetrag-app") req.Header.Set("X-Title", "vetrag-app")
} }
start := time.Now()
client := &http.Client{} client := &http.Client{}
resp, err := client.Do(req) resp, err := client.Do(req)
dur := time.Since(start)
if err != nil { if err != nil {
logrus.WithFields(logrus.Fields{
"event": "llm_response",
"status": 0,
"latency_ms": dur.Milliseconds(),
"error": err,
}).Error("[LLM] request failed")
return "", err return "", err
} }
defer resp.Body.Close() defer resp.Body.Close()
raw, err := io.ReadAll(resp.Body) raw, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return "", err return "", err
} }
logrus.WithFields(logrus.Fields{
"event": "llm_raw_response",
"status": resp.StatusCode,
"latency_ms": dur.Milliseconds(),
"raw_trunc": truncate(string(raw), 600),
"raw_len": len(raw),
}).Debug("[LLM] raw response body")
parseVariant := "unknown"
// Attempt OpenAI/OpenRouter style parse first
var openAI struct { var openAI struct {
Choices []struct { Choices []struct {
Message struct { Message struct {
@ -137,18 +197,66 @@ func (llm *OpenAIClient) openAICompletion(ctx context.Context, prompt string, fo
} }
if err := json.Unmarshal(raw, &openAI); err == nil { if err := json.Unmarshal(raw, &openAI); err == nil {
if openAI.Error != nil || resp.StatusCode >= 400 { if openAI.Error != nil || resp.StatusCode >= 400 {
parseVariant = "openai"
var msg string var msg string
if openAI.Error != nil { if openAI.Error != nil {
msg = openAI.Error.Message msg = openAI.Error.Message
} else { } else {
msg = string(raw) msg = string(raw)
} }
logrus.WithFields(logrus.Fields{
"event": "llm_response",
"status": resp.StatusCode,
"latency_ms": dur.Milliseconds(),
"parse_variant": parseVariant,
"error": msg,
}).Error("[LLM] provider error")
return "", fmt.Errorf("provider error: %s", msg) return "", fmt.Errorf("provider error: %s", msg)
} }
if len(openAI.Choices) > 0 && openAI.Choices[0].Message.Content != "" { if len(openAI.Choices) > 0 && openAI.Choices[0].Message.Content != "" {
return openAI.Choices[0].Message.Content, nil parseVariant = "openai"
content := openAI.Choices[0].Message.Content
logrus.WithFields(logrus.Fields{
"event": "llm_response",
"status": resp.StatusCode,
"latency_ms": dur.Milliseconds(),
"parse_variant": parseVariant,
"content_len": len(content),
"content_snip": truncate(content, 300),
}).Info("[LLM] parsed response")
return content, nil
} }
} }
// As a fallback, attempt Ollama format parse
var ollama struct {
Message struct {
Content string `json:"content"`
} `json:"message"`
Error string `json:"error"`
}
if err := json.Unmarshal(raw, &ollama); err == nil && ollama.Message.Content != "" {
parseVariant = "ollama"
content := ollama.Message.Content
logrus.WithFields(logrus.Fields{
"event": "llm_response",
"status": resp.StatusCode,
"latency_ms": dur.Milliseconds(),
"parse_variant": parseVariant,
"content_len": len(content),
"content_snip": truncate(content, 300),
}).Info("[LLM] parsed response")
return content, nil
}
logrus.WithFields(logrus.Fields{
"event": "llm_response",
"status": resp.StatusCode,
"latency_ms": dur.Milliseconds(),
"parse_variant": parseVariant,
"raw_snip": truncate(string(raw), 300),
}).Error("[LLM] unrecognized response format")
return "", fmt.Errorf("unrecognized LLM response format: %.200s", string(raw)) return "", fmt.Errorf("unrecognized LLM response format: %.200s", string(raw))
} }