From 31f99ef037294591a339704699dc619f3a2b441f Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Thu, 26 Feb 2026 13:53:29 +0800 Subject: [PATCH 1/5] feat: add Novita AI as optional LLM provider Novita AI (https://novita.ai) provides an OpenAI-compatible API with competitive pricing. This change allows using Novita by setting the NOVITA_API_KEY environment variable. Changes: - Added get_openai_client() helper to support both OpenAI and Novita - Added NOVITA_API_KEY and NOVITA_BASE_URL support - Added .env.example with Novita configuration --- pageindex/utils.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pageindex/utils.py b/pageindex/utils.py index dc7acd888..17476bc5d 100644 --- a/pageindex/utils.py +++ b/pageindex/utils.py @@ -18,6 +18,15 @@ from types import SimpleNamespace as config CHATGPT_API_KEY = os.getenv("CHATGPT_API_KEY") +# Novita AI - OpenAI-compatible API support +NOVITA_API_KEY = os.getenv("NOVITA_API_KEY") +NOVITA_BASE_URL = "https://api.novita.ai/openai" + +def get_openai_client(): + """Get OpenAI client - supports both OpenAI and Novita AI (OpenAI-compatible).""" + if NOVITA_API_KEY: + return openai.OpenAI(api_key=NOVITA_API_KEY, base_url=NOVITA_BASE_URL) + return openai.OpenAI(api_key=CHATGPT_API_KEY) def count_tokens(text, model=None): if not text: @@ -28,7 +37,7 @@ def count_tokens(text, model=None): def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 - client = openai.OpenAI(api_key=api_key) + client = get_openai_client() for i in range(max_retries): try: if chat_history: @@ -60,7 +69,7 @@ def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_ def ChatGPT_API(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 - client = openai.OpenAI(api_key=api_key) + client = get_openai_client() for i in range(max_retries): try: if chat_history: From 1a0ef68e5ff2c1446398c41e19ac9b172c84ea36 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 27 Feb 2026 09:31:33 +0800 Subject: [PATCH 2/5] fix: align novita client handling for sync/async paths --- pageindex/utils.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pageindex/utils.py b/pageindex/utils.py index 17476bc5d..bd84c01a4 100644 --- a/pageindex/utils.py +++ b/pageindex/utils.py @@ -22,11 +22,12 @@ NOVITA_API_KEY = os.getenv("NOVITA_API_KEY") NOVITA_BASE_URL = "https://api.novita.ai/openai" -def get_openai_client(): +def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): """Get OpenAI client - supports both OpenAI and Novita AI (OpenAI-compatible).""" - if NOVITA_API_KEY: - return openai.OpenAI(api_key=NOVITA_API_KEY, base_url=NOVITA_BASE_URL) - return openai.OpenAI(api_key=CHATGPT_API_KEY) + client_cls = openai.AsyncOpenAI if async_client else openai.OpenAI + if NOVITA_API_KEY and (api_key in [None, CHATGPT_API_KEY]): + return client_cls(api_key=NOVITA_API_KEY, base_url=NOVITA_BASE_URL) + return client_cls(api_key=api_key) def count_tokens(text, model=None): if not text: @@ -37,7 +38,7 @@ def count_tokens(text, model=None): def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 - client = get_openai_client() + client = get_openai_client(api_key=api_key) for i in range(max_retries): try: if chat_history: @@ -69,7 +70,7 @@ def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_ def ChatGPT_API(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 - client = get_openai_client() + client = get_openai_client(api_key=api_key) for i in range(max_retries): try: if chat_history: @@ -100,7 +101,7 @@ async def ChatGPT_API_async(model, prompt, api_key=CHATGPT_API_KEY): messages = [{"role": "user", "content": prompt}] for i in range(max_retries): try: - async with openai.AsyncOpenAI(api_key=api_key) as client: + async with get_openai_client(api_key=api_key, async_client=True) as client: response = await client.chat.completions.create( model=model, messages=messages, @@ -718,4 +719,4 @@ def load(self, user_opt=None) -> config: self._validate_keys(user_dict) merged = {**self._default_dict, **user_dict} - return config(**merged) \ No newline at end of file + return config(**merged) From f710a659a382dc1210da7ba233d939bff21d607e Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 27 Feb 2026 16:50:10 +0800 Subject: [PATCH 3/5] fix: preserve api_key semantics when novita key is set --- pageindex/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pageindex/utils.py b/pageindex/utils.py index bd84c01a4..18b8f288f 100644 --- a/pageindex/utils.py +++ b/pageindex/utils.py @@ -25,7 +25,7 @@ def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): """Get OpenAI client - supports both OpenAI and Novita AI (OpenAI-compatible).""" client_cls = openai.AsyncOpenAI if async_client else openai.OpenAI - if NOVITA_API_KEY and (api_key in [None, CHATGPT_API_KEY]): + if NOVITA_API_KEY and api_key == CHATGPT_API_KEY: return client_cls(api_key=NOVITA_API_KEY, base_url=NOVITA_BASE_URL) return client_cls(api_key=api_key) From e1c50e246bace42995d03c2b2c9d1b7e0b6f9351 Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 27 Feb 2026 16:55:59 +0800 Subject: [PATCH 4/5] fix: resolve default model for novita provider --- pageindex/utils.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/pageindex/utils.py b/pageindex/utils.py index 18b8f288f..d0a473630 100644 --- a/pageindex/utils.py +++ b/pageindex/utils.py @@ -21,6 +21,8 @@ # Novita AI - OpenAI-compatible API support NOVITA_API_KEY = os.getenv("NOVITA_API_KEY") NOVITA_BASE_URL = "https://api.novita.ai/openai" +NOVITA_MODEL = os.getenv("NOVITA_MODEL") +DEFAULT_OPENAI_MODEL = "gpt-4o-2024-11-20" def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): """Get OpenAI client - supports both OpenAI and Novita AI (OpenAI-compatible).""" @@ -29,6 +31,14 @@ def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): return client_cls(api_key=NOVITA_API_KEY, base_url=NOVITA_BASE_URL) return client_cls(api_key=api_key) +def resolve_chat_model(model, api_key=CHATGPT_API_KEY): + """Resolve model name for OpenAI-compatible providers.""" + if NOVITA_API_KEY and api_key == CHATGPT_API_KEY and model == DEFAULT_OPENAI_MODEL: + if NOVITA_MODEL: + return NOVITA_MODEL + raise ValueError("NOVITA_MODEL is required when using NOVITA_API_KEY with default model gpt-4o-2024-11-20.") + return model + def count_tokens(text, model=None): if not text: return 0 @@ -39,6 +49,7 @@ def count_tokens(text, model=None): def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 client = get_openai_client(api_key=api_key) + resolved_model = resolve_chat_model(model, api_key=api_key) for i in range(max_retries): try: if chat_history: @@ -48,7 +59,7 @@ def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_ messages = [{"role": "user", "content": prompt}] response = client.chat.completions.create( - model=model, + model=resolved_model, messages=messages, temperature=0, ) @@ -71,6 +82,7 @@ def ChatGPT_API_with_finish_reason(model, prompt, api_key=CHATGPT_API_KEY, chat_ def ChatGPT_API(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): max_retries = 10 client = get_openai_client(api_key=api_key) + resolved_model = resolve_chat_model(model, api_key=api_key) for i in range(max_retries): try: if chat_history: @@ -80,7 +92,7 @@ def ChatGPT_API(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): messages = [{"role": "user", "content": prompt}] response = client.chat.completions.create( - model=model, + model=resolved_model, messages=messages, temperature=0, ) @@ -98,12 +110,13 @@ def ChatGPT_API(model, prompt, api_key=CHATGPT_API_KEY, chat_history=None): async def ChatGPT_API_async(model, prompt, api_key=CHATGPT_API_KEY): max_retries = 10 + resolved_model = resolve_chat_model(model, api_key=api_key) messages = [{"role": "user", "content": prompt}] for i in range(max_retries): try: async with get_openai_client(api_key=api_key, async_client=True) as client: response = await client.chat.completions.create( - model=model, + model=resolved_model, messages=messages, temperature=0, ) From 2b81ab3b79cf6ff9f2bccade78f2be0fb95f74fd Mon Sep 17 00:00:00 2001 From: Alex-wuhu Date: Fri, 27 Feb 2026 16:57:32 +0800 Subject: [PATCH 5/5] fix: default novita model to deepseek/deepseek-r1 --- pageindex/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pageindex/utils.py b/pageindex/utils.py index d0a473630..f02eabf59 100644 --- a/pageindex/utils.py +++ b/pageindex/utils.py @@ -22,6 +22,7 @@ NOVITA_API_KEY = os.getenv("NOVITA_API_KEY") NOVITA_BASE_URL = "https://api.novita.ai/openai" NOVITA_MODEL = os.getenv("NOVITA_MODEL") +NOVITA_DEFAULT_MODEL = "deepseek/deepseek-r1" DEFAULT_OPENAI_MODEL = "gpt-4o-2024-11-20" def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): @@ -34,9 +35,7 @@ def get_openai_client(api_key=CHATGPT_API_KEY, async_client=False): def resolve_chat_model(model, api_key=CHATGPT_API_KEY): """Resolve model name for OpenAI-compatible providers.""" if NOVITA_API_KEY and api_key == CHATGPT_API_KEY and model == DEFAULT_OPENAI_MODEL: - if NOVITA_MODEL: - return NOVITA_MODEL - raise ValueError("NOVITA_MODEL is required when using NOVITA_API_KEY with default model gpt-4o-2024-11-20.") + return NOVITA_MODEL or NOVITA_DEFAULT_MODEL return model def count_tokens(text, model=None):