diff --git a/config.example.yaml b/config.example.yaml deleted file mode 100644 index 5f357fc..0000000 --- a/config.example.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# This file was created automatically at ~/.deepseek-cursor-proxy/config.yaml. -# API keys are read from Cursor's Authorization header and forwarded upstream. - -# `model` is the fallback when a request has no model; Cursor's requested -# DeepSeek model name is otherwise respected. -base_url: https://api.deepseek.com -model: deepseek-v4-pro -thinking: enabled -reasoning_effort: high -display_reasoning: true - -host: 127.0.0.1 -port: 9000 -ngrok: true -verbose: false -request_timeout: 300 -max_request_body_bytes: 20971520 -cors: false - -reasoning_content_path: reasoning_content.sqlite3 -missing_reasoning_strategy: recover -reasoning_cache_max_age_seconds: 604800 -reasoning_cache_max_rows: 10000 diff --git a/src/deepseek_cursor_proxy/config.py b/src/deepseek_cursor_proxy/config.py index ba6590b..aaa53c9 100644 --- a/src/deepseek_cursor_proxy/config.py +++ b/src/deepseek_cursor_proxy/config.py @@ -14,6 +14,23 @@ REASONING_CONTENT_FILE_NAME = "reasoning_content.sqlite3" TRUE_VALUES = {"1", "true", "yes", "on"} FALSE_VALUES = {"0", "false", "no", "off"} MISSING = object() + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 9000 +DEFAULT_UPSTREAM_BASE_URL = "https://api.deepseek.com" +DEFAULT_UPSTREAM_MODEL = "deepseek-v4-pro" +DEFAULT_THINKING = "enabled" +DEFAULT_REASONING_EFFORT = "high" +DEFAULT_CURSOR_DISPLAY_REASONING = True +DEFAULT_NGROK = True +DEFAULT_VERBOSE = False +DEFAULT_REQUEST_TIMEOUT = 300.0 +DEFAULT_MAX_REQUEST_BODY_BYTES = 20 * 1024 * 1024 +DEFAULT_CORS = False +DEFAULT_MISSING_REASONING_STRATEGY = "recover" +DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS = 30 * 24 * 60 * 60 +DEFAULT_REASONING_CACHE_MAX_ROWS = 100_000 + DEFAULT_CONFIG_HEADER = ( "# This file was created automatically at ~/.deepseek-cursor-proxy/config.yaml." ) @@ -22,24 +39,24 @@ DEFAULT_CONFIG_TEXT = f"""{DEFAULT_CONFIG_HEADER} # `model` is the fallback when a request has no model; Cursor's requested # DeepSeek model name is otherwise respected. -base_url: https://api.deepseek.com -model: deepseek-v4-pro -thinking: enabled -reasoning_effort: high -display_reasoning: true +base_url: {DEFAULT_UPSTREAM_BASE_URL} +model: {DEFAULT_UPSTREAM_MODEL} +thinking: {DEFAULT_THINKING} +reasoning_effort: {DEFAULT_REASONING_EFFORT} +display_reasoning: {str(DEFAULT_CURSOR_DISPLAY_REASONING).lower()} -host: 127.0.0.1 -port: 9000 -ngrok: true -verbose: false -request_timeout: 300 -max_request_body_bytes: 20971520 -cors: false +host: {DEFAULT_HOST} +port: {DEFAULT_PORT} +ngrok: {str(DEFAULT_NGROK).lower()} +verbose: {str(DEFAULT_VERBOSE).lower()} +request_timeout: {DEFAULT_REQUEST_TIMEOUT:g} +max_request_body_bytes: {DEFAULT_MAX_REQUEST_BODY_BYTES} +cors: {str(DEFAULT_CORS).lower()} -reasoning_content_path: reasoning_content.sqlite3 -missing_reasoning_strategy: recover -reasoning_cache_max_age_seconds: 604800 -reasoning_cache_max_rows: 10000 +reasoning_content_path: {REASONING_CONTENT_FILE_NAME} +missing_reasoning_strategy: {DEFAULT_MISSING_REASONING_STRATEGY} +reasoning_cache_max_age_seconds: {DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS} +reasoning_cache_max_rows: {DEFAULT_REASONING_CACHE_MAX_ROWS} """ @@ -144,39 +161,39 @@ def settings_from_config( def normalize_thinking(value: Any) -> str: - thinking = as_str(value, "enabled").strip().lower() + thinking = as_str(value, DEFAULT_THINKING).strip().lower() if thinking in {"passthrough", "pass-through", "pass_through"}: return "pass-through" if thinking in {"enabled", "disabled"}: return thinking - return "enabled" + return DEFAULT_THINKING def normalize_missing_reasoning_strategy(value: Any) -> str: - strategy = as_str(value, "recover").strip().lower() + strategy = as_str(value, DEFAULT_MISSING_REASONING_STRATEGY).strip().lower() if strategy in {"recover", "reject"}: return strategy - return "recover" + return DEFAULT_MISSING_REASONING_STRATEGY @dataclass(frozen=True) class ProxyConfig: - host: str = "127.0.0.1" - port: int = 9000 - upstream_base_url: str = "https://api.deepseek.com" - upstream_model: str = "deepseek-v4-pro" - thinking: str = "enabled" - reasoning_effort: str = "high" - request_timeout: float = 300.0 - max_request_body_bytes: int = 20 * 1024 * 1024 + host: str = DEFAULT_HOST + port: int = DEFAULT_PORT + upstream_base_url: str = DEFAULT_UPSTREAM_BASE_URL + upstream_model: str = DEFAULT_UPSTREAM_MODEL + thinking: str = DEFAULT_THINKING + reasoning_effort: str = DEFAULT_REASONING_EFFORT + request_timeout: float = DEFAULT_REQUEST_TIMEOUT + max_request_body_bytes: int = DEFAULT_MAX_REQUEST_BODY_BYTES reasoning_content_path: Path = field(default_factory=default_reasoning_content_path) - missing_reasoning_strategy: str = "recover" - reasoning_cache_max_age_seconds: int = 7 * 24 * 60 * 60 - reasoning_cache_max_rows: int = 10000 - cursor_display_reasoning: bool = True - cors: bool = False - verbose: bool = False - ngrok: bool = False + missing_reasoning_strategy: str = DEFAULT_MISSING_REASONING_STRATEGY + reasoning_cache_max_age_seconds: int = DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS + reasoning_cache_max_rows: int = DEFAULT_REASONING_CACHE_MAX_ROWS + cursor_display_reasoning: bool = DEFAULT_CURSOR_DISPLAY_REASONING + cors: bool = DEFAULT_CORS + verbose: bool = DEFAULT_VERBOSE + ngrok: bool = DEFAULT_NGROK @classmethod def from_file( @@ -189,32 +206,32 @@ class ProxyConfig: return cls( host=as_str( setting_value(settings, "host"), - "127.0.0.1", + DEFAULT_HOST, ), port=as_int( setting_value(settings, "port"), - 9000, + DEFAULT_PORT, ), upstream_base_url=as_str( setting_value(settings, "base_url"), - "https://api.deepseek.com", + DEFAULT_UPSTREAM_BASE_URL, ).rstrip("/"), upstream_model=as_str( setting_value(settings, "model"), - "deepseek-v4-pro", + DEFAULT_UPSTREAM_MODEL, ), thinking=normalize_thinking(setting_value(settings, "thinking")), reasoning_effort=as_str( setting_value(settings, "reasoning_effort"), - "high", + DEFAULT_REASONING_EFFORT, ), request_timeout=as_float( setting_value(settings, "request_timeout"), - 300.0, + DEFAULT_REQUEST_TIMEOUT, ), max_request_body_bytes=as_int( setting_value(settings, "max_request_body_bytes"), - 20 * 1024 * 1024, + DEFAULT_MAX_REQUEST_BODY_BYTES, ), reasoning_content_path=as_path( setting_value(settings, "reasoning_content_path"), @@ -226,26 +243,26 @@ class ProxyConfig: ), reasoning_cache_max_age_seconds=as_int( setting_value(settings, "reasoning_cache_max_age_seconds"), - 7 * 24 * 60 * 60, + DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS, ), reasoning_cache_max_rows=as_int( setting_value(settings, "reasoning_cache_max_rows"), - 10000, + DEFAULT_REASONING_CACHE_MAX_ROWS, ), cursor_display_reasoning=as_bool( setting_value(settings, "display_reasoning"), - True, + DEFAULT_CURSOR_DISPLAY_REASONING, ), cors=as_bool( setting_value(settings, "cors"), - False, + DEFAULT_CORS, ), verbose=as_bool( setting_value(settings, "verbose"), - False, + DEFAULT_VERBOSE, ), ngrok=as_bool( setting_value(settings, "ngrok"), - False, + DEFAULT_NGROK, ), ) diff --git a/tests/test_config.py b/tests/test_config.py index 6d84807..a303053 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -8,6 +8,14 @@ import unittest from unittest.mock import patch from deepseek_cursor_proxy.config import ( + DEFAULT_MISSING_REASONING_STRATEGY, + DEFAULT_NGROK, + DEFAULT_PORT, + DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS, + DEFAULT_REASONING_CACHE_MAX_ROWS, + DEFAULT_THINKING, + DEFAULT_UPSTREAM_MODEL, + DEFAULT_VERBOSE, ProxyConfig, default_config_path, default_reasoning_content_path, @@ -30,6 +38,7 @@ class ConfigTests(unittest.TestCase): ProxyConfig().reasoning_content_path, home / ".deepseek-cursor-proxy" / "reasoning_content.sqlite3", ) + self.assertEqual(ProxyConfig().ngrok, DEFAULT_NGROK) def test_missing_default_config_file_is_populated(self) -> None: with TemporaryDirectory() as temp_dir: @@ -39,17 +48,37 @@ class ConfigTests(unittest.TestCase): config = ProxyConfig.from_file(config_path=None) config_path = default_config_path() + config_text = config_path.read_text(encoding="utf-8") + self.assertTrue(config_path.exists()) + self.assertIn(f"model: {DEFAULT_UPSTREAM_MODEL}", config_text) self.assertIn( - "model: deepseek-v4-pro", config_path.read_text(encoding="utf-8") + f"missing_reasoning_strategy: {DEFAULT_MISSING_REASONING_STRATEGY}", + config_text, ) self.assertIn( - "missing_reasoning_strategy: recover", - config_path.read_text(encoding="utf-8"), + "reasoning_cache_max_age_seconds: " + f"{DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS}", + config_text, ) + self.assertIn( + f"reasoning_cache_max_rows: {DEFAULT_REASONING_CACHE_MAX_ROWS}", + config_text, + ) + self.assertIn(f"ngrok: {str(DEFAULT_NGROK).lower()}", config_text) self.assertEqual(stat.S_IMODE(config_path.stat().st_mode), 0o600) - self.assertEqual(config.upstream_model, "deepseek-v4-pro") - self.assertEqual(config.missing_reasoning_strategy, "recover") + self.assertEqual(config.upstream_model, DEFAULT_UPSTREAM_MODEL) + self.assertEqual(config.ngrok, DEFAULT_NGROK) + self.assertEqual( + config.missing_reasoning_strategy, DEFAULT_MISSING_REASONING_STRATEGY + ) + self.assertEqual( + config.reasoning_cache_max_age_seconds, + DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS, + ) + self.assertEqual( + config.reasoning_cache_max_rows, DEFAULT_REASONING_CACHE_MAX_ROWS + ) def test_missing_explicit_config_file_is_not_populated(self) -> None: with TemporaryDirectory() as temp_dir: @@ -58,7 +87,15 @@ class ConfigTests(unittest.TestCase): config = ProxyConfig.from_file(config_path=config_path) self.assertFalse(config_path.exists()) - self.assertEqual(config.upstream_model, "deepseek-v4-pro") + self.assertEqual(config.upstream_model, DEFAULT_UPSTREAM_MODEL) + self.assertEqual(config.ngrok, DEFAULT_NGROK) + self.assertEqual( + config.reasoning_cache_max_age_seconds, + DEFAULT_REASONING_CACHE_MAX_AGE_SECONDS, + ) + self.assertEqual( + config.reasoning_cache_max_rows, DEFAULT_REASONING_CACHE_MAX_ROWS + ) def test_loads_config_from_user_yaml_file(self) -> None: with TemporaryDirectory() as temp_dir: @@ -124,10 +161,13 @@ class ConfigTests(unittest.TestCase): config = ProxyConfig.from_file(config_path=config_path) - self.assertEqual(config.thinking, "enabled") - self.assertEqual(config.missing_reasoning_strategy, "recover") - self.assertEqual(config.port, 9000) - self.assertFalse(config.verbose) + self.assertEqual(config.thinking, DEFAULT_THINKING) + self.assertEqual( + config.missing_reasoning_strategy, DEFAULT_MISSING_REASONING_STRATEGY + ) + self.assertEqual(config.port, DEFAULT_PORT) + self.assertEqual(config.ngrok, DEFAULT_NGROK) + self.assertEqual(config.verbose, DEFAULT_VERBOSE) def test_relative_reasoning_content_path_in_config_is_relative_to_config_file( self,