{"record":{"id":"ec0053e17d79dbdf","repo":"BerriAI/litellm","slug":"microsoft-purview-dlp-token-id-completion-prompts","errorCode":null,"errorMessage":"Microsoft Purview DLP: Token-id completion prompts cannot be scanned for DLP in blocking mode","messagePattern":"Microsoft Purview DLP: Token-id completion prompts cannot be scanned for DLP in blocking mode","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"error","filePath":"litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py","lineNumber":413,"sourceCode":"\n        prompt_text: str | None = None\n        if call_type in (\"responses\", \"aresponses\"):\n            # Route Responses API calls to the responses-specific extractor\n            # before the generic ``messages`` branch.  This mirrors\n            # ``async_logging_hook`` and ensures ``instructions`` (system\n            # prompt) content is included in the DLP scan, and prevents a\n            # crafted ``messages`` key in the request from being scanned in\n            # place of the actual ``input``.\n            prompt_text = self._responses_api_input_to_str(data, raise_on_failure=True)\n        elif call_type in (\"text_completion\", \"atext_completion\"):\n            raw_prompt: Final = data.get(\"prompt\")\n            # Reject every token-id prompt shape Purview cannot evaluate —\n            # flat ``list[int]`` (single prompt), ``list[list[int]]`` (multi-prompt\n            # batches), and mixed lists that include any token-id sub-array.\n            # Empty/whitespace-only strings also yield ``prompt_text is None`` but\n            # contain no sensitive data and pass through harmlessly below.\n            if self.is_token_id_prompt(raw_prompt):\n                raise HTTPException(\n                    status_code=400,\n                    detail={\n                        \"error\": (\n                            \"Microsoft Purview DLP: Token-id completion prompts \"\n                            \"cannot be scanned for DLP in blocking mode\"\n                        ),\n                    },\n                )\n            prompt_text = self.completion_prompt_to_str(raw_prompt)\n        else:\n            messages: Final[list | None] = data.get(\"messages\")\n            if messages:\n                prompt_text = self.get_prompt_text_for_dlp(cast(list[Any], messages))\n\n        if not prompt_text:\n            return data\n\n        await self._check_content(","sourceCodeStart":395,"sourceCodeEnd":431,"githubUrl":"https://github.com/BerriAI/litellm/blob/77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8/litellm/proxy/guardrails/guardrail_hooks/microsoft_purview/purview_dlp.py#L395-L431","documentation":"Fail-closed HTTPException from the Purview pre-call hook on /v1/completions (text_completion/atext_completion). Token-id prompts — a flat list[int], list[list[int]], or any mixed list containing token-id sub-arrays — carry no scannable text, so Purview cannot evaluate them; blocking mode therefore rejects them with 400 instead of letting content bypass DLP. Note the hook deliberately ignores a crafted 'messages' key when 'prompt' is present.","triggerScenarios":"Calling /v1/completions with prompt=[1234, 5678, ...] or prompt=[[...], [...]] through a model with a blocking Purview guardrail (mode pre_call); some client SDKs defaulting to token arrays when given pre-tokenized input","commonSituations":"Porting token-level workflows or caching-by-token-id pipelines behind a LiteLLM proxy; research tooling that passes BPE ids directly; upgrading a completion endpoint to add DLP and discovering the old payload shape is now rejected","solutions":["Send the prompt as a string, or a list of strings, instead of token ids: prompt=\"...\" or prompt=[\"...\", \"...\"]","Or move the workload to /chat/completions (messages), which the guardrail can scan natively","If token privacy is the reason for token-id prompts, that traffic fundamentally cannot pass a text DLP scan — keep it on an unguarded deployment"],"exampleFix":"# before\nclient.completions.create(model=\"my-model\", prompt=[9122, 233, 908])\n\n# after\nclient.completions.create(model=\"my-model\", prompt=\"Summarize the incident report\")","handlingStrategy":"type-guard","validationCode":"def is_token_id_prompt(prompt) -> bool:\n    if isinstance(prompt, list) and prompt:\n        if all(isinstance(t, int) for t in prompt):\n            return True\n        if any(isinstance(p, list) for p in prompt):\n            return True\n    return False\n\n# before sending to a Purview-guarded /v1/completions route:\nif is_token_id_prompt(payload[\"prompt\"]):\n    raise ValueError(\"blocking DLP cannot scan token-id prompts; send text\")","typeGuard":"from typing import Any\n\ndef is_scannable_completion_prompt(prompt: Any) -> bool:\n    \"\"\"True when prompt is text LiteLLM's DLP scan can evaluate.\"\"\"\n    if isinstance(prompt, str):\n        return True\n    if isinstance(prompt, list):\n        return all(isinstance(p, str) for p in prompt)\n    return False","tryCatchPattern":null,"preventionTips":["Default your SDK calls to string prompts; only use token arrays on routes known to be unguarded","Encode this rule in your API gateway/client wrapper: reject token-id prompts before they hit the proxy","Document for the team: token-id inputs are structurally incompatible with text-based DLP scanning"],"tags":["microsoft-purview","guardrails","text-completions","token-ids","fail-closed"],"backgroundTag":"unsupported-prompt-format","analyzedSha":"77b7c6c40c0c5aa5fbcb1d6a1825ac39ca8829b8","analyzedAt":"2026-08-18T11:44:31.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}