Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(openai-callback): add Azure OpenAI fine-tuned models #694

Merged
merged 1 commit into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 42 additions & 15 deletions pandasai/helpers/openai_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,53 @@
"gpt-35-turbo-instruct-completion": 0.002,
"gpt-35-turbo-16k-completion": 0.004,
"gpt-35-turbo-16k-0613-completion": 0.004,
# Others
"text-davinci-003": 0.02,
# Fine-tuned input
"gpt-3.5-turbo-0613-finetuned": 0.012,
# Fine-tuned output
"gpt-3.5-turbo-0613-finetuned-completion": 0.016,
# Azure Fine-tuned output
"gpt-35-turbo-0613-azure-finetuned": 0.0015,
# Azure Fine-tuned output
"gpt-35-turbo-0613-azure-finetuned-completion": 0.002,
# Others
"text-davinci-003": 0.02,
}


def standardize_model_name(
model_name: str,
is_completion: bool = False,
) -> str:
"""
Standardize the model name to a format that can be used in the OpenAI API.

Args:
model_name: Model name to standardize.
is_completion: Whether the model is used for completion or not.
Defaults to False.

Returns:
Standardized model name.

"""
model_name = model_name.lower()
if ".ft-" in model_name:
model_name = model_name.split(".ft-")[0] + "-azure-finetuned"
if "ft:" in model_name:
model_name = model_name.split(":")[1] + "-finetuned"
if is_completion and (
model_name.startswith("gpt-4")
or model_name.startswith("gpt-3.5")
or model_name.startswith("gpt-35")
or "finetuned" in model_name
):
# The cost of completion token is different from
# the cost of prompt tokens.
return model_name + "-completion"
else:
return model_name

Comment on lines +55 to +87
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The standardize_model_name() function is a good addition to the codebase. It improves code consistency and readability by standardizing the model name for use in the OpenAI API. However, the function could be simplified by using a dictionary to map the model prefixes to their standardized names, which would make the code more maintainable and easier to extend in the future.


def get_openai_token_cost_for_model(
model_name: str,
num_tokens: int,
Expand All @@ -65,18 +103,7 @@ def get_openai_token_cost_for_model(
Returns:
float: Cost in USD.
"""
model_name = model_name.lower()
if "ft:" in model_name:
model_name = model_name.split(":")[1] + "-finetuned"
if is_completion and (
model_name.startswith("gpt-4")
or model_name.startswith("gpt-3.5")
or model_name.startswith("gpt-35")
or "finetuned" in model_name
):
# The cost of completion token is different from
# the cost of prompt tokens.
model_name = model_name + "-completion"
model_name = standardize_model_name(model_name, is_completion=is_completion)
if model_name not in MODEL_COST_PER_1K_TOKENS:
raise ValueError(
f"Unknown model: {model_name}. Please provide a valid OpenAI model name."
Expand Down Expand Up @@ -107,7 +134,7 @@ def __call__(self, response: OpenAIObject) -> None:
if "total_tokens" not in usage:
return None

model_name = response.model
model_name = standardize_model_name(response.model)
if model_name in MODEL_COST_PER_1K_TOKENS:
prompt_cost = get_openai_token_cost_for_model(
model_name, usage.prompt_tokens
Expand Down
100 changes: 99 additions & 1 deletion tests/helpers/test_openai_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,105 @@ def test_handler_unknown_model(self, handler: OpenAICallbackHandler) -> None:
# cost must be 0.0 for unknown model
assert handler.total_cost == 0.0

@pytest.mark.skip
@pytest.mark.parametrize(
"model_name,expected_cost",
[
("gpt-3.5-turbo", 0.0035),
(
"gpt-3.5-turbo-0613",
0.0035,
),
(
"gpt-3.5-turbo-16k-0613",
0.007,
),
(
"gpt-3.5-turbo-16k",
0.007,
),
("gpt-4", 0.09),
("gpt-4-0613", 0.09),
("gpt-4-32k", 0.18),
("gpt-4-32k-0613", 0.18),
],
)
def test_handler_openai(
self, handler: OpenAICallbackHandler, model_name: str, expected_cost: float
) -> None:
response = OpenAIObject.construct_from(
{
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 1000,
"total_tokens": 2000,
},
"model": model_name,
}
)
handler(response)
assert handler.total_cost == expected_cost

@pytest.mark.parametrize(
"model_name,expected_cost",
[
("gpt-35-turbo", 0.0035),
(
"gpt-35-turbo-0613",
0.0035,
),
(
"gpt-35-turbo-16k-0613",
0.007,
),
(
"gpt-35-turbo-16k",
0.007,
),
("gpt-4", 0.09),
("gpt-4-0613", 0.09),
("gpt-4-32k", 0.18),
("gpt-4-32k-0613", 0.18),
],
)
Comment on lines +98 to +119
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test_handler_azure_openai function is almost identical to the test_handler_openai function, and they test the same logic with slightly different model names. To avoid code duplication, consider combining these two functions into one and extending the parameterized inputs to include all the model names.

def test_handler_azure_openai(
self, handler: OpenAICallbackHandler, model_name: str, expected_cost: float
) -> None:
response = OpenAIObject.construct_from(
{
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 1000,
"total_tokens": 2000,
},
"model": model_name,
}
)
handler(response)
assert handler.total_cost == expected_cost

@pytest.mark.parametrize(
"model_name, expected_cost",
[
("ft:gpt-3.5-turbo-0613:your-org:custom-model-name:1abcdefg", 0.028),
("gpt-35-turbo-0613.ft-0123456789abcdefghijklmnopqrstuv", 0.0035),
],
)
def test_handler_finetuned_model(
self, handler: OpenAICallbackHandler, model_name: str, expected_cost: float
):
response = OpenAIObject.construct_from(
{
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 1000,
"total_tokens": 2000,
},
"model": model_name,
}
)
handler(response)
assert handler.total_cost == expected_cost

def test_openai_callback(self, mocker):
df = pd.DataFrame([1, 2, 3])
llm = OpenAI(api_token="test")
Expand Down