Skip to content

Commit cc6c340

Browse files
committed
optimize the config
1 parent b419df8 commit cc6c340

File tree

2 files changed

+233
-17
lines changed

2 files changed

+233
-17
lines changed

src/open_deep_research/configuration.py

Lines changed: 170 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import os
44
from enum import Enum
5-
from typing import Any, List, Optional
5+
from typing import Any, Dict, List, Optional
66

77
from langchain_core.runnables import RunnableConfig
8-
from pydantic import BaseModel, Field
8+
from pydantic import BaseModel, Field, model_validator
99

1010

1111
class SearchAPI(Enum):
@@ -16,6 +16,66 @@ class SearchAPI(Enum):
1616
TAVILY = "tavily"
1717
NONE = "none"
1818

19+
class ModelPreset(Enum):
20+
"""Enumeration of available model presets for quick configuration."""
21+
22+
DEEPSEEK_OPENROUTER = "deepseek_openrouter"
23+
GPT4_OPENAI = "gpt4_openai"
24+
CLAUDE_ANTHROPIC = "claude_anthropic"
25+
GEMINI_GOOGLE = "gemini_google"
26+
CUSTOM = "custom"
27+
28+
# Model preset configurations
29+
MODEL_PRESETS: Dict[ModelPreset, Dict[str, Any]] = {
30+
ModelPreset.DEEPSEEK_OPENROUTER: {
31+
"summarization_model": "openai:gpt-4o-mini",
32+
"research_model": "openai:deepseek/deepseek-chat",
33+
"compression_model": "openai:deepseek/deepseek-chat",
34+
"final_report_model": "openai:deepseek/deepseek-chat",
35+
"summarization_model_max_tokens": 8192,
36+
"research_model_max_tokens": 10000,
37+
"compression_model_max_tokens": 8192,
38+
"final_report_model_max_tokens": 10000,
39+
"description": "使用 OpenRouter API 的 DeepSeek 模型,成本效益高"
40+
},
41+
ModelPreset.GPT4_OPENAI: {
42+
"summarization_model": "openai:gpt-4o-mini",
43+
"research_model": "openai:gpt-4o",
44+
"compression_model": "openai:gpt-4o",
45+
"final_report_model": "openai:gpt-4o",
46+
"summarization_model_max_tokens": 8192,
47+
"research_model_max_tokens": 8192,
48+
"compression_model_max_tokens": 8192,
49+
"final_report_model_max_tokens": 8192,
50+
"description": "使用 OpenAI GPT-4o 模型,性能优秀但成本较高"
51+
},
52+
ModelPreset.CLAUDE_ANTHROPIC: {
53+
"summarization_model": "anthropic:claude-3-5-haiku",
54+
"research_model": "anthropic:claude-3-5-sonnet",
55+
"compression_model": "anthropic:claude-3-5-sonnet",
56+
"final_report_model": "anthropic:claude-3-5-sonnet",
57+
"summarization_model_max_tokens": 8192,
58+
"research_model_max_tokens": 8192,
59+
"compression_model_max_tokens": 8192,
60+
"final_report_model_max_tokens": 8192,
61+
"description": "使用 Anthropic Claude 模型,擅长推理和分析"
62+
},
63+
ModelPreset.GEMINI_GOOGLE: {
64+
"summarization_model": "google:gemini-1.5-flash",
65+
"research_model": "google:gemini-1.5-pro",
66+
"compression_model": "google:gemini-1.5-pro",
67+
"final_report_model": "google:gemini-1.5-pro",
68+
"summarization_model_max_tokens": 8192,
69+
"research_model_max_tokens": 8192,
70+
"compression_model_max_tokens": 8192,
71+
"final_report_model_max_tokens": 8192,
72+
"description": "使用 Google Gemini 模型,支持长上下文"
73+
},
74+
ModelPreset.CUSTOM: {
75+
"description": "自定义模型配置,需要手动设置各个模型参数"
76+
}
77+
}
78+
1979
class MCPConfig(BaseModel):
2080
"""Configuration for Model Context Protocol (MCP) servers."""
2181

@@ -38,6 +98,25 @@ class MCPConfig(BaseModel):
3898
class Configuration(BaseModel):
3999
"""Main configuration class for the Deep Research agent."""
40100

101+
# Model Preset Selection
102+
model_preset: ModelPreset = Field(
103+
default=ModelPreset.DEEPSEEK_OPENROUTER,
104+
metadata={
105+
"x_oap_ui_config": {
106+
"type": "select",
107+
"default": ModelPreset.DEEPSEEK_OPENROUTER.value,
108+
"description": "Choose a model preset for quick configuration. When not CUSTOM, individual model settings will be overridden.",
109+
"options": [
110+
{"label": "DeepSeek (OpenRouter) - 成本效益", "value": ModelPreset.DEEPSEEK_OPENROUTER.value},
111+
{"label": "GPT-4o (OpenAI) - 高性能", "value": ModelPreset.GPT4_OPENAI.value},
112+
{"label": "Claude (Anthropic) - 善于推理", "value": ModelPreset.CLAUDE_ANTHROPIC.value},
113+
{"label": "Gemini (Google) - 长上下文", "value": ModelPreset.GEMINI_GOOGLE.value},
114+
{"label": "Custom - 自定义配置", "value": ModelPreset.CUSTOM.value}
115+
]
116+
}
117+
}
118+
)
119+
41120
# General Configuration
42121
max_structured_output_retries: int = Field(
43122
default=3,
@@ -151,12 +230,12 @@ class Configuration(BaseModel):
151230
}
152231
)
153232
research_model: str = Field(
154-
default="openai:gpt-4.1",
233+
default="openai:deepseek/deepseek-chat",
155234
metadata={
156235
"x_oap_ui_config": {
157236
"type": "text",
158-
"default": "openai:gpt-4.1",
159-
"description": "Model for conducting research. NOTE: Make sure your Researcher Model supports the selected search API."
237+
"default": "openai:deepseek/deepseek-chat",
238+
"description": "Model for conducting research. Use 'openai:model_name' format for OpenRouter models to explicitly use OpenAI provider."
160239
}
161240
}
162241
)
@@ -171,12 +250,12 @@ class Configuration(BaseModel):
171250
}
172251
)
173252
compression_model: str = Field(
174-
default="openai:gpt-4.1",
253+
default="openai:deepseek/deepseek-chat",
175254
metadata={
176255
"x_oap_ui_config": {
177256
"type": "text",
178-
"default": "openai:gpt-4.1",
179-
"description": "Model for compressing research findings from sub-agents. NOTE: Make sure your Compression Model supports the selected search API."
257+
"default": "openai:deepseek/deepseek-chat",
258+
"description": "Model for compressing research findings from sub-agents. Use 'openai:model_name' format for OpenRouter models."
180259
}
181260
}
182261
)
@@ -191,12 +270,12 @@ class Configuration(BaseModel):
191270
}
192271
)
193272
final_report_model: str = Field(
194-
default="openai:gpt-4.1",
273+
default="openai:deepseek/deepseek-chat",
195274
metadata={
196275
"x_oap_ui_config": {
197276
"type": "text",
198-
"default": "openai:gpt-4.1",
199-
"description": "Model for writing the final report from all research findings"
277+
"default": "openai:deepseek/deepseek-chat",
278+
"description": "Model for writing the final report from all research findings. Use 'openai:model_name' format for OpenRouter models."
200279
}
201280
}
202281
)
@@ -231,6 +310,61 @@ class Configuration(BaseModel):
231310
}
232311
}
233312
)
313+
apiKeys: Optional[dict[str, str]] = Field(
314+
default={
315+
"OPENAI_API_KEY": os.environ.get("OPENAI_API_KEY"),
316+
"ANTHROPIC_API_KEY": os.environ.get("ANTHROPIC_API_KEY"),
317+
"GOOGLE_API_KEY": os.environ.get("GOOGLE_API_KEY"),
318+
"OPENROUTER_API_KEY": os.environ.get("OPENROUTER_API_KEY"),
319+
"TAVILY_API_KEY": os.environ.get("TAVILY_API_KEY")
320+
},
321+
optional=True
322+
)
323+
324+
@model_validator(mode='before')
325+
@classmethod
326+
def apply_model_preset(cls, data: Any) -> Any:
327+
"""Apply model preset configuration if not using custom preset."""
328+
if not isinstance(data, dict):
329+
return data
330+
331+
# Get the model preset from the data
332+
model_preset = data.get('model_preset', ModelPreset.DEEPSEEK_OPENROUTER)
333+
334+
# If using custom preset, don't override the values
335+
if model_preset == ModelPreset.CUSTOM:
336+
return data
337+
338+
# Ensure model_preset is a ModelPreset enum
339+
if isinstance(model_preset, str):
340+
try:
341+
model_preset = ModelPreset(model_preset)
342+
except ValueError:
343+
model_preset = ModelPreset.DEEPSEEK_OPENROUTER
344+
345+
# Apply preset configuration
346+
preset_config = MODEL_PRESETS.get(model_preset, {})
347+
348+
# Create a copy of data to modify
349+
result = data.copy()
350+
351+
# Apply preset values for model fields
352+
model_fields = [
353+
'summarization_model', 'research_model', 'compression_model', 'final_report_model',
354+
'summarization_model_max_tokens', 'research_model_max_tokens',
355+
'compression_model_max_tokens', 'final_report_model_max_tokens'
356+
]
357+
358+
for field_name in model_fields:
359+
if field_name in preset_config:
360+
# Only apply preset if the field is not explicitly set by user
361+
if field_name not in data or data[field_name] is None:
362+
result[field_name] = preset_config[field_name]
363+
# For non-custom presets, always apply the preset (override user values)
364+
else:
365+
result[field_name] = preset_config[field_name]
366+
367+
return result
234368

235369

236370
@classmethod
@@ -245,6 +379,31 @@ def from_runnable_config(
245379
for field_name in field_names
246380
}
247381
return cls(**{k: v for k, v in values.items() if v is not None})
382+
383+
def get_preset_description(self) -> str:
384+
"""Get the description of the current model preset."""
385+
preset_config = MODEL_PRESETS.get(self.model_preset, {})
386+
return preset_config.get("description", "Unknown preset")
387+
388+
def get_preset_info(self) -> Dict[str, Any]:
389+
"""Get detailed information about the current model preset."""
390+
preset_config = MODEL_PRESETS.get(self.model_preset, {})
391+
return {
392+
"preset": self.model_preset.value,
393+
"description": preset_config.get("description", "Unknown preset"),
394+
"models": {
395+
"summarization": self.summarization_model,
396+
"research": self.research_model,
397+
"compression": self.compression_model,
398+
"final_report": self.final_report_model
399+
},
400+
"max_tokens": {
401+
"summarization": self.summarization_model_max_tokens,
402+
"research": self.research_model_max_tokens,
403+
"compression": self.compression_model_max_tokens,
404+
"final_report": self.final_report_model_max_tokens
405+
}
406+
}
248407

249408
class Config:
250409
"""Pydantic configuration."""

src/open_deep_research/utils.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -678,7 +678,10 @@ def is_token_limit_exceeded(exception: Exception, model_name: str = None) -> boo
678678
provider = None
679679
if model_name:
680680
model_str = str(model_name).lower()
681-
if model_str.startswith('openai:'):
681+
# Handle OpenRouter models using OpenAI provider format (openai:deepseek/model)
682+
if (model_str.startswith('openai:') or
683+
model_str.startswith('openrouter:') or
684+
('/' in model_str and not model_str.startswith(('anthropic:', 'google:')))):
682685
provider = 'openai'
683686
elif model_str.startswith('anthropic:'):
684687
provider = 'anthropic'
@@ -707,7 +710,7 @@ def _check_openai_token_limit(exception: Exception, error_str: str) -> bool:
707710
class_name = exception.__class__.__name__
708711
module_name = getattr(exception.__class__, '__module__', '')
709712

710-
# Check if this is an OpenAI exception
713+
# Check if this is an OpenAI exception (including OpenRouter using OpenAI API)
711714
is_openai_exception = (
712715
'openai' in exception_type.lower() or
713716
'openai' in module_name.lower()
@@ -826,6 +829,17 @@ def _check_gemini_token_limit(exception: Exception, error_str: str) -> bool:
826829
"bedrock:us.anthropic.claude-sonnet-4-20250514-v1:0": 200000,
827830
"bedrock:us.anthropic.claude-opus-4-20250514-v1:0": 200000,
828831
"anthropic.claude-opus-4-1-20250805-v1:0": 200000,
832+
# OpenRouter models using OpenAI provider format
833+
"openai:deepseek/deepseek-chat": 256000,
834+
"openai:deepseek/deepseek-chat-v3": 256000,
835+
"openai:deepseek/deepseek-chat-v3.1": 256000,
836+
# Legacy OpenRouter models
837+
"openrouter:deepseek/deepseek-chat": 256000,
838+
"openrouter:deepseek/deepseek-chat-v3": 256000,
839+
"openrouter:deepseek/deepseek-chat-v3.1": 256000,
840+
"openrouter:openai/gpt-4o": 128000,
841+
"openrouter:openai/gpt-4o-mini": 128000,
842+
"openrouter:anthropic/claude-3.5-sonnet": 200000,
829843
}
830844

831845
def get_model_token_limit(model_string):
@@ -889,29 +903,72 @@ def get_config_value(value):
889903
else:
890904
return value.value
891905

906+
def get_model_config_for_openrouter(model_name: str, api_key: str) -> dict:
907+
"""Get model configuration for OpenRouter models.
908+
909+
Args:
910+
model_name: The OpenRouter model name (e.g., "openrouter:deepseek/deepseek-chat")
911+
api_key: The OpenRouter API key
912+
913+
Returns:
914+
Dictionary with model configuration including base_url
915+
"""
916+
# Extract the actual model name from the OpenRouter format
917+
if model_name.startswith("openrouter:"):
918+
actual_model = model_name[len("openrouter:"):]
919+
else:
920+
actual_model = model_name
921+
922+
return {
923+
"model": "openai", # Use OpenAI provider for OpenRouter compatibility
924+
"openai_api_base": "https://openrouter.ai/api/v1",
925+
"openai_api_key": api_key,
926+
"model_name": actual_model,
927+
}
928+
892929
def get_api_key_for_model(model_name: str, config: RunnableConfig):
893930
"""Get API key for a specific model from environment or config."""
894931
should_get_from_config = os.getenv("GET_API_KEYS_FROM_CONFIG", "false")
895932
model_name = model_name.lower()
933+
896934
if should_get_from_config.lower() == "true":
897935
api_keys = config.get("configurable", {}).get("apiKeys", {})
898936
if not api_keys:
899937
return None
900-
if model_name.startswith("openai:"):
938+
939+
# Check for OpenRouter models using OpenAI provider (openai:deepseek/model)
940+
if model_name.startswith("openai:") and "/" in model_name:
941+
# This is likely an OpenRouter model using OpenAI provider
942+
return api_keys.get("OPENAI_API_KEY")
943+
elif model_name.startswith("openai:"):
901944
return api_keys.get("OPENAI_API_KEY")
902945
elif model_name.startswith("anthropic:"):
903946
return api_keys.get("ANTHROPIC_API_KEY")
904947
elif model_name.startswith("google"):
905948
return api_keys.get("GOOGLE_API_KEY")
906-
return None
949+
elif model_name.startswith("deepseek"):
950+
return api_keys.get("DEEPSEEK_API_KEY")
951+
elif model_name.startswith("openrouter:"):
952+
return api_keys.get("OPENROUTER_API_KEY")
953+
# For models that don't match any prefix, use OpenAI key (for OpenRouter compatibility)
954+
return api_keys.get("OPENAI_API_KEY")
907955
else:
908-
if model_name.startswith("openai:"):
956+
# Check for OpenRouter models using OpenAI provider (openai:deepseek/model)
957+
if model_name.startswith("openai:") and "/" in model_name:
958+
# This is likely an OpenRouter model using OpenAI provider
959+
return os.getenv("OPENAI_API_KEY")
960+
elif model_name.startswith("openai:"):
909961
return os.getenv("OPENAI_API_KEY")
910962
elif model_name.startswith("anthropic:"):
911963
return os.getenv("ANTHROPIC_API_KEY")
912964
elif model_name.startswith("google"):
913965
return os.getenv("GOOGLE_API_KEY")
914-
return None
966+
elif model_name.startswith("deepseek"):
967+
return os.getenv("DEEPSEEK_API_KEY")
968+
elif model_name.startswith("openrouter:"):
969+
return os.getenv("OPENROUTER_API_KEY")
970+
# For models that don't match any prefix, use OpenAI key (for OpenRouter compatibility)
971+
return os.getenv("OPENAI_API_KEY")
915972

916973
def get_tavily_api_key(config: RunnableConfig):
917974
"""Get Tavily API key from environment or config."""

0 commit comments

Comments
 (0)