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.

Files changed (36) hide show
  1. chatlas/__init__.py +21 -9
  2. chatlas/_auto.py +9 -9
  3. chatlas/_chat.py +38 -9
  4. chatlas/{_anthropic.py → _provider_anthropic.py} +13 -5
  5. chatlas/_provider_cloudflare.py +165 -0
  6. chatlas/{_databricks.py → _provider_databricks.py} +13 -2
  7. chatlas/_provider_deepseek.py +171 -0
  8. chatlas/{_github.py → _provider_github.py} +2 -2
  9. chatlas/{_google.py → _provider_google.py} +5 -5
  10. chatlas/{_groq.py → _provider_groq.py} +2 -2
  11. chatlas/_provider_huggingface.py +155 -0
  12. chatlas/_provider_mistral.py +181 -0
  13. chatlas/{_ollama.py → _provider_ollama.py} +2 -2
  14. chatlas/{_openai.py → _provider_openai.py} +28 -9
  15. chatlas/_provider_openrouter.py +149 -0
  16. chatlas/{_perplexity.py → _provider_perplexity.py} +2 -2
  17. chatlas/_provider_portkey.py +123 -0
  18. chatlas/{_snowflake.py → _provider_snowflake.py} +3 -3
  19. chatlas/_tokens.py +27 -12
  20. chatlas/_turn.py +3 -4
  21. chatlas/_typing_extensions.py +3 -3
  22. chatlas/_version.py +16 -3
  23. chatlas/data/prices.json +2769 -163
  24. chatlas/types/__init__.py +3 -3
  25. chatlas/types/anthropic/_client.py +1 -1
  26. chatlas/types/anthropic/_client_bedrock.py +1 -1
  27. chatlas/types/anthropic/_submit.py +5 -5
  28. chatlas/types/google/_submit.py +23 -29
  29. chatlas/types/openai/_client.py +1 -1
  30. chatlas/types/openai/_client_azure.py +1 -1
  31. chatlas/types/openai/_submit.py +28 -3
  32. {chatlas-0.9.1.dist-info → chatlas-0.10.0.dist-info}/METADATA +4 -4
  33. chatlas-0.10.0.dist-info/RECORD +54 -0
  34. chatlas-0.9.1.dist-info/RECORD +0 -48
  35. {chatlas-0.9.1.dist-info → chatlas-0.10.0.dist-info}/WHEEL +0 -0
  36. {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, name: str, model: str, input_tokens: int, output_tokens: int
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
- "cost": compute_cost(name, model, input_tokens, output_tokens),
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
- price = compute_cost(name, model, input_tokens, output_tokens)
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(provider.name, provider.model, tokens[0], tokens[1])
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["output"] / 1e6)
150
- return input_price + output_price
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 2 representing the number of input and output
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
-
@@ -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__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
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.9.1'
21
- __version_tuple__ = version_tuple = (0, 9, 1)
31
+ __version__ = version = '0.10.0'
32
+ __version_tuple__ = version_tuple = (0, 10, 0)
33
+
34
+ __commit_id__ = commit_id = None