(parent context.Context, msgs []chmctx.Message, tools []Tool, out chan<- Event)
| 290 | ContextWindow int |
| 291 | Budget cloud.BudgetStatus |
| 292 | } |
| 293 | |
| 294 | // Probe sends a minimal hello chat just to harvest response headers in one |
| 295 | // round trip: status validates the URL/model/key combo, X-Context-Window gives |
| 296 | // the live size, X-Budget-Remaining the live fraction. The body is closed |
| 297 | // unread; on the cloud proxy that may already charge one token, the cost of a |
| 298 | // single round-trip "key works AND here is your real window". Returns the |
| 299 | // standard cloud errors (Unreachable, Unauthorized, BudgetExhausted) for |
| 300 | // errors.Is branching. |
| 301 | func (c *Client) Probe(parent context.Context) (ProbeResult, error) { |
| 302 | resp, budget, err := c.post(parent, request{ |
| 303 | Model: c.Model, |
| 304 | Input: []any{messageItem{Type: "message", Role: "user", Content: "hi"}}, |
| 305 | Stream: true, |
| 306 | }) |
| 307 | if err != nil { |
| 308 | return ProbeResult{Budget: budget}, err |
| 309 | } |
| 310 | defer resp.Body.Close() |
| 311 | return ProbeResult{ |
| 312 | ContextWindow: cloud.ContextWindowFromHeaders(resp.Header), |
| 313 | Budget: cloud.FromHeaders(resp.Header), |
| 314 | }, nil |
| 315 | } |
| 316 | |
| 317 | // Chat streams an assistant response on the returned channel, closing it when |
| 318 | // the stream ends. Reasoning runs at `medium` effort: decode is the serialised |
| 319 | // critical path of every round, and `high` bought deliberation the agent loop |
| 320 | // already gets from seeing each tool result. If the server rejects the effort |
| 321 | // (see rejectsReasoning), post drops it for this Client's lifetime so the model |
| 322 | // still works at the server's own default. |
| 323 | func (c *Client) Chat(parent context.Context, messages []chmctx.Message, tools []Tool) <-chan Event { |
| 324 | out := make(chan Event, 32) |
| 325 | go c.run(parent, messages, tools, out) |
| 326 | return out |
| 327 | } |
| 328 | |
| 329 | func (c *Client) run(parent context.Context, msgs []chmctx.Message, tools []Tool, out chan<- Event) { |
| 330 | defer close(out) |
| 331 | start := time.Now() |
| 332 | |
| 333 | // Pre-stream retry: transient failures (proxy hiccups: 5xx, 429, LiteLLM's |
| 334 | // transient 404) are resent after a rising backoff instead of killing the |
| 335 | // turn. Only here, before any token has streamed — a mid-stream resend |
| 336 | // would duplicate content already in the transcript. Probe stays |
| 337 | // retry-free: its job is fast feedback on a misconfigured profile. |
| 338 | resp, errEvt := c.sendChat(parent, msgs, tools) |
| 339 | for attempt := 0; errEvt != nil && attempt < len(c.RetryBackoff) && retryable(errEvt.Err); attempt++ { |
| 340 | delay := c.RetryBackoff[attempt] |
| 341 | hint := fmt.Sprintf("retry %d/%d in %s", attempt+1, len(c.RetryBackoff), delay) |
no test coverage detected