Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
lang: cURL
source: |-
# OpenAI provider - Chat Completions
# OpenAI provider — endpoint required (/v1/responses or /v1/chat/completions)
curl -sS -X POST "https://inference.do-ai.run/v1/batches" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-H "Content-Type: application/json" \
Expand All @@ -12,14 +12,13 @@ source: |-
"request_id": "c7e3ad1e-20c3-4e47-9bf2-6f2a4d6a2f11"
}'

# Anthropic provider - Messages
# Anthropic provider — DO NOT send endpoint
curl -sS -X POST "https://inference.do-ai.run/v1/batches" \
-H "Authorization: Bearer $DIGITALOCEAN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"file_id": "a1b2c3d4-e5f6-4789-90ab-cdef12345678",
"provider": "anthropic",
"endpoint": "/v1/messages",
"completion_window": "24h",
"request_id": "2f1a7d9e-8c03-4d2c-9b7e-6f8e2b1a4c77"
}'
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ lang: cURL
source: |-
# UPLOAD_URL is the exact upload_url returned by POST /v1/batches/files.
# Use it verbatim; do not modify the host, path, or query string.
#
# Send the raw JSONL bytes with --data-binary so line endings and UTF-8
# are preserved. The presigned URL is signature-sensitive: prefer
# application/octet-stream (or omit Content-Type entirely) — a custom
# value such as application/jsonl can break signature matching unless
# the URL was signed for that exact header.
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: application/jsonl" \
--data-binary "@eval_prompts_v1.jsonl"
-H "Content-Type: application/octet-stream" \
--data-binary "@batch_requests.jsonl"
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
lang: JavaScript
source: |-
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues.

result.id → result.batch_id.
result.cancel_requested_at doesn't exist; use cancelled_at or just print status.

import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const result = await client.batches.cancel(process.env.BATCH_ID);

console.log("batch_id: ", result.batch_id);
console.log("status: ", result.status);
console.log("cancelled_at:", result.cancelled_at);
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
lang: JavaScript
source: |-
import { randomUUID } from "node:crypto";
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const batch = await client.batches.create({
file_id: process.env.BATCH_INPUT_FILE_ID,
provider: "openai",
endpoint: "/v1/chat/completions",
completion_window: "24h",
request_id: randomUUID(),
});

console.log("batch_id:", batch.batch_id);
console.log("status: ", batch.status);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
lang: JavaScript
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right against batch_file_create_response.yml. One nit: client.files.create(...) reads like OpenAI-Files; if the SDK actually exposes this as client.batches.files.create(...) (the URL is /v1/batches/files), prefer that name for clarity.

source: |-
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const intent = await client.files.create({
file_name: "batch_requests.jsonl",
});

console.log("file_id: ", intent.file_id);
console.log("upload_url:", intent.upload_url);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
lang: JavaScript
source: |-
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const batch = await client.batches.retrieve(process.env.BATCH_ID);

console.log("batch_id: ", batch.batch_id);
console.log("status: ", batch.status);
console.log("request_counts:", batch.request_counts);
console.log("output_file_id:", batch.output_file_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
lang: JavaScript
source: |-
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const batchId = process.env.BATCH_ID;

// client.files.content resolves the result envelope and follows the
// presigned URL for you, returning the raw fetch Response.
const resp = await client.files.content(batchId);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Likely wrong API call. client.files.content(batchId) passes a batch id to a files helper. The endpoint GET /v1/batches/{batch_id}/results returns presigned URLs in batch_results_response.yml; you then fetch(output_file_url). Should be:
const links = await client.batches.results.retrieve(batchId);
if (!links.result_available) { console.log("not ready"); return; }
const resp = await fetch(links.output_file_url);

if (!resp.ok) {
throw new Error(`results not ready: HTTP ${resp.status}`);
}

const body = await resp.text();
const lines = body.split("\n").filter(Boolean);

console.log(`got ${lines.length} line(s); first entry:`);
console.log(lines[0]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
lang: JavaScript
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — wrong pagination shape. Uses page.edges.map(e => e.node) (Relay-style), but batch_list_response.yml is { object, data, has_more, first_id, last_id }. Should be:

for (const b of page.data ?? []) {
console.log(${b.batch_id}\t${b.status}\t${b.created_at});
}
console.log("has_more:", page.has_more, "last_id:", page.last_id);

source: |-
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

const page = await client.batches.list({ limit: 20 });

for (const b of page.data ?? []) {
console.log(`${b.batch_id}\t${b.status}\t${b.created_at}`);
}

console.log("has_more:", page.has_more);
console.log("last_id: ", page.last_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
lang: JavaScript
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Combines step 1 (reserve intent) and step 2 (PUT bytes) into one snippet. That's fine but it duplicates create_batch_file. Consider trimming step 1 here so each example documents one endpoint, matching the curl pair.

source: |-
// Two-step upload flow:
// 1. Reserve a file_id + presigned upload_url via client.files.create.
// 2. PUT the raw JSONL bytes to upload_url.
//
// The presigned URL is short-lived (~15 minutes) and signature-sensitive —
// use it verbatim and prefer Content-Type application/octet-stream (or
// omit the header entirely). A custom value such as application/jsonl
// can break signature matching.
import { readFile } from "node:fs/promises";
import { InferenceClient } from "@digitalocean/dots";

const client = new InferenceClient({
apiKey: process.env.DIGITALOCEAN_TOKEN,
});

// Step 1: reserve the upload slot.
const intent = await client.files.create({ file_name: "batch_requests.jsonl" });

// Step 2: PUT the JSONL bytes to the presigned URL.
const body = await readFile("batch_requests.jsonl");
const res = await fetch(intent.upload_url, {
method: "PUT",
headers: { "Content-Type": "application/octet-stream" },
body,
});
if (!res.ok) {
throw new Error(`Upload failed: HTTP ${res.status} ${res.statusText}`);
}

console.log("uploaded file_id:", intent.file_id);
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
lang: Python
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two blockers.

result.get("id") → result.get("batch_id").
result.get("cancel_requested_at") doesn't exist on batch.yml. Use cancelled_at (or print status only — the cancel response is the full batch and the user mostly cares that status is cancelling / cancelled).

source: |-
import os

from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

result = client.batches.cancel(os.environ["BATCH_ID"])

print("batch_id: ", result.get("batch_id"))
print("status: ", result.get("status"))
print("cancelled_at:", result.get("cancelled_at"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
lang: Python
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — request body doesn't match batch_create_request.yml.

input_file_id= should be file_id= (line 8 of the example).
Missing required provider (e.g. "openai").
Missing required request_id — it's the idempotency key. Add import uuid and pass request_id=str(uuid.uuid4()).
Suggested:

batch = client.batches.create( body={ "file_id": os.environ["BATCH_INPUT_FILE_ID"], "provider": "openai", "endpoint": "/v1/chat/completions", "completion_window": "24h", "request_id": str(uuid.uuid4()), } )

print("batch_id:", batch.get("batch_id"))

Also batch.get("id") → batch.get("batch_id") per batch.yml:12.

source: |-
import os
import uuid

from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

batch = client.batches.create(
file_id=os.environ["BATCH_INPUT_FILE_ID"],
provider="openai",
endpoint="/v1/chat/completions",
completion_window="24h",
request_id=str(uuid.uuid4()),
)

print("batch_id:", batch.get("batch_id"))
print("status: ", batch.get("status"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
lang: Python
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — wrong endpoint and wrong response shape.
The spec endpoint is POST /v1/batches/files, which returns { file_id, upload_url, expires_at } per batch_file_create_response.yml. The example instead calls client.files.create(file=input_path, purpose="batch") (OpenAI Files-style: send the bytes + a purpose) and reads uploaded.filename / uploaded.bytes — none of those exist on this response, and purpose isn't on the request schema.

Mirror the dots version: call the batch-files create method with file_name=... and print file_id / upload_url. The actual JSONL bytes belong in inference_upload_batch_file.yml, not here.

source: |-
import json
import os
from pathlib import Path

from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

input_path = Path("batch_requests.jsonl")
requests = [
{
"custom_id": "q-1",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "llama3.3-70b-instruct",
"messages": [
{"role": "user", "content": "One fun fact about octopuses."}
],
"max_tokens": 128,
},
},
{
"custom_id": "q-2",
"method": "POST",
"url": "/v1/chat/completions",
"body": {
"model": "llama3.3-70b-instruct",
"messages": [
{"role": "user", "content": "One fun fact about sharks."}
],
"max_tokens": 128,
},
},
]
input_path.write_text("\n".join(json.dumps(r) for r in requests) + "\n")

uploaded = client.files.create(file=input_path, purpose="batch")

print("file_id: ", uploaded.file_id)
print("filename:", uploaded.filename)
print("bytes: ", uploaded.bytes)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
lang: Python
source: |-
import os

from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

batch = client.batches.retrieve(os.environ["BATCH_ID"])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — batch.get("id") is always None. Per batch.yml, the field is batch_id. Change to batch.get("batch_id").

print("batch_id: ", batch.get("batch_id"))
print("status: ", batch.get("status"))
print("request_counts:", batch.get("request_counts"))
print("output_file_id:", batch.get("output_file_id"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
lang: Python
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocker — wrong field name. Line reads links["output_file_id"], but batch_results_response.yml returns output_file_url (a short-lived presigned URL). The endpoint does not return an output file ID.

The follow-up client.files.content(...) call also doesn't compose: you GET the presigned URL with requests.get, you don't pass it through the SDK. Rewrite as:

import requests
links = client.batches.results.retrieve(batch_id)
if not links.get("result_available"):
print("results not ready yet"); raise SystemExit(0)
resp = requests.get(links["output_file_url"], timeout=60)
resp.raise_for_status()
Path("batch_output.jsonl").write_bytes(resp.content)

source: |-
import os
from pathlib import Path

import requests
from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

batch_id = os.environ["BATCH_ID"]

links = client.batches.results.retrieve(batch_id)

if not links.get("result_available"):
print("results not ready yet; poll batch status and retry")
raise SystemExit(0)

resp = requests.get(links["output_file_url"], timeout=60)
resp.raise_for_status()

out = Path("batch_output.jsonl")
out.write_bytes(resp.content)

print("wrote:", out)
print("----- preview -----")
print(resp.text[:500])
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
lang: Python
source: |-
import os

from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

resp = client.batches.list(limit=20)

for b in resp.get("data") or []:
print(f"{b.get('batch_id'):40} {b.get('status'):12} {b.get('created_at')}")

print("has_more:", resp.get("has_more"))
print("last_id: ", resp.get("last_id"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
lang: Python
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading lead comment. Lines 1–4 claim client.files.create() "performs both steps for you, prefer it" — that contradicts your create_batch_file example, which only reserves the intent. Drop the comment or rewrite it to say "step 1 reserves file_id+upload_url (see create_batch_file); this example PUTs the bytes."

PUT logic itself looks fine. Minor: avoid printing upload_url-derived state.

source: |-
# Two-step upload flow:
# 1. Reserve a file_id + presigned upload_url via client.batches.files.create.
# 2. PUT the raw JSONL bytes to upload_url.
#
# The presigned URL is short-lived (~15 minutes) and signature-sensitive —
# use it verbatim and prefer Content-Type application/octet-stream (or
# omit the header entirely). A custom value such as application/jsonl
# can break signature matching.
import os
from pathlib import Path

import requests
from pydo import Client

client = Client(token=os.environ.get("DIGITALOCEAN_TOKEN"))

input_path = Path("batch_requests.jsonl")

# Step 1: reserve the upload slot.
intent = client.batches.files.create(file_name=input_path.name)
upload_url = intent["upload_url"]
file_id = intent["file_id"]

# Step 2: PUT the JSONL bytes to the presigned URL.
with input_path.open("rb") as fh:
put = requests.put(
upload_url,
data=fh,
headers={"Content-Type": "application/octet-stream"},
timeout=60,
)
put.raise_for_status()

print("uploaded file_id:", file_id)
7 changes: 6 additions & 1 deletion specification/resources/inference/inference_cancel_batch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ description: >
Requests cancellation of a batch job. The job transitions to `cancelling`
and, once in-flight requests drain, to `cancelled`. Jobs already in a
terminal state (`completed`, `failed`, `expired`, `cancelled`) cannot be
cancelled and return `409 Conflict`.
cancelled and return `409 Conflict`. Cancellation is also rejected with
`409 Conflict` while the job has not yet been submitted to the upstream
provider — there is nothing to cancel until the provider batch id is
assigned.


Partial results produced before cancellation remain available via
Expand Down Expand Up @@ -54,5 +57,7 @@ responses:
$ref: '../../shared/responses/unexpected_error.yml'
x-codeSamples:
- $ref: 'examples/curl/inference_cancel_batch.yml'
- $ref: 'examples/python/inference_cancel_batch.yml'
- $ref: 'examples/dots/inference_cancel_batch.yml'
security:
- inference_bearer_auth: []
7 changes: 4 additions & 3 deletions specification/resources/inference/inference_create_batch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,17 @@ requestBody:
endpoint: "/v1/chat/completions"
completion_window: "24h"
request_id: "c7e3ad1e-20c3-4e47-9bf2-6f2a4d6a2f11"
OpenAI Embeddings:
OpenAI Responses:
value:
file_id: "a1b2c3d4-e5f6-4789-90ab-cdef12345678"
provider: "openai"
endpoint: "/v1/embeddings"
endpoint: "/v1/responses"
completion_window: "24h"
request_id: "9f7b9d4a-4e6c-4a27-8e35-1b0e4c5a9a12"
Anthropic Messages:
value:
file_id: "a1b2c3d4-e5f6-4789-90ab-cdef12345678"
provider: "anthropic"
endpoint: "/v1/messages"
completion_window: "24h"
request_id: "2f1a7d9e-8c03-4d2c-9b7e-6f8e2b1a4c77"
metadata:
Expand Down Expand Up @@ -80,5 +79,7 @@ responses:
$ref: '../../shared/responses/unexpected_error.yml'
x-codeSamples:
- $ref: 'examples/curl/inference_create_batch.yml'
- $ref: 'examples/python/inference_create_batch.yml'
- $ref: 'examples/dots/inference_create_batch.yml'
security:
- inference_bearer_auth: []
Loading
Loading