diff --git a/.github/workflows/pypi.yml b/.github/workflows/pypi.yml index 594f161c..c55f409b 100644 --- a/.github/workflows/pypi.yml +++ b/.github/workflows/pypi.yml @@ -29,8 +29,7 @@ jobs: path: dist/ publish-to-pypi: - name: >- - Publish Python 🐍 distribution 📦 to PyPI + name: Publish Python 🐍 distribution 📦 to PyPI if: startsWith(github.ref, 'refs/tags/') # only publish to PyPI on tag pushes needs: - build @@ -51,6 +50,7 @@ jobs: publish-to-testpypi: name: Publish Python 🐍 distribution 📦 to TestPyPI + if: startsWith(github.ref, 'refs/tags/') # only publish to Test PyPI on tag pushes needs: - build runs-on: ubuntu-latest diff --git a/COMMIT_LOG.md b/COMMIT_LOG.md index e0c8558a..5d1329cd 100644 --- a/COMMIT_LOG.md +++ b/COMMIT_LOG.md @@ -1,3 +1,11 @@ +### v0.22.15 - 08/11/2025 + +* **Mon Aug 11 2025:** bump langchain deps +* **Mon Aug 11 2025:** add llm_pre_init/llm_pre_call hooks to provider class. migrate provider_chat_openai to Responses API +* **Mon Aug 11 2025:** use per-provider non-streaming method for title gen if available +* **Mon Aug 11 2025:** change default backend model to GPT 4.1 Nano +* **Sun Aug 10 2025:** add verbosity parameter + ### v0.22.14 - 08/07/2025 * **Thu Aug 07 2025:** gpt-5 diff --git a/lwe/backends/api/conversation_storage_manager.py b/lwe/backends/api/conversation_storage_manager.py index ae6d277c..0d454538 100644 --- a/lwe/backends/api/conversation_storage_manager.py +++ b/lwe/backends/api/conversation_storage_manager.py @@ -215,6 +215,9 @@ def gen_title_thread(self, conversation_id): f"Title generation LLM provider: {provider.name}, model: {getattr(llm, provider.model_property_name, 'N/A')}" ) result = llm.invoke(new_messages) + provider_non_streaming_method = getattr(provider, "handle_non_streaming_response", None) + if provider_non_streaming_method: + result = provider_non_streaming_method(result) request = ApiRequest(orm=self.orm, config=self.config) message, _tool_calls = request.extract_message_content(result) title = message["message"].replace("\n", ", ").strip().strip("'\"") diff --git a/lwe/backends/api/request.py b/lwe/backends/api/request.py index e6467343..eb1ee98b 100644 --- a/lwe/backends/api/request.py +++ b/lwe/backends/api/request.py @@ -305,28 +305,6 @@ def strip_out_messages_over_max_tokens(self, messages, max_tokens): util.print_status_message(False, max_tokens_exceeded_warning) return messages - # TODO: Remove this when o1 models support system messages. - def is_openai_reasoning_model(self): - if self.provider.name == "provider_chat_openai": - model_name = getattr(self.llm, self.provider.model_property_name) - if model_name.startswith("o1") or model_name.startswith("o3") or model_name.startswith("o4") or model_name.startswith("gpt-5"): - return True - return False - - def is_openai_responses_api_series(self): - if self.provider.name == "provider_chat_openai": - model_name = getattr(self.llm, self.provider.model_property_name) - if model_name.startswith("o1-pro") or model_name.startswith("o3-pro"): - return True - return False - - def is_openai_legacy_reasoning_model(self): - if self.provider.name == "provider_chat_openai": - model_name = getattr(self.llm, self.provider.model_property_name) - if model_name.startswith("o1-mini") or model_name.startswith("o1-preview"): - return True - return False - def call_llm(self, messages): """ Call the LLM. @@ -338,13 +316,9 @@ def call_llm(self, messages): """ stream = self.request_overrides.get("stream", False) self.log.debug(f"Calling LLM with message count: {len(messages)}") - # TODO: Remove this when o1 models support system messages. - if self.is_openai_reasoning_model(): - if self.is_openai_responses_api_series(): - self.llm.use_responses_api = True - if self.is_openai_legacy_reasoning_model(): - messages = [{**m, "role": "user"} if m["role"] == "system" else m for m in messages] - self.llm.temperature = 1 + llm_pre_call_method = getattr(self.provider, "llm_pre_call", None) + if llm_pre_call_method: + messages = llm_pre_call_method(self.llm, messages) messages = self.build_chat_request(messages) if stream: return self.execute_llm_streaming(messages) diff --git a/lwe/backends/api/schema/alembic/versions/e7373d57cace_upgrade_openai_presets_to_responses_api.py b/lwe/backends/api/schema/alembic/versions/e7373d57cace_upgrade_openai_presets_to_responses_api.py new file mode 100644 index 00000000..9fc1afc3 --- /dev/null +++ b/lwe/backends/api/schema/alembic/versions/e7373d57cace_upgrade_openai_presets_to_responses_api.py @@ -0,0 +1,61 @@ +"""Upgrade OpenAI presets to Responses API + +Revision ID: e7373d57cace +Revises: 4e642f725923 +Create Date: 2025-08-10 10:22:37.929701 + +""" +from alembic import op +import os +import yaml +import traceback + + +# revision identifiers, used by Alembic. +revision = 'e7373d57cace' +down_revision = '4e642f725923' +branch_labels = None +depends_on = None + + +def upgrade_preset(preset_path): + if not preset_path.endswith(".yaml"): + print(f"Skipping file {preset_path}, not a preset") + return + with open(preset_path, "r") as f: + preset_data = yaml.safe_load(f) + if "metadata" in preset_data and "provider" in preset_data["metadata"] and preset_data["metadata"]["provider"] == "chat_openai": + if "model_customizations" in preset_data and "n" in preset_data["model_customizations"]: + print(f"'n' setting found in preset {preset_path}, removing.") + del preset_data["model_customizations"]["n"] + with open(preset_path, "w") as f: + yaml.safe_dump(preset_data, f) + print(f"Upgraded preset file schema: {preset_path}") + + +def execute_upgrade(): + config_dir = op.get_context().config.attributes["config_dir"] + main_presets_dir = os.path.join(config_dir, "presets") + profiles_dir = os.path.join(config_dir, "profiles") + if os.path.exists(main_presets_dir): + print("Main presets directory found, processing presets...") + for preset_file in os.listdir(main_presets_dir): + upgrade_preset(os.path.join(main_presets_dir, preset_file)) + if os.path.exists(profiles_dir): + print("Profiles directory found, processing profiles...") + for profile in os.listdir(profiles_dir): + profile_dir = os.path.join(profiles_dir, profile) + presets_dir = os.path.join(profile_dir, "presets") + if os.path.exists(presets_dir): + print(f"Presets directory found for profile: {profile}, processing presets...") + for preset_file in os.listdir(presets_dir): + upgrade_preset(os.path.join(presets_dir, preset_file)) + + +def upgrade() -> None: + try: + execute_upgrade() + except Exception as e: + print(f"Error during migration: {e}") + print(traceback.format_exc()) + raise e diff --git a/lwe/core/constants.py b/lwe/core/constants.py index 4692ce5f..d8664f12 100644 --- a/lwe/core/constants.py +++ b/lwe/core/constants.py @@ -10,7 +10,7 @@ ] # Backend specific constants -API_BACKEND_DEFAULT_MODEL = "gpt-4o-mini" +API_BACKEND_DEFAULT_MODEL = "gpt-4.1-nano" SYSTEM_MESSAGE_DEFAULT = "You are a helpful assistant." SYSTEM_MESSAGE_PROGRAMMER = ( diff --git a/lwe/core/provider.py b/lwe/core/provider.py index 12bd193a..0dce40f3 100644 --- a/lwe/core/provider.py +++ b/lwe/core/provider.py @@ -324,6 +324,9 @@ def make_llm(self, customizations=None, tools=None, tool_choice=None, use_defaul for key in constants.PROVIDER_PRIVATE_CUSTOMIZATION_KEYS: final_customizations.pop(key, None) llm_class = self.llm_factory() + llm_pre_init_method = getattr(self, "llm_pre_init", None) + if llm_pre_init_method: + final_customizations = llm_pre_init_method(final_customizations) llm = llm_class(**final_customizations) if tools: self.log.debug(f"Provider {self.display_name} called with tools") diff --git a/lwe/plugins/provider_chat_openai.py b/lwe/plugins/provider_chat_openai.py index afce870d..449d0440 100644 --- a/lwe/plugins/provider_chat_openai.py +++ b/lwe/plugins/provider_chat_openai.py @@ -205,6 +205,30 @@ def prepare_messages_method(self): def llm_factory(self): return CustomChatOpenAI + def llm_pre_init(self, customizations): + model_name = customizations.get(self.model_property_name, "") + if self.is_reasoning_model(model_name): + customizations["temperature"] = 1 + if not self.is_legacy_reasoning_model(model_name): + customizations["use_responses_api"] = True + return customizations + + def llm_pre_call(self, llm, messages): + model_name = getattr(llm, self.model_property_name) + if self.is_legacy_reasoning_model(model_name): + messages = [{**m, "role": "user"} if m["role"] == "system" else m for m in messages] + return messages + + def is_reasoning_model(self, model_name): + if model_name.startswith("o1") or model_name.startswith("o3") or model_name.startswith("o4") or model_name.startswith("gpt-5"): + return True + return False + + def is_legacy_reasoning_model(self, model_name): + if model_name.startswith("o1-mini") or model_name.startswith("o1-preview"): + return True + return False + def format_responses_content(self, content_list): return "".join([content['text'] for content in content_list if 'text' in content]) @@ -226,12 +250,12 @@ def customization_config(self): "model_name": PresetValue(str, options=self.available_models), "temperature": PresetValue(float, min_value=0.0, max_value=2.0), "reasoning_effort": PresetValue(str, options=["low", "medium", "high"], include_none=True), + "verbosity": PresetValue(str, options=["low", "medium", "high"], include_none=True), "openai_api_base": PresetValue(str, include_none=True), "openai_api_key": PresetValue(str, include_none=True, private=True), "openai_organization": PresetValue(str, include_none=True, private=True), "request_timeout": PresetValue(int), "max_retries": PresetValue(int, 1, 10), - "n": PresetValue(int, 1, 10), "max_tokens": PresetValue(int, include_none=True), "top_p": PresetValue(float, min_value=0.0, max_value=1.0), "presence_penalty": PresetValue(float, min_value=-2.0, max_value=2.0), diff --git a/lwe/presets/gpt-4-chatbot-responses.yaml b/lwe/presets/gpt-4-chatbot-responses.yaml index 165e4abd..be4d3786 100644 --- a/lwe/presets/gpt-4-chatbot-responses.yaml +++ b/lwe/presets/gpt-4-chatbot-responses.yaml @@ -4,9 +4,7 @@ metadata: provider: chat_openai system_message: default model_customizations: - max_tokens: null model_name: gpt-4o - n: 1 top_p: 0.5 request_timeout: 60 temperature: 0.5 diff --git a/lwe/presets/gpt-4-code-generation.yaml b/lwe/presets/gpt-4-code-generation.yaml index a0375163..f4d35eee 100644 --- a/lwe/presets/gpt-4-code-generation.yaml +++ b/lwe/presets/gpt-4-code-generation.yaml @@ -4,9 +4,7 @@ metadata: provider: chat_openai system_message: programmer model_customizations: - max_tokens: null model_name: gpt-4-turbo - n: 1 top_p: 0.1 request_timeout: 60 temperature: 0.2 diff --git a/lwe/presets/gpt-4-creative-writing.yaml b/lwe/presets/gpt-4-creative-writing.yaml index e07169f3..be024c9b 100644 --- a/lwe/presets/gpt-4-creative-writing.yaml +++ b/lwe/presets/gpt-4-creative-writing.yaml @@ -4,9 +4,7 @@ metadata: provider: chat_openai system_message: creative_writer model_customizations: - max_tokens: null model_name: gpt-4o - n: 1 top_p: 0.8 request_timeout: 60 temperature: 0.7 diff --git a/lwe/presets/gpt-4o-mini.yaml b/lwe/presets/gpt-4o-mini.yaml index f714ee80..0733f003 100644 --- a/lwe/presets/gpt-4o-mini.yaml +++ b/lwe/presets/gpt-4o-mini.yaml @@ -4,4 +4,3 @@ metadata: provider: chat_openai model_customizations: model_name: gpt-4o-mini - n: 1 diff --git a/lwe/version.py b/lwe/version.py index d89a58bb..e513db1e 100644 --- a/lwe/version.py +++ b/lwe/version.py @@ -1 +1 @@ -__version__ = "0.22.14" +__version__ = "0.22.15" diff --git a/pyproject.toml b/pyproject.toml index b601337e..d4d113aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,10 +26,10 @@ dependencies = [ "email-validator", "Jinja2", "kreuzberg", - "langchain>=0.3.19,<0.4", - "langchain-core>=0.3.39,<0.4", - "langchain-community>=0.3.16,<0.4", - "langchain_openai>=0.3.3", + "langchain>=0.3.27,<0.4", + "langchain-core>=0.3.74,<0.4", + "langchain-community>=0.3.27,<0.4", + "langchain_openai>=0.3.29", "names", "numexpr>=2.8.4", "openpyxl",