Complete is the non-streaming API. It validates locally, dispatches to the
provider, retries the failures worth retrying, and returns one
Response.
Reference#
Parameters
ctx— bounds the whole call, including every retry and every backoff delay between them. Cancelling it stops retrying immediately, even mid-backoff.req— the request. Validated before anything is sent.
Returns
A *Response and a nil error on success. On failure, a nil response and an
error wrapping one of the
sentinels — recoverable as
*skyl.Error with errors.As.
When retries are exhausted, the error is wrapped:
skyl: giving up after 4 attempts: …. The sentinel survives the wrapping.
Caveats
req.Validate()runs first, so a malformed request fails locally withErrBadRequestand costs no round trip.- The model string is not validated. A typo reaches the provider and returns
ErrNotFound. WithTimeoutbounds one attempt, not the sequence. Bound the sequence withctx.- A hook fires once per attempt, including retried ones.
reqis reused across retries — do not mutate it from another goroutine.
Usage#
A basic call
resp, err := client.Complete(ctx, &skyl.Request{
Model: "gpt-5.6",
MaxTokens: 1024,
Messages: []skyl.Message{skyl.UserText("Explain Go channels in two sentences.")},
})
if err != nil {
return err
}
fmt.Println(resp.Text())resp, err := client.Complete(ctx, &skyl.Request{
Model: "gpt-5.6",
MaxTokens: 1024,
Messages: []skyl.Message{skyl.UserText("Explain Go channels in two sentences.")},
})
if err != nil {
return err
}
fmt.Println(resp.Text())Bounding the whole call
// WithTimeout bounds an attempt; this bounds everything.
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
resp, err := client.Complete(ctx, req)// WithTimeout bounds an attempt; this bounds everything.
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
defer cancel()
resp, err := client.Complete(ctx, req)Branching on failure
resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrRefusal):
// Never retried — the same prompt gets the same answer.
case errors.Is(err, context.DeadlineExceeded):
// Works because *Error wraps its cause as well as its sentinel.
default:
return err
}resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrRefusal):
// Never retried — the same prompt gets the same answer.
case errors.Is(err, context.DeadlineExceeded):
// Works because *Error wraps its cause as well as its sentinel.
default:
return err
}A tool loop
for round := 0; round < 5; round++ {
resp, err := client.Complete(ctx, req)
if err != nil {
return nil, err
}
calls := resp.ToolCalls()
if len(calls) == 0 {
return resp, nil
}
// The assistant turn must precede any result, on every provider.
req.Messages = append(req.Messages, resp.Message)
for _, call := range calls {
req.Messages = append(req.Messages, skyl.ToolResultMessage(call.ID, run(call)))
}
}
return nil, errors.New("gave up after 5 tool rounds")for round := 0; round < 5; round++ {
resp, err := client.Complete(ctx, req)
if err != nil {
return nil, err
}
calls := resp.ToolCalls()
if len(calls) == 0 {
return resp, nil
}
// The assistant turn must precede any result, on every provider.
req.Messages = append(req.Messages, resp.Message)
for _, call := range calls {
req.Messages = append(req.Messages, skyl.ToolResultMessage(call.ID, run(call)))
}
}
return nil, errors.New("gave up after 5 tool rounds")Troubleshooting#
I get ErrBadRequest without any network call
That is Request.Validate doing its job.
The message names the problem — a missing model, an empty message list, a tool
with no name. Fix the request; retrying will not help.
resp.Text() is empty but there was no error
Check resp.StopReason. StopToolUse means the model wants a tool run and
produced no prose — normal, not a failure. StopRefusal means it declined but
said nothing.
if resp.Text() == "" && resp.StopReason == skyl.StopToolUse {
// Expected: run the calls in resp.ToolCalls().
}if resp.Text() == "" && resp.StopReason == skyl.StopToolUse {
// Expected: run the calls in resp.ToolCalls().
}The answer is cut off mid-sentence
resp.StopReason == skyl.StopMaxTokens means you hit the cap and the response
is truncated. Raise MaxTokens; retrying unchanged produces the same
result.
It retried something that could never succeed
It should not have. Only rate limits, server errors and transport failures are
retried. If you saw retries on an auth failure or a malformed request, check
whether the adapter classified it correctly — an unclassified error with
StatusCode == 0 is treated as retryable, and a provider returning a transport
error for a permanent failure would land there.