Skip to content

Batch Inference

Batch inference lets KeGAL submit multiple LLM calls in a single asynchronous job instead of making them one by one in real time. Most providers offer a ~50% cost reduction for batch jobs and have no rate-limit pressure on throughput.

KeGAL exposes batch inference at two independent levels, both controlled entirely from the YAML graph file.


Provider Support

Provider Batch API Cost reduction Tools Structured output
anthropic client.messages.batches.create() ~50% Yes Yes
anthropic_aws Same as anthropic ~50% Yes Yes
openai JSONL upload + client.batches.create() ~50% Yes Yes
gemini client.batches.create() ~50% Yes Yes
bedrock CreateModelInvocationJob (S3 + IAM) ~50% No No
ollama None — uses thread pool over complete() None Yes Yes

Bedrock batch does not support tools or structured output. Declaring a batch node that uses those features with a Bedrock model raises ValueError at Compiler() construction.

Ollama has no native batch API. When a batch mode is activated on an Ollama node, KeGAL falls back to a ThreadPoolExecutor that calls complete() concurrently, providing parallelism without a batch queue.


Level 1: Intra-node Batch (batch_user_messages)

Semantic: one node, one prompt template, one model — N calls, one per user message. All N calls share the same system prompt, tools, and node configuration. The provider's native async batch API submits all N requests in a single job.

New fields

On Graph (top level):

Field Type Description
batch_user_messages list[str] List of user messages to process. Mutually exclusive with user_message.

On NodePrompt:

Field Type Description
batch_use_messages list[int] Indices into batch_user_messages this node processes.

On GraphNode:

Field Type Description
batch_message_passing NodeBatchMessagePassing Controls how the N batch outputs flow to downstream nodes.

NodeBatchMessagePassing has two boolean flags, both defaulting to false:

Flag Meaning
output: true The N outputs are collected after the batch job completes and forwarded downstream as individual items.
input: true This node receives each upstream batch item as a separate call (N calls, one per item).

Constraint

batch_message_passing.output and batch_message_passing.input must be declared on both the producing node and the consuming node respectively. If the producing node has output: true but the downstream node does not have input: true, ValueError is raised at Compiler() construction.

Output format

How the N results reach the downstream node depends on which message_passing variant is used on the producing node:

With message_passing.output: true (tagged format)

The N results are concatenated into a single string using <message_N> XML tags and passed to the downstream node as the {message_passing} placeholder in a single LLM call:

Text output:

<message_0>text result for message 0</message_0>
<message_1>text result for message 1</message_1>
<message_2>text result for message 2</message_2>

Structured output (each object serialised with json.dumps):

<message_0>{"field": "value_0"}</message_0>
<message_1>{"field": "value_1"}</message_1>
<message_2>{"field": "value_2"}</message_2>

With batch_message_passing.output: true (pass-through format)

Each item is passed to the downstream node individually, identical to how a regular non-batch node passes its output. The downstream node is called N times, once per item. This is transparent to the downstream node: it sees a normal single-item input on each call.

YAML Example

batch_user_messages:
  - "Analyse the financial performance of company A."
  - "Analyse the financial performance of company B."
  - "Analyse the financial performance of company C."

models:
  - llm: "anthropic"
    model: "claude-sonnet-4-6"
    api_key: "${ANTHROPIC_API_KEY}"

prompts:
  # 0 — analyst: one call per company
  - template:
      system_template:
        role: |
          You are a financial analyst. Produce a concise summary of the company's
          financial performance covering revenue, margin, and key risks.
      prompt_template:
        task: "{user_message}"

  # 1 — synthesiser: receives all three summaries
  - template:
      system_template:
        role: |
          You are a portfolio strategist. Given the individual company analyses below,
          produce a comparative overview and investment recommendation.
      prompt_template:
        analyses: |
          {message_passing}

nodes:
  - id: "analyst"
    model: 0
    temperature: 0.3
    max_tokens: 512
    show: true
    prompt:
      template: 0
      batch_use_messages: [0, 1, 2]   # process all three user messages
    message_passing:
      output: true                     # tagged <message_N> format to synthesiser

  - id: "synthesiser"
    model: 0
    temperature: 0.5
    max_tokens: 1024
    show: true
    message_passing:
      input: true                      # receives <message_0>, <message_1>, <message_2>
    prompt:
      template: 1

edges:
  - node: "analyst"
  - node: "synthesiser"

Batch envelope in outputs

When a node runs in batch mode, CompiledNodeOutput.response contains a batch envelope for developer inspection:

{
  "messages": ["summary of company A", "summary of company B", "summary of company C"],
  "batch_size": 3
}

For structured output nodes:

{
  "messages": [{"revenue": "...", "margin": "..."}, {"revenue": "...", "margin": "..."}, {"revenue": "...", "margin": "..."}],
  "batch_size": 3
}

This envelope is for get_outputs() inspection only. It is never sent to a downstream LLM as-is.


Level 2: Inter-node Batch (batch_children / batch_fan_in)

Semantic: N separate nodes, each with its own prompt and model, submitted together as one batch API job. Functionally equivalent to children / fan_in, but the N requests go to the provider in a single batch job rather than as concurrent real-time calls.

All nodes in a batch_children or batch_fan_in group must reference the same model index. Using different model indices raises ValueError at Compiler() construction.

New edge fields

Field Type Description
batch_children list[GraphEdge] Nodes to submit as a single batch job when this node completes. Mutually exclusive with children and ordered_children.
batch_fan_in list[GraphEdge] Nodes this node waits for, submitted as a single batch job. Mutually exclusive with fan_in and ordered_fan_in.

YAML Example

models:
  - llm: "anthropic"
    model: "claude-haiku-4-5-20251001"
    api_key: "${ANTHROPIC_API_KEY}"

prompts:
  - template:    # 0 — dispatcher
      system_template:
        role: Identify the top three research sub-questions from the user's query.
      prompt_template:
        query: "{user_message}"

  - template:    # 1 — branch analyst (reused by all three branches)
      system_template:
        role: Research this specific sub-question in depth.
      prompt_template:
        question: "{message_passing}"

  - template:    # 2 — synthesiser
      system_template:
        role: Combine the three research findings into a coherent report.
      prompt_template:
        findings: "{message_passing}"

nodes:
  - id: "dispatcher"
    model: 0
    temperature: 0.2
    max_tokens: 256
    show: false
    prompt: { template: 0, user_message: true }
    message_passing: { output: true }

  - id: "branch_1"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 1 }
    message_passing: { input: true, output: true }

  - id: "branch_2"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 1 }
    message_passing: { input: true, output: true }

  - id: "branch_3"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 1 }
    message_passing: { input: true, output: true }

  - id: "synthesiser"
    model: 0
    temperature: 0.5
    max_tokens: 1024
    show: true
    prompt: { template: 2 }
    message_passing: { input: true }

edges:
  - node: "dispatcher"
    batch_children:              # submit branch_1, branch_2, branch_3 as one batch job
      - node: "branch_1"
      - node: "branch_2"
      - node: "branch_3"

  - node: "synthesiser"
    batch_fan_in:                # wait for all three; they were submitted as a batch
      - node: "branch_1"
      - node: "branch_2"
      - node: "branch_3"

AWS Bedrock: extra model configuration

Bedrock batch requires an S3 bucket for input/output files and an IAM role with the appropriate permissions. Add three fields to the model entry:

models:
  - llm: "bedrock"
    model: "arn:aws:bedrock:us-east-1::foundation-model/amazon.nova-lite-v1:0"
    aws_region_name: "${AWS_REGION}"
    aws_access_key: "${AWS_ACCESS_KEY_ID}"
    aws_secret_key: "${AWS_SECRET_ACCESS_KEY}"
    batch_role_arn: "${BEDROCK_BATCH_ROLE_ARN}"
    batch_s3_input_uri: "s3://my-bucket/kegal/input"
    batch_s3_output_uri: "s3://my-bucket/kegal/output"
Field Type Description
batch_role_arn str ARN of the IAM role Bedrock assumes to read/write S3.
batch_s3_input_uri str S3 prefix where KeGAL writes the JSONL input file.
batch_s3_output_uri str S3 prefix where Bedrock writes results.

If any of the three fields are absent when a batch mode is activated on a Bedrock node, ValueError is raised at Compiler() construction, listing the missing fields.


Mutual exclusivity rules

Field Exclusive with
batch_user_messages user_message
batch_children children, ordered_children
batch_fan_in fan_in, ordered_fan_in

Out of scope for v1

  • ReAct controller nodes inside batch graphs (ValueError at construction)
  • Streaming partial results during batch polling
  • Mixed model indices in a single batch_children / batch_fan_in group
  • Bedrock batch with tools or structured output