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.
Files changed (45) hide show
  1. {vlmparse-0.1.6 → vlmparse-0.1.7}/PKG-INFO +1 -1
  2. {vlmparse-0.1.6 → vlmparse-0.1.7}/pyproject.toml +1 -1
  3. {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_batch_parser.py +14 -0
  4. {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_cli.py +8 -38
  5. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/cli.py +16 -36
  6. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/granite_docling.py +1 -0
  7. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/openai_converter.py +9 -7
  8. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/constants.py +3 -0
  9. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/converter_with_server.py +64 -25
  10. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/servers/docker_server.py +6 -7
  11. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/servers/utils.py +39 -11
  12. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/PKG-INFO +1 -1
  13. {vlmparse-0.1.6 → vlmparse-0.1.7}/LICENSE +0 -0
  14. {vlmparse-0.1.6 → vlmparse-0.1.7}/README.md +0 -0
  15. {vlmparse-0.1.6 → vlmparse-0.1.7}/setup.cfg +0 -0
  16. {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_all_converters_mocked.py +0 -0
  17. {vlmparse-0.1.6 → vlmparse-0.1.7}/tests/test_end2end.py +0 -0
  18. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/base_model.py +0 -0
  19. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/build_doc.py +0 -0
  20. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/chandra.py +0 -0
  21. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/deepseekocr.py +0 -0
  22. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/docling.py +0 -0
  23. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/dotsocr.py +0 -0
  24. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/hunyuanocr.py +0 -0
  25. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/lightonocr.py +0 -0
  26. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/mineru.py +0 -0
  27. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/nanonetocr.py +0 -0
  28. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/olmocr.py +0 -0
  29. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/paddleocrvl.py +0 -0
  30. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/cleaner.py +0 -0
  31. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/html_to_md_conversion.py +0 -0
  32. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/pipe_utils/utils.py +0 -0
  33. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/clients/prompts.py +0 -0
  34. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/converter.py +0 -0
  35. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/data_model/box.py +0 -0
  36. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/data_model/document.py +0 -0
  37. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/registries.py +0 -0
  38. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/st_viewer/fs_nav.py +0 -0
  39. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/st_viewer/st_viewer.py +0 -0
  40. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse/utils.py +0 -0
  41. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/SOURCES.txt +0 -0
  42. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/dependency_links.txt +0 -0
  43. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/entry_points.txt +0 -0
  44. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/requires.txt +0 -0
  45. {vlmparse-0.1.6 → vlmparse-0.1.7}/vlmparse.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vlmparse
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Requires-Python: >=3.11.0
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "vlmparse"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  authors = []
9
9
  description = ""
10
10
  readme = "README.md"
@@ -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", mock_container)
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", mock_container)
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
- mock_container,
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
- vllm_kwargs: dict | None = None,
15
- forget_predefined_vllm_kwargs: bool = False,
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
- vllm_kwargs: Additional keyword arguments to pass to the VLLM server.
24
- forget_predefined_vllm_kwargs: If True, the predefined VLLM kwargs from the docker config will be replaced by vllm_kwargs otherwise the predefined kwargs will be updated with vllm_kwargs with a risk of collision of argument names.
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.registries import docker_config_registry
27
+ from vlmparse.converter_with_server import start_server
30
28
 
31
- docker_config = docker_config_registry.get(model)
32
- if docker_config is None:
33
- logger.warning(
34
- f"No Docker configuration found for model: {model}, using default configuration"
35
- )
36
- return
37
-
38
- docker_config.docker_port = port
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
- logger.info(f"✓ Container ID: {container.id}")
59
- logger.info(f"✓ Container name: {container.name}")
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
- return "".join(response_parts)
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
- page.prompt_tokens = usage.prompt_tokens
179
- page.completion_tokens = usage.completion_tokens
180
- if hasattr(usage, "reasoning_tokens"):
181
- page.reasoning_tokens = usage.reasoning_tokens
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
@@ -1,2 +1,5 @@
1
+ import os
2
+
1
3
  IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".tiff", ".tif", ".bmp", ".gif", ".webp"]
2
4
  PDF_EXTENSION = ".pdf"
5
+ DEFAULT_SERVER_PORT = os.getenv("VLMPARSE_DEFAULT_PORT", 8056)
@@ -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
- vllm_kwargs: dict | None = None,
22
- forget_predefined_vllm_kwargs: bool = False,
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.vllm_kwargs = vllm_kwargs
31
- self.forget_predefined_vllm_kwargs = forget_predefined_vllm_kwargs
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 = docker_config_registry.get(
50
- self.model, default=self.with_vllm_server
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
- vllm_kwargs: dict | None = None,
53
- forget_predefined_vllm_kwargs: bool = False,
52
+ vllm_args: dict | None = None,
53
+ forget_predefined_vllm_args: bool = False,
54
54
  ) -> list[str]:
55
- if vllm_kwargs is not None:
56
- new_kwargs = [f"--{k}={v}" for k, v in vllm_kwargs.items()]
57
- if forget_predefined_vllm_kwargs:
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(new_kwargs)
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")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vlmparse
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Requires-Python: >=3.11.0
5
5
  Description-Content-Type: text/markdown
6
6
  License-File: LICENSE
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes