v0.0.5: context compaction, minimal config, retry/search/code symbols/diff/status/test commands
This commit is contained in:
+79
-26
@@ -10,8 +10,13 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maxChatAttempts = 3
|
||||
|
||||
const retryDelay = 100 * time.Millisecond
|
||||
|
||||
type OpenAICompatibleClient struct {
|
||||
baseURL string
|
||||
apiKey string
|
||||
@@ -31,34 +36,82 @@ func NewOpenAICompatibleClient(baseURL, apiKey string, httpClient *http.Client)
|
||||
|
||||
func (c *OpenAICompatibleClient) ChatStream(ctx context.Context, req ChatRequest, emit func(StreamEvent) error) error {
|
||||
req.Stream = true
|
||||
extra := make(map[string]any, len(req.Extra)+1)
|
||||
for key, value := range req.Extra {
|
||||
extra[key] = value
|
||||
}
|
||||
if _, exists := extra["stream_options"]; !exists {
|
||||
extra["stream_options"] = map[string]any{"include_usage": true}
|
||||
}
|
||||
req.Extra = extra
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal chat request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create chat request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "text/event-stream")
|
||||
if c.apiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= maxChatAttempts; attempt++ {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+"/v1/chat/completions", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("create chat request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "text/event-stream")
|
||||
if c.apiKey != "" {
|
||||
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send chat request: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
lastErr = fmt.Errorf("send chat request: %w", err)
|
||||
if !shouldRetryRequest(ctx, attempt, 0) {
|
||||
return lastErr
|
||||
}
|
||||
if waitErr := waitBeforeRetry(ctx); waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
limit := io.LimitReader(resp.Body, 4096)
|
||||
data, _ := io.ReadAll(limit)
|
||||
return fmt.Errorf("chat request failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(data)))
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
limit := io.LimitReader(resp.Body, 4096)
|
||||
data, _ := io.ReadAll(limit)
|
||||
_ = resp.Body.Close()
|
||||
lastErr = fmt.Errorf("chat request failed: status=%d body=%s", resp.StatusCode, strings.TrimSpace(string(data)))
|
||||
if !shouldRetryRequest(ctx, attempt, resp.StatusCode) {
|
||||
return lastErr
|
||||
}
|
||||
if waitErr := waitBeforeRetry(ctx); waitErr != nil {
|
||||
return waitErr
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return readSSE(resp.Body, emit)
|
||||
defer resp.Body.Close()
|
||||
return readSSE(resp.Body, emit)
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func shouldRetryRequest(ctx context.Context, attempt int, status int) bool {
|
||||
if ctx.Err() != nil || attempt >= maxChatAttempts {
|
||||
return false
|
||||
}
|
||||
if status == 0 {
|
||||
return true
|
||||
}
|
||||
return status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500
|
||||
}
|
||||
|
||||
func waitBeforeRetry(ctx context.Context) error {
|
||||
timer := time.NewTimer(retryDelay)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func readSSE(r io.Reader, emit func(StreamEvent) error) error {
|
||||
@@ -79,7 +132,7 @@ func readSSE(r io.Reader, emit func(StreamEvent) error) error {
|
||||
if parseErr != nil {
|
||||
return parseErr
|
||||
}
|
||||
if event.Content != "" || len(event.ToolCalls) > 0 || event.FinishReason != "" {
|
||||
if event.Content != "" || len(event.ToolCalls) > 0 || event.FinishReason != "" || event.Usage != nil {
|
||||
if emitErr := emit(event); emitErr != nil {
|
||||
return emitErr
|
||||
}
|
||||
@@ -100,6 +153,7 @@ type streamPayload struct {
|
||||
} `json:"delta"`
|
||||
FinishReason string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage *Usage `json:"usage,omitempty"`
|
||||
Error *struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
@@ -127,15 +181,14 @@ func parseStreamPayload(payload string) (StreamEvent, error) {
|
||||
}
|
||||
return StreamEvent{}, fmt.Errorf("provider error: %s", decoded.Error.Message)
|
||||
}
|
||||
event := StreamEvent{Usage: decoded.Usage}
|
||||
if len(decoded.Choices) == 0 {
|
||||
return StreamEvent{}, nil
|
||||
return event, nil
|
||||
}
|
||||
|
||||
choice := decoded.Choices[0]
|
||||
event := StreamEvent{
|
||||
Content: choice.Delta.Content,
|
||||
FinishReason: choice.FinishReason,
|
||||
}
|
||||
event.Content = choice.Delta.Content
|
||||
event.FinishReason = choice.FinishReason
|
||||
for _, tc := range choice.Delta.ToolCalls {
|
||||
event.ToolCalls = append(event.ToolCalls, ToolCallDelta{
|
||||
Index: tc.Index,
|
||||
|
||||
Reference in New Issue
Block a user