BerriAI/litellm · error · ValueError

Bedrock batch embedding `input` must be a string (or a…

Error message

Bedrock batch embedding `input` must be a string (or a single-element list of strings). Got type {type(candidate).__name__} for model={model}.

What it means

Raised while LiteLLM converts an OpenAI /v1/embeddings batch JSONL record into a Bedrock Titan v2 InvokeModel body, which accepts exactly one string in `inputText`. After unwrapping a single-element list, the remaining value must be a Python str; anything else (dict, float, None inside a list, bytes) is rejected before any S3 upload happens. The message names the offending type and the model so you can locate the bad JSONL line.

Solutions

  1. Make `input` a plain JSON string, or a one-element array containing exactly one string: "input": "text to embed"
  2. Emit one JSONL line per embedding — multi-element string lists raise a separate error
  3. If you pre-tokenized the input (arrays of ints), stop: Bedrock batch only accepts raw text for Titan v2
  4. Lint the JSONL locally before upload (see validation snippet) so the failure surfaces before the S3 PUT is signed

Example fix

// before (JSONL line)
{"custom_id": "req_1", "url": "/v1/embeddings", "body": {"model": "amazon.titan-embed-text-v2:0", "input": {"text": "hello"}}}

// after
{"custom_id": "req_1", "url": "/v1/embeddings", "body": {"model": "amazon.titan-embed-text-v2:0", "input": "hello"}}
Defensive patterns

Strategy: type-guard

Validate before calling

import json

def validate_embedding_batch(path: str) -> None:
    for i, line in enumerate(open(path)):
        if not line.strip():
            continue
        rec = json.loads(line)
        inp = rec.get("body", {}).get("input")
        ok = isinstance(inp, str) or (isinstance(inp, list) and len(inp) == 1 and isinstance(inp[0], str))
        if not ok:
            raise ValueError(f"line {i}: `input` must be a string or [string], got {inp!r}")

Type guard

from typing import Any

def is_valid_bedrock_batch_input(inp: Any) -> bool:
    if isinstance(inp, list):
        return len(inp) == 1 and isinstance(inp[0], str) and not isinstance(inp[0], bool)
    return isinstance(inp, str)

Try / catch

try:
    file = await client.files.create(file=batch_file, purpose="batch")
except ValueError as e:
    if "batch embedding `input` must be a string" in str(e):
        # extract the offending record, fix, and re-upload
        log_and_repair_batch_file(e)
    else:
        raise

Prevention

When it happens

Trigger: Uploading a purpose=batch file (litellm.create_file / client.files.create) where an embedding record's `input` is not a string: e.g. {"url": "/v1/embeddings", "body": {"model": "amazon.titan-embed-text-v2:0", "input": {"text": "hi"}}} or "input": [3.14] or "input": [null]. Records without a `url` are classified as embeddings when the body has `input` but no `messages`/`prompt`, so they hit the same check.

Common situations: Reusing OpenAI embedding payloads that wrap text in nested objects; embedding pipelines that emit numbers or nulls for empty documents; hand-edited JSONL files where a quoting mistake turns the string into a number/object.

Related errors


AI-assisted analysis of BerriAI/litellm@77b7c6c40c (2026-08-18). Data as JSON: /api/errors/38cd0e726c0aad7a. Report an issue: GitHub.

Appendix: source

Thrown at litellm/llms/bedrock/files/transformation.py:638

                    "record. Got a list with "
                    f"{len(raw_input)} items for model={model}; emit one "
                    "JSONL line per input string instead."
                )
        else:
            candidate = raw_input

        # Catches pre-tokenized inputs (List[int] from OpenAI spec, or a
        # single int slipping past the list-unwrap above).
        # NOTE: bool is a subclass of int but treating True/False as a token
        # is meaningless either way, so the broad check is fine.
        if isinstance(candidate, (list, int)):
            raise NotImplementedError(
                "Bedrock Titan v2 batch embedding does not support "
                "pre-tokenized integer inputs. Pass `input` as a string "
                f"(model={model})."
            )
        if not isinstance(candidate, str):
            raise ValueError(
                "Bedrock batch embedding `input` must be a string (or a "
                "single-element list of strings). Got type "
                f"{type(candidate).__name__} for model={model}."
            )
        return candidate

    def _map_openai_embedding_to_bedrock_params(
        self,
        openai_request_body: _OpenAIBatchRecordBody,
        model: str,
    ) -> dict[str, object]:
        """
        Transform an OpenAI /v1/embeddings request body into the
        Bedrock InvokeModel `modelInput` for embedding models that AWS
        supports via batch inference (CreateModelInvocationJob).

        Currently routes Amazon Titan Text Embeddings V2 only; other
        embedding providers (Titan G1, Titan Multimodal, Cohere Embed,

View on GitHub (pinned to 77b7c6c40c)