Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,18 @@
type ClientOption func(*Client)

// WithBaseURL sets the daemon base URL (default: http://127.0.0.1:4590).
func WithBaseURL(u string) ClientOption {

Check failure on line 29 in client.go

View workflow job for this annotation

GitHub Actions / deadcode

unreachable func: WithBaseURL
return func(c *Client) { c.baseURL = strings.TrimRight(u, "/") }
}

// WithHTTPClient sets a custom http.Client.
func WithHTTPClient(hc *http.Client) ClientOption {

Check failure on line 34 in client.go

View workflow job for this annotation

GitHub Actions / deadcode

unreachable func: WithHTTPClient
return func(c *Client) { c.httpClient = hc }
}

// WithAPIKey sets an API key for authentication. The key is sent as
// an Authorization: Bearer header on every request.
func WithAPIKey(key string) ClientOption {

Check failure on line 40 in client.go

View workflow job for this annotation

GitHub Actions / deadcode

unreachable func: WithAPIKey
return func(c *Client) { c.apiKey = key }
}

Expand All @@ -48,8 +48,12 @@
// retries with exponential backoff on transient failures.
func New(opts ...ClientOption) *Client {
c := &Client{
baseURL: defaultBaseURL,
httpClient: &http.Client{Timeout: 30 * time.Second},
baseURL: defaultBaseURL,
httpClient: &http.Client{
Transport: &http.Transport{
ResponseHeaderTimeout: 5 * time.Second,
},
},
}
for _, opt := range opts {
opt(c)
Expand All @@ -76,7 +80,7 @@
}

// ChatStream sends a prompt and streams the response via SSE.
func (c *Client) ChatStream(ctx context.Context, req ChatRequest) (*StreamReader, error) {

Check failure on line 83 in client.go

View workflow job for this annotation

GitHub Actions / deadcode

unreachable func: Client.ChatStream
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("hawk-sdk: marshal request: %w", err)
Expand Down
19 changes: 18 additions & 1 deletion retry.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,27 @@ package hawksdk
import (
"bytes"
"context"
cryptorand "crypto/rand"
"encoding/binary"
"io"
"math"
"math/rand"
"net/http"
"sync/atomic"
"time"
)

// jitterSeed provides a per-process random seed for jitter, avoiding the
// global math/rand state which is not safe for concurrent use in some
// runtimes and can produce predictable sequences.
var jitterSeed = atomic.Int64{}

func init() {
var buf [8]byte
_, _ = cryptorand.Read(buf[:])
jitterSeed.Store(int64(binary.LittleEndian.Uint64(buf[:])))
}

// RetryConfig configures the automatic retry behavior for API requests.
type RetryConfig struct {
// MaxRetries is the maximum number of retry attempts.
Expand Down Expand Up @@ -83,7 +97,10 @@ func (cfg *RetryConfig) backoffDuration(attempt int) time.Duration {
backoff = float64(cfg.MaxBackoff)
}
// Full jitter: random value between 0 and calculated backoff.
jittered := time.Duration(rand.Float64() * backoff)
// Uses a per-process seed to avoid the global math/rand state.
seed := jitterSeed.Add(1)
rng := rand.New(rand.NewSource(seed))
jittered := time.Duration(rng.Float64() * backoff)
return jittered
}

Expand Down
Loading