chatlas 0.9.1__py3-none-any.whl → 0.10.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.
Potentially problematic release.
This version of chatlas might be problematic. Click here for more details.
- chatlas/__init__.py +21 -9
- chatlas/_auto.py +9 -9
- chatlas/_chat.py +38 -9
- chatlas/{_anthropic.py → _provider_anthropic.py} +13 -5
- chatlas/_provider_cloudflare.py +165 -0
- chatlas/{_databricks.py → _provider_databricks.py} +13 -2
- chatlas/_provider_deepseek.py +171 -0
- chatlas/{_github.py → _provider_github.py} +2 -2
- chatlas/{_google.py → _provider_google.py} +5 -5
- chatlas/{_groq.py → _provider_groq.py} +2 -2
- chatlas/_provider_huggingface.py +155 -0
- chatlas/_provider_mistral.py +181 -0
- chatlas/{_ollama.py → _provider_ollama.py} +2 -2
- chatlas/{_openai.py → _provider_openai.py} +28 -9
- chatlas/_provider_openrouter.py +149 -0
- chatlas/{_perplexity.py → _provider_perplexity.py} +2 -2
- chatlas/_provider_portkey.py +123 -0
- chatlas/{_snowflake.py → _provider_snowflake.py} +3 -3
- chatlas/_tokens.py +27 -12
- chatlas/_turn.py +3 -4
- chatlas/_typing_extensions.py +3 -3
- chatlas/_version.py +16 -3
- chatlas/data/prices.json +2769 -163
- chatlas/types/__init__.py +3 -3
- chatlas/types/anthropic/_client.py +1 -1
- chatlas/types/anthropic/_client_bedrock.py +1 -1
- chatlas/types/anthropic/_submit.py +5 -5
- chatlas/types/google/_submit.py +23 -29
- chatlas/types/openai/_client.py +1 -1
- chatlas/types/openai/_client_azure.py +1 -1
- chatlas/types/openai/_submit.py +28 -3
- {chatlas-0.9.1.dist-info → chatlas-0.10.0.dist-info}/METADATA +4 -4
- chatlas-0.10.0.dist-info/RECORD +54 -0
- chatlas-0.9.1.dist-info/RECORD +0 -48
- {chatlas-0.9.1.dist-info → chatlas-0.10.0.dist-info}/WHEEL +0 -0
- {chatlas-0.9.1.dist-info → chatlas-0.10.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import TYPE_CHECKING, Optional
|
|
5
|
+
|
|
6
|
+
from ._chat import Chat
|
|
7
|
+
from ._logging import log_model_default
|
|
8
|
+
from ._provider_openai import OpenAIProvider
|
|
9
|
+
from ._utils import drop_none
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ._provider_openai import ChatCompletion
|
|
13
|
+
from .types.openai import ChatClientArgs, SubmitInputArgs
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def ChatPortkey(
|
|
17
|
+
*,
|
|
18
|
+
system_prompt: Optional[str] = None,
|
|
19
|
+
model: Optional[str] = None,
|
|
20
|
+
api_key: Optional[str] = None,
|
|
21
|
+
virtual_key: Optional[str] = None,
|
|
22
|
+
base_url: str = "https://api.portkey.ai/v1",
|
|
23
|
+
kwargs: Optional["ChatClientArgs"] = None,
|
|
24
|
+
) -> Chat["SubmitInputArgs", ChatCompletion]:
|
|
25
|
+
"""
|
|
26
|
+
Chat with a model hosted on PortkeyAI
|
|
27
|
+
|
|
28
|
+
[PortkeyAI](https://portkey.ai/docs/product/ai-gateway/universal-api)
|
|
29
|
+
provides an interface (AI Gateway) to connect through its Universal API to a
|
|
30
|
+
variety of LLMs providers with a single endpoint.
|
|
31
|
+
|
|
32
|
+
Prerequisites
|
|
33
|
+
-------------
|
|
34
|
+
|
|
35
|
+
::: {.callout-note}
|
|
36
|
+
## Portkey credentials
|
|
37
|
+
|
|
38
|
+
Follow the instructions at <https://portkey.ai/docs/introduction/make-your-first-request>
|
|
39
|
+
to get started making requests to PortkeyAI. You will need to set the
|
|
40
|
+
`PORTKEY_API_KEY` environment variable to your Portkey API key, and optionally
|
|
41
|
+
the `PORTKEY_VIRTUAL_KEY` environment variable to your virtual key.
|
|
42
|
+
:::
|
|
43
|
+
|
|
44
|
+
Examples
|
|
45
|
+
--------
|
|
46
|
+
```python
|
|
47
|
+
import os
|
|
48
|
+
from chatlas import ChatPortkey
|
|
49
|
+
|
|
50
|
+
chat = ChatPortkey(api_key=os.getenv("PORTKEY_API_KEY"))
|
|
51
|
+
chat.chat("What is the capital of France?")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
system_prompt
|
|
57
|
+
A system prompt to set the behavior of the assistant.
|
|
58
|
+
model
|
|
59
|
+
The model to use for the chat. The default, None, will pick a reasonable
|
|
60
|
+
default, and warn you about it. We strongly recommend explicitly
|
|
61
|
+
choosing a model for all but the most casual use.
|
|
62
|
+
api_key
|
|
63
|
+
The API key to use for authentication. You generally should not supply
|
|
64
|
+
this directly, but instead set the `PORTKEY_API_KEY` environment variable.
|
|
65
|
+
virtual_key
|
|
66
|
+
An (optional) virtual identifier, storing the LLM provider's API key. See
|
|
67
|
+
[documentation](https://portkey.ai/docs/product/ai-gateway/virtual-keys).
|
|
68
|
+
You generally should not supply this directly, but instead set the
|
|
69
|
+
`PORTKEY_VIRTUAL_KEY` environment variable.
|
|
70
|
+
base_url
|
|
71
|
+
The base URL for the Portkey API. The default is suitable for most users.
|
|
72
|
+
kwargs
|
|
73
|
+
Additional arguments to pass to the OpenAIProvider, such as headers or
|
|
74
|
+
other client configuration options.
|
|
75
|
+
|
|
76
|
+
Returns
|
|
77
|
+
-------
|
|
78
|
+
Chat
|
|
79
|
+
A chat object that retains the state of the conversation.
|
|
80
|
+
|
|
81
|
+
Notes
|
|
82
|
+
-----
|
|
83
|
+
This function is a lightweight wrapper around [](`~chatlas.ChatOpenAI`) with
|
|
84
|
+
the defaults tweaked for PortkeyAI.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
if model is None:
|
|
88
|
+
model = log_model_default("gpt-4.1")
|
|
89
|
+
if api_key is None:
|
|
90
|
+
api_key = os.getenv("PORTKEY_API_KEY")
|
|
91
|
+
|
|
92
|
+
kwargs2 = add_default_headers(
|
|
93
|
+
kwargs or {},
|
|
94
|
+
api_key=api_key,
|
|
95
|
+
virtual_key=virtual_key,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return Chat(
|
|
99
|
+
provider=OpenAIProvider(
|
|
100
|
+
api_key=api_key,
|
|
101
|
+
model=model,
|
|
102
|
+
base_url=base_url,
|
|
103
|
+
name="Portkey",
|
|
104
|
+
kwargs=kwargs2,
|
|
105
|
+
),
|
|
106
|
+
system_prompt=system_prompt,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def add_default_headers(
|
|
111
|
+
kwargs: "ChatClientArgs",
|
|
112
|
+
api_key: Optional[str] = None,
|
|
113
|
+
virtual_key: Optional[str] = None,
|
|
114
|
+
) -> "ChatClientArgs":
|
|
115
|
+
headers = kwargs.get("default_headers", None)
|
|
116
|
+
default_headers = drop_none(
|
|
117
|
+
{
|
|
118
|
+
"x-portkey-api-key": api_key,
|
|
119
|
+
"x-portkey-virtual-key": virtual_key,
|
|
120
|
+
**(headers or {}),
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
return {"default_headers": default_headers, **kwargs}
|
|
@@ -537,12 +537,12 @@ class SnowflakeProvider(
|
|
|
537
537
|
arguments=params,
|
|
538
538
|
)
|
|
539
539
|
)
|
|
540
|
-
|
|
540
|
+
# Snowflake does not currently appear to support caching, so we set cached tokens to 0
|
|
541
541
|
usage = completion.usage
|
|
542
542
|
if usage is None:
|
|
543
|
-
tokens = (0, 0)
|
|
543
|
+
tokens = (0, 0, 0)
|
|
544
544
|
else:
|
|
545
|
-
tokens = (usage.prompt_tokens or 0, usage.completion_tokens or 0)
|
|
545
|
+
tokens = (usage.prompt_tokens or 0, usage.completion_tokens or 0, 0)
|
|
546
546
|
|
|
547
547
|
tokens_log(self, tokens)
|
|
548
548
|
|
chatlas/_tokens.py
CHANGED
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
import orjson
|
|
9
9
|
|
|
10
10
|
from ._logging import logger
|
|
11
|
-
from ._typing_extensions import TypedDict
|
|
11
|
+
from ._typing_extensions import NotRequired, TypedDict
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from ._provider import Provider
|
|
@@ -23,6 +23,7 @@ class TokenUsage(TypedDict):
|
|
|
23
23
|
model: str
|
|
24
24
|
input: int
|
|
25
25
|
output: int
|
|
26
|
+
cached_input: int
|
|
26
27
|
cost: float | None
|
|
27
28
|
|
|
28
29
|
|
|
@@ -32,11 +33,16 @@ class ThreadSafeTokenCounter:
|
|
|
32
33
|
self._tokens: dict[str, TokenUsage] = {}
|
|
33
34
|
|
|
34
35
|
def log_tokens(
|
|
35
|
-
self,
|
|
36
|
+
self,
|
|
37
|
+
name: str,
|
|
38
|
+
model: str,
|
|
39
|
+
input_tokens: int,
|
|
40
|
+
output_tokens: int,
|
|
41
|
+
cached_tokens: int,
|
|
36
42
|
) -> None:
|
|
37
43
|
logger.info(
|
|
38
44
|
f"Provider '{name}' generated a response of {output_tokens} tokens "
|
|
39
|
-
f"from an input of {input_tokens} tokens."
|
|
45
|
+
f"from an input of {input_tokens} tokens and {cached_tokens} cached input tokens."
|
|
40
46
|
)
|
|
41
47
|
|
|
42
48
|
with self._lock:
|
|
@@ -46,12 +52,18 @@ class ThreadSafeTokenCounter:
|
|
|
46
52
|
"model": model,
|
|
47
53
|
"input": input_tokens,
|
|
48
54
|
"output": output_tokens,
|
|
49
|
-
"
|
|
55
|
+
"cached_input": cached_tokens,
|
|
56
|
+
"cost": compute_cost(
|
|
57
|
+
name, model, input_tokens, output_tokens, cached_tokens
|
|
58
|
+
),
|
|
50
59
|
}
|
|
51
60
|
else:
|
|
52
61
|
self._tokens[name]["input"] += input_tokens
|
|
53
62
|
self._tokens[name]["output"] += output_tokens
|
|
54
|
-
|
|
63
|
+
self._tokens[name]["cached_input"] += cached_tokens
|
|
64
|
+
price = compute_cost(
|
|
65
|
+
name, model, input_tokens, output_tokens, cached_tokens
|
|
66
|
+
)
|
|
55
67
|
if price is not None:
|
|
56
68
|
cost = self._tokens[name]["cost"]
|
|
57
69
|
if cost is None:
|
|
@@ -71,11 +83,13 @@ class ThreadSafeTokenCounter:
|
|
|
71
83
|
_token_counter = ThreadSafeTokenCounter()
|
|
72
84
|
|
|
73
85
|
|
|
74
|
-
def tokens_log(provider: "Provider", tokens: tuple[int, int]) -> None:
|
|
86
|
+
def tokens_log(provider: "Provider", tokens: tuple[int, int, int]) -> None:
|
|
75
87
|
"""
|
|
76
88
|
Log token usage for a provider in a thread-safe manner.
|
|
77
89
|
"""
|
|
78
|
-
_token_counter.log_tokens(
|
|
90
|
+
_token_counter.log_tokens(
|
|
91
|
+
provider.name, provider.model, tokens[0], tokens[1], tokens[2]
|
|
92
|
+
)
|
|
79
93
|
|
|
80
94
|
|
|
81
95
|
def tokens_reset() -> None:
|
|
@@ -95,11 +109,11 @@ class TokenPrice(TypedDict):
|
|
|
95
109
|
"""The provider name (e.g., "OpenAI", "Anthropic", etc.)"""
|
|
96
110
|
model: str
|
|
97
111
|
"""The model name (e.g., "gpt-3.5-turbo", "claude-2", etc.)"""
|
|
98
|
-
cached_input: float
|
|
112
|
+
cached_input: NotRequired[float]
|
|
99
113
|
"""The cost per user token in USD per million tokens for cached input"""
|
|
100
114
|
input: float
|
|
101
115
|
"""The cost per user token in USD per million tokens"""
|
|
102
|
-
output: float
|
|
116
|
+
output: NotRequired[float]
|
|
103
117
|
"""The cost per assistant token in USD per million tokens"""
|
|
104
118
|
|
|
105
119
|
|
|
@@ -132,7 +146,7 @@ def get_token_pricing(name: str, model: str) -> TokenPrice | None:
|
|
|
132
146
|
|
|
133
147
|
|
|
134
148
|
def compute_cost(
|
|
135
|
-
name: str, model: str, input_tokens: int, output_tokens: int
|
|
149
|
+
name: str, model: str, input_tokens: int, output_tokens: int, cached_tokens: int = 0
|
|
136
150
|
) -> float | None:
|
|
137
151
|
"""
|
|
138
152
|
Compute the cost of a turn.
|
|
@@ -146,8 +160,9 @@ def compute_cost(
|
|
|
146
160
|
if price is None:
|
|
147
161
|
return None
|
|
148
162
|
input_price = input_tokens * (price["input"] / 1e6)
|
|
149
|
-
output_price = output_tokens * (price
|
|
150
|
-
|
|
163
|
+
output_price = output_tokens * (price.get("output", 0) / 1e6)
|
|
164
|
+
cached_price = cached_tokens * (price.get("cached_input", 0) / 1e6)
|
|
165
|
+
return input_price + output_price + cached_price
|
|
151
166
|
|
|
152
167
|
|
|
153
168
|
def token_usage() -> list[TokenUsage] | None:
|
chatlas/_turn.py
CHANGED
|
@@ -55,7 +55,7 @@ class Turn(BaseModel, Generic[CompletionT]):
|
|
|
55
55
|
contents
|
|
56
56
|
A list of [](`~chatlas.types.Content`) objects.
|
|
57
57
|
tokens
|
|
58
|
-
A numeric vector of length
|
|
58
|
+
A numeric vector of length 3 representing the number of input, output, and cached
|
|
59
59
|
tokens (respectively) used in this turn. Currently only recorded for
|
|
60
60
|
assistant turns.
|
|
61
61
|
finish_reason
|
|
@@ -69,7 +69,7 @@ class Turn(BaseModel, Generic[CompletionT]):
|
|
|
69
69
|
|
|
70
70
|
role: Literal["user", "assistant", "system"]
|
|
71
71
|
contents: list[ContentUnion] = Field(default_factory=list)
|
|
72
|
-
tokens: Optional[tuple[int, int]] = None
|
|
72
|
+
tokens: Optional[tuple[int, int, int]] = None
|
|
73
73
|
finish_reason: Optional[str] = None
|
|
74
74
|
completion: Optional[CompletionT] = Field(default=None, exclude=True)
|
|
75
75
|
|
|
@@ -80,7 +80,7 @@ class Turn(BaseModel, Generic[CompletionT]):
|
|
|
80
80
|
role: Literal["user", "assistant", "system"],
|
|
81
81
|
contents: str | Sequence[Content | str],
|
|
82
82
|
*,
|
|
83
|
-
tokens: Optional[tuple[int, int]] = None,
|
|
83
|
+
tokens: Optional[tuple[int, int, int]] = None,
|
|
84
84
|
finish_reason: Optional[str] = None,
|
|
85
85
|
completion: Optional[CompletionT] = None,
|
|
86
86
|
**kwargs,
|
|
@@ -134,4 +134,3 @@ def user_turn(*args: Content | str) -> Turn:
|
|
|
134
134
|
raise ValueError("Must supply at least one input.")
|
|
135
135
|
|
|
136
136
|
return Turn("user", args)
|
|
137
|
-
|
chatlas/_typing_extensions.py
CHANGED
|
@@ -14,13 +14,13 @@ else:
|
|
|
14
14
|
# they should both come from the same typing module.
|
|
15
15
|
# https://peps.python.org/pep-0655/#usage-in-python-3-11
|
|
16
16
|
if sys.version_info >= (3, 11):
|
|
17
|
-
from typing import Required, TypedDict
|
|
17
|
+
from typing import NotRequired, Required, TypedDict
|
|
18
18
|
else:
|
|
19
|
-
from typing_extensions import Required, TypedDict
|
|
19
|
+
from typing_extensions import NotRequired, Required, TypedDict
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
# The only purpose of the following line is so that pyright will put all of the
|
|
23
23
|
# conditional imports into the .pyi file when generating type stubs. Without this line,
|
|
24
24
|
# pyright will not include the above imports in the generated .pyi file, and it will
|
|
25
25
|
# result in a lot of red squiggles in user code.
|
|
26
|
-
_: "ParamSpec | TypeGuard | is_typeddict | Required | TypedDict" # type: ignore
|
|
26
|
+
_: "ParamSpec | TypeGuard | is_typeddict | NotRequired | Required | TypedDict" # type: ignore
|
chatlas/_version.py
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.
|
|
21
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.10.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 10, 0)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|