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. Files worth keeping go into the asset library (SaveAsset), where they get a stable spicy://a/ reference that does not expire. 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 AssetErrorReasons() []string
- func IsAssetErrorReason(reason string) bool
- func NewIdempotencyKey() (string, error)
- func VerifyWebhook(secret string, header http.Header, rawBody []byte, tolerance time.Duration) (string, error)
- type APIError
- type Asset
- type AssetCharacter
- type AssetCharacterList
- type AssetLayer
- type AssetLimits
- type AssetList
- type AssetPlan
- type AssetScope
- type AssetUsage
- type AssetUsageTotals
- 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) DeleteAsset(ctx context.Context, assetID string) (*DeletedAsset, error)
- func (c *Client) EachAsset(ctx context.Context, opts ListAssetsOptions, fn func(LibraryAsset) bool) error
- func (c *Client) EachCharacter(ctx context.Context, opts ListCharactersOptions, fn func(AssetCharacter) bool) error
- func (c *Client) EachTask(ctx context.Context, opts ListTasksOptions, fn func(TaskListItem) bool) error
- func (c *Client) GetAsset(ctx context.Context, assetID string) (*LibraryAsset, error)
- func (c *Client) GetAssetUsage(ctx context.Context) (*AssetUsage, error)
- func (c *Client) GetBalance(ctx context.Context) (*Balance, error)
- func (c *Client) GetCharacter(ctx context.Context, characterID string) (*AssetCharacter, 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) ListAssets(ctx context.Context, opts ListAssetsOptions) (*AssetList, error)
- func (c *Client) ListCharacters(ctx context.Context, opts ListCharactersOptions) (*AssetCharacterList, 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) SaveAsset(ctx context.Context, input SaveAssetInput, idempotencyKey string) (*LibraryAsset, error)
- func (c *Client) UpdateAsset(ctx context.Context, assetID string, input UpdateAssetInput) (*LibraryAsset, error)
- func (c *Client) Upload(ctx context.Context, data []byte, contentType string) (*UploadedFile, error)
- func (c *Client) UploadAssetFile(ctx context.Context, path string, opts UploadAssetOptions) (*LibraryAsset, 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 DeletedAsset
- type DownloadTicket
- type FundingGrant
- type FundingOverview
- type HTTPDoer
- type LibraryAsset
- type ListAssetsOptions
- type ListCharactersOptions
- type ListModelsOptions
- type ListTasksOptions
- type Model
- type ModelExample
- type ModelList
- type ModelPrice
- type Options
- type PurgeResult
- type Quote
- type RetryTaskResult
- type SaveAssetInput
- type Task
- type TaskList
- type TaskListItem
- type TaskOutput
- type TaskRetention
- type TaskTranscript
- type TaskTranscriptChannel
- type TaskTranscriptWord
- type UpdateAssetInput
- type UploadAssetOptions
- type UploadTicket
- type UploadedFile
- type UsageDay
- type UsageModel
- type UsageOptions
- type UsageReport
- type WaitOptions
- type WaitTimeoutError
Constants ¶
const ( AssetSourceTaskOutput = "task_output" AssetSourceUpload = "upload" AssetSourceURL = "url" )
Sources a saved asset can come from: the output of one of your successful tasks, a committed upload, or a public URL the server downloads once.
const ( AssetSortRecent = "recent" AssetSortOldest = "oldest" AssetSortName = "name" AssetSortSize = "size" )
Orders of an asset listing. AssetSortRecent, newest first, is the server's default. A cursor only works with the sort it was issued for.
const ( ReasonAssetLibraryLocked = "asset_library_locked" ReasonAssetLibraryFull = "asset_library_full" ReasonAssetFileTooLarge = "asset_file_too_large" ReasonAssetDailyLimit = "asset_daily_limit" ReasonAssetStorageFull = "asset_storage_full" ReasonAssetPermissionDenied = "asset_permission_denied" ReasonAssetWorkspaceForbidden = "asset_workspace_forbidden" ReasonAssetEntityNameTaken = "asset_entity_name_taken" )
Reasons the asset library attaches to a failed response as data.reason, surfaced as APIError.Reason:
asset_library_locked 400 the library opens after the owner's first top-up.
asset_library_full 400 the allowance's storage or item count is used up.
asset_file_too_large 400 the file is above the plan's per-file limit.
asset_daily_limit 429 today's (UTC) allowance of new assets is used up;
RetryAfter runs until 00:00 UTC.
asset_storage_full 503 storage is temporarily full across the platform.
asset_permission_denied 403 the key's team role does not allow this change.
asset_workspace_forbidden 403 this key or workspace cannot reach that library,
for example because its creator left the team.
asset_entity_name_taken 409 the library already has a character with that name.
The server may add reasons later. APIError.Reason carries any value unchanged, so keep a default branch and treat an unknown value like an empty one.
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 AssetURIPrefix = "spicy://a/"
AssetURIPrefix starts a saved asset's stable reference, as in spicy://a/ast_.... Every method that takes an asset ID accepts either the bare ID or this reference.
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.3.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 AssetErrorReasons ¶ added in v0.3.0
func AssetErrorReasons() []string
AssetErrorReasons returns the asset library reasons the contract lists today, as a fresh slice the caller may keep.
func IsAssetErrorReason ¶ added in v0.3.0
IsAssetErrorReason reports whether reason is one of the asset library reasons the contract lists today.
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
// Reason is the failure envelope's data.reason, a stable machine-readable
// refinement of Code, and empty when the server sent none. Branch on it
// (or on Code), never on Message. The asset library's values are the
// Reason* constants; any other value is passed through exactly as
// received, so keep a default branch and treat an unknown one like an
// empty one.
Reason string
}
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 AssetCharacter ¶ added in v0.3.0
type AssetCharacter struct {
ID string `json:"id"`
// Kind is "character", the only kind today.
Kind string `json:"kind"`
// Name is unique within the library, ignoring case.
Name string `json:"name"`
Note string `json:"note"`
// CoverAssetID and Cover are empty and nil while the character has no
// images.
CoverAssetID string `json:"coverAssetId"`
Cover *LibraryAsset `json:"cover"`
// Assets are the character's images in order.
Assets []LibraryAsset `json:"assets"`
Count int `json:"count"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
AssetCharacter is a named, ordered set of images in the asset library. Characters are created and arranged in the console; the API only reads them.
A character references assets instead of copying them, so each image carries its own spicy://a/ URI. To use a character in a generation request, put the URIs of its images, in order, into the model's reference image field.
func (*AssetCharacter) URIs ¶ added in v0.3.0
func (c *AssetCharacter) URIs() []string
URIs returns the spicy://a/ references of the character's images in order, ready for a model's reference image field.
type AssetCharacterList ¶ added in v0.3.0
type AssetCharacterList struct {
Items []AssetCharacter `json:"items"`
// NextCursor is absent on the last page.
NextCursor string `json:"nextCursor,omitempty"`
}
AssetCharacterList is one page of characters, newest first.
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 AssetLimits ¶ added in v0.3.0
type AssetLimits struct {
Tier string `json:"tier"`
MaxCount int `json:"maxCount"`
MaxBytes int64 `json:"maxBytes"`
MaxFileBytes int64 `json:"maxFileBytes"`
// GlobalMaxBytes is the platform-wide ceiling; 0 disables it.
GlobalMaxBytes int64 `json:"globalMaxBytes"`
NameMaxChars int `json:"nameMaxChars"`
}
AssetLimits are the limits of the current plan.
type AssetList ¶ added in v0.3.0
type AssetList struct {
Items []LibraryAsset `json:"items"`
// NextCursor is absent on the last page. It is opaque, and only works with
// the filters and sort it was issued for.
NextCursor string `json:"nextCursor,omitempty"`
}
AssetList is one page of the asset library.
type AssetPlan ¶ added in v0.3.0
type AssetPlan struct {
// ID is the lowercase plan key, for example free or pro.
ID string `json:"id"`
Name string `json:"name"`
// MinTopupUSD is the lifetime top-up, a decimal USD string, that unlocks
// this plan.
MinTopupUSD string `json:"minTopupUsd"`
// MaxBytes is the total storage in bytes (binary units); 0 means saving
// is closed on this plan.
MaxBytes int64 `json:"maxBytes"`
MaxCount int `json:"maxCount"`
MaxFileBytes int64 `json:"maxFileBytes"`
}
AssetPlan is one storage plan. Plans unlock with the owner's lifetime top-ups and never go down.
type AssetScope ¶ added in v0.3.0
type AssetScope struct {
// WorkspaceID is the personal workspace or the team, and empty for an
// account with no personal workspace yet.
WorkspaceID string `json:"workspaceId"`
Personal bool `json:"personal"`
// Role is always owner for a personal library. For a team key it is the
// current role of the member who created the key: owner, admin,
// developer, billing or readonly.
Role string `json:"role"`
// CanWrite says whether new assets can be saved here.
CanWrite bool `json:"canWrite"`
// CanTopUp says whether the caller can see and raise the owner's
// allowance. When it is false, AssetUsage.LifetimeTopupUSD and
// TopupToNextTierUSD are empty.
CanTopUp bool `json:"canTopUp"`
}
AssetScope says which library the key reaches and what it may do there.
type AssetUsage ¶ added in v0.3.0
type AssetUsage struct {
Tier AssetPlan `json:"tier"`
Usage AssetUsageTotals `json:"usage"`
Limits AssetLimits `json:"limits"`
// Full is true when no new asset fits, including plans where saving is
// closed.
Full bool `json:"full"`
// NextTier is the next plan with more storage or items, nil on the
// highest plan.
NextTier *AssetPlan `json:"nextTier"`
// TopupToNextTierUSD and LifetimeTopupUSD are decimal USD strings, empty
// when Scope.CanTopUp is false.
TopupToNextTierUSD string `json:"topupToNextTierUsd,omitempty"`
LifetimeTopupUSD string `json:"lifetimeTopupUsd,omitempty"`
Tiers []AssetPlan `json:"tiers"`
// DailyCreateLimit is the number of new assets allowed per allowance per
// UTC day, and CreatedToday how many were saved so far.
DailyCreateLimit int `json:"dailyCreateLimit"`
CreatedToday int `json:"createdToday"`
Scope AssetScope `json:"scope"`
}
AssetUsage is the storage plan, what is stored, the limits and how many assets were saved today. Usage and limits cover the owner's whole allowance: the personal library plus every team library they own.
type AssetUsageTotals ¶ added in v0.3.0
AssetUsageTotals is what the allowance holds.
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, and LoRA weights (application/octet-stream, a .safetensors file) at 2 GiB; the ticket's MaxBytes is the authoritative ceiling.
The ticket lives about twenty minutes (two hours for LoRA weights) and that window covers both the PUT and the commit, so ask for it right before uploading rather than in advance.
func (*Client) DeleteAsset ¶ added in v0.3.0
DeleteAsset deletes an asset. It disappears at once and its URI stops working in new requests. Deleting an unknown or already deleted asset returns the same result, so repeating it is safe and no Idempotency-Key is sent.
func (*Client) EachAsset ¶ added in v0.3.0
func (c *Client) EachAsset(ctx context.Context, opts ListAssetsOptions, fn func(LibraryAsset) bool) error
EachAsset walks every page of ListAssets and calls fn once per asset, starting from opts.Cursor when it is set. Return false from fn to stop early; the error is the first page that failed.
The filters and Sort go out unchanged on every page and only the cursor moves, because a cursor only works with the query it was issued for. A cursor that comes back twice is refused instead of looping. Limit sets the page size, not the total.
func (*Client) EachCharacter ¶ added in v0.3.0
func (c *Client) EachCharacter(ctx context.Context, opts ListCharactersOptions, fn func(AssetCharacter) bool) error
EachCharacter walks every page of ListCharacters and calls fn once per character. Return false from fn to stop early. A cursor that comes back twice is refused instead of looping.
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) GetAsset ¶ added in v0.3.0
GetAsset reads one asset with a fresh private PreviewURL. assetID may be the bare ast_... ID or its spicy://a/ reference. An unknown asset, another account's asset and one in a library this key does not reach are all the same 404.
func (*Client) GetAssetUsage ¶ added in v0.3.0
func (c *Client) GetAssetUsage(ctx context.Context) (*AssetUsage, error)
GetAssetUsage reads the storage plan, what is stored, the limits, how many assets were saved today and the Scope the key acts with.
func (*Client) GetBalance ¶
GetBalance reads the net available, held and total balance for this account, with the funding sources behind them.
func (*Client) GetCharacter ¶ added in v0.3.0
GetCharacter reads one character with its images in order, each with a fresh private PreviewURL. An unknown character and one outside the key's library are the same 404.
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) ListAssets ¶ added in v0.3.0
ListAssets returns one page of the ready assets in the key's library. Use EachAsset to walk every page.
func (*Client) ListCharacters ¶ added in v0.3.0
func (c *Client) ListCharacters(ctx context.Context, opts ListCharactersOptions) (*AssetCharacterList, error)
ListCharacters returns one page of the library's characters, newest first, each with its images in order. Characters are read-only through the API.
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) SaveAsset ¶ added in v0.3.0
func (c *Client) SaveAsset(ctx context.Context, input SaveAssetInput, idempotencyKey string) (*LibraryAsset, error)
SaveAsset copies a file into the library and returns the saved asset. Saving the same source again returns the asset you already have and uses no extra space.
The server requires an Idempotency-Key on every save. Leave idempotencyKey empty and a fresh one is minted for this call, which also makes the automatic resend on a network fault safe: every attempt of the call carries the same key. Pass your own (see NewIdempotencyKey) to replay the same save safely after an uncertain response, for example across a process restart; the server binds the key to the exact request for 24 hours.
func (*Client) UpdateAsset ¶ added in v0.3.0
func (c *Client) UpdateAsset(ctx context.Context, assetID string, input UpdateAssetInput) (*LibraryAsset, error)
UpdateAsset renames an asset, marks or unmarks it as a favorite, or both, and returns the updated asset. The URI stays the same. Repeating the request is safe, so no Idempotency-Key is sent.
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.
Put the URI into the model input field whose x-ui.widget is "upload" (or inside the list for "multi-upload"); LoRA weights go into loras[].path. The URI lasts one day. To keep a file longer, save it to the asset library with SaveAsset (source AssetSourceUpload) or upload it with UploadAssetFile.
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) UploadAssetFile ¶ added in v0.3.0
func (c *Client) UploadAssetFile(ctx context.Context, path string, opts UploadAssetOptions) (*LibraryAsset, error)
UploadAssetFile uploads a local file with UploadFile and saves it to the library in one call, returning the saved asset. The upload's type and size limits apply. Nothing is saved when the upload fails. Every call uploads again, so there is no idempotency key to pass: the save uses a fresh one for its own retries.
func (*Client) UploadFile ¶
UploadFile uploads a local file. An empty contentType is inferred from the extension, which only works for the accepted media types; ".safetensors" LoRA weights are sent as application/octet-stream.
The file is streamed from disk rather than read into memory first, because LoRA weights may be 2 GiB. A file that large can outlast DefaultUploadTimeout on an ordinary uplink, so raise Options.UploadTimeout for big ones; the ticket itself stays valid for two hours.
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 DeletedAsset ¶ added in v0.3.0
DeletedAsset confirms a deletion.
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 LibraryAsset ¶ added in v0.3.0
type LibraryAsset struct {
ID string `json:"id"`
// URI is the stable spicy://a/ast_... reference. It works only in
// generation requests made with keys of the same workspace, in any media
// field of a model input; it is never a downloadable link. Store this.
URI string `json:"uri"`
Name string `json:"name"`
// Kind is image, video or audio.
Kind string `json:"kind"`
ContentType string `json:"contentType"`
Bytes int64 `json:"bytes"`
// Width and Height are measured by the server, and 0 when they do not
// apply.
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
// DurationSeconds is measured by the server for audio and video, and 0
// otherwise.
DurationSeconds float64 `json:"durationSeconds,omitempty"`
// Source is one of the AssetSource* values.
Source string `json:"source"`
// SourceID is your task or upload ID; for a URL, an opaque digest of the
// address, which itself is not stored.
SourceID string `json:"sourceId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
// PreviewURL is a private preview signed for at most 20 minutes, and empty
// when signing failed. Show it; never store or share it.
PreviewURL string `json:"previewUrl"`
PreviewURLExpiresAt *time.Time `json:"previewUrlExpiresAt,omitempty"`
// Favorite belongs to the library, so everyone in a team sees the same
// favorites.
Favorite bool `json:"favorite"`
// Model is the public name of the model that generated the asset, and
// ModelName its display name. Both are empty for uploads and links.
Model string `json:"model"`
ModelName string `json:"modelName"`
}
LibraryAsset is one saved file of the asset library. It is a different record from Asset, which is one output of a task.
type ListAssetsOptions ¶ added in v0.3.0
type ListAssetsOptions struct {
// Kind is image, video or audio.
Kind string
// Source keeps the assets from any of these sources (sent
// comma-separated); see the AssetSource* values.
Source []string
// Q is a case-insensitive substring of the asset name.
Q string
// Favorite, when non-nil, lists only favorites (true) or only the assets
// that are not favorites (false). It is a pointer because false is a
// filter of its own, not "no filter".
Favorite *bool
// CollectionID keeps the assets of one folder of the same library.
// Folders are made in the console; an ID outside the key's library is
// answered with 404.
CollectionID string
// EntityID keeps the images of one character; an unknown ID gives an
// empty page.
EntityID string
// Sort is one of the AssetSort* values; the server defaults to
// AssetSortRecent. An unknown sort is refused before anything is sent.
Sort string
// Cursor is the previous page's NextCursor, unchanged and with the same
// filters and Sort. A cursor sent with a different Sort fails with 400,
// which is returned as-is rather than retried.
Cursor string
// Limit is the page size; the server defaults to 100. Its upper bound is
// the server's to enforce, as with ListTasksOptions.Limit.
Limit int
}
ListAssetsOptions filters and orders an asset listing. A zero field means "do not filter".
type ListCharactersOptions ¶ added in v0.3.0
type ListCharactersOptions struct {
// AssetID keeps only the characters that contain this asset, as ast_...
// or spicy://a/ast_....
AssetID string
// Cursor is the previous page's NextCursor, unchanged.
Cursor string
// Limit is the page size; the server defaults to 50.
Limit int
}
ListCharactersOptions filters a character listing. A zero field means "do not filter".
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 SaveAssetInput ¶ added in v0.3.0
type SaveAssetInput struct {
// Source is one of the AssetSource* values.
Source string `json:"source"`
// TaskID, with Key or Index, selects an output of one of your own
// successful tasks (with a team key, one of the team's). Index is the
// zero-based position in Task.Output.Assets; it is a pointer because 0 is
// a real position.
TaskID string `json:"taskId,omitempty"`
Key string `json:"key,omitempty"`
Index *int `json:"index,omitempty"`
// FileID is a committed upload, as fil_... or spicy://f/fil_....
FileID string `json:"fileId,omitempty"`
// URL is a public http or https address on port 80 or 443 without
// credentials. The server downloads it once and reads the type from the
// file itself.
URL string `json:"url,omitempty"`
// Name is optional; the server defaults to "Saved asset".
Name string `json:"name,omitempty"`
}
SaveAssetInput names exactly one source to save. Fields that belong to another source are refused before anything is sent.
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 UpdateAssetInput ¶ added in v0.3.0
type UpdateAssetInput struct {
// Name renames the asset when it is not empty.
Name string `json:"name,omitempty"`
// Favorite marks (true) or unmarks (false) the asset when it is non-nil.
Favorite *bool `json:"favorite,omitempty"`
}
UpdateAssetInput changes an asset's name, its favorite mark or both. Only what is set is sent.
type UploadAssetOptions ¶ added in v0.3.0
type UploadAssetOptions struct {
// Name defaults to the file's base name, cut to 80 characters.
Name string
// ContentType is inferred from the extension when empty, as in UploadFile.
ContentType string
}
UploadAssetOptions tunes UploadAssetFile. Every zero field takes its default.
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.