instructor 1.2.4__tar.gz → 1.2.6__tar.gz

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 (35) hide show
  1. {instructor-1.2.4 → instructor-1.2.6}/PKG-INFO +4 -3
  2. {instructor-1.2.4 → instructor-1.2.6}/instructor/__init__.py +1 -1
  3. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/files.py +19 -15
  4. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/hub.py +5 -5
  5. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/jobs.py +38 -28
  6. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/usage.py +17 -21
  7. {instructor-1.2.4 → instructor-1.2.6}/instructor/client.py +149 -65
  8. {instructor-1.2.4 → instructor-1.2.6}/instructor/client_anthropic.py +16 -15
  9. {instructor-1.2.4 → instructor-1.2.6}/instructor/client_cohere.py +14 -14
  10. {instructor-1.2.4 → instructor-1.2.6}/instructor/client_groq.py +6 -8
  11. {instructor-1.2.4 → instructor-1.2.6}/instructor/client_mistral.py +4 -6
  12. {instructor-1.2.4 → instructor-1.2.6}/instructor/distil.py +50 -28
  13. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/citation.py +6 -6
  14. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/iterable.py +12 -8
  15. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/maybe.py +11 -12
  16. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/parallel.py +5 -9
  17. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/partial.py +14 -17
  18. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/simple_type.py +6 -3
  19. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/validators.py +1 -1
  20. {instructor-1.2.4 → instructor-1.2.6}/instructor/function_calls.py +52 -36
  21. {instructor-1.2.4 → instructor-1.2.6}/instructor/patch.py +13 -20
  22. {instructor-1.2.4 → instructor-1.2.6}/instructor/process_response.py +8 -13
  23. {instructor-1.2.4 → instructor-1.2.6}/instructor/retry.py +11 -11
  24. {instructor-1.2.4 → instructor-1.2.6}/instructor/utils.py +53 -11
  25. {instructor-1.2.4 → instructor-1.2.6}/pyproject.toml +25 -6
  26. {instructor-1.2.4 → instructor-1.2.6}/LICENSE +0 -0
  27. {instructor-1.2.4 → instructor-1.2.6}/README.md +0 -0
  28. {instructor-1.2.4 → instructor-1.2.6}/instructor/_types/__init__.py +0 -0
  29. {instructor-1.2.4 → instructor-1.2.6}/instructor/_types/_alias.py +0 -0
  30. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/__init__.py +0 -0
  31. {instructor-1.2.4 → instructor-1.2.6}/instructor/cli/cli.py +0 -0
  32. {instructor-1.2.4 → instructor-1.2.6}/instructor/dsl/__init__.py +0 -0
  33. {instructor-1.2.4 → instructor-1.2.6}/instructor/exceptions.py +0 -0
  34. {instructor-1.2.4 → instructor-1.2.6}/instructor/mode.py +0 -0
  35. {instructor-1.2.4 → instructor-1.2.6}/instructor/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: instructor
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: structured outputs for llm
5
5
  Home-page: https://github.com/jxnl/instructor
6
6
  License: MIT
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.12
16
16
  Provides-Extra: anthropic
17
17
  Provides-Extra: cohere
18
18
  Provides-Extra: groq
19
+ Provides-Extra: litellm
19
20
  Provides-Extra: mistralai
20
21
  Provides-Extra: test-docs
21
22
  Requires-Dist: aiohttp (>=3.9.1,<4.0.0)
@@ -25,11 +26,11 @@ Requires-Dist: diskcache (>=5.6.3,<6.0.0) ; extra == "test-docs"
25
26
  Requires-Dist: docstring-parser (>=0.16,<0.17)
26
27
  Requires-Dist: fastapi (>=0.109.2,<0.110.0) ; extra == "test-docs"
27
28
  Requires-Dist: groq (>=0.4.2,<0.5.0) ; extra == "groq" or extra == "test-docs"
28
- Requires-Dist: litellm (>=1.0.0,<2.0.0) ; extra == "test-docs"
29
+ Requires-Dist: litellm (>=1.35.31,<2.0.0) ; extra == "test-docs" or extra == "litellm"
29
30
  Requires-Dist: mistralai (>=0.1.8,<0.2.0) ; extra == "test-docs" or extra == "mistralai"
30
31
  Requires-Dist: openai (>=1.1.0,<2.0.0)
31
32
  Requires-Dist: pandas (>=2.2.0,<3.0.0) ; extra == "test-docs"
32
- Requires-Dist: pydantic (==2.7.0)
33
+ Requires-Dist: pydantic (>=2.7.0,<3.0.0)
33
34
  Requires-Dist: pydantic-core (>=2.18.0,<3.0.0)
34
35
  Requires-Dist: pydantic_extra_types (>=2.6.0,<3.0.0) ; extra == "test-docs"
35
36
  Requires-Dist: redis (>=5.0.1,<6.0.0) ; extra == "test-docs"
@@ -1,4 +1,4 @@
1
- import importlib
1
+ import importlib.util
2
2
 
3
3
  from .mode import Mode
4
4
  from .process_response import handle_response_model
@@ -1,6 +1,8 @@
1
+ # type: ignore - stub mismatched
2
+
1
3
  import time
2
4
  from datetime import datetime
3
- from typing import List
5
+ from typing import Literal, cast
4
6
 
5
7
  import openai
6
8
  import typer
@@ -14,7 +16,7 @@ console = Console()
14
16
 
15
17
 
16
18
  # Sample response data
17
- def generate_file_table(files: List[openai.types.FileObject]) -> Table:
19
+ def generate_file_table(files: list[openai.types.FileObject]) -> Table:
18
20
  table = Table(
19
21
  title="OpenAI Files",
20
22
  )
@@ -36,7 +38,7 @@ def generate_file_table(files: List[openai.types.FileObject]) -> Table:
36
38
  return table
37
39
 
38
40
 
39
- def get_files() -> List[openai.types.FileObject]:
41
+ def get_files() -> list[openai.types.FileObject]:
40
42
  files = client.files.list()
41
43
  files = files.data
42
44
  files = sorted(files, key=lambda x: x.created_at, reverse=True)
@@ -50,15 +52,17 @@ def get_file_status(file_id: str) -> str:
50
52
 
51
53
  @app.command(
52
54
  help="Upload a file to OpenAI's servers, will monitor the upload status until it is processed",
53
- ) # type: ignore[misc]
55
+ )
54
56
  def upload(
55
- filepath: str = typer.Argument(..., help="Path to the file to upload"),
57
+ filepath: str = typer.Argument(help="Path to the file to upload"),
56
58
  purpose: str = typer.Option("fine-tune", help="Purpose of the file"),
57
59
  poll: int = typer.Option(5, help="Polling interval in seconds"),
58
60
  ) -> None:
61
+ # Literals aren't supported by Typer yet.
62
+ file_purpose = cast(Literal["fine-tune", "assistants"], purpose)
59
63
  with open(filepath, "rb") as file:
60
- response = client.files.create(file=file, purpose=purpose)
61
- file_id = response["id"]
64
+ response = client.files.create(file=file, purpose=file_purpose)
65
+ file_id = response["id"] # type: ignore - types might be out of date
62
66
  with console.status(f"Monitoring upload: {file_id}...") as status:
63
67
  status.spinner_style = "dots"
64
68
  while True:
@@ -71,10 +75,10 @@ def upload(
71
75
 
72
76
  @app.command(
73
77
  help="Download a file from OpenAI's servers",
74
- ) # type: ignore[misc]
78
+ )
75
79
  def download(
76
- file_id: str = typer.Argument(..., help="ID of the file to download"),
77
- output: str = typer.Argument(..., help="Output path for the downloaded file"),
80
+ file_id: str = typer.Argument(help="ID of the file to download"),
81
+ output: str = typer.Argument(help="Output path for the downloaded file"),
78
82
  ) -> None:
79
83
  with console.status(f"[bold green]Downloading file {file_id}...", spinner="dots"):
80
84
  content = client.files.download(file_id)
@@ -85,8 +89,8 @@ def download(
85
89
 
86
90
  @app.command(
87
91
  help="Delete a file from OpenAI's servers",
88
- ) # type: ignore[misc]
89
- def delete(file_id: str = typer.Argument(..., help="ID of the file to delete")) -> None:
92
+ )
93
+ def delete(file_id: str = typer.Argument(help="ID of the file to delete")) -> None:
90
94
  with console.status(f"[bold red]Deleting file {file_id}...", spinner="dots"):
91
95
  try:
92
96
  client.files.delete(file_id)
@@ -98,9 +102,9 @@ def delete(file_id: str = typer.Argument(..., help="ID of the file to delete"))
98
102
 
99
103
  @app.command(
100
104
  help="Monitor the status of a file on OpenAI's servers",
101
- ) # type: ignore[misc]
105
+ )
102
106
  def status(
103
- file_id: str = typer.Argument(..., help="ID of the file to check the status of"),
107
+ file_id: str = typer.Argument(help="ID of the file to check the status of"),
104
108
  ) -> None:
105
109
  with console.status(f"Monitoring status of file {file_id}...") as status:
106
110
  while True:
@@ -113,7 +117,7 @@ def status(
113
117
 
114
118
  @app.command(
115
119
  help="List the files on OpenAI's servers",
116
- ) # type: ignore[misc]
120
+ )
117
121
  def list() -> None:
118
122
  files = get_files()
119
123
  console.log(generate_file_table(files))
@@ -58,7 +58,7 @@ class HubClient:
58
58
  else:
59
59
  raise Exception(f"Failed to fetch cookbooks: {response.status_code}")
60
60
 
61
- def get_content_markdown(self, branch, slug):
61
+ def get_content_markdown(self, branch: str, slug: str) -> str:
62
62
  """Get markdown content."""
63
63
  url = f"{self.base_url}/api/{branch}/items/{slug}/md/"
64
64
  response = httpx.get(url)
@@ -67,7 +67,7 @@ class HubClient:
67
67
  else:
68
68
  raise Exception(f"Failed to fetch markdown content: {response.status_code}")
69
69
 
70
- def get_content_python(self, branch, slug):
70
+ def get_content_python(self, branch: str, slug: str) -> str:
71
71
  """Get Python code blocks from content."""
72
72
  url = f"{self.base_url}/api/{branch}/items/{slug}/py/"
73
73
  response = httpx.get(url)
@@ -76,12 +76,12 @@ class HubClient:
76
76
  else:
77
77
  raise Exception(f"Failed to fetch Python content: {response.status_code}")
78
78
 
79
- def get_cookbook_id(self, id: int, branch: str = "main") -> HubPage:
79
+ def get_cookbook_id(self, id: int, branch: str = "main") -> Optional[HubPage]:
80
80
  for cookbook in self.get_cookbooks(branch):
81
81
  if cookbook.id == id:
82
82
  return cookbook
83
83
 
84
- def get_cookbook_slug(self, slug: str, branch: str = "main") -> HubPage:
84
+ def get_cookbook_slug(self, slug: str, branch: str = "main") -> Optional[HubPage]:
85
85
  for cookbook in self.get_cookbooks(branch):
86
86
  if cookbook.slug == slug:
87
87
  return cookbook
@@ -155,7 +155,7 @@ def pull(
155
155
 
156
156
  if file:
157
157
  with open(file, "w") as f:
158
- f.write(output)
158
+ f.write(output) # type: ignore - markdown is writable
159
159
  return
160
160
 
161
161
  if page:
@@ -1,13 +1,13 @@
1
- from typing import Dict, List, Union
1
+ from typing import Optional, TypedDict
2
2
  from openai import OpenAI
3
3
 
4
+ from openai.types.fine_tuning.job_create_params import Hyperparameters
4
5
  import typer
5
6
  import time
6
7
  from rich.live import Live
7
8
  from rich.table import Table
8
9
  from rich.console import Console
9
10
  from datetime import datetime
10
- from typing import cast
11
11
  from openai.types.fine_tuning import FineTuningJob
12
12
 
13
13
  client = OpenAI()
@@ -15,10 +15,15 @@ app = typer.Typer()
15
15
  console = Console()
16
16
 
17
17
 
18
- def generate_table(jobs: List[FineTuningJob]) -> Table:
18
+ class FuneTuningParams(TypedDict, total=False):
19
+ hyperparameters: Hyperparameters
20
+ validation_file: Optional[str]
21
+ suffix: Optional[str]
22
+
23
+
24
+ def generate_table(jobs: list[FineTuningJob]) -> Table:
19
25
  # Sorting the jobs by creation time
20
- jobs = sorted(jobs, key=lambda x: (cast(FineTuningJob, x)).created_at, reverse=True)
21
- jobs = cast(List[FineTuningJob], jobs)
26
+ jobs = sorted(jobs, key=lambda x: x.created_at, reverse=True)
22
27
 
23
28
  table = Table(
24
29
  title="OpenAI Fine Tuning Job Monitoring",
@@ -66,7 +71,7 @@ def status_color(status: str) -> str:
66
71
  )
67
72
 
68
73
 
69
- def get_jobs(limit: int = 5) -> List[FineTuningJob]:
74
+ def get_jobs(limit: int = 5) -> list[FineTuningJob]:
70
75
  return client.fine_tuning.jobs.list(limit=limit).data
71
76
 
72
77
 
@@ -78,7 +83,7 @@ def get_file_status(file_id: str) -> str:
78
83
  @app.command(
79
84
  name="list",
80
85
  help="Monitor the status of the most recent fine-tuning jobs.",
81
- ) # type: ignore[misc]
86
+ )
82
87
  def watch(
83
88
  limit: int = typer.Option(5, help="Limit the number of jobs to monitor"),
84
89
  poll: int = typer.Option(5, help="Polling interval in seconds"),
@@ -97,24 +102,24 @@ def watch(
97
102
 
98
103
  @app.command(
99
104
  help="Create a fine-tuning job from an existing ID.",
100
- ) # type: ignore[misc]
105
+ )
101
106
  def create_from_id(
102
- id: str = typer.Argument(..., help="ID of the existing fine-tuning job"),
107
+ id: str = typer.Argument(help="ID of the existing fine-tuning job"),
103
108
  model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"),
104
- n_epochs: int = typer.Option(
109
+ n_epochs: Optional[int] = typer.Option(
105
110
  None, help="Number of epochs for fine-tuning", show_default=False
106
111
  ),
107
- batch_size: int = typer.Option(
112
+ batch_size: Optional[int] = typer.Option(
108
113
  None, help="Batch size for fine-tuning", show_default=False
109
114
  ),
110
- learning_rate_multiplier: float = typer.Option(
115
+ learning_rate_multiplier: Optional[float] = typer.Option(
111
116
  None, help="Learning rate multiplier for fine-tuning", show_default=False
112
117
  ),
113
- validation_file_id: str = typer.Option(
118
+ validation_file_id: Optional[str] = typer.Option(
114
119
  None, help="ID of the uploaded validation file"
115
120
  ),
116
121
  ) -> None:
117
- hyperparameters_dict: Dict[str, Union[int, float, str]] = {}
122
+ hyperparameters_dict: Hyperparameters = {}
118
123
  if n_epochs is not None:
119
124
  hyperparameters_dict["n_epochs"] = n_epochs
120
125
  if batch_size is not None:
@@ -128,7 +133,7 @@ def create_from_id(
128
133
  job = client.fine_tuning.jobs.create(
129
134
  training_file=id,
130
135
  model=model,
131
- hyperparameters=hyperparameters_dict if hyperparameters_dict else None,
136
+ hyperparameters=hyperparameters_dict,
132
137
  validation_file=validation_file_id if validation_file_id else None,
133
138
  )
134
139
  console.log(f"[bold green]Fine-tuning job created with ID: {job.id}")
@@ -137,24 +142,28 @@ def create_from_id(
137
142
 
138
143
  @app.command(
139
144
  help="Create a fine-tuning job from a file.",
140
- ) # type: ignore[misc]
145
+ )
141
146
  def create_from_file(
142
- file: str = typer.Argument(..., help="Path to the file for fine-tuning"),
147
+ file: str = typer.Argument(help="Path to the file for fine-tuning"),
143
148
  model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"),
144
149
  poll: int = typer.Option(2, help="Polling interval in seconds"),
145
- n_epochs: int = typer.Option(
150
+ n_epochs: Optional[int] = typer.Option(
146
151
  None, help="Number of epochs for fine-tuning", show_default=False
147
152
  ),
148
- batch_size: int = typer.Option(
153
+ batch_size: Optional[int] = typer.Option(
149
154
  None, help="Batch size for fine-tuning", show_default=False
150
155
  ),
151
- learning_rate_multiplier: float = typer.Option(
156
+ learning_rate_multiplier: Optional[float] = typer.Option(
152
157
  None, help="Learning rate multiplier for fine-tuning", show_default=False
153
158
  ),
154
- validation_file: str = typer.Option(None, help="Path to the validation file"),
155
- model_suffix: str = typer.Option(None, help="Suffix to identify the model"),
159
+ validation_file: Optional[str] = typer.Option(
160
+ None, help="Path to the validation file"
161
+ ),
162
+ model_suffix: Optional[str] = typer.Option(
163
+ None, help="Suffix to identify the model"
164
+ ),
156
165
  ) -> None:
157
- hyperparameters_dict: Dict[str, Union[int, float, str]] = {}
166
+ hyperparameters_dict: Hyperparameters = {}
158
167
  if n_epochs is not None:
159
168
  hyperparameters_dict["n_epochs"] = n_epochs
160
169
  if batch_size is not None:
@@ -177,8 +186,9 @@ def create_from_file(
177
186
  status.spinner_style = "dots"
178
187
  while True:
179
188
  file_status = get_file_status(file_id)
180
- if validation_file_id:
181
- validation_file_status = get_file_status(validation_file_id)
189
+ validation_file_status = (
190
+ get_file_status(validation_file_id) if validation_file_id else ""
191
+ )
182
192
 
183
193
  if file_status == "processed" and (
184
194
  not validation_file_id or validation_file_status == "processed"
@@ -192,7 +202,7 @@ def create_from_file(
192
202
 
193
203
  time.sleep(poll)
194
204
 
195
- additional_params: Dict[str, Union[str, Dict[str, Union[int, float, str]]]] = {}
205
+ additional_params: FuneTuningParams = {}
196
206
  if hyperparameters_dict:
197
207
  additional_params["hyperparameters"] = hyperparameters_dict
198
208
  if validation_file:
@@ -218,9 +228,9 @@ def create_from_file(
218
228
 
219
229
  @app.command(
220
230
  help="Cancel a fine-tuning job.",
221
- ) # type: ignore[misc]
231
+ )
222
232
  def cancel(
223
- id: str = typer.Argument(..., help="ID of the fine-tuning job to cancel"),
233
+ id: str = typer.Argument(help="ID of the fine-tuning job to cancel"),
224
234
  ) -> None:
225
235
  with console.status(f"[bold red]Cancelling job {id}...", spinner="dots"):
226
236
  try:
@@ -1,9 +1,11 @@
1
- from typing import List, Dict, Any, Union, DefaultDict
1
+ from typing import Any, Union
2
+ from collections.abc import Awaitable
2
3
  from datetime import datetime, timedelta
3
4
  import typer
4
5
  import os
5
6
  import aiohttp
6
7
  import asyncio
8
+ from builtins import list as List
7
9
  from collections import defaultdict
8
10
  from rich.console import Console
9
11
  from rich.table import Table
@@ -18,7 +20,7 @@ console = Console()
18
20
  api_key = os.environ.get("OPENAI_API_KEY")
19
21
 
20
22
 
21
- async def fetch_usage(date: str) -> Dict[str, Any]:
23
+ async def fetch_usage(date: str) -> dict[str, Any]:
22
24
  headers = {"Authorization": f"Bearer {api_key}"}
23
25
  url = f"https://api.openai.com/v1/usage?date={date}"
24
26
  async with aiohttp.ClientSession() as session:
@@ -26,9 +28,9 @@ async def fetch_usage(date: str) -> Dict[str, Any]:
26
28
  return await resp.json()
27
29
 
28
30
 
29
- async def get_usage_for_past_n_days(n_days: int) -> List[Dict[str, Any]]:
30
- tasks = []
31
- all_data = []
31
+ async def get_usage_for_past_n_days(n_days: int) -> list[dict[str, Any]]:
32
+ tasks: List[Awaitable[dict[str, Any]]] = [] # noqa: UP006 - conflicting with the fn name
33
+ all_data: List[dict[str, Any]] = [] # noqa: UP006 - conflicting with the fn name
32
34
  with Progress() as progress:
33
35
  if n_days > 1:
34
36
  task = progress.add_task("[green]Fetching usage data...", total=n_days)
@@ -46,13 +48,7 @@ async def get_usage_for_past_n_days(n_days: int) -> List[Dict[str, Any]]:
46
48
 
47
49
 
48
50
  # Define the cost per unit for each model
49
- # Add temporary body type hint here because mypy may infer the dict type
50
- # from the first few items (?) in the dict, which may not be representative of
51
- # the entire dict.
52
- MODEL_COSTS: Dict[
53
- ModelNames,
54
- Union[Dict[str, float], float],
55
- ] = {
51
+ MODEL_COSTS = {
56
52
  "gpt-4-0125-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
57
53
  "gpt-4-turbo-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
58
54
  "gpt-4-1106-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
@@ -79,7 +75,7 @@ MODEL_COSTS: Dict[
79
75
 
80
76
  def get_model_cost(
81
77
  model: ModelNames,
82
- ) -> Union[Dict[str, float], float]:
78
+ ) -> Union[dict[str, float], float]:
83
79
  """Get the cost details for a given model."""
84
80
  if model in MODEL_COSTS:
85
81
  return MODEL_COSTS[model]
@@ -104,7 +100,7 @@ def calculate_cost(
104
100
  """Calculate the cost based on the snapshot ID and number of tokens."""
105
101
  cost = get_model_cost(snapshot_id)
106
102
 
107
- if isinstance(cost, float):
103
+ if isinstance(cost, (float, int)):
108
104
  return cost * (n_context_tokens + n_generated_tokens)
109
105
 
110
106
  prompt_cost = cost["prompt"] * n_context_tokens
@@ -112,13 +108,13 @@ def calculate_cost(
112
108
  return prompt_cost + completion_cost
113
109
 
114
110
 
115
- def group_and_sum_by_date_and_snapshot(usage_data: List[Dict[str, Any]]) -> Table:
111
+ def group_and_sum_by_date_and_snapshot(usage_data: list[dict[str, Any]]) -> Table:
116
112
  """Group and sum the usage data by date and snapshot, including costs."""
117
- summary: DefaultDict[
118
- str, DefaultDict[str, Dict[str, Union[int, float]]]
119
- ] = defaultdict(
120
- lambda: defaultdict(
121
- lambda: {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0}
113
+ summary: defaultdict[str, defaultdict[str, dict[str, Union[int, float]]]] = (
114
+ defaultdict(
115
+ lambda: defaultdict(
116
+ lambda: {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0}
117
+ )
122
118
  )
123
119
  )
124
120
 
@@ -160,7 +156,7 @@ def group_and_sum_by_date_and_snapshot(usage_data: List[Dict[str, Any]]) -> Tabl
160
156
  return table
161
157
 
162
158
 
163
- @app.command(help="Displays OpenAI API usage data for the past N days.") # type: ignore[misc]
159
+ @app.command(help="Displays OpenAI API usage data for the past N days.")
164
160
  def list(
165
161
  n: int = typer.Option(0, help="Number of days."),
166
162
  ) -> None: