{"record":{"id":"38cd0e726c0aad7a","repo":"BerriAI/litellm","slug":"bedrock-batch-embedding-input-must-be-a-string","errorCode":null,"errorMessage":"Bedrock batch embedding `input` must be a string (or a single-element list of strings). Got type {type(candidate).__name__} for model={model}.","messagePattern":"Bedrock batch embedding `input` must be a string \\(or a single-element list of strings\\)\\. Got type (.+?) for model=(.+?)\\.","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"litellm/llms/bedrock/files/transformation.py","lineNumber":638,"sourceCode":"                    \"record. Got a list with \"\n                    f\"{len(raw_input)} items for model={model}; emit one \"\n                    \"JSONL line per input string instead.\"\n                )\n        else:\n            candidate = raw_input\n\n        # Catches pre-tokenized inputs (List[int] from OpenAI spec, or a\n        # single int slipping past the list-unwrap above).\n        # NOTE: bool is a subclass of int but treating True/False as a token\n        # is meaningless either way, so the broad check is fine.\n        if isinstance(candidate, (list, int)):\n            raise NotImplementedError(\n                \"Bedrock Titan v2 batch embedding does not support \"\n                \"pre-tokenized integer inputs. Pass `input` as a string \"\n                f\"(model={model}).\"\n            )\n        if not isinstance(candidate, str):\n            raise ValueError(\n                \"Bedrock batch embedding `input` must be a string (or a \"\n                \"single-element list of strings). Got type \"\n                f\"{type(candidate).__name__} for model={model}.\"\n            )\n        return candidate\n\n    def _map_openai_embedding_to_bedrock_params(\n        self,\n        openai_request_body: _OpenAIBatchRecordBody,\n        model: str,\n    ) -> dict[str, object]:\n        \"\"\"\n        Transform an OpenAI /v1/embeddings request body into the\n        Bedrock InvokeModel `modelInput` for embedding models that AWS\n        supports via batch inference (CreateModelInvocationJob).\n\n        Currently routes Amazon Titan Text Embeddings V2 only; other\n        embedding providers (Titan G1, Titan Multimodal, Cohere Embed,","sourceCodeStart":620,"sourceCodeEnd":656,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/llms/bedrock/files/transformation.py#L620-L656","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Make `input` a plain JSON string, or a one-element array containing exactly one string: \"input\": \"text to embed\"","Emit one JSONL line per embedding — multi-element string lists raise a separate error","If you pre-tokenized the input (arrays of ints), stop: Bedrock batch only accepts raw text for Titan v2","Lint the JSONL locally before upload (see validation snippet) so the failure surfaces before the S3 PUT is signed"],"exampleFix":"// before (JSONL line)\n{\"custom_id\": \"req_1\", \"url\": \"/v1/embeddings\", \"body\": {\"model\": \"amazon.titan-embed-text-v2:0\", \"input\": {\"text\": \"hello\"}}}\n\n// after\n{\"custom_id\": \"req_1\", \"url\": \"/v1/embeddings\", \"body\": {\"model\": \"amazon.titan-embed-text-v2:0\", \"input\": \"hello\"}}","handlingStrategy":"type-guard","validationCode":"import json\n\ndef validate_embedding_batch(path: str) -> None:\n    for i, line in enumerate(open(path)):\n        if not line.strip():\n            continue\n        rec = json.loads(line)\n        inp = rec.get(\"body\", {}).get(\"input\")\n        ok = isinstance(inp, str) or (isinstance(inp, list) and len(inp) == 1 and isinstance(inp[0], str))\n        if not ok:\n            raise ValueError(f\"line {i}: `input` must be a string or [string], got {inp!r}\")","typeGuard":"from typing import Any\n\ndef is_valid_bedrock_batch_input(inp: Any) -> bool:\n    if isinstance(inp, list):\n        return len(inp) == 1 and isinstance(inp[0], str) and not isinstance(inp[0], bool)\n    return isinstance(inp, str)","tryCatchPattern":"try:\n    file = await client.files.create(file=batch_file, purpose=\"batch\")\nexcept ValueError as e:\n    if \"batch embedding `input` must be a string\" in str(e):\n        # extract the offending record, fix, and re-upload\n        log_and_repair_batch_file(e)\n    else:\n        raise","preventionTips":["Generate embedding JSONL with a typed writer that only emits str inputs","Never pre-tokenize inputs for Bedrock batch — pass raw text","Run the local JSONL lint (validationCode) in CI before any upload"],"tags":["bedrock","embeddings","batch","jsonl","input-validation","litellm"],"backgroundTag":"request-body-validation","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}