mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-27 06:13:07 +00:00
Backport of #45301 to stable/1.104.x.
Cherry-picked from 50ab9a3211 (main).
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
149 lines
5.3 KiB
Python
149 lines
5.3 KiB
Python
import base64
|
|
import json
|
|
from collections.abc import Iterator, Mapping, Sequence
|
|
from dataclasses import dataclass
|
|
from itertools import accumulate
|
|
from types import MappingProxyType
|
|
from typing import Final
|
|
|
|
from integration._support.wire import Reply
|
|
from pydantic import JsonValue
|
|
|
|
PDF_MEDIA_TYPE: Final = "application/pdf"
|
|
COUNT_TOKENS_TARGET: Final = "/v1/messages/count_tokens"
|
|
COUNT_REFUSED: Final = Reply(
|
|
status=400,
|
|
body=json.dumps(
|
|
{"type": "error", "error": {"type": "invalid_request_error", "message": "count_tokens is not supported"}}
|
|
).encode(),
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Page:
|
|
width: int = 612
|
|
height: int = 792
|
|
text: str | None = None
|
|
|
|
|
|
LETTER: Final = Page()
|
|
NARROW: Final = Page(width=100, height=1000)
|
|
_RENDERED_TOKENS: Final = MappingProxyType({(612, 792): 1534, (100, 1000): 328})
|
|
|
|
|
|
def rendered_tokens(pages: Sequence[Page]) -> int:
|
|
return sum(_RENDERED_TOKENS[(page.width, page.height)] for page in pages)
|
|
|
|
|
|
def _escaped(text: str) -> str:
|
|
return text.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)")
|
|
|
|
|
|
def _content_stream(page: Page) -> bytes:
|
|
stream: Final = f"BT /F1 12 Tf 72 {page.height - 72} Td ({_escaped(page.text or '')}) Tj ET".encode()
|
|
return b"<< /Length " + str(len(stream)).encode() + b" >>\nstream\n" + stream + b"\nendstream"
|
|
|
|
|
|
def _page_object(page: Page, contents_id: int | None) -> bytes:
|
|
contents: Final = f" /Contents {contents_id} 0 R" if contents_id is not None else ""
|
|
return (
|
|
f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 {page.width} {page.height}]"
|
|
f" /Resources << /Font << /F1 1 0 R >> >>{contents} >>"
|
|
).encode()
|
|
|
|
|
|
def _page_bodies(pages: Sequence[Page], starts: Sequence[int]) -> Iterator[bytes]:
|
|
for page, start in zip(pages, starts):
|
|
if page.text is None:
|
|
yield _page_object(page, None)
|
|
else:
|
|
yield _content_stream(page)
|
|
yield _page_object(page, start)
|
|
|
|
|
|
def pdf_bytes(pages: Sequence[Page]) -> bytes:
|
|
starts: Final = tuple(accumulate((1 if page.text is None else 2 for page in pages), initial=3))
|
|
page_ids: Final = tuple(start + (0 if page.text is None else 1) for start, page in zip(starts, pages))
|
|
kids: Final = " ".join(f"{identity} 0 R" for identity in page_ids)
|
|
bodies: Final = (
|
|
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
|
f"<< /Type /Pages /Kids [{kids}] /Count {len(pages)} >>".encode(),
|
|
*_page_bodies(pages, starts),
|
|
b"<< /Type /Catalog /Pages 2 0 R >>",
|
|
)
|
|
header: Final = b"%PDF-1.4\n"
|
|
objects: Final = tuple(
|
|
f"{number} 0 obj\n".encode() + body + b"\nendobj\n" for number, body in enumerate(bodies, start=1)
|
|
)
|
|
offsets: Final = tuple(accumulate((len(chunk) for chunk in objects), initial=len(header)))
|
|
entries: Final = b"".join(f"{offset:010d} 00000 n \n".encode() for offset in offsets[:-1])
|
|
trailer: Final = (
|
|
f"xref\n0 {len(bodies) + 1}\n".encode()
|
|
+ b"0000000000 65535 f \n"
|
|
+ entries
|
|
+ f"trailer\n<< /Size {len(bodies) + 1} /Root {starts[-1]} 0 R >>\nstartxref\n{offsets[-1]}\n%%EOF\n".encode()
|
|
)
|
|
return header + b"".join(objects) + trailer
|
|
|
|
|
|
def encoded(raw: bytes) -> str:
|
|
return base64.b64encode(raw).decode()
|
|
|
|
|
|
def data_url(media_type: str, raw: bytes) -> str:
|
|
return f"data:{media_type};base64,{encoded(raw)}"
|
|
|
|
|
|
def pdf_data_url(pages: Sequence[Page]) -> str:
|
|
return data_url(PDF_MEDIA_TYPE, pdf_bytes(pages))
|
|
|
|
|
|
def base64_source(data: JsonValue, media_type: JsonValue = PDF_MEDIA_TYPE) -> dict[str, JsonValue]:
|
|
return {"type": "base64", "media_type": media_type, "data": data}
|
|
|
|
|
|
def document(source: Mapping[str, JsonValue], **fields: JsonValue) -> dict[str, JsonValue]:
|
|
return {"type": "document", "source": dict(source), **fields}
|
|
|
|
|
|
def pdf_document(pages: Sequence[Page], **fields: JsonValue) -> dict[str, JsonValue]:
|
|
return document(base64_source(encoded(pdf_bytes(pages))), **fields)
|
|
|
|
|
|
def text_document(text: str) -> dict[str, JsonValue]:
|
|
return document({"type": "text", "media_type": "text/plain", "data": text})
|
|
|
|
|
|
def chat_file(file_data: JsonValue, filename: JsonValue = "document.pdf") -> dict[str, JsonValue]:
|
|
return {"type": "file", "file": {"filename": filename, "file_data": file_data}}
|
|
|
|
|
|
def responses_input_file(file_data: JsonValue, filename: JsonValue = "document.pdf") -> dict[str, JsonValue]:
|
|
return {"type": "input_file", "filename": filename, "file_data": file_data}
|
|
|
|
|
|
def messages_body(model: str, blocks: Sequence[JsonValue], text: str, **fields: JsonValue) -> dict[str, JsonValue]:
|
|
return {
|
|
"model": model,
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": [*blocks, {"type": "text", "text": text}]}],
|
|
**fields,
|
|
}
|
|
|
|
|
|
def chat_body(model: str, parts: Sequence[JsonValue], text: str, **fields: JsonValue) -> dict[str, JsonValue]:
|
|
return {
|
|
"model": model,
|
|
"max_tokens": 64,
|
|
"messages": [{"role": "user", "content": [*parts, {"type": "text", "text": text}]}],
|
|
**fields,
|
|
}
|
|
|
|
|
|
def responses_body(model: str, items: Sequence[JsonValue], text: str, **fields: JsonValue) -> dict[str, JsonValue]:
|
|
return {
|
|
"model": model,
|
|
"max_output_tokens": 64,
|
|
"input": [{"role": "user", "content": [*items, {"type": "input_text", "text": text}]}],
|
|
**fields,
|
|
}
|