Go
A net/http client with a submit, a poll loop and typed responses, in one file.
package silvertext
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"time"
)
const base = "https://api.silvertext.com"
type Client struct {
Key string
HTTP *http.Client
}
func New() *Client {
return &Client{Key: os.Getenv("SILVERTEXT_API_KEY"), HTTP: &http.Client{Timeout: 60 * time.Second}}
}
type APIError struct {
Status int
Code string `json:"code"`
Message string `json:"message"`
}
func (e *APIError) Error() string { return fmt.Sprintf("%d %s: %s", e.Status, e.Code, e.Message) }
type Summary struct {
ID string `json:"id"`
Status string `json:"status"`
Progress float64 `json:"progress"`
Phase string `json:"phase"`
WordCount *int `json:"wordCount"`
Scores *struct {
Plagiarism float64 `json:"plagiarism"`
GrammarSpelling *float64 `json:"grammarSpelling"`
Citation *float64 `json:"citation"`
AIContent *float64 `json:"aiContent"`
Clarity *float64 `json:"clarity"`
} `json:"scores"`
Rejection *struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"rejection"`
}
func (c *Client) do(ctx context.Context, method, path string, body any, idem string, out any) error {
var payload []byte
if body != nil {
payload, _ = json.Marshal(body)
}
delay := time.Second
for attempt := 1; ; attempt++ {
req, _ := http.NewRequestWithContext(ctx, method, base+path, bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+c.Key)
req.Header.Set("Content-Type", "application/json")
if idem != "" {
req.Header.Set("Idempotency-Key", idem)
}
res, err := c.HTTP.Do(req)
if err == nil && res.StatusCode < 400 {
defer res.Body.Close()
if out != nil && res.ContentLength != 0 {
return json.NewDecoder(res.Body).Decode(out)
}
return nil
}
retryable := err != nil || res.StatusCode == 429 || res.StatusCode >= 500
if !retryable || attempt >= 4 {
if err != nil {
return err
}
defer res.Body.Close()
var wrapped struct{ Error APIError `json:"error"` }
_ = json.NewDecoder(res.Body).Decode(&wrapped)
wrapped.Error.Status = res.StatusCode
return &wrapped.Error
}
wait := delay
if res != nil {
if s, err := strconv.Atoi(res.Header.Get("Retry-After")); err == nil && s > 0 {
wait = time.Duration(s) * time.Second
}
res.Body.Close()
}
time.Sleep(wait)
if delay < 30*time.Second {
delay *= 2
}
}
}
func (c *Client) CreateCheck(ctx context.Context, body map[string]any, idem string) (string, error) {
var out struct{ ID string `json:"id"` }
err := c.do(ctx, http.MethodPost, "/v1/checks", body, idem, &out)
return out.ID, err
}
func (c *Client) GetSummary(ctx context.Context, id string) (*Summary, error) {
var s Summary
err := c.do(ctx, http.MethodGet, "/v1/checks/"+id+"?view=summary", nil, "", &s)
return &s, err
}
// GetReport decodes the parts you ask for into out (a struct with a `report` field).
func (c *Client) GetReport(ctx context.Context, id string, include string, out any) error {
return c.do(ctx, http.MethodGet, "/v1/checks/"+id+"?include="+include, nil, "", out)
}
func (c *Client) Cancel(ctx context.Context, id string) error {
return c.do(ctx, http.MethodPost, "/v1/checks/"+id+"/cancel", nil, "", nil)
}
var final = map[string]bool{"done": true, "rejected": true, "failed": true, "cancelled": true}
func (c *Client) Wait(ctx context.Context, id string) (*Summary, error) {
delay := 3 * time.Second
started := time.Now()
for {
s, err := c.GetSummary(ctx, id)
if err != nil {
return nil, err
}
if final[s.Status] {
return s, nil
}
select {
case <-ctx.Done():
_ = c.Cancel(context.Background(), id)
return nil, ctx.Err()
case <-time.After(delay):
}
if time.Since(started) > time.Minute && delay < 10*time.Second {
delay += 2 * time.Second
}
}
}Using it
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
st := silvertext.New()
id, err := st.CreateCheck(ctx, map[string]any{
"title": "Q3 launch post",
"text": draft,
"options": map[string]any{"citations": false},
}, "post:"+postID+":v3")
if err != nil {
log.Fatal(err)
}
summary, err := st.Wait(ctx, id)
if err != nil || summary.Status != "done" {
log.Fatal(err, summary.Status)
}
fmt.Printf("plagiarism %.0f%%\n", summary.Scores.Plagiarism)The context deadline doubles as the give-up point: when it expires, Wait cancels the check so its credits are released.