cmdop-llm 0.1.0__py3-none-any.whl

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.
cmdop_llm/__init__.py ADDED
@@ -0,0 +1,89 @@
1
+ """CMDOP LLM Python SDK.
2
+
3
+ A drop-in replacement for OpenAI Python SDK that connects to llm.cmdop.com.
4
+ Provides access to 200+ AI models via OpenRouter, plus custom vision/OCR endpoints.
5
+
6
+ Example:
7
+ ```python
8
+ from cmdop_llm import CmdopLLM
9
+
10
+ client = CmdopLLM(api_key="your-api-key")
11
+
12
+ # Chat completion (same as OpenAI)
13
+ response = client.chat.completions.create(
14
+ model="anthropic/claude-3.5-sonnet",
15
+ messages=[{"role": "user", "content": "Hello!"}]
16
+ )
17
+
18
+ # Vision analysis (CMDOP-specific)
19
+ result = client.vision.analyze(image_url="https://...")
20
+
21
+ # OCR (CMDOP-specific)
22
+ result = client.ocr.extract(image_url="https://...")
23
+ ```
24
+ """
25
+
26
+ from openai import (
27
+ APIConnectionError,
28
+ APIError,
29
+ APIStatusError,
30
+ APITimeoutError,
31
+ AsyncStream,
32
+ AuthenticationError,
33
+ BadRequestError,
34
+ ConflictError,
35
+ InternalServerError,
36
+ NotFoundError,
37
+ OpenAIError,
38
+ PermissionDeniedError,
39
+ RateLimitError,
40
+ Stream,
41
+ UnprocessableEntityError,
42
+ )
43
+ from openai._types import NOT_GIVEN, NotGiven
44
+
45
+ from ._client import AsyncClient, AsyncCmdopLLM, Client, CmdopLLM
46
+ from ._version import __author__, __email__, __title__, __version__
47
+ from .resources import AsyncOCR, AsyncVision, OCR, Vision
48
+ from .types import OCRRequest, OCRResponse, VisionAnalyzeRequest, VisionAnalyzeResponse
49
+
50
+ __all__ = [
51
+ # Version info
52
+ "__version__",
53
+ "__title__",
54
+ "__author__",
55
+ "__email__",
56
+ # Main clients
57
+ "CmdopLLM",
58
+ "AsyncCmdopLLM",
59
+ "Client",
60
+ "AsyncClient",
61
+ # Custom resources
62
+ "Vision",
63
+ "AsyncVision",
64
+ "OCR",
65
+ "AsyncOCR",
66
+ # Custom types
67
+ "VisionAnalyzeRequest",
68
+ "VisionAnalyzeResponse",
69
+ "OCRRequest",
70
+ "OCRResponse",
71
+ # Re-exported from OpenAI
72
+ "OpenAIError",
73
+ "APIError",
74
+ "APIStatusError",
75
+ "APIConnectionError",
76
+ "APITimeoutError",
77
+ "AuthenticationError",
78
+ "BadRequestError",
79
+ "ConflictError",
80
+ "NotFoundError",
81
+ "PermissionDeniedError",
82
+ "RateLimitError",
83
+ "UnprocessableEntityError",
84
+ "InternalServerError",
85
+ "Stream",
86
+ "AsyncStream",
87
+ "NOT_GIVEN",
88
+ "NotGiven",
89
+ ]
cmdop_llm/_client.py ADDED
@@ -0,0 +1,217 @@
1
+ """Main client classes for CMDOP LLM service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Mapping, Optional, Union
7
+
8
+ import httpx
9
+ from openai import AsyncOpenAI, OpenAI
10
+ from openai._compat import cached_property
11
+ from openai._types import NOT_GIVEN, NotGiven, Timeout
12
+
13
+ from ._constants import DEFAULT_BASE_URL, DEFAULT_MAX_RETRIES, ENV_API_KEY, ENV_BASE_URL
14
+ from .resources.ocr import AsyncOCR, OCR
15
+ from .resources.vision import AsyncVision, Vision
16
+
17
+ __all__ = ["CmdopLLM", "AsyncCmdopLLM"]
18
+
19
+
20
+ class CmdopLLM(OpenAI):
21
+ """Synchronous client for CMDOP LLM service.
22
+
23
+ A drop-in replacement for OpenAI client that connects to llm.cmdop.com.
24
+ Provides access to 200+ AI models via OpenRouter, plus custom vision/OCR endpoints.
25
+
26
+ Example:
27
+ ```python
28
+ from cmdop_llm import CmdopLLM
29
+
30
+ # Initialize with API key
31
+ client = CmdopLLM(api_key="your-cmdop-api-key")
32
+
33
+ # Or use CMDOP_API_KEY environment variable
34
+ client = CmdopLLM()
35
+
36
+ # Chat completion (same as OpenAI)
37
+ response = client.chat.completions.create(
38
+ model="anthropic/claude-3.5-sonnet",
39
+ messages=[{"role": "user", "content": "Hello!"}]
40
+ )
41
+ print(response.choices[0].message.content)
42
+
43
+ # Vision analysis (CMDOP-specific)
44
+ result = client.vision.analyze(
45
+ image_url="https://example.com/image.jpg"
46
+ )
47
+ print(result.description)
48
+
49
+ # OCR text extraction (CMDOP-specific)
50
+ result = client.ocr.extract(
51
+ image_url="https://example.com/document.png"
52
+ )
53
+ print(result.text)
54
+ ```
55
+
56
+ Args:
57
+ api_key: CMDOP API key. If not provided, reads from CMDOP_API_KEY env var.
58
+ base_url: Override the default base URL (https://llm.cmdop.com).
59
+ timeout: Request timeout in seconds or httpx.Timeout object.
60
+ max_retries: Maximum number of retries for failed requests.
61
+ default_headers: Headers to include in all requests.
62
+ default_query: Query parameters to include in all requests.
63
+ http_client: Custom httpx.Client instance.
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ *,
69
+ api_key: Optional[str] = None,
70
+ base_url: Union[str, httpx.URL, None] = None,
71
+ timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
72
+ max_retries: int = DEFAULT_MAX_RETRIES,
73
+ default_headers: Optional[Mapping[str, str]] = None,
74
+ default_query: Optional[Mapping[str, object]] = None,
75
+ http_client: Optional[httpx.Client] = None,
76
+ ) -> None:
77
+ # Get API key from environment if not provided
78
+ if api_key is None:
79
+ api_key = os.environ.get(ENV_API_KEY)
80
+ if api_key is None:
81
+ raise ValueError(
82
+ f"API key is required. Set {ENV_API_KEY} environment variable "
83
+ "or pass api_key parameter."
84
+ )
85
+
86
+ # Get base URL from environment or use default
87
+ if base_url is None:
88
+ base_url = os.environ.get(ENV_BASE_URL, DEFAULT_BASE_URL)
89
+
90
+ super().__init__(
91
+ api_key=api_key,
92
+ base_url=base_url,
93
+ timeout=timeout,
94
+ max_retries=max_retries,
95
+ default_headers=default_headers,
96
+ default_query=default_query,
97
+ http_client=http_client,
98
+ )
99
+
100
+ @cached_property
101
+ def vision(self) -> Vision:
102
+ """Access vision analysis endpoints.
103
+
104
+ Returns:
105
+ Vision resource for image analysis.
106
+ """
107
+ return Vision(self)
108
+
109
+ @cached_property
110
+ def ocr(self) -> OCR:
111
+ """Access OCR text extraction endpoints.
112
+
113
+ Returns:
114
+ OCR resource for text extraction from images.
115
+ """
116
+ return OCR(self)
117
+
118
+
119
+ class AsyncCmdopLLM(AsyncOpenAI):
120
+ """Asynchronous client for CMDOP LLM service.
121
+
122
+ Async version of CmdopLLM for use with asyncio.
123
+
124
+ Example:
125
+ ```python
126
+ import asyncio
127
+ from cmdop_llm import AsyncCmdopLLM
128
+
129
+ async def main():
130
+ client = AsyncCmdopLLM()
131
+
132
+ # Chat completion
133
+ response = await client.chat.completions.create(
134
+ model="openai/gpt-4o",
135
+ messages=[{"role": "user", "content": "Hello!"}]
136
+ )
137
+ print(response.choices[0].message.content)
138
+
139
+ # Streaming
140
+ stream = await client.chat.completions.create(
141
+ model="openai/gpt-4o",
142
+ messages=[{"role": "user", "content": "Write a poem"}],
143
+ stream=True
144
+ )
145
+ async for chunk in stream:
146
+ if chunk.choices[0].delta.content:
147
+ print(chunk.choices[0].delta.content, end="")
148
+
149
+ asyncio.run(main())
150
+ ```
151
+
152
+ Args:
153
+ api_key: CMDOP API key. If not provided, reads from CMDOP_API_KEY env var.
154
+ base_url: Override the default base URL (https://llm.cmdop.com).
155
+ timeout: Request timeout in seconds or httpx.Timeout object.
156
+ max_retries: Maximum number of retries for failed requests.
157
+ default_headers: Headers to include in all requests.
158
+ default_query: Query parameters to include in all requests.
159
+ http_client: Custom httpx.AsyncClient instance.
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ *,
165
+ api_key: Optional[str] = None,
166
+ base_url: Union[str, httpx.URL, None] = None,
167
+ timeout: Union[float, Timeout, None, NotGiven] = NOT_GIVEN,
168
+ max_retries: int = DEFAULT_MAX_RETRIES,
169
+ default_headers: Optional[Mapping[str, str]] = None,
170
+ default_query: Optional[Mapping[str, object]] = None,
171
+ http_client: Optional[httpx.AsyncClient] = None,
172
+ ) -> None:
173
+ # Get API key from environment if not provided
174
+ if api_key is None:
175
+ api_key = os.environ.get(ENV_API_KEY)
176
+ if api_key is None:
177
+ raise ValueError(
178
+ f"API key is required. Set {ENV_API_KEY} environment variable "
179
+ "or pass api_key parameter."
180
+ )
181
+
182
+ # Get base URL from environment or use default
183
+ if base_url is None:
184
+ base_url = os.environ.get(ENV_BASE_URL, DEFAULT_BASE_URL)
185
+
186
+ super().__init__(
187
+ api_key=api_key,
188
+ base_url=base_url,
189
+ timeout=timeout,
190
+ max_retries=max_retries,
191
+ default_headers=default_headers,
192
+ default_query=default_query,
193
+ http_client=http_client,
194
+ )
195
+
196
+ @cached_property
197
+ def vision(self) -> AsyncVision:
198
+ """Access vision analysis endpoints.
199
+
200
+ Returns:
201
+ AsyncVision resource for image analysis.
202
+ """
203
+ return AsyncVision(self)
204
+
205
+ @cached_property
206
+ def ocr(self) -> AsyncOCR:
207
+ """Access OCR text extraction endpoints.
208
+
209
+ Returns:
210
+ AsyncOCR resource for text extraction from images.
211
+ """
212
+ return AsyncOCR(self)
213
+
214
+
215
+ # Aliases for convenience
216
+ Client = CmdopLLM
217
+ AsyncClient = AsyncCmdopLLM
@@ -0,0 +1,12 @@
1
+ """Constants for cmdop-llm package."""
2
+
3
+ # Default base URL for CMDOP LLM service
4
+ DEFAULT_BASE_URL = "https://llm.cmdop.com"
5
+
6
+ # Environment variable names
7
+ ENV_API_KEY = "CMDOP_API_KEY"
8
+ ENV_BASE_URL = "CMDOP_BASE_URL"
9
+
10
+ # Default timeout settings (seconds)
11
+ DEFAULT_TIMEOUT = 60.0
12
+ DEFAULT_MAX_RETRIES = 2
cmdop_llm/_version.py ADDED
@@ -0,0 +1,6 @@
1
+ """Version information for cmdop-llm package."""
2
+
3
+ __version__ = "0.1.0"
4
+ __title__ = "cmdop-llm"
5
+ __author__ = "CMDOP Team"
6
+ __email__ = "support@cmdop.com"
cmdop_llm/py.typed ADDED
@@ -0,0 +1 @@
1
+ # PEP 561 marker file - this package supports type checking
@@ -0,0 +1,11 @@
1
+ """Custom resources for CMDOP LLM service."""
2
+
3
+ from .ocr import AsyncOCR, OCR
4
+ from .vision import AsyncVision, Vision
5
+
6
+ __all__ = [
7
+ "Vision",
8
+ "AsyncVision",
9
+ "OCR",
10
+ "AsyncOCR",
11
+ ]
@@ -0,0 +1,180 @@
1
+ """OCR extraction resource for CMDOP LLM service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal, Optional, Union
6
+
7
+ import httpx
8
+ from openai._compat import cached_property
9
+ from openai._resource import AsyncAPIResource, SyncAPIResource
10
+ from openai._types import NOT_GIVEN, Body, Headers, NotGiven, Query
11
+
12
+ from ..types.ocr import OCRResponse
13
+
14
+ if TYPE_CHECKING:
15
+ from .._client import AsyncCmdopLLM, CmdopLLM
16
+
17
+ __all__ = ["OCR", "AsyncOCR"]
18
+
19
+
20
+ class OCR(SyncAPIResource):
21
+ """OCR text extraction resource.
22
+
23
+ Provides text extraction from images using vision models.
24
+
25
+ Example:
26
+ ```python
27
+ from cmdop_llm import CmdopLLM
28
+
29
+ client = CmdopLLM()
30
+ result = client.ocr.extract(
31
+ image_url="https://example.com/document.png"
32
+ )
33
+ print(result.text)
34
+ ```
35
+ """
36
+
37
+ _client: CmdopLLM
38
+
39
+ @cached_property
40
+ def with_raw_response(self) -> OCRWithRawResponse:
41
+ """Access raw HTTP response data."""
42
+ return OCRWithRawResponse(self)
43
+
44
+ def extract(
45
+ self,
46
+ *,
47
+ image: Optional[str] = None,
48
+ image_url: Optional[str] = None,
49
+ model: Optional[str] = None,
50
+ mode: Literal["tiny", "small", "base", "gundam"] = "base",
51
+ fetch_image: bool = True,
52
+ extra_headers: Headers | None = None,
53
+ extra_query: Query | None = None,
54
+ extra_body: Body | None = None,
55
+ timeout: Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN,
56
+ ) -> OCRResponse:
57
+ """Extract text from an image using OCR.
58
+
59
+ Args:
60
+ image: Base64-encoded image data.
61
+ image_url: URL of the image to extract text from.
62
+ model: Model to use. Defaults to server default.
63
+ mode: OCR quality mode ("tiny", "small", "base", "gundam").
64
+ fetch_image: Whether server should fetch image from URL.
65
+ extra_headers: Additional headers to send.
66
+ extra_query: Additional query parameters.
67
+ extra_body: Additional body parameters.
68
+ timeout: Request timeout.
69
+
70
+ Returns:
71
+ OCRResponse with extracted text.
72
+
73
+ Raises:
74
+ ValueError: If neither image nor image_url is provided.
75
+ openai.APIError: If the request fails.
76
+ """
77
+ if not image and not image_url:
78
+ raise ValueError("Either 'image' or 'image_url' must be provided")
79
+
80
+ body = {
81
+ "image": image,
82
+ "image_url": image_url,
83
+ "model": model,
84
+ "mode": mode,
85
+ "fetch_image": fetch_image,
86
+ }
87
+ # Remove None values
88
+ body = {k: v for k, v in body.items() if v is not None}
89
+
90
+ return self._post(
91
+ "/vision/ocr",
92
+ body=body,
93
+ options={
94
+ "extra_headers": extra_headers,
95
+ "extra_query": extra_query,
96
+ "extra_body": extra_body,
97
+ "timeout": timeout,
98
+ },
99
+ cast_to=OCRResponse,
100
+ )
101
+
102
+
103
+ class AsyncOCR(AsyncAPIResource):
104
+ """Async OCR text extraction resource."""
105
+
106
+ _client: AsyncCmdopLLM
107
+
108
+ @cached_property
109
+ def with_raw_response(self) -> AsyncOCRWithRawResponse:
110
+ """Access raw HTTP response data."""
111
+ return AsyncOCRWithRawResponse(self)
112
+
113
+ async def extract(
114
+ self,
115
+ *,
116
+ image: Optional[str] = None,
117
+ image_url: Optional[str] = None,
118
+ model: Optional[str] = None,
119
+ mode: Literal["tiny", "small", "base", "gundam"] = "base",
120
+ fetch_image: bool = True,
121
+ extra_headers: Headers | None = None,
122
+ extra_query: Query | None = None,
123
+ extra_body: Body | None = None,
124
+ timeout: Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN,
125
+ ) -> OCRResponse:
126
+ """Extract text from an image using OCR (async).
127
+
128
+ See OCR.extract for full documentation.
129
+ """
130
+ if not image and not image_url:
131
+ raise ValueError("Either 'image' or 'image_url' must be provided")
132
+
133
+ body = {
134
+ "image": image,
135
+ "image_url": image_url,
136
+ "model": model,
137
+ "mode": mode,
138
+ "fetch_image": fetch_image,
139
+ }
140
+ body = {k: v for k, v in body.items() if v is not None}
141
+
142
+ return await self._post(
143
+ "/vision/ocr",
144
+ body=body,
145
+ options={
146
+ "extra_headers": extra_headers,
147
+ "extra_query": extra_query,
148
+ "extra_body": extra_body,
149
+ "timeout": timeout,
150
+ },
151
+ cast_to=OCRResponse,
152
+ )
153
+
154
+
155
+ class OCRWithRawResponse:
156
+ """Raw response wrapper for OCR resource."""
157
+
158
+ def __init__(self, ocr: OCR) -> None:
159
+ self._ocr = ocr
160
+
161
+ def extract(self, **kwargs) -> httpx.Response: # type: ignore
162
+ """Get raw response from extract."""
163
+ return self._ocr._client.with_raw_response._post( # type: ignore
164
+ "/vision/ocr",
165
+ body=kwargs,
166
+ )
167
+
168
+
169
+ class AsyncOCRWithRawResponse:
170
+ """Raw response wrapper for AsyncOCR resource."""
171
+
172
+ def __init__(self, ocr: AsyncOCR) -> None:
173
+ self._ocr = ocr
174
+
175
+ async def extract(self, **kwargs) -> httpx.Response: # type: ignore
176
+ """Get raw response from extract."""
177
+ return await self._ocr._client.with_raw_response._post( # type: ignore
178
+ "/vision/ocr",
179
+ body=kwargs,
180
+ )
@@ -0,0 +1,186 @@
1
+ """Vision analysis resource for CMDOP LLM service."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Literal, Optional, Union
6
+
7
+ import httpx
8
+ from openai._compat import cached_property
9
+ from openai._resource import AsyncAPIResource, SyncAPIResource
10
+ from openai._types import NOT_GIVEN, Body, Headers, NotGiven, Query
11
+
12
+ from ..types.vision import VisionAnalyzeResponse
13
+
14
+ if TYPE_CHECKING:
15
+ from .._client import AsyncCmdopLLM, CmdopLLM
16
+
17
+ __all__ = ["Vision", "AsyncVision"]
18
+
19
+
20
+ class Vision(SyncAPIResource):
21
+ """Vision analysis resource.
22
+
23
+ Provides image analysis capabilities using vision models.
24
+
25
+ Example:
26
+ ```python
27
+ from cmdop_llm import CmdopLLM
28
+
29
+ client = CmdopLLM()
30
+ result = client.vision.analyze(
31
+ image_url="https://example.com/image.jpg",
32
+ prompt="Describe this image"
33
+ )
34
+ print(result.description)
35
+ ```
36
+ """
37
+
38
+ _client: CmdopLLM
39
+
40
+ @cached_property
41
+ def with_raw_response(self) -> VisionWithRawResponse:
42
+ """Access raw HTTP response data."""
43
+ return VisionWithRawResponse(self)
44
+
45
+ def analyze(
46
+ self,
47
+ *,
48
+ image: Optional[str] = None,
49
+ image_url: Optional[str] = None,
50
+ prompt: Optional[str] = None,
51
+ model: Optional[str] = None,
52
+ ocr_mode: Literal["tiny", "small", "base", "gundam"] = "base",
53
+ fetch_image: bool = True,
54
+ extra_headers: Headers | None = None,
55
+ extra_query: Query | None = None,
56
+ extra_body: Body | None = None,
57
+ timeout: Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN,
58
+ ) -> VisionAnalyzeResponse:
59
+ """Analyze an image using a vision model.
60
+
61
+ Args:
62
+ image: Base64-encoded image data.
63
+ image_url: URL of the image to analyze.
64
+ prompt: Custom prompt for the analysis.
65
+ model: Model to use. Defaults to server default.
66
+ ocr_mode: OCR quality mode ("tiny", "small", "base", "gundam").
67
+ fetch_image: Whether server should fetch image from URL.
68
+ extra_headers: Additional headers to send.
69
+ extra_query: Additional query parameters.
70
+ extra_body: Additional body parameters.
71
+ timeout: Request timeout.
72
+
73
+ Returns:
74
+ VisionAnalyzeResponse with extracted text and description.
75
+
76
+ Raises:
77
+ ValueError: If neither image nor image_url is provided.
78
+ openai.APIError: If the request fails.
79
+ """
80
+ if not image and not image_url:
81
+ raise ValueError("Either 'image' or 'image_url' must be provided")
82
+
83
+ body = {
84
+ "image": image,
85
+ "image_url": image_url,
86
+ "prompt": prompt,
87
+ "model": model,
88
+ "ocr_mode": ocr_mode,
89
+ "fetch_image": fetch_image,
90
+ }
91
+ # Remove None values
92
+ body = {k: v for k, v in body.items() if v is not None}
93
+
94
+ return self._post(
95
+ "/vision/analyze",
96
+ body=body,
97
+ options={
98
+ "extra_headers": extra_headers,
99
+ "extra_query": extra_query,
100
+ "extra_body": extra_body,
101
+ "timeout": timeout,
102
+ },
103
+ cast_to=VisionAnalyzeResponse,
104
+ )
105
+
106
+
107
+ class AsyncVision(AsyncAPIResource):
108
+ """Async vision analysis resource."""
109
+
110
+ _client: AsyncCmdopLLM
111
+
112
+ @cached_property
113
+ def with_raw_response(self) -> AsyncVisionWithRawResponse:
114
+ """Access raw HTTP response data."""
115
+ return AsyncVisionWithRawResponse(self)
116
+
117
+ async def analyze(
118
+ self,
119
+ *,
120
+ image: Optional[str] = None,
121
+ image_url: Optional[str] = None,
122
+ prompt: Optional[str] = None,
123
+ model: Optional[str] = None,
124
+ ocr_mode: Literal["tiny", "small", "base", "gundam"] = "base",
125
+ fetch_image: bool = True,
126
+ extra_headers: Headers | None = None,
127
+ extra_query: Query | None = None,
128
+ extra_body: Body | None = None,
129
+ timeout: Union[float, httpx.Timeout, None, NotGiven] = NOT_GIVEN,
130
+ ) -> VisionAnalyzeResponse:
131
+ """Analyze an image using a vision model (async).
132
+
133
+ See Vision.analyze for full documentation.
134
+ """
135
+ if not image and not image_url:
136
+ raise ValueError("Either 'image' or 'image_url' must be provided")
137
+
138
+ body = {
139
+ "image": image,
140
+ "image_url": image_url,
141
+ "prompt": prompt,
142
+ "model": model,
143
+ "ocr_mode": ocr_mode,
144
+ "fetch_image": fetch_image,
145
+ }
146
+ body = {k: v for k, v in body.items() if v is not None}
147
+
148
+ return await self._post(
149
+ "/vision/analyze",
150
+ body=body,
151
+ options={
152
+ "extra_headers": extra_headers,
153
+ "extra_query": extra_query,
154
+ "extra_body": extra_body,
155
+ "timeout": timeout,
156
+ },
157
+ cast_to=VisionAnalyzeResponse,
158
+ )
159
+
160
+
161
+ class VisionWithRawResponse:
162
+ """Raw response wrapper for Vision resource."""
163
+
164
+ def __init__(self, vision: Vision) -> None:
165
+ self._vision = vision
166
+
167
+ def analyze(self, **kwargs) -> httpx.Response: # type: ignore
168
+ """Get raw response from analyze."""
169
+ return self._vision._client.with_raw_response._post( # type: ignore
170
+ "/vision/analyze",
171
+ body=kwargs,
172
+ )
173
+
174
+
175
+ class AsyncVisionWithRawResponse:
176
+ """Raw response wrapper for AsyncVision resource."""
177
+
178
+ def __init__(self, vision: AsyncVision) -> None:
179
+ self._vision = vision
180
+
181
+ async def analyze(self, **kwargs) -> httpx.Response: # type: ignore
182
+ """Get raw response from analyze."""
183
+ return await self._vision._client.with_raw_response._post( # type: ignore
184
+ "/vision/analyze",
185
+ body=kwargs,
186
+ )
@@ -0,0 +1,11 @@
1
+ """Type definitions for cmdop-llm custom endpoints."""
2
+
3
+ from .ocr import OCRRequest, OCRResponse
4
+ from .vision import VisionAnalyzeRequest, VisionAnalyzeResponse
5
+
6
+ __all__ = [
7
+ "VisionAnalyzeRequest",
8
+ "VisionAnalyzeResponse",
9
+ "OCRRequest",
10
+ "OCRResponse",
11
+ ]
cmdop_llm/types/ocr.py ADDED
@@ -0,0 +1,50 @@
1
+ """Type definitions for OCR endpoint."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import BaseModel, model_validator
6
+
7
+
8
+ class OCRRequest(BaseModel):
9
+ """Request for OCR text extraction."""
10
+
11
+ image: Optional[str] = None
12
+ """Base64-encoded image data."""
13
+
14
+ image_url: Optional[str] = None
15
+ """URL of the image to extract text from."""
16
+
17
+ model: Optional[str] = None
18
+ """Model to use for OCR. Defaults to server default."""
19
+
20
+ mode: Literal["tiny", "small", "base", "gundam"] = "base"
21
+ """OCR mode affecting quality and speed."""
22
+
23
+ fetch_image: bool = True
24
+ """Whether to fetch image from URL on server side."""
25
+
26
+ @model_validator(mode="after")
27
+ def validate_image_source(self) -> "OCRRequest":
28
+ """Ensure at least one image source is provided."""
29
+ if not self.image and not self.image_url:
30
+ raise ValueError("Either 'image' or 'image_url' must be provided")
31
+ return self
32
+
33
+
34
+ class OCRResponse(BaseModel):
35
+ """Response from OCR extraction."""
36
+
37
+ text: str
38
+ """Extracted text content."""
39
+
40
+ model: str
41
+ """Model used for extraction."""
42
+
43
+ cost_usd: float
44
+ """Cost of the request in USD."""
45
+
46
+ tokens_input: int
47
+ """Number of input tokens used."""
48
+
49
+ tokens_output: int
50
+ """Number of output tokens generated."""
@@ -0,0 +1,59 @@
1
+ """Type definitions for vision analysis endpoint."""
2
+
3
+ from typing import Literal, Optional
4
+
5
+ from pydantic import BaseModel, model_validator
6
+
7
+
8
+ class VisionAnalyzeRequest(BaseModel):
9
+ """Request for vision analysis."""
10
+
11
+ image: Optional[str] = None
12
+ """Base64-encoded image data."""
13
+
14
+ image_url: Optional[str] = None
15
+ """URL of the image to analyze."""
16
+
17
+ prompt: Optional[str] = None
18
+ """Custom prompt for the analysis."""
19
+
20
+ model: Optional[str] = None
21
+ """Model to use for analysis. Defaults to server default."""
22
+
23
+ ocr_mode: Literal["tiny", "small", "base", "gundam"] = "base"
24
+ """OCR mode for text extraction quality."""
25
+
26
+ fetch_image: bool = True
27
+ """Whether to fetch image from URL on server side."""
28
+
29
+ @model_validator(mode="after")
30
+ def validate_image_source(self) -> "VisionAnalyzeRequest":
31
+ """Ensure at least one image source is provided."""
32
+ if not self.image and not self.image_url:
33
+ raise ValueError("Either 'image' or 'image_url' must be provided")
34
+ return self
35
+
36
+
37
+ class VisionAnalyzeResponse(BaseModel):
38
+ """Response from vision analysis."""
39
+
40
+ extracted_text: str
41
+ """Text extracted from the image."""
42
+
43
+ description: str
44
+ """Description of the image content."""
45
+
46
+ language: Optional[str] = None
47
+ """Detected language of the text."""
48
+
49
+ model: str
50
+ """Model used for analysis."""
51
+
52
+ cost_usd: float
53
+ """Cost of the request in USD."""
54
+
55
+ tokens_input: int
56
+ """Number of input tokens used."""
57
+
58
+ tokens_output: int
59
+ """Number of output tokens generated."""
@@ -0,0 +1,198 @@
1
+ Metadata-Version: 2.4
2
+ Name: cmdop-llm
3
+ Version: 0.1.0
4
+ Summary: Python SDK for CMDOP LLM Service - OpenAI-compatible API for 200+ AI models
5
+ Project-URL: Homepage, https://cmdop.com
6
+ Project-URL: Documentation, https://sdk.cmdop.com
7
+ Project-URL: Repository, https://github.com/markolofsen/cmdop-client
8
+ Project-URL: Issues, https://github.com/markolofsen/cmdop-client/issues
9
+ Author-email: CMDOP Team <support@cmdop.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,anthropic,api,chat,claude,cmdop,gpt,llm,openai,openrouter
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.9
25
+ Requires-Dist: httpx<1,>=0.23.0
26
+ Requires-Dist: openai<3.0.0,>=1.0.0
27
+ Requires-Dist: pydantic<3,>=2.0.0
28
+ Requires-Dist: typing-extensions>=4.5.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: mypy>=1.0.0; extra == 'dev'
31
+ Requires-Dist: pyright>=1.1.300; extra == 'dev'
32
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
33
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
34
+ Requires-Dist: respx>=0.20.0; extra == 'dev'
35
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
36
+ Description-Content-Type: text/markdown
37
+
38
+ # CMDOP LLM Python SDK
39
+
40
+ Python SDK for CMDOP LLM Service - OpenAI-compatible API for 200+ AI models.
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ pip install cmdop-llm
46
+ ```
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ from cmdop_llm import CmdopLLM
52
+
53
+ client = CmdopLLM(api_key="your-api-key")
54
+
55
+ response = client.chat.completions.create(
56
+ model="anthropic/claude-3.5-sonnet",
57
+ messages=[{"role": "user", "content": "Hello!"}]
58
+ )
59
+ print(response.choices[0].message.content)
60
+ ```
61
+
62
+ ## Features
63
+
64
+ - **Drop-in OpenAI replacement** - Same API, different models
65
+ - **200+ Models** - GPT-4, Claude, Llama, Mistral, Gemini via single endpoint
66
+ - **Streaming** - Real-time token streaming
67
+ - **Vision & OCR** - Image analysis and text extraction
68
+ - **Image Generation** - FLUX, DALL-E and other models
69
+ - **Async Support** - Full async/await support
70
+
71
+ ## Environment Variables
72
+
73
+ ```bash
74
+ export CMDOP_API_KEY="your-api-key"
75
+ export CMDOP_BASE_URL="https://llm.cmdop.com" # Optional, default
76
+ ```
77
+
78
+ ## Usage Examples
79
+
80
+ ### Chat Completion
81
+
82
+ ```python
83
+ from cmdop_llm import CmdopLLM
84
+
85
+ client = CmdopLLM()
86
+
87
+ response = client.chat.completions.create(
88
+ model="openai/gpt-4o",
89
+ messages=[
90
+ {"role": "system", "content": "You are a helpful assistant."},
91
+ {"role": "user", "content": "Explain quantum computing."}
92
+ ],
93
+ temperature=0.7,
94
+ max_tokens=1000,
95
+ )
96
+ print(response.choices[0].message.content)
97
+ ```
98
+
99
+ ### Streaming
100
+
101
+ ```python
102
+ stream = client.chat.completions.create(
103
+ model="anthropic/claude-3.5-sonnet",
104
+ messages=[{"role": "user", "content": "Write a poem."}],
105
+ stream=True,
106
+ )
107
+
108
+ for chunk in stream:
109
+ if chunk.choices[0].delta.content:
110
+ print(chunk.choices[0].delta.content, end="")
111
+ ```
112
+
113
+ ### Vision Analysis
114
+
115
+ ```python
116
+ result = client.vision.analyze(
117
+ image_url="https://example.com/image.jpg",
118
+ prompt="Describe this image"
119
+ )
120
+ print(result.description)
121
+ print(result.extracted_text)
122
+ ```
123
+
124
+ ### OCR Text Extraction
125
+
126
+ ```python
127
+ result = client.ocr.extract(
128
+ image_url="https://example.com/document.png"
129
+ )
130
+ print(result.text)
131
+ ```
132
+
133
+ ### Image Generation
134
+
135
+ ```python
136
+ response = client.images.generate(
137
+ model="black-forest-labs/FLUX.1-schnell",
138
+ prompt="A futuristic cityscape",
139
+ size="1024x1024",
140
+ )
141
+ print(response.data[0].url)
142
+ ```
143
+
144
+ ### Async Usage
145
+
146
+ ```python
147
+ import asyncio
148
+ from cmdop_llm import AsyncCmdopLLM
149
+
150
+ async def main():
151
+ client = AsyncCmdopLLM()
152
+
153
+ response = await client.chat.completions.create(
154
+ model="openai/gpt-4o",
155
+ messages=[{"role": "user", "content": "Hello!"}]
156
+ )
157
+ print(response.choices[0].message.content)
158
+
159
+ asyncio.run(main())
160
+ ```
161
+
162
+ ## Available Models
163
+
164
+ Access 200+ models including:
165
+
166
+ - **OpenAI**: gpt-4o, gpt-4o-mini, gpt-4-turbo
167
+ - **Anthropic**: claude-3.5-sonnet, claude-3-opus, claude-3-haiku
168
+ - **Google**: gemini-pro, gemini-1.5-pro
169
+ - **Meta**: llama-3.1-405b, llama-3.1-70b
170
+ - **Mistral**: mistral-large, mixtral-8x22b
171
+ - **Image**: FLUX.1-schnell, FLUX.1-pro, stable-diffusion-xl
172
+
173
+ Use model format: `provider/model-name` (e.g., `openai/gpt-4o`)
174
+
175
+ ## API Reference
176
+
177
+ ### CmdopLLM
178
+
179
+ ```python
180
+ CmdopLLM(
181
+ api_key: str = None, # From CMDOP_API_KEY env if not set
182
+ base_url: str = None, # Default: https://llm.cmdop.com
183
+ timeout: float = None, # Request timeout
184
+ max_retries: int = 2, # Retry count
185
+ )
186
+ ```
187
+
188
+ ### Resources
189
+
190
+ - `client.chat.completions` - Chat completions (OpenAI compatible)
191
+ - `client.images` - Image generation (OpenAI compatible)
192
+ - `client.models` - List available models
193
+ - `client.vision` - Vision analysis (CMDOP specific)
194
+ - `client.ocr` - OCR extraction (CMDOP specific)
195
+
196
+ ## License
197
+
198
+ MIT
@@ -0,0 +1,15 @@
1
+ cmdop_llm/__init__.py,sha256=LaW6PRIW-GO8ipcsvfLtA2_lNI4MvJsUiL4GwvdiKVc,2140
2
+ cmdop_llm/_client.py,sha256=Y_ycGrvkuBIzgxx2HWg95W3piJ1Ml5CRSMUwyh4rYFY,7032
3
+ cmdop_llm/_constants.py,sha256=bxyJOaffvTUPRn12GveQYeFRof8iLpw8Xgi_z_usgJQ,301
4
+ cmdop_llm/_version.py,sha256=-XsuXf8DEZujLc8xXXaLl82JH7PEwJ1nbZJqHaYy3FI,154
5
+ cmdop_llm/py.typed,sha256=qww23I-O7yv_Kf5Wu2UW3r8K1Vd12UDESBl5tmzRSI0,60
6
+ cmdop_llm/resources/__init__.py,sha256=Jqx7K-ijPxSahuYBJQHT9xngSWgrAeB0WF4TEiOsHEw,193
7
+ cmdop_llm/resources/ocr.py,sha256=I44rXAWgqDhQFBF6ewqBw9SQoWZc-3ENwe-_iIk0MzY,5438
8
+ cmdop_llm/resources/vision.py,sha256=xRr4eQEALoxyPw3VUxWibCMNelT4_EeQyHIGxFvQ9nU,5852
9
+ cmdop_llm/types/__init__.py,sha256=uuB068ElAXawfftqneg9Cb9CZxq3TZuOCkwZm8l6-hQ,270
10
+ cmdop_llm/types/ocr.py,sha256=WOWV1OYGKoZjmghrOnff4274rrnI4qQEKv9ppDNlXa0,1312
11
+ cmdop_llm/types/vision.py,sha256=nsvG58ggFRb6cRAClcbsaesHA1nu4BYbH5NleZR8UAg,1584
12
+ cmdop_llm-0.1.0.dist-info/METADATA,sha256=SDznx7azTjqwelhjT6SsITtJ3SiYeAYbSz202lEyUwk,5147
13
+ cmdop_llm-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ cmdop_llm-0.1.0.dist-info/licenses/LICENSE,sha256=8flEwqCzbjvfOljG-NWHrUXYYyNVOpWMLXNyvEeuiDA,1062
15
+ cmdop_llm-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CMDOP
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.