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.
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/costimizer.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/costimizer.svg)](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,5 @@
1
+ from costimizer.client import Costimizer
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["Costimizer", "__version__"]
@@ -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,8 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def missing_extra_error(*, provider: str, extra: str, package: str) -> ImportError:
5
+ return ImportError(
6
+ f'{provider} support requires the "{package}" package. '
7
+ f'Install it with: pip install "costimizer[{extra}]"'
8
+ )
@@ -0,0 +1,3 @@
1
+ from costimizer.ai.anthropic.anthropic import Anthropic
2
+
3
+ __all__ = ["Anthropic"]
@@ -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,3 @@
1
+ from costimizer.ai.gemini.gemini import Gemini
2
+
3
+ __all__ = ["Gemini"]
@@ -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,3 @@
1
+ from costimizer.ai.openai.openai import OpenAI
2
+
3
+ __all__ = ["OpenAI"]
@@ -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,3 @@
1
+ from costimizer.ai.openrouter.openrouter import OpenRouter
2
+
3
+ __all__ = ["OpenRouter"]
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/costimizer.svg)](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,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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+