Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,20 @@ The `markitdown-ocr` plugin adds OCR support to PDF, DOCX, PPTX, and XLSX conver

```bash
pip install markitdown-ocr
pip install openai # or any OpenAI-compatible client
pip install openai
```

**Usage:**

Pass the same `llm_client` and `llm_model` you would use for image descriptions:
```bash
markitdown document.pdf --use-plugins --llm-client openai --llm-model gpt-4o
```

The CLI creates an `openai.OpenAI()` client only when both LLM options are supplied.
Install it with `pip install openai` and set `OPENAI_API_KEY`.

Or use the Python API and pass the same `llm_client` and `llm_model` you would use
for image descriptions:

```python
from markitdown import MarkItDown
Expand Down
54 changes: 51 additions & 3 deletions packages/markitdown/src/markitdown/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,17 @@ def main():
help="Use 3rd-party plugins to convert files. Use --list-plugins to see installed plugins.",
)

parser.add_argument(
"--llm-client",
choices=("openai",),
help="LLM client to use. Currently supports: openai.",
)

parser.add_argument(
"--llm-model",
help="LLM model name. Required when using --llm-client.",
)

parser.add_argument(
"--list-plugins",
action="store_true",
Expand Down Expand Up @@ -200,6 +211,8 @@ def main():
)
sys.exit(0)

llm_kwargs = _create_llm_kwargs(parser, args)

if args.use_docintel:
if args.endpoint is None:
_exit_with_error(
Expand All @@ -209,7 +222,9 @@ def main():
_exit_with_error("Filename is required when using Document Intelligence.")

markitdown = MarkItDown(
enable_plugins=args.use_plugins, docintel_endpoint=args.endpoint
enable_plugins=args.use_plugins,
docintel_endpoint=args.endpoint,
**llm_kwargs,
)
elif args.use_cu:
if args.cu_endpoint is None:
Expand Down Expand Up @@ -240,9 +255,11 @@ def main():
_exit_with_error(f"Unknown file type: {name}")
cu_kwargs["cu_file_types"] = cu_types

markitdown = MarkItDown(enable_plugins=args.use_plugins, **cu_kwargs)
markitdown = MarkItDown(
enable_plugins=args.use_plugins, **cu_kwargs, **llm_kwargs
)
else:
markitdown = MarkItDown(enable_plugins=args.use_plugins)
markitdown = MarkItDown(enable_plugins=args.use_plugins, **llm_kwargs)

if args.filename is None:
result = markitdown.convert_stream(
Expand Down Expand Up @@ -272,6 +289,37 @@ def _handle_output(args, result: DocumentConverterResult):
)


def _create_llm_kwargs(
parser: argparse.ArgumentParser, args: argparse.Namespace
) -> Dict[str, Any]:
if args.llm_client is None and args.llm_model is None:
return {}

if args.llm_client is None:
parser.error("--llm-model requires --llm-client.")
if args.llm_model is None or not args.llm_model.strip():
parser.error("--llm-client requires a non-empty --llm-model.")

try:
from openai import OpenAI, OpenAIError
except ModuleNotFoundError as error:
if error.name == "openai":
parser.error(
"The 'openai' package is required for --llm-client openai. "
"Install it with 'pip install openai'."
)
raise

try:
llm_client = OpenAI()
except OpenAIError:
parser.error(
"Unable to initialize the OpenAI client. Set OPENAI_API_KEY and try again."
)

return {"llm_client": llm_client, "llm_model": args.llm_model.strip()}


def _exit_with_error(message: str):
print(message)
sys.exit(1)
Expand Down
210 changes: 210 additions & 0 deletions packages/markitdown/tests/test_cli_misc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
#!/usr/bin/env python3 -m pytest
import builtins
import subprocess
import sys
from types import SimpleNamespace
from unittest.mock import Mock

import pytest

import markitdown.__main__ as cli
from markitdown import __version__

# This file contains CLI tests that are not directly tested by the FileTestVectors.
Expand Down Expand Up @@ -27,6 +35,208 @@ def test_invalid_flag() -> None:
assert "SYNTAX" in result.stderr, "Expected 'SYNTAX' to appear in STDERR"


@pytest.mark.parametrize(
("arguments", "expected_kwargs"),
[
(["input.pdf", "--use-plugins"], {"enable_plugins": True}),
(
[
"input.pdf",
"--use-plugins",
"--use-docintel",
"--endpoint",
"https://example.cognitiveservices.azure.com",
],
{
"enable_plugins": True,
"docintel_endpoint": "https://example.cognitiveservices.azure.com",
},
),
(
[
"input.pdf",
"--use-plugins",
"--use-cu",
"--cu-endpoint",
"https://example.cognitiveservices.azure.com",
],
{
"enable_plugins": True,
"cu_endpoint": "https://example.cognitiveservices.azure.com",
},
),
],
)
def test_llm_options_are_forwarded_to_all_constructor_paths(
monkeypatch, arguments, expected_kwargs
) -> None:
class FakeOpenAIError(Exception):
pass

openai_client = Mock()
openai_constructor = Mock(return_value=openai_client)
markitdown_constructor = Mock(
return_value=SimpleNamespace(
convert=Mock(return_value=SimpleNamespace(markdown=""))
)
)
monkeypatch.setitem(
sys.modules,
"openai",
SimpleNamespace(
OpenAI=openai_constructor,
OpenAIError=FakeOpenAIError,
),
)
monkeypatch.setattr(cli, "MarkItDown", markitdown_constructor)
monkeypatch.setattr(
sys,
"argv",
[
"markitdown",
*arguments,
"--llm-client",
"openai",
"--llm-model",
"gpt-4o",
],
)

cli.main()

openai_constructor.assert_called_once_with()
markitdown_constructor.assert_called_once_with(
**expected_kwargs,
llm_client=openai_client,
llm_model="gpt-4o",
)


@pytest.mark.parametrize(
("arguments", "message"),
[
(["input.pdf", "--llm-model", "gpt-4o"], "--llm-model requires --llm-client."),
(
["input.pdf", "--llm-client", "openai"],
"--llm-client requires a non-empty --llm-model.",
),
(
["input.pdf", "--llm-client", "openai", "--llm-model", ""],
"--llm-client requires a non-empty --llm-model.",
),
],
)
def test_llm_options_require_client_and_model(
monkeypatch, capsys, arguments, message
) -> None:
monkeypatch.setattr(sys, "argv", ["markitdown", *arguments])

with pytest.raises(SystemExit) as exc_info:
cli.main()

assert exc_info.value.code == 2
assert message in capsys.readouterr().err


def test_llm_client_rejects_unsupported_values(monkeypatch, capsys) -> None:
monkeypatch.setattr(
sys, "argv", ["markitdown", "input.pdf", "--llm-client", "azure-openai"]
)

with pytest.raises(SystemExit) as exc_info:
cli.main()

assert exc_info.value.code == 2
assert "invalid choice" in capsys.readouterr().err


def test_openai_is_imported_only_when_requested(monkeypatch) -> None:
import_module = builtins.__import__

def fail_if_openai_imported(name, *args, **kwargs):
if name == "openai":
raise AssertionError("openai should not be imported without --llm-client")
return import_module(name, *args, **kwargs)

markitdown_constructor = Mock(
return_value=SimpleNamespace(
convert=Mock(return_value=SimpleNamespace(markdown=""))
)
)
monkeypatch.setattr(builtins, "__import__", fail_if_openai_imported)
monkeypatch.setattr(cli, "MarkItDown", markitdown_constructor)
monkeypatch.setattr(sys, "argv", ["markitdown", "input.pdf"])

cli.main()

markitdown_constructor.assert_called_once_with(enable_plugins=False)


def test_openai_missing_package_error_is_actionable(monkeypatch, capsys) -> None:
import_module = builtins.__import__

def raise_missing_openai(name, *args, **kwargs):
if name == "openai":
raise ModuleNotFoundError("No module named 'openai'", name="openai")
return import_module(name, *args, **kwargs)

monkeypatch.delitem(sys.modules, "openai", raising=False)
monkeypatch.setattr(builtins, "__import__", raise_missing_openai)
monkeypatch.setattr(
sys,
"argv",
[
"markitdown",
"input.pdf",
"--llm-client",
"openai",
"--llm-model",
"gpt-4o",
],
)

with pytest.raises(SystemExit) as exc_info:
cli.main()

assert exc_info.value.code == 2
assert "pip install openai" in capsys.readouterr().err


def test_openai_missing_api_key_error_is_actionable(monkeypatch, capsys) -> None:
class FakeOpenAIError(Exception):
pass

openai_constructor = Mock(
side_effect=FakeOpenAIError("The api_key client option must be set")
)
monkeypatch.setitem(
sys.modules,
"openai",
SimpleNamespace(
OpenAI=openai_constructor,
OpenAIError=FakeOpenAIError,
),
)
monkeypatch.setattr(
sys,
"argv",
[
"markitdown",
"input.pdf",
"--llm-client",
"openai",
"--llm-model",
"gpt-4o",
],
)

with pytest.raises(SystemExit) as exc_info:
cli.main()

assert exc_info.value.code == 2
assert "Set OPENAI_API_KEY" in capsys.readouterr().err


if __name__ == "__main__":
"""Runs this file's tests from the command line."""
test_version()
Expand Down