SDK Task Polling and Streaming Response
Services on Ace Data Cloud are divided into two categories based on response mode:
| Type | Typical Services | Calling Mode |
|---|---|---|
| Synchronous Generation | NanoBanana / Flux / Seedream / Chat Completions (non-streaming) / Google Search | One HTTP call, results in the response body |
| Streaming Response | Chat Completions (stream: true) |
SSE, multi-frame token push |
| Asynchronous Tasks | Midjourney / Sora / Veo / Luma / Kling / Hailuo / Suno / Pixverse / Seedance | First create a task to get task_id, then poll /<provider>/tasks |
This article focuses on the last two categories: TaskHandle polling for asynchronous tasks and the details, pitfalls, and cross-language differences of chat streaming responses.
¶ I. TaskHandle — Unified Abstraction for Asynchronous Tasks
All three SDKs encapsulate asynchronous tasks into TaskHandle, providing the same four methods:
| Method | Behavior |
|---|---|
get() |
Pull the latest status once (POST /<provider>/tasks {id, action: "retrieve"}) |
is_completed() / isCompleted() |
Call get() once to check if status is succeeded / failed |
wait() |
Block polling until succeeded / failed or max_wait timeout |
result property |
The complete response obtained from the last wait() call; null before calling |
¶ Two Calling Methods for Creating Tasks
Each asynchronous resource (images.generate / video.generate / audio.generate) has a wait parameter:
wait=False(default): Immediately returnsTaskHandle, business code decides when to poll.wait=True: The SDK internally callshandle.wait(), returning the response after completion. Only use this when you are sure the upstream will definitely send thestatus: succeededfield—a few providers do not adhere to this convention, causingwaitto keep polling untilmax_waitthrows aTimeoutError.
¶ Unit Differences (⚠️ Must Read)
The units of poll_interval and max_wait differ across the three languages, which is a common pitfall when migrating across languages:
| Language | poll_interval Unit |
max_wait Unit |
Default Value |
|---|---|---|---|
| TypeScript | Milliseconds | Milliseconds | pollInterval=3000, maxWait=600000 |
| Python | Seconds | Seconds | poll_interval=3.0, max_wait=600.0 |
| Go | (TaskHandle not yet exposed in Go SDK) | — | — |
Treating TS's
{ pollInterval: 3000 }as seconds and translating it to Pythonpoll_interval=3000will cause the SDK to wait 50 minutes before polling a second time.
¶ Example: Python Explicit Polling for Midjourney
import os, time
from acedatacloud import AceDataCloud
client = AceDataCloud(api_token=os.environ["ACEDATACLOUD_API_TOKEN"])
# wait=False immediately gets the handle
handle = client.images.generate(
provider="midjourney",
prompt="a cinematic photo of a banana wearing a tuxedo",
wait=False,
)
print("task_id", handle.id)
t0 = time.time()
result = handle.wait(poll_interval=3.0, max_wait=180.0)
print("elapsed_s", round(time.time() - t0, 1))
print("status", result.get("response", result).get("status"))
print("images", [it.get("image_url") for it in (result.get("response", result).get("data") or [])])
The entire code does the following:
images.generate(..., wait=False)submits thepromptto the Midjourney upstream and immediately gets thehandle, without blocking.handle.wait(poll_interval=3.0, max_wait=180.0)internally POSTs to/midjourney/tasksevery 3 seconds until thestatuschanges tosucceededorfailed, or the total time exceeds 180 seconds, throwing aTimeoutError.- Upon completion,
result["response"]["data"]usually contains 4 images (Midjourney defaults to a 2x2 grid).
¶ Example: TypeScript Explicit Polling
import { AceDataCloud } from '@acedatacloud/sdk';
const client = new AceDataCloud();
// wait: false immediately gets the handle
const handle: any = await client.images.generate({
provider: 'midjourney',
prompt: 'a cinematic photo of a banana wearing a tuxedo',
wait: false,
});
console.log('task_id', handle.id);
const t0 = Date.now();
const result: any = await handle.wait({ pollInterval: 3000, maxWait: 180_000 });
console.log('elapsed_s', ((Date.now() - t0) / 1000).toFixed(1));
const response = result.response ?? result;
console.log('status', response.status);
console.log('images', (response.data ?? []).map((it: any) => it.image_url));
¶ Trade-offs Between Synchronous Generation and Asynchronous Tasks
If your provider itself generates images synchronously (NanoBanana / Flux / Seedream), do not pass wait:
# ✅ Recommended
res = client.images.generate(provider="nano-banana", prompt="...")
url = res["data"][0]["image_url"]
# ❌ Anti-example: This will trigger the SDK to poll /nano-banana/tasks internally, wasting RTT
res = client.images.generate(provider="nano-banana", prompt="...", wait=True)
The judgment method is simple: If the upstream API documentation does not have the task_id + /tasks pair, it is synchronous generation; the response of synchronous generation already contains the final result in the data field.
¶ Internal Protocol of TaskHandle
The TaskHandle.get() call is:
POST {API_BASE}/<provider>/tasks
Authorization: Bearer {token}
Content-Type: application/json
{"id": "<task_id>", "action": "retrieve"}
The response has a unified structure:
{
"task_id": "...",
"trace_id": "...",
"response": {
"status": "pending | running | succeeded | failed",
"data": [...]
}
}
The SDK also supports older responses without the outer response wrapper—it directly reads the top-level status, so switching between old and new upstream versions does not affect business code.
¶ II. SSE Streaming Response (chat.completions)
chat.completions.create(stream=True) is currently the only streaming interface in the SDK (audio/video streams are not yet supported). The iterative styles of the three languages are as follows:
| Language | Iteration | Cancellation Mechanism |
|---|---|---|
| TypeScript | for await (const chunk of stream) |
AbortController passed to fetch |
| Python | for chunk in client.openai.chat.completions.create(..., stream=True) |
Break out of the loop (connection closed automatically by SDK) |
| Go | chunks, errs := ...CreateStream(ctx, req) → for chunk := range chunks |
Cancel context.Context |
¶ TypeScript
import { AceDataCloud } from '@acedatacloud/sdk';
const client = new AceDataCloud();
const stream: any = await client.openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'Count from 1 to 5 separated by spaces. Just the numbers.' }],
max_tokens: 30,
temperature: 0,
stream: true
});
let chunks = 0;
let collected = '';
for await (const chunk of stream) {
chunks++;
const delta = chunk?.choices?.[0]?.delta?.content;
if (delta) collected += delta;
}
console.log('chunks', chunks);
console.log('collected', collected);
Actual running result:
total_elapsed_ms 2616
first_chunk_ms 2481
chunks 13
collected 1 2 3 4 5
¶ Python
import os
from acedatacloud import AceDataCloud
client = AceDataCloud(api_token=os.environ["ACEDATACLOUD_API_TOKEN"])
chunks = 0
collected = []
for chunk in client.openai.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Count from 1 to 5 separated by spaces. Just the numbers."}],
max_tokens=30,
temperature=0,
stream=True,
):
chunks += 1
delta = (chunk.get("choices") or [{}])[0].get("delta", {}).get("content")
if delta:
collected.append(delta)
print("chunks", chunks)
print("collected", "".join(collected))
Actual running result:
total_elapsed_ms 2111
first_chunk_ms 2104
chunks 12
collected 1 2 3 4 5
¶ Go
chunks, errs := client.OpenAI().Chat().Completions().CreateStream(ctx, adc.ChatCompletionRequest{
Model: "gpt-4o-mini",
Messages: []map[string]any{{"role": "user", "content": "Count from 1 to 5 separated by spaces. Just the numbers."}},
MaxTokens: 30,
})
cnt := 0
collected := ""
for chunk := range chunks {
cnt++
if ch, ok := chunk["choices"].([]any); ok && len(ch) > 0 {
if d, ok := ch[0].(map[string]any)["delta"].(map[string]any); ok {
if s, ok := d["content"].(string); ok {
collected += s
}
}
}
}
if e, ok := <-errs; ok && e != nil {
log.Println("stream_err", e)
}
fmt.Println("chunks", cnt, "collected", collected)
Actual running result:
total_elapsed_ms 1816
first_chunk_ms 1633
chunks 13
collected 1 2 3 4 5
¶ Structure of Stream Chunks
Each chunk is an OpenAI compatible chat.completion.chunk:
{
"id": "chatcmpl-...",
"object": "chat.completion.chunk",
"model": "gpt-4o-mini",
"choices": [
{
"index": 0,
"delta": { "content": " 3" },
"finish_reason": null
}
]
}
- The first chunk usually has
delta.role: "assistant"butcontentis empty. - The middle chunks each have
delta.content, which can be concatenated directly. - The last chunk has an empty
delta, andfinish_reasonisstop/length/content_filter.
¶ Cancelling Midway
| Language | Cancellation Method |
|---|---|
| TypeScript | Pass signal: abortController.signal in the create() call, call abortController.abort() |
| Python | Use break to exit the for loop, the SDK will close the HTTPx stream in __exit__ |
| Go | Call cancel() on the ctx passed in NewClient, the chunks channel will close immediately |
Tokens generated before cancellation are still billed — tokens generated before the cancellation moment will still be charged based on actual consumption.
¶ III. Timeouts and Retries
The three SDKs share the same retry strategy:
| Trigger Condition | Behavior |
|---|---|
| HTTP 408 / 409 / 429 / 5xx | Default retries 2 times, exponential backoff 1s → 2s → 4s |
| Network layer errors (DNS, connection refused, TLS failure) | Same as above |
| 401 / 403 / 404 / 422 | No retries, directly throw the corresponding typed error |
Streaming (stream=True) requests |
No retries — cannot replay once the first frame has been streamed |
Explicit timeout triggered |
Throw APITimeoutError (Python) / TimeoutError (TS) / context.DeadlineExceeded (Go) |
To disable retries: pass max_retries=0 / maxRetries: 0 / WithMaxRetries(0) when constructing the client.
Polling of asynchronous tasks (TaskHandle) is not affected by max_retries — its loop is business-level rather than HTTP-level, controlled by max_wait for total duration.
¶ IV. Common Pitfalls
- Do not pass
waitfor synchronous providers: NanoBanana / Flux / Seedream are synchronously generated, forcingwait=Truewill make the SDK poll atasksinterface that will not update. - TaskHandle unit differences: Python is in seconds, TS is in milliseconds, make sure to convert when porting across languages.
wait=Truemay still result inTimeoutError: The upstream response must meetstatus in ('succeeded','failed')to exit the loop; if the provider uses different field names, the business code must handlehandle.get()parsing itself.- Streaming cancellation: Tokens generated before cancellation have already been billed.
- Reuse client within the same process: The SDK has a built-in connection pool, frequently creating
new AceDataCloud()/AceDataCloud()will make TLS handshake a bottleneck.
