nub/internal/llm/openai/adapter.go
Tom a97013d876 initial commit
- v 0.1.0 siehe CHANGELOG.md
2026-07-25 11:08:01 +02:00

98 lines
2.3 KiB
Go

package openai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"iter"
"net/http"
"time"
"nub/internal/llm"
)
type Adapter struct {
name string
baseURL string
apiKey string
caps llm.Caps
httpClient *http.Client
}
func New(name, baseURL, apiKey string, caps llm.Caps) *Adapter {
return &Adapter{
name: name,
baseURL: baseURL,
apiKey: apiKey,
caps: caps,
httpClient: &http.Client{Timeout: 10 * time.Minute},
}
}
func (a *Adapter) Name() string { return a.name }
func (a *Adapter) Caps() llm.Caps { return a.caps }
// Stream öffnet die HTTP-Verbindung synchron (damit Verbindungsfehler sofort
// als error zurückkommen) und liefert einen Iterator über die synthetisierten
// Events. Retry ist bewusst nicht Teil dieser Methode (E-12) — siehe
// withRetry im Loop-Aufrufer.
func (a *Adapter) Stream(ctx context.Context, req llm.Request) (iter.Seq2[llm.Event, error], error) {
wireReq := buildRequest(req, a.caps)
body, err := json.Marshal(wireReq)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, a.baseURL+"/chat/completions", bytes.NewReader(body))
if err != nil {
return nil, fmt.Errorf("build http request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/json")
if a.apiKey != "" && a.apiKey != "none" {
httpReq.Header.Set("Authorization", "Bearer "+a.apiKey)
}
resp, err := a.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
if resp.StatusCode >= 300 {
defer resp.Body.Close()
payload, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
return nil, &StatusError{Code: resp.StatusCode, Body: string(payload)}
}
dec := newDecoder(resp.Body)
return func(yield func(llm.Event, error) bool) {
defer resp.Body.Close()
for {
events, more, err := dec.next()
if err != nil {
yield(nil, err)
return
}
for _, ev := range events {
if !yield(ev, nil) {
return
}
}
if !more {
return
}
}
}, nil
}
// StatusError trägt den HTTP-Status, damit der Retry-Layer (E-12, 5.9) ihn
// klassifizieren kann, ohne Strings zu parsen.
type StatusError struct {
Code int
Body string
}
func (e *StatusError) Error() string {
return fmt.Sprintf("openai: http %d: %s", e.Code, e.Body)
}