retab 0.0.35__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.
Files changed (111) hide show
  1. retab-0.0.35.dist-info/METADATA +417 -0
  2. retab-0.0.35.dist-info/RECORD +111 -0
  3. retab-0.0.35.dist-info/WHEEL +5 -0
  4. retab-0.0.35.dist-info/top_level.txt +1 -0
  5. uiform/__init__.py +4 -0
  6. uiform/_resource.py +28 -0
  7. uiform/_utils/__init__.py +0 -0
  8. uiform/_utils/ai_models.py +100 -0
  9. uiform/_utils/benchmarking copy.py +588 -0
  10. uiform/_utils/benchmarking.py +485 -0
  11. uiform/_utils/chat.py +332 -0
  12. uiform/_utils/display.py +443 -0
  13. uiform/_utils/json_schema.py +2161 -0
  14. uiform/_utils/mime.py +168 -0
  15. uiform/_utils/responses.py +163 -0
  16. uiform/_utils/stream_context_managers.py +52 -0
  17. uiform/_utils/usage/__init__.py +0 -0
  18. uiform/_utils/usage/usage.py +300 -0
  19. uiform/client.py +701 -0
  20. uiform/py.typed +0 -0
  21. uiform/resources/__init__.py +0 -0
  22. uiform/resources/consensus/__init__.py +3 -0
  23. uiform/resources/consensus/client.py +114 -0
  24. uiform/resources/consensus/completions.py +252 -0
  25. uiform/resources/consensus/completions_stream.py +278 -0
  26. uiform/resources/consensus/responses.py +325 -0
  27. uiform/resources/consensus/responses_stream.py +373 -0
  28. uiform/resources/deployments/__init__.py +9 -0
  29. uiform/resources/deployments/client.py +78 -0
  30. uiform/resources/deployments/endpoints.py +322 -0
  31. uiform/resources/deployments/links.py +452 -0
  32. uiform/resources/deployments/logs.py +211 -0
  33. uiform/resources/deployments/mailboxes.py +496 -0
  34. uiform/resources/deployments/outlook.py +531 -0
  35. uiform/resources/deployments/tests.py +158 -0
  36. uiform/resources/documents/__init__.py +3 -0
  37. uiform/resources/documents/client.py +255 -0
  38. uiform/resources/documents/extractions.py +441 -0
  39. uiform/resources/evals.py +812 -0
  40. uiform/resources/files.py +24 -0
  41. uiform/resources/finetuning.py +62 -0
  42. uiform/resources/jsonlUtils.py +1046 -0
  43. uiform/resources/models.py +45 -0
  44. uiform/resources/openai_example.py +22 -0
  45. uiform/resources/processors/__init__.py +3 -0
  46. uiform/resources/processors/automations/__init__.py +9 -0
  47. uiform/resources/processors/automations/client.py +78 -0
  48. uiform/resources/processors/automations/endpoints.py +317 -0
  49. uiform/resources/processors/automations/links.py +356 -0
  50. uiform/resources/processors/automations/logs.py +211 -0
  51. uiform/resources/processors/automations/mailboxes.py +435 -0
  52. uiform/resources/processors/automations/outlook.py +444 -0
  53. uiform/resources/processors/automations/tests.py +158 -0
  54. uiform/resources/processors/client.py +474 -0
  55. uiform/resources/prompt_optimization.py +76 -0
  56. uiform/resources/schemas.py +369 -0
  57. uiform/resources/secrets/__init__.py +9 -0
  58. uiform/resources/secrets/client.py +20 -0
  59. uiform/resources/secrets/external_api_keys.py +109 -0
  60. uiform/resources/secrets/webhook.py +62 -0
  61. uiform/resources/usage.py +271 -0
  62. uiform/types/__init__.py +0 -0
  63. uiform/types/ai_models.py +645 -0
  64. uiform/types/automations/__init__.py +0 -0
  65. uiform/types/automations/cron.py +58 -0
  66. uiform/types/automations/endpoints.py +21 -0
  67. uiform/types/automations/links.py +28 -0
  68. uiform/types/automations/mailboxes.py +60 -0
  69. uiform/types/automations/outlook.py +68 -0
  70. uiform/types/automations/webhooks.py +21 -0
  71. uiform/types/chat.py +8 -0
  72. uiform/types/completions.py +93 -0
  73. uiform/types/consensus.py +10 -0
  74. uiform/types/db/__init__.py +0 -0
  75. uiform/types/db/annotations.py +24 -0
  76. uiform/types/db/files.py +36 -0
  77. uiform/types/deployments/__init__.py +0 -0
  78. uiform/types/deployments/cron.py +59 -0
  79. uiform/types/deployments/endpoints.py +28 -0
  80. uiform/types/deployments/links.py +36 -0
  81. uiform/types/deployments/mailboxes.py +67 -0
  82. uiform/types/deployments/outlook.py +76 -0
  83. uiform/types/deployments/webhooks.py +21 -0
  84. uiform/types/documents/__init__.py +0 -0
  85. uiform/types/documents/correct_orientation.py +13 -0
  86. uiform/types/documents/create_messages.py +226 -0
  87. uiform/types/documents/extractions.py +297 -0
  88. uiform/types/evals.py +207 -0
  89. uiform/types/events.py +76 -0
  90. uiform/types/extractions.py +85 -0
  91. uiform/types/jobs/__init__.py +0 -0
  92. uiform/types/jobs/base.py +150 -0
  93. uiform/types/jobs/batch_annotation.py +22 -0
  94. uiform/types/jobs/evaluation.py +133 -0
  95. uiform/types/jobs/finetune.py +6 -0
  96. uiform/types/jobs/prompt_optimization.py +41 -0
  97. uiform/types/jobs/webcrawl.py +6 -0
  98. uiform/types/logs.py +231 -0
  99. uiform/types/mime.py +257 -0
  100. uiform/types/modalities.py +68 -0
  101. uiform/types/pagination.py +6 -0
  102. uiform/types/schemas/__init__.py +0 -0
  103. uiform/types/schemas/enhance.py +53 -0
  104. uiform/types/schemas/evaluate.py +55 -0
  105. uiform/types/schemas/generate.py +32 -0
  106. uiform/types/schemas/layout.py +58 -0
  107. uiform/types/schemas/object.py +631 -0
  108. uiform/types/schemas/templates.py +107 -0
  109. uiform/types/secrets/__init__.py +0 -0
  110. uiform/types/secrets/external_api_keys.py +22 -0
  111. uiform/types/standards.py +39 -0
uiform/_utils/mime.py ADDED
@@ -0,0 +1,168 @@
1
+ import base64
2
+ import hashlib
3
+ import io
4
+ import json
5
+ import mimetypes
6
+ from pathlib import Path
7
+ from typing import Literal, Sequence, TypeVar, get_args
8
+
9
+ import httpx
10
+ import PIL.Image
11
+ from pydantic import HttpUrl
12
+
13
+ from ..types.mime import MIMEData
14
+ from ..types.modalities import SUPPORTED_TYPES
15
+
16
+ T = TypeVar('T')
17
+
18
+
19
+ def generate_blake2b_hash_from_bytes(bytes_: bytes) -> str:
20
+ return hashlib.blake2b(bytes_, digest_size=8).hexdigest()
21
+
22
+
23
+ def generate_blake2b_hash_from_base64(base64_string: str) -> str:
24
+ return generate_blake2b_hash_from_bytes(base64.b64decode(base64_string))
25
+
26
+
27
+ def generate_blake2b_hash_from_string(input_string: str) -> str:
28
+ return generate_blake2b_hash_from_bytes(input_string.encode('utf-8'))
29
+
30
+
31
+ def generate_blake2b_hash_from_dict(input_dict: dict) -> str:
32
+ return generate_blake2b_hash_from_string(json.dumps(input_dict, sort_keys=True).strip())
33
+
34
+
35
+ def convert_pil_image_to_mime_data(image: PIL.Image.Image) -> MIMEData:
36
+ """Convert a PIL Image object to a MIMEData object.
37
+
38
+ Args:
39
+ image: PIL Image object to convert
40
+
41
+ Returns:
42
+ MIMEData object containing the image data
43
+ """
44
+ # Convert PIL image to base64 string
45
+ buffered = io.BytesIO()
46
+ choosen_format = image.format if (image.format and image.format.lower() in ['png', 'jpeg', 'gif', 'webp']) else "JPEG"
47
+ image.save(buffered, format=choosen_format)
48
+ base64_content = base64.b64encode(buffered.getvalue()).decode("utf-8")
49
+
50
+ content_hash = hashlib.sha256(base64_content.encode("utf-8")).hexdigest()
51
+
52
+ # Create MIMEData object
53
+ return MIMEData(filename=f"image_{content_hash}.{choosen_format.lower()}", url=f"data:image/{choosen_format.lower()};base64,{base64_content}")
54
+
55
+
56
+ def convert_mime_data_to_pil_image(mime_data: MIMEData) -> PIL.Image.Image:
57
+ """Convert a MIMEData object to a PIL Image object.
58
+
59
+ Args:
60
+ mime_data: MIMEData object containing image data
61
+
62
+ Returns:
63
+ PIL Image object
64
+
65
+ Raises:
66
+ ValueError: If the MIMEData object does not contain image data
67
+ """
68
+ if not mime_data.mime_type.startswith("image/"):
69
+ raise ValueError("MIMEData object does not contain image data")
70
+
71
+ # Decode base64 content to bytes
72
+ image_bytes = base64.b64decode(mime_data.content)
73
+
74
+ # Create PIL Image from bytes
75
+ image = PIL.Image.open(io.BytesIO(image_bytes))
76
+
77
+ return image
78
+
79
+
80
+ def prepare_mime_document(document: Path | str | bytes | io.IOBase | MIMEData | PIL.Image.Image | HttpUrl) -> MIMEData:
81
+ """
82
+ Convert documents (file paths or file-like objects) to MIMEData objects.
83
+
84
+ Args:
85
+ document: A path, string, bytes, or file-like object (IO[bytes])
86
+
87
+ Returns:
88
+ A MIMEData object
89
+ """
90
+ # Check if document is a HttpUrl (Pydantic type)
91
+
92
+ if isinstance(document, PIL.Image.Image):
93
+ return convert_pil_image_to_mime_data(document)
94
+
95
+ if isinstance(document, MIMEData):
96
+ return document
97
+
98
+ if isinstance(document, bytes):
99
+ # `document` is already the raw bytes
100
+ try:
101
+ import puremagic
102
+
103
+ extension = puremagic.from_string(document)
104
+ if extension.lower() in [".jpg", ".jpeg", ".jfif"]:
105
+ extension = ".jpeg"
106
+ except:
107
+ extension = '.txt'
108
+ file_bytes = document
109
+ filename = "uploaded_file" + extension
110
+ elif isinstance(document, io.IOBase):
111
+ # `document` is a file-like object
112
+ file_bytes = document.read()
113
+ filename = getattr(document, "name", "uploaded_file")
114
+ filename = Path(filename).name
115
+ elif hasattr(document, 'unicode_string') and callable(getattr(document, 'unicode_string')):
116
+ with httpx.Client() as client:
117
+ url: str = document.unicode_string() # type: ignore
118
+ response = client.get(url)
119
+ response.raise_for_status()
120
+ try:
121
+ import puremagic
122
+
123
+ extension = puremagic.from_string(response.content)
124
+ if extension.lower() in [".jpg", ".jpeg", ".jfif"]:
125
+ extension = ".jpeg"
126
+ except:
127
+ extension = '.txt'
128
+ file_bytes = response.content # Fix: Use response.content instead of document
129
+ filename = "uploaded_file" + extension
130
+ else:
131
+ # `document` is a path or a string; cast it to Path
132
+ assert isinstance(document, (Path, str))
133
+ pathdoc = Path(document)
134
+ with open(pathdoc, "rb") as f:
135
+ file_bytes = f.read()
136
+ filename = pathdoc.name
137
+
138
+ # Base64-encode
139
+ encoded_content = base64.b64encode(file_bytes).decode("utf-8")
140
+ # Compute SHA-256 hash over the *base64-encoded* content
141
+ hash_obj = hashlib.sha256(encoded_content.encode("utf-8"))
142
+ content_hash = hash_obj.hexdigest()
143
+
144
+ # Guess MIME type based on file extension
145
+ guessed_type, _ = mimetypes.guess_type(filename)
146
+ mime_type = guessed_type or "application/octet-stream"
147
+ # Build and return the MIMEData object
148
+ mime_data = MIMEData(filename=filename, url=f"data:{mime_type};base64,{encoded_content}")
149
+ assert_valid_file_type(mime_data.extension) # <-- Validate extension as needed
150
+
151
+ return mime_data
152
+
153
+
154
+ def prepare_mime_document_list(documents: Sequence[Path | str | bytes | MIMEData | io.IOBase | PIL.Image.Image]) -> list[MIMEData]:
155
+ """
156
+ Convert documents (file paths or file-like objects) to MIMEData objects.
157
+
158
+ Args:
159
+ documents: List of document paths or file-like objects
160
+
161
+ Returns:
162
+ List of MIMEData objects
163
+ """
164
+ return [prepare_mime_document(doc) for doc in documents]
165
+
166
+
167
+ def assert_valid_file_type(file_extension: str) -> None:
168
+ assert "." + file_extension in get_args(SUPPORTED_TYPES), f"Invalid file type: {file_extension}. Must be one of: {get_args(SUPPORTED_TYPES)}"
@@ -0,0 +1,163 @@
1
+ import datetime
2
+ import json
3
+
4
+ from jiter import from_json
5
+ from openai.types.chat.chat_completion_content_part_image_param import ChatCompletionContentPartImageParam, ImageURL
6
+ from openai.types.chat.chat_completion_content_part_param import ChatCompletionContentPartParam
7
+ from openai.types.chat.chat_completion_content_part_text_param import ChatCompletionContentPartTextParam
8
+ from openai.types.chat.parsed_chat_completion import ParsedChatCompletionMessage
9
+ from openai.types.completion_usage import CompletionTokensDetails, CompletionUsage, PromptTokensDetails
10
+ from openai.types.responses.easy_input_message_param import EasyInputMessageParam
11
+
12
+ # from openai.types.responses.response_input_param import ResponseInputParam
13
+ from openai.types.responses.response import Response
14
+ from openai.types.responses.response_input_image_param import ResponseInputImageParam
15
+ from openai.types.responses.response_input_message_content_list_param import ResponseInputMessageContentListParam
16
+ from openai.types.responses.response_input_param import ResponseInputItemParam
17
+ from openai.types.responses.response_input_text_param import ResponseInputTextParam
18
+
19
+ from ..types.chat import ChatCompletionUiformMessage
20
+ from ..types.documents.extractions import UiParsedChatCompletion, UiParsedChoice
21
+
22
+
23
+ def convert_to_openai_format(messages: list[ChatCompletionUiformMessage]) -> list[ResponseInputItemParam]:
24
+ """
25
+ Converts a list of ChatCompletionUiformMessage to the OpenAI ResponseInputParam format.
26
+
27
+ Args:
28
+ messages: List of chat messages in UIForm format
29
+
30
+ Returns:
31
+ Messages in OpenAI ResponseInputParam format for the Responses API
32
+ """
33
+ formatted_messages: list[ResponseInputItemParam] = []
34
+
35
+ for message in messages:
36
+ role = message["role"]
37
+ content = message["content"]
38
+
39
+ # Handle different content formats
40
+ formatted_content: ResponseInputMessageContentListParam = []
41
+
42
+ if isinstance(content, str):
43
+ # Simple text content - provide direct string value
44
+ formatted_content.append(ResponseInputTextParam(text=content, type="input_text"))
45
+ elif isinstance(content, list):
46
+ # Content is a list of parts
47
+ for part in content:
48
+ if part["type"] == "text":
49
+ formatted_content.append(ResponseInputTextParam(text=part["text"], type="input_text"))
50
+ elif part["type"] == "image_url":
51
+ if "detail" in part["image_url"]:
52
+ detail = part["image_url"]["detail"]
53
+ else:
54
+ detail = "high"
55
+ formatted_content.append(ResponseInputImageParam(image_url=part["image_url"]["url"], type="input_image", detail=detail))
56
+ else:
57
+ print(f"Not supported content type: {part['type']}... Skipping...")
58
+
59
+ # Create Message structure which is one of the types in ResponseInputItemParam
60
+ formatted_message = EasyInputMessageParam(role=role, content=formatted_content, type="message")
61
+
62
+ formatted_messages.append(formatted_message)
63
+
64
+ return formatted_messages
65
+
66
+
67
+ def convert_from_openai_format(messages: list[ResponseInputItemParam]) -> list[ChatCompletionUiformMessage]:
68
+ """
69
+ Converts messages from OpenAI ResponseInputParam format to ChatCompletionUiformMessage format.
70
+
71
+ Args:
72
+ messages: Messages in OpenAI ResponseInputParam format
73
+
74
+ Returns:
75
+ List of chat messages in UIForm format
76
+ """
77
+ formatted_messages: list[ChatCompletionUiformMessage] = []
78
+
79
+ for message in messages:
80
+ if "type" not in message:
81
+ # The type is required by all other sub-types of ResponseInputItemParam except for EasyInputMessageParam and Message, which are messages.
82
+ message["type"] = "message"
83
+
84
+ if message["type"] != "message":
85
+ print(f"Not supported message type: {message['type']}... Skipping...")
86
+ continue
87
+ role = message["role"]
88
+ content = message["content"]
89
+ formatted_content: str | list[ChatCompletionContentPartParam]
90
+
91
+ if isinstance(content, str):
92
+ formatted_content = content
93
+ else:
94
+ # Handle different content formats
95
+ formatted_content = []
96
+ for part in content:
97
+ if part["type"] == "input_text":
98
+ formatted_content.append(ChatCompletionContentPartTextParam(text=part["text"], type="text"))
99
+ elif part["type"] == "input_image":
100
+ image_url = part.get("image_url") or ""
101
+ image_detail = part.get("detail") or "high"
102
+ formatted_content.append(ChatCompletionContentPartImageParam(image_url=ImageURL(url=image_url, detail=image_detail), type="image_url"))
103
+ else:
104
+ print(f"Not supported content type: {part['type']}... Skipping...")
105
+
106
+ # Create message in UIForm format
107
+ formatted_message = ChatCompletionUiformMessage(role=role, content=formatted_content)
108
+ formatted_messages.append(formatted_message)
109
+
110
+ return formatted_messages
111
+
112
+
113
+ def parse_openai_responses_response(response: Response) -> UiParsedChatCompletion:
114
+ """
115
+ Convert an OpenAI Response (Responses API) to UiParsedChatCompletion type.
116
+
117
+ Args:
118
+ response: Response from OpenAI Responses API
119
+
120
+ Returns:
121
+ Parsed response in UiParsedChatCompletion format
122
+ """
123
+ # Create the UiParsedChatCompletion object
124
+ if response.usage:
125
+ usage = CompletionUsage(
126
+ prompt_tokens=response.usage.input_tokens,
127
+ completion_tokens=response.usage.output_tokens,
128
+ total_tokens=response.usage.total_tokens,
129
+ prompt_tokens_details=PromptTokensDetails(
130
+ cached_tokens=response.usage.input_tokens_details.cached_tokens,
131
+ ),
132
+ completion_tokens_details=CompletionTokensDetails(
133
+ reasoning_tokens=response.usage.output_tokens_details.reasoning_tokens,
134
+ ),
135
+ )
136
+ else:
137
+ usage = None
138
+
139
+ # Parse the ParsedChoice
140
+ choices = []
141
+ output_text = response.output_text
142
+ result_object = from_json(bytes(output_text, "utf-8"), partial_mode=True) # Attempt to parse the result even if EOF is reached
143
+
144
+ choices.append(
145
+ UiParsedChoice(
146
+ index=0,
147
+ message=ParsedChatCompletionMessage(
148
+ role="assistant",
149
+ content=json.dumps(result_object),
150
+ ),
151
+ finish_reason="stop",
152
+ )
153
+ )
154
+
155
+ return UiParsedChatCompletion(
156
+ id=response.id,
157
+ choices=choices,
158
+ created=int(datetime.datetime.now().timestamp()),
159
+ model=response.model,
160
+ object="chat.completion",
161
+ likelihoods={},
162
+ usage=usage,
163
+ )
@@ -0,0 +1,52 @@
1
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
2
+ from typing import Any, AsyncGenerator, Callable, Generator, TypeVar, Union
3
+
4
+ T = TypeVar('T')
5
+
6
+
7
+ class AsyncGeneratorContextManager(AbstractAsyncContextManager[AsyncGenerator[T, None]]):
8
+ def __init__(self, generator_func: Callable[..., AsyncGenerator[T, None]], *args: Any, **kwargs: Any):
9
+ self.generator_func = generator_func
10
+ self.args = args
11
+ self.kwargs = kwargs
12
+ self.iterator: Union[AsyncGenerator[T, None], None] = None
13
+
14
+ async def __aenter__(self) -> AsyncGenerator[T, None]:
15
+ # Create the asynchronous iterator from the generator function
16
+ self.iterator = self.generator_func(*self.args, **self.kwargs)
17
+ return self.iterator
18
+
19
+ async def __aexit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
20
+ # Ensure the iterator is properly closed if it supports aclose
21
+ if self.iterator is not None:
22
+ await self.iterator.aclose()
23
+
24
+
25
+ class GeneratorContextManager(AbstractContextManager[Generator[T, None, None]]):
26
+ def __init__(self, generator_func: Callable[..., Generator[T, None, None]], *args: Any, **kwargs: Any):
27
+ self.generator_func = generator_func
28
+ self.args = args
29
+ self.kwargs = kwargs
30
+ self.iterator: Union[Generator[T, None, None], None] = None
31
+
32
+ def __enter__(self) -> Generator[T, None, None]:
33
+ self.iterator = self.generator_func(*self.args, **self.kwargs)
34
+ return self.iterator
35
+
36
+ def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: Any) -> None:
37
+ if self.iterator is not None:
38
+ self.iterator.close()
39
+
40
+
41
+ def as_async_context_manager(func: Callable[..., AsyncGenerator[T, None]]) -> Callable[..., AsyncGeneratorContextManager[T]]:
42
+ def wrapper(*args: Any, **kwargs: Any) -> AsyncGeneratorContextManager[T]:
43
+ return AsyncGeneratorContextManager(func, *args, **kwargs)
44
+
45
+ return wrapper
46
+
47
+
48
+ def as_context_manager(func: Callable[..., Generator[T, None, None]]) -> Callable[..., GeneratorContextManager[T]]:
49
+ def wrapper(*args: Any, **kwargs: Any) -> GeneratorContextManager[T]:
50
+ return GeneratorContextManager(func, *args, **kwargs)
51
+
52
+ return wrapper
File without changes
@@ -0,0 +1,300 @@
1
+ from typing import Optional, Dict
2
+
3
+ from openai.types.completion_usage import CompletionUsage
4
+ from pydantic import BaseModel, Field
5
+
6
+ # https://platform.openai.com/docs/guides/prompt-caching
7
+ from ...types.ai_models import Amount, Pricing, get_model_card
8
+
9
+ # ─── PRICING MODELS ────────────────────────────────────────────────────────────
10
+
11
+
12
+ def compute_api_call_cost(pricing: Pricing, usage: CompletionUsage, is_ft: bool = False) -> Amount:
13
+ """
14
+ Computes the price (as an Amount) for the given token usage, based on the pricing.
15
+
16
+ Assumptions / rules:
17
+ - The pricing rates are per 1,000,000 tokens.
18
+ - The usage is broken out into prompt and completion tokens.
19
+ - If details are provided, the prompt tokens are split into:
20
+ • cached text tokens (if any),
21
+ • audio tokens (if any), and
22
+ • the remaining are treated as "regular" text tokens.
23
+ - Similarly, completion tokens are split into audio tokens (if any) and the remaining text.
24
+ - For text tokens, if a cached price is not explicitly provided in pricing.text.cached,
25
+ we assume a 50% discount off the prompt price.
26
+ - (A similar rule could be applied to audio tokens – i.e. 20% of the normal price –
27
+ if cached audio tokens were provided. In our usage model, however, only text tokens
28
+ are marked as cached.)
29
+ - If is_ft is True, the price is adjusted using the ft_price_hike multiplier.
30
+ """
31
+
32
+ # ----- Process prompt tokens -----
33
+ prompt_cached_text = 0
34
+ prompt_audio = 0
35
+ if usage.prompt_tokens_details:
36
+ prompt_cached_text = usage.prompt_tokens_details.cached_tokens or 0
37
+ prompt_audio = usage.prompt_tokens_details.audio_tokens or 0
38
+
39
+ # The rest of the prompt tokens are assumed to be regular (non-cached) text tokens.
40
+ prompt_regular_text = usage.prompt_tokens - prompt_cached_text - prompt_audio
41
+
42
+ # ----- Process completion tokens -----
43
+ completion_audio = 0
44
+ if usage.completion_tokens_details:
45
+ completion_audio = usage.completion_tokens_details.audio_tokens or 0
46
+
47
+ # The rest are text tokens.
48
+ completion_regular_text = usage.completion_tokens - completion_audio
49
+
50
+ # ----- Calculate text token costs -----
51
+ # Regular prompt text tokens cost at the standard prompt rate.
52
+ cost_text_prompt = prompt_regular_text * pricing.text.prompt
53
+
54
+ # Cached text tokens: if the pricing model provides a cached rate, use it.
55
+ # Otherwise, apply a 50% discount (i.e. 0.5 * prompt price).
56
+ text_cached_price = pricing.text.prompt * pricing.text.cached_discount
57
+ cost_text_cached = prompt_cached_text * text_cached_price
58
+
59
+ # Completion text tokens cost at the standard completion rate.
60
+ cost_text_completion = completion_regular_text * pricing.text.completion
61
+
62
+ total_text_cost = cost_text_prompt + cost_text_cached + cost_text_completion
63
+
64
+ # ----- Calculate audio token costs (if any) -----
65
+ total_audio_cost = 0.0
66
+ if pricing.audio:
67
+ cost_audio_prompt = prompt_audio * pricing.audio.prompt
68
+ cost_audio_completion = completion_audio * pricing.audio.completion
69
+ total_audio_cost = cost_audio_prompt + cost_audio_completion
70
+
71
+ total_cost = (total_text_cost + total_audio_cost) / 1e6
72
+
73
+ # Apply fine-tuning price hike if applicable
74
+ if is_ft and hasattr(pricing, 'ft_price_hike'):
75
+ total_cost *= pricing.ft_price_hike
76
+
77
+ return Amount(value=total_cost, currency="USD")
78
+
79
+
80
+
81
+
82
+ def compute_cost_from_model(model: str, usage: CompletionUsage) -> Amount:
83
+ # Extract base model name for fine-tuned models like "ft:gpt-4o:uiform:4389573"
84
+ is_ft = False
85
+ if model.startswith("ft:"):
86
+ # Split by colon and take the second part (index 1) which contains the base model
87
+ parts = model.split(":")
88
+ if len(parts) > 1:
89
+ model = parts[1]
90
+ is_ft = True
91
+
92
+ # Use the get_model_card function from types.ai_models
93
+ try:
94
+ model_card = get_model_card(model)
95
+ pricing = model_card.pricing
96
+ except ValueError as e:
97
+ raise ValueError(f"No pricing information found for model: {model}")
98
+
99
+ return compute_api_call_cost(pricing, usage, is_ft)
100
+
101
+
102
+ ########################
103
+ # USELESS FOR NOW
104
+ ########################
105
+
106
+
107
+ class CompletionsUsage(BaseModel):
108
+ """The aggregated completions usage details of the specific time bucket."""
109
+
110
+ object: str = Field(default="organization.usage.completions.result", description="Type identifier for the completions usage object")
111
+ input_tokens: int = Field(
112
+ description="The aggregated number of text input tokens used, including cached tokens. For customers subscribe to scale tier, this includes scale tier tokens."
113
+ )
114
+ input_cached_tokens: int = Field(
115
+ description="The aggregated number of text input tokens that has been cached from previous requests. For customers subscribe to scale tier, this includes scale tier tokens."
116
+ )
117
+ output_tokens: int = Field(description="The aggregated number of text output tokens used. For customers subscribe to scale tier, this includes scale tier tokens.")
118
+ input_audio_tokens: int = Field(description="The aggregated number of audio input tokens used, including cached tokens.")
119
+ output_audio_tokens: int = Field(description="The aggregated number of audio output tokens used.")
120
+ num_model_requests: int = Field(description="The count of requests made to the model.")
121
+ project_id: Optional[str] = Field(default=None, description="When group_by=project_id, this field provides the project ID of the grouped usage result.")
122
+ user_id: Optional[str] = Field(default=None, description="When group_by=user_id, this field provides the user ID of the grouped usage result.")
123
+ api_key_id: Optional[str] = Field(default=None, description="When group_by=api_key_id, this field provides the API key ID of the grouped usage result.")
124
+ model: Optional[str] = Field(default=None, description="When group_by=model, this field provides the model name of the grouped usage result.")
125
+ batch: Optional[bool] = Field(default=None, description="When group_by=batch, this field tells whether the grouped usage result is batch or not.")
126
+
127
+ ########################
128
+ # DETAILED COST BREAKDOWN
129
+ ########################
130
+
131
+ class TokenCounts(BaseModel):
132
+ """Detailed breakdown of token counts by type and category."""
133
+
134
+ # Prompt token counts
135
+ prompt_regular_text: int
136
+ prompt_cached_text: int
137
+ prompt_audio: int
138
+
139
+ # Completion token counts
140
+ completion_regular_text: int
141
+ completion_audio: int
142
+
143
+ # Total tokens (should match sum of all components)
144
+ total_tokens: int
145
+
146
+
147
+ class CostBreakdown(BaseModel):
148
+ """Detailed breakdown of API call costs by token type and usage category."""
149
+
150
+ # Total cost amount
151
+ total: Amount
152
+
153
+ # Text token costs broken down by category
154
+ text_prompt_cost: Amount
155
+ text_cached_cost: Amount
156
+ text_completion_cost: Amount
157
+ text_total_cost: Amount
158
+
159
+ # Audio token costs broken down by category (if applicable)
160
+ audio_prompt_cost: Optional[Amount] = None
161
+ audio_completion_cost: Optional[Amount] = None
162
+ audio_total_cost: Optional[Amount] = None
163
+
164
+ # Token counts for reference
165
+ token_counts: TokenCounts
166
+
167
+ # Model and fine-tuning information
168
+ model: str
169
+ is_fine_tuned: bool = False
170
+
171
+
172
+ def compute_api_call_cost_with_breakdown(pricing: Pricing, usage: CompletionUsage, model: str, is_ft: bool = False) -> CostBreakdown:
173
+ """
174
+ Computes a detailed price breakdown for the given token usage, based on the pricing.
175
+
176
+ Returns a CostBreakdown object containing costs broken down by token type and category.
177
+ """
178
+ # ----- Process prompt tokens -----
179
+ prompt_cached_text = 0
180
+ prompt_audio = 0
181
+ if usage.prompt_tokens_details:
182
+ prompt_cached_text = usage.prompt_tokens_details.cached_tokens or 0
183
+ prompt_audio = usage.prompt_tokens_details.audio_tokens or 0
184
+
185
+ # The rest of the prompt tokens are assumed to be regular (non-cached) text tokens.
186
+ prompt_regular_text = usage.prompt_tokens - prompt_cached_text - prompt_audio
187
+
188
+ # ----- Process completion tokens -----
189
+ completion_audio = 0
190
+ if usage.completion_tokens_details:
191
+ completion_audio = usage.completion_tokens_details.audio_tokens or 0
192
+
193
+ # The rest are text tokens.
194
+ completion_regular_text = usage.completion_tokens - completion_audio
195
+
196
+ # ----- Calculate text token costs -----
197
+ # Regular prompt text tokens cost at the standard prompt rate.
198
+ cost_text_prompt = prompt_regular_text * pricing.text.prompt
199
+
200
+ # Cached text tokens: if the pricing model provides a cached rate, use it.
201
+ # Otherwise, apply a 50% discount (i.e. 0.5 * prompt price).
202
+ text_cached_price = pricing.text.prompt * pricing.text.cached_discount
203
+ cost_text_cached = prompt_cached_text * text_cached_price
204
+
205
+ # Completion text tokens cost at the standard completion rate.
206
+ cost_text_completion = completion_regular_text * pricing.text.completion
207
+
208
+ total_text_cost = cost_text_prompt + cost_text_cached + cost_text_completion
209
+
210
+ # ----- Calculate audio token costs (if any) -----
211
+ cost_audio_prompt = 0.0
212
+ cost_audio_completion = 0.0
213
+ total_audio_cost = 0.0
214
+
215
+ if pricing.audio and (prompt_audio > 0 or completion_audio > 0):
216
+ cost_audio_prompt = prompt_audio * pricing.audio.prompt
217
+ cost_audio_completion = completion_audio * pricing.audio.completion
218
+ total_audio_cost = cost_audio_prompt + cost_audio_completion
219
+
220
+ # Convert to dollars (divide by 1M) and create Amount objects
221
+ ft_multiplier = pricing.ft_price_hike if is_ft else 1.0
222
+
223
+ # Create Amount objects for each cost category
224
+ text_prompt_amount = Amount(value=(cost_text_prompt / 1e6) * ft_multiplier, currency="USD")
225
+ text_cached_amount = Amount(value=(cost_text_cached / 1e6) * ft_multiplier, currency="USD")
226
+ text_completion_amount = Amount(value=(cost_text_completion / 1e6) * ft_multiplier, currency="USD")
227
+ text_total_amount = Amount(value=(total_text_cost / 1e6) * ft_multiplier, currency="USD")
228
+
229
+ # Audio amounts (if applicable)
230
+ audio_prompt_amount = None
231
+ audio_completion_amount = None
232
+ audio_total_amount = None
233
+
234
+ if pricing.audio and (prompt_audio > 0 or completion_audio > 0):
235
+ audio_prompt_amount = Amount(value=(cost_audio_prompt / 1e6) * ft_multiplier, currency="USD")
236
+ audio_completion_amount = Amount(value=(cost_audio_completion / 1e6) * ft_multiplier, currency="USD")
237
+ audio_total_amount = Amount(value=(total_audio_cost / 1e6) * ft_multiplier, currency="USD")
238
+
239
+ # Total cost
240
+ total_cost = (total_text_cost + total_audio_cost) / 1e6 * ft_multiplier
241
+ total_amount = Amount(value=total_cost, currency="USD")
242
+
243
+ # Create TokenCounts object with token usage breakdown
244
+ token_counts = TokenCounts(
245
+ prompt_regular_text=prompt_regular_text,
246
+ prompt_cached_text=prompt_cached_text,
247
+ prompt_audio=prompt_audio,
248
+ completion_regular_text=completion_regular_text,
249
+ completion_audio=completion_audio,
250
+ total_tokens=usage.total_tokens
251
+ )
252
+
253
+ return CostBreakdown(
254
+ total=total_amount,
255
+ text_prompt_cost=text_prompt_amount,
256
+ text_cached_cost=text_cached_amount,
257
+ text_completion_cost=text_completion_amount,
258
+ text_total_cost=text_total_amount,
259
+ audio_prompt_cost=audio_prompt_amount,
260
+ audio_completion_cost=audio_completion_amount,
261
+ audio_total_cost=audio_total_amount,
262
+ token_counts=token_counts,
263
+ model=model,
264
+ is_fine_tuned=is_ft
265
+ )
266
+
267
+
268
+ def compute_cost_from_model_with_breakdown(model: str, usage: CompletionUsage) -> CostBreakdown:
269
+ """
270
+ Computes a detailed cost breakdown for an API call using the specified model and usage.
271
+
272
+ Args:
273
+ model: The model name (can be a fine-tuned model like "ft:gpt-4o:uiform:4389573")
274
+ usage: Token usage statistics for the API call
275
+
276
+ Returns:
277
+ CostBreakdown object with detailed cost information
278
+
279
+ Raises:
280
+ ValueError: If no pricing information is found for the model
281
+ """
282
+ # Extract base model name for fine-tuned models like "ft:gpt-4o:uiform:4389573"
283
+ original_model = model
284
+ is_ft = False
285
+
286
+ if model.startswith("ft:"):
287
+ # Split by colon and take the second part (index 1) which contains the base model
288
+ parts = model.split(":")
289
+ if len(parts) > 1:
290
+ model = parts[1]
291
+ is_ft = True
292
+
293
+ # Use the get_model_card function from types.ai_models
294
+ try:
295
+ model_card = get_model_card(model)
296
+ pricing = model_card.pricing
297
+ except ValueError as e:
298
+ raise ValueError(f"No pricing information found for model: {original_model}")
299
+
300
+ return compute_api_call_cost_with_breakdown(pricing, usage, original_model, is_ft)