Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,8 @@ impl OcrProviderConfig for VertexAiDeepSeekOcrConfig {
})?;
let usage = response.get("usage").cloned();
let content = first_choice_content(&response_json)?;
let mut ocr_data = ocr_data_from_content(content.clone(), usage.clone(), model);
let provider_model = deepseek_model_name(model);
let mut ocr_data = ocr_data_from_content(content.clone(), usage.clone(), &provider_model);

if !ocr_data.get("pages").is_some_and(Value::is_array) {
ocr_data = json!({
Expand All @@ -309,7 +310,7 @@ impl OcrProviderConfig for VertexAiDeepSeekOcrConfig {
other => other.to_string(),
}
}],
"model": ocr_data.get("model").and_then(Value::as_str).unwrap_or(model),
"model": ocr_data.get("model").and_then(Value::as_str).unwrap_or(&provider_model),
"usage_info": ocr_data.get("usage_info").cloned().or(usage).unwrap_or_else(|| json!({})),
});
}
Expand All @@ -332,7 +333,7 @@ impl OcrProviderConfig for VertexAiDeepSeekOcrConfig {
model: object
.get("model")
.and_then(Value::as_str)
.unwrap_or(model)
.unwrap_or(&provider_model)
.to_string(),
document_annotation: object.get("document_annotation").cloned(),
usage_info,
Expand Down Expand Up @@ -429,7 +430,7 @@ mod tests {
response.pages,
vec![json!({"index": 0, "markdown": "# OCR text"})]
);
assert_eq!(response.model, "deepseek-ocr-maas");
assert_eq!(response.model, "deepseek-ai/deepseek-ocr-maas");
assert_eq!(response.usage_info, Some(json!({"prompt_tokens": 1})));
}
}
29 changes: 26 additions & 3 deletions litellm/cost_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
from litellm.litellm_core_utils.litellm_logging import (
Logging as LitellmLoggingObject,
)
from litellm.llms.base_llm.ocr.transformation import OCRUsageInfo
else:
LitellmLoggingObject = Any

Expand Down Expand Up @@ -1871,6 +1872,23 @@ def response_cost_calculator(
raise e


def _ocr_token_cost(usage_info: "OCRUsageInfo", model_info: ModelInfo | None) -> tuple[float, float] | None:
if model_info is None:
return None
input_cost_per_token: Final = model_info.get("input_cost_per_token") or 0.0
output_cost_per_token: Final = model_info.get("output_cost_per_token") or 0.0
if input_cost_per_token == 0.0 and output_cost_per_token == 0.0:
return None
token_counts: Final = usage_info.model_extra
if token_counts is None:
return None
prompt_tokens: Final = token_counts.get("prompt_tokens")
completion_tokens: Final = token_counts.get("completion_tokens")
if not isinstance(prompt_tokens, int) or not isinstance(completion_tokens, int):
return None
return prompt_tokens * input_cost_per_token, completion_tokens * output_cost_per_token


def ocr_cost(
model: str,
custom_llm_provider: str | None,
Expand All @@ -1883,9 +1901,8 @@ def ocr_cost(
response: Optional[Any] - response object

Returns:
Tuple[float, float]: cost of OCR processing

(Parent function requires a tuple, so we return a tuple. Cost is only in the first element.)
Tuple[float, float]: (prompt cost, completion cost) when priced per token,
otherwise the page cost in the first element
"""
from litellm.llms.base_llm.ocr.transformation import OCRResponse

Expand Down Expand Up @@ -1917,6 +1934,12 @@ def ocr_cost(
pages_processed: Final = response.usage_info.pages_processed
annotation_pages: Final = response.usage_info.pages_processed_annotation or 0
has_billable_annotation_pages: Final = annotation_rate is not None and annotation_pages > 0
has_page_pricing: Final = (
pages_processed is not None and ocr_cost_per_page is not None
) or has_billable_annotation_pages
token_cost: Final = _ocr_token_cost(response.usage_info, model_info)
if not has_page_pricing and token_cost is not None:
return token_cost

if pages_processed is None and not has_billable_annotation_pages:
if cost_per_credit is not None or ocr_cost_per_page is None:
Expand Down
15 changes: 10 additions & 5 deletions litellm/llms/vertex_ai/ocr/deepseek_transformation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
LiteLLMLoggingObj = Any


def _provider_model_name(model: str) -> str:
return model if model.startswith("deepseek-ai/") else f"deepseek-ai/{model}"


class VertexAIDeepSeekOCRConfig(BaseOCRConfig):
"""
Vertex AI DeepSeek OCR transformation configuration.
Expand Down Expand Up @@ -177,7 +181,7 @@ def transform_ocr_request(
content_item = {"type": "image_url", "image_url": document_url}

# Build DeepSeek OCR request
provider_model: Final = model if model.startswith("deepseek-ai/") else f"deepseek-ai/{model}"
provider_model: Final = _provider_model_name(model)
data: Final = {
"model": provider_model,
"messages": [{"role": "user", "content": [content_item]}],
Expand Down Expand Up @@ -261,6 +265,7 @@ def transform_ocr_response(
"""
verbose_logger.debug("Vertex AI DeepSeek OCR transform_ocr_response called")
verbose_logger.debug("Raw response: %s", raw_response.text)
provider_model: Final = _provider_model_name(model)

try:
response_json: Final = raw_response.json()
Expand Down Expand Up @@ -288,14 +293,14 @@ def transform_ocr_response(
# If content is markdown text, create a single page with the markdown
ocr_data = {
"pages": [{"index": 0, "markdown": content}],
"model": model,
"model": provider_model,
"usage_info": response_json.get("usage", {}),
}
except json.JSONDecodeError:
# If JSON parsing fails, treat content as markdown
ocr_data = {
"pages": [{"index": 0, "markdown": content}],
"model": model,
"model": provider_model,
"usage_info": response_json.get("usage", {}),
}

Expand All @@ -309,7 +314,7 @@ def transform_ocr_response(
"markdown": (content if isinstance(content, str) else json.dumps(content)),
}
],
"model": ocr_data.get("model", model),
"model": ocr_data.get("model", provider_model),
"usage_info": ocr_data.get("usage_info", response_json.get("usage", {})),
}

Expand Down Expand Up @@ -339,7 +344,7 @@ def transform_ocr_response(

return OCRResponse(
pages=pages,
model=ocr_data.get("model", model),
model=ocr_data.get("model", provider_model),
document_annotation=ocr_data.get("document_annotation"),
usage_info=usage_info,
object="ocr",
Expand Down
1 change: 0 additions & 1 deletion litellm/model_prices_and_context_window_backup.json
Original file line number Diff line number Diff line change
Expand Up @@ -45806,7 +45806,6 @@
"mode": "ocr",
"input_cost_per_token": 3e-07,
"output_cost_per_token": 1.2e-06,
"ocr_cost_per_page": 0.0003,
"source": "https://cloud.google.com/vertex-ai/pricing",
"supported_regions": [
"us-central1"
Expand Down
1 change: 0 additions & 1 deletion model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -45806,7 +45806,6 @@
"mode": "ocr",
"input_cost_per_token": 3e-07,
"output_cost_per_token": 1.2e-06,
"ocr_cost_per_page": 0.0003,
"source": "https://cloud.google.com/vertex-ai/pricing",
"supported_regions": [
"us-central1"
Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from unittest.mock import MagicMock

import httpx
import pytest

from litellm.cost_calculator import completion_cost
from litellm.llms.vertex_ai.ocr.deepseek_transformation import VertexAIDeepSeekOCRConfig

PROMPT_TOKENS = 901
COMPLETION_TOKENS = 212
INPUT_COST_PER_TOKEN = 3e-07
OUTPUT_COST_PER_TOKEN = 1.2e-06


def _deepseek_chat_response() -> httpx.Response:
return httpx.Response(
status_code=200,
json={
"choices": [{"message": {"role": "assistant", "content": "# OCR text"}}],
"usage": {
"prompt_tokens": PROMPT_TOKENS,
"completion_tokens": COMPLETION_TOKENS,
"total_tokens": PROMPT_TOKENS + COMPLETION_TOKENS,
},
},
request=httpx.Request("POST", "https://us-central1-aiplatform.googleapis.com"),
)


@pytest.mark.parametrize("model", ["deepseek-ocr-maas", "deepseek-ai/deepseek-ocr-maas"])
def test_response_is_priced_from_token_usage_for_either_model_name(local_model_cost_map: None, model: str) -> None:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Test lines exceed limit

Several newly added test declarations and assertions exceed the repository's 120-character Python line limit, including this line, line 45, and tests/test_litellm/test_cost_calculator.py:4480; wrap them to keep formatting and lint checks passing.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

response = VertexAIDeepSeekOCRConfig().transform_ocr_response(
model=model,
raw_response=_deepseek_chat_response(),
logging_obj=MagicMock(),
)

cost = completion_cost(
completion_response=response,
model=f"vertex_ai/{model}",
custom_llm_provider="vertex_ai",
call_type="ocr",
)

assert response.model == "deepseek-ai/deepseek-ocr-maas"
assert cost == pytest.approx(PROMPT_TOKENS * INPUT_COST_PER_TOKEN + COMPLETION_TOKENS * OUTPUT_COST_PER_TOKEN)
assert cost > 0


def test_json_content_without_pages_reports_the_canonical_model(local_model_cost_map: None) -> None:
raw_response = httpx.Response(
200,
json={
"choices": [{"message": {"role": "assistant", "content": '{"text": "# OCR text"}'}}],
"usage": {"prompt_tokens": PROMPT_TOKENS, "completion_tokens": COMPLETION_TOKENS},
},
request=httpx.Request("POST", "https://example.invalid"),
)

response = VertexAIDeepSeekOCRConfig().transform_ocr_response(
model="deepseek-ocr-maas",
raw_response=raw_response,
logging_obj=MagicMock(),
)

assert response.model == "deepseek-ai/deepseek-ocr-maas"
assert response.pages[0].markdown == '{"text": "# OCR text"}'
101 changes: 100 additions & 1 deletion tests/test_litellm/test_cost_calculator.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import litellm
from litellm.cost_calculator import (
BaseTokenUsageProcessor,
RealtimeAPITokenUsageProcessor,
completion_cost,
cost_per_token,
handle_realtime_stream_cost_calculation,
ocr_cost,
RealtimeAPITokenUsageProcessor,
response_cost_calculator,
)
from litellm.llms.base_llm.ocr.transformation import OCRPage, OCRResponse, OCRUsageInfo
from litellm.types.llms.openai import OpenAIRealtimeStreamList
from litellm.types.utils import (
CacheCreationTokenDetails,
Expand Down Expand Up @@ -4473,3 +4475,100 @@ def test_explicit_pricing_precedes_private_provider_response_model(
)

assert selected == expected


def test_ocr_cost_prices_token_usage_when_pages_are_not_reported(_local_model_cost_map):
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# OCR text")],
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
usage_info=OCRUsageInfo.model_validate({"prompt_tokens": 901, "completion_tokens": 278, "total_tokens": 1179}),
)

cost = completion_cost(
completion_response=response,
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
custom_llm_provider="vertex_ai",
call_type="ocr",
)

assert cost == pytest.approx(901 * 3e-07 + 278 * 1.2e-06)


def test_ocr_cost_does_not_price_tokens_for_page_priced_models(_local_model_cost_map):
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# OCR text")],
model="mistral/mistral-ocr-latest",
usage_info=OCRUsageInfo.model_validate({"prompt_tokens": 901, "completion_tokens": 278}),
)

with pytest.raises(ValueError, match="pages_processed is None"):
completion_cost(
completion_response=response,
model="mistral/mistral-ocr-latest",
custom_llm_provider="mistral",
call_type="ocr",
)


def test_ocr_cost_prefers_page_pricing_when_pages_are_reported(_local_model_cost_map, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setitem(
litellm.model_cost,
"vertex_ai/page-and-token-ocr",
{
"mode": "ocr",
"litellm_provider": "vertex_ai",
"input_cost_per_token": 3e-07,
"output_cost_per_token": 1.2e-06,
"ocr_cost_per_page": 0.001,
},
)
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# OCR text")],
model="vertex_ai/page-and-token-ocr",
usage_info=OCRUsageInfo.model_validate(
{"pages_processed": 2, "prompt_tokens": 901, "completion_tokens": 278}
),
)

cost = completion_cost(
completion_response=response,
model="vertex_ai/page-and-token-ocr",
custom_llm_provider="vertex_ai",
call_type="ocr",
)

assert cost == pytest.approx(2 * 0.001)


def test_ocr_cost_splits_token_cost_into_prompt_and_completion(_local_model_cost_map):
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# OCR text")],
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
usage_info=OCRUsageInfo.model_validate({"prompt_tokens": 901, "completion_tokens": 278}),
)

prompt_cost, completion_cost_value = ocr_cost(
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
custom_llm_provider="vertex_ai",
response=response,
)

assert prompt_cost == pytest.approx(901 * 3e-07)
assert completion_cost_value == pytest.approx(278 * 1.2e-06)


def test_ocr_cost_stays_zero_when_token_priced_response_lacks_token_counts(_local_model_cost_map):
response = OCRResponse(
pages=[OCRPage(index=0, markdown="# OCR text")],
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
usage_info=OCRUsageInfo(),
)

cost = completion_cost(
completion_response=response,
model="vertex_ai/deepseek-ai/deepseek-ocr-maas",
custom_llm_provider="vertex_ai",
call_type="ocr",
)

assert cost == 0.0
Loading