costimizer 0.1.0__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.
- costimizer-0.1.0/LICENSE +21 -0
- costimizer-0.1.0/PKG-INFO +91 -0
- costimizer-0.1.0/README.md +46 -0
- costimizer-0.1.0/costimizer/__init__.py +5 -0
- costimizer-0.1.0/costimizer/ai/__init__.py +10 -0
- costimizer-0.1.0/costimizer/ai/_extras.py +8 -0
- costimizer-0.1.0/costimizer/ai/anthropic/__init__.py +3 -0
- costimizer-0.1.0/costimizer/ai/anthropic/anthropic.py +64 -0
- costimizer-0.1.0/costimizer/ai/gemini/__init__.py +3 -0
- costimizer-0.1.0/costimizer/ai/gemini/gemini.py +64 -0
- costimizer-0.1.0/costimizer/ai/openai/__init__.py +3 -0
- costimizer-0.1.0/costimizer/ai/openai/openai.py +67 -0
- costimizer-0.1.0/costimizer/ai/openrouter/__init__.py +3 -0
- costimizer-0.1.0/costimizer/ai/openrouter/openrouter.py +70 -0
- costimizer-0.1.0/costimizer/ai/utils.py +156 -0
- costimizer-0.1.0/costimizer/client.py +69 -0
- costimizer-0.1.0/costimizer.egg-info/PKG-INFO +91 -0
- costimizer-0.1.0/costimizer.egg-info/SOURCES.txt +21 -0
- costimizer-0.1.0/costimizer.egg-info/dependency_links.txt +1 -0
- costimizer-0.1.0/costimizer.egg-info/requires.txt +26 -0
- costimizer-0.1.0/costimizer.egg-info/top_level.txt +1 -0
- costimizer-0.1.0/pyproject.toml +66 -0
- costimizer-0.1.0/setup.cfg +4 -0
costimizer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Costimizer
|
|
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.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: costimizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Costimizer AI observability SDK for LLM FinOps
|
|
5
|
+
Author-email: Costimizer <itadmin@costimizer.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://costimizer.ai
|
|
8
|
+
Project-URL: Documentation, https://costimizer.ai
|
|
9
|
+
Project-URL: Repository, https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk
|
|
10
|
+
Project-URL: Issues, https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk/-/issues
|
|
11
|
+
Keywords: llm,finops,observability,openai,anthropic,gemini,openrouter,cost-tracking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx>=0.28.0
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: openai>=1.60.0; extra == "openai"
|
|
28
|
+
Provides-Extra: openrouter
|
|
29
|
+
Requires-Dist: openai>=1.60.0; extra == "openrouter"
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
|
|
32
|
+
Provides-Extra: gemini
|
|
33
|
+
Requires-Dist: google-genai>=1.0.0; extra == "gemini"
|
|
34
|
+
Provides-Extra: google
|
|
35
|
+
Requires-Dist: google-genai>=1.0.0; extra == "google"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: openai>=1.60.0; extra == "all"
|
|
38
|
+
Requires-Dist: anthropic>=0.40.0; extra == "all"
|
|
39
|
+
Requires-Dist: google-genai>=1.0.0; extra == "all"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
43
|
+
Requires-Dist: twine>=5.1.0; extra == "dev"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Costimizer Python SDK
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/costimizer/)
|
|
49
|
+
|
|
50
|
+
Capture LLM calls and send them to Costimizer FinOps.
|
|
51
|
+
|
|
52
|
+
Install only the provider client you use — same pattern as PostHog:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "costimizer[openai]"
|
|
56
|
+
pip install "costimizer[all]" # every provider
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Core SDK (`httpx` only) installs with:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install costimizer
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## OpenAI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install "costimizer[openai]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from costimizer import Costimizer
|
|
73
|
+
from costimizer.ai.openai import OpenAI
|
|
74
|
+
|
|
75
|
+
costimizer = Costimizer(
|
|
76
|
+
project_token="fo_ingest_your_key",
|
|
77
|
+
host="https://api.costimizer.ai",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
client = OpenAI(
|
|
81
|
+
api_key="sk-...",
|
|
82
|
+
costimizer_client=costimizer,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response = client.chat.completions.create(
|
|
86
|
+
model="gpt-4o-mini",
|
|
87
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
88
|
+
costimizer_trace_name="support-chat",
|
|
89
|
+
)
|
|
90
|
+
costimizer.shutdown()
|
|
91
|
+
```
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Costimizer Python SDK
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/costimizer/)
|
|
4
|
+
|
|
5
|
+
Capture LLM calls and send them to Costimizer FinOps.
|
|
6
|
+
|
|
7
|
+
Install only the provider client you use — same pattern as PostHog:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install "costimizer[openai]"
|
|
11
|
+
pip install "costimizer[all]" # every provider
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Core SDK (`httpx` only) installs with:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install costimizer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## OpenAI
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install "costimizer[openai]"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from costimizer import Costimizer
|
|
28
|
+
from costimizer.ai.openai import OpenAI
|
|
29
|
+
|
|
30
|
+
costimizer = Costimizer(
|
|
31
|
+
project_token="fo_ingest_your_key",
|
|
32
|
+
host="https://api.costimizer.ai",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
client = OpenAI(
|
|
36
|
+
api_key="sk-...",
|
|
37
|
+
costimizer_client=costimizer,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
response = client.chat.completions.create(
|
|
41
|
+
model="gpt-4o-mini",
|
|
42
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
43
|
+
costimizer_trace_name="support-chat",
|
|
44
|
+
)
|
|
45
|
+
costimizer.shutdown()
|
|
46
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Provider-specific AI wrappers live under costimizer.ai.*.
|
|
2
|
+
|
|
3
|
+
Install only the provider SDK you need:
|
|
4
|
+
|
|
5
|
+
pip install "costimizer[openai]"
|
|
6
|
+
pip install "costimizer[openrouter]"
|
|
7
|
+
pip install "costimizer[anthropic]"
|
|
8
|
+
pip install "costimizer[gemini]"
|
|
9
|
+
pip install "costimizer[all]"
|
|
10
|
+
"""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from costimizer.ai.utils import track_llm_call
|
|
6
|
+
from costimizer.client import Costimizer
|
|
7
|
+
|
|
8
|
+
DEFAULT_BASE_URL = "https://api.anthropic.com"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Anthropic:
|
|
12
|
+
"""Drop-in wrapper around anthropic.Anthropic that reports generations to Costimizer."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
costimizer_client: Costimizer,
|
|
18
|
+
*,
|
|
19
|
+
privacy_mode: str = "metadata_only",
|
|
20
|
+
) -> None:
|
|
21
|
+
try:
|
|
22
|
+
from anthropic import Anthropic as AnthropicClient
|
|
23
|
+
except ImportError as exc:
|
|
24
|
+
from costimizer.ai._extras import missing_extra_error
|
|
25
|
+
|
|
26
|
+
raise missing_extra_error(
|
|
27
|
+
provider="Anthropic",
|
|
28
|
+
extra="anthropic",
|
|
29
|
+
package="anthropic",
|
|
30
|
+
) from exc
|
|
31
|
+
|
|
32
|
+
self._client = AnthropicClient(api_key=api_key)
|
|
33
|
+
self._costimizer_client = costimizer_client
|
|
34
|
+
self._privacy_mode = privacy_mode
|
|
35
|
+
self.messages = _WrappedMessages(self)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _WrappedMessages:
|
|
39
|
+
def __init__(self, root: Anthropic) -> None:
|
|
40
|
+
self._root = root
|
|
41
|
+
|
|
42
|
+
def create(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
costimizer_distinct_id: str | None = None,
|
|
46
|
+
costimizer_trace_id: str | None = None,
|
|
47
|
+
costimizer_trace_name: str | None = None,
|
|
48
|
+
costimizer_properties: dict[str, Any] | None = None,
|
|
49
|
+
costimizer_privacy_mode: str | None = None,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> Any:
|
|
52
|
+
root = self._root
|
|
53
|
+
return track_llm_call(
|
|
54
|
+
costimizer_client=root._costimizer_client,
|
|
55
|
+
provider="anthropic",
|
|
56
|
+
call_fn=root._client.messages.create,
|
|
57
|
+
trace_id=costimizer_trace_id,
|
|
58
|
+
trace_name=costimizer_trace_name,
|
|
59
|
+
distinct_id=costimizer_distinct_id,
|
|
60
|
+
properties=costimizer_properties,
|
|
61
|
+
privacy_mode=costimizer_privacy_mode or root._privacy_mode,
|
|
62
|
+
base_url=DEFAULT_BASE_URL,
|
|
63
|
+
**kwargs,
|
|
64
|
+
)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from costimizer.ai.utils import track_llm_call
|
|
6
|
+
from costimizer.client import Costimizer
|
|
7
|
+
|
|
8
|
+
DEFAULT_BASE_URL = "https://generativelanguage.googleapis.com"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Gemini:
|
|
12
|
+
"""Drop-in wrapper around google.genai.Client that reports generations to Costimizer."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
costimizer_client: Costimizer,
|
|
18
|
+
*,
|
|
19
|
+
privacy_mode: str = "metadata_only",
|
|
20
|
+
) -> None:
|
|
21
|
+
try:
|
|
22
|
+
from google import genai
|
|
23
|
+
except ImportError as exc:
|
|
24
|
+
from costimizer.ai._extras import missing_extra_error
|
|
25
|
+
|
|
26
|
+
raise missing_extra_error(
|
|
27
|
+
provider="Gemini",
|
|
28
|
+
extra="gemini",
|
|
29
|
+
package="google-genai",
|
|
30
|
+
) from exc
|
|
31
|
+
|
|
32
|
+
self._client = genai.Client(api_key=api_key)
|
|
33
|
+
self._costimizer_client = costimizer_client
|
|
34
|
+
self._privacy_mode = privacy_mode
|
|
35
|
+
self.models = _WrappedModels(self)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class _WrappedModels:
|
|
39
|
+
def __init__(self, root: Gemini) -> None:
|
|
40
|
+
self._root = root
|
|
41
|
+
|
|
42
|
+
def generate_content(
|
|
43
|
+
self,
|
|
44
|
+
*,
|
|
45
|
+
costimizer_distinct_id: str | None = None,
|
|
46
|
+
costimizer_trace_id: str | None = None,
|
|
47
|
+
costimizer_trace_name: str | None = None,
|
|
48
|
+
costimizer_properties: dict[str, Any] | None = None,
|
|
49
|
+
costimizer_privacy_mode: str | None = None,
|
|
50
|
+
**kwargs: Any,
|
|
51
|
+
) -> Any:
|
|
52
|
+
root = self._root
|
|
53
|
+
return track_llm_call(
|
|
54
|
+
costimizer_client=root._costimizer_client,
|
|
55
|
+
provider="gemini",
|
|
56
|
+
call_fn=root._client.models.generate_content,
|
|
57
|
+
trace_id=costimizer_trace_id,
|
|
58
|
+
trace_name=costimizer_trace_name,
|
|
59
|
+
distinct_id=costimizer_distinct_id,
|
|
60
|
+
properties=costimizer_properties,
|
|
61
|
+
privacy_mode=costimizer_privacy_mode or root._privacy_mode,
|
|
62
|
+
base_url=DEFAULT_BASE_URL,
|
|
63
|
+
**kwargs,
|
|
64
|
+
)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from costimizer.ai.utils import track_llm_call
|
|
6
|
+
from costimizer.client import Costimizer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class OpenAI:
|
|
10
|
+
"""Drop-in wrapper around openai.OpenAI that reports generations to Costimizer."""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
api_key: str,
|
|
15
|
+
costimizer_client: Costimizer,
|
|
16
|
+
*,
|
|
17
|
+
privacy_mode: str = "full_content",
|
|
18
|
+
) -> None:
|
|
19
|
+
try:
|
|
20
|
+
from openai import OpenAI as OpenAIClient
|
|
21
|
+
except ImportError as exc:
|
|
22
|
+
from costimizer.ai._extras import missing_extra_error
|
|
23
|
+
|
|
24
|
+
raise missing_extra_error(
|
|
25
|
+
provider="OpenAI",
|
|
26
|
+
extra="openai",
|
|
27
|
+
package="openai",
|
|
28
|
+
) from exc
|
|
29
|
+
|
|
30
|
+
self._client = OpenAIClient(api_key=api_key)
|
|
31
|
+
self._costimizer_client = costimizer_client
|
|
32
|
+
self._privacy_mode = privacy_mode
|
|
33
|
+
self.chat = _WrappedChat(self)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _WrappedChat:
|
|
37
|
+
def __init__(self, root: OpenAI) -> None:
|
|
38
|
+
self.completions = _WrappedCompletions(root)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _WrappedCompletions:
|
|
42
|
+
def __init__(self, root: OpenAI) -> None:
|
|
43
|
+
self._root = root
|
|
44
|
+
|
|
45
|
+
def create(
|
|
46
|
+
self,
|
|
47
|
+
*,
|
|
48
|
+
costimizer_distinct_id: str | None = None,
|
|
49
|
+
costimizer_trace_id: str | None = None,
|
|
50
|
+
costimizer_trace_name: str | None = None,
|
|
51
|
+
costimizer_properties: dict[str, Any] | None = None,
|
|
52
|
+
costimizer_privacy_mode: str | None = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> Any:
|
|
55
|
+
root = self._root
|
|
56
|
+
return track_llm_call(
|
|
57
|
+
costimizer_client=root._costimizer_client,
|
|
58
|
+
provider="openai",
|
|
59
|
+
call_fn=root._client.chat.completions.create,
|
|
60
|
+
trace_id=costimizer_trace_id,
|
|
61
|
+
trace_name=costimizer_trace_name,
|
|
62
|
+
distinct_id=costimizer_distinct_id,
|
|
63
|
+
properties=costimizer_properties,
|
|
64
|
+
privacy_mode=costimizer_privacy_mode or root._privacy_mode,
|
|
65
|
+
base_url=str(root._client.base_url),
|
|
66
|
+
**kwargs,
|
|
67
|
+
)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from costimizer.ai.utils import track_llm_call
|
|
6
|
+
from costimizer.client import Costimizer
|
|
7
|
+
|
|
8
|
+
DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class OpenRouter:
|
|
12
|
+
"""Drop-in wrapper around the OpenAI SDK pointed at OpenRouter."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
api_key: str,
|
|
17
|
+
costimizer_client: Costimizer,
|
|
18
|
+
*,
|
|
19
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
20
|
+
privacy_mode: str = "metadata_only",
|
|
21
|
+
) -> None:
|
|
22
|
+
try:
|
|
23
|
+
from openai import OpenAI as OpenAIClient
|
|
24
|
+
except ImportError as exc:
|
|
25
|
+
from costimizer.ai._extras import missing_extra_error
|
|
26
|
+
|
|
27
|
+
raise missing_extra_error(
|
|
28
|
+
provider="OpenRouter",
|
|
29
|
+
extra="openrouter",
|
|
30
|
+
package="openai",
|
|
31
|
+
) from exc
|
|
32
|
+
|
|
33
|
+
self._client = OpenAIClient(api_key=api_key, base_url=base_url)
|
|
34
|
+
self._costimizer_client = costimizer_client
|
|
35
|
+
self._privacy_mode = privacy_mode
|
|
36
|
+
self.chat = _WrappedChat(self)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _WrappedChat:
|
|
40
|
+
def __init__(self, root: OpenRouter) -> None:
|
|
41
|
+
self.completions = _WrappedCompletions(root)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _WrappedCompletions:
|
|
45
|
+
def __init__(self, root: OpenRouter) -> None:
|
|
46
|
+
self._root = root
|
|
47
|
+
|
|
48
|
+
def create(
|
|
49
|
+
self,
|
|
50
|
+
*,
|
|
51
|
+
costimizer_distinct_id: str | None = None,
|
|
52
|
+
costimizer_trace_id: str | None = None,
|
|
53
|
+
costimizer_trace_name: str | None = None,
|
|
54
|
+
costimizer_properties: dict[str, Any] | None = None,
|
|
55
|
+
costimizer_privacy_mode: str | None = None,
|
|
56
|
+
**kwargs: Any,
|
|
57
|
+
) -> Any:
|
|
58
|
+
root = self._root
|
|
59
|
+
return track_llm_call(
|
|
60
|
+
costimizer_client=root._costimizer_client,
|
|
61
|
+
provider="openrouter",
|
|
62
|
+
call_fn=root._client.chat.completions.create,
|
|
63
|
+
trace_id=costimizer_trace_id,
|
|
64
|
+
trace_name=costimizer_trace_name,
|
|
65
|
+
distinct_id=costimizer_distinct_id,
|
|
66
|
+
properties=costimizer_properties,
|
|
67
|
+
privacy_mode=costimizer_privacy_mode or root._privacy_mode,
|
|
68
|
+
base_url=str(root._client.base_url),
|
|
69
|
+
**kwargs,
|
|
70
|
+
)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
import uuid
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _coerce_int(value: Any) -> int:
|
|
10
|
+
if value is None:
|
|
11
|
+
return 0
|
|
12
|
+
try:
|
|
13
|
+
return int(value)
|
|
14
|
+
except (TypeError, ValueError):
|
|
15
|
+
return 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def extract_usage(provider: str, response: Any, kwargs: dict[str, Any]) -> tuple[str, int, int]:
|
|
19
|
+
model = str(kwargs.get("model", "unknown"))
|
|
20
|
+
input_tokens = 0
|
|
21
|
+
output_tokens = 0
|
|
22
|
+
|
|
23
|
+
if response is None:
|
|
24
|
+
return model, input_tokens, output_tokens
|
|
25
|
+
|
|
26
|
+
if provider in {"openai", "openrouter"}:
|
|
27
|
+
model = str(getattr(response, "model", model) or model)
|
|
28
|
+
usage = getattr(response, "usage", None)
|
|
29
|
+
if usage is not None:
|
|
30
|
+
input_tokens = _coerce_int(getattr(usage, "prompt_tokens", 0))
|
|
31
|
+
output_tokens = _coerce_int(getattr(usage, "completion_tokens", 0))
|
|
32
|
+
elif provider == "anthropic":
|
|
33
|
+
model = str(getattr(response, "model", model) or model)
|
|
34
|
+
usage = getattr(response, "usage", None)
|
|
35
|
+
if usage is not None:
|
|
36
|
+
input_tokens = _coerce_int(getattr(usage, "input_tokens", 0))
|
|
37
|
+
output_tokens = _coerce_int(getattr(usage, "output_tokens", 0))
|
|
38
|
+
elif provider in {"gemini", "google"}:
|
|
39
|
+
model = str(getattr(response, "model", model) or model)
|
|
40
|
+
usage = getattr(response, "usage_metadata", None)
|
|
41
|
+
if usage is not None:
|
|
42
|
+
input_tokens = _coerce_int(getattr(usage, "prompt_token_count", 0))
|
|
43
|
+
output_tokens = _coerce_int(getattr(usage, "candidates_token_count", 0))
|
|
44
|
+
|
|
45
|
+
return model, input_tokens, output_tokens
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _json_safe(value: Any) -> Any:
|
|
49
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
50
|
+
return value
|
|
51
|
+
if isinstance(value, dict):
|
|
52
|
+
return {str(k): _json_safe(v) for k, v in value.items()}
|
|
53
|
+
if isinstance(value, (list, tuple)):
|
|
54
|
+
return [_json_safe(item) for item in value]
|
|
55
|
+
if hasattr(value, "model_dump"):
|
|
56
|
+
return _json_safe(value.model_dump())
|
|
57
|
+
if hasattr(value, "to_dict"):
|
|
58
|
+
return _json_safe(value.to_dict())
|
|
59
|
+
if hasattr(value, "__dict__"):
|
|
60
|
+
return _json_safe(
|
|
61
|
+
{k: v for k, v in vars(value).items() if not k.startswith("_")}
|
|
62
|
+
)
|
|
63
|
+
return str(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def extract_content(
|
|
67
|
+
provider: str,
|
|
68
|
+
response: Any,
|
|
69
|
+
kwargs: dict[str, Any],
|
|
70
|
+
) -> tuple[Any, list[dict[str, Any]] | None]:
|
|
71
|
+
if provider in {"openai", "openrouter"}:
|
|
72
|
+
input_content = kwargs.get("messages")
|
|
73
|
+
output_messages = None
|
|
74
|
+
choices = getattr(response, "choices", None)
|
|
75
|
+
if choices:
|
|
76
|
+
message = choices[0].message
|
|
77
|
+
output_messages = [
|
|
78
|
+
{"role": "assistant", "content": getattr(message, "content", None)}
|
|
79
|
+
]
|
|
80
|
+
return input_content, output_messages
|
|
81
|
+
|
|
82
|
+
if provider == "anthropic":
|
|
83
|
+
input_content = kwargs.get("messages")
|
|
84
|
+
output_messages = None
|
|
85
|
+
content = getattr(response, "content", None)
|
|
86
|
+
if content:
|
|
87
|
+
text = getattr(content[0], "text", None)
|
|
88
|
+
output_messages = [{"role": "assistant", "content": text}]
|
|
89
|
+
return input_content, output_messages
|
|
90
|
+
|
|
91
|
+
if provider in {"gemini", "google"}:
|
|
92
|
+
input_content = kwargs.get("contents")
|
|
93
|
+
text = getattr(response, "text", None)
|
|
94
|
+
output_messages = [{"role": "assistant", "content": text}] if text else None
|
|
95
|
+
return input_content, output_messages
|
|
96
|
+
|
|
97
|
+
return None, None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def track_llm_call(
|
|
101
|
+
*,
|
|
102
|
+
costimizer_client: Any,
|
|
103
|
+
provider: str,
|
|
104
|
+
call_fn: Callable[..., Any],
|
|
105
|
+
trace_id: str | None,
|
|
106
|
+
trace_name: str | None,
|
|
107
|
+
distinct_id: str | None,
|
|
108
|
+
properties: dict[str, Any] | None,
|
|
109
|
+
privacy_mode: Literal["metadata_only", "full_content"] = "full_content",
|
|
110
|
+
base_url: str,
|
|
111
|
+
**kwargs: Any,
|
|
112
|
+
) -> Any:
|
|
113
|
+
started = time.perf_counter()
|
|
114
|
+
response = None
|
|
115
|
+
error: Exception | None = None
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
response = call_fn(**kwargs)
|
|
119
|
+
except Exception as exc:
|
|
120
|
+
error = exc
|
|
121
|
+
finally:
|
|
122
|
+
latency_ms = (time.perf_counter() - started) * 1000
|
|
123
|
+
resolved_trace_id = trace_id or str(uuid.uuid4())
|
|
124
|
+
model, input_tokens, output_tokens = extract_usage(provider, response, kwargs)
|
|
125
|
+
|
|
126
|
+
event: dict[str, Any] = {
|
|
127
|
+
"type": "generation",
|
|
128
|
+
"trace_id": resolved_trace_id,
|
|
129
|
+
"trace_name": trace_name,
|
|
130
|
+
"provider": provider,
|
|
131
|
+
"model": model,
|
|
132
|
+
"latency_ms": round(latency_ms, 2),
|
|
133
|
+
"input_tokens": input_tokens,
|
|
134
|
+
"output_tokens": output_tokens,
|
|
135
|
+
"is_error": error is not None,
|
|
136
|
+
"distinct_id": distinct_id,
|
|
137
|
+
"base_url": base_url,
|
|
138
|
+
"properties": properties or {},
|
|
139
|
+
"privacy_mode": privacy_mode,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if error is not None:
|
|
143
|
+
event["error_message"] = str(error)
|
|
144
|
+
|
|
145
|
+
if privacy_mode == "full_content" and response is not None:
|
|
146
|
+
input_content, output_messages = extract_content(provider, response, kwargs)
|
|
147
|
+
event["input_messages"] = _json_safe(input_content)
|
|
148
|
+
if output_messages is not None:
|
|
149
|
+
event["output_messages"] = _json_safe(output_messages)
|
|
150
|
+
|
|
151
|
+
costimizer_client.enqueue(event)
|
|
152
|
+
|
|
153
|
+
if error is not None:
|
|
154
|
+
raise error
|
|
155
|
+
|
|
156
|
+
return response
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import atexit
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Costimizer:
|
|
12
|
+
"""Sends AI observability events to the Costimizer FinOps API."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
project_token: str,
|
|
17
|
+
host: str = "https://api.costimizer.ai",
|
|
18
|
+
*,
|
|
19
|
+
flush_interval_seconds: float = 2.0,
|
|
20
|
+
max_batch_size: int = 50,
|
|
21
|
+
) -> None:
|
|
22
|
+
self._project_token = project_token
|
|
23
|
+
self._host = host.rstrip("/")
|
|
24
|
+
self._flush_interval_seconds = flush_interval_seconds
|
|
25
|
+
self._max_batch_size = max_batch_size
|
|
26
|
+
self._queue: list[dict[str, Any]] = []
|
|
27
|
+
self._lock = threading.Lock()
|
|
28
|
+
self._http = httpx.Client(timeout=10.0)
|
|
29
|
+
self._stop = threading.Event()
|
|
30
|
+
self._worker = threading.Thread(target=self._flush_loop, daemon=True)
|
|
31
|
+
self._worker.start()
|
|
32
|
+
atexit.register(self.shutdown)
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def ingest_url(self) -> str:
|
|
36
|
+
return f"{self._host}/api/v1/observability/events"
|
|
37
|
+
|
|
38
|
+
def enqueue(self, event: dict[str, Any]) -> None:
|
|
39
|
+
with self._lock:
|
|
40
|
+
self._queue.append(event)
|
|
41
|
+
if len(self._queue) >= self._max_batch_size:
|
|
42
|
+
self._flush_locked()
|
|
43
|
+
|
|
44
|
+
def shutdown(self) -> None:
|
|
45
|
+
self._stop.set()
|
|
46
|
+
self._worker.join(timeout=5.0)
|
|
47
|
+
with self._lock:
|
|
48
|
+
self._flush_locked()
|
|
49
|
+
self._http.close()
|
|
50
|
+
|
|
51
|
+
def _flush_loop(self) -> None:
|
|
52
|
+
while not self._stop.wait(self._flush_interval_seconds):
|
|
53
|
+
with self._lock:
|
|
54
|
+
self._flush_locked()
|
|
55
|
+
|
|
56
|
+
def _flush_locked(self) -> None:
|
|
57
|
+
if not self._queue:
|
|
58
|
+
return
|
|
59
|
+
batch = self._queue[:]
|
|
60
|
+
self._queue.clear()
|
|
61
|
+
try:
|
|
62
|
+
self._http.post(
|
|
63
|
+
self.ingest_url,
|
|
64
|
+
json={"events": batch},
|
|
65
|
+
headers={"Authorization": f"Bearer {self._project_token}"},
|
|
66
|
+
)
|
|
67
|
+
except httpx.HTTPError:
|
|
68
|
+
# MVP: drop on failure so LLM calls are never blocked.
|
|
69
|
+
pass
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: costimizer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Costimizer AI observability SDK for LLM FinOps
|
|
5
|
+
Author-email: Costimizer <itadmin@costimizer.ai>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://costimizer.ai
|
|
8
|
+
Project-URL: Documentation, https://costimizer.ai
|
|
9
|
+
Project-URL: Repository, https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk
|
|
10
|
+
Project-URL: Issues, https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk/-/issues
|
|
11
|
+
Keywords: llm,finops,observability,openai,anthropic,gemini,openrouter,cost-tracking
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx>=0.28.0
|
|
26
|
+
Provides-Extra: openai
|
|
27
|
+
Requires-Dist: openai>=1.60.0; extra == "openai"
|
|
28
|
+
Provides-Extra: openrouter
|
|
29
|
+
Requires-Dist: openai>=1.60.0; extra == "openrouter"
|
|
30
|
+
Provides-Extra: anthropic
|
|
31
|
+
Requires-Dist: anthropic>=0.40.0; extra == "anthropic"
|
|
32
|
+
Provides-Extra: gemini
|
|
33
|
+
Requires-Dist: google-genai>=1.0.0; extra == "gemini"
|
|
34
|
+
Provides-Extra: google
|
|
35
|
+
Requires-Dist: google-genai>=1.0.0; extra == "google"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: openai>=1.60.0; extra == "all"
|
|
38
|
+
Requires-Dist: anthropic>=0.40.0; extra == "all"
|
|
39
|
+
Requires-Dist: google-genai>=1.0.0; extra == "all"
|
|
40
|
+
Provides-Extra: dev
|
|
41
|
+
Requires-Dist: pytest>=8.3.0; extra == "dev"
|
|
42
|
+
Requires-Dist: build>=1.2.0; extra == "dev"
|
|
43
|
+
Requires-Dist: twine>=5.1.0; extra == "dev"
|
|
44
|
+
Dynamic: license-file
|
|
45
|
+
|
|
46
|
+
# Costimizer Python SDK
|
|
47
|
+
|
|
48
|
+
[](https://pypi.org/project/costimizer/)
|
|
49
|
+
|
|
50
|
+
Capture LLM calls and send them to Costimizer FinOps.
|
|
51
|
+
|
|
52
|
+
Install only the provider client you use — same pattern as PostHog:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "costimizer[openai]"
|
|
56
|
+
pip install "costimizer[all]" # every provider
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Core SDK (`httpx` only) installs with:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install costimizer
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## OpenAI
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install "costimizer[openai]"
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from costimizer import Costimizer
|
|
73
|
+
from costimizer.ai.openai import OpenAI
|
|
74
|
+
|
|
75
|
+
costimizer = Costimizer(
|
|
76
|
+
project_token="fo_ingest_your_key",
|
|
77
|
+
host="https://api.costimizer.ai",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
client = OpenAI(
|
|
81
|
+
api_key="sk-...",
|
|
82
|
+
costimizer_client=costimizer,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
response = client.chat.completions.create(
|
|
86
|
+
model="gpt-4o-mini",
|
|
87
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
88
|
+
costimizer_trace_name="support-chat",
|
|
89
|
+
)
|
|
90
|
+
costimizer.shutdown()
|
|
91
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
costimizer/__init__.py
|
|
5
|
+
costimizer/client.py
|
|
6
|
+
costimizer.egg-info/PKG-INFO
|
|
7
|
+
costimizer.egg-info/SOURCES.txt
|
|
8
|
+
costimizer.egg-info/dependency_links.txt
|
|
9
|
+
costimizer.egg-info/requires.txt
|
|
10
|
+
costimizer.egg-info/top_level.txt
|
|
11
|
+
costimizer/ai/__init__.py
|
|
12
|
+
costimizer/ai/_extras.py
|
|
13
|
+
costimizer/ai/utils.py
|
|
14
|
+
costimizer/ai/anthropic/__init__.py
|
|
15
|
+
costimizer/ai/anthropic/anthropic.py
|
|
16
|
+
costimizer/ai/gemini/__init__.py
|
|
17
|
+
costimizer/ai/gemini/gemini.py
|
|
18
|
+
costimizer/ai/openai/__init__.py
|
|
19
|
+
costimizer/ai/openai/openai.py
|
|
20
|
+
costimizer/ai/openrouter/__init__.py
|
|
21
|
+
costimizer/ai/openrouter/openrouter.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
httpx>=0.28.0
|
|
2
|
+
|
|
3
|
+
[all]
|
|
4
|
+
openai>=1.60.0
|
|
5
|
+
anthropic>=0.40.0
|
|
6
|
+
google-genai>=1.0.0
|
|
7
|
+
|
|
8
|
+
[anthropic]
|
|
9
|
+
anthropic>=0.40.0
|
|
10
|
+
|
|
11
|
+
[dev]
|
|
12
|
+
pytest>=8.3.0
|
|
13
|
+
build>=1.2.0
|
|
14
|
+
twine>=5.1.0
|
|
15
|
+
|
|
16
|
+
[gemini]
|
|
17
|
+
google-genai>=1.0.0
|
|
18
|
+
|
|
19
|
+
[google]
|
|
20
|
+
google-genai>=1.0.0
|
|
21
|
+
|
|
22
|
+
[openai]
|
|
23
|
+
openai>=1.60.0
|
|
24
|
+
|
|
25
|
+
[openrouter]
|
|
26
|
+
openai>=1.60.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
costimizer
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "costimizer"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Costimizer AI observability SDK for LLM FinOps"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Costimizer", email = "itadmin@costimizer.ai" },
|
|
10
|
+
]
|
|
11
|
+
keywords = [
|
|
12
|
+
"llm",
|
|
13
|
+
"finops",
|
|
14
|
+
"observability",
|
|
15
|
+
"openai",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"gemini",
|
|
18
|
+
"openrouter",
|
|
19
|
+
"cost-tracking",
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Development Status :: 4 - Beta",
|
|
23
|
+
"Intended Audience :: Developers",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"Programming Language :: Python :: 3.10",
|
|
28
|
+
"Programming Language :: Python :: 3.11",
|
|
29
|
+
"Programming Language :: Python :: 3.12",
|
|
30
|
+
"Programming Language :: Python :: 3.13",
|
|
31
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
32
|
+
]
|
|
33
|
+
dependencies = [
|
|
34
|
+
"httpx>=0.28.0",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://costimizer.ai"
|
|
39
|
+
Documentation = "https://costimizer.ai"
|
|
40
|
+
Repository = "https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk"
|
|
41
|
+
Issues = "https://gitlab.bigohtech.com/costimizer/finops/finops-ai-sdk/-/issues"
|
|
42
|
+
|
|
43
|
+
[project.optional-dependencies]
|
|
44
|
+
openai = ["openai>=1.60.0"]
|
|
45
|
+
openrouter = ["openai>=1.60.0"]
|
|
46
|
+
anthropic = ["anthropic>=0.40.0"]
|
|
47
|
+
gemini = ["google-genai>=1.0.0"]
|
|
48
|
+
google = ["google-genai>=1.0.0"]
|
|
49
|
+
all = [
|
|
50
|
+
"openai>=1.60.0",
|
|
51
|
+
"anthropic>=0.40.0",
|
|
52
|
+
"google-genai>=1.0.0",
|
|
53
|
+
]
|
|
54
|
+
dev = [
|
|
55
|
+
"pytest>=8.3.0",
|
|
56
|
+
"build>=1.2.0",
|
|
57
|
+
"twine>=5.1.0",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[build-system]
|
|
61
|
+
requires = ["setuptools>=68.0"]
|
|
62
|
+
build-backend = "setuptools.build_meta"
|
|
63
|
+
|
|
64
|
+
[tool.setuptools.packages.find]
|
|
65
|
+
where = ["."]
|
|
66
|
+
include = ["costimizer*"]
|