MCPcopy Create free account
hub / github.com/codehamr/codehamr / readSSE

Function readSSE

internal/llm/llm.go:468–536  ·  view source on GitHub ↗

readSSE reads OpenAI SSE frames until [DONE] or EOF, forwarding content/reasoning/tool-call events to out. Returns the final assistant message (content + accumulated tool calls), the server completion and prompt token counts, and any scanner error. parent is threaded through so sends abort on cancel

(parent context.Context, body io.Reader, budget cloud.BudgetStatus, out chan<- Event, onFrame func())

Source from the content-addressed store, hash-verified

466// own name in each dialect (OpenAI "reasoning.effort", vLLM "Unexpected
467// reasoning effort", Ollama's "<model> does not support thinking"), never on a
468// lone generic word, so an unrelated 400 that merely mentions "thinking" can't
469// latch reasoning off for the Client's whole life. Dropping the field is the
470// right remedy for all of them, but note what it costs: the server then applies
471// its own default (on the Qwen3.8 scale that is xhigh, i.e. MORE reasoning than
472// we asked for, not less). A compatibility fix, never a way to think less.
473func rejectsReasoning(errBody []byte) bool {
474 for _, sig := range []string{"reasoning.effort", "reasoning_effort", "reasoning effort", "does not support thinking"} {
475 if bytes.Contains(errBody, []byte(sig)) {
476 return true
477 }
478 }
479 return false
480}
481
482// doPost performs one round-trip, mapping status into the typed cloud errors
483// Probe and sendChat share. On 200 it returns the live response with body open
484// for streaming; on non-200 the body is drained and closed first. Budget is set
485// only on 402. errBody returns the raw body on a non-2xx other than 401/402, so
486// post can check it for the reasoning fallback signal without re-reading.
487func (c *Client) doPost(parent context.Context, body request) (*http.Response, cloud.BudgetStatus, []byte, error) {
488 buf, err := json.Marshal(body)
489 if err != nil {
490 return nil, cloud.BudgetStatus{}, nil, err
491 }
492 req, err := http.NewRequestWithContext(parent, "POST", c.BaseURL+"/v1/responses", bytes.NewReader(buf))
493 if err != nil {
494 return nil, cloud.BudgetStatus{}, nil, err
495 }
496 req.Header.Set("Content-Type", "application/json")
497 req.Header.Set("Accept", "text/event-stream")
498 if c.Token != "" {
499 req.Header.Set("Authorization", cloud.AuthHeader(c.Token))
500 }
501 resp, err := c.HTTP.Do(req)
502 if err != nil {
503 return nil, cloud.BudgetStatus{}, nil, cloud.ErrUnreachable{Err: err}
504 }
505 if resp.StatusCode == 200 {
506 return resp, cloud.BudgetStatus{}, nil, nil
507 }
508 defer resp.Body.Close()
509 switch resp.StatusCode {
510 case 401:
511 // Drain before the deferred Close so the keep-alive connection returns
512 // to the pool instead of being discarded, same as the 402/default arms.
513 _, _ = io.Copy(io.Discard, resp.Body)
514 return nil, cloud.BudgetStatus{}, nil, cloud.ErrUnauthorized
515 case 402:
516 // Pass depleted. Body ignored: the status code is the whole signal,
517 // the UI banner is fixed text, the returned snapshot reflects it.
518 _, _ = io.Copy(io.Discard, resp.Body)
519 return nil, cloud.BudgetStatus{Set: true, Remaining: 0}, nil, cloud.ErrBudgetExhausted
520 default:
521 b, _ := io.ReadAll(resp.Body)
522 msg := errorMessageFromBody(b)
523 if resp.StatusCode == 404 && strings.Contains(msg, "litellm.NotFoundError") {
524 msg += " · check LiteLLM's upstream URL and model; if that upstream only supports /chat/completions, set use_chat_completions_api: true under this model's litellm_params in the proxy config"
525 } else if resp.StatusCode == 404 {

Callers 1

runMethod · 0.85

Calls 4

dispatchDeltaFunction · 0.85
sendEventFunction · 0.85
resolveMethod · 0.80
StringMethod · 0.80

Tested by

no test coverage detected