Help us shape the future of Go! Take the 2026 Go Developer Survey

spicy

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 5, 2026 License: MIT Imports: 19 Imported by: 0

README

spicy-go

Official Go SDK for SpicyAPI — image, video, audio and text models behind one API.

Get a key · Models · Docs · Status


One endpoint in front of 142 model families across 214 callable endpoints, billed in USD per request rather than in credits. Media generation is asynchronous and quotable before you spend; text models speak the OpenAI, Anthropic and Gemini wire formats.

go get github.com/spicyapi-ai/spicy-go

Requires Go 1.23+. Standard library only — this package goes into your dependency graph, and every module it adds is one more thing for you to audit.

Generate something

client, err := spicy.New(spicy.Options{}) // reads SPICY_API_KEY from the environment
if err != nil {
    return err
}

request := spicy.CreateTaskInput{
    Model: "MODEL_ID_FROM_CATALOG", // copy a real id from ListModels
    Input: map[string]any{"prompt": "a lantern in fog"},
}

quote, err := client.Quote(ctx, request)
if err != nil {
    return err
}
log.Printf("this will cost at most %s", quote.MaxCharge)

// Store this next to your own record of the job before sending it: its whole
// purpose is to still be there after the crash that lost the response.
key, err := spicy.NewIdempotencyKey()
if err != nil {
    return err
}

request.Model = quote.Model
request.QuoteID = quote.QuoteID
request.ExpectedCost = quote.EstimatedCost

task, err := client.CreateTask(ctx, request, key, spicy.CreateTaskOptions{})
if err != nil {
    return err
}

final, err := client.WaitForTerminal(ctx, task.TaskID, spicy.WaitOptions{})
if err != nil {
    return err
}
for _, asset := range final.ReadyAssets() {
    log.Println(asset.URL)
}

Build Input from that model's own inputSchema — every model has different fields, and ListModels returns them.

Skipping the first poll

WaitSeconds asks the service to hold the connection open until the task finishes, which on a quick model means the result arrives with the submission.

seconds := 30
task, err := client.CreateTask(ctx, request, key, spicy.CreateTaskOptions{WaitSeconds: &seconds})
if err != nil {
    return err
}

// Task is nil whenever the wait ran out. That is the ordinary outcome, not an
// error — the generation is still running, so poll from here.
final := task.Task
if final == nil {
    final, err = client.WaitForTerminal(ctx, task.TaskID, spicy.WaitOptions{})
    if err != nil {
        return err
    }
}
log.Println(final.State, final.Cost)

It is an optimisation, never a guarantee. Code that reads task.Task without the nil check works right up until the first generation slower than the budget, and then panics in production.

The value is sent exactly as given: the service clamps anything above its ceiling and ignores — rather than rejects — a value that is not a positive integer, so a typo here cannot fail a generation that would otherwise succeed. This client enforces no ceiling of its own, and lifts that one request's deadline to cover the wait.

A terminal state is not a successful one. failed, canceled and expired all end the wait; check final.State.

Start from a local file

Image-to-video, face swap and image editing need your material on our side first. Upload returns a spicy:// URI; that is what goes into Input.

uploaded, err := client.UploadFile(ctx, "/path/to/reference.png", "")
// uploaded.URI -> "spicy://f/fil_..."

Images, MP4/WebM video, MP3/WAV audio and reference documents (PDF, Word, Excel, PowerPoint, Pages, Numbers, plain text, Markdown) are accepted. The content type is inferred from the extension, except for Keynote: .key is also how private keys are named, so pass "application/vnd.apple.keynote" yourself rather than have a key file uploaded on a guess.

Balance, usage and history

All three are scoped to this API key: sibling keys on the same account, and generations started in the web console, are not included, and their absence is not a fault.

balance, err := client.GetBalance(ctx)
if err != nil {
    return err
}
log.Printf("%s available, %s held by accepted tasks", balance.Available, balance.Held)

usage, err := client.GetUsage(ctx, spicy.UsageOptions{From: "2026-09-01", To: "2026-09-08"})
if err != nil {
    return err
}
log.Printf("%d calls, %s settled over [%s,%s)", usage.TotalCalls, usage.TotalSpend, usage.From, usage.To)

// EachTask repeats every filter on every page and moves only the cursor.
if err := client.EachTask(ctx, spicy.ListTasksOptions{From: "2026-09-01", To: "2026-09-08"},
    func(task spicy.TaskListItem) bool {
        log.Println(task.TaskID, task.State, task.Cost)
        return true // return false to stop early
    }); err != nil {
    return err
}

Amounts are decimal USD strings, never floats. Dates are UTC calendar dates in a half-open interval [from, to) of at most 92 days.

Pin From and To on any walk that can cross midnight UTC. Left empty the server recomputes its own window — to is tomorrow in UTC — on every page, so a walk that starts at 23:59 and continues at 00:01 asks two different questions and splices the answers together. Nothing reports it.

GetUsage is for reconciliation, not for progress. It sits behind an account-wide budget of thirty requests a minute shared by every key, and that limiter fails closed — polling it can lock every other key out of its own reporting. Follow a task with WaitForTerminal. Settlement can also land late, so a figure read today can still change tomorrow.

Errors

Failures come back as *spicy.APIError, reachable with errors.As:

var apiErr *spicy.APIError
if errors.As(err, &apiErr) {
    log.Println(apiErr.Status, apiErr.Code, apiErr.RequestID, apiErr.RetryAfter)
}

Branch on Code, never on the message — messages are translated, codes are not. HTTP 503 is shared by three different business codes, so the status alone is not enough to decide what to do:

Code Meaning What to do
40003 uploaded bytes do not match their ticket upload again
40004 no deployment serves that parameter combination change the named parameter
40310 the account's email address is not verified yet open the verification link; the same key then works
40901 the price moved before the task was created quote again, keep the same idempotency key
503 a dependency is briefly unavailable back off by RetryAfter
50301 no usable deployment or price right now refresh the catalogue, do not hammer
50302 a synchronous generation failed upstream and was refunded retry with a new idempotency key

Two things that will save you money

Keep one idempotency key per submission. Reuse it for every resend, including after a timeout. A lost response does not prove the task was not created — a fresh key turns an unknown outcome into a second paid task. CreateTask requires the key for exactly this reason.

A task that succeeds is charged, even if the result disappoints. Quote reserves nothing, so quote first when the price matters. Failures are normally free, with one exception: on a model whose page says content-policy refusals are billed, a task that fails with content_rejected settles at the estimate it was accepted at. Read Cost once Settled is true rather than assuming.

What this package does not do

Text models. They speak the OpenAI, Anthropic and Gemini wire formats — the established Go clients for those already work against https://api.spicyapi.ai/v1 with the same key. One part of this package still helps there: chat models that read images, video or audio take the spicy://f/ URI from UploadFile in an image_url, video_url or audio_url message part. Which parts a model accepts is in its input schema; see sending media to a text model.

Client-side applications. Never ship this key inside a binary you distribute. Call from your server.


Also available in TypeScript · Python · Go · PHP · Java

Documentation

Overview

Package spicy is the official SpicyAPI client for Go.

SpicyAPI serves image, video and audio generation models behind one API. This package covers the asynchronous media workflow end to end: discover a model and its input schema, upload local reference material, quote a request, submit it, wait for a terminal state, and collect the result.

Text models are deliberately out of scope. They speak the OpenAI, Anthropic and Gemini wire formats, so the established clients for those already work against this service; wrapping them here would add nothing. The one piece of this package they do need is Upload: a chat model that reads images, video or audio takes the spicy://f/ URI it returns in an image_url, video_url or audio_url message part.

It depends on the standard library only.

What it covers is the asynchronous generation pipeline: find a model and its input schema, quote it, create a task, poll that task to a terminal state, upload reference media, refresh an expired artefact link, and verify the webhook that announces a result. Text models are reached through the OpenAI, Anthropic and Gemini compatible layers instead, where the official libraries work by changing only the base URL.

Two things decide most of the code below:

  • Every /api/v1 response is the {code,msg,data,request_id} envelope, and a successful HTTP status does not mean the call succeeded. Branch on the business code, never on the message text.
  • HTTP 202 from task creation means accepted, not finished. Success or failure is reported by the task's state, read back with GetTask.

The contract this file is written against is published at https://docs.spicyapi.ai/openapi.yaml.

Index

Constants

View Source
const (
	// APIBaseURL is the only production API root.
	APIBaseURL = "https://api.spicyapi.ai/api/v1"

	// DefaultRequestTimeout bounds one JSON API attempt, not the whole call.
	// Give the context its own deadline to bound a call including retries.
	DefaultRequestTimeout = 30 * time.Second

	// DefaultUploadTimeout bounds the PUT that streams bytes into object
	// storage. It is deliberately far longer than DefaultRequestTimeout:
	// audio and video may be 90 MiB, and 30 seconds would demand a sustained
	// 25 Mbit/s upstream from the caller. A connection cut halfway through
	// reports a timeout, which reads like a network fault but is this client
	// hanging up on itself.
	DefaultUploadTimeout = 10 * time.Minute

	// DefaultWaitTimeout bounds WaitForTerminal. It is a local safety bound,
	// not a service level: reaching it says nothing about the remote task.
	DefaultWaitTimeout = 10 * time.Minute

	// DefaultPollInterval is the first gap between task polls; the gap grows
	// by half on every pass and stops at MaxPollInterval.
	DefaultPollInterval = 2 * time.Second
	// MaxPollInterval caps the growth of the polling gap.
	MaxPollInterval = 15 * time.Second

	// DefaultMaxRetries is how many extra attempts a replayable request gets.
	DefaultMaxRetries = 3

	// WebhookTolerance is the default clock skew VerifyWebhook accepts.
	WebhookTolerance = 5 * time.Minute
)

Defaults for a new Client. Every one of them can be overridden in Options.

View Source
const (
	StateQueued    = "queued"
	StateRunning   = "running"
	StateSucceeded = "succeeded"
	StateFailed    = "failed"
	StateCanceled  = "canceled"
	StateExpired   = "expired"
)

Task states. The two in-flight states are the stable part of this set: treat anything else as terminal rather than listing the terminal values, so that a state added later ends a wait instead of extending it forever.

View Source
const (
	ErrorCodeInvalidRequest         = "invalid_request"
	ErrorCodeUnsupportedCombination = "unsupported_combination"
	ErrorCodeContentRejected        = "content_rejected"
	ErrorCodeRateLimited            = "rate_limited"
	ErrorCodeUpstreamUnavailable    = "upstream_unavailable"
	ErrorCodeGenerationFailed       = "generation_failed"
	ErrorCodeTimeout                = "timeout"
	ErrorCodeInvalidAsset           = "invalid_asset"
	ErrorCodeUpstreamFailed         = "upstream_failed"
)

Failure identifiers reported on a terminal task. The set is closed; handle any value you do not recognise the way you handle ErrorCodeUpstreamFailed.

View Source
const (
	CodeSuccess                = 200
	CodeUploadMismatch         = 40003
	CodeUnsupportedCombination = 40004
	CodeInsufficientBalance    = 40201
	CodeSpendCapReached        = 40202
	CodeEmailNotVerified       = 40310
	CodeQuoteRejected          = 40901
	CodeDependencyUnavailable  = 503
	CodeModelUnavailable       = 50301
	CodeGenerationRefunded     = 50302
)

Business codes worth naming. They travel in the envelope's code field and are the thing to branch on: several of them share one HTTP status.

40003  the bytes that were uploaded do not match the ticket that was
       issued for them (size, media type or signature). Not retryable:
       request a fresh ticket with the real content type and size and
       upload again.
40004  the request is valid but no deployment serves this exact
       combination of settings right now. Not retryable: change the
       parameter named in the message, or pick another model. Waiting
       does not help, and nothing was created or held.
40310  the account has not verified its email address yet, so its keys
       cannot run models. Not retryable from code: open the verification
       link sent at sign-up, or send a new one from the Console. The same
       key works as soon as the address is verified.
40901  the quote expired, or the request or price changed since it was
       signed. Quote again, then resubmit.
503    a supporting dependency is briefly unavailable. Retryable; honour
       Retry-After when the response carries it.
50301  the model itself has no usable deployment or effective price.
       Retryable, but repetition will not fix an unlisted model.
50302  a synchronous generation failed at the model and the hold was
       released, so nothing was charged. The same request body is safe to
       submit again — under a new idempotency key, because the old key is
       already spent on the attempt that failed. This client therefore
       does not resend it for you.

503, 50301 and 50302 share HTTP 503 and mean three different things, which is the reason this client reads the business code first and the status second.

View Source
const UserAgent = "SpicyAPI-Go/" + Version

UserAgent identifies this client on every API request.

Without it net/http sends Go-http-client/1.1, which says nothing beyond "a Go program". That is the difference between being able to answer "which clients are affected by this" from a log and having to guess: a bad release, a version-specific bug or a deprecation all become visible only if the requests carry a version.

The shape matches the sibling clients (SpicyAPI-Java/…, SpicyAPI-PHP/…) so that one query covers every language.

View Source
const Version = "0.2.0"

Version is this package's released version, and the only place that number is written down. UserAgent derives from it, so bumping it here is the whole release edit — a second literal somewhere else is how a client ends up announcing a version it is not.

Keep it equal to the git tag it ships under, without the leading v. Nothing enforces that from inside the package: there is no build metadata to read back, because a tagged module exposes its version only to the programs that depend on it, and never to itself.

Variables

View Source
var (
	ErrWebhookRejected  = errors.New("spicy: webhook rejected")
	ErrWebhookStale     = fmt.Errorf("%w: timestamp outside the tolerance window", ErrWebhookRejected)
	ErrWebhookSignature = fmt.Errorf("%w: signature mismatch", ErrWebhookRejected)
)

Webhook rejection reasons. Every one of them wraps ErrWebhookRejected, so a handler that only wants to answer 401 can test for that alone.

Functions

func NewIdempotencyKey

func NewIdempotencyKey() (string, error)

NewIdempotencyKey mints a key with enough entropy that two submissions never collide. Store it next to your own record of the job before you send it: the point of the key is to be available again after the crash that lost the response.

func VerifyWebhook

func VerifyWebhook(secret string, header http.Header, rawBody []byte, tolerance time.Duration) (string, error)

VerifyWebhook authenticates one callBackUrl delivery and returns the task ID the signature was computed over. Use that returned ID, not one you read out of the body yourself.

This verifies the scheme used by the per-task callBackUrl. Naming it precisely matters because a second, account-level delivery scheme is planned; when that ships it gets its own verifier rather than extra arguments here, so code written against this function keeps working unchanged.

The signature is

base64( HMAC-SHA256( secret, "<taskID>.<timestamp>.<sha256_hex(rawBody)>" ) )

where the task ID comes from top-level task_id in payload version 1 and from data.taskId in version 2. Read X-Webhook-Payload-Version to choose; never infer the version from the shape of the JSON, because an old delivery can be retried long after the platform moved on.

rawBody must be the exact bytes that arrived. Decoding and re-encoding the JSON first changes key order, spacing and escaping, and the digest then fails every single time for a reason that looks like a wrong secret.

The digest is part of the message on purpose. Signing only the task ID and the timestamp would let anyone who intercepted one real delivery pair those two values with a body of their own — and it would verify.

tolerance bounds the accepted clock skew; pass 0 for WebhookTolerance.

Types

type APIError

type APIError struct {
	// Status is the HTTP status. It is 0 when no response was parsed.
	Status int
	// Code is the business code from the envelope, 0 when the body was not an
	// envelope. Branch on this, not on Message.
	Code int
	// Message is the server's explanation, in English unless the account or
	// the Accept-Language header selects another language. Never parse it.
	Message string
	// RequestID correlates this call with our logs.
	RequestID string
	// RetryAfter is the server's own backoff request, 0 when absent.
	RetryAfter time.Duration
}

APIError is a rejected call: an HTTP status, the business code to branch on, and the request ID to quote to support. Recover it with errors.As.

func (*APIError) Error

func (e *APIError) Error() string

func (*APIError) Refunded

func (e *APIError) Refunded() bool

Refunded reports code 50302: generation failed at the model and the hold was released, so this cost nothing. Submit the same input again under a new idempotency key.

func (*APIError) Retryable

func (e *APIError) Retryable() bool

Retryable reports whether sending the identical request again is worth it.

type Asset

type Asset struct {
	// Key identifies this output for CreateDownloadURL.
	Key string `json:"key,omitempty"`
	// URL is a signed link that works on its own. Never attach your API key to
	// it: it is already a bearer grant, and sending the key leaks it to storage.
	// It is absent while Pending or Unavailable is true.
	URL string `json:"url,omitempty"`
	// ExpiresAt is this link's expiry, normally about twenty minutes out. It is
	// not the retention deadline of the result; poll again for a fresh link.
	ExpiresAt       *time.Time `json:"expiresAt,omitempty"`
	MIME            string     `json:"mime,omitempty"`
	Width           int        `json:"width,omitempty"`
	Height          int        `json:"height,omitempty"`
	DurationSeconds float64    `json:"durationSeconds,omitempty"`
	Bytes           int64      `json:"bytes,omitempty"`
	Role            string     `json:"role,omitempty"`
	NSFW            bool       `json:"nsfw,omitempty"`
	// Pending means the artefact is still being copied into our own storage.
	Pending bool `json:"pending,omitempty"`
	// Unavailable means it will not arrive; do not keep waiting for it.
	Unavailable bool `json:"unavailable,omitempty"`
	// Layer is set only on the outputs of a layer-decomposition task.
	Layer *AssetLayer `json:"layer,omitempty"`
}

Asset is one generated artefact.

type AssetLayer

type AssetLayer struct {
	ZIndex int `json:"zIndex"`
	// Name is the model's own label for the layer, when it gives one.
	Name string `json:"name,omitempty"`
	// BoundingBox is [x1, y1, x2, y2] in base-image pixels: scale the layer
	// into that box to put it back in place. It is nil on the base image and on
	// models that do not report positions, where the layers still work as
	// standalone cut-outs.
	BoundingBox []int `json:"boundingBox,omitempty"`
}

AssetLayer places one output of a layer-decomposition task.

The first asset is the base image (the source with the separated elements removed, ZIndex 0) and every other asset is one transparent PNG layer. Stack them by ascending ZIndex to rebuild the picture, rather than relying on the order of Assets.

type Balance

type Balance struct {
	// Funding breaks Available down by where the money came from. It is absent
	// on accounts with nothing but a plain wallet.
	Funding *FundingOverview `json:"funding,omitempty"`
	// Available is what a new task can draw on, Held what accepted tasks have
	// already reserved, and Total the two together.
	Available string `json:"available"`
	Held      string `json:"held"`
	Total     string `json:"total"`
}

Balance is what this account can spend right now. Every amount is a decimal USD string, never a float: cents do not survive binary floating point.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client calls the SpicyAPI task surface. It is safe for concurrent use.

func New

func New(opts Options) (*Client, error)

New builds a client. It fails when no API key is available, because every endpoint here needs one and discovering that at the first call is worse.

func (*Client) CommitUpload

func (c *Client) CommitUpload(ctx context.Context, fileID string) (*UploadedFile, error)

CommitUpload verifies the bytes against the ticket — size, media type, signature — copies them to an immutable private object and returns the spicy:// URI to put in task input. Repeating a successful commit is idempotent.

Code 40003 here means the stored file does not match what the ticket was issued for. Do not retry it: ask for a new ticket with the real content type and size and upload again.

func (*Client) CreateDownloadURL

func (c *Client) CreateDownloadURL(ctx context.Context, taskID, key string) (*DownloadTicket, error)

CreateDownloadURL issues a fresh signed link to one output of a finished task. A finished task already carries usable links, so this is only needed once those have expired — polling the task again does the same job.

Leave key empty to select the first output. Do not attach your API key when fetching the link: it is a bearer grant on its own.

func (*Client) CreateTask

func (c *Client) CreateTask(
	ctx context.Context,
	input CreateTaskInput,
	idempotencyKey string,
	opts CreateTaskOptions,
) (*CreateTaskResult, error)

CreateTask submits a generation and returns as soon as it is accepted.

Acceptance is not completion. The returned state is queued; read the outcome back with GetTask or WaitForTerminal. EstimatedCost is held, not charged, and it is also the ceiling: the settled charge can only be lower.

CreateTaskOptions.WaitSeconds asks the service to hold the connection open until the task finishes, which saves the first poll on quick models. When it pays off the complete record arrives in CreateTaskResult.Task; when it does not, Task is nil and nothing else changes. **Check for nil.** A wait that runs out is the ordinary case, not an error, and the task is still running.

A wait also gets its own deadline for this one request, because the default per-request budget is shorter than the service's wait ceiling and would otherwise cut off a response that was on its way — turning a working optimisation into a retry storm that ends in a misleading timeout.

idempotencyKey is required by this client even though the contract allows it to be omitted, because the automatic resend on a network fault is only sound when it exists. Without a key, a request that times out leaves you unable to tell an accepted task from a lost one, and the obvious reaction — send it again — is a second generation and a second charge. Reuse the same value for every attempt at the same logical submission: for 24 hours, and for this API key, a repeat returns the original task instead of creating another.

func (*Client) CreateUploadURL

func (c *Client) CreateUploadURL(ctx context.Context, contentType string, size int64) (*UploadTicket, error)

CreateUploadURL asks for a presigned slot for exactly one file of exactly the declared size. Images are capped at 10 MiB, audio and video at 90 MiB and 600 seconds, reference documents (PDF, Office, iWork, plain text and Markdown) at 90 MiB; the ticket's MaxBytes is the authoritative ceiling.

The ticket lives about twenty minutes and that window covers both the PUT and the commit, so ask for it right before uploading rather than in advance.

func (*Client) EachTask

func (c *Client) EachTask(ctx context.Context, opts ListTasksOptions, fn func(TaskListItem) bool) error

EachTask walks every page of ListTasks and calls fn once per task. Return false from fn to stop early; the error is the first page that failed.

Every filter is repeated unchanged on each request and only the cursor moves, because that is what the contract asks for: a cursor is only meaningful against the query it was issued for, and changing a filter mid-walk silently produces a page that belongs to neither query.

**Pin From and To yourself for any walk that might cross midnight UTC.** With them empty the server recomputes its default window — To is tomorrow in UTC — on every page, so a walk that starts at 23:59 and continues at 00:01 asks two different questions and splices the answers together. Nothing reports this: the pages arrive, the loop ends, and the result is quietly wrong.

func (*Client) GetBalance

func (c *Client) GetBalance(ctx context.Context) (*Balance, error)

GetBalance reads the net available, held and total balance for this account, with the funding sources behind them.

func (*Client) GetModel

func (c *Client) GetModel(ctx context.Context, model string) (*Model, error)

GetModel reads one model, always including its input schema.

A model identifier contains slashes, so it is escaped into a single path segment here; sending them raw would address a different resource.

func (*Client) GetTask

func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error)

GetTask reads one task created by this API key. An unknown task, another account's task and a sibling key's task are all a 404; the three cases are deliberately indistinguishable.

func (*Client) GetUsage

func (c *Client) GetUsage(ctx context.Context, opts UsageOptions) (*UsageReport, error)

GetUsage reports this API key's settled spend and call counts.

The scope is this API key alone, and the numbers are attributed to when a task was created. **Settlement can land late**, so a figure read today can still change tomorrow — this is a reconciliation view, not a running total.

It is also the wrong way to follow a task. This endpoint sits behind an account-wide bucket of thirty requests a minute shared by every key on the account, and that limiter fails closed: polling it here can lock every other key out of its own reporting. Use GetTask or WaitForTerminal for progress.

func (*Client) ListModels

func (c *Client) ListModels(ctx context.Context, opts ListModelsOptions) (*ModelList, error)

ListModels returns every model this API key may call, with the prices that apply to this account. Enabled and Available must both be true before a model is callable.

func (*Client) ListTasks

func (c *Client) ListTasks(ctx context.Context, opts ListTasksOptions) (*TaskList, error)

ListTasks returns one page of this API key's task history, newest first.

The scope is this API key alone. Tasks created by sibling keys on the same account, and generations started in the web console (which use no key), are not here and their absence is not a fault.

Pages read live state rather than a frozen snapshot, so a task can move between states, or settle, while you walk them.

func (*Client) PurgeTask

func (c *Client) PurgeTask(ctx context.Context, taskID string) (*PurgeResult, error)

PurgeTask destroys a finished task's stored content: the generated objects, the result payload and the request text including the prompt.

It destroys content, not the spend record. The ledger, the charged amount, the model, the state, the timestamps and the request ID all remain, which is what BillingRetained says out loud on the way back.

Unlike the other write endpoints this one reads no Idempotency-Key header: the task ID is the idempotency key, so a repeat after a timeout is safe and returns the original purgedAt. A queued or running task returns 400 — an accepted generation cannot be stopped, so wait for it to finish first.

func (*Client) Quote

func (c *Client) Quote(ctx context.Context, input CreateTaskInput) (*Quote, error)

Quote prices one exact request for five minutes without creating a task, holding funds or starting anything. It runs the same validation and admission rules as CreateTask, so it is also the cheapest way to find out that an input is wrong.

Pass the returned QuoteID and EstimatedCost to CreateTask to refuse a price that moved in the meantime.

func (*Client) RetryTask

func (c *Client) RetryTask(ctx context.Context, taskID, idempotencyKey string) (*RetryTaskResult, error)

RetryTask creates a new task from a failed or expired one. The source task is never modified, and the current schema, price, permissions and balance are all evaluated again — a retry can cost a different amount, or be refused outright.

The source must be terminal and settled; retrying too early returns 409, and retrying a succeeded task returns 404 with no way to tell it from a task that was never there. Retention is inherited from the source, so one family of tasks never ends up with two different expiry times.

func (*Client) Upload

func (c *Client) Upload(ctx context.Context, data []byte, contentType string) (*UploadedFile, error)

Upload runs all three steps: take a ticket, PUT the bytes straight into object storage, then commit. Only the URI on the returned file belongs in task input — not the upload URL, not a storage key, not the bare file ID.

Uploading is free; you pay for the task that consumes the file. A public HTTPS URL can be passed in the input field directly and skips all of this.

func (*Client) UploadFile

func (c *Client) UploadFile(ctx context.Context, path, contentType string) (*UploadedFile, error)

UploadFile reads a local file and uploads it. An empty contentType is inferred from the extension, which only works for the accepted media types.

func (*Client) WaitForTerminal

func (c *Client) WaitForTerminal(ctx context.Context, taskID string, opts WaitOptions) (*Task, error)

WaitForTerminal polls a task until it stops being queued or running, backing off between reads.

Terminal is defined as "neither of the two in-flight states" rather than as a list of the four known endings. A client that enumerates the endings will either loop forever or reject a real answer the day a new one is added, and that failure arrives silently. The four endings today are "succeeded", "failed", "canceled" and "expired"; only "succeeded" carries a result.

A local timeout is not a verdict: it returns *WaitTimeoutError, and the generation is still running on our side. Keep the task ID and reconcile it later, or use a callback URL instead of waiting.

type CreateTaskInput

type CreateTaskInput struct {
	// Model is an exact identifier from the catalogue.
	Model string `json:"model"`
	// Input is validated against that model's input schema. Media fields take
	// a public HTTPS URL, a committed spicy:// URI, or a small Base64 data URI.
	// Image fields also take a persona image created in the Console, as
	// spicy://p/<assetId>, from keys created in your personal workspace.
	Input map[string]any `json:"input"`
	// CallBackURL is an optional public HTTPS endpoint for terminal delivery.
	// Verify every delivery with VerifyWebhook before acting on it.
	CallBackURL string `json:"callBackUrl,omitempty"`
	// QuoteID and ExpectedCost together confirm a price: if the price moved
	// since the quote was signed, the task is rejected with code 40901 instead
	// of being accepted at the new amount.
	QuoteID      string `json:"quoteId,omitempty"`
	ExpectedCost string `json:"expectedCost,omitempty"`
}

CreateTaskInput is the body shared by Quote and CreateTask.

type CreateTaskOptions

type CreateTaskOptions struct {
	// RetentionSeconds, when non-nil, shortens how long this task's outputs
	// and its stored request text are kept. It can only shorten: the effective
	// value is the smallest of this, the account setting and the platform
	// maximum, and the result is reported back in Task.Retention.
	//
	// It is a pointer because 0 is a meaningful value — it means the outputs
	// go as soon as the task reaches a terminal state — so it must not be what
	// an empty options struct sends.
	RetentionSeconds *int

	// WaitSeconds, when non-nil, asks the service to hold the connection open
	// for that many seconds waiting for the task to finish, so the common case
	// needs no polling loop at all. Finishing inside the budget fills
	// CreateTaskResult.Task with the complete record; running out of it
	// returns the ordinary acceptance and you poll from there.
	//
	// **It is an optimisation, not a guarantee.** Nothing about setting it
	// makes the result terminal, and code written as though it does breaks on
	// the first generation slower than the budget.
	//
	// The value is sent exactly as given. The service clamps anything above
	// its ceiling and **ignores** — rather than rejects — a value that is not
	// a positive integer, on the reasoning that a typo in an optimisation
	// parameter should not fail a generation that would otherwise succeed. So
	// no ceiling is enforced here: a copy of the platform's number would be a
	// constant that can go stale without anything saying so, exactly as with
	// RetentionSeconds.
	//
	// It is a pointer for the same reason RetentionSeconds is: so that an
	// empty options struct sends no parameter at all.
	//
	// Setting it also lifts this one request's own deadline to cover the wait,
	// because the ordinary per-request budget is shorter than the service's
	// ceiling — see CreateTask.
	WaitSeconds *int
}

CreateTaskOptions carries the optional header that accompanies a submission.

type CreateTaskResult

type CreateTaskResult struct {
	TaskID string `json:"taskId"`
	// State is queued for a new submission. An idempotent replay reports the
	// original task's current state instead, which may already be terminal.
	State string `json:"state"`
	// EstimatedCost is the amount held, and the ceiling on the final charge.
	// Unused funds are released; nothing above the hold is ever collected.
	//
	// It is **empty when Task is non-nil**. A task that already finished has a
	// real cost rather than an estimate, and the service sends that instead;
	// the number is Task.Cost.
	EstimatedCost string `json:"estimatedCost"`
	// DeadlineAt is the server's execution deadline, fixed at acceptance. It
	// is not a local wait bound and not an artefact expiry.
	DeadlineAt time.Time `json:"deadlineAt"`

	// Task is the complete terminal record, and is non-nil only when
	// CreateTaskOptions.WaitSeconds was set and the task finished inside that
	// budget. It is exactly what GetTask would return.
	//
	// Nil is the ordinary outcome and is not a failure: the wait ran out, the
	// task is still running, and you poll from here. **Setting WaitSeconds is
	// an optimisation, never a guarantee** — code that reads Task without
	// checking for nil works right up until the first slow generation.
	//
	// Note this is not the same question as "is it finished": State can
	// already be terminal on a nil Task, because an idempotent replay reports
	// the original task's current state. InFlight answers that one.
	Task *Task `json:"-"`
}

CreateTaskResult is the acceptance record, not a result.

func (*CreateTaskResult) InFlight

func (r *CreateTaskResult) InFlight() bool

InFlight reports whether the task was still queued or running when the service produced this answer. Everything else is terminal, including a state this client has never heard of — same rule as Task.InFlight.

type CreditFacility

type CreditFacility struct {
	Enabled      bool   `json:"enabled"`
	LimitUsd     string `json:"limitUsd"`
	AvailableUsd string `json:"availableUsd"`
	// UsedUsd is outstanding settled principal; HeldUsd is credit reserved by
	// tasks that have been accepted but not yet settled. Both occupy the limit.
	UsedUsd   string     `json:"usedUsd"`
	HeldUsd   string     `json:"heldUsd"`
	ExpiresAt *time.Time `json:"expiresAt,omitempty"`
	Status    string     `json:"status"`
	Version   int64      `json:"version"`
}

CreditFacility is approved pay-later capacity. LimitUsd is a ceiling, not funds: it is not in the wallet and it is not yours to spend twice.

type DownloadTicket

type DownloadTicket struct {
	Key       string    `json:"key"`
	URL       string    `json:"url"`
	ExpiresAt time.Time `json:"expiresAt"`
}

DownloadTicket is a fresh signed link to one output of a finished task.

type FundingGrant

type FundingGrant struct {
	ID           string     `json:"id"`
	Name         string     `json:"name"`
	AmountUsd    string     `json:"amountUsd"`
	AvailableUsd string     `json:"availableUsd"`
	HeldUsd      string     `json:"heldUsd"`
	SpentUsd     string     `json:"spentUsd"`
	Status       string     `json:"status"`
	StartsAt     time.Time  `json:"startsAt"`
	ExpiresAt    *time.Time `json:"expiresAt,omitempty"`
	// ModelSlugs restricts this grant. **Empty means every model**, not none.
	ModelSlugs   []string  `json:"modelSlugs,omitempty"`
	CustomerMemo string    `json:"customerMemo"`
	CreatedAt    time.Time `json:"createdAt"`
}

FundingGrant is one promotional grant.

type FundingOverview

type FundingOverview struct {
	// BalanceUsd is the net wallet balance. It can be negative after credit is
	// consumed or after a payment is recovered.
	BalanceUsd string `json:"balanceUsd"`
	HeldUsd    string `json:"heldUsd"`
	// PrepaidAvailableUsd is unrestricted; GrantAvailableUsd may not pay for
	// every model, and which models it covers is decided when a task is
	// admitted, not here. So a non-zero grant balance is not a promise that
	// the next task will be affordable.
	PrepaidAvailableUsd string `json:"prepaidAvailableUsd"`
	GrantAvailableUsd   string `json:"grantAvailableUsd"`
	// CashShortfallUsd is a payment recovery gap that neither grants nor credit
	// can cover. Non-zero blocks new tasks.
	CashShortfallUsd string          `json:"cashShortfallUsd"`
	Credit           *CreditFacility `json:"credit,omitempty"`
	Grants           []FundingGrant  `json:"grants,omitempty"`
	// GrantsHasMore says Grants was truncated. This endpoint has no cursor, so
	// treat the list as a sample rather than a ledger when it is true.
	GrantsHasMore bool `json:"grantsHasMore"`
}

FundingOverview separates the sources behind a balance, because they do not behave alike: a grant can be restricted to certain models and credit is an approved ceiling rather than money on hand.

type HTTPDoer

type HTTPDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

HTTPDoer is the slice of *http.Client this package needs, so that tests can substitute a transport without a network.

type ListModelsOptions

type ListModelsOptions struct {
	// Modality is image, video, audio or text.
	Modality string
	// Provider is the model's creator.
	Provider string
	// Task is an exact task, for example text-to-image or text-to-speech.
	// "edit" is the one alias: it selects every endpoint published with the
	// .../edit suffix, which covers both image-to-image and the single-purpose
	// image-edit tools, while "image-to-image" selects only the former.
	Task string
	// Search matches names and descriptions.
	Search string
	// IncludeSchema adds each model's input JSON Schema, which is what you
	// need to build or validate an input. It makes the response much larger.
	IncludeSchema bool
	// IncludeExamples adds inputs that already validate against that schema —
	// the fastest way to get a first call working.
	IncludeExamples bool
}

ListModelsOptions filters the catalogue. A zero field means "do not filter".

type ListTasksOptions

type ListTasksOptions struct {
	// From and To are UTC dates in YYYY-MM-DD form and bound a half-open
	// interval [From, To) of at most 92 days. They are strings, not time.Time,
	// because the contract's type here is a calendar date: turning one into an
	// instant forces a timezone that the range does not have.
	//
	// Left empty the server defaults To to tomorrow in UTC and From to seven
	// days before it — see EachTask for why that default and pagination do not
	// mix well.
	From string
	To   string
	// State is one of the six task states.
	State string
	// Model is an exact catalogue identifier or a declared alias.
	Model string
	// Limit is the page size, 1 to 100, defaulting to 20 on the server.
	//
	// The upper bound is deliberately **not** enforced here. It is the
	// platform's number, and a copy of it in this package is a constant that
	// can go stale without anything saying so — it would reject a page size
	// the service had already started accepting. A value the server dislikes
	// costs one round trip and comes back as a plain 400.
	Limit int
	// Cursor is the NextCursor of the previous page. Pass it through
	// unchanged; it is opaque and constructing one yourself is meaningless.
	Cursor string
}

ListTasksOptions filters task history. A zero field means "do not filter".

type Model

type Model struct {
	// Model is the exact identifier to pass as CreateTaskInput.Model.
	Model  string `json:"model"`
	Family string `json:"family"`
	// DisplayName is for your UI, not for calls.
	DisplayName string `json:"displayName"`
	// Provider is the model's creator, the lab that built it.
	Provider string `json:"provider"`
	// Modality is image, video, audio or text.
	Modality string `json:"modality"`
	// Tasks holds exactly one internal classification and is not always the
	// suffix of Model: editing endpoints publish as .../edit and classify as
	// image-to-image, or as image-edit for single-purpose tools such as
	// background removal. Call with Model; group with Tasks. The set is open
	// and grows as models are added.
	Tasks []string `json:"tasks"`
	Async bool     `json:"async"`
	// Mature and PolicyTier are informational metadata about the model. They
	// take no part in authorization, availability or rejection.
	Mature     bool   `json:"mature"`
	PolicyTier string `json:"policyTier"`
	// Toolkit groups related endpoints for display: subject-swap for identity
	// swaps, studio-tools for single-purpose image and video tools such as
	// upscaling. Empty when the model belongs to no toolkit. It takes no part
	// in routing, pricing or authorization, and the set is open.
	Toolkit string `json:"toolkit,omitempty"`
	// TaskTimeoutSeconds is the platform execution deadline, not the duration
	// of the media that comes out.
	TaskTimeoutSeconds int `json:"taskTimeoutSeconds"`
	// Enabled and Available both have to be true before a model can be called.
	Enabled   bool `json:"enabled"`
	Available bool `json:"available"`
	// QuantityField names the input field that multiplies the price.
	QuantityField string       `json:"quantityField"`
	Pricing       []ModelPrice `json:"pricing"`
	StartingPrice *ModelPrice  `json:"startingPrice,omitempty"`
	// InputSchema is JSON Schema 2020-12 and stays raw on purpose: it is what
	// validates your input and what a form generator renders, and both jobs
	// want the document itself. Request it with ListModelsOptions.IncludeSchema
	// or read it from GetModel, which always includes it.
	InputSchema json.RawMessage `json:"inputSchema,omitempty"`
	// Version changes whenever the schema or metadata changes; cache on it.
	Version       string         `json:"version"`
	Availability  string         `json:"availability"`
	Badges        []string       `json:"badges"`
	RelatedModels []string       `json:"relatedModels"`
	Examples      []ModelExample `json:"examples,omitempty"`
	UpdatedAt     time.Time      `json:"updatedAt"`
}

Model is one callable endpoint from the authenticated catalogue. The typed fields are the ones an integration usually reads; the response carries more.

type ModelExample

type ModelExample struct {
	ID         string         `json:"id"`
	Input      map[string]any `json:"input"`
	SortWeight int            `json:"sortWeight"`
}

ModelExample is an input that validates against the model's current schema.

type ModelList

type ModelList struct {
	Total int     `json:"total"`
	Items []Model `json:"items"`
}

ModelList is the ListModels response.

type ModelPrice

type ModelPrice struct {
	// Variant identifies the tier, for example "720p" or
	// "duration=5;resolution=720p". Empty when the model has a single price.
	Variant string `json:"variant"`
	// Unit is per_image, per_second, per_request, per_1k_tokens or
	// per_1k_characters (text-to-speech priced on the length of the script).
	// Treat it as open: a unit added later must not break a client that only
	// displays it.
	Unit string `json:"unit"`
	// Price is a decimal USD string. Keep it a string end to end; binary
	// floating point cannot hold these amounts exactly.
	Price string `json:"price"`
	// RegularPrice is the undiscounted price, present only while an offer runs.
	RegularPrice string     `json:"regularPrice,omitempty"`
	OfferLabel   string     `json:"offerLabel,omitempty"`
	OfferPercent string     `json:"offerPercent,omitempty"`
	OfferEndsAt  *time.Time `json:"offerEndsAt,omitempty"`
	Currency     string     `json:"currency"`
}

ModelPrice is one price tier of a model.

type Options

type Options struct {
	// APIKey defaults to the SPICY_API_KEY environment variable. Keep it
	// server-side; it is a bearer credential for your balance.
	APIKey string
	// BaseURL defaults to APIBaseURL.
	BaseURL string
	// HTTPClient defaults to a client with no timeout of its own, because
	// every request here already carries a context deadline.
	HTTPClient HTTPDoer

	RequestTimeout time.Duration
	UploadTimeout  time.Duration
	WaitTimeout    time.Duration
	// MaxRetries is the number of extra attempts a replayable request gets.
	// Set it to -1 to disable retries entirely.
	MaxRetries int

	// Now and Rand exist for deterministic tests.
	Now  func() time.Time
	Rand func() float64
	// Sleep must return the context error if the context ends first.
	Sleep func(ctx context.Context, d time.Duration) error
}

Options configures New. Every zero value falls back to a documented default.

type PurgeResult

type PurgeResult struct {
	TaskID string `json:"taskId"`
	// ContentState is purged after a successful destroy and on every repeat.
	ContentState string `json:"contentState"`
	// PurgedAt is the time of the first destroy, not of this call.
	PurgedAt         *time.Time `json:"purgedAt,omitempty"`
	ContentRemovedBy string     `json:"contentRemovedBy,omitempty"`
	// BillingRetained is always true. Destroying content never touches the
	// ledger, the charged amount or anything else the spend record is made of.
	BillingRetained bool `json:"billingRetained"`
	// MediaDeletionPending is true while the background sweep is still
	// removing the stored objects, which takes about a minute.
	MediaDeletionPending bool `json:"mediaDeletionPending"`
}

PurgeResult reports what PurgeTask destroyed.

type Quote

type Quote struct {
	// QuoteID goes back to CreateTask together with ExpectedCost.
	QuoteID string `json:"quoteId"`
	Model   string `json:"model"`
	// EstimatedCost is what the request is expected to cost, MaxCharge the
	// ceiling. Both are decimal USD strings.
	EstimatedCost string `json:"estimatedCost"`
	MaxCharge     string `json:"maxCharge"`
	Currency      string `json:"currency"`
	// Quantity and Unit explain how the amount was reached.
	Quantity  string    `json:"quantity"`
	Unit      string    `json:"unit"`
	ExpiresAt time.Time `json:"expiresAt"`
}

Quote is a signed price for one exact request. It reserves nothing and guarantees no capacity; it only fixes the price for five minutes.

type RetryTaskResult

type RetryTaskResult struct {
	CreateTaskResult
	SourceTaskID string `json:"sourceTaskId"`
}

RetryTaskResult additionally names the task this one was created from.

type Task

type Task struct {
	TaskID string `json:"taskId"`
	// SourceTaskID is set when this task came from RetryTask.
	SourceTaskID string `json:"sourceTaskId,omitempty"`
	Model        string `json:"model"`
	State        string `json:"state"`
	// Input is the normalized input, omitted once retention has redacted it.
	Input  map[string]any `json:"input,omitempty"`
	Output *TaskOutput    `json:"output,omitempty"`
	// ErrorCode is one of the nine ErrorCode constants. Branch on it.
	ErrorCode string `json:"errorCode,omitempty"`
	// ErrorMessage explains the failure in English. It may carry the model
	// service's own wording, with identifiers removed. Do not parse it.
	ErrorMessage string `json:"errorMessage,omitempty"`
	// Cost is a decimal USD string: the final charge once Settled is true, and
	// the held estimate before that.
	//
	// A failed or expired task normally releases its hold in full, with one
	// exception: on a model
	// whose page states that content-policy refusals are billed, a task that
	// fails with ErrorCodeContentRejected settles at the accepted estimate.
	// Read Cost once Settled is true instead of assuming a failure was free.
	Cost    string `json:"cost"`
	Settled bool   `json:"settled"`

	CreatedAt        time.Time      `json:"createdAt"`
	DeadlineAt       *time.Time     `json:"deadlineAt,omitempty"`
	CompletedAt      *time.Time     `json:"completedAt,omitempty"`
	ContentState     string         `json:"contentState,omitempty"`
	ContentRemovedBy string         `json:"contentRemovedBy,omitempty"`
	PurgedAt         *time.Time     `json:"purgedAt,omitempty"`
	Retention        *TaskRetention `json:"retention,omitempty"`
}

Task is the full record returned by GetTask. Terminal and non-terminal states share this shape.

func (*Task) HasPendingAssets

func (t *Task) HasPendingAssets() bool

HasPendingAssets reports whether an artefact is still being copied. A task can be succeeded with its files not yet linkable.

func (*Task) InFlight

func (t *Task) InFlight() bool

InFlight reports whether the task is still queued or running. Everything else is terminal, including a state this client has never heard of.

func (*Task) ReadyAssets

func (t *Task) ReadyAssets() []Asset

ReadyAssets returns the artefacts that have a usable URL right now.

type TaskList

type TaskList struct {
	Items   []TaskListItem `json:"items"`
	HasMore bool           `json:"hasMore"`
	// NextCursor is present only when HasMore is true. It is opaque: pass it
	// back unchanged and never build one yourself.
	NextCursor string `json:"nextCursor,omitempty"`
}

TaskList is one page of task history.

type TaskListItem

type TaskListItem struct {
	TaskID string `json:"taskId"`
	// RequestID is the X-Request-Id of the request that created the task, so a
	// synchronous text call made through the OpenAI, Anthropic or Gemini
	// compatible endpoints can be matched to its charge here. Empty when none
	// was recorded.
	RequestID string `json:"requestId,omitempty"`
	Model     string `json:"model"`
	State     string `json:"state"`
	// Cost is the final charged amount once Settled is true, and the current
	// held estimate before that. A decimal USD string, so an unsettled row is
	// a ceiling rather than a bill.
	Cost       string    `json:"cost"`
	Settled    bool      `json:"settled"`
	CreatedAt  time.Time `json:"createdAt"`
	DeadlineAt time.Time `json:"deadlineAt"`
	// CompletedAt is absent while the task is still in flight.
	CompletedAt *time.Time `json:"completedAt,omitempty"`
}

TaskListItem is one row of task history: metadata only. There is no input, no output, no signed media URL and no processing detail here — read a selected task back with GetTask when you need any of that.

type TaskOutput

type TaskOutput struct {
	// Text is the plain-text result. For a multichannel transcription it holds
	// one line per channel, in channel order.
	Text string `json:"text,omitempty"`
	// Transcript is the structured speech-to-text result, with timing and
	// language details. Nil on every other task, and on transcription models
	// that return plain text only.
	Transcript *TaskTranscript `json:"transcript,omitempty"`
	Assets     []Asset         `json:"assets,omitempty"`
}

TaskOutput is the result payload of a finished task.

Not every endpoint produces files. Transcription and other text endpoints answer in Text and return no assets at all, so check what you actually got before reaching for Assets[0].

type TaskRetention

type TaskRetention struct {
	OutputsExpireAt time.Time `json:"outputsExpireAt"`
	PromptsExpireAt time.Time `json:"promptsExpireAt"`
	Source          string    `json:"source"`
}

TaskRetention says when this task's content disappears and which layer decided it: the per-request header, the account setting, or the platform maximum.

type TaskTranscript

type TaskTranscript struct {
	Text string `json:"text,omitempty"`
	// Language is the detected language as the model reports it, for example
	// "en"; LanguageCode is the ISO 639-3 form, for example "eng", when known.
	Language     string `json:"language,omitempty"`
	LanguageCode string `json:"languageCode,omitempty"`
	// LanguageProbability is the confidence of that detection, 0 to 1. It is a
	// pointer because "not reported" and "zero confidence" are different.
	LanguageProbability *float64 `json:"languageProbability,omitempty"`
	// DurationSeconds is the length of the transcribed audio.
	DurationSeconds float64              `json:"durationSeconds,omitempty"`
	Words           []TaskTranscriptWord `json:"words,omitempty"`
	// Channels has one entry per audio channel when multichannel
	// transcription was requested, and is empty otherwise.
	Channels []TaskTranscriptChannel `json:"channels,omitempty"`
}

TaskTranscript is a structured transcription. It is erased together with the rest of the output on the prompt-retention schedule.

type TaskTranscriptChannel

type TaskTranscriptChannel struct {
	// Index is zero-based.
	Index        int                  `json:"index"`
	Text         string               `json:"text,omitempty"`
	Language     string               `json:"language,omitempty"`
	LanguageCode string               `json:"languageCode,omitempty"`
	Words        []TaskTranscriptWord `json:"words,omitempty"`
}

TaskTranscriptChannel is the transcript of one audio channel.

type TaskTranscriptWord

type TaskTranscriptWord struct {
	Text  string  `json:"text"`
	Start float64 `json:"start"`
	End   float64 `json:"end"`
	// Type is the segment type, for example "word", when the model reports one.
	Type string `json:"type,omitempty"`
	// SpeakerID is present only when the model separates speakers.
	SpeakerID string `json:"speakerId,omitempty"`
}

TaskTranscriptWord is one timed segment of a transcript. Start and End are seconds from the beginning of the audio.

type UploadTicket

type UploadTicket struct {
	FileID string `json:"fileId"`
	// Key is a compatibility alias of the final URI and is unusable until the
	// commit succeeds. Put UploadedFile.URI in task input, never this.
	Key       string `json:"key"`
	UploadURL string `json:"uploadUrl"`
	Method    string `json:"method"`
	// Headers must be forwarded to the PUT exactly as given.
	Headers map[string]string `json:"headers"`
	// ExpiresAt is about twenty minutes out and covers the PUT and the commit
	// together. Ask for a ticket right before uploading, not in advance.
	ExpiresAt time.Time `json:"expiresAt"`
	// MaxBytes is the authoritative ceiling for this upload.
	MaxBytes int64 `json:"maxBytes"`
}

UploadTicket is a presigned slot for exactly one file of a declared size.

type UploadedFile

type UploadedFile struct {
	FileID string `json:"fileId"`
	Status string `json:"status"`
	Bytes  int64  `json:"bytes"`
	// ContentType is what the server verified, which may differ from what you
	// declared; if it does, the commit fails with code 40003 instead.
	ContentType string `json:"contentType"`
	SHA256      string `json:"sha256"`
	// URI is the spicy://f/fil_... value to put in task input. Use this one,
	// not the upload URL, not a storage key, not the bare file ID.
	URI       string    `json:"uri"`
	ExpiresAt time.Time `json:"expiresAt"`
	// DurationSeconds is a decimal string, present for audio and video.
	DurationSeconds string `json:"durationSeconds,omitempty"`
	Width           int    `json:"width,omitempty"`
	Height          int    `json:"height,omitempty"`
}

UploadedFile is a committed file, ready to be referenced by a task.

type UsageDay

type UsageDay struct {
	Day       string `json:"day"`
	Calls     int64  `json:"calls"`
	Succeeded int64  `json:"succeeded"`
	Failed    int64  `json:"failed"`
	Spend     string `json:"spend"`
}

UsageDay is one day of activity. Day is a UTC calendar date, YYYY-MM-DD.

type UsageModel

type UsageModel struct {
	Model     string `json:"model"`
	Calls     int64  `json:"calls"`
	Succeeded int64  `json:"succeeded"`
	Failed    int64  `json:"failed"`
	Spend     string `json:"spend"`
}

UsageModel is one model's share of the same window.

type UsageOptions

type UsageOptions struct {
	From string
	To   string
}

UsageOptions bounds a usage query to a half-open UTC interval [From, To) of at most 92 days, both as YYYY-MM-DD. Left empty the server uses the last seven days.

type UsageReport

type UsageReport struct {
	// From and To echo the window actually used, as UTC calendar dates. Read
	// them rather than assuming your own: leaving the options empty lets the
	// server pick, and this is the only place it says what it picked.
	From     string `json:"from"`
	To       string `json:"to"`
	Currency string `json:"currency"`
	// TotalSpend counts settled charges only, never pending holds.
	TotalCalls int64  `json:"totalCalls"`
	TotalSpend string `json:"totalSpend"`
	// Days and Models are sparse: a day or a model with no activity is absent
	// rather than present with zeroes.
	Days   []UsageDay   `json:"days"`
	Models []UsageModel `json:"models"`
}

UsageReport is settled spend and call counts over one window.

type WaitOptions

type WaitOptions struct {
	// Timeout is the local bound on the whole wait, DefaultWaitTimeout by
	// default. It is not a service level.
	Timeout time.Duration
	// Interval is the first gap between polls, DefaultPollInterval by default.
	Interval time.Duration
	// MaxInterval caps how far that gap grows, MaxPollInterval by default.
	MaxInterval time.Duration
	// AcceptPendingAssets returns a terminal task even when its artefacts are
	// still being copied into our storage, so they have no URL yet. Leave it
	// false to get back a record you can actually download from.
	AcceptPendingAssets bool
}

WaitOptions tunes WaitForTerminal. Every zero field takes its default.

type WaitTimeoutError

type WaitTimeoutError struct {
	TaskID    string
	Timeout   time.Duration
	LastState string
}

WaitTimeoutError means a local polling deadline elapsed. It is not evidence that the task failed: the generation is still running somewhere, so store TaskID and reconcile it later instead of submitting the work twice.

func (*WaitTimeoutError) Error

func (e *WaitTimeoutError) Error() string

func (*WaitTimeoutError) Unwrap

func (e *WaitTimeoutError) Unwrap() error

Unwrap lets errors.Is(err, context.DeadlineExceeded) recognise this.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL