Add streaming Ollama response with live post updates
This commit is contained in:
@@ -54,6 +54,13 @@ type ListModelsResponse struct {
|
||||
} `json:"models"`
|
||||
}
|
||||
|
||||
// StreamEvent represents a single event from a streaming chat response.
|
||||
type StreamEvent struct {
|
||||
Token string // delta content from the model
|
||||
Done bool // true for the final event
|
||||
Error error // non-nil if the stream failed
|
||||
}
|
||||
|
||||
// ChatCompletion sends a chat request and returns the full response.
|
||||
func (c *OllamaClient) ChatCompletion(req *ChatRequest) (*ChatResponse, error) {
|
||||
req.Stream = false
|
||||
@@ -87,6 +94,72 @@ func (c *OllamaClient) ChatCompletion(req *ChatRequest) (*ChatResponse, error) {
|
||||
return &chatResp, nil
|
||||
}
|
||||
|
||||
// streamingChunk represents a single line from Ollama's ndjson stream.
|
||||
type streamingChunk struct {
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"message"`
|
||||
Done bool `json:"done"`
|
||||
EvalCount int `json:"eval_count"`
|
||||
PromptEvalCount int `json:"prompt_eval_count"`
|
||||
}
|
||||
|
||||
// ChatCompletionStream sends a streaming chat request and returns a channel
|
||||
// of StreamEvents. The caller must drain the channel until Done or Error.
|
||||
func (c *OllamaClient) ChatCompletionStream(req *ChatRequest) (<-chan StreamEvent, error) {
|
||||
req.Stream = true
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequest("POST", c.baseURL+"/api/chat", strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("ollama returned status %d: %s", resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||||
}
|
||||
|
||||
ch := make(chan StreamEvent)
|
||||
go func() {
|
||||
defer resp.Body.Close()
|
||||
defer close(ch)
|
||||
|
||||
decoder := json.NewDecoder(resp.Body)
|
||||
for {
|
||||
var chunk streamingChunk
|
||||
if err := decoder.Decode(&chunk); err != nil {
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
ch <- StreamEvent{Error: fmt.Errorf("decode chunk: %w", err)}
|
||||
return
|
||||
}
|
||||
|
||||
ch <- StreamEvent{
|
||||
Token: chunk.Message.Content,
|
||||
Done: chunk.Done,
|
||||
}
|
||||
|
||||
if chunk.Done {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// ListModels fetches the list of available models from Ollama.
|
||||
func (c *OllamaClient) ListModels() ([]string, error) {
|
||||
httpReq, err := http.NewRequest("GET", c.baseURL+"/api/tags", nil)
|
||||
|
||||
Reference in New Issue
Block a user