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) }