(ctx context.Context, text, model string)
| 193 | } |
| 194 | |
| 195 | func (a *Adapter) invokeStream(ctx context.Context, text, model string) (types.BackendResponse, error) { |
| 196 | t0 := time.Now() |
| 197 | payload := map[string]any{ |
| 198 | "contents": []map[string]any{ |
| 199 | { |
| 200 | "role": "user", |
| 201 | "parts": []map[string]string{{"text": text}}, |
| 202 | }, |
| 203 | }, |
| 204 | } |
| 205 | raw, err := json.Marshal(payload) |
| 206 | if err != nil { |
| 207 | return types.BackendResponse{}, err |
| 208 | } |
| 209 | url := a.baseURL + a.actionPath(model, "streamGenerateContent") + "?alt=sse" |
| 210 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw)) |
| 211 | if err != nil { |
| 212 | return types.BackendResponse{}, err |
| 213 | } |
| 214 | httpReq.Header.Set("Content-Type", "application/json") |
| 215 | a.applyAPIKey(httpReq) |
| 216 | a.applyExtraHeaders(httpReq) |
| 217 | |
| 218 | resp, err := a.httpClient.Do(httpReq) |
| 219 | if err != nil { |
| 220 | if errors.Is(err, context.DeadlineExceeded) { |
| 221 | return types.BackendResponse{}, upstream.New(upstream.KindTimeout, err.Error()) |
| 222 | } |
| 223 | return types.BackendResponse{}, err |
| 224 | } |
| 225 | defer resp.Body.Close() |
| 226 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { |
| 227 | if resp.StatusCode >= 500 { |
| 228 | return types.BackendResponse{}, upstream.New(upstream.KindUpstream5xx, fmt.Sprintf("gemini stream failed status=%d", resp.StatusCode)) |
| 229 | } |
| 230 | return types.BackendResponse{}, upstream.New(upstream.KindUpstream4xx, fmt.Sprintf("gemini stream failed status=%d", resp.StatusCode)) |
| 231 | } |
| 232 | |
| 233 | ct := strings.ToLower(resp.Header.Get("Content-Type")) |
| 234 | if !strings.Contains(ct, "event-stream") && !strings.Contains(ct, "text/plain") { |
| 235 | // Fall back to single JSON body (non-SSE). |
| 236 | var parsed geminiGenerateResponse |
| 237 | if err := json.NewDecoder(resp.Body).Decode(&parsed); err != nil { |
| 238 | return types.BackendResponse{}, upstream.New(upstream.KindBackendError, err.Error()) |
| 239 | } |
| 240 | out := geminiCandidatesText(parsed.Candidates) |
| 241 | t1 := time.Now() |
| 242 | lat := t1.Sub(t0).Milliseconds() |
| 243 | return types.BackendResponse{ |
| 244 | Output: map[string]any{ |
| 245 | "text": out, |
| 246 | "backend": a.cfg.Name, |
| 247 | "stream_requested": true, |
| 248 | "stream_degraded": true, |
| 249 | }, |
| 250 | Timing: &types.BackendTiming{ |
| 251 | TTFTMs: lat, |
| 252 | CompletionLatencyMs: lat, |
no test coverage detected