From e48da5157a6a73a814064bc86ffd2eecfb2a968c Mon Sep 17 00:00:00 2001 From: Carson Date: Tue, 4 Nov 2025 18:09:21 -0600 Subject: [PATCH 1/7] feat: Add service_tier paramater to ChatOpenAI() --- chatlas/_provider_openai.py | 15 +++++++++++++++ chatlas/_provider_openai_azure.py | 17 ++++++++++++++++- tests/test_provider_openai.py | 5 +++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index 0b21ee4c..cf24880a 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -48,6 +48,9 @@ def ChatOpenAI( model: "Optional[ResponsesModel | str]" = None, api_key: Optional[str] = None, base_url: str = "https://api.openai.com/v1", + service_tier: Optional[ + Literal["auto", "default", "flex", "scale", "priority"] + ] = None, kwargs: Optional["ChatClientArgs"] = None, ) -> Chat["SubmitInputArgs", Response]: """ @@ -92,6 +95,13 @@ def ChatOpenAI( variable. base_url The base URL to the endpoint; the default uses OpenAI. + service_tier + Request a specific service tier. Options: + - `"auto"` (default): uses the service tier configured in Project settings. + - `"default"`: standard pricing and performance. + - `"flex"`: slower and cheaper. + - `"scale"`: batch-like pricing for high-volume use. + - `"priority"`: faster and more expensive. kwargs Additional arguments to pass to the `openai.OpenAI()` client constructor. @@ -145,6 +155,10 @@ def ChatOpenAI( if model is None: model = log_model_default("gpt-4.1") + kwargs_chat: "SubmitInputArgs" = {} + if service_tier is not None: + kwargs_chat["service_tier"] = service_tier + return Chat( provider=OpenAIProvider( api_key=api_key, @@ -153,6 +167,7 @@ def ChatOpenAI( kwargs=kwargs, ), system_prompt=system_prompt, + kwargs_chat=kwargs_chat, ) diff --git a/chatlas/_provider_openai_azure.py b/chatlas/_provider_openai_azure.py index 7a1b78bc..0f2bc39d 100644 --- a/chatlas/_provider_openai_azure.py +++ b/chatlas/_provider_openai_azure.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING, Literal, Optional from openai import AsyncAzureOpenAI, AzureOpenAI from openai.types.chat import ChatCompletion @@ -21,6 +21,9 @@ def ChatAzureOpenAI( api_version: str, api_key: Optional[str] = None, system_prompt: Optional[str] = None, + service_tier: Optional[ + Literal["auto", "default", "flex", "scale", "priority"] + ] = None, kwargs: Optional["ChatAzureClientArgs"] = None, ) -> Chat["SubmitInputArgs", ChatCompletion]: """ @@ -62,6 +65,13 @@ def ChatAzureOpenAI( variable. system_prompt A system prompt to set the behavior of the assistant. + service_tier + Request a specific service tier. Options: + - `"auto"` (default): uses the service tier configured in Project settings. + - `"default"`: standard pricing and performance. + - `"flex"`: slower and cheaper. + - `"scale"`: batch-like pricing for high-volume use. + - `"priority"`: faster and more expensive. kwargs Additional arguments to pass to the `openai.AzureOpenAI()` client constructor. @@ -71,6 +81,10 @@ def ChatAzureOpenAI( A Chat object. """ + kwargs_chat: "SubmitInputArgs" = {} + if service_tier is not None: + kwargs_chat["service_tier"] = service_tier + return Chat( provider=OpenAIAzureProvider( endpoint=endpoint, @@ -80,6 +94,7 @@ def ChatAzureOpenAI( kwargs=kwargs, ), system_prompt=system_prompt, + kwargs_chat=kwargs_chat, ) diff --git a/tests/test_provider_openai.py b/tests/test_provider_openai.py index a36e14dc..93908e93 100644 --- a/tests/test_provider_openai.py +++ b/tests/test_provider_openai.py @@ -105,3 +105,8 @@ def test_openai_custom_http_client(): def test_openai_list_models(): assert_list_models(ChatOpenAI) + + +def test_openai_service_tier(): + chat = ChatOpenAI(service_tier="flex") + assert chat.kwargs_chat.get("service_tier") == "flex" \ No newline at end of file From cbbc716c8445c153ef0fdc73fbbdf9b5b5195154 Mon Sep 17 00:00:00 2001 From: Carson Date: Wed, 5 Nov 2025 11:47:08 -0600 Subject: [PATCH 2/7] Include some better error handling --- chatlas/_provider_openai.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index cf24880a..ba8739ee 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -276,6 +276,16 @@ def stream_text(self, chunk): def stream_merge_chunks(self, completion, chunk): if chunk.type == "response.completed": return chunk.response + elif chunk.type == "response.failed": + error = chunk.response.error + if error is None: + msg = "Request failed with an unknown error." + else: + msg = f"Request failed ({error.code}): {error.message}" + raise RuntimeError(msg) + elif chunk.type == "error": + raise RuntimeError(f"Request errored: {chunk.message}") + # Since this value won't actually be used, we can lie about the type return cast(Response, None) From 663b960e5f46be2e3b06e3513b34f96ef8d2852a Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Dec 2025 15:26:31 -0600 Subject: [PATCH 3/7] Fix typing issue --- chatlas/_provider_openai.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index ba8739ee..75a5b7bf 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -2,7 +2,7 @@ import base64 import warnings -from typing import TYPE_CHECKING, Optional, cast +from typing import TYPE_CHECKING, Literal, Optional, cast import orjson from openai.types.responses import Response, ResponseStreamEvent From dc85114e17c4ba7dd1dbbf4686426e6222fe2b33 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Dec 2025 16:08:17 -0600 Subject: [PATCH 4/7] Update to latest --- chatlas/_provider_openai.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index af6ea703..889d34b6 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -321,12 +321,9 @@ def value_cost( if tokens is None: return None - # Extract service_tier from completion if available - variant = "" - if completion is not None: - variant = getattr(completion, "service_tier", None) or "" - - return get_token_cost(self.name, self.model, tokens, variant) + return get_token_cost( + self.name, self.model, tokens, completion.service_tier or "" + ) def batch_result_turn(self, result, has_data_model: bool = False): response = BatchResult.model_validate(result).response From be69cd3978162da090323b08c19f99e8ba881175 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Dec 2025 16:13:36 -0600 Subject: [PATCH 5/7] Add a test that service_tier influences price --- tests/test_provider_openai.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/test_provider_openai.py b/tests/test_provider_openai.py index 93908e93..42bcafab 100644 --- a/tests/test_provider_openai.py +++ b/tests/test_provider_openai.py @@ -109,4 +109,27 @@ def test_openai_list_models(): def test_openai_service_tier(): chat = ChatOpenAI(service_tier="flex") - assert chat.kwargs_chat.get("service_tier") == "flex" \ No newline at end of file + assert chat.kwargs_chat.get("service_tier") == "flex" + + +def test_openai_service_tier_affects_pricing(): + from chatlas._tokens import get_token_cost + + chat = ChatOpenAI(service_tier="priority") + chat.chat("What is 1+1?") + + turn = chat.get_last_turn() + assert turn is not None + assert turn.tokens is not None + assert turn.cost is not None + + # Verify that cost was calculated using priority pricing + tokens = turn.tokens + priority_cost = get_token_cost("OpenAI", chat.provider.model, tokens, "priority") + assert priority_cost is not None + assert turn.cost == priority_cost + + # Verify priority pricing is more expensive than default + default_cost = get_token_cost("OpenAI", chat.provider.model, tokens, "") + assert default_cost is not None + assert turn.cost > default_cost \ No newline at end of file From 30d6442a4a4e170b58e04cd156ec82354105fc77 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Dec 2025 16:15:59 -0600 Subject: [PATCH 6/7] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb7e8559..fb1ca385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Added support for built-in provider tools via a new `ToolBuiltIn` class. This enables provider-specific functionality like OpenAI's image generation to be registered and used as tools. Built-in tools pass raw provider definitions directly to the API rather than wrapping Python functions. (#214) * `ChatGoogle()` gains basic support for image generation. (#214) +* `ChatOpenAI()` and `ChatAzureOpenAI()` gain a new `service_tier` parameter to request a specific service tier (e.g., `"flex"` for slower/cheaper or `"priority"` for faster/more expensive). (#204) ### Changes From accc41d713a80bfd966b7482262f5bbdbaefc60e Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 29 Dec 2025 16:19:13 -0600 Subject: [PATCH 7/7] fix: handle completion of None --- chatlas/_provider_openai.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/chatlas/_provider_openai.py b/chatlas/_provider_openai.py index 889d34b6..e35eef4c 100644 --- a/chatlas/_provider_openai.py +++ b/chatlas/_provider_openai.py @@ -321,9 +321,11 @@ def value_cost( if tokens is None: return None - return get_token_cost( - self.name, self.model, tokens, completion.service_tier or "" - ) + service_tier = "" + if completion is not None: + service_tier = completion.service_tier or "" + + return get_token_cost(self.name, self.model, tokens, service_tier) def batch_result_turn(self, result, has_data_model: bool = False): response = BatchResult.model_validate(result).response