참고소스 수정본

This commit is contained in:
LASTA_DEV01\lasta
2026-05-12 19:40:31 +09:00
parent 0f34a451fc
commit 2e9204243d
8708 changed files with 3259488 additions and 869 deletions

View 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}")

View 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()

View 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()

View 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))

View 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()

View 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()