참고소스 수정본
This commit is contained in:
0
참고/instructor-main/instructor/cli/__init__.py
Normal file
0
참고/instructor-main/instructor/cli/__init__.py
Normal file
560
참고/instructor-main/instructor/cli/batch.py
Normal file
560
참고/instructor-main/instructor/cli/batch.py
Normal file
@@ -0,0 +1,560 @@
|
||||
import os
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.live import Live
|
||||
import typer
|
||||
import time
|
||||
import json
|
||||
import warnings
|
||||
from instructor.batch import BatchProcessor, BatchJobInfo
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
app = typer.Typer()
|
||||
|
||||
console = Console()
|
||||
|
||||
|
||||
def generate_table(
|
||||
batch_jobs: list[BatchJobInfo], provider: str, full_id: bool = False
|
||||
):
|
||||
"""Generate enhanced table for batch jobs using unified BatchJobInfo objects
|
||||
|
||||
Args:
|
||||
batch_jobs: List of batch job info objects
|
||||
provider: Provider name (openai, anthropic)
|
||||
full_id: If True, show full batch IDs without truncation
|
||||
"""
|
||||
table = Table(title=f"{provider.title()} Batch Jobs")
|
||||
|
||||
# Adjust column width based on full_id flag
|
||||
id_max_width = None if full_id else 20
|
||||
table.add_column("Batch ID", style="dim", max_width=id_max_width, no_wrap=True)
|
||||
table.add_column("Status", min_width=10)
|
||||
table.add_column("Created", style="dim", min_width=10)
|
||||
table.add_column("Started", style="dim", min_width=10)
|
||||
table.add_column("Duration", style="dim", min_width=7)
|
||||
|
||||
# Add provider-specific columns for request counts
|
||||
if provider == "openai":
|
||||
table.add_column("Completed", justify="right", min_width=8)
|
||||
table.add_column("Failed", justify="right", min_width=6)
|
||||
table.add_column("Total", justify="right", min_width=6)
|
||||
elif provider == "anthropic":
|
||||
table.add_column("Succeeded", justify="right", min_width=8)
|
||||
table.add_column("Errored", justify="right", min_width=7)
|
||||
table.add_column("Processing", justify="right", min_width=9)
|
||||
|
||||
for batch_job in batch_jobs:
|
||||
# Color code status
|
||||
status_color = {
|
||||
"pending": "yellow",
|
||||
"processing": "blue",
|
||||
"completed": "green",
|
||||
"failed": "red",
|
||||
"cancelled": "red",
|
||||
"expired": "red",
|
||||
}.get(batch_job.status.value, "white")
|
||||
|
||||
colored_status = f"[{status_color}]{batch_job.status.value}[/{status_color}]"
|
||||
|
||||
# Format timestamps
|
||||
created_str = (
|
||||
batch_job.timestamps.created_at.strftime("%m/%d %H:%M")
|
||||
if batch_job.timestamps.created_at
|
||||
else "N/A"
|
||||
)
|
||||
started_str = (
|
||||
batch_job.timestamps.started_at.strftime("%m/%d %H:%M")
|
||||
if batch_job.timestamps.started_at
|
||||
else "N/A"
|
||||
)
|
||||
|
||||
# Calculate duration
|
||||
duration_str = "N/A"
|
||||
if batch_job.timestamps.started_at and batch_job.timestamps.completed_at:
|
||||
duration = (
|
||||
batch_job.timestamps.completed_at - batch_job.timestamps.started_at
|
||||
)
|
||||
total_minutes = duration.total_seconds() / 60
|
||||
if total_minutes < 60:
|
||||
duration_str = f"{int(total_minutes)}m"
|
||||
else:
|
||||
hours = total_minutes / 60
|
||||
duration_str = f"{hours:.1f}h"
|
||||
elif batch_job.timestamps.started_at and batch_job.status.value == "processing":
|
||||
from datetime import datetime, timezone
|
||||
|
||||
duration = datetime.now(timezone.utc) - batch_job.timestamps.started_at
|
||||
total_minutes = duration.total_seconds() / 60
|
||||
if total_minutes < 60:
|
||||
duration_str = f"{int(total_minutes)}m"
|
||||
else:
|
||||
hours = total_minutes / 60
|
||||
duration_str = f"{hours:.1f}h"
|
||||
|
||||
# Truncate batch ID for display only if full_id is False
|
||||
batch_id_display = str(batch_job.id)
|
||||
if not full_id and len(batch_id_display) > 18:
|
||||
batch_id_display = batch_id_display[:15] + "..."
|
||||
|
||||
if provider == "openai":
|
||||
table.add_row(
|
||||
batch_id_display,
|
||||
colored_status,
|
||||
created_str,
|
||||
started_str,
|
||||
duration_str,
|
||||
str(batch_job.request_counts.completed or 0),
|
||||
str(batch_job.request_counts.failed or 0),
|
||||
str(batch_job.request_counts.total or 0),
|
||||
)
|
||||
elif provider == "anthropic":
|
||||
table.add_row(
|
||||
str(batch_job.id),
|
||||
colored_status,
|
||||
created_str,
|
||||
started_str,
|
||||
duration_str,
|
||||
str(batch_job.request_counts.succeeded or 0),
|
||||
str(batch_job.request_counts.errored or 0),
|
||||
str(batch_job.request_counts.processing or 0),
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def get_jobs(limit: int = 10, provider: str = "openai") -> list[BatchJobInfo]:
|
||||
"""Get batch jobs for the specified provider using BatchProcessor"""
|
||||
|
||||
# Create a dummy model string for the provider
|
||||
# We just need the provider part for listing batches
|
||||
model_map = {
|
||||
"openai": "openai/gpt-4o-mini",
|
||||
"anthropic": "anthropic/claude-3-sonnet",
|
||||
}
|
||||
|
||||
if provider not in model_map:
|
||||
raise ValueError(f"Unsupported provider: {provider}")
|
||||
|
||||
# Create a dummy response model (not used for listing)
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DummyModel(BaseModel):
|
||||
dummy: str = "dummy"
|
||||
|
||||
try:
|
||||
# Create BatchProcessor instance
|
||||
processor = BatchProcessor(model_map[provider], DummyModel)
|
||||
# Get batch jobs
|
||||
return processor.list_batches(limit=limit)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error listing {provider} batch jobs: {e}[/red]")
|
||||
return []
|
||||
|
||||
|
||||
@app.command(name="list", help="See all existing batch jobs")
|
||||
def watch(
|
||||
limit: int = typer.Option(10, help="Total number of batch jobs to show"),
|
||||
poll: int = typer.Option(
|
||||
10, help="Time in seconds to wait for the batch job to complete"
|
||||
),
|
||||
screen: bool = typer.Option(False, help="Enable or disable screen output"),
|
||||
live: bool = typer.Option(
|
||||
False, help="Enable live polling to continuously update the table"
|
||||
),
|
||||
provider: str = typer.Option(
|
||||
"openai",
|
||||
help="Provider to use (e.g., 'openai', 'anthropic')",
|
||||
),
|
||||
# Deprecated flag for backward compatibility
|
||||
use_anthropic: bool = typer.Option(
|
||||
None,
|
||||
help="[DEPRECATED] Use --model instead. Use Anthropic API instead of OpenAI",
|
||||
),
|
||||
full_id: bool = typer.Option(
|
||||
False,
|
||||
"--full-id",
|
||||
help="Show full batch IDs without truncation",
|
||||
),
|
||||
):
|
||||
"""
|
||||
Monitor the status of the most recent batch jobs
|
||||
"""
|
||||
# Handle deprecated flag
|
||||
if use_anthropic is not None:
|
||||
warnings.warn(
|
||||
"--use-anthropic is deprecated. Use --provider 'anthropic' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if use_anthropic:
|
||||
provider = "anthropic"
|
||||
|
||||
# Check if required API key is available for the provider
|
||||
required_keys = {
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"openai": "OPENAI_API_KEY",
|
||||
}
|
||||
|
||||
if provider in required_keys and not os.getenv(required_keys[provider]):
|
||||
console.print(
|
||||
f"[red]Error: {required_keys[provider]} environment variable not set for {provider}[/red]"
|
||||
)
|
||||
return
|
||||
|
||||
batch_jobs = get_jobs(limit, provider)
|
||||
table = generate_table(batch_jobs, provider, full_id=full_id)
|
||||
|
||||
if not live:
|
||||
# Show table once and exit
|
||||
console.print(table)
|
||||
return
|
||||
|
||||
# Live polling mode
|
||||
with Live(table, refresh_per_second=2, screen=screen) as live_table:
|
||||
while True:
|
||||
batch_jobs = get_jobs(limit, provider)
|
||||
table = generate_table(batch_jobs, provider, full_id=full_id)
|
||||
live_table.update(table)
|
||||
time.sleep(poll)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Create a batch job from a file",
|
||||
)
|
||||
def create_from_file(
|
||||
file_path: str = typer.Option(help="File containing the batch job requests"),
|
||||
model: str = typer.Option(
|
||||
"openai/gpt-4o-mini",
|
||||
help="Model in format 'provider/model-name' (e.g., 'openai/gpt-4', 'anthropic/claude-3-sonnet')",
|
||||
),
|
||||
description: str = typer.Option(
|
||||
"Instructor batch job",
|
||||
help="Description/metadata for the batch job",
|
||||
),
|
||||
completion_window: str = typer.Option(
|
||||
"24h",
|
||||
help="Completion window for the batch job (OpenAI only)",
|
||||
),
|
||||
# Deprecated flag for backward compatibility
|
||||
use_anthropic: bool = typer.Option(
|
||||
None,
|
||||
help="[DEPRECATED] Use --model instead. Use Anthropic API instead of OpenAI",
|
||||
),
|
||||
):
|
||||
"""Create a batch job from a file using the unified BatchProcessor"""
|
||||
# Handle deprecated flag
|
||||
if use_anthropic is not None:
|
||||
warnings.warn(
|
||||
"--use-anthropic is deprecated. Use --model 'anthropic/claude-3-sonnet' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if use_anthropic:
|
||||
model = "anthropic/claude-3-sonnet"
|
||||
|
||||
try:
|
||||
# Create a dummy response model (not used for direct file submission)
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DummyModel(BaseModel):
|
||||
dummy: str = "dummy"
|
||||
|
||||
# Create BatchProcessor instance
|
||||
processor = BatchProcessor(model, DummyModel)
|
||||
|
||||
# Prepare metadata
|
||||
metadata = {
|
||||
"description": description,
|
||||
}
|
||||
|
||||
with console.status(f"[bold green]Submitting batch job...", spinner="dots"):
|
||||
batch_id = processor.submit_batch(
|
||||
file_path, metadata=metadata, completion_window=completion_window
|
||||
)
|
||||
|
||||
console.print(f"[bold green]Batch job created with ID: {batch_id}[/bold green]")
|
||||
|
||||
# Show updated batch list
|
||||
provider_name = model.split("/", 1)[0]
|
||||
watch(limit=5, poll=2, screen=False, live=False, provider=provider_name)
|
||||
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error creating batch job: {e}[/bold red]")
|
||||
|
||||
|
||||
@app.command(help="Cancel a batch job")
|
||||
def cancel(
|
||||
batch_id: str = typer.Option(help="Batch job ID to cancel"),
|
||||
provider: str = typer.Option(
|
||||
"openai",
|
||||
help="Provider to use (e.g., 'openai', 'anthropic')",
|
||||
),
|
||||
# Deprecated flag for backward compatibility
|
||||
use_anthropic: bool = typer.Option(
|
||||
None,
|
||||
help="[DEPRECATED] Use --provider 'anthropic' instead. Use Anthropic API instead of OpenAI",
|
||||
),
|
||||
):
|
||||
"""Cancel a batch job using the unified BatchProcessor"""
|
||||
# Handle deprecated flag
|
||||
if use_anthropic is not None:
|
||||
warnings.warn(
|
||||
"--use-anthropic is deprecated. Use --provider 'anthropic' instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if use_anthropic:
|
||||
provider = "anthropic"
|
||||
|
||||
try:
|
||||
# Create a dummy response model (not used for cancellation)
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DummyModel(BaseModel):
|
||||
dummy: str = "dummy"
|
||||
|
||||
# Create a dummy model string for the provider
|
||||
model_map = {
|
||||
"openai": "openai/gpt-4o-mini",
|
||||
"anthropic": "anthropic/claude-3-sonnet",
|
||||
}
|
||||
|
||||
if provider not in model_map:
|
||||
console.print(f"[red]Unsupported provider: {provider}[/red]")
|
||||
return
|
||||
|
||||
# Create BatchProcessor instance
|
||||
processor = BatchProcessor(model_map[provider], DummyModel)
|
||||
|
||||
with console.status(
|
||||
f"[bold yellow]Cancelling {provider} batch job...", spinner="dots"
|
||||
):
|
||||
processor.cancel_batch(batch_id)
|
||||
|
||||
console.print(
|
||||
f"[bold green]Batch {batch_id} cancelled successfully![/bold green]"
|
||||
)
|
||||
|
||||
# Show updated status
|
||||
watch(limit=5, poll=2, screen=False, live=False, provider=provider)
|
||||
|
||||
except NotImplementedError as e:
|
||||
console.print(f"[yellow]Note: {e}[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error cancelling batch {batch_id}: {e}[/bold red]")
|
||||
|
||||
|
||||
@app.command(help="Delete a completed batch job")
|
||||
def delete(
|
||||
batch_id: str = typer.Option(help="Batch job ID to delete"),
|
||||
provider: str = typer.Option(
|
||||
"openai",
|
||||
help="Provider to use (e.g., 'openai', 'anthropic')",
|
||||
),
|
||||
):
|
||||
"""Delete a batch job using the unified BatchProcessor"""
|
||||
try:
|
||||
# Create a dummy response model (not used for deletion)
|
||||
from pydantic import BaseModel
|
||||
|
||||
class DummyModel(BaseModel):
|
||||
dummy: str = "dummy"
|
||||
|
||||
# Create a dummy model string for the provider
|
||||
model_map = {
|
||||
"openai": "openai/gpt-4o-mini",
|
||||
"anthropic": "anthropic/claude-3-sonnet",
|
||||
}
|
||||
|
||||
if provider not in model_map:
|
||||
console.print(f"[red]Unsupported provider: {provider}[/red]")
|
||||
return
|
||||
|
||||
# Create BatchProcessor instance
|
||||
processor = BatchProcessor(model_map[provider], DummyModel)
|
||||
|
||||
with console.status(
|
||||
f"[bold yellow]Deleting {provider} batch job...", spinner="dots"
|
||||
):
|
||||
processor.delete_batch(batch_id)
|
||||
|
||||
console.print(
|
||||
f"[bold green]Batch {batch_id} deleted successfully![/bold green]"
|
||||
)
|
||||
|
||||
# Show updated status
|
||||
watch(limit=5, poll=2, screen=False, live=False, provider=provider)
|
||||
|
||||
except NotImplementedError as e:
|
||||
console.print(f"[yellow]Note: {e}[/yellow]")
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error deleting batch {batch_id}: {e}[/bold red]")
|
||||
|
||||
|
||||
@app.command(help="Download the file associated with a batch job")
|
||||
def download_file(
|
||||
batch_id: str = typer.Option(help="Batch job ID to download"),
|
||||
download_file_path: str = typer.Option(help="Path to download file to"),
|
||||
provider: str = typer.Option(
|
||||
"openai",
|
||||
help="Provider to use (e.g., 'openai', 'anthropic')",
|
||||
),
|
||||
):
|
||||
try:
|
||||
if provider == "anthropic":
|
||||
from anthropic import Anthropic
|
||||
|
||||
client = Anthropic()
|
||||
# TODO: Remove beta fallback when stable API is available
|
||||
try:
|
||||
batches_client = client.messages.batches
|
||||
except AttributeError:
|
||||
batches_client = client.beta.messages.batches
|
||||
batch = batches_client.retrieve(batch_id)
|
||||
if batch.processing_status != "ended":
|
||||
raise ValueError("Only completed Jobs can be downloaded")
|
||||
|
||||
results_url = batch.results_url
|
||||
if not results_url:
|
||||
raise ValueError("Results URL not available")
|
||||
|
||||
with open(download_file_path, "w") as file:
|
||||
for result in tqdm(client.messages.batches.results(batch_id)):
|
||||
file.write(json.dumps(result.model_dump()) + "\n")
|
||||
else:
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.retrieve(batch_id=batch_id)
|
||||
status = batch.status
|
||||
|
||||
if status != "completed":
|
||||
raise ValueError("Only completed Jobs can be downloaded")
|
||||
|
||||
file_id = batch.output_file_id
|
||||
|
||||
assert file_id, f"Equivalent Output File not found for {batch_id}"
|
||||
file_response = client.files.content(file_id)
|
||||
|
||||
with open(download_file_path, "w") as file:
|
||||
file.write(file_response.text)
|
||||
|
||||
except Exception as e:
|
||||
console.log(f"[bold red]Error downloading file for {batch_id}: {e}")
|
||||
|
||||
|
||||
@app.command(help="Retrieve results from a batch job")
|
||||
def results(
|
||||
batch_id: str = typer.Option(help="Batch job ID to get results from"),
|
||||
output_file: str = typer.Option(help="File to save the results to"),
|
||||
model: str = typer.Option(
|
||||
"openai/gpt-4o-mini",
|
||||
help="Model in format 'provider/model-name' (e.g., 'openai/gpt-4', 'anthropic/claude-3-sonnet')",
|
||||
),
|
||||
):
|
||||
"""Retrieve and save batch job results"""
|
||||
provider, _ = model.split("/", 1)
|
||||
|
||||
try:
|
||||
if provider == "openai":
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI()
|
||||
batch = client.batches.retrieve(batch_id=batch_id)
|
||||
|
||||
if batch.status != "completed":
|
||||
console.print(
|
||||
f"[yellow]Batch status is '{batch.status}', not completed[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
file_id = batch.output_file_id
|
||||
if not file_id:
|
||||
console.print("[red]No output file available[/red]")
|
||||
return
|
||||
|
||||
file_response = client.files.content(file_id)
|
||||
with open(output_file, "w") as f:
|
||||
f.write(file_response.text)
|
||||
console.print(f"[bold green]Results saved to: {output_file}[/bold green]")
|
||||
|
||||
elif provider == "anthropic":
|
||||
from anthropic import Anthropic
|
||||
|
||||
client = Anthropic()
|
||||
batch = client.beta.messages.batches.retrieve(batch_id)
|
||||
|
||||
if batch.processing_status != "ended":
|
||||
console.print(
|
||||
f"[yellow]Batch status is '{batch.processing_status}', not ended[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Get results from Anthropic batch API
|
||||
results_iter = client.beta.messages.batches.results(batch_id)
|
||||
|
||||
with open(output_file, "w") as f:
|
||||
for result in results_iter:
|
||||
f.write(json.dumps(result.model_dump()) + "\n")
|
||||
console.print(f"[bold green]Results saved to: {output_file}[/bold green]")
|
||||
|
||||
else:
|
||||
console.print(f"[red]Unsupported provider: {provider}[/red]")
|
||||
|
||||
except Exception as e:
|
||||
console.log(f"[bold red]Error retrieving results for {batch_id}: {e}")
|
||||
|
||||
|
||||
@app.command(help="Create batch job using BatchProcessor")
|
||||
def create(
|
||||
messages_file: str = typer.Option(help="JSONL file with message conversations"),
|
||||
model: str = typer.Option(
|
||||
"openai/gpt-4o-mini",
|
||||
help="Model in format 'provider/model-name' (e.g., 'openai/gpt-4', 'anthropic/claude-3-sonnet')",
|
||||
),
|
||||
response_model: str = typer.Option(
|
||||
help="Python class path for response model (e.g., 'examples.User')"
|
||||
),
|
||||
output_file: str = typer.Option(
|
||||
"batch_requests.jsonl", help="Output file for batch requests"
|
||||
),
|
||||
max_tokens: int = typer.Option(1000, help="Maximum tokens per request"),
|
||||
temperature: float = typer.Option(0.1, help="Temperature for generation"),
|
||||
):
|
||||
"""Create a batch job using the unified BatchProcessor"""
|
||||
try:
|
||||
# Import the response model dynamically
|
||||
module_path, class_name = response_model.rsplit(".", 1)
|
||||
import importlib
|
||||
|
||||
module = importlib.import_module(module_path)
|
||||
response_class = getattr(module, class_name)
|
||||
|
||||
# Load messages from file
|
||||
messages_list = []
|
||||
with open(messages_file) as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
messages_list.append(json.loads(line))
|
||||
|
||||
# Create batch processor
|
||||
processor = BatchProcessor(model, response_class)
|
||||
|
||||
# Create batch file
|
||||
with console.status(
|
||||
f"[bold green]Creating batch file with {len(messages_list)} requests...",
|
||||
spinner="dots",
|
||||
):
|
||||
processor.create_batch_from_messages(
|
||||
messages_list, output_file, max_tokens, temperature
|
||||
)
|
||||
|
||||
console.print(f"[bold green]Batch file created: {output_file}[/bold green]")
|
||||
console.print(
|
||||
f"[yellow]Use 'instructor batch create-from-file --file-path {output_file}' to submit the batch[/yellow]"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
console.log(f"[bold red]Error creating batch: {e}")
|
||||
35
참고/instructor-main/instructor/cli/cli.py
Normal file
35
참고/instructor-main/instructor/cli/cli.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from typing import Optional
|
||||
import typer
|
||||
from typer import Typer, launch
|
||||
import instructor.cli.jobs as jobs
|
||||
import instructor.cli.files as files
|
||||
import instructor.cli.usage as usage
|
||||
import instructor.cli.deprecated_hub as hub
|
||||
import instructor.cli.batch as batch
|
||||
|
||||
app: Typer = typer.Typer()
|
||||
|
||||
app.add_typer(jobs.app, name="jobs", help="Monitor and create fine tuning jobs")
|
||||
app.add_typer(files.app, name="files", help="Manage files on OpenAI's servers")
|
||||
app.add_typer(usage.app, name="usage", help="Check OpenAI API usage data")
|
||||
app.add_typer(
|
||||
hub.app, name="hub", help="[DEPRECATED] The instructor hub is no longer available"
|
||||
)
|
||||
app.add_typer(batch.app, name="batch", help="Manage OpenAI Batch jobs")
|
||||
|
||||
|
||||
@app.command()
|
||||
def docs(
|
||||
query: Optional[str] = typer.Argument(None, help="Search the documentation"),
|
||||
) -> None:
|
||||
"""
|
||||
Open the instructor documentation website.
|
||||
"""
|
||||
if query:
|
||||
launch(f"https://python.useinstructor.com/?q={query}")
|
||||
else:
|
||||
launch("https://python.useinstructor.com/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
19
참고/instructor-main/instructor/cli/deprecated_hub.py
Normal file
19
참고/instructor-main/instructor/cli/deprecated_hub.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from typer import Exit, echo, Typer
|
||||
|
||||
app: Typer = Typer(help="Instructor Hub CLI (Deprecated)")
|
||||
|
||||
|
||||
@app.command(name="hub")
|
||||
def hub() -> None:
|
||||
"""
|
||||
This command has been deprecated. The instructor hub is no longer available.
|
||||
Please refer to our cookbook examples at https://python.useinstructor.com/examples/
|
||||
"""
|
||||
echo(
|
||||
"The instructor hub has been deprecated. Please refer to our cookbook examples at https://python.useinstructor.com/examples/"
|
||||
)
|
||||
raise Exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
123
참고/instructor-main/instructor/cli/files.py
Normal file
123
참고/instructor-main/instructor/cli/files.py
Normal file
@@ -0,0 +1,123 @@
|
||||
# type: ignore - stub mismatched
|
||||
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Literal, cast
|
||||
|
||||
import openai
|
||||
import typer
|
||||
from openai import OpenAI
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
|
||||
client = OpenAI()
|
||||
app = typer.Typer()
|
||||
console = Console()
|
||||
|
||||
|
||||
# Sample response data
|
||||
def generate_file_table(files: list[openai.types.FileObject]) -> Table:
|
||||
table = Table(
|
||||
title="OpenAI Files",
|
||||
)
|
||||
table.add_column("File ID", style="dim")
|
||||
table.add_column("Size (bytes)", justify="right")
|
||||
table.add_column("Creation Time")
|
||||
table.add_column("Filename")
|
||||
table.add_column("Purpose")
|
||||
|
||||
for file in files:
|
||||
table.add_row(
|
||||
file["id"],
|
||||
str(file["bytes"]),
|
||||
str(datetime.fromtimestamp(file["created_at"])),
|
||||
file["filename"],
|
||||
file["purpose"],
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def get_files() -> list[openai.types.FileObject]:
|
||||
files = client.files.list()
|
||||
files = files.data
|
||||
files = sorted(files, key=lambda x: x.created_at, reverse=True)
|
||||
return files
|
||||
|
||||
|
||||
def get_file_status(file_id: str) -> str:
|
||||
response = client.files.retrieve(file_id)
|
||||
return response.status
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Upload a file to OpenAI's servers, will monitor the upload status until it is processed",
|
||||
)
|
||||
def upload(
|
||||
filepath: str = typer.Argument(help="Path to the file to upload"),
|
||||
purpose: str = typer.Option("fine-tune", help="Purpose of the file"),
|
||||
poll: int = typer.Option(5, help="Polling interval in seconds"),
|
||||
) -> None:
|
||||
# Literals aren't supported by Typer yet.
|
||||
file_purpose = cast(Literal["fine-tune", "assistants"], purpose)
|
||||
with open(filepath, "rb") as file:
|
||||
response = client.files.create(file=file, purpose=file_purpose)
|
||||
file_id = response["id"] # type: ignore - types might be out of date
|
||||
with console.status(f"Monitoring upload: {file_id}...") as status:
|
||||
status.spinner_style = "dots"
|
||||
while True:
|
||||
file_status = get_file_status(file_id)
|
||||
if file_status == "processed":
|
||||
console.log(f"[bold green]File {file_id} uploaded successfully!")
|
||||
break
|
||||
time.sleep(poll)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Download a file from OpenAI's servers",
|
||||
)
|
||||
def download(
|
||||
file_id: str = typer.Argument(help="ID of the file to download"),
|
||||
output: str = typer.Argument(help="Output path for the downloaded file"),
|
||||
) -> None:
|
||||
with console.status(f"[bold green]Downloading file {file_id}...", spinner="dots"):
|
||||
content = client.files.download(file_id)
|
||||
with open(output, "wb") as file:
|
||||
file.write(content)
|
||||
console.log(f"[bold green]File {file_id} downloaded successfully!")
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Delete a file from OpenAI's servers",
|
||||
)
|
||||
def delete(file_id: str = typer.Argument(help="ID of the file to delete")) -> None:
|
||||
with console.status(f"[bold red]Deleting file {file_id}...", spinner="dots"):
|
||||
try:
|
||||
client.files.delete(file_id)
|
||||
console.log(f"[bold red]File {file_id} deleted successfully!")
|
||||
except Exception as e:
|
||||
console.log(f"[bold red]Error deleting file {file_id}: {e}")
|
||||
return
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Monitor the status of a file on OpenAI's servers",
|
||||
)
|
||||
def status(
|
||||
file_id: str = typer.Argument(help="ID of the file to check the status of"),
|
||||
) -> None:
|
||||
with console.status(f"Monitoring status of file {file_id}...") as status:
|
||||
while True:
|
||||
file_status = get_file_status(file_id)
|
||||
status.update(f"File status: {file_status}")
|
||||
if file_status in ["pending", "processed"]:
|
||||
break
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="List the files on OpenAI's servers",
|
||||
)
|
||||
def list() -> None:
|
||||
files = get_files()
|
||||
console.log(generate_file_table(files))
|
||||
244
참고/instructor-main/instructor/cli/jobs.py
Normal file
244
참고/instructor-main/instructor/cli/jobs.py
Normal file
@@ -0,0 +1,244 @@
|
||||
from typing import Optional, TypedDict
|
||||
from openai import OpenAI
|
||||
|
||||
from openai.types.fine_tuning.job_create_params import Hyperparameters
|
||||
import typer
|
||||
import time
|
||||
from rich.live import Live
|
||||
from rich.table import Table
|
||||
from rich.console import Console
|
||||
from datetime import datetime
|
||||
from openai.types.fine_tuning import FineTuningJob
|
||||
|
||||
client = OpenAI()
|
||||
app = typer.Typer()
|
||||
console = Console()
|
||||
|
||||
|
||||
class FuneTuningParams(TypedDict, total=False):
|
||||
hyperparameters: Hyperparameters
|
||||
validation_file: Optional[str]
|
||||
suffix: Optional[str]
|
||||
|
||||
|
||||
def generate_table(jobs: list[FineTuningJob]) -> Table:
|
||||
# Sorting the jobs by creation time
|
||||
jobs = sorted(jobs, key=lambda x: x.created_at, reverse=True)
|
||||
|
||||
table = Table(
|
||||
title="OpenAI Fine Tuning Job Monitoring",
|
||||
caption="Automatically refreshes every 5 seconds, press Ctrl+C to exit",
|
||||
)
|
||||
|
||||
table.add_column("Job ID", style="dim")
|
||||
table.add_column("Status")
|
||||
table.add_column("Creation Time", justify="right")
|
||||
table.add_column("Completion Time", justify="right")
|
||||
table.add_column("Model Name")
|
||||
table.add_column("File ID")
|
||||
table.add_column("Epochs")
|
||||
table.add_column("Base Model")
|
||||
|
||||
for job in jobs:
|
||||
status_emoji = {
|
||||
"running": "⏳",
|
||||
"succeeded": "✅",
|
||||
"failed": "❌",
|
||||
"cancelled": "🚫",
|
||||
}.get(job.status, "❓")
|
||||
|
||||
finished_at = (
|
||||
str(datetime.fromtimestamp(job.finished_at)) if job.finished_at else "N/A"
|
||||
)
|
||||
|
||||
table.add_row(
|
||||
job.id,
|
||||
f"{status_emoji} [{status_color(job.status)}]{job.status}[/]",
|
||||
str(datetime.fromtimestamp(job.created_at)),
|
||||
finished_at,
|
||||
job.fine_tuned_model,
|
||||
job.training_file,
|
||||
str(job.hyperparameters.n_epochs),
|
||||
job.model,
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
def status_color(status: str) -> str:
|
||||
return {"running": "yellow", "succeeded": "green", "failed": "red"}.get(
|
||||
status, "white"
|
||||
)
|
||||
|
||||
|
||||
def get_jobs(limit: int = 5) -> list[FineTuningJob]:
|
||||
return client.fine_tuning.jobs.list(limit=limit).data
|
||||
|
||||
|
||||
def get_file_status(file_id: str) -> str:
|
||||
response = client.files.retrieve(file_id)
|
||||
return response.status
|
||||
|
||||
|
||||
@app.command(
|
||||
name="list",
|
||||
help="Monitor the status of the most recent fine-tuning jobs.",
|
||||
)
|
||||
def watch(
|
||||
limit: int = typer.Option(5, help="Limit the number of jobs to monitor"),
|
||||
poll: int = typer.Option(5, help="Polling interval in seconds"),
|
||||
screen: bool = typer.Option(False, help="Enable or disable screen output"),
|
||||
) -> None:
|
||||
"""
|
||||
Monitor the status of the most recent fine-tuning jobs.
|
||||
"""
|
||||
jobs = get_jobs(limit=limit)
|
||||
with Live(generate_table(jobs), refresh_per_second=2, screen=screen) as live_table:
|
||||
while True:
|
||||
jobs = get_jobs(limit=limit)
|
||||
live_table.update(generate_table(jobs))
|
||||
time.sleep(poll)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Create a fine-tuning job from an existing ID.",
|
||||
)
|
||||
def create_from_id(
|
||||
id: str = typer.Argument(help="ID of the existing fine-tuning job"),
|
||||
model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"),
|
||||
n_epochs: Optional[int] = typer.Option(
|
||||
None, help="Number of epochs for fine-tuning", show_default=False
|
||||
),
|
||||
batch_size: Optional[int] = typer.Option(
|
||||
None, help="Batch size for fine-tuning", show_default=False
|
||||
),
|
||||
learning_rate_multiplier: Optional[float] = typer.Option(
|
||||
None, help="Learning rate multiplier for fine-tuning", show_default=False
|
||||
),
|
||||
validation_file_id: Optional[str] = typer.Option(
|
||||
None, help="ID of the uploaded validation file"
|
||||
),
|
||||
) -> None:
|
||||
hyperparameters_dict: Hyperparameters = {}
|
||||
if n_epochs is not None:
|
||||
hyperparameters_dict["n_epochs"] = n_epochs
|
||||
if batch_size is not None:
|
||||
hyperparameters_dict["batch_size"] = batch_size
|
||||
if learning_rate_multiplier is not None:
|
||||
hyperparameters_dict["learning_rate_multiplier"] = learning_rate_multiplier
|
||||
|
||||
with console.status(
|
||||
f"[bold green]Creating fine-tuning job from ID {id}...", spinner="dots"
|
||||
):
|
||||
job = client.fine_tuning.jobs.create(
|
||||
training_file=id,
|
||||
model=model,
|
||||
hyperparameters=hyperparameters_dict,
|
||||
validation_file=validation_file_id if validation_file_id else None,
|
||||
)
|
||||
console.log(f"[bold green]Fine-tuning job created with ID: {job.id}")
|
||||
watch(limit=5, poll=2, screen=False)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Create a fine-tuning job from a file.",
|
||||
)
|
||||
def create_from_file(
|
||||
file: str = typer.Argument(help="Path to the file for fine-tuning"),
|
||||
model: str = typer.Option("gpt-3.5-turbo", help="Model to use for fine-tuning"),
|
||||
poll: int = typer.Option(2, help="Polling interval in seconds"),
|
||||
n_epochs: Optional[int] = typer.Option(
|
||||
None, help="Number of epochs for fine-tuning", show_default=False
|
||||
),
|
||||
batch_size: Optional[int] = typer.Option(
|
||||
None, help="Batch size for fine-tuning", show_default=False
|
||||
),
|
||||
learning_rate_multiplier: Optional[float] = typer.Option(
|
||||
None, help="Learning rate multiplier for fine-tuning", show_default=False
|
||||
),
|
||||
validation_file: Optional[str] = typer.Option(
|
||||
None, help="Path to the validation file"
|
||||
),
|
||||
model_suffix: Optional[str] = typer.Option(
|
||||
None, help="Suffix to identify the model"
|
||||
),
|
||||
) -> None:
|
||||
hyperparameters_dict: Hyperparameters = {}
|
||||
if n_epochs is not None:
|
||||
hyperparameters_dict["n_epochs"] = n_epochs
|
||||
if batch_size is not None:
|
||||
hyperparameters_dict["batch_size"] = batch_size
|
||||
if learning_rate_multiplier is not None:
|
||||
hyperparameters_dict["learning_rate_multiplier"] = learning_rate_multiplier
|
||||
|
||||
with open(file, "rb") as file_buffer:
|
||||
response = client.files.create(file=file_buffer, purpose="fine-tune")
|
||||
|
||||
file_id = response.id
|
||||
|
||||
validation_file_id = None
|
||||
if validation_file:
|
||||
with open(validation_file, "rb") as val_file:
|
||||
val_response = client.files.create(file=val_file, purpose="fine-tune")
|
||||
validation_file_id = val_response.id
|
||||
|
||||
with console.status(f"Monitoring upload: {file_id} before finetuning...") as status:
|
||||
status.spinner_style = "dots"
|
||||
while True:
|
||||
file_status = get_file_status(file_id)
|
||||
validation_file_status = (
|
||||
get_file_status(validation_file_id) if validation_file_id else ""
|
||||
)
|
||||
|
||||
if file_status == "processed" and (
|
||||
not validation_file_id or validation_file_status == "processed"
|
||||
):
|
||||
console.log(f"[bold green]File {file_id} uploaded successfully!")
|
||||
if validation_file_id:
|
||||
console.log(
|
||||
f"[bold green]Validation file {validation_file_id} uploaded successfully!"
|
||||
)
|
||||
break
|
||||
|
||||
time.sleep(poll)
|
||||
|
||||
additional_params: FuneTuningParams = {}
|
||||
if hyperparameters_dict:
|
||||
additional_params["hyperparameters"] = hyperparameters_dict
|
||||
if validation_file:
|
||||
additional_params["validation_file"] = validation_file
|
||||
if model_suffix:
|
||||
additional_params["suffix"] = model_suffix
|
||||
|
||||
job = client.fine_tuning.jobs.create(
|
||||
training_file=file_id,
|
||||
model=model,
|
||||
**additional_params,
|
||||
)
|
||||
if validation_file_id:
|
||||
console.log(
|
||||
f"[bold green]Fine-tuning job created with ID: {job.id} from file ID: {file_id} and validation_file ID: {validation_file_id}"
|
||||
)
|
||||
else:
|
||||
console.log(
|
||||
f"[bold green]Fine-tuning job created with ID: {job.id} from file ID: {file_id}"
|
||||
)
|
||||
watch(limit=5, poll=poll, screen=False)
|
||||
|
||||
|
||||
@app.command(
|
||||
help="Cancel a fine-tuning job.",
|
||||
)
|
||||
def cancel(
|
||||
id: str = typer.Argument(help="ID of the fine-tuning job to cancel"),
|
||||
) -> None:
|
||||
with console.status(f"[bold red]Cancelling job {id}...", spinner="dots"):
|
||||
try:
|
||||
client.fine_tuning.jobs.cancel(id)
|
||||
console.log(f"[bold red]Job {id} cancelled successfully!")
|
||||
except Exception as e:
|
||||
console.log(f"[bold red]Error cancelling job {id}: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
177
참고/instructor-main/instructor/cli/usage.py
Normal file
177
참고/instructor-main/instructor/cli/usage.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from typing import Any, Union
|
||||
from collections.abc import Awaitable
|
||||
from datetime import datetime, timedelta
|
||||
import typer
|
||||
import os
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from builtins import list as List
|
||||
from collections import defaultdict
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from rich.progress import Progress
|
||||
|
||||
from instructor._types._alias import ModelNames
|
||||
|
||||
|
||||
app = typer.Typer()
|
||||
console = Console()
|
||||
|
||||
api_key = os.environ.get("OPENAI_API_KEY")
|
||||
|
||||
|
||||
async def fetch_usage(date: str) -> dict[str, Any]:
|
||||
headers = {"Authorization": f"Bearer {api_key}"}
|
||||
url = f"https://api.openai.com/v1/usage?date={date}"
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, headers=headers) as resp:
|
||||
return await resp.json()
|
||||
|
||||
|
||||
async def get_usage_for_past_n_days(n_days: int) -> list[dict[str, Any]]:
|
||||
tasks: List[Awaitable[dict[str, Any]]] = [] # noqa: UP006 - conflicting with the fn name
|
||||
all_data: List[dict[str, Any]] = [] # noqa: UP006 - conflicting with the fn name
|
||||
with Progress() as progress:
|
||||
if n_days > 1:
|
||||
task = progress.add_task("[green]Fetching usage data...", total=n_days)
|
||||
for i in range(n_days):
|
||||
date = (datetime.now() - timedelta(days=i)).strftime("%Y-%m-%d")
|
||||
tasks.append(fetch_usage(date))
|
||||
progress.update(task, advance=1)
|
||||
else:
|
||||
tasks.append(fetch_usage(datetime.now().strftime("%Y-%m-%d")))
|
||||
|
||||
fetched_data = await asyncio.gather(*tasks)
|
||||
for data in fetched_data:
|
||||
all_data.extend(data.get("data", []))
|
||||
return all_data
|
||||
|
||||
|
||||
# Define the cost per unit for each model
|
||||
MODEL_COSTS = {
|
||||
"gpt-4o": {"prompt": 0.005 / 1000, "completion": 0.015 / 1000},
|
||||
"gpt-4o-2024-05-13": {"prompt": 0.005 / 1000, "completion": 0.015 / 1000},
|
||||
"gpt-4-turbo": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4-turbo-2024-04-09": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4-0125-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4-turbo-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4-1106-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4-vision-preview": {"prompt": 0.01 / 1000, "completion": 0.03 / 1000},
|
||||
"gpt-4": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000},
|
||||
"gpt-4-0314": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000},
|
||||
"gpt-4-0613": {"prompt": 0.03 / 1000, "completion": 0.06 / 1000},
|
||||
"gpt-4-32k": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000},
|
||||
"gpt-4-32k-0314": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000},
|
||||
"gpt-4-32k-0613": {"prompt": 0.06 / 1000, "completion": 0.12 / 1000},
|
||||
"gpt-3.5-turbo": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000},
|
||||
"gpt-3.5-turbo-16k": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000},
|
||||
"gpt-3.5-turbo-0301": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000},
|
||||
"gpt-3.5-turbo-0613": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000},
|
||||
"gpt-3.5-turbo-1106": {"prompt": 0.0010 / 1000, "completion": 0.0020 / 1000},
|
||||
"gpt-3.5-turbo-0125": {"prompt": 0.0005 / 1000, "completion": 0.0015 / 1000},
|
||||
"gpt-3.5-turbo-16k-0613": {"prompt": 0.0030 / 1000, "completion": 0.0040 / 1000},
|
||||
"gpt-3.5-turbo-instruct": {"prompt": 0.0015 / 1000, "completion": 0.0020 / 1000},
|
||||
"text-embedding-3-small": 0.00002 / 1000,
|
||||
"text-embedding-3-large": 0.00013 / 1000,
|
||||
"text-embedding-ada-002": 0.00010 / 1000,
|
||||
}
|
||||
|
||||
|
||||
def get_model_cost(
|
||||
model: ModelNames,
|
||||
) -> Union[dict[str, float], float]:
|
||||
"""Get the cost details for a given model."""
|
||||
if model in MODEL_COSTS:
|
||||
return MODEL_COSTS[model]
|
||||
|
||||
if model.startswith("gpt-3.5-turbo-16k"):
|
||||
return MODEL_COSTS["gpt-3.5-turbo-16k"]
|
||||
elif model.startswith("gpt-3.5-turbo"):
|
||||
return MODEL_COSTS["gpt-3.5-turbo"]
|
||||
elif model.startswith("gpt-4-turbo"):
|
||||
return MODEL_COSTS["gpt-4-turbo-preview"]
|
||||
elif model.startswith("gpt-4-32k"):
|
||||
return MODEL_COSTS["gpt-4-32k"]
|
||||
elif model.startswith("gpt-4o"):
|
||||
return MODEL_COSTS["gpt-4o"]
|
||||
elif model.startswith("gpt-4"):
|
||||
return MODEL_COSTS["gpt-4"]
|
||||
else:
|
||||
raise ValueError(f"Cost for model {model} not found")
|
||||
|
||||
|
||||
def calculate_cost(
|
||||
snapshot_id: ModelNames,
|
||||
n_context_tokens: int,
|
||||
n_generated_tokens: int,
|
||||
) -> float:
|
||||
"""Calculate the cost based on the snapshot ID and number of tokens."""
|
||||
cost = get_model_cost(snapshot_id)
|
||||
|
||||
if isinstance(cost, (float, int)):
|
||||
return cost * (n_context_tokens + n_generated_tokens)
|
||||
|
||||
prompt_cost = cost["prompt"] * n_context_tokens
|
||||
completion_cost = cost["completion"] * n_generated_tokens
|
||||
return prompt_cost + completion_cost
|
||||
|
||||
|
||||
def group_and_sum_by_date_and_snapshot(usage_data: list[dict[str, Any]]) -> Table:
|
||||
"""Group and sum the usage data by date and snapshot, including costs."""
|
||||
summary: defaultdict[str, defaultdict[str, dict[str, Union[int, float]]]] = (
|
||||
defaultdict(
|
||||
lambda: defaultdict(
|
||||
lambda: {"total_requests": 0, "total_tokens": 0, "total_cost": 0.0}
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for usage in usage_data:
|
||||
snapshot_id = usage["snapshot_id"]
|
||||
date = datetime.fromtimestamp(usage["aggregation_timestamp"]).strftime(
|
||||
"%Y-%m-%d"
|
||||
)
|
||||
summary[date][snapshot_id]["total_requests"] += usage["n_requests"]
|
||||
summary[date][snapshot_id]["total_tokens"] += usage["n_generated_tokens_total"]
|
||||
|
||||
# Calculate and add the cost
|
||||
cost = calculate_cost(
|
||||
snapshot_id,
|
||||
usage["n_context_tokens_total"],
|
||||
usage["n_generated_tokens_total"],
|
||||
)
|
||||
summary[date][snapshot_id]["total_cost"] += cost
|
||||
|
||||
table = Table(title="Usage Summary by Date, Snapshot, and Cost")
|
||||
table.add_column("Date", style="dim")
|
||||
table.add_column("Model", style="dim")
|
||||
table.add_column("Total Requests", justify="right")
|
||||
table.add_column("Total Cost ($)", justify="right")
|
||||
|
||||
# Sort dates and snapshots in descending order
|
||||
sorted_dates = sorted(summary.keys(), reverse=True)
|
||||
for date in sorted_dates:
|
||||
sorted_snapshots = sorted(summary[date].keys(), reverse=True)
|
||||
for snapshot_id in sorted_snapshots:
|
||||
data = summary[date][snapshot_id]
|
||||
table.add_row(
|
||||
date,
|
||||
snapshot_id,
|
||||
str(data["total_requests"]),
|
||||
"{:.2f}".format(data["total_cost"]),
|
||||
)
|
||||
|
||||
return table
|
||||
|
||||
|
||||
@app.command(help="Displays OpenAI API usage data for the past N days.")
|
||||
def list(
|
||||
n: int = typer.Option(0, help="Number of days."),
|
||||
) -> None:
|
||||
all_data = asyncio.run(get_usage_for_past_n_days(n))
|
||||
table = group_and_sum_by_date_and_snapshot(all_data)
|
||||
console.print(table)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
Reference in New Issue
Block a user