Skip to content

Commit 4c101c5

Browse files
implementing prompt caching in openrouter
1 parent 44b10d0 commit 4c101c5

File tree

5 files changed

+79
-11
lines changed

5 files changed

+79
-11
lines changed

camel/configs/openrouter_config.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
1414
from __future__ import annotations
1515

16-
from typing import Dict, Optional, Sequence, Union
16+
from typing import Any, Dict, Optional, Sequence, Union
1717

1818
from camel.configs.base_config import BaseConfig
1919
from camel.types import NotGiven
@@ -86,6 +86,13 @@ class OpenRouterConfig(BaseConfig):
8686
forces the model to call that tool. :obj:`"none"` is the default
8787
when no tools are present. :obj:`"auto"` is the default if tools
8888
are present. (default: :obj:`None`)
89+
extra_body (dict, optional): Used to pass provider-specific parameters
90+
to OpenRouter. This is where you can specify provider-specific
91+
caching options, such as "anthropic": {"cache_control": {"type": "ephemeral"}}
92+
or other beta features. (default: :obj:`None`)
93+
include_usage (bool, optional): Whether to include token usage in the
94+
response, which is essential for tracking cache hits/misses.
95+
(default: :obj:`None`)
8996
"""
9097

9198
temperature: Optional[float] = None
@@ -101,8 +108,8 @@ class OpenRouterConfig(BaseConfig):
101108
tool_choice: Optional[
102109
Union[Dict[str, Union[str, Dict[str, str]]], str]
103110
] = None
104-
105-
111+
enable_prompt_caching: bool = False
112+
ttl: str = "5m"
106113
OPENROUTER_API_PARAMS = {
107114
param for param in OpenRouterConfig.model_fields.keys()
108115
}

camel/models/openrouter_model.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# limitations under the License.
1313
# ========= Copyright 2023-2026 @ CAMEL-AI.org. All Rights Reserved. =========
1414
import os
15-
from typing import Any, Dict, Optional, Union
15+
from typing import Any, Dict, List, Optional, Union
1616

1717
from camel.configs import OpenRouterConfig
1818
from camel.models.openai_compatible_model import OpenAICompatibleModel
@@ -81,3 +81,47 @@ def __init__(
8181
max_retries=max_retries,
8282
**kwargs,
8383
)
84+
def _prepare_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
85+
if not self.model_config_dict.get("enable_prompt_caching"):
86+
return messages
87+
88+
implicit_caching_prefixes = (
89+
"openai/",
90+
"deepseek/",
91+
"google/gemini-2.5-pro",
92+
"google/gemini-2.5-flash",
93+
"x-ai/",
94+
"moonshotai/"
95+
96+
)
97+
98+
model_name = str(self.model_type).lower()
99+
100+
needs_explicit_caching = not any(
101+
model_name.startswith(p) for p in implicit_caching_prefixes
102+
)
103+
104+
# 4. Apply transformation only if needed
105+
if needs_explicit_caching and messages:
106+
if messages[0].get("role") == "system":
107+
sys_msg = messages[0]
108+
content = sys_msg.get("content")
109+
110+
ttl = self.model_config_dict.get("cache_ttl", "5m")
111+
cache_obj = {"type": "ephemeral"}
112+
113+
if model_name.startswith("anthropic/") and ttl == "1h":
114+
cache_obj["ttl"] = "1h"
115+
116+
if isinstance(content, str):
117+
sys_msg["content"] = [
118+
{
119+
"type": "text",
120+
"text": content,
121+
"cache_control": cache_obj,
122+
}
123+
]
124+
elif isinstance(content, list) and content:
125+
content[-1]["cache_control"] = cache_obj
126+
127+
return messages

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dependencies = [
3535
"pillow>=10.0.0",
3636
"google-search-results>=2.4.2",
3737
"pyyaml>=6.0.3",
38+
"pytest>=7.4.4",
3839
]
3940

4041
[tool.uv]

test/models/test_openrouter_model.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@
1818
from camel.models import OpenRouterModel
1919
from camel.types import ModelType
2020
from camel.utils import OpenAITokenCounter
21-
22-
21+
import os
2322
@pytest.mark.model_backend
2423
@pytest.mark.parametrize(
2524
"model_type",
@@ -38,8 +37,23 @@ def test_openrouter_model(model_type: ModelType):
3837
assert isinstance(model.model_type.value_for_tiktoken, str)
3938
assert isinstance(model.model_type.token_limit, int)
4039

40+
def test_openrouter_gemini_caching(model_type="google/gemma-3-27b-it:free"):
41+
config = {"enable_prompt_caching": True}
42+
model = OpenRouterModel(model_type, model_config_dict=config, api_key=os.environ.get("OPENROUTER_API_KEY"))
43+
44+
messages = [{"role": "system", "content": "Large context..."}]
45+
prepared = model._prepare_messages(messages)
46+
47+
assert isinstance(prepared[0]["content"], list)
48+
assert "cache_control" in prepared[0]["content"][0]
49+
assert prepared[0]["content"][0]["cache_control"]["type"] == "ephemeral"
4150

42-
@pytest.mark.model_backend
43-
def test_openrouter_model_stream_property():
44-
model = OpenRouterModel(ModelType.OPENROUTER_LLAMA_3_1_70B)
45-
assert model.stream is False
51+
def test_openrouter_openai(model_type="openai/gpt-4o-mini"):
52+
config = {"enable_prompt_caching": True}
53+
model = OpenRouterModel(model_type, model_config_dict=config, api_key=os.environ.get("OPENROUTER_AP I_KEY"))
54+
55+
messages = [{"role": "system", "content": "Large context..."}]
56+
prepared = model._prepare_messages(messages)
57+
58+
assert isinstance(prepared[0]["content"], str)
59+
assert "cache_control" not in prepared[0]["content"][0]

uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)