nimbro-api 0.2.1__tar.gz → 0.2.2__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 (98) hide show
  1. {nimbro_api-0.2.1/src/nimbro_api.egg-info → nimbro_api-0.2.2}/PKG-INFO +4 -2
  2. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/README.md +2 -1
  3. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/pyproject.toml +2 -1
  4. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/base/vlm_gist_base.py +32 -10
  5. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/client/vlm_gist.py +6 -4
  6. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/test/vlm_gist_test.py +6 -2
  7. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/dam_base.py +6 -2
  8. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/florence2_base.py +5 -1
  9. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/kosmos2_base.py +6 -2
  10. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/mmgroundingdino_base.py +6 -2
  11. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/sam2_realtime_base.py +6 -2
  12. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/utility.py +26 -6
  13. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/__init__.py +2 -1
  14. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/chat_completions_base.py +14 -10
  15. nimbro_api-0.2.2/src/nimbro_api/api/openai/base/classify_base.py +224 -0
  16. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/embeddings_base.py +5 -1
  17. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/images_base.py +5 -1
  18. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/speech_base.py +5 -1
  19. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/transcriptions_base.py +5 -1
  20. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/translations_base.py +5 -1
  21. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/chat_completions.py +4 -3
  22. nimbro_api-0.2.2/src/nimbro_api/api/openai/client/classify.py +237 -0
  23. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/chat_completions_test.py +223 -169
  24. nimbro_api-0.2.2/src/nimbro_api/api/openai/test/classify_test.py +72 -0
  25. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/utility.py +5 -1
  26. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/api.py +13 -17
  27. {nimbro_api-0.2.1 → nimbro_api-0.2.2/src/nimbro_api.egg-info}/PKG-INFO +4 -2
  28. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api.egg-info/SOURCES.txt +3 -0
  29. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api.egg-info/requires.txt +1 -0
  30. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/LICENSE +0 -0
  31. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/setup.cfg +0 -0
  32. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/__init__.py +0 -0
  33. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/__init__.py +0 -0
  34. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/__init__.py +0 -0
  35. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/base/__init__.py +0 -0
  36. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/base/no_base.py +0 -0
  37. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/client/__init__.py +0 -0
  38. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/client/no.py +0 -0
  39. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/test/__init__.py +0 -0
  40. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/misc/test/no_test.py +0 -0
  41. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/__init__.py +0 -0
  42. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/base/__init__.py +0 -0
  43. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/__init__.py +0 -0
  44. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/dam.py +0 -0
  45. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/florence2.py +0 -0
  46. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/kosmos2.py +0 -0
  47. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/mmgroundingdino.py +0 -0
  48. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/client/sam2_realtime.py +0 -0
  49. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/__init__.py +0 -0
  50. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/dam_test.py +0 -0
  51. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/florence2_test.py +0 -0
  52. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/kosmos2_test.py +0 -0
  53. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/mmgroundingdino_test.py +0 -0
  54. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/nimbro_vision_servers/test/sam2_realtime_test.py +0 -0
  55. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/base/__init__.py +0 -0
  56. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/__init__.py +0 -0
  57. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/embeddings.py +0 -0
  58. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/images.py +0 -0
  59. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/speech.py +0 -0
  60. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/transcriptions.py +0 -0
  61. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/client/translations.py +0 -0
  62. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/__init__.py +0 -0
  63. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/json_decode.py +0 -0
  64. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/pointing_molmo.py +0 -0
  65. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/pointing_qwen.py +0 -0
  66. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/reasoning_extract.py +0 -0
  67. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/string_normalize.py +0 -0
  68. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/string_strip.py +0 -0
  69. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/parser/template.py +0 -0
  70. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/__init__.py +0 -0
  71. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/embeddings_test.py +0 -0
  72. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/images_test.py +0 -0
  73. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/speech_test.py +0 -0
  74. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/transcriptions_test.py +0 -0
  75. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/test/translations_test.py +0 -0
  76. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/api/openai/voice_presets.json +0 -0
  77. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/client/__init__.py +0 -0
  78. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/client/client.py +0 -0
  79. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/client/client_base.py +0 -0
  80. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/core/__init__.py +0 -0
  81. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/core/core.py +0 -0
  82. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/core/core_base.py +0 -0
  83. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/fonts/DejaVuSans.ttf +0 -0
  84. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/fonts/LICENSE +0 -0
  85. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/__init__.py +0 -0
  86. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/assets/test.mp4 +0 -0
  87. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/assets/test.pdf +0 -0
  88. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/assets/test.png +0 -0
  89. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/assets/test.wav +0 -0
  90. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/test/test.py +0 -0
  91. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/__init__.py +0 -0
  92. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/io.py +0 -0
  93. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/logger.py +0 -0
  94. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/misc.py +0 -0
  95. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/string.py +0 -0
  96. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api/utility/visual.py +0 -0
  97. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api.egg-info/dependency_links.txt +0 -0
  98. {nimbro_api-0.2.1 → nimbro_api-0.2.2}/src/nimbro_api.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nimbro-api
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: A robust and flexible framework for API clients in Python
5
5
  Author-email: Bastian Pätzold <paetzoldbastian@gmail.com>
6
6
  Maintainer-email: Bastian Pätzold <paetzoldbastian@gmail.com>
@@ -42,6 +42,7 @@ Requires-Dist: pybase64; extra == "full"
42
42
  Requires-Dist: numpy; extra == "full"
43
43
  Requires-Dist: pillow; extra == "full"
44
44
  Requires-Dist: opencv-python; extra == "full"
45
+ Requires-Dist: jsonschema; extra == "full"
45
46
  Dynamic: license-file
46
47
 
47
48
  # NimbRo API
@@ -65,6 +66,7 @@ Dynamic: license-file
65
66
  | Client | API Docs |
66
67
  |--------|----------|
67
68
  | [nimbro_api.openai.ChatCompletions](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/chat_completions.py) | [Chat Completions](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) |
69
+ | [nimbro_api.openai.Classify](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/classify.py) | [Classify](https://docs.vllm.ai/en/latest/models/pooling_models/classify) |
68
70
  | [nimbro_api.openai.Embeddings](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/embeddings.py) | [Embeddings](https://developers.openai.com/api/reference/resources/embeddings/methods/create) |
69
71
  | [nimbro_api.openai.Images](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/images.py) | [Images](https://developers.openai.com/api/reference/resources/images/methods/generate) |
70
72
  | [nimbro_api.openai.Speech](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/speech.py) | [Speech](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create) |
@@ -123,7 +125,7 @@ Create and use a [ChatCompletions](https://github.com/ptzld/nimbro-api/blob/main
123
125
  ```python
124
126
  import nimbro_api
125
127
  # nimbro_api.set_api_key(name="OPENROUTER_API_KEY", key="supersecretkey") # alternatively, set API key here
126
- client = nimbro_api.openai.ChatCompletions(endpoint="OpenRouter", model="google/gemini-3-flash-preview")
128
+ client = nimbro_api.openai.ChatCompletions(endpoint="OpenRouter", model="~google/gemini-flash-latest")
127
129
  success, message, response = client.prompt(text="Tell me a joke!")
128
130
  ```
129
131
 
@@ -19,6 +19,7 @@
19
19
  | Client | API Docs |
20
20
  |--------|----------|
21
21
  | [nimbro_api.openai.ChatCompletions](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/chat_completions.py) | [Chat Completions](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) |
22
+ | [nimbro_api.openai.Classify](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/classify.py) | [Classify](https://docs.vllm.ai/en/latest/models/pooling_models/classify) |
22
23
  | [nimbro_api.openai.Embeddings](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/embeddings.py) | [Embeddings](https://developers.openai.com/api/reference/resources/embeddings/methods/create) |
23
24
  | [nimbro_api.openai.Images](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/images.py) | [Images](https://developers.openai.com/api/reference/resources/images/methods/generate) |
24
25
  | [nimbro_api.openai.Speech](https://github.com/ptzld/nimbro-api/blob/main/src/nimbro_api/api/openai/client/speech.py) | [Speech](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create) |
@@ -77,7 +78,7 @@ Create and use a [ChatCompletions](https://github.com/ptzld/nimbro-api/blob/main
77
78
  ```python
78
79
  import nimbro_api
79
80
  # nimbro_api.set_api_key(name="OPENROUTER_API_KEY", key="supersecretkey") # alternatively, set API key here
80
- client = nimbro_api.openai.ChatCompletions(endpoint="OpenRouter", model="google/gemini-3-flash-preview")
81
+ client = nimbro_api.openai.ChatCompletions(endpoint="OpenRouter", model="~google/gemini-flash-latest")
81
82
  success, message, response = client.prompt(text="Tell me a joke!")
82
83
  ```
83
84
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nimbro-api"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "A robust and flexible framework for API clients in Python"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -58,6 +58,7 @@ full = [
58
58
  "numpy",
59
59
  "pillow",
60
60
  "opencv-python",
61
+ "jsonschema",
61
62
  ]
62
63
 
63
64
  [project.urls]
@@ -244,6 +244,8 @@ class VlmGistBase(ClientBase):
244
244
  assert_type_value(obj=output_dir, type_or_value=[None, str], name="argument 'output_dir'")
245
245
  if isinstance(output_dir, str):
246
246
  output_dir = os.path.realpath(output_dir)
247
+ assert_log(expression=os.path.isfile(output_dir), message="Expected argument 'output_dir' to not exist or point to an existing folder but it points to an existing file.")
248
+ os.makedirs(output_dir, exist_ok=True)
247
249
  assert_log(expression=IMPORT_ERROR is None, message=f"Visual utilities are not available due to missing dependencies: {IMPORT_ERROR}")
248
250
  if isinstance(result, str):
249
251
  success, message, result = read_json(file_path=result, name="result", logger=self._logger)
@@ -305,20 +307,34 @@ class VlmGistBase(ClientBase):
305
307
  if image is None:
306
308
  images = [None] * num_images
307
309
  else:
308
- if not isinstance(image, list):
310
+ if isinstance(image, list):
311
+ assert_log(expression=len(image) == num_images, message=f"Expected argument 'image' provided as list to contain '{num_images}' item{'' if num_images == 1 else 's'} but got '{len(image)}'.")
312
+ elif os.path.isdir(image):
313
+ self._logger.debug("Argument 'image' provide as local path.")
314
+ path = image
315
+ image = sorted([os.path.join(image, item) for item in os.listdir(path) if os.path.isfile(os.path.join(path, item))])
316
+ self._logger.debug(f"Files in '{path}': {image}")
317
+ assert_log(expression=len(image) == num_images, message=f"Expected argument 'image' provided as path to contain '{num_images}' item{'' if num_images == 1 else 's'} but got '{len(image)}'.")
318
+ else:
319
+ assert_log(expression=num_images == 1, message=f"Expected argument 'image' to be provided as list containing '{num_images}' item{'' if num_images == 1 else 's'} but got 'str'.")
309
320
  image = [image]
310
- assert_log(expression=len(image) == num_images, message=f"Expected argument 'image' provided as list to contain '{num_images}' item{'' if num_images == 1 else 's'} but got '{len(image)}'.")
311
321
  images = [None] * num_images
312
322
  for i, item in enumerate(image):
313
323
  if item is not None:
314
324
  success, message, images[i], _ = parse_image_b64(image=item, logger=self._logger)
315
325
  if not success:
316
326
  self._logger.error(f"Failed to visualize result '{i + 1}' of '{num_images}': {message}")
327
+
317
328
  # visualize
329
+
330
+ self._logger.info(f"Visualizing '{num_images}' result{'' if num_images == 1 else 's'}.")
331
+
318
332
  visualizations = [None] * num_images
319
333
  paths = [None] * num_images
320
334
 
321
335
  for i, batch_item in enumerate(batch):
336
+ # TODO parallelize using batch.size and batch.threading
337
+
322
338
  # validate data
323
339
  if images[i] is None and image is not None:
324
340
  continue
@@ -452,7 +468,7 @@ class VlmGistBase(ClientBase):
452
468
  if sum(item is None for item in points) == len(points):
453
469
  points = None
454
470
 
455
- # visualize
471
+ # draw
456
472
  self._logger.debug(f"Visualizing result '{i + 1}' of '{num_images}'.")
457
473
  tic = time.perf_counter()
458
474
  if vis_args is None:
@@ -481,7 +497,6 @@ class VlmGistBase(ClientBase):
481
497
  success, visual = cv2.imencode('.png', visual)
482
498
  if success:
483
499
  visual = visual.tobytes()
484
- os.makedirs(output_dir, exist_ok=True)
485
500
  out_path = os.path.join(output_dir, f"{i}_{datetime.datetime.now().isoformat()[:23].replace('-', '_').replace(':', '_').replace('.', '_')}.png")
486
501
  with open(out_path, "wb") as file:
487
502
  file.write(visual)
@@ -495,7 +510,8 @@ class VlmGistBase(ClientBase):
495
510
 
496
511
  # consolidate
497
512
  num_success = sum(item is not None for item in visualizations)
498
- success = num_success == num_images
513
+ # success = num_success == num_images
514
+ success = True # to prevent any retry attempts
499
515
  if num_success == 0:
500
516
  visualizations = None
501
517
  paths = None
@@ -669,7 +685,10 @@ class VlmGistBase(ClientBase):
669
685
  f"{('thread' if settings['batch']['style'] == 'threading' else 'process')}"
670
686
  f"{'' if (settings['batch']['size'] if settings['batch']['size'] > 0 else len(image)) == 1 else ('s' if settings['batch']['style'] == 'threading' else 'es')}."
671
687
  )
672
- self._logger.info(message)
688
+ if settings['message_process']:
689
+ self._logger.info(message)
690
+ else:
691
+ self._logger.debug(message)
673
692
 
674
693
  # full settings here instead of individual results
675
694
  data['run']['type'] = "batch"
@@ -709,7 +728,7 @@ class VlmGistBase(ClientBase):
709
728
  # extract batch results
710
729
  successes = [res[0] for res in results]
711
730
  failures = len(image) - sum(successes)
712
- data['run']['success'] = failures == 0 # TODO keep 0 but make retry in batch mode only be applied to workers while using different retry for batch (zero or other setting)
731
+ data['run']['success'] = failures == 0
713
732
  duration = time.perf_counter() - stamp_global
714
733
  data['run']['message'] = (
715
734
  f"Processed batch with '{len(image)}' image{'' if len(image) == 1 else 's'} using "
@@ -945,8 +964,8 @@ class VlmGistBase(ClientBase):
945
964
  except json.JSONDecodeError:
946
965
  pass
947
966
  else:
948
- data['structured_description']['logs'].append("Extracted possible structured description from string.")
949
- self._logger.warn(data['structured_description']['logs'][-1])
967
+ data['structured_description']['logs'].append("Parsed text response as JSON.")
968
+ self._logger.debug(data['structured_description']['logs'][-1])
950
969
  break
951
970
  else:
952
971
  return False, "Failed to extract JSON from structured description received as string.", data
@@ -959,7 +978,10 @@ class VlmGistBase(ClientBase):
959
978
 
960
979
  # structured description must be list
961
980
  if not isinstance(description, list):
962
- return False, f"Expected structured description to be of type 'list' but got '{type(description).__name__}'.", data
981
+ try:
982
+ return False, f"Expected structured description to be of type 'list' but got '{type(description).__name__}': {description}", data
983
+ except Exception:
984
+ return False, f"Expected structured description to be of type 'list' but got '{type(description).__name__}'.", data
963
985
 
964
986
  # obtain image dimensions if required
965
987
  dimensions = None
@@ -15,7 +15,7 @@ default_settings = {
15
15
  'logger_severity': "warn",
16
16
  'endpoint': "OpenRouter",
17
17
  'model': "~google/gemini-pro-latest",
18
- 'reasoning_effort': "none"
18
+ 'reasoning_effort': "minimal"
19
19
  },
20
20
  'system_prompt_role': "system",
21
21
  'system_prompt': "You are a visual perception system that identifies and analyzes objects and other visible features in an image.",
@@ -35,7 +35,7 @@ default_settings = {
35
35
  'logger_severity': "warn",
36
36
  'endpoint': "OpenRouter",
37
37
  'model': "~google/gemini-pro-latest",
38
- 'reasoning_effort': "none"
38
+ 'reasoning_effort': "minimal"
39
39
  },
40
40
  'use_scene_description': False,
41
41
  'system_prompt_role': "system",
@@ -194,11 +194,13 @@ class VlmGist(Client):
194
194
  - logger_severity (str | None): Logger severity applied to each worker in ["debug", "info", "warn", "error", "fatal", "off"] (str) or `None` to adopt global process-wide severity.
195
195
  - size (int): Number of parallel workers when processing a list of images (>= 0). Use `0` to spawn one worker per image.
196
196
  - style (str): Parallelization backend used for batch processing. One of ["threading", "multiprocessing"].
197
- - retry (bool | int): Retry behavior for a single worker. The global 'retry' setting is applied to the entire batch, triggering retry when at least one worker failed (after using up all their retry attempts).
197
+ - retry (bool | int): Retry behavior for a single worker when passing multiple images.
198
+ Then, the global 'retry' setting is applied to the entire batch, triggering retry when at least one worker failed (after using up all their retry attempts).
198
199
  retry (bool | int):
199
200
  Defines retry behavior in failure cases, if the cause is eligible for retry:
200
201
  - If `True`, retries indefinitely. If `False`, failure is returned immediately.
201
202
  - Use a positive integer (`int`) to permit a specific number of retry attempts.
203
+ - Scope depends on mode (see 'batch.retry').
202
204
 
203
205
  Raises:
204
206
  UnrecoverableError: If 'name' is provided and does not refer to an existing setting.
@@ -239,7 +241,7 @@ class VlmGist(Client):
239
241
  result (str | dict):
240
242
  A result returned by `run()` (for either a single image or a batch), either as the dictionary (`dict`) itself or as a path (`str`) to a JSON file containing it.
241
243
  image (str | bytes | list[str] | list[bytes] | None):
242
- The image(s) corresponding to the result as a local path, URL, Base64 encoding (all `str`), or raw `bytes`.
244
+ The image(s) corresponding to the result as a local path to a file or a folder containing files, URL, Base64 encoding (all `str`), or raw `bytes`.
243
245
  For a batch result, a `list` of length matching the batch size is required. If `None`, the image must be provided
244
246
  within 'result', which requires the setting 'include_image' to have been enabled during the originating call to `run()`. Defaults to `None`.
245
247
  output_dir (str | None):
@@ -578,7 +578,9 @@ def test_13_parallel_threads(n=10):
578
578
  client = VlmGist(settings={
579
579
  'batch.size': n,
580
580
  'batch.style': "threading",
581
- 'include_image': True
581
+ 'batch.retry': 2,
582
+ 'include_image': True,
583
+ 'retry': False
582
584
  })
583
585
  images = [os.path.join(nimbro_api.__path__[0], "test", "assets", "test.png")] * n
584
586
  success, message, result = client.run(image=images)
@@ -592,7 +594,9 @@ def test_14_parallel_multiprocessing(n=10):
592
594
  client = VlmGist(settings={
593
595
  'batch.size': n,
594
596
  'batch.style': "multiprocessing",
595
- 'include_image': False
597
+ 'batch.retry': 2,
598
+ 'include_image': False,
599
+ 'retry': False
596
600
  })
597
601
  images = [os.path.join(nimbro_api.__path__[0], "test", "assets", "test.png")] * n
598
602
  success, message, result = client.run(image=images)
@@ -4,7 +4,7 @@ import time
4
4
  from nimbro_api.client import ClientBase
5
5
  from nimbro_api.utility.io import parse_image_b64
6
6
  from nimbro_api.utility.api import get_api_key, validate_endpoint, post_request
7
- from nimbro_api.utility.misc import assert_type_value, assert_keys, assert_log, format_obj
7
+ from nimbro_api.utility.misc import UnrecoverableError, assert_type_value, assert_keys, assert_log, format_obj
8
8
  from nimbro_api.utility.string import is_base64
9
9
  from ..utility import get_status, get_health, get_flavors, load, unload
10
10
 
@@ -183,7 +183,9 @@ class DamBase(ClientBase):
183
183
  return False, message, None
184
184
 
185
185
  # retrieve API key
186
- api_key = self.get_api_key()[2]
186
+ success, message, api_key = self.get_api_key()
187
+ if not success:
188
+ raise UnrecoverableError(message)
187
189
 
188
190
  # construct payload
189
191
  headers = {
@@ -192,6 +194,8 @@ class DamBase(ClientBase):
192
194
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
193
195
  'X-Title': "NimbRo API"
194
196
  }
197
+ if api_key == "":
198
+ del headers['Authorization']
195
199
  data = {
196
200
  'images': [image_file],
197
201
  'prompts': [prompts],
@@ -157,7 +157,9 @@ class Florence2Base(ClientBase):
157
157
  return False, message, None
158
158
 
159
159
  # retrieve API key
160
- api_key = self.get_api_key()[2]
160
+ success, message, api_key = self.get_api_key()
161
+ if not success:
162
+ raise UnrecoverableError(message)
161
163
 
162
164
  # construct payload
163
165
  headers = {
@@ -166,6 +168,8 @@ class Florence2Base(ClientBase):
166
168
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
167
169
  'X-Title': "NimbRo API"
168
170
  }
171
+ if api_key == "":
172
+ del headers['Authorization']
169
173
  data = {
170
174
  'images': [image_file],
171
175
  'prompts': [self._settings['prompt']],
@@ -3,7 +3,7 @@ import time
3
3
  from nimbro_api.client import ClientBase
4
4
  from nimbro_api.utility.io import parse_image_b64
5
5
  from nimbro_api.utility.api import get_api_key, validate_endpoint, post_request
6
- from nimbro_api.utility.misc import assert_type_value, assert_log, format_obj
6
+ from nimbro_api.utility.misc import UnrecoverableError, assert_type_value, assert_log, format_obj
7
7
  from ..utility import get_status, get_health, get_flavors, load, unload
8
8
 
9
9
  class Kosmos2Base(ClientBase):
@@ -152,7 +152,9 @@ class Kosmos2Base(ClientBase):
152
152
  return False, message, None
153
153
 
154
154
  # retrieve API key
155
- api_key = self.get_api_key()[2]
155
+ success, message, api_key = self.get_api_key()
156
+ if not success:
157
+ raise UnrecoverableError(message)
156
158
 
157
159
  # construct payload
158
160
  headers = {
@@ -161,6 +163,8 @@ class Kosmos2Base(ClientBase):
161
163
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
162
164
  'X-Title': "NimbRo API"
163
165
  }
166
+ if api_key == "":
167
+ del headers['Authorization']
164
168
  data = {
165
169
  'images': [image_file],
166
170
  'prompts': [self._settings['prompt']],
@@ -3,7 +3,7 @@ import time
3
3
  from nimbro_api.client import ClientBase
4
4
  from nimbro_api.utility.io import parse_image_b64
5
5
  from nimbro_api.utility.api import get_api_key, validate_endpoint, post_request
6
- from nimbro_api.utility.misc import assert_type_value, assert_log
6
+ from nimbro_api.utility.misc import UnrecoverableError, assert_type_value, assert_log
7
7
  from ..utility import get_status, get_health, get_flavors, load, unload
8
8
 
9
9
  class MmGroundingDinoBase(ClientBase):
@@ -148,7 +148,9 @@ class MmGroundingDinoBase(ClientBase):
148
148
  return False, message, None
149
149
 
150
150
  # retrieve API key
151
- api_key = self.get_api_key()[2]
151
+ success, message, api_key = self.get_api_key()
152
+ if not success:
153
+ raise UnrecoverableError(message)
152
154
 
153
155
  # construct payload
154
156
  headers = {
@@ -157,6 +159,8 @@ class MmGroundingDinoBase(ClientBase):
157
159
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
158
160
  'X-Title': "NimbRo API"
159
161
  }
162
+ if api_key == "":
163
+ del headers['Authorization']
160
164
  data = {
161
165
  'images': [image_file],
162
166
  'inference_parameters': [{
@@ -4,7 +4,7 @@ import copy
4
4
  from nimbro_api.client import ClientBase
5
5
  from nimbro_api.utility.io import parse_image_b64
6
6
  from nimbro_api.utility.api import get_api_key, validate_endpoint, post_request
7
- from nimbro_api.utility.misc import assert_type_value, assert_log
7
+ from nimbro_api.utility.misc import UnrecoverableError, assert_type_value, assert_log
8
8
  from ..utility import get_status, get_health, get_flavors, load, unload
9
9
 
10
10
  class Sam2RealtimeBase(ClientBase):
@@ -187,7 +187,9 @@ class Sam2RealtimeBase(ClientBase):
187
187
  self._logger.debug(message)
188
188
 
189
189
  # retrieve API key
190
- api_key = self.get_api_key()[2]
190
+ success, message, api_key = self.get_api_key()
191
+ if not success:
192
+ raise UnrecoverableError(message)
191
193
 
192
194
  # construct payload
193
195
  headers = {
@@ -196,6 +198,8 @@ class Sam2RealtimeBase(ClientBase):
196
198
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
197
199
  'X-Title': "NimbRo API"
198
200
  }
201
+ if api_key == "":
202
+ del headers['Authorization']
199
203
  if prompts is None:
200
204
  data = {
201
205
  'images': [image_file],
@@ -2,7 +2,7 @@ import datetime
2
2
 
3
3
  import nimbro_api
4
4
  from nimbro_api.utility.api import get_request, post_request
5
- from nimbro_api.utility.misc import assert_type_value, assert_log
5
+ from nimbro_api.utility.misc import UnrecoverableError, assert_type_value, assert_log
6
6
 
7
7
  def get_status(self, age):
8
8
  # parse arguments
@@ -41,7 +41,9 @@ def get_status(self, age):
41
41
  self._logger.debug(message)
42
42
 
43
43
  # retrieve API key
44
- api_key = self.get_api_key()[2]
44
+ success, message, api_key = self.get_api_key()
45
+ if not success:
46
+ raise UnrecoverableError(message)
45
47
 
46
48
  # use status API
47
49
  headers = {
@@ -50,6 +52,8 @@ def get_status(self, age):
50
52
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
51
53
  'X-Title': "NimbRo API"
52
54
  }
55
+ if api_key == "":
56
+ del headers['Authorization']
53
57
  success, message, response = get_request(
54
58
  api_name="NimbRo-Vision-Servers API",
55
59
  api_url=f"{self._endpoint['api_url']}/status",
@@ -132,7 +136,9 @@ def get_health(self, age):
132
136
  self._logger.debug(message)
133
137
 
134
138
  # retrieve API key
135
- api_key = self.get_api_key()[2]
139
+ success, message, api_key = self.get_api_key()
140
+ if not success:
141
+ raise UnrecoverableError(message)
136
142
 
137
143
  # use health API
138
144
  headers = {
@@ -141,6 +147,8 @@ def get_health(self, age):
141
147
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
142
148
  'X-Title': "NimbRo API"
143
149
  }
150
+ if api_key == "":
151
+ del headers['Authorization']
144
152
  success, message, response = get_request(
145
153
  api_name="NimbRo-Vision-Servers API",
146
154
  api_url=f"{self._endpoint['api_url']}/health",
@@ -210,7 +218,9 @@ def get_flavors(self, age):
210
218
  self._logger.debug(message)
211
219
 
212
220
  # retrieve API key
213
- api_key = self.get_api_key()[2]
221
+ success, message, api_key = self.get_api_key()
222
+ if not success:
223
+ raise UnrecoverableError(message)
214
224
 
215
225
  # use model_flavors API
216
226
  headers = {
@@ -219,6 +229,8 @@ def get_flavors(self, age):
219
229
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
220
230
  'X-Title': "NimbRo API"
221
231
  }
232
+ if api_key == "":
233
+ del headers['Authorization']
222
234
  success, message, response = get_request(
223
235
  api_name="NimbRo-Vision-Servers API",
224
236
  api_url=f"{self._endpoint['api_url']}/model_flavors",
@@ -295,7 +307,9 @@ def load(self, flavor=None, age=0):
295
307
  self._logger.debug(message)
296
308
 
297
309
  # retrieve API key
298
- api_key = self.get_api_key()[2]
310
+ success, message, api_key = self.get_api_key()
311
+ if not success:
312
+ raise UnrecoverableError(message)
299
313
 
300
314
  # use load API
301
315
  headers = {
@@ -304,6 +318,8 @@ def load(self, flavor=None, age=0):
304
318
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
305
319
  'X-Title': "NimbRo API"
306
320
  }
321
+ if api_key == "":
322
+ del headers['Authorization']
307
323
  success, message, _ = post_request(
308
324
  api_name="NimbRo-Vision-Servers API",
309
325
  api_url=f"{self._endpoint['api_url']}/load",
@@ -339,7 +355,9 @@ def unload(self):
339
355
  self._logger.debug(message)
340
356
 
341
357
  # retrieve API key
342
- api_key = self.get_api_key()[2]
358
+ success, message, api_key = self.get_api_key()
359
+ if not success:
360
+ raise UnrecoverableError(message)
343
361
 
344
362
  # use unload API
345
363
  headers = {
@@ -348,6 +366,8 @@ def unload(self):
348
366
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
349
367
  'X-Title': "NimbRo API"
350
368
  }
369
+ if api_key == "":
370
+ del headers['Authorization']
351
371
  success, message, response = post_request(
352
372
  api_name="NimbRo-Vision-Servers API",
353
373
  api_url=f"{self._endpoint['api_url']}/unload",
@@ -1,8 +1,9 @@
1
1
  from .client.chat_completions import ChatCompletions
2
+ from .client.classify import Classify
2
3
  from .client.embeddings import Embeddings
3
4
  from .client.images import Images
4
5
  from .client.speech import Speech
5
6
  from .client.transcriptions import Transcriptions
6
7
  from .client.translations import Translations
7
8
 
8
- __all__ = ["ChatCompletions", "Embeddings", "Images", "Speech", "Transcriptions", "Translations"]
9
+ __all__ = ["ChatCompletions", "Classify", "Embeddings", "Images", "Speech", "Transcriptions", "Translations"]
@@ -131,7 +131,7 @@ class ChatCompletionsBase(ClientBase):
131
131
 
132
132
  # reasoning_effort
133
133
  assert_type_value(obj=settings['reasoning_effort'], type_or_value=str, name="setting 'reasoning_effort'")
134
- valid_values = ["", "none", "minimal", "low", "medium", "high"]
134
+ valid_values = ["", "none", "minimal", "low", "medium", "high", "xhigh", "max"]
135
135
  assert_log(
136
136
  expression=settings['reasoning_effort'] in valid_values,
137
137
  message=f"Expected setting 'reasoning_effort' to be in {valid_values} but got '{settings['reasoning_effort']}'."
@@ -717,12 +717,11 @@ class ChatCompletionsBase(ClientBase):
717
717
 
718
718
  def generate_completion(self, response_type):
719
719
  # retrieve API key
720
- try:
721
- api_key = self.get_api_key()[2]
722
- except UnrecoverableError as e:
720
+ success, message, api_key = self.get_api_key()
721
+ if not success:
723
722
  self._logger.debug("Restoring original context.")
724
723
  self.messages = self.context_dump
725
- raise e
724
+ raise UnrecoverableError(message)
726
725
 
727
726
  # validate connection
728
727
  success, message = self.validate_connection(api_key=api_key)
@@ -1078,6 +1077,8 @@ class ChatCompletionsBase(ClientBase):
1078
1077
  'HTTP-Referer': "https://github.com/ptzld/nimbro-api",
1079
1078
  'X-Title': "NimbRo API"
1080
1079
  }
1080
+ if api_key == "":
1081
+ del headers['Authorization']
1081
1082
 
1082
1083
  if self._endpoint['api_flavor'] == "openai":
1083
1084
  data = {
@@ -1094,7 +1095,7 @@ class ChatCompletionsBase(ClientBase):
1094
1095
  'n': 1,
1095
1096
  'stream': self._settings['stream']
1096
1097
  }
1097
- if self._settings['reasoning_effort'] not in ["", "none"]:
1098
+ if self._settings['reasoning_effort'] != "":
1098
1099
  data['reasoning_effort'] = self._settings['reasoning_effort']
1099
1100
  if len(self.tools) == 0:
1100
1101
  del data['tools']
@@ -1118,6 +1119,10 @@ class ChatCompletionsBase(ClientBase):
1118
1119
  'n': 1,
1119
1120
  'stream': self._settings['stream']
1120
1121
  }
1122
+ if self._settings['reasoning_effort'] != "":
1123
+ data['reasoning_effort'] = self._settings['reasoning_effort']
1124
+ if len(self.tools) > 0:
1125
+ data['parallel_tool_calls'] = self._settings['max_tool_calls'] is None or self._settings['max_tool_calls'] > 1
1121
1126
 
1122
1127
  elif self._endpoint['api_flavor'] == "openrouter":
1123
1128
  data = {
@@ -1133,7 +1138,7 @@ class ChatCompletionsBase(ClientBase):
1133
1138
  'n': 1,
1134
1139
  'stream': self._settings['stream']
1135
1140
  }
1136
- if self._settings['reasoning_effort'] not in ["", "none"]:
1141
+ if self._settings['reasoning_effort'] != "":
1137
1142
  data['reasoning'] = {
1138
1143
  'effort': self._settings['reasoning_effort'],
1139
1144
  'exclude': False
@@ -1665,7 +1670,7 @@ class ChatCompletionsBase(ClientBase):
1665
1670
 
1666
1671
  # error case: tool-choice "use specific function" was violated
1667
1672
  if len(self.tools) > 0 and response_type != "text" and response_type != "auto" and response_type != "always" and response_type != "json":
1668
- if text != "":
1673
+ if text != "": # is text actually forbidden if the response still contains the demanded tool call?
1669
1674
  is_valid = False
1670
1675
  logs.append(f"Completion contains text content despite tool-choice being set to '{response_type}'.")
1671
1676
  correction_messages[-1]['content'] = f"Your response must only contain a tool-call of '{response_type}' without additional text."
@@ -1764,8 +1769,7 @@ class ChatCompletionsBase(ClientBase):
1764
1769
  logs.append(f"Completion contains a tool-call that violates the JSON Schema: {errors[0].message}")
1765
1770
  reason = f"Your response contains a tool-call that violates the JSON Schema: {errors[0].message}"
1766
1771
  else:
1767
- logs.append("Tool-call cannot be validated against tool definitions because the 'jsonschema' module is not available.")
1768
- self._logger.warn(logs[-1], once=True)
1772
+ self._logger.warn("Tool-call cannot be validated against tool definitions because the 'jsonschema' module is not available.", once=True)
1769
1773
 
1770
1774
  if success:
1771
1775
  self._logger.debug("Tool-call is valid.")