vlmparse 0.1.6__tar.gz → 0.1.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vlmparse-0.1.6 → vlmparse-0.1.7}/PKG-INFO +1 -1
- {vlmparse-0.1.6 → vlmparse-0.1.7}/pyproject.toml +1 -1
- {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_batch_parser.py +14 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_cli.py +8 -38
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/cli.py +16 -36
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/granite_docling.py +1 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/openai_converter.py +9 -7
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/constants.py +3 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/converter_with_server.py +64 -25
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/servers/docker_server.py +6 -7
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/servers/utils.py +39 -11
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/PKG-INFO +1 -1
- {vlmparse-0.1.6 → vlmparse-0.1.7}/LICENSE +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/README.md +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/setup.cfg +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_all_converters_mocked.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_end2end.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/base_model.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/build_doc.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/chandra.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/deepseekocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/docling.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/dotsocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/hunyuanocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/lightonocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/mineru.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/nanonetocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/olmocr.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/paddleocrvl.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/cleaner.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/html_to_md_conversion.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/utils.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/prompts.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/converter.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/data_model/box.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/data_model/document.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/registries.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/st_viewer/fs_nav.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/st_viewer/st_viewer.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/utils.py +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/SOURCES.txt +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/dependency_links.txt +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/entry_points.txt +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/requires.txt +0 -0
- {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/top_level.txt +0 -0
|
@@ -34,6 +34,8 @@ class TestBatchParser:
|
|
|
34
34
|
mock_server = MagicMock()
|
|
35
35
|
mock_client = MagicMock()
|
|
36
36
|
|
|
37
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
38
|
+
|
|
37
39
|
mock_config.get_server.return_value = mock_server
|
|
38
40
|
mock_config.get_client.return_value = mock_client
|
|
39
41
|
mock_docker_registry.get.return_value = mock_config
|
|
@@ -87,7 +89,11 @@ class TestBatchParser:
|
|
|
87
89
|
"""Test that parse method updates client configuration and calls batch."""
|
|
88
90
|
# Setup mocks
|
|
89
91
|
mock_config = MagicMock()
|
|
92
|
+
mock_server = MagicMock()
|
|
90
93
|
mock_client = MagicMock()
|
|
94
|
+
|
|
95
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
96
|
+
mock_config.get_server.return_value = mock_server
|
|
91
97
|
mock_config.get_client.return_value = mock_client
|
|
92
98
|
mock_docker_registry.get.return_value = mock_config
|
|
93
99
|
|
|
@@ -136,7 +142,11 @@ class TestBatchParser:
|
|
|
136
142
|
|
|
137
143
|
# Setup mocks
|
|
138
144
|
mock_config = MagicMock()
|
|
145
|
+
mock_server = MagicMock()
|
|
139
146
|
mock_client = MagicMock()
|
|
147
|
+
|
|
148
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
149
|
+
mock_config.get_server.return_value = mock_server
|
|
140
150
|
mock_config.get_client.return_value = mock_client
|
|
141
151
|
mock_docker_registry.get.return_value = mock_config
|
|
142
152
|
|
|
@@ -161,7 +171,11 @@ class TestBatchParser:
|
|
|
161
171
|
):
|
|
162
172
|
"""Test that retrylast raises ValueError if no previous runs found."""
|
|
163
173
|
mock_config = MagicMock()
|
|
174
|
+
mock_server = MagicMock()
|
|
164
175
|
mock_client = MagicMock()
|
|
176
|
+
|
|
177
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
178
|
+
mock_config.get_server.return_value = mock_server
|
|
165
179
|
mock_config.get_client.return_value = mock_client
|
|
166
180
|
mock_docker_registry.get.return_value = mock_config
|
|
167
181
|
|
|
@@ -46,7 +46,7 @@ def mock_docker_server():
|
|
|
46
46
|
mock_container.name = "test_container_name"
|
|
47
47
|
|
|
48
48
|
# Configure server.start() to return base_url and container
|
|
49
|
-
mock_server.start.return_value = ("http://localhost:8056",
|
|
49
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
50
50
|
mock_config.get_server.return_value = mock_server
|
|
51
51
|
mock_registry.get.return_value = mock_config
|
|
52
52
|
|
|
@@ -79,7 +79,7 @@ class TestServeCommand:
|
|
|
79
79
|
cli.serve(model="lightonocr")
|
|
80
80
|
|
|
81
81
|
# Verify registry was called with correct model
|
|
82
|
-
mock_registry.get.assert_called_once_with("lightonocr")
|
|
82
|
+
mock_registry.get.assert_called_once_with("lightonocr", default=True)
|
|
83
83
|
|
|
84
84
|
# Verify port was set to default
|
|
85
85
|
assert mock_config.docker_port == 8056
|
|
@@ -128,7 +128,7 @@ class TestServeCommand:
|
|
|
128
128
|
# Should not raise an exception, just log warning
|
|
129
129
|
cli.serve(model="unknown_model")
|
|
130
130
|
|
|
131
|
-
mock_registry.get.assert_called_once_with("unknown_model")
|
|
131
|
+
mock_registry.get.assert_called_once_with("unknown_model", default=True)
|
|
132
132
|
|
|
133
133
|
|
|
134
134
|
class TestConvertCommand:
|
|
@@ -216,6 +216,7 @@ class TestConvertCommand:
|
|
|
216
216
|
mock_converter = MagicMock()
|
|
217
217
|
mock_converter.batch.return_value = None
|
|
218
218
|
|
|
219
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
219
220
|
mock_docker_config.get_server.return_value = mock_server
|
|
220
221
|
mock_docker_config.get_client.return_value = mock_converter
|
|
221
222
|
mock_docker_registry.get.return_value = mock_docker_config
|
|
@@ -241,6 +242,7 @@ class TestConvertCommand:
|
|
|
241
242
|
mock_converter = MagicMock()
|
|
242
243
|
mock_converter.batch.return_value = None
|
|
243
244
|
|
|
245
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
244
246
|
mock_docker_config.get_server.return_value = mock_server
|
|
245
247
|
mock_docker_config.get_client.return_value = mock_converter
|
|
246
248
|
mock_docker_registry.get.return_value = mock_docker_config
|
|
@@ -354,6 +356,7 @@ class TestCLIIntegration:
|
|
|
354
356
|
|
|
355
357
|
mock_converter.batch.return_value = None
|
|
356
358
|
|
|
359
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
357
360
|
mock_docker_config.get_server.return_value = mock_server
|
|
358
361
|
mock_docker_config.get_client.return_value = mock_converter
|
|
359
362
|
mock_docker_registry.get.return_value = mock_docker_config
|
|
@@ -377,7 +380,7 @@ class TestCLIIntegration:
|
|
|
377
380
|
mock_container = MagicMock()
|
|
378
381
|
mock_container.id = "test_id"
|
|
379
382
|
mock_container.name = "test_name"
|
|
380
|
-
mock_server.start.return_value = ("http://localhost:8056",
|
|
383
|
+
mock_server.start.return_value = ("http://localhost:8056", None)
|
|
381
384
|
mock_docker_config.get_server.return_value = mock_server
|
|
382
385
|
mock_docker_registry.get.return_value = mock_docker_config
|
|
383
386
|
|
|
@@ -464,7 +467,7 @@ class TestCLIConvertInDepth:
|
|
|
464
467
|
|
|
465
468
|
mock_server.start.return_value = (
|
|
466
469
|
"http://localhost:8000/v1",
|
|
467
|
-
|
|
470
|
+
None,
|
|
468
471
|
)
|
|
469
472
|
mock_docker_config.get_server.return_value = mock_server
|
|
470
473
|
|
|
@@ -727,36 +730,3 @@ class TestCLIConvertInDepth:
|
|
|
727
730
|
assert len(page_contents) == 2
|
|
728
731
|
assert "Page 1" in page_contents[0]
|
|
729
732
|
assert "Page 2" in page_contents[1]
|
|
730
|
-
|
|
731
|
-
def test_convert_with_nanonet_postprompt(self, cli, file_path, mock_openai_api):
|
|
732
|
-
"""Test that Nanonet model uses its postprompt correctly."""
|
|
733
|
-
from vlmparse.registries import converter_config_registry
|
|
734
|
-
|
|
735
|
-
messages_sent = []
|
|
736
|
-
|
|
737
|
-
async def capture_messages(*args, **kwargs):
|
|
738
|
-
messages_sent.append(kwargs.get("messages", []))
|
|
739
|
-
mock_response = MagicMock()
|
|
740
|
-
mock_response.choices = [MagicMock()]
|
|
741
|
-
mock_response.choices[0].message.content = "Extracted text"
|
|
742
|
-
return mock_response
|
|
743
|
-
|
|
744
|
-
mock_openai_api.chat.completions.create = AsyncMock(
|
|
745
|
-
side_effect=capture_messages
|
|
746
|
-
)
|
|
747
|
-
|
|
748
|
-
cli.convert(
|
|
749
|
-
inputs=[str(file_path)],
|
|
750
|
-
model="nanonets/Nanonets-OCR2-3B",
|
|
751
|
-
uri="http://mocked-api/v1",
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
# Verify messages were sent (2 pages)
|
|
755
|
-
assert len(messages_sent) == 2
|
|
756
|
-
|
|
757
|
-
# Verify config has postprompt set
|
|
758
|
-
config = converter_config_registry.get(
|
|
759
|
-
"nanonets/Nanonets-OCR2-3B", uri="http://mocked-api/v1"
|
|
760
|
-
)
|
|
761
|
-
assert config.postprompt is not None
|
|
762
|
-
assert len(config.postprompt) > 0
|
|
@@ -11,8 +11,8 @@ class DParseCLI:
|
|
|
11
11
|
model: str,
|
|
12
12
|
port: int | None = None,
|
|
13
13
|
gpus: str | None = None,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
vllm_args: list[str] | None = None,
|
|
15
|
+
forget_predefined_vllm_args: bool = False,
|
|
16
16
|
):
|
|
17
17
|
"""Deploy a VLLM server in a Docker container.
|
|
18
18
|
|
|
@@ -20,43 +20,26 @@ class DParseCLI:
|
|
|
20
20
|
model: Model name
|
|
21
21
|
port: VLLM server port (default: 8056)
|
|
22
22
|
gpus: Comma-separated GPU device IDs (e.g., "0" or "0,1,2"). If not specified, all GPUs will be used.
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
vllm_args: Additional keyword arguments to pass to the VLLM server.
|
|
24
|
+
forget_predefined_vllm_args: If True, the predefined VLLM kwargs from the docker config will be replaced by vllm_args otherwise the predefined kwargs will be updated with vllm_args with a risk of collision of argument names.
|
|
25
25
|
"""
|
|
26
|
-
if port is None:
|
|
27
|
-
port = 8056
|
|
28
26
|
|
|
29
|
-
from vlmparse.
|
|
27
|
+
from vlmparse.converter_with_server import start_server
|
|
30
28
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Only override GPU configuration if explicitly specified
|
|
41
|
-
# This preserves CPU-only settings from the config
|
|
42
|
-
if gpus is not None:
|
|
43
|
-
docker_config.gpu_device_ids = [g.strip() for g in str(gpus).split(",")]
|
|
44
|
-
server = docker_config.get_server(auto_stop=False)
|
|
45
|
-
|
|
46
|
-
if server is None:
|
|
47
|
-
logger.error(f"Model server not found for model: {model}")
|
|
48
|
-
return
|
|
49
|
-
|
|
50
|
-
# Deploy server and leave it running (cleanup=False)
|
|
51
|
-
logger.info(
|
|
52
|
-
f"Deploying VLLM server for {docker_config.model_name} on port {port}..."
|
|
29
|
+
base_url, container, _, _ = start_server(
|
|
30
|
+
model=model,
|
|
31
|
+
gpus=gpus,
|
|
32
|
+
port=port,
|
|
33
|
+
with_vllm_server=True,
|
|
34
|
+
vllm_args=vllm_args,
|
|
35
|
+
forget_predefined_vllm_args=forget_predefined_vllm_args,
|
|
36
|
+
auto_stop=False,
|
|
53
37
|
)
|
|
54
38
|
|
|
55
|
-
base_url, container = server.start()
|
|
56
|
-
|
|
57
39
|
logger.info(f"✓ VLLM server ready at {base_url}")
|
|
58
|
-
|
|
59
|
-
|
|
40
|
+
if container is not None:
|
|
41
|
+
logger.info(f"✓ Container ID: {container.id}")
|
|
42
|
+
logger.info(f"✓ Container name: {container.name}")
|
|
60
43
|
|
|
61
44
|
def convert(
|
|
62
45
|
self,
|
|
@@ -69,7 +52,6 @@ class DParseCLI:
|
|
|
69
52
|
with_vllm_server: bool = False,
|
|
70
53
|
concurrency: int = 10,
|
|
71
54
|
dpi: int | None = None,
|
|
72
|
-
vllm_kwargs: dict | None = None,
|
|
73
55
|
debug: bool = False,
|
|
74
56
|
):
|
|
75
57
|
"""Parse PDF documents and save results.
|
|
@@ -84,7 +66,6 @@ class DParseCLI:
|
|
|
84
66
|
mode: Output mode - "document" (save as JSON zip), "md" (save as markdown file), "md_page" (save as folder of markdown pages)
|
|
85
67
|
with_vllm_server: If True, a local VLLM server will be deployed if the model is not found in the registry. Note that if the model is in the registry and the uri is None, the server will be anyway deployed.
|
|
86
68
|
dpi: DPI to use for the conversion. If not specified, the default DPI will be used.
|
|
87
|
-
vllm_kwargs: Additional keyword arguments to pass to the VLLM server.
|
|
88
69
|
debug: If True, run in debug mode (single-threaded, no concurrency)
|
|
89
70
|
"""
|
|
90
71
|
from vlmparse.converter_with_server import ConverterWithServer
|
|
@@ -95,7 +76,6 @@ class DParseCLI:
|
|
|
95
76
|
gpus=gpus,
|
|
96
77
|
with_vllm_server=with_vllm_server,
|
|
97
78
|
concurrency=concurrency,
|
|
98
|
-
vllm_kwargs=vllm_kwargs,
|
|
99
79
|
) as converter_with_server:
|
|
100
80
|
return converter_with_server.parse(
|
|
101
81
|
inputs=inputs, out_folder=out_folder, mode=mode, dpi=dpi, debug=debug
|
|
@@ -34,6 +34,7 @@ class GraniteDoclingDockerServerConfig(VLLMDockerServerConfig):
|
|
|
34
34
|
class GraniteDoclingConverterConfig(OpenAIConverterConfig):
|
|
35
35
|
"""Granite Docling converter configuration."""
|
|
36
36
|
|
|
37
|
+
model_name: str = "ibm-granite/granite-docling-258M"
|
|
37
38
|
preprompt: str | None = None
|
|
38
39
|
postprompt: str | None = "Convert this page to docling."
|
|
39
40
|
completion_kwargs: dict | None = {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
|
-
from typing import Literal
|
|
2
|
+
from typing import Literal, Optional
|
|
3
3
|
|
|
4
4
|
from loguru import logger
|
|
5
5
|
from pydantic import Field
|
|
@@ -101,7 +101,7 @@ class OpenAIConverterClient(BaseConverter):
|
|
|
101
101
|
|
|
102
102
|
async def _get_chat_completion(
|
|
103
103
|
self, messages: list[dict], completion_kwargs: dict | None = None
|
|
104
|
-
) -> tuple[str, "CompletionUsage"]: # noqa: F821
|
|
104
|
+
) -> tuple[str, Optional["CompletionUsage"]]: # noqa: F821
|
|
105
105
|
"""Helper to handle chat completion with optional streaming."""
|
|
106
106
|
if completion_kwargs is None:
|
|
107
107
|
completion_kwargs = self.config.completion_kwargs
|
|
@@ -117,7 +117,8 @@ class OpenAIConverterClient(BaseConverter):
|
|
|
117
117
|
async for chunk in response_stream:
|
|
118
118
|
if chunk.choices and chunk.choices[0].delta.content:
|
|
119
119
|
response_parts.append(chunk.choices[0].delta.content)
|
|
120
|
-
|
|
120
|
+
|
|
121
|
+
return "".join(response_parts), None
|
|
121
122
|
else:
|
|
122
123
|
response_obj = await self.model.chat.completions.create(
|
|
123
124
|
model=self.config.llm_params.model_name,
|
|
@@ -175,9 +176,10 @@ class OpenAIConverterClient(BaseConverter):
|
|
|
175
176
|
|
|
176
177
|
text = html_to_md_keep_tables(text)
|
|
177
178
|
page.text = text
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
179
|
+
if usage is not None:
|
|
180
|
+
page.prompt_tokens = usage.prompt_tokens
|
|
181
|
+
page.completion_tokens = usage.completion_tokens
|
|
182
|
+
if hasattr(usage, "reasoning_tokens"):
|
|
183
|
+
page.reasoning_tokens = usage.reasoning_tokens
|
|
182
184
|
|
|
183
185
|
return page
|
|
@@ -5,10 +5,61 @@ from typing import Literal
|
|
|
5
5
|
|
|
6
6
|
from loguru import logger
|
|
7
7
|
|
|
8
|
+
from vlmparse.constants import DEFAULT_SERVER_PORT
|
|
8
9
|
from vlmparse.servers.utils import get_model_from_uri
|
|
9
10
|
from vlmparse.utils import get_file_paths
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
def start_server(
|
|
14
|
+
model: str,
|
|
15
|
+
gpus: str,
|
|
16
|
+
port: None | int = None,
|
|
17
|
+
with_vllm_server: bool = True,
|
|
18
|
+
vllm_args: list[str] = {},
|
|
19
|
+
forget_predefined_vllm_args: bool = False,
|
|
20
|
+
auto_stop: bool = False,
|
|
21
|
+
):
|
|
22
|
+
from vlmparse.registries import docker_config_registry
|
|
23
|
+
|
|
24
|
+
base_url = ""
|
|
25
|
+
container = None
|
|
26
|
+
docker_config = docker_config_registry.get(model, default=with_vllm_server)
|
|
27
|
+
|
|
28
|
+
if port is None:
|
|
29
|
+
port = DEFAULT_SERVER_PORT
|
|
30
|
+
|
|
31
|
+
if docker_config is None:
|
|
32
|
+
logger.warning(
|
|
33
|
+
f"No Docker configuration found for model: {model}, using default configuration"
|
|
34
|
+
)
|
|
35
|
+
return "", container, None, docker_config
|
|
36
|
+
|
|
37
|
+
gpu_device_ids = None
|
|
38
|
+
if gpus is not None:
|
|
39
|
+
gpu_device_ids = [g.strip() for g in str(gpus).split(",")]
|
|
40
|
+
|
|
41
|
+
if docker_config is not None:
|
|
42
|
+
if port is not None:
|
|
43
|
+
docker_config.docker_port = port
|
|
44
|
+
docker_config.gpu_device_ids = gpu_device_ids
|
|
45
|
+
docker_config.update_command_args(
|
|
46
|
+
vllm_args,
|
|
47
|
+
forget_predefined_vllm_args=forget_predefined_vllm_args,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger.info(
|
|
51
|
+
f"Deploying VLLM server for {docker_config.model_name} on port {port}..."
|
|
52
|
+
)
|
|
53
|
+
server = docker_config.get_server(auto_stop=auto_stop)
|
|
54
|
+
if server is None:
|
|
55
|
+
logger.error(f"Model server not found for model: {model}")
|
|
56
|
+
return "", container, None, docker_config
|
|
57
|
+
|
|
58
|
+
base_url, container = server.start()
|
|
59
|
+
|
|
60
|
+
return base_url, container, server, docker_config
|
|
61
|
+
|
|
62
|
+
|
|
12
63
|
class ConverterWithServer:
|
|
13
64
|
def __init__(
|
|
14
65
|
self,
|
|
@@ -18,8 +69,8 @@ class ConverterWithServer:
|
|
|
18
69
|
port: int | None = None,
|
|
19
70
|
with_vllm_server: bool = False,
|
|
20
71
|
concurrency: int = 10,
|
|
21
|
-
|
|
22
|
-
|
|
72
|
+
vllm_args: dict | None = None,
|
|
73
|
+
forget_predefined_vllm_args: bool = False,
|
|
23
74
|
):
|
|
24
75
|
self.model = model
|
|
25
76
|
self.uri = uri
|
|
@@ -27,8 +78,8 @@ class ConverterWithServer:
|
|
|
27
78
|
self.gpus = gpus
|
|
28
79
|
self.with_vllm_server = with_vllm_server
|
|
29
80
|
self.concurrency = concurrency
|
|
30
|
-
self.
|
|
31
|
-
self.
|
|
81
|
+
self.vllm_args = vllm_args
|
|
82
|
+
self.forget_predefined_vllm_args = forget_predefined_vllm_args
|
|
32
83
|
self.server = None
|
|
33
84
|
self.client = None
|
|
34
85
|
|
|
@@ -36,32 +87,20 @@ class ConverterWithServer:
|
|
|
36
87
|
self.model = get_model_from_uri(self.uri)
|
|
37
88
|
|
|
38
89
|
def start_server_and_client(self):
|
|
39
|
-
from vlmparse.registries import
|
|
40
|
-
converter_config_registry,
|
|
41
|
-
docker_config_registry,
|
|
42
|
-
)
|
|
43
|
-
|
|
44
|
-
gpu_device_ids = None
|
|
45
|
-
if self.gpus is not None:
|
|
46
|
-
gpu_device_ids = [g.strip() for g in self.gpus.split(",")]
|
|
90
|
+
from vlmparse.registries import converter_config_registry
|
|
47
91
|
|
|
48
92
|
if self.uri is None:
|
|
49
|
-
docker_config =
|
|
50
|
-
self.model,
|
|
93
|
+
_, _, self.server, docker_config = start_server(
|
|
94
|
+
model=self.model,
|
|
95
|
+
gpus=self.gpus,
|
|
96
|
+
port=self.port,
|
|
97
|
+
with_vllm_server=self.with_vllm_server,
|
|
98
|
+
vllm_args=self.vllm_args,
|
|
99
|
+
forget_predefined_vllm_args=self.forget_predefined_vllm_args,
|
|
100
|
+
auto_stop=True,
|
|
51
101
|
)
|
|
52
102
|
|
|
53
103
|
if docker_config is not None:
|
|
54
|
-
if self.port is not None:
|
|
55
|
-
docker_config.docker_port = self.port
|
|
56
|
-
docker_config.gpu_device_ids = gpu_device_ids
|
|
57
|
-
docker_config.update_command_args(
|
|
58
|
-
self.vllm_kwargs,
|
|
59
|
-
forget_predefined_vllm_kwargs=self.forget_predefined_vllm_kwargs,
|
|
60
|
-
)
|
|
61
|
-
self.server = docker_config.get_server(auto_stop=True)
|
|
62
|
-
|
|
63
|
-
self.server.start()
|
|
64
|
-
|
|
65
104
|
self.client = docker_config.get_client()
|
|
66
105
|
else:
|
|
67
106
|
self.client = converter_config_registry.get(self.model).get_client()
|
|
@@ -49,15 +49,14 @@ class DockerServerConfig(BaseModel):
|
|
|
49
49
|
|
|
50
50
|
def update_command_args(
|
|
51
51
|
self,
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
vllm_args: dict | None = None,
|
|
53
|
+
forget_predefined_vllm_args: bool = False,
|
|
54
54
|
) -> list[str]:
|
|
55
|
-
if
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
self.command_args = new_kwargs
|
|
55
|
+
if vllm_args is not None:
|
|
56
|
+
if forget_predefined_vllm_args:
|
|
57
|
+
self.command_args = vllm_args
|
|
59
58
|
else:
|
|
60
|
-
self.command_args.extend(
|
|
59
|
+
self.command_args.extend(vllm_args)
|
|
61
60
|
|
|
62
61
|
return self.command_args
|
|
63
62
|
|
|
@@ -2,6 +2,7 @@ import getpass
|
|
|
2
2
|
import time
|
|
3
3
|
from contextlib import contextmanager
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from urllib.parse import parse_qsl, urlparse
|
|
5
6
|
|
|
6
7
|
import docker
|
|
7
8
|
from loguru import logger
|
|
@@ -222,25 +223,52 @@ def docker_server(
|
|
|
222
223
|
logger.info("Container stopped")
|
|
223
224
|
|
|
224
225
|
|
|
226
|
+
def normalize_uri(uri: str) -> tuple:
|
|
227
|
+
u = urlparse(uri)
|
|
228
|
+
|
|
229
|
+
# --- Normalize scheme ---
|
|
230
|
+
scheme = (u.scheme or "http").lower()
|
|
231
|
+
|
|
232
|
+
# --- Normalize host ---
|
|
233
|
+
host = (u.hostname or "").lower()
|
|
234
|
+
if host in ("localhost", "0.0.0.0"):
|
|
235
|
+
host = "localhost"
|
|
236
|
+
|
|
237
|
+
# --- Normalize port (apply defaults) ---
|
|
238
|
+
if u.port:
|
|
239
|
+
port = u.port
|
|
240
|
+
else:
|
|
241
|
+
port = 443 if scheme == "https" else 80
|
|
242
|
+
|
|
243
|
+
# --- Normalize path ---
|
|
244
|
+
# Treat empty path as "/" and remove trailing slash (except root)
|
|
245
|
+
path = u.path or "/"
|
|
246
|
+
if path != "/" and path.endswith("/"):
|
|
247
|
+
path = path.rstrip("/")
|
|
248
|
+
|
|
249
|
+
# Collapse duplicate slashes
|
|
250
|
+
while "//" in path:
|
|
251
|
+
path = path.replace("//", "/")
|
|
252
|
+
|
|
253
|
+
# --- Normalize query parameters (sorted) ---
|
|
254
|
+
query_pairs = parse_qsl(u.query, keep_blank_values=True)
|
|
255
|
+
query = "&".join(f"{k}={v}" for k, v in sorted(query_pairs))
|
|
256
|
+
|
|
257
|
+
return (scheme, host, port, path, query)
|
|
258
|
+
|
|
259
|
+
|
|
225
260
|
def get_model_from_uri(uri: str) -> str:
|
|
226
261
|
model = None
|
|
227
262
|
client = docker.from_env()
|
|
228
263
|
containers = client.containers.list()
|
|
264
|
+
|
|
265
|
+
uri = normalize_uri(uri)
|
|
266
|
+
|
|
229
267
|
for container in containers:
|
|
230
268
|
c_uri = container.labels.get("vlmparse_uri")
|
|
231
269
|
c_model = container.labels.get("vlmparse_model_name")
|
|
232
|
-
if c_uri is not None:
|
|
233
|
-
c_uri = c_uri.replace("localhost", "0.0.0.0")
|
|
234
|
-
|
|
235
|
-
# Check if user URI matches container URI (ignoring /v1 suffix if missing)
|
|
236
|
-
if c_uri and (
|
|
237
|
-
c_uri == uri or c_uri.startswith(uri.rstrip("/")) or uri.startswith(c_uri)
|
|
238
|
-
):
|
|
239
|
-
# Update URI to the correct one from container (likely has /v1)
|
|
240
|
-
if len(c_uri) > len(uri.rstrip("/")):
|
|
241
|
-
logger.info(f"Updating URI from {uri} to {c_uri}")
|
|
242
|
-
uri = c_uri
|
|
243
270
|
|
|
271
|
+
if c_uri and uri == normalize_uri(c_uri):
|
|
244
272
|
# Infer model if not provided
|
|
245
273
|
if model is None and c_model:
|
|
246
274
|
logger.info(f"Inferred model {c_model} from container")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|