(parent context.Context, msgs []chmctx.Message, tools []Tool, out chan<- Event)
| 290 | }) |
| 291 | if err != nil { |
| 292 | return ProbeResult{Budget: budget}, err |
| 293 | } |
| 294 | defer resp.Body.Close() |
| 295 | return ProbeResult{ |
| 296 | ContextWindow: cloud.ContextWindowFromHeaders(resp.Header), |
| 297 | Budget: cloud.FromHeaders(resp.Header), |
| 298 | }, nil |
| 299 | } |
| 300 | |
| 301 | // Chat streams an assistant response on the returned channel, closing it when |
| 302 | // the stream ends. Reasoning runs at `high` effort by default; if the server |
| 303 | // rejects the tools + reasoning_effort combo (newer OpenAI models do), postChat |
| 304 | // drops reasoning_effort for this Client's lifetime so the model still works, |
| 305 | // with tools but no reasoning. Staying on chat-completions is the product line; |
| 306 | // we do not branch to /v1/responses to keep reasoning. |
| 307 | func (c *Client) Chat(parent context.Context, messages []chmctx.Message, tools []Tool) <-chan Event { |
| 308 | out := make(chan Event, 32) |
| 309 | go c.run(parent, messages, tools, out) |
| 310 | return out |
| 311 | } |
| 312 | |
| 313 | func (c *Client) run(parent context.Context, msgs []chmctx.Message, tools []Tool, out chan<- Event) { |
| 314 | defer close(out) |
| 315 | start := time.Now() |
| 316 | |
| 317 | // Pre-stream retry: transient failures (proxy hiccups: 5xx, 429, LiteLLM's |
| 318 | // transient 404) are resent after a rising backoff instead of killing the |
| 319 | // turn. Only here, before any token has streamed — a mid-stream resend |
| 320 | // would duplicate content already in the transcript. Probe stays |
| 321 | // retry-free: its job is fast feedback on a misconfigured profile. |
| 322 | resp, errEvt := c.sendChat(parent, msgs, tools) |
| 323 | for attempt := 0; errEvt != nil && attempt < len(c.RetryBackoff) && retryable(errEvt.Err); attempt++ { |
| 324 | delay := c.RetryBackoff[attempt] |
| 325 | hint := fmt.Sprintf("retry %d/%d in %s", attempt+1, len(c.RetryBackoff), delay) |
| 326 | if !sendEvent(parent, out, Event{Kind: EventRetry, Content: hint, Err: errEvt.Err}) { |
| 327 | return |
| 328 | } |
| 329 | select { |
| 330 | case <-time.After(delay): |
| 331 | case <-parent.Done(): |
| 332 | // Ctrl+C during the wait: unwind silently, exactly like a |
| 333 | // cancelled sendEvent — the TUI already aborted the turn. |
| 334 | return |
| 335 | } |
| 336 | resp, errEvt = c.sendChat(parent, msgs, tools) |
| 337 | } |
| 338 | if errEvt != nil { |
| 339 | sendEvent(parent, out, *errEvt) |
| 340 | return |
| 341 | } |
no test coverage detected