QuiverAI
The QuiverAI provider generates and vectorizes SVG graphics with the Arrow family of models. Output is raw SVG markup, which works with text-based assertions like is-xml, contains, and llm-rubric. Promptfoo also indexes valid single-SVG outputs in the Media Library while preserving the original SVG text for assertions and exports.
Two endpoints are supported:
- Text → SVG (
quiverai:<model>) callsPOST /v1/svgs/generations. - Image → SVG (
quiverai:vectorize:<model>) callsPOST /v1/svgs/vectorizations.
Setup
- Create an API key at app.quiver.ai
- Set the environment variable:
export QUIVERAI_API_KEY=your-api-key
Models
Run GET /v1/models for the live list. The currently released Arrow models are:
| Model | Provider id | Use case |
|---|---|---|
| Arrow 1.1 | quiverai:arrow-1.1 | Default. Best general-purpose tradeoff between quality and credit cost. |
| Arrow 1.1 Max | quiverai:arrow-1.1-max | Higher fidelity for dense illustrations, logos, and technical drawings. |
| Arrow 1.0 | quiverai:arrow-1.0 | Previous-generation model retained for parity. |
The default model is arrow-1.1.
Provider format
quiverai:<model-name> # text → SVG (default)
quiverai:vectorize:<model-name> # image → SVG
quiverai:generate:<model-name> # explicit text → SVG (alias)
quiverai:chat:<model-name> is a legacy alias for the generation endpoint.
Text → SVG
providers:
- id: quiverai:arrow-1.1
config:
temperature: 0.7
max_output_tokens: 8192
instructions: 'flat design, minimal color palette'
With reference images (URL string shorthand or { url } / { base64 }):
providers:
- id: quiverai:arrow-1.1
config:
references:
- https://example.com/style-reference.png
- { url: https://example.com/another.png }
instructions: 'Match the style of the reference image'
Generation parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
instructions | string | — | Style guidance separate from the prompt |
references | array | — | Reference images: URL string, { url }, or { base64 }. Arrow 1.1 accepts up to 4 references; Arrow 1.1 Max accepts up to 16. |
temperature | number | 1 | Randomness (0–2) |
top_p | number | 1 | Nucleus sampling (0–1) |
presence_penalty | number | 0 | Penalize repeated patterns (-2 to 2) |
max_output_tokens | integer | — | Maximum output tokens (1–131,072) |
n | integer | 1 | Number of SVGs to generate (1–16) |
stream | boolean | true | Set false to enable response caching |
apiKey | string | — | API key (overrides environment variable) |
apiBaseUrl | string | — | Custom API base URL |
When n > 1, multiple SVGs are joined with double newlines and ordered by the response's index.
Image → SVG
The vectorize endpoint converts a raster image (PNG, JPEG, WebP) to SVG. The image can come from the prompt or from config.image.
providers:
- id: quiverai:vectorize:arrow-1.1
config:
auto_crop: true
target_size: 1024
prompts:
- '{{image_url}}'
tests:
- vars:
image_url: https://example.com/logo.png
assert:
- type: is-xml
Accepted prompt forms:
- A plain
https://...URL - A
data:image/...;base64,...data URL - A JSON object string like
{"url": "..."}or{"base64": "..."} - A raw base64 payload (treated as
{ base64: ... })
When using an inline data URL in a YAML config, pass it through a variable such as
'{{image_data}}' or set config.image.base64. A bare data: string in
prompts: is interpreted by Promptfoo's prompt loader before the QuiverAI
provider sees it.
You can also provide the image directly in the config and use the prompt for unrelated context:
providers:
- id: quiverai:vectorize:arrow-1.1
config:
image:
url: https://example.com/logo.png
auto_crop: true
Vectorize parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
image | object | — | Override image input ({ url } or { base64 }); falls back to the prompt |
auto_crop | boolean | false | Auto-crop to the dominant subject before vectorization |
target_size | integer | — | Square resize target in pixels (128–4,096) |
temperature | number | 1 | Randomness (0–2) |
top_p | number | 1 | Nucleus sampling (0–1) |
presence_penalty | number | 0 | Penalize repeated patterns (-2 to 2) |
max_output_tokens | integer | — | Maximum output tokens (1–131,072) |
stream | boolean | true | Set false to enable response caching |
apiKey | string | — | API key (overrides environment variable) |
apiBaseUrl | string | — | Custom API base URL |
Streaming
Streaming is on by default. The provider receives generating, reasoning, and draft events while the SVG is being produced and assembles the final SVG from the content event(s). Set stream: false to use the JSON endpoint and enable response caching.
Billing and metadata
QuiverAI bills in credits, not USD. Each successful response surfaces credit cost on the response (top-level credits for non-streaming, per-output credits on streaming content events). Promptfoo exposes both fields via response metadata:
result.metadata.responseId; // server-generated request/output id
result.metadata.credits; // total credits debited for this call
The deprecated usage token block is also propagated to tokenUsage for backwards compatibility, even though the API now zeros those values.
Pipeline: GPT Image → QuiverAI vectorize
Chaining a high-quality raster generator (such as OpenAI's gpt-image-2) into QuiverAI's vectorizer is one of the cleanest ways to produce a consistent, editable SVG icon set. Wrap the two calls in a custom JS provider so the pipeline is one provider in your eval and works with normal is-xml, contains, and llm-rubric assertions.
class GptImageToQuiverPipeline {
constructor(options = {}) {
this.providerId = options.id || 'pipeline:gpt-image-2->quiverai-vectorize';
this.config = options.config || {};
}
id() {
return this.providerId;
}
async callApi(prompt) {
// 1. Generate raster with OpenAI gpt-image-2.
const imgRes = await fetch('https://api.openai.com/v1/images/generations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: this.config.imageModel || 'gpt-image-2',
prompt,
size: '1024x1024',
quality: 'high',
background: 'auto', // gpt-image-2 does not accept 'transparent'
n: 1,
}),
});
if (!imgRes.ok) {
throw new Error(`OpenAI image step failed: HTTP ${imgRes.status}`);
}
const img = await imgRes.json();
const rasterB64 = img.data?.[0]?.b64_json;
if (!rasterB64) {
throw new Error('OpenAI image step returned no image data');
}
// 2. Vectorize with QuiverAI Arrow.
const svgRes = await fetch('https://api.quiver.ai/v1/svgs/vectorizations', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${process.env.QUIVERAI_API_KEY}`,
},
body: JSON.stringify({
model: this.config.vectorizeModel || 'arrow-1.1',
image: { base64: rasterB64 },
auto_crop: true,
target_size: 1024,
}),
});
if (!svgRes.ok) {
throw new Error(`QuiverAI vectorize step failed: HTTP ${svgRes.status}`);
}
const svg = await svgRes.json();
const outputSvg = svg.data?.[0]?.svg;
if (!outputSvg) {
throw new Error('QuiverAI vectorize step returned no SVG data');
}
return {
output: outputSvg,
metadata: { credits: svg.credits, responseId: svg.id },
};
}
}
module.exports = GptImageToQuiverPipeline;
prompts:
- 'Centered icon of {{subject}}, flat vector illustration, bold shapes, minimal palette, clear silhouette.'
providers:
- id: file://pipeline-provider.js
label: 'GPT Image-2 → Arrow 1.1'
config:
imageModel: gpt-image-2
vectorizeModel: arrow-1.1
- id: file://pipeline-provider.js
label: 'GPT Image-2 → Arrow 1.1 Max'
config:
imageModel: gpt-image-2
vectorizeModel: arrow-1.1-max
tests:
- vars:
subject: a friendly red panda mascot facing forward
assert:
- type: is-xml
- type: llm-rubric
value: A clearly recognizable red panda face with reddish-orange fur and dark facial markings.
A complete working example, including red-panda-themed prompts and side-by-side Arrow 1.1 / Arrow 1.1 Max configs, lives at examples/provider-quiverai/promptfooconfig.pipeline.yaml. In the May 2026 verification run behind this example, Arrow 1.1 debited 15 credits per vectorize and Arrow 1.1 Max debited 20 — both surfaced via metadata.credits so you can budget per eval. Read the live GET /v1/models response for current pricing_credits.
Each pipeline call hits two providers serially, so individual evaluations take longer than a pure generation run. Lower --max-concurrency if you start hitting QuiverAI's per-minute rate limit, and prefer stream: false on the vectorize step when you want response caching across re-runs.
Example
prompts:
- 'Create a simple SVG icon of: {{subject}}'
providers:
- id: quiverai:arrow-1.1
config:
max_output_tokens: 8192
tests:
- vars:
subject: a red heart
assert:
- type: is-xml
- type: llm-rubric
value: Contains a heart shape in red color
- vars:
subject: a yellow star
assert:
- type: is-xml
- type: llm-rubric
value: Contains a star shape in yellow/gold color
llm-rubric assertions require a grading provider. By default this uses OpenAI, so set OPENAI_API_KEY or configure a different grader.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
insufficient_credits | Account has no credits | Add credits at app.quiver.ai |
invalid_api_key | Key is missing or invalid | Check QUIVERAI_API_KEY is set correctly |
rate_limit_exceeded | Per-minute rate limit hit | Reduce --max-concurrency or add delays between requests |
weekly_limit_exceeded | Org weekly quota hit | Wait for the rolling weekly window or contact support — retries cannot recover. |
account_frozen | Account is frozen | Contact QuiverAI support |
model_not_found | Invalid model name | Use one of arrow-1.1, arrow-1.1-max, arrow-1.0 |
upstream_error | Transient upstream dependency fail | Retry — this is usually transient |
Error messages include a request_id for debugging with QuiverAI support.
Environment Variables
| Variable | Description |
|---|---|
QUIVERAI_API_KEY | API key (required) |
See Also
- QuiverAI example — generation, vectorization, and pipeline configs
- QuiverAI API docs
- Custom JS providers — the pipeline pattern
- Configuration Reference