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
- Variables
- func NewIdempotencyKey() (string, error)
- func VerifyWebhook(secret string, header http.Header, rawBody []byte, tolerance time.Duration) (string, error)
- type APIError
- type Asset
- type AssetLayer
- type Balance
- type Client
- func (c *Client) CommitUpload(ctx context.Context, fileID string) (*UploadedFile, error)
- func (c *Client) CreateDownloadURL(ctx context.Context, taskID, key string) (*DownloadTicket, error)
- func (c *Client) CreateTask(ctx context.Context, input CreateTaskInput, idempotencyKey string, ...) (*CreateTaskResult, error)
- func (c *Client) CreateUploadURL(ctx context.Context, contentType string, size int64) (*UploadTicket, error)
- func (c *Client) EachTask(ctx context.Context, opts ListTasksOptions, fn func(TaskListItem) bool) error
- func (c *Client) GetBalance(ctx context.Context) (*Balance, error)
- func (c *Client) GetModel(ctx context.Context, model string) (*Model, error)
- func (c *Client) GetTask(ctx context.Context, taskID string) (*Task, error)
- func (c *Client) GetUsage(ctx context.Context, opts UsageOptions) (*UsageReport, error)
- func (c *Client) ListModels(ctx context.Context, opts ListModelsOptions) (*ModelList, error)
- func (c *Client) ListTasks(ctx context.Context, opts ListTasksOptions) (*TaskList, error)
- func (c *Client) PurgeTask(ctx context.Context, taskID string) (*PurgeResult, error)
- func (c *Client) Quote(ctx context.Context, input CreateTaskInput) (*Quote, error)
- func (c *Client) RetryTask(ctx context.Context, taskID, idempotencyKey string) (*RetryTaskResult, error)
- func (c *Client) Upload(ctx context.Context, data []byte, contentType string) (*UploadedFile, error)
- func (c *Client) UploadFile(ctx context.Context, path, contentType string) (*UploadedFile, error)
- func (c *Client) WaitForTerminal(ctx context.Context, taskID string, opts WaitOptions) (*Task, error)
- type CreateTaskInput
- type CreateTaskOptions
- type CreateTaskResult
- type CreditFacility
- type DownloadTicket
- type FundingGrant
- type FundingOverview
- type HTTPDoer
- type ListModelsOptions
- type ListTasksOptions
- type Model
- type ModelExample
- type ModelList
- type ModelPrice
- type Options
- type PurgeResult
- type Quote
- type RetryTaskResult
- type Task
- type TaskList
- type TaskListItem
- type TaskOutput
- type TaskRetention
- type TaskTranscript
- type TaskTranscriptChannel
- type TaskTranscriptWord
- type UploadTicket
- type UploadedFile
- type UsageDay
- type UsageModel
- type UsageOptions
- type UsageReport
- type WaitOptions
- type WaitTimeoutError
Constants ¶
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.
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.
const ( ErrorCodeInvalidRequest = "invalid_request" ErrorCodeUnsupportedCombination = "unsupported_combination" ErrorCodeContentRejected = "content_rejected" ErrorCodeRateLimited = "rate_limited" 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.
const ( CodeSuccess = 200 CodeUploadMismatch = 40003 CodeUnsupportedCombination = 40004 CodeInsufficientBalance = 40201 CodeSpendCapReached = 40202 CodeEmailNotVerified = 40310 CodeQuoteRejected = 40901 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.
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.
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 ¶
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 ¶
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.
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 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 ¶
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 ¶
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 ¶
GetBalance reads the net available, held and total balance for this account, with the funding sources behind them.
func (*Client) GetModel ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
HasPendingAssets reports whether an artefact is still being copied. A task can be succeeded with its files not yet linkable.
func (*Task) InFlight ¶
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 ¶
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 ¶
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 ¶
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.