Skip to content
Esc
↑↓navigate↵open⌘Jpreview
On this page

Partial batch results

Keep the SVGs that finished when other conversions in the same request fail, from JSON responses, native streams, and Responses.

A request for several SVGs can finish some of them and fail on others. The API returns every SVG that finished, and no field counts them for you. Collect the finished SVGs from each response shape:

Response shape Finished SVGs A failed conversion
Native JSON, stream: false Documents in data Omitted from data. There is no per-output error.
Native stream, stream: true content events One event: error after the finished SVGs, then data: [DONE].
Responses JSON, stream: false Completed items in output status can be completed, failed, or incomplete; keep completed items.
Responses stream, stream: true Completed response.output_item.done items Terminal responses can also hold completed items.

The examples use arrow-2. Check GET /v1/models with your API key first: the model must list svg_generate for native generation or open_responses for Responses. See the Quickstart for model discovery and key authority.

Each example on this page requests four SVGs, and one of them fails. The other three finish out of order.

Native JSON responses

POST /v1/svgs/generations without streaming answers 200 when at least one SVG finished:

  • data holds one { svg, mime_type } document for each SVG that finished, so data.length can be smaller than the n you requested.
  • The order of data is unspecified. Documents carry no index, so you cannot tell which requested output a document came from, and you do not need to.
  • A conversion that failed is left out of data. The response has no error object for it.
  • When every conversion fails, the request returns an error status with the JSON error envelope instead of 200.

A request for four SVGs that finished three returns:

{
  "id": "resp_01J9AZ3XJ7D5S9ZV2Q5Z8E1A4N",
  "created": 1704067200,
  "data": [
    {
      "mime_type": "image/svg+xml",
      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...</svg>"
    },
    {
      "mime_type": "image/svg+xml",
      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...</svg>"
    },
    {
      "mime_type": "image/svg+xml",
      "svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" ...</svg>"
    }
  ],
  "usage": { "input_tokens": 1200, "output_tokens": 7800, "total_tokens": 9000 }
}

Save every document in data. Use Node.js 22 or later, run npm install @quiverai/sdk@0.9.4, and save this as generate-batch.mjs:

import { writeFile } from "node:fs/promises";
import { QuiverAI } from "@quiverai/sdk";

const apiKey = process.env.QUIVERAI_API_KEY;
if (!apiKey)
  throw new Error("Set QUIVERAI_API_KEY before running this example.");

const client = new QuiverAI({ bearerAuth: apiKey });
const requested = 4;

const { result } = await client.createSVGs.generateSVG({
  generateSVGRequest: {
    model: "arrow-2",
    prompt: "Four distinct minimalist compass icons in flat blue geometry",
    n: requested,
    stream: false,
  },
});
if ("code" in result) {
  throw new Error(
    `No SVG was returned: ${result.code} (request ${result.requestId}).`
  );
}
if (!("data" in result)) throw new Error("Expected a JSON response.");

for (const [position, document] of result.data.entries()) {
  await writeFile(`compass-${position + 1}.svg`, document.svg, "utf8");
}
console.log(`Saved ${result.data.length} of ${requested} requested SVGs.`);

The file names number the documents in the order they arrived. They are not the positions you requested.

Native streams

With stream: true, each output sends draft events while it is produced and one content event when it finishes:

  • draft is a preview. Its markup can be partial, and a draft for an output that later fails never gets a content event. Show a draft while you wait, but do not save it or treat it as a usable SVG.
  • content is a finished SVG. Keep each one as it arrives.
  • data.index and data.id identify the output. Every draft and the final content event of one output share its index and id. Outputs finish in any order.
  • A failure ends the stream. The stream sends event: error after the SVGs that finished, then data: [DONE]. The error does not name an output. An index with no content event did not finish: it failed, or it was still in progress when the stream ended. The SVGs you already received are still valid.
  • Keep the indices you received. If outputs 0, 2 and 3 finish, they stay 0, 2 and 3. Do not renumber them to 0, 1 and 2. If you only need the finished SVGs, you can collect the content events and ignore the missing index.

The stream for the example below looks like this, with SVG markup shortened:

event: draft
data: {"id":"svg_out_a","index":0,"svg":"<svg","type":"draft","update_type":"snapshot"}

event: content
data: {"id":"svg_out_c","index":2,"svg":"<svg ...</svg>","type":"content","usage":{"input_tokens":1200,"output_tokens":3000,"total_tokens":4200}}

event: content
data: {"id":"svg_out_a","index":0,"svg":"<svg ...</svg>","type":"content","usage":{"input_tokens":1200,"output_tokens":5400,"total_tokens":6600}}

event: content
data: {"id":"svg_out_d","index":3,"svg":"<svg ...</svg>","type":"content","usage":{"input_tokens":1200,"output_tokens":7800,"total_tokens":9000}}

event: error
data: {"type":"error","status":502,"code":"model_error","message":"Model error","request_id":"550e8400-e29b-41d4-a716-446655440000"}

data: [DONE]

Save this as stream-batch.mjs:

import { writeFile } from "node:fs/promises";
import { QuiverAI } from "@quiverai/sdk";

const apiKey = process.env.QUIVERAI_API_KEY;
if (!apiKey)
  throw new Error("Set QUIVERAI_API_KEY before running this example.");

const client = new QuiverAI({ bearerAuth: apiKey });
const requested = 4;

const { result } = await client.createSVGs.generateSVG({
  generateSVGRequest: {
    model: "arrow-2",
    prompt: "Four distinct minimalist compass icons in flat blue geometry",
    n: requested,
    stream: true,
  },
});
if ("code" in result) {
  throw new Error(
    `No SVG was returned: ${result.code} (request ${result.requestId}).`
  );
}
if ("data" in result) throw new Error("Expected an event stream.");

const previews = new Map();
const finished = new Map();
let failure = null;

try {
  for await (const event of result) {
    if (event.event === "error") {
      failure = event.data;
    } else if (event.event === "draft" && event.data.type === "draft") {
      const { id, svg, updateType } = event.data;
      previews.set(
        id,
        updateType === "delta" ? (previews.get(id) ?? "") + svg : svg
      );
    } else if (event.event === "content" && event.data.type === "content") {
      const { id, index = 0, svg } = event.data;
      previews.delete(id);
      await writeFile(`compass-${index}.svg`, svg, "utf8");
      finished.set(index, id);
    }
  }
} finally {
  const received = [...finished.keys()].sort((a, b) => a - b);
  console.log(
    `Saved outputs ${received.join(", ")} of ${requested} requested.`
  );
  if (previews.size > 0)
    console.log(`Discarded unfinished previews: ${previews.size}.`);
}
if (failure) {
  const missing = [...Array(requested).keys()].filter(
    (index) => !finished.has(index)
  );
  console.error(
    `Output ${missing.join(", ")} did not finish: ${failure.code} (request ${failure.requestId}).`
  );
}

It saves compass-0.svg, compass-2.svg and compass-3.svg, and reports that output 1 did not finish. The SDK converts field names to camel case, so update_type and request_id on the wire are updateType and requestId here.

If the connection closes before data: [DONE], the outcome is uncertain. The QuiverAI SDK consumes that sentinel, so its iterator cannot distinguish it from a clean early EOF. Keep the completed SVGs it delivered and investigate any missing outputs; see Streaming failures.

Responses

POST /v1/responses returns SVGs as output items: calls to a tool you declared, such as a write_file function, or SVG markup in a message item when you declare no tool. Read only items whose status is completed, and parse a call’s arguments with the schema you declared for that tool. Never execute a call that is not completed or whose arguments do not match your schema.

The outcome differs between the two transports:

  • Without streaming, a partial result answers 200. Its status can be completed, failed, or incomplete; take individually completed calls from output in each case. Never execute an incomplete call.
  • With streaming, each response.output_item.done event carries one finished item and its output_index. Check that item’s status is completed. A terminal response can also carry completed items in response.output. An event: error can come before response.failed.

The OpenAI SDKs raise an exception when they read event: error: openai 7.23.0 for JavaScript and openai 3.19.2 for Python both do. The response.failed after it never reaches your loop, so collect items from response.output_item.done as they arrive. The streaming helpers fail the same way: after event: error, Python’s stream.get_final_response() raises and JavaScript’s stream.finalResponse() rejects. Iterate the events yourself instead of relying on either helper.

The examples declare write_file, collect completed calls by output_index as they arrive and from the terminal response when available, and save each valid file even if the stream breaks:

Run npm install openai@7.23.0 and save this as responses-batch.mjs:

import { writeFile } from "node:fs/promises";
import OpenAI from "openai";

const apiKey = process.env.QUIVERAI_API_KEY;
if (!apiKey) throw new Error("Set QUIVERAI_API_KEY before running this example.");

const client = new OpenAI({ apiKey, baseURL: "https://api.quiver.ai/v1" });
const paths = ["compass-1.svg", "compass-2.svg", "compass-3.svg", "compass-4.svg"];

const completedItems = new Map();
let failure = null;
let terminal = false;

try {
  const stream = await client.responses.create({
    model: "arrow-2",
    input: `Create four distinct minimalist compass icons in blue. Use write_file once for each of ${paths.join(", ")}.`,
    tools: [{
      type: "function",
      name: "write_file",
      description: "Stage one SVG file for the caller to save.",
      parameters: {
        type: "object",
        properties: { path: { type: "string" }, content: { type: "string" } },
        required: ["path", "content"],
        additionalProperties: false,
      },
      strict: false,
    }],
    stream: true,
    store: false,
  });
  for await (const event of stream) {
    if (event.type === "response.output_item.done" &&
        event.item.type === "function_call" && event.item.status === "completed") {
      completedItems.set(event.output_index, event.item);
    } else if (
      event.type === "response.completed" ||
      event.type === "response.failed" ||
      event.type === "response.incomplete"
    ) {
      terminal = true;
      for (const [index, item] of (event.response.output ?? []).entries()) {
        if (item.type === "function_call" && item.status === "completed" &&
            !completedItems.has(index)) {
          completedItems.set(index, item);
        }
      }
      if (event.type !== "response.completed") {
        failure = event.response.error?.message ?? event.type;
      }
    }
  }
} catch (error) {
  failure = error instanceof Error ? error.message : String(error);
}
if (!terminal && !failure) failure = "stream ended before a terminal response";

const staged = new Map();
for (const [, item] of [...completedItems].sort(([a], [b]) => a - b)) {
  if (item.type !== "function_call" || item.name !== "write_file") continue;
  if (item.status !== "completed") continue;
  let args;
  try {
    args = JSON.parse(item.arguments);
  } catch {
    continue;
  }
  if (args === null || typeof args !== "object" || Array.isArray(args) ||
      Object.keys(args).length !== 2 || !paths.includes(args.path) ||
      typeof args.content !== "string" || !args.content.trim()) {
    continue;
  }
  staged.set(args.path, args.content);
}

for (const [path, svg] of staged) await writeFile(path, svg, "utf8");
console.log(`Saved ${[...staged.keys()].join(", ") || "no files"}.`);
const missing = paths.filter((path) => !staged.has(path));
if (missing.length) console.log(`Missing: ${missing.join(", ")}.`);
if (failure) console.error(`The response did not finish (${failure}).`);

Use Python 3.10 or later, run pip install openai==3.19.2, and save this as responses_batch.py:

import json
import os
from pathlib import Path

import openai

api_key = os.environ.get("QUIVERAI_API_KEY")
if not api_key:
    raise SystemExit("Set QUIVERAI_API_KEY before running this example.")

client = openai.OpenAI(api_key=api_key, base_url="https://api.quiver.ai/v1")
paths = ["compass-1.svg", "compass-2.svg", "compass-3.svg", "compass-4.svg"]
tools = [{
    "type": "function",
    "name": "write_file",
    "description": "Stage one SVG file for the caller to save.",
    "parameters": {
        "type": "object",
        "properties": {"path": {"type": "string"}, "content": {"type": "string"}},
        "required": ["path", "content"],
        "additionalProperties": False,
    },
    "strict": False,
}]

completed_items = {}
failure = None
terminal = False

try:
    stream = client.responses.create(
        model="arrow-2",
        input="Create four distinct minimalist compass icons in blue. "
        f"Use write_file once for each of {', '.join(paths)}.",
        tools=tools,
        stream=True,
        store=False,
    )
    for event in stream:
        if (event.type == "response.output_item.done" and
                event.item.type == "function_call" and event.item.status == "completed"):
            completed_items[event.output_index] = event.item
        elif event.type in ("response.completed", "response.failed", "response.incomplete"):
            terminal = True
            for index, item in enumerate(event.response.output or []):
                if item.type == "function_call" and item.status == "completed":
                    completed_items.setdefault(index, item)
            if event.type != "response.completed":
                failure = event.response.error.message if event.response.error else event.type
except openai.APIError as error:
    failure = error.message
if not terminal and not failure:
    failure = "stream ended before a terminal response"

staged = {}
for _, item in sorted(completed_items.items()):
    if item.type != "function_call" or item.name != "write_file":
        continue
    if item.status != "completed":
        continue
    try:
        args = json.loads(item.arguments)
    except json.JSONDecodeError:
        continue
    if not isinstance(args, dict) or set(args) != {"path", "content"}:
        continue
    if args["path"] not in paths:
        continue
    content = args.get("content")
    if not isinstance(content, str) or not content.strip():
        continue
    staged[args["path"]] = content

for path, svg in staged.items():
    Path(path).write_text(svg, encoding="utf-8")
print(f"Saved {', '.join(staged) or 'no files'}.")
missing = [path for path in paths if path not in staged]
if missing:
    print(f"Missing: {', '.join(missing)}.")
if failure:
    print(f"The response did not finish ({failure}).")

Both save compass-1.svg, compass-3.svg and compass-4.svg when the call for compass-2.svg does not complete, and report the missing file.

Completed calls, previews, and revisions

Three kinds of Responses output look alike and are handled differently:

  • Previews are never output. Argument deltas, items still in_progress, and reasoning text are not a finished SVG. Do not save or execute them.
  • A completed call can be revised. While a tool loop continues, Arrow can send a later call for the same file, for example a correction after you acknowledge a draft. Key staged SVGs by path, so the later call replaces the earlier one.
  • A failed or incomplete response keeps its completed calls. In a stream, keep each response.output_item.done whose item has status: "completed"; a done event can also close an incomplete item. In a buffered response, use completed items in output. Never execute an incomplete item or partial arguments.

To ask for the missing SVG, or to let Arrow refine the ones you kept, continue the loop: replay every completed call and send a matching function_call_output for each, as in the Quickstart. Arrow can revise any of them on that turn, and the latest completed call for a path is the one to keep.

Retry only what is missing

Do not retry the whole batch automatically after a partial result. A repeat of the same request generates new versions of the SVGs you already have, and it is charged again. Decide whether you need the missing outputs, then ask for only those: a native request with n set to the number missing, or a Responses turn that asks for the missing files. Each request is charged separately.

Usage and charges

A partial result settles once, for the whole request:

  • Fixed-credit models charge each SVG returned; see API pricing. A JSON response’s credits is the charge for the request. On a stream, each content event that delivers a billable SVG carries that SVG’s credits. A content event without credits is not charged.
  • Token-priced models, including Arrow 2 and Arrow 2 Telos, charge the whole request’s token usage once. usage describes the request, not one SVG. On a native stream, each content event’s usage is the request’s running total when that SVG finished. Do not add these totals together, and do not split them between SVGs.
  • A stream that ends with an error can settle more usage than the last usage you saw: the request’s final usage is measured after the last SVG you received. On /v1/responses, a failed response can report usage: null. Check Logs and Usage for the settled charge.

See Billing and usage for how each billing kind is read from the model catalog.

Was this page helpful?