tokenator 0.1.8__py3-none-any.whl → 0.1.9__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
tokenator/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """Tokenator - Track and analyze your OpenAI API token usage and costs."""
2
2
 
3
3
  import logging
4
- from .client_openai import tokenator_openai
4
+ from .openai.client_openai import tokenator_openai
5
5
  from .client_anthropic import tokenator_anthropic
6
6
  from . import usage
7
7
  from .utils import get_default_db_path
@@ -0,0 +1,78 @@
1
+ import logging
2
+ from typing import AsyncIterator, Callable, Generic, List, Optional, TypeVar
3
+
4
+ from openai import AsyncStream, AsyncOpenAI
5
+ from openai.types.chat import ChatCompletionChunk
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ _T = TypeVar("_T") # or you might specifically do _T = ChatCompletionChunk
10
+
11
+
12
+ class AsyncStreamInterceptor(AsyncStream[_T]):
13
+ """
14
+ A wrapper around openai.AsyncStream that delegates all functionality
15
+ to the 'base_stream' but intercepts each chunk to handle usage or
16
+ logging logic. This preserves .response and other methods.
17
+
18
+ You can store aggregated usage in a local list and process it when
19
+ the stream ends (StopAsyncIteration).
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ base_stream: AsyncStream[_T],
25
+ usage_callback: Optional[Callable[[List[_T]], None]] = None,
26
+ ):
27
+ # We do NOT call super().__init__() because openai.AsyncStream
28
+ # expects constructor parameters we don't want to re-initialize.
29
+ # Instead, we just store the base_stream and delegate everything to it.
30
+ self._base_stream = base_stream
31
+ self._usage_callback = usage_callback
32
+ self._chunks: List[_T] = []
33
+
34
+ @property
35
+ def response(self):
36
+ """Expose the original stream's 'response' so user code can do stream.response, etc."""
37
+ return self._base_stream.response
38
+
39
+ def __aiter__(self) -> AsyncIterator[_T]:
40
+ """
41
+ Called when we do 'async for chunk in wrapped_stream:'
42
+ We simply return 'self'. Then __anext__ does the rest.
43
+ """
44
+ return self
45
+
46
+ async def __anext__(self) -> _T:
47
+ """
48
+ Intercept iteration. We pull the next chunk from the base_stream.
49
+ If it's the end, do any final usage logging, then raise StopAsyncIteration.
50
+ Otherwise, we can accumulate usage info or do whatever we need with the chunk.
51
+ """
52
+ try:
53
+ chunk = await self._base_stream.__anext__()
54
+ except StopAsyncIteration:
55
+ # Once the base stream is fully consumed, we can do final usage/logging.
56
+ if self._usage_callback and self._chunks:
57
+ self._usage_callback(self._chunks)
58
+ raise
59
+
60
+ # Intercept each chunk
61
+ self._chunks.append(chunk)
62
+ return chunk
63
+
64
+ async def __aenter__(self) -> "AsyncStreamInterceptor[_T]":
65
+ """Support async with ... : usage."""
66
+ await self._base_stream.__aenter__()
67
+ return self
68
+
69
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
70
+ """
71
+ Ensure we propagate __aexit__ to the base stream,
72
+ so connections are properly closed.
73
+ """
74
+ return await self._base_stream.__aexit__(exc_type, exc_val, exc_tb)
75
+
76
+ async def close(self) -> None:
77
+ """Delegate close to the base_stream."""
78
+ await self._base_stream.close()
@@ -6,8 +6,9 @@ import logging
6
6
  from openai import AsyncOpenAI, AsyncStream, OpenAI, Stream
7
7
  from openai.types.chat import ChatCompletion, ChatCompletionChunk
8
8
 
9
- from .models import Usage, TokenUsageStats
10
- from .base_wrapper import BaseWrapper, ResponseType
9
+ from ..models import Usage, TokenUsageStats
10
+ from ..base_wrapper import BaseWrapper, ResponseType
11
+ from .AsyncStreamInterceptor import AsyncStreamInterceptor
11
12
 
12
13
  logger = logging.getLogger(__name__)
13
14
 
@@ -87,37 +88,54 @@ class OpenAIWrapper(BaseOpenAIWrapper):
87
88
 
88
89
 
89
90
  class AsyncOpenAIWrapper(BaseOpenAIWrapper):
90
- async def create(self, *args: Any, execution_id: Optional[str] = None, **kwargs: Any) -> Union[ChatCompletion, AsyncIterator[ChatCompletion]]:
91
- """Create a chat completion and log token usage."""
91
+ async def create(
92
+ self,
93
+ *args: Any,
94
+ execution_id: Optional[str] = None,
95
+ **kwargs: Any
96
+ ) -> Union[ChatCompletion, AsyncIterator[ChatCompletionChunk]]:
97
+ """
98
+ Create a chat completion and log token usage.
99
+ """
92
100
  logger.debug("Creating chat completion with args: %s, kwargs: %s", args, kwargs)
93
-
94
- if kwargs.get('stream', False):
95
- response = await self.client.chat.completions.create(*args, **kwargs)
96
- return self._wrap_streaming_response(response, execution_id)
97
-
101
+
102
+ # If user wants a stream, return an interceptor
103
+ if kwargs.get("stream", False):
104
+ base_stream = await self.client.chat.completions.create(*args, **kwargs)
105
+
106
+ # Define a callback that will get called once the stream ends
107
+ def usage_callback(chunks):
108
+ # Mimic your old logic to gather usage from chunk.usage
109
+ # e.g. ChatCompletionChunk.usage
110
+ # Then call self._log_usage(...)
111
+ if not chunks:
112
+ return
113
+ # Build usage_data from the first chunk's model
114
+ usage_data = TokenUsageStats(
115
+ model=chunks[0].model,
116
+ usage=Usage(),
117
+ )
118
+ # Sum up usage from all chunks
119
+ for ch in chunks:
120
+ if ch.usage:
121
+ usage_data.usage.prompt_tokens += ch.usage.prompt_tokens
122
+ usage_data.usage.completion_tokens += ch.usage.completion_tokens
123
+ usage_data.usage.total_tokens += ch.usage.total_tokens
124
+
125
+ self._log_usage(usage_data, execution_id=execution_id)
126
+
127
+ # Return the interceptor that wraps the real AsyncStream
128
+ return AsyncStreamInterceptor(
129
+ base_stream=base_stream,
130
+ usage_callback=usage_callback,
131
+ )
132
+
133
+ # Non-streaming path remains unchanged
98
134
  response = await self.client.chat.completions.create(*args, **kwargs)
99
135
  usage_data = self._process_response_usage(response)
100
136
  if usage_data:
101
137
  self._log_usage(usage_data, execution_id=execution_id)
102
138
  return response
103
-
104
- async def _wrap_streaming_response(self, response_iter: AsyncStream[ChatCompletionChunk], execution_id: Optional[str]) -> AsyncIterator[ChatCompletionChunk]:
105
- """Wrap streaming response to capture final usage stats"""
106
- chunks_with_usage = []
107
- async for chunk in response_iter:
108
- if isinstance(chunk, ChatCompletionChunk) and chunk.usage is not None:
109
- chunks_with_usage.append(chunk)
110
- yield chunk
111
-
112
- if len(chunks_with_usage) > 0:
113
- usage_data: TokenUsageStats = TokenUsageStats(model=chunks_with_usage[0].model, usage=Usage())
114
- for chunk in chunks_with_usage:
115
- usage_data.usage.prompt_tokens += chunk.usage.prompt_tokens
116
- usage_data.usage.completion_tokens += chunk.usage.completion_tokens
117
- usage_data.usage.total_tokens += chunk.usage.total_tokens
118
-
119
- self._log_usage(usage_data, execution_id=execution_id)
120
-
121
139
  @overload
122
140
  def tokenator_openai(
123
141
  client: OpenAI,
@@ -147,5 +165,3 @@ def tokenator_openai(
147
165
  return AsyncOpenAIWrapper(client=client, db_path=db_path)
148
166
 
149
167
  raise ValueError("Client must be an instance of OpenAI or AsyncOpenAI")
150
-
151
- __all__ = ["tokenator_openai"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: tokenator
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: Token usage tracking wrapper for LLMs
5
5
  License: MIT
6
6
  Author: Ujjwal Maheshwari
@@ -27,7 +27,7 @@ Have you ever wondered about :
27
27
  - How much does it cost to do run a complex AI workflow with multiple LLM providers?
28
28
  - How much money did I spent today on development?
29
29
 
30
- Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes and track your LLM usage.
30
+ Afraid not, tokenator is here! With tokenator's easy to use API, you can start tracking LLM usage in a matter of minutes.
31
31
 
32
32
  Get started with just 3 lines of code!
33
33
 
@@ -80,7 +80,32 @@ cost.last_day("google")
80
80
  ### Example `cost` object
81
81
 
82
82
  ```json
83
-
83
+ # print(cost.last_hour().model_dump_json(indent=4))
84
+
85
+ usage : {
86
+ "total_cost": 0.0004,
87
+ "total_tokens": 79,
88
+ "prompt_tokens": 52,
89
+ "completion_tokens": 27,
90
+ "providers": [
91
+ {
92
+ "total_cost": 0.0004,
93
+ "total_tokens": 79,
94
+ "prompt_tokens": 52,
95
+ "completion_tokens": 27,
96
+ "provider": "openai",
97
+ "models": [
98
+ {
99
+ "total_cost": 0.0004,
100
+ "total_tokens": 79,
101
+ "prompt_tokens": 52,
102
+ "completion_tokens": 27,
103
+ "model": "gpt-4o-2024-08-06"
104
+ }
105
+ ]
106
+ }
107
+ ]
108
+ }
84
109
  ```
85
110
 
86
111
  ## Features
@@ -1,17 +1,18 @@
1
- tokenator/__init__.py,sha256=ZKe0zMGa_AqOeXUVgYqivUavht_byk03XNFEvAnxqsA,576
1
+ tokenator/__init__.py,sha256=mYwK5EJTlbh_7WvylzxXcL-yzWe_fESSL6FLrlY1qck,583
2
2
  tokenator/base_wrapper.py,sha256=vSu_pStKYulho7_5g0jMCNf84KRxC4kTKep0v8YE61M,2377
3
3
  tokenator/client_anthropic.py,sha256=1ejWIZBxtk-mWTVaKWeMUvS2hZ_Dn-vNKYa3yopdjAU,6714
4
- tokenator/client_openai.py,sha256=_4jvchKzpCFhpioMZTYIWV7_ephQp1abMCtswUDJv1M,6339
5
4
  tokenator/create_migrations.py,sha256=n1OVbWrdwvBdaN-Aqqt1gLCPQidfoQfeJtGsab_epGk,746
6
5
  tokenator/migrations/env.py,sha256=LR_hONDa8Saiq9CyNUpH8kZCi5PtXLaDlfABs_CePkk,1415
7
6
  tokenator/migrations/script.py.mako,sha256=nJL-tbLQE0Qy4P9S4r4ntNAcikPtoFUlvXe6xvm9ot8,635
8
7
  tokenator/migrations/versions/f6f1f2437513_initial_migration.py,sha256=DvHcjnREmUHZVX9q1e6PS4wNK_d4qGw-8pz0eS4_3mE,1860
9
8
  tokenator/migrations.py,sha256=BFgZRsdIx-Qs_WwDaH6cyi2124mLf5hA8VrIlW7f7Mg,1134
10
9
  tokenator/models.py,sha256=EprE_MMJxDS-YXlcIQLZzfekH7xTYbeOC3bx3B2osVw,1171
10
+ tokenator/openai/AsyncStreamInterceptor.py,sha256=estfEFBFyo5BWqTNwHlCZ-wE0dRjtGeyQ0ihBeW3jrU,2842
11
+ tokenator/openai/client_openai.py,sha256=q-0abTq54zRORPLeushdHx1UYq-hOAlp6qY8wAOP2GQ,6682
11
12
  tokenator/schemas.py,sha256=V7NYfY9eZvH3J6uOwXJz4dSAU6WYzINRnfFi1wWsTcc,2280
12
13
  tokenator/usage.py,sha256=aHjGwzDzaiVznahNk5HqVyk3IxDo5FtFVfOUCeE7DZ4,7833
13
14
  tokenator/utils.py,sha256=5mDiGHgt4koCY0onHwkRjwZIuAgP6QvrDZCwD20Sdk8,1969
14
- tokenator-0.1.8.dist-info/LICENSE,sha256=wdG-B6-ODk8RQ4jq5uXSn0w1UWTzCH_MMyvh7AwtGns,1074
15
- tokenator-0.1.8.dist-info/METADATA,sha256=1xgNdiPKTJlnBCPH6iMfi7-LoOl-t9soFzH_5V_eYIk,2444
16
- tokenator-0.1.8.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
17
- tokenator-0.1.8.dist-info/RECORD,,
15
+ tokenator-0.1.9.dist-info/LICENSE,sha256=wdG-B6-ODk8RQ4jq5uXSn0w1UWTzCH_MMyvh7AwtGns,1074
16
+ tokenator-0.1.9.dist-info/METADATA,sha256=A7x7gEjbTwOBoR7mxGiHKiZVvKL8ZD6ecL7Wd0y6jfM,3093
17
+ tokenator-0.1.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
18
+ tokenator-0.1.9.dist-info/RECORD,,