Skip to content

Streaming Prediction

Submit images and receive results as a real-time NDJSON stream (newline-delimited JSON). Each line is a JSON object pushed as processing progresses — ideal for progress indicators or large batches where you want partial results early.

Two versions are available:

  • v1 — upload files directly as multipart/form-data
  • v2 — pass image URLs in a JSON body

POST /inference/predict/stream (v1)

Request

Content-Type: multipart/form-data

Field Type Required Description
api_key string Yes Your API key
query string No Natural language query for LLM analysis
files File[] Yes One or more image files
metadata string[] Yes One JSON string per file controlling inference flags (see below)

Each metadata entry is a JSON-stringified object that controls how the corresponding file is processed:

{ "is_yolo": true, "is_llm": false }
Key Type Description
is_yolo boolean Run YOLO object detection on this file
is_llm boolean Run LLM-based analysis on this file

Metadata ordering

metadata is a parallel array to files. Both arrays must have the same length.

Response

Content-Type: application/x-ndjson

Each line is a JSON object. Read lines incrementally as they arrive:

{"status": "processing", "filename": "photo.jpg"}
{"status": "done", "filename": "photo.jpg", "result": { ... }}

Code Examples

async function predictStream(apiKey, query, files, metadata, onChunk) {
  const form = new FormData();
  form.append("api_key", apiKey);

  if (query) form.append("query", query);

  for (const file of files) {
    form.append("files", file);
  }

  // Each metadata entry must be a JSON string
  for (const m of metadata) {
    form.append("metadata", JSON.stringify(m));
  }

  const response = await fetch(
    "https://sightapi.raku.so/api/v1/inference/predict/stream",
    { method: "POST", body: form }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail?.message ?? "Request failed");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      if (line.trim()) onChunk(JSON.parse(line));
    }
  }
}

// Usage (browser)
const fileInput = document.querySelector("input[type=file]");
await predictStream(
  "YOUR_API_KEY",
  "find me the serial number from the images",
  [...fileInput.files],
  [
    { is_yolo: true,  is_llm: false },
    { is_yolo: true,  is_llm: true  },
    { is_yolo: false, is_llm: true  },
  ],
  (chunk) => console.log("Received:", chunk)
);
interface FileMetadata {
  is_yolo: boolean;
  is_llm: boolean;
}

type ChunkHandler = (chunk: unknown) => void;

async function predictStream(
  apiKey: string,
  query: string | null,
  files: File[],
  metadata: FileMetadata[],
  onChunk: ChunkHandler
): Promise<void> {
  const form = new FormData();
  form.append("api_key", apiKey);

  if (query) form.append("query", query);

  for (const file of files) {
    form.append("files", file);
  }

  // Each metadata entry must be a JSON string
  for (const m of metadata) {
    form.append("metadata", JSON.stringify(m));
  }

  const response = await fetch(
    "https://sightapi.raku.so/api/v1/inference/predict/stream",
    { method: "POST", body: form }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error((error.detail as { message: string })?.message ?? "Request failed");
  }

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (line.trim()) onChunk(JSON.parse(line));
    }
  }
}
import json
import httpx
from collections.abc import Generator

def predict_stream(
    api_key: str,
    query: str | None,
    file_paths: list[str],
    metadata: list[dict],
) -> Generator[dict, None, None]:
    files = [("files", open(p, "rb")) for p in file_paths]

    # metadata entries must be JSON strings
    data = {
        "api_key": api_key,
        "metadata": [json.dumps(m) for m in metadata],
    }
    if query:
        data["query"] = query

    with httpx.Client() as client:
        with client.stream(
            "POST",
            "https://sightapi.raku.so/api/v1/inference/predict/stream",
            data=data,
            files=files,
        ) as response:
            response.raise_for_status()
            for line in response.iter_lines():
                if line:
                    yield json.loads(line)

# Usage
for chunk in predict_stream(
    api_key="YOUR_API_KEY",
    query="find me the serial number from the images",
    file_paths=["./img1.jpg", "./img2.jpg", "./img3.jpg"],
    metadata=[
        {"is_yolo": True,  "is_llm": False},
        {"is_yolo": True,  "is_llm": True },
        {"is_yolo": False, "is_llm": True },
    ],
):
    print("Received:", chunk)
using System.Net.Http;
using System.Text.Json;
using System.Runtime.CompilerServices;

public record FileMetadata(bool IsYolo, bool IsLlm);

public class RakuSightClient
{
    private static readonly HttpClient Http = new();

    public static async IAsyncEnumerable<JsonDocument> PredictStreamAsync(
        string apiKey,
        string? query,
        IEnumerable<(string FileName, Stream Content)> files,
        IEnumerable<FileMetadata> metadata,
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        using var form = new MultipartFormDataContent();
        form.Add(new StringContent(apiKey), "api_key");

        if (query is not null)
            form.Add(new StringContent(query), "query");

        foreach (var (fileName, content) in files)
            form.Add(new StreamContent(content), "files", fileName);

        // Each metadata entry must be a JSON string
        foreach (var m in metadata)
        {
            var json = JsonSerializer.Serialize(new { is_yolo = m.IsYolo, is_llm = m.IsLlm });
            form.Add(new StringContent(json), "metadata");
        }

        using var request = new HttpRequestMessage(HttpMethod.Post,
            "https://sightapi.raku.so/api/v1/inference/predict/stream")
        {
            Content = form,
        };

        using var response = await Http.SendAsync(
            request, HttpCompletionOption.ResponseHeadersRead, ct);

        response.EnsureSuccessStatusCode();

        using var stream = await response.Content.ReadAsStreamAsync(ct);
        using var reader = new System.IO.StreamReader(stream);

        while (!reader.EndOfStream)
        {
            var line = await reader.ReadLineAsync(ct);
            if (!string.IsNullOrWhiteSpace(line))
                yield return JsonDocument.Parse(line);
        }
    }
}

// Usage
await using var s1 = File.OpenRead("img1.jpg");
await using var s2 = File.OpenRead("img2.jpg");
await using var s3 = File.OpenRead("img3.jpg");

await foreach (var chunk in RakuSightClient.PredictStreamAsync(
    "YOUR_API_KEY",
    "find me the serial number from the images",
    [("img1.jpg", s1), ("img2.jpg", s2), ("img3.jpg", s3)],
    [
        new FileMetadata(IsYolo: true,  IsLlm: false),
        new FileMetadata(IsYolo: true,  IsLlm: true),
        new FileMetadata(IsYolo: false, IsLlm: true),
    ]))
{
    Console.WriteLine(chunk.RootElement);
}

OkHttp recommended

Java's built-in HttpClient does not support streaming multipart responses easily. Use OkHttp:

import okhttp3.*;
import java.io.*;

OkHttpClient client = new OkHttpClient();

// Each metadata entry is a JSON string
RequestBody body = new MultipartBody.Builder()
    .setType(MultipartBody.FORM)
    .addFormDataPart("api_key", "YOUR_API_KEY")
    .addFormDataPart("query", "find me the serial number from the images")
    .addFormDataPart("metadata", "{\"is_yolo\": true,  \"is_llm\": false}")
    .addFormDataPart("metadata", "{\"is_yolo\": true,  \"is_llm\": true}")
    .addFormDataPart("metadata", "{\"is_yolo\": false, \"is_llm\": true}")
    .addFormDataPart("files", "img1.jpg",
        RequestBody.create(new File("img1.jpg"), MediaType.parse("image/jpeg")))
    .addFormDataPart("files", "img2.jpg",
        RequestBody.create(new File("img2.jpg"), MediaType.parse("image/jpeg")))
    .addFormDataPart("files", "img3.jpg",
        RequestBody.create(new File("img3.jpg"), MediaType.parse("image/jpeg")))
    .build();

Request request = new Request.Builder()
    .url("https://sightapi.raku.so/api/v1/inference/predict/stream")
    .post(body)
    .build();

try (Response response = client.newCall(request).execute()) {
    if (!response.isSuccessful()) throw new IOException("Unexpected code: " + response);

    BufferedReader reader = new BufferedReader(
        new InputStreamReader(response.body().byteStream()));

    String line;
    while ((line = reader.readLine()) != null) {
        if (!line.isBlank()) {
            System.out.println("Received: " + line);
            // parse with Jackson: objectMapper.readTree(line)
        }
    }
}
package main

import (
    "bufio"
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
)

type FileMetadata struct {
    IsYolo bool `json:"is_yolo"`
    IsLlm  bool `json:"is_llm"`
}

func predictStream(
    apiKey, query string,
    filePaths []string,
    metadata []FileMetadata,
    onChunk func(map[string]any),
) error {
    var buf bytes.Buffer
    w := multipart.NewWriter(&buf)

    _ = w.WriteField("api_key", apiKey)
    if query != "" {
        _ = w.WriteField("query", query)
    }

    // Each metadata entry must be a JSON string
    for _, m := range metadata {
        b, _ := json.Marshal(m)
        _ = w.WriteField("metadata", string(b))
    }

    for _, path := range filePaths {
        f, err := os.Open(path)
        if err != nil {
            return err
        }
        defer f.Close()

        part, _ := w.CreateFormFile("files", filepath.Base(path))
        io.Copy(part, f)
    }
    w.Close()

    resp, err := http.Post(
        "https://sightapi.raku.so/api/v1/inference/predict/stream",
        w.FormDataContentType(),
        &buf,
    )
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            continue
        }
        var chunk map[string]any
        json.Unmarshal([]byte(line), &chunk)
        onChunk(chunk)
    }

    return scanner.Err()
}

func main() {
    err := predictStream(
        "YOUR_API_KEY",
        "find me the serial number from the images",
        []string{"./img1.jpg", "./img2.jpg", "./img3.jpg"},
        []FileMetadata{
            {IsYolo: true,  IsLlm: false},
            {IsYolo: true,  IsLlm: true},
            {IsYolo: false, IsLlm: true},
        },
        func(chunk map[string]any) {
            fmt.Println("Received:", chunk)
        },
    )
    if err != nil {
        panic(err)
    }
}

POST /inference/predict/stream/v2

Same streaming behavior as v1, but accepts image URLs in a JSON body instead of file uploads.

Request

Content-Type: application/json

{
  "api_key": "YOUR_API_KEY",
  "query": "What objects are in this image?",
  "data": [
    {
      "image_url": "https://storage.example.com/images/photo.jpg",
      "filename": "photo.jpg",
      "is_yolo": true,
      "is_llm": false
    }
  ]
}

Response

Content-Type: application/x-ndjson — same streaming format as v1.

Code Examples

async function predictStreamV2(apiKey, query, data, onChunk) {
  const response = await fetch(
    "https://sightapi.raku.so/api/v1/inference/predict/stream/v2",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ api_key: apiKey, query, data }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.detail?.message ?? "Request failed");
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop();

    for (const line of lines) {
      if (line.trim()) onChunk(JSON.parse(line));
    }
  }
}

// Usage
await predictStreamV2(
  "YOUR_API_KEY",
  "Describe the scene",
  [{ image_url: "https://storage.example.com/photo.jpg", filename: "photo.jpg", is_yolo: true, is_llm: false }],
  (chunk) => console.log("Received:", chunk)
);
interface DataItem {
  image_url: string;
  filename: string;
  is_yolo: boolean;
  is_llm: boolean;
}

async function predictStreamV2(
  apiKey: string,
  query: string,
  data: DataItem[],
  onChunk: (chunk: unknown) => void
): Promise<void> {
  const response = await fetch(
    "https://sightapi.raku.so/api/v1/inference/predict/stream/v2",
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ api_key: apiKey, query, data }),
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw new Error((error.detail as { message: string })?.message ?? "Request failed");
  }

  const reader = response.body!.getReader();
  const decoder = new TextDecoder();
  let buffer = "";

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split("\n");
    buffer = lines.pop() ?? "";

    for (const line of lines) {
      if (line.trim()) onChunk(JSON.parse(line));
    }
  }
}
import httpx
import json
from collections.abc import Generator

def predict_stream_v2(
    api_key: str,
    query: str,
    data: list[dict],
) -> Generator[dict, None, None]:
    payload = {"api_key": api_key, "query": query, "data": data}

    with httpx.Client() as client:
        with client.stream(
            "POST",
            "https://sightapi.raku.so/api/v1/inference/predict/stream/v2",
            json=payload,
        ) as response:
            response.raise_for_status()
            for line in response.iter_lines():
                if line:
                    yield json.loads(line)

# Usage
for chunk in predict_stream_v2(
    api_key="YOUR_API_KEY",
    query="Describe the scene",
    data=[
        {
            "image_url": "https://storage.example.com/images/photo.jpg",
            "filename": "photo.jpg",
            "is_yolo": True,
            "is_llm": False,
        }
    ],
):
    print("Received:", chunk)
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
using System.Runtime.CompilerServices;

public class RakuSightClient
{
    private static readonly HttpClient Http = new();

    public static async IAsyncEnumerable<JsonDocument> PredictStreamV2Async(
        string apiKey, string query, IEnumerable<object> data,
        [EnumeratorCancellation] CancellationToken ct = default)
    {
        var payload = new { api_key = apiKey, query, data };

        using var request = new HttpRequestMessage(HttpMethod.Post,
            "https://sightapi.raku.so/api/v1/inference/predict/stream/v2")
        {
            Content = JsonContent.Create(payload),
        };

        using var response = await Http.SendAsync(
            request, HttpCompletionOption.ResponseHeadersRead, ct);

        response.EnsureSuccessStatusCode();

        using var stream = await response.Content.ReadAsStreamAsync(ct);
        using var reader = new System.IO.StreamReader(stream);

        while (!reader.EndOfStream)
        {
            var line = await reader.ReadLineAsync(ct);
            if (!string.IsNullOrWhiteSpace(line))
                yield return JsonDocument.Parse(line);
        }
    }
}
import okhttp3.*;
import java.io.*;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;

public class RakuSightClient {

    private static final OkHttpClient CLIENT = new OkHttpClient();
    private static final ObjectMapper MAPPER = new ObjectMapper();

    public static void predictStreamV2(
            String apiKey, String query, List<?> data) throws Exception {

        String bodyJson = MAPPER.writeValueAsString(
            new java.util.HashMap<String, Object>() {{
                put("api_key", apiKey);
                put("query", query);
                put("data", data);
            }}
        );

        RequestBody body = RequestBody.create(
            bodyJson, MediaType.parse("application/json"));

        Request request = new Request.Builder()
            .url("https://sightapi.raku.so/api/v1/inference/predict/stream/v2")
            .post(body)
            .build();

        try (Response response = CLIENT.newCall(request).execute()) {
            if (!response.isSuccessful()) throw new IOException("Failed: " + response);

            BufferedReader reader = new BufferedReader(
                new InputStreamReader(response.body().byteStream()));

            String line;
            while ((line = reader.readLine()) != null) {
                if (!line.isBlank()) {
                    System.out.println("Received: " + line);
                }
            }
        }
    }
}
package main

import (
    "bufio"
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type DataItem struct {
    ImageURL string `json:"image_url"`
    Filename string `json:"filename"`
    IsYolo   bool   `json:"is_yolo"`
    IsLlm    bool   `json:"is_llm"`
}

type PredictRequest struct {
    APIKey string     `json:"api_key"`
    Query  string     `json:"query"`
    Data   []DataItem `json:"data"`
}

func predictStreamV2(
    apiKey, query string,
    data []DataItem,
    onChunk func(map[string]any),
) error {
    payload, _ := json.Marshal(PredictRequest{APIKey: apiKey, Query: query, Data: data})

    resp, err := http.Post(
        "https://sightapi.raku.so/api/v1/inference/predict/stream/v2",
        "application/json",
        bytes.NewReader(payload),
    )
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    scanner := bufio.NewScanner(resp.Body)
    for scanner.Scan() {
        line := scanner.Text()
        if line == "" {
            continue
        }
        var chunk map[string]any
        json.Unmarshal([]byte(line), &chunk)
        onChunk(chunk)
    }

    return scanner.Err()
}

func main() {
    err := predictStreamV2(
        "YOUR_API_KEY",
        "Describe the scene",
        []DataItem{
            {ImageURL: "https://storage.example.com/photo.jpg", Filename: "photo.jpg", IsYolo: true},
        },
        func(chunk map[string]any) {
            fmt.Println("Received:", chunk)
        },
    )
    if err != nil {
        panic(err)
    }
}