sync: migrate ai-operator to Gitea (2026-08-10)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package llm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"ai-operator/internal/config"
|
||||
)
|
||||
|
||||
type OpenAIStreaming struct {
|
||||
cfg config.Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewOpenAIStreaming(cfg config.Config) *OpenAIStreaming {
|
||||
return &OpenAIStreaming{cfg: cfg, client: &http.Client{Timeout: cfg.LLM.Timeout}}
|
||||
}
|
||||
|
||||
func (p *OpenAIStreaming) StreamGenerate(ctx context.Context, req GenerateRequest) (<-chan Event, error) {
|
||||
if p.cfg.OpenAI.APIKey == "" {
|
||||
return nil, errors.New("OPENAI_API_KEY is required for streaming LLM")
|
||||
}
|
||||
out := make(chan Event, 64)
|
||||
go p.run(ctx, req, out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (p *OpenAIStreaming) run(ctx context.Context, req GenerateRequest, out chan<- Event) {
|
||||
defer close(out)
|
||||
payload := map[string]any{
|
||||
"model": p.cfg.LLM.Model,
|
||||
"temperature": p.cfg.LLM.Temperature,
|
||||
"max_tokens": p.cfg.LLM.MaxOutputTokens,
|
||||
"stream": true,
|
||||
}
|
||||
msgs := []map[string]string{{"role": "system", "content": req.SystemPrompt}}
|
||||
for _, m := range req.Messages {
|
||||
if strings.TrimSpace(m.Content) != "" {
|
||||
msgs = append(msgs, map[string]string{"role": m.Role, "content": m.Content})
|
||||
}
|
||||
}
|
||||
payload["messages"] = msgs
|
||||
body, _ := json.Marshal(payload)
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.openai.com/v1/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
out <- Event{Type: EventError, Error: err.Error()}
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Authorization", "Bearer "+p.cfg.OpenAI.APIKey)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
resp, err := p.client.Do(httpReq)
|
||||
if err != nil {
|
||||
out <- Event{Type: EventError, Error: err.Error()}
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode > 299 {
|
||||
out <- Event{Type: EventError, Error: "openai streaming llm returned non-2xx"}
|
||||
return
|
||||
}
|
||||
sc := bufio.NewScanner(resp.Body)
|
||||
for sc.Scan() {
|
||||
line := strings.TrimSpace(sc.Text())
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
continue
|
||||
}
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if data == "[DONE]" {
|
||||
out <- Event{Type: EventDone}
|
||||
return
|
||||
}
|
||||
var chunk struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"delta"`
|
||||
} `json:"choices"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
for _, c := range chunk.Choices {
|
||||
if c.Delta.Content != "" {
|
||||
out <- Event{Type: EventTextDelta, Text: c.Delta.Content}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := sc.Err(); err != nil {
|
||||
out <- Event{Type: EventError, Error: err.Error()}
|
||||
return
|
||||
}
|
||||
out <- Event{Type: EventDone}
|
||||
}
|
||||
Reference in New Issue
Block a user