From a03377698c9e253388cdb0a36573ae9cce9b82f8 Mon Sep 17 00:00:00 2001 From: openhands Date: Mon, 9 Mar 2026 01:18:53 +0000 Subject: [PATCH] Consume SDK AgentSettings schema in OpenHands Co-authored-by: openhands --- .../__tests__/routes/llm-settings.test.tsx | 22 +- frontend/src/mocks/settings-handlers.ts | 20 +- .../src/utils/sdk-settings-schema.test.ts | 44 +-- .../live_status_app_conversation_service.py | 82 ++++- openhands/server/routes/settings.py | 17 +- openhands/storage/data_models/settings.py | 45 ++- pyproject.toml | 17 +- ...st_live_status_app_conversation_service.py | 93 +++++- tests/unit/server/routes/test_settings_api.py | 56 ++-- .../unit/storage/data_models/test_settings.py | 28 ++ uv.lock | 299 +++++++++++------- 11 files changed, 506 insertions(+), 217 deletions(-) diff --git a/frontend/__tests__/routes/llm-settings.test.tsx b/frontend/__tests__/routes/llm-settings.test.tsx index bfcec75368..eb7689076e 100644 --- a/frontend/__tests__/routes/llm-settings.test.tsx +++ b/frontend/__tests__/routes/llm-settings.test.tsx @@ -62,10 +62,10 @@ describe("LlmSettingsScreen", () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); - expect(screen.getByTestId("sdk-settings-llm_model")).toBeInTheDocument(); - expect(screen.getByTestId("sdk-settings-llm_api_key")).toBeInTheDocument(); + expect(screen.getByTestId("sdk-settings-llm.model")).toBeInTheDocument(); + expect(screen.getByTestId("sdk-settings-llm.api_key")).toBeInTheDocument(); expect( - screen.queryByTestId("sdk-settings-critic_mode"), + screen.queryByTestId("sdk-settings-critic.mode"), ).not.toBeInTheDocument(); }); @@ -77,15 +77,15 @@ describe("LlmSettingsScreen", () => { await screen.findByTestId("llm-settings-screen"); await userEvent.click(screen.getByTestId("llm-settings-advanced-toggle")); - const criticSwitch = screen.getByTestId("sdk-settings-enable_critic"); + const criticSwitch = screen.getByTestId("sdk-settings-critic.enabled"); expect(criticSwitch).toBeInTheDocument(); expect( - screen.queryByTestId("sdk-settings-critic_mode"), + screen.queryByTestId("sdk-settings-critic.mode"), ).not.toBeInTheDocument(); await userEvent.click(criticSwitch); - expect(screen.getByTestId("sdk-settings-critic_mode")).toBeInTheDocument(); + expect(screen.getByTestId("sdk-settings-critic.mode")).toBeInTheDocument(); }); it("starts in advanced mode when advanced sdk values override defaults", async () => { @@ -93,8 +93,8 @@ describe("LlmSettingsScreen", () => { buildSettings({ sdk_settings_values: { ...MOCK_DEFAULT_USER_SETTINGS.sdk_settings_values, - critic_mode: "all_actions", - enable_critic: true, + "critic.mode": "all_actions", + "critic.enabled": true, }, }), ); @@ -102,7 +102,7 @@ describe("LlmSettingsScreen", () => { renderLlmSettingsScreen(); await screen.findByTestId("llm-settings-screen"); - expect(screen.getByTestId("sdk-settings-critic_mode")).toBeInTheDocument(); + expect(screen.getByTestId("sdk-settings-critic.mode")).toBeInTheDocument(); }); it("saves changed schema-driven fields through the generic settings payload", async () => { @@ -113,7 +113,7 @@ describe("LlmSettingsScreen", () => { renderLlmSettingsScreen(); - const llmModelInput = await screen.findByTestId("sdk-settings-llm_model"); + const llmModelInput = await screen.findByTestId("sdk-settings-llm.model"); await userEvent.clear(llmModelInput); await userEvent.type(llmModelInput, "openai/gpt-4o-mini"); await userEvent.click(screen.getByTestId("save-button")); @@ -121,7 +121,7 @@ describe("LlmSettingsScreen", () => { await waitFor(() => { expect(saveSettingsSpy).toHaveBeenCalledWith( expect.objectContaining({ - llm_model: "openai/gpt-4o-mini", + "llm.model": "openai/gpt-4o-mini", }), ); }); diff --git a/frontend/src/mocks/settings-handlers.ts b/frontend/src/mocks/settings-handlers.ts index d240f9052a..991f161a04 100644 --- a/frontend/src/mocks/settings-handlers.ts +++ b/frontend/src/mocks/settings-handlers.ts @@ -4,14 +4,14 @@ import { DEFAULT_SETTINGS } from "#/services/settings"; import { Provider, Settings } from "#/types/settings"; const MOCK_SDK_SETTINGS_SCHEMA: NonNullable = { - model_name: "SDKSettings", + model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { - key: "llm_model", + key: "llm.model", label: "Model", widget: "text", section: "llm", @@ -25,7 +25,7 @@ const MOCK_SDK_SETTINGS_SCHEMA: NonNullable = { required: true, }, { - key: "llm_api_key", + key: "llm.api_key", label: "API key", widget: "password", section: "llm", @@ -45,7 +45,7 @@ const MOCK_SDK_SETTINGS_SCHEMA: NonNullable = { label: "Critic", fields: [ { - key: "enable_critic", + key: "critic.enabled", label: "Enable critic", widget: "boolean", section: "critic", @@ -59,7 +59,7 @@ const MOCK_SDK_SETTINGS_SCHEMA: NonNullable = { required: true, }, { - key: "critic_mode", + key: "critic.mode", label: "Critic mode", widget: "select", section: "critic", @@ -70,7 +70,7 @@ const MOCK_SDK_SETTINGS_SCHEMA: NonNullable = { { label: "finish_and_message", value: "finish_and_message" }, { label: "all_actions", value: "all_actions" }, ], - depends_on: ["enable_critic"], + depends_on: ["critic.enabled"], advanced: true, secret: false, required: true, @@ -103,10 +103,10 @@ export const MOCK_DEFAULT_USER_SETTINGS: Settings = { max_budget_per_task: DEFAULT_SETTINGS.max_budget_per_task, sdk_settings_schema: MOCK_SDK_SETTINGS_SCHEMA, sdk_settings_values: { - critic_mode: "finish_and_message", - enable_critic: false, - llm_api_key: null, - llm_model: DEFAULT_SETTINGS.llm_model, + "critic.mode": "finish_and_message", + "critic.enabled": false, + "llm.api_key": null, + "llm.model": DEFAULT_SETTINGS.llm_model, }, }; diff --git a/frontend/src/utils/sdk-settings-schema.test.ts b/frontend/src/utils/sdk-settings-schema.test.ts index 656caf7e80..c11915d2e8 100644 --- a/frontend/src/utils/sdk-settings-schema.test.ts +++ b/frontend/src/utils/sdk-settings-schema.test.ts @@ -31,14 +31,14 @@ const BASE_SETTINGS: Settings = { search_api_key: "", search_api_key_set: false, sdk_settings_schema: { - model_name: "SDKSettings", + model_name: "AgentSettings", sections: [ { key: "llm", label: "LLM", fields: [ { - key: "llm_model", + key: "llm.model", label: "Model", widget: "text", section: "llm", @@ -52,7 +52,7 @@ const BASE_SETTINGS: Settings = { required: true, }, { - key: "llm_api_key", + key: "llm.api_key", label: "API key", widget: "password", section: "llm", @@ -72,7 +72,7 @@ const BASE_SETTINGS: Settings = { label: "Critic", fields: [ { - key: "enable_critic", + key: "critic.enabled", label: "Enable critic", widget: "boolean", section: "critic", @@ -86,7 +86,7 @@ const BASE_SETTINGS: Settings = { required: true, }, { - key: "critic_mode", + key: "critic.mode", label: "Critic mode", widget: "select", section: "critic", @@ -97,7 +97,7 @@ const BASE_SETTINGS: Settings = { { label: "finish_and_message", value: "finish_and_message" }, { label: "all_actions", value: "all_actions" }, ], - depends_on: ["enable_critic"], + depends_on: ["critic.enabled"], advanced: true, secret: false, required: true, @@ -107,9 +107,9 @@ const BASE_SETTINGS: Settings = { ], }, sdk_settings_values: { - critic_mode: "finish_and_message", - enable_critic: false, - llm_model: "openai/gpt-4o", + "critic.mode": "finish_and_message", + "critic.enabled": false, + "llm.model": "openai/gpt-4o", }, security_analyzer: null, user_consents_to_analytics: false, @@ -119,10 +119,10 @@ const BASE_SETTINGS: Settings = { describe("sdk settings schema helpers", () => { it("builds initial form values from the current settings", () => { expect(buildInitialSettingsFormValues(BASE_SETTINGS)).toEqual({ - critic_mode: "finish_and_message", - enable_critic: false, - llm_api_key: "", - llm_model: "openai/gpt-4o", + "critic.mode": "finish_and_message", + "critic.enabled": false, + "llm.api_key": "", + "llm.model": "openai/gpt-4o", }); }); @@ -134,7 +134,7 @@ describe("sdk settings schema helpers", () => { ...BASE_SETTINGS, sdk_settings_values: { ...BASE_SETTINGS.sdk_settings_values, - critic_mode: "all_actions", + "critic.mode": "all_actions", }, }), ).toBe(true); @@ -165,7 +165,7 @@ describe("sdk settings schema helpers", () => { expect( getVisibleSettingsSections( BASE_SETTINGS.sdk_settings_schema!, - { ...values, enable_critic: true }, + { ...values, "critic.enabled": true }, true, )[1].fields, ).toHaveLength(2); @@ -176,19 +176,19 @@ describe("sdk settings schema helpers", () => { BASE_SETTINGS.sdk_settings_schema!, { ...buildInitialSettingsFormValues(BASE_SETTINGS), - enable_critic: true, - llm_api_key: "new-key", + "critic.enabled": true, + "llm.api_key": "new-key", }, { - enable_critic: true, - llm_api_key: true, - llm_model: false, + "critic.enabled": true, + "llm.api_key": true, + "llm.model": false, }, ); expect(payload).toEqual({ - enable_critic: true, - llm_api_key: "new-key", + "critic.enabled": true, + "llm.api_key": "new-key", }); }); }); diff --git a/openhands/app_server/app_conversation/live_status_app_conversation_service.py b/openhands/app_server/app_conversation/live_status_app_conversation_service.py index b4ca372c16..fe45acce94 100644 --- a/openhands/app_server/app_conversation/live_status_app_conversation_service.py +++ b/openhands/app_server/app_conversation/live_status_app_conversation_service.py @@ -2,6 +2,7 @@ import asyncio import json import logging import os +import re import tempfile import zipfile from collections import defaultdict @@ -80,7 +81,10 @@ from openhands.app_server.utils.llm_metadata import ( from openhands.integrations.provider import ProviderType from openhands.integrations.service_types import SuggestedTask from openhands.sdk import Agent, AgentContext, LocalWorkspace +from openhands.sdk.context.condenser import LLMSummarizingCondenser +from openhands.sdk.critic.impl.api import APIBasedCritic from openhands.sdk.llm import LLM +from openhands.sdk.settings import AgentSettings from openhands.sdk.plugin import PluginSource from openhands.sdk.secret import LookupSecret, SecretValue, StaticSecret from openhands.sdk.utils.paging import page_iterator @@ -113,6 +117,25 @@ Your role ends when the plan is finalized. Implementation is handled by the code """ +def _get_default_critic( + llm: LLM, _settings: AgentSettings, _agent: Agent +) -> APIBasedCritic | None: + base_url = llm.base_url + api_key = llm.api_key + if base_url is None or api_key is None: + return None + + pattern = r'^https?://llm-proxy\.[^./]+\.all-hands\.dev' + if not re.match(pattern, base_url): + return None + + return APIBasedCritic( + server_url=f"{base_url.rstrip('/')}/vllm", + api_key=api_key, + model_name='critic', + ) + + @dataclass class LiveStatusAppConversationService(AppConversationServiceBase): """AppConversationService which combines live status info from the sandbox with stored data.""" @@ -695,6 +718,19 @@ class LiveStatusAppConversationService(AppConversationServiceBase): return secrets + def _get_agent_settings( + self, user: UserInfo, llm_model: str | None + ) -> AgentSettings: + agent_settings = user.to_agent_settings() + if llm_model is None: + return agent_settings + + return agent_settings.model_copy( + update={ + 'llm': agent_settings.llm.model_copy(update={'model': llm_model}), + } + ) + def _configure_llm(self, user: UserInfo, llm_model: str | None) -> LLM: """Configure LLM settings. @@ -705,15 +741,18 @@ class LiveStatusAppConversationService(AppConversationServiceBase): Returns: Configured LLM instance """ - model = llm_model or user.llm_model - base_url = user.llm_base_url - if model and model.startswith('openhands/'): - base_url = user.llm_base_url or self.openhands_provider_base_url + agent_settings = self._get_agent_settings(user, llm_model) + llm_settings = agent_settings.llm + base_url = llm_settings.base_url + if llm_settings.model.startswith('openhands/'): + base_url = llm_settings.base_url or self.openhands_provider_base_url return LLM( - model=model, + model=llm_settings.model, base_url=base_url, - api_key=user.llm_api_key, + api_key=llm_settings.api_key, + timeout=llm_settings.timeout, + max_input_tokens=llm_settings.max_input_tokens, usage_id='agent', ) @@ -933,6 +972,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): secrets: dict[str, SecretValue] | None = None, git_provider: ProviderType | None = None, working_dir: str | None = None, + agent_settings: AgentSettings | None = None, ) -> Agent: """Create an agent with appropriate tools and context based on agent type. @@ -945,16 +985,16 @@ class LiveStatusAppConversationService(AppConversationServiceBase): secrets: Optional dictionary of secrets for authentication git_provider: Optional git provider type for computing plan path working_dir: Optional working directory for computing plan path + agent_settings: Resolved SDK agent settings for this conversation Returns: Configured Agent instance with context """ - # Create condenser with user's settings - condenser = self._create_condenser(llm, agent_type, condenser_max_size) + condenser = None + if agent_settings is None: + condenser = self._create_condenser(llm, agent_type, condenser_max_size) - # Create agent based on type if agent_type == AgentType.PLAN: - # Compute plan path if working_dir is provided plan_path = None if working_dir: plan_path = self._compute_plan_path(working_dir, git_provider) @@ -977,10 +1017,25 @@ class LiveStatusAppConversationService(AppConversationServiceBase): mcp_config=mcp_config, ) - # Prepare system message suffix based on agent type + if agent_settings is not None: + agent = agent_settings.apply_to_agent(agent, critic_factory=_get_default_critic) + if agent_type == AgentType.PLAN and isinstance( + agent.condenser, LLMSummarizingCondenser + ): + agent = agent.model_copy( + update={ + 'condenser': agent.condenser.model_copy( + update={ + 'llm': agent.condenser.llm.model_copy( + update={'usage_id': 'planning_condenser'} + ) + } + ) + } + ) + effective_system_message_suffix = system_message_suffix if agent_type == AgentType.PLAN: - # Prepend planning-specific instruction to prevent "Ready to proceed?" behavior if system_message_suffix: effective_system_message_suffix = ( f'{PLANNING_AGENT_INSTRUCTION}\n\n{system_message_suffix}' @@ -988,7 +1043,6 @@ class LiveStatusAppConversationService(AppConversationServiceBase): else: effective_system_message_suffix = PLANNING_AGENT_INSTRUCTION - # Add agent context agent_context = AgentContext( system_message_suffix=effective_system_message_suffix, secrets=secrets ) @@ -1234,6 +1288,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): # Configure LLM and MCP llm, mcp_config = await self._configure_llm_and_mcp(user, llm_model) + agent_settings = self._get_agent_settings(user, llm_model) # Create agent with context agent = self._create_agent_with_context( @@ -1245,6 +1300,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase): secrets=secrets, git_provider=git_provider, working_dir=working_dir, + agent_settings=agent_settings, ) # Finalize and return the conversation request diff --git a/openhands/server/routes/settings.py b/openhands/server/routes/settings.py index 46951ef54e..809db5ae46 100644 --- a/openhands/server/routes/settings.py +++ b/openhands/server/routes/settings.py @@ -30,7 +30,7 @@ from openhands.server.user_auth import ( get_user_settings, get_user_settings_store, ) -from openhands.storage.data_models.settings import Settings +from openhands.storage.data_models.settings import SDK_LEGACY_FIELD_MAP, Settings from openhands.storage.secrets.secrets_store import SecretsStore from openhands.storage.settings.settings_store import SettingsStore from openhands.utils.llm import get_provider_api_base, is_openhands_model @@ -39,14 +39,13 @@ LITE_LLM_API_URL = os.environ.get( 'LITE_LLM_API_URL', 'https://llm-proxy.app.all-hands.dev' ) - def _get_sdk_settings_schema() -> dict[str, Any] | None: try: settings_module = importlib.import_module('openhands.sdk.settings') except ModuleNotFoundError: return None - return settings_module.SDKSettings.export_schema().model_dump(mode='json') + return settings_module.AgentSettings.export_schema().model_dump(mode='json') def _get_sdk_field_keys(schema: dict[str, Any] | None) -> set[str]: @@ -84,10 +83,12 @@ def _extract_sdk_settings_values( continue if field_key in values: continue - if field_key not in Settings.model_fields: + + legacy_field = SDK_LEGACY_FIELD_MAP.get(field_key) + if legacy_field is None or legacy_field not in Settings.model_fields: continue - values[field_key] = getattr(settings, field_key) + values[field_key] = getattr(settings, legacy_field) return values @@ -104,8 +105,12 @@ def _apply_settings_payload( sdk_settings_values = dict(settings.sdk_settings_values) for key, value in payload.items(): + legacy_field = SDK_LEGACY_FIELD_MAP.get(key) if key in Settings.model_fields: setattr(settings, key, value) + elif legacy_field in Settings.model_fields: + setattr(settings, legacy_field, value) + if key in sdk_field_keys and key not in secret_field_keys: sdk_settings_values[key] = value @@ -177,7 +182,7 @@ async def load_settings( ): settings_with_token_data.llm_base_url = None - settings_with_token_data.sdk_settings_values['llm_base_url'] = ( + settings_with_token_data.sdk_settings_values['llm.base_url'] = ( settings_with_token_data.llm_base_url ) diff --git a/openhands/storage/data_models/settings.py b/openhands/storage/data_models/settings.py index ce992c39fe..922fb2b10b 100644 --- a/openhands/storage/data_models/settings.py +++ b/openhands/storage/data_models/settings.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Annotated +from typing import Annotated, Any from pydantic import ( BaseModel, @@ -17,10 +17,29 @@ from openhands.core.config.llm_config import LLMConfig from openhands.core.config.mcp_config import MCPConfig from openhands.core.config.utils import load_openhands_config from openhands.storage.data_models.secrets import Secrets +from openhands.sdk.settings import AgentSettings + +SDK_LEGACY_FIELD_MAP: dict[str, str] = { + 'llm.model': 'llm_model', + 'llm.api_key': 'llm_api_key', + 'llm.base_url': 'llm_base_url', + 'condenser.enabled': 'enable_default_condenser', + 'condenser.max_size': 'condenser_max_size', +} + + +def _assign_dotted_value(target: dict[str, Any], dotted_key: str, value: Any) -> None: + current = target + parts = dotted_key.split('.') + for part in parts[:-1]: + current = current.setdefault(part, {}) + current[parts[-1]] = value + + class Settings(BaseModel): - """Persisted settings for OpenHands sessions""" + """Persisted settings for OpenHands sessions.""" language: str | None = None agent: str | None = None @@ -192,3 +211,25 @@ class Settings(BaseModel): # Create new settings with merged MCP config self.mcp_config = merged_mcp return self + + def to_agent_settings(self) -> AgentSettings: + """Build SDK AgentSettings from persisted OpenHands settings. + + Values stored in ``sdk_settings_values`` take precedence. Legacy flat fields + are used as a fallback so older stored settings continue to work. + """ + payload: dict[str, Any] = {} + sdk_values = dict(self.sdk_settings_values) + + for key, value in sdk_values.items(): + _assign_dotted_value(payload, key, value) + + for key, legacy_field in SDK_LEGACY_FIELD_MAP.items(): + if key in sdk_values: + continue + legacy_value = getattr(self, legacy_field) + if legacy_value is None: + continue + _assign_dotted_value(payload, key, legacy_value) + + return AgentSettings.model_validate(payload) diff --git a/pyproject.toml b/pyproject.toml index 44ef787e25..8901eb7439 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,7 @@ dependencies = [ "dirhash", "docker", "fastapi", - "fastmcp>=2.12.4,<2.12.5", + "fastmcp>=3.0.0,<4", "google-api-python-client>=2.164", "google-auth-httplib2", "google-auth-oauthlib", @@ -210,7 +210,7 @@ prompt-toolkit = "^3.0.50" poetry = "^2.1.2" anyio = "4.9.0" pythonnet = "*" -fastmcp = "^2.12.4" # Note: 2.12.0+ has breaking auth API changes +fastmcp = "^3.0.0" # Required by openhands-sdk 1.12.0+ mcp = "^1.25.0" # CVE-2025-66416 fix (DNS rebinding protection) python-frontmatter = "^1.1.0" shellingham = "^1.5.4" @@ -250,9 +250,9 @@ e2b-code-interpreter = { version = "^2.0.0", optional = true } pybase62 = "^1.0.0" # V1 dependencies -openhands-sdk = "1.11.5" -openhands-agent-server = "1.11.5" -openhands-tools = "1.11.5" +openhands-sdk = "1.12.0" +openhands-agent-server = "1.12.0" +openhands-tools = "1.12.0" jwcrypto = ">=1.5.6" sqlalchemy = { extras = [ "asyncio" ], version = "^2.0.40" } pg8000 = "^1.31.5" @@ -320,3 +320,10 @@ lint.pydocstyle.convention = "google" concurrency = [ "gevent" ] relative_files = true omit = [ "enterprise/tests/*", "**/test_*" ] + +# Use the SDK issue branch for cross-repo settings schema changes until released. +[tool.uv.sources] +openhands-sdk = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-sdk", branch = "openhands/issue-2228-sdk-settings-schema" } +openhands-agent-server = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-agent-server", branch = "openhands/issue-2228-sdk-settings-schema" } +openhands-tools = { git = "https://github.com/OpenHands/software-agent-sdk.git", subdirectory = "openhands-tools", branch = "openhands/issue-2228-sdk-settings-schema" } + diff --git a/tests/unit/app_server/test_live_status_app_conversation_service.py b/tests/unit/app_server/test_live_status_app_conversation_service.py index b203c240f9..65c5afad8b 100644 --- a/tests/unit/app_server/test_live_status_app_conversation_service.py +++ b/tests/unit/app_server/test_live_status_app_conversation_service.py @@ -5,7 +5,7 @@ import json import os import zipfile from datetime import datetime -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch from uuid import UUID, uuid4 import pytest @@ -36,12 +36,16 @@ from openhands.app_server.user.user_context import UserContext from openhands.integrations.provider import ProviderToken, ProviderType from openhands.integrations.service_types import SuggestedTask, TaskType from openhands.sdk import Agent, Event +from openhands.sdk.critic.impl.api import APIBasedCritic from openhands.sdk.llm import LLM from openhands.sdk.secret import LookupSecret, StaticSecret +from openhands.sdk.settings import AgentSettings from openhands.sdk.workspace import LocalWorkspace from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace from openhands.server.types import AppMode from openhands.storage.data_models.conversation_metadata import ConversationTrigger +from openhands.storage.data_models.settings import Settings + # Env var used by openhands SDK LLM to skip context-window validation (e.g. for gpt-4 in tests) _ALLOW_SHORT_CONTEXT_WINDOWS = 'ALLOW_SHORT_CONTEXT_WINDOWS' @@ -110,12 +114,25 @@ class TestLiveStatusAppConversationService: self.mock_user.condenser_max_size = None # Default to None self.mock_user.llm_base_url = 'https://api.openai.com/v1' self.mock_user.mcp_config = None # Default to None to avoid error handling path + self.mock_user.sdk_settings_values = {} + self.mock_user.to_agent_settings = Mock(side_effect=self._mock_user_to_agent_settings) # Mock sandbox self.mock_sandbox = Mock(spec=SandboxInfo) self.mock_sandbox.id = uuid4() self.mock_sandbox.status = SandboxStatus.RUNNING + def _mock_user_to_agent_settings(self) -> AgentSettings: + return Settings( + llm_model=self.mock_user.llm_model, + llm_api_key=self.mock_user.llm_api_key, + llm_base_url=self.mock_user.llm_base_url, + enable_default_condenser=True, + condenser_max_size=self.mock_user.condenser_max_size, + sdk_settings_values=dict(self.mock_user.sdk_settings_values), + ).to_agent_settings() + + def test_apply_suggested_task_sets_prompt_and_trigger(self): """Test suggested task prompts populate initial message and trigger.""" suggested_task = SuggestedTask( @@ -481,6 +498,24 @@ class TestLiveStatusAppConversationService: == 'mcp_api_key' ) + @pytest.mark.asyncio + async def test_configure_llm_and_mcp_uses_sdk_agent_settings(self): + """SDK AgentSettings values should drive the configured LLM.""" + self.mock_user.sdk_settings_values = { + 'llm.model': 'sdk-model', + 'llm.base_url': 'https://sdk-llm.example.com', + 'llm.timeout': 123, + 'llm.max_input_tokens': 456, + } + self.mock_user_context.get_mcp_api_key.return_value = None + + llm, _ = await self.service._configure_llm_and_mcp(self.mock_user, None) + + assert llm.model == 'sdk-model' + assert llm.base_url == 'https://sdk-llm.example.com' + assert llm.timeout == 123 + assert llm.max_input_tokens == 456 + @pytest.mark.asyncio async def test_configure_llm_and_mcp_openhands_model_prefers_user_base_url(self): """openhands/* model uses user.llm_base_url when provided.""" @@ -902,6 +937,44 @@ class TestLiveStatusAppConversationService: mock_llm, AgentType.DEFAULT, self.mock_user.condenser_max_size ) + + @patch( + 'openhands.app_server.app_conversation.live_status_app_conversation_service.get_default_tools', + return_value=[], + ) + def test_create_agent_with_context_applies_sdk_agent_settings(self, _mock_get_tools): + """Resolved SDK AgentSettings should affect V1 agent startup.""" + llm = LLM( + model='openhands/default', + base_url='https://llm-proxy.app.all-hands.dev', + api_key=SecretStr('test_api_key'), + ) + agent_settings = AgentSettings.model_validate( + { + 'llm': { + 'model': 'openhands/default', + 'base_url': 'https://llm-proxy.app.all-hands.dev', + 'api_key': 'test_api_key', + }, + 'condenser': {'enabled': False}, + 'critic': {'enabled': True, 'mode': 'all_actions'}, + } + ) + + agent = self.service._create_agent_with_context( + llm, + AgentType.DEFAULT, + None, + {}, + condenser_max_size=None, + agent_settings=agent_settings, + ) + + assert agent.condenser is None + assert isinstance(agent.critic, APIBasedCritic) + assert agent.critic.mode == 'all_actions' + + @patch( 'openhands.app_server.app_conversation.live_status_app_conversation_service.get_planning_tools' ) @@ -1171,6 +1244,7 @@ class TestLiveStatusAppConversationService: self.service._configure_llm_and_mcp = AsyncMock( return_value=(mock_llm, mock_mcp_config) ) + self.service._get_agent_settings = Mock(return_value=Mock(spec=AgentSettings)) self.service._create_agent_with_context = Mock(return_value=mock_agent) self.service._finalize_conversation_request = AsyncMock( return_value=mock_final_request @@ -1199,6 +1273,7 @@ class TestLiveStatusAppConversationService: self.service._configure_llm_and_mcp.assert_called_once_with( self.mock_user, 'gpt-4' ) + self.service._get_agent_settings.assert_called_once_with(self.mock_user, 'gpt-4') self.service._create_agent_with_context.assert_called_once_with( mock_llm, AgentType.DEFAULT, @@ -1208,6 +1283,7 @@ class TestLiveStatusAppConversationService: secrets=mock_secrets, git_provider=ProviderType.GITHUB, working_dir='/test/dir', + agent_settings=ANY, ) self.service._finalize_conversation_request.assert_called_once() @@ -2039,12 +2115,27 @@ class TestPluginHandling: self.mock_user.condenser_max_size = None self.mock_user.mcp_config = None self.mock_user.security_analyzer = None + self.mock_user.sdk_settings_values = {} + self.mock_user.to_agent_settings = Mock( + side_effect=self._mock_user_to_agent_settings + ) # Mock sandbox self.mock_sandbox = Mock(spec=SandboxInfo) self.mock_sandbox.id = uuid4() self.mock_sandbox.status = SandboxStatus.RUNNING + + def _mock_user_to_agent_settings(self) -> AgentSettings: + return Settings( + llm_model=self.mock_user.llm_model, + llm_api_key=self.mock_user.llm_api_key, + llm_base_url=self.mock_user.llm_base_url, + enable_default_condenser=True, + condenser_max_size=self.mock_user.condenser_max_size, + sdk_settings_values=dict(self.mock_user.sdk_settings_values), + ).to_agent_settings() + def test_construct_initial_message_with_plugin_params_no_plugins(self): """Test _construct_initial_message_with_plugin_params with no plugins returns original message.""" from openhands.agent_server.models import SendMessageRequest, TextContent diff --git a/tests/unit/server/routes/test_settings_api.py b/tests/unit/server/routes/test_settings_api.py index 1addd88bf4..2eb270756b 100644 --- a/tests/unit/server/routes/test_settings_api.py +++ b/tests/unit/server/routes/test_settings_api.py @@ -83,25 +83,27 @@ def test_client(): async def test_settings_api_endpoints(test_client): """Test that the settings API endpoints work with the new auth system.""" sdk_settings_schema = { - 'model_name': 'SDKSettings', + 'model_name': 'AgentSettings', 'sections': [ { 'key': 'llm', 'label': 'LLM', 'fields': [ - {'key': 'llm_timeout'}, - {'key': 'llm_api_key', 'secret': True}, + {'key': 'llm.model'}, + {'key': 'llm.base_url'}, + {'key': 'llm.timeout'}, + {'key': 'llm.api_key', 'secret': True}, ], }, { 'key': 'critic', 'label': 'Critic', 'fields': [ - {'key': 'enable_critic'}, - {'key': 'critic_mode'}, - {'key': 'enable_iterative_refinement'}, - {'key': 'critic_threshold'}, - {'key': 'max_refinement_iterations'}, + {'key': 'critic.enabled'}, + {'key': 'critic.mode'}, + {'key': 'critic.enable_iterative_refinement'}, + {'key': 'critic.threshold'}, + {'key': 'critic.max_refinement_iterations'}, ], }, ], @@ -114,16 +116,16 @@ async def test_settings_api_endpoints(test_client): 'max_iterations': 100, 'security_analyzer': 'default', 'confirmation_mode': True, - 'llm_model': 'test-model', - 'llm_api_key': 'test-key', - 'llm_base_url': 'https://test.com', - 'llm_timeout': 123, + 'llm.model': 'test-model', + 'llm.api_key': 'test-key', + 'llm.base_url': 'https://test.com', + 'llm.timeout': 123, 'remote_runtime_resource_factor': 2, - 'enable_critic': True, - 'critic_mode': 'all_actions', - 'enable_iterative_refinement': True, - 'critic_threshold': 0.7, - 'max_refinement_iterations': 4, + 'critic.enabled': True, + 'critic.mode': 'all_actions', + 'critic.enable_iterative_refinement': True, + 'critic.threshold': 0.7, + 'critic.max_refinement_iterations': 4, } with patch( @@ -140,16 +142,18 @@ async def test_settings_api_endpoints(test_client): response = test_client.get('/api/settings') assert response.status_code == 200 response_data = response.json() - assert response_data['sdk_settings_schema']['model_name'] == 'SDKSettings' - assert response_data['sdk_settings_values']['llm_timeout'] == 123 - assert response_data['sdk_settings_values']['enable_critic'] is True - assert response_data['sdk_settings_values']['critic_mode'] == 'all_actions' + assert response_data['sdk_settings_schema']['model_name'] == 'AgentSettings' + assert response_data['sdk_settings_values']['llm.model'] == 'test-model' + assert response_data['sdk_settings_values']['llm.timeout'] == 123 + assert response_data['sdk_settings_values']['critic.enabled'] is True + assert response_data['sdk_settings_values']['critic.mode'] == 'all_actions' assert ( - response_data['sdk_settings_values']['enable_iterative_refinement'] is True + response_data['sdk_settings_values']['critic.enable_iterative_refinement'] + is True ) - assert response_data['sdk_settings_values']['critic_threshold'] == 0.7 - assert response_data['sdk_settings_values']['max_refinement_iterations'] == 4 - assert response_data['sdk_settings_values']['llm_api_key'] is None + assert response_data['sdk_settings_values']['critic.threshold'] == 0.7 + assert response_data['sdk_settings_values']['critic.max_refinement_iterations'] == 4 + assert response_data['sdk_settings_values']['llm.api_key'] is None # Test updating with partial settings partial_settings = { @@ -163,7 +167,7 @@ async def test_settings_api_endpoints(test_client): response = test_client.get('/api/settings') assert response.status_code == 200 - assert response.json()['sdk_settings_values']['llm_timeout'] == 123 + assert response.json()['sdk_settings_values']['llm.timeout'] == 123 # Test the unset-provider-tokens endpoint response = test_client.post('/api/unset-provider-tokens') diff --git a/tests/unit/storage/data_models/test_settings.py b/tests/unit/storage/data_models/test_settings.py index a4bec95801..998d333bd6 100644 --- a/tests/unit/storage/data_models/test_settings.py +++ b/tests/unit/storage/data_models/test_settings.py @@ -106,6 +106,34 @@ def test_settings_preserve_sdk_settings_values(): } + +def test_settings_to_agent_settings_prefers_sdk_values_and_legacy_fallbacks(): + settings = Settings( + llm_model='legacy-model', + llm_api_key='legacy-key', + llm_base_url='https://legacy.example.com', + enable_default_condenser=True, + condenser_max_size=88, + sdk_settings_values={ + 'llm.model': 'sdk-model', + 'condenser.enabled': False, + 'critic.enabled': True, + 'critic.mode': 'all_actions', + }, + ) + + agent_settings = settings.to_agent_settings() + + assert agent_settings.llm.model == 'sdk-model' + assert agent_settings.llm.api_key.get_secret_value() == 'legacy-key' + assert agent_settings.llm.base_url == 'https://legacy.example.com' + assert agent_settings.condenser.enabled is False + assert agent_settings.condenser.max_size == 88 + assert agent_settings.critic.enabled is True + assert agent_settings.critic.mode == 'all_actions' + + + def test_settings_no_pydantic_frozen_field_warning(): """Test that Settings model does not trigger Pydantic UnsupportedFieldAttributeWarning. diff --git a/uv.lock b/uv.lock index c266974d4d..0ced3237b1 100644 --- a/uv.lock +++ b/uv.lock @@ -6,6 +6,30 @@ resolution-markers = [ "python_full_version < '3.13'", ] +[[package]] +name = "agent-client-protocol" +version = "0.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/7b/7cdac86db388809d9e3bc58cac88cc7dfa49b7615b98fab304a828cd7f8a/agent_client_protocol-0.8.1.tar.gz", hash = "sha256:1bbf15663bf51f64942597f638e32a6284c5da918055d9672d3510e965143dbd", size = 68866, upload-time = "2026-02-13T15:34:54.567Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/f3/219eeca0ad4a20843d4b9eaac5532f87018b9d25730a62a16f54f6c52d1a/agent_client_protocol-0.8.1-py3-none-any.whl", hash = "sha256:9421a11fd435b4831660272d169c3812d553bb7247049c138c3ca127e4b8af8e", size = 54529, upload-time = "2026-02-13T15:34:53.344Z" }, +] + +[[package]] +name = "aiofile" +version = "3.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "caio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/e2/d7cb819de8df6b5c1968a2756c3cb4122d4fa2b8fc768b53b7c9e5edb646/aiofile-3.9.0.tar.gz", hash = "sha256:e5ad718bb148b265b6df1b3752c4d1d83024b93da9bd599df74b9d9ffcf7919b", size = 17943, upload-time = "2024-10-08T10:39:35.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/25/da1f0b4dd970e52bf5a36c204c107e11a0c6d3ed195eba0bfbc664c312b2/aiofile-3.9.0-py3-none-any.whl", hash = "sha256:ce2f6c1571538cbdfa0143b04e16b208ecb0e9cb4148e528af8a640ed51cc8aa", size = 19539, upload-time = "2024-10-08T10:39:32.955Z" }, +] + [[package]] name = "aiofiles" version = "24.1.0" @@ -373,6 +397,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/be/6985abb1011fda8a523cfe21ed9629e397d6e06fb5bae99750402b25c95b/bashlex-0.18-py2.py3-none-any.whl", hash = "sha256:91d73a23a3e51711919c1c899083890cdecffc91d8c088942725ac13e9dcfffa", size = 69539, upload-time = "2023-01-18T15:21:24.167Z" }, ] +[[package]] +name = "beartype" +version = "0.22.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/94/1009e248bbfbab11397abca7193bea6626806be9a327d399810d523a07cb/beartype-0.22.9.tar.gz", hash = "sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f", size = 1608866, upload-time = "2025-12-13T06:50:30.72Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/cc/18245721fa7747065ab478316c7fea7c74777d07f37ae60db2e84f8172e8/beartype-0.22.9-py3-none-any.whl", hash = "sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2", size = 1333658, upload-time = "2025-12-13T06:50:28.266Z" }, +] + [[package]] name = "beautifulsoup4" version = "4.14.3" @@ -600,6 +633,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080, upload-time = "2025-02-20T21:01:16.647Z" }, ] +[[package]] +name = "caio" +version = "0.9.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/92/88/b8527e1b00c1811db339a1df8bd1ae49d146fcea9d6a5c40e3a80aaeb38d/caio-0.9.25.tar.gz", hash = "sha256:16498e7f81d1d0f5a4c0ad3f2540e65fe25691376e0a5bd367f558067113ed10", size = 26781, upload-time = "2025-12-26T15:21:36.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/25/79c98ebe12df31548ba4eaf44db11b7cad6b3e7b4203718335620939083c/caio-0.9.25-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fb7ff95af4c31ad3f03179149aab61097a71fd85e05f89b4786de0359dffd044", size = 36983, upload-time = "2025-12-26T15:21:36.075Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/21288691f16d479945968a0a4f2856818c1c5be56881d51d4dac9b255d26/caio-0.9.25-cp312-cp312-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:97084e4e30dfa598449d874c4d8e0c8d5ea17d2f752ef5e48e150ff9d240cd64", size = 82012, upload-time = "2025-12-26T15:22:20.983Z" }, + { url = "https://files.pythonhosted.org/packages/03/c4/8a1b580875303500a9c12b9e0af58cb82e47f5bcf888c2457742a138273c/caio-0.9.25-cp312-cp312-manylinux_2_34_aarch64.whl", hash = "sha256:4fa69eba47e0f041b9d4f336e2ad40740681c43e686b18b191b6c5f4c5544bfb", size = 81502, upload-time = "2026-03-04T22:08:22.381Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/0fe770b8ffc8362c48134d1592d653a81a3d8748d764bec33864db36319d/caio-0.9.25-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:6bebf6f079f1341d19f7386db9b8b1f07e8cc15ae13bfdaff573371ba0575d69", size = 80200, upload-time = "2026-03-04T22:08:23.382Z" }, + { url = "https://files.pythonhosted.org/packages/31/57/5e6ff127e6f62c9f15d989560435c642144aa4210882f9494204bc892305/caio-0.9.25-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d6c2a3411af97762a2b03840c3cec2f7f728921ff8adda53d7ea2315a8563451", size = 36979, upload-time = "2025-12-26T15:21:35.484Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/f21af50e72117eb528c422d4276cbac11fb941b1b812b182e0a9c70d19c5/caio-0.9.25-cp313-cp313-manylinux2010_x86_64.manylinux2014_x86_64.manylinux_2_12_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0998210a4d5cd5cb565b32ccfe4e53d67303f868a76f212e002a8554692870e6", size = 81900, upload-time = "2025-12-26T15:22:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/9c/12/c39ae2a4037cb10ad5eb3578eb4d5f8c1a2575c62bba675f3406b7ef0824/caio-0.9.25-cp313-cp313-manylinux_2_34_aarch64.whl", hash = "sha256:1a177d4777141b96f175fe2c37a3d96dec7911ed9ad5f02bac38aaa1c936611f", size = 81523, upload-time = "2026-03-04T22:08:25.187Z" }, + { url = "https://files.pythonhosted.org/packages/22/59/f8f2e950eb4f1a5a3883e198dca514b9d475415cb6cd7b78b9213a0dd45a/caio-0.9.25-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:9ed3cfb28c0e99fec5e208c934e5c157d0866aa9c32aa4dc5e9b6034af6286b7", size = 80243, upload-time = "2026-03-04T22:08:26.449Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/1f76c8d1bafe3b0614e06b2195784a3765bbf7b0a067661af9e2dd47fc33/caio-0.9.25-py3-none-any.whl", hash = "sha256:06c0bb02d6b929119b1cfbe1ca403c768b2013a369e2db46bfa2a5761cf82e40", size = 19087, upload-time = "2025-12-26T15:22:00.221Z" }, +] + [[package]] name = "cdp-use" version = "1.4.5" @@ -1290,6 +1340,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "fakeredis" +version = "2.34.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "redis" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, +] + +[package.optional-dependencies] +lua = [ + { name = "lupa" }, +] + [[package]] name = "farama-notifications" version = "0.0.4" @@ -1325,24 +1393,34 @@ wheels = [ [[package]] name = "fastmcp" -version = "2.12.4" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, { name = "httpx" }, + { name = "jsonref" }, + { name = "jsonschema-path" }, { name = "mcp" }, - { name = "openapi-core" }, { name = "openapi-pydantic" }, + { name = "opentelemetry-api" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, { name = "python-dotenv" }, + { name = "pyyaml" }, { name = "rich" }, + { name = "uncalled-for" }, + { name = "uvicorn" }, + { name = "watchfiles" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/b2/57845353a9bc63002995a982e66f3d0be4ec761e7bcb89e7d0638518d42a/fastmcp-2.12.4.tar.gz", hash = "sha256:b55fe89537038f19d0f4476544f9ca5ac171033f61811cc8f12bdeadcbea5016", size = 7167745, upload-time = "2025-09-26T16:43:27.71Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/70/862026c4589441f86ad3108f05bfb2f781c6b322ad60a982f40b303b47d7/fastmcp-3.1.0.tar.gz", hash = "sha256:e25264794c734b9977502a51466961eeecff92a0c2f3b49c40c070993628d6d0", size = 17347083, upload-time = "2026-03-03T02:43:11.283Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/c7/562ff39f25de27caec01e4c1e88cbb5fcae5160802ba3d90be33165df24f/fastmcp-2.12.4-py3-none-any.whl", hash = "sha256:56188fbbc1a9df58c537063f25958c57b5c4d715f73e395c41b51550b247d140", size = 329090, upload-time = "2025-09-26T16:43:25.314Z" }, + { url = "https://files.pythonhosted.org/packages/17/07/516f5b20d88932e5a466c2216b628e5358a71b3a9f522215607c3281de05/fastmcp-3.1.0-py3-none-any.whl", hash = "sha256:b1f73b56fd3b0cb2bd9e2a144fc650d5cc31587ed129d996db7710e464ae8010", size = 633749, upload-time = "2026-03-03T02:43:09.06Z" }, ] [[package]] @@ -2236,15 +2314,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/6d/0d9848617b9f753b87f214f1c682592f7ca42de085f564352f10f0843026/ipywidgets-8.1.8-py3-none-any.whl", hash = "sha256:ecaca67aed704a338f88f67b1181b58f821ab5dc89c1f0f5ef99db43c1c2921e", size = 139808, upload-time = "2025-11-01T21:18:10.956Z" }, ] -[[package]] -name = "isodate" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705, upload-time = "2024-10-08T23:04:11.5Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, -] - [[package]] name = "isoduration" version = "20.11.0" @@ -2411,6 +2480,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, ] +[[package]] +name = "jsonref" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/0d/c1f3277e90ccdb50d33ed5ba1ec5b3f0a242ed8c1b1a85d3afeb68464dca/jsonref-1.1.0.tar.gz", hash = "sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552", size = 8814, upload-time = "2023-01-16T16:10:04.455Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/ec/e1db9922bceb168197a558a2b8c03a7963f1afe93517ddd3cf99f202f996/jsonref-1.1.0-py3-none-any.whl", hash = "sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9", size = 9425, upload-time = "2023-01-16T16:10:02.255Z" }, +] + [[package]] name = "jsonschema" version = "4.26.0" @@ -2752,32 +2830,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] -[[package]] -name = "lazy-object-proxy" -version = "1.12.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/08/a2/69df9c6ba6d316cfd81fe2381e464db3e6de5db45f8c43c6a23504abf8cb/lazy_object_proxy-1.12.0.tar.gz", hash = "sha256:1f5a462d92fd0cfb82f1fab28b51bfb209fabbe6aabf7f0d51472c0c124c0c61", size = 43681, upload-time = "2025-08-22T13:50:06.783Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/1b/b5f5bd6bda26f1e15cd3232b223892e4498e34ec70a7f4f11c401ac969f1/lazy_object_proxy-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ee0d6027b760a11cc18281e702c0309dd92da458a74b4c15025d7fc490deede", size = 26746, upload-time = "2025-08-22T13:42:37.572Z" }, - { url = "https://files.pythonhosted.org/packages/55/64/314889b618075c2bfc19293ffa9153ce880ac6153aacfd0a52fcabf21a66/lazy_object_proxy-1.12.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ab2c584e3cc8be0dfca422e05ad30a9abe3555ce63e9ab7a559f62f8dbc6ff9", size = 71457, upload-time = "2025-08-22T13:42:38.743Z" }, - { url = "https://files.pythonhosted.org/packages/11/53/857fc2827fc1e13fbdfc0ba2629a7d2579645a06192d5461809540b78913/lazy_object_proxy-1.12.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14e348185adbd03ec17d051e169ec45686dcd840a3779c9d4c10aabe2ca6e1c0", size = 71036, upload-time = "2025-08-22T13:42:40.184Z" }, - { url = "https://files.pythonhosted.org/packages/2b/24/e581ffed864cd33c1b445b5763d617448ebb880f48675fc9de0471a95cbc/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c4fcbe74fb85df8ba7825fa05eddca764138da752904b378f0ae5ab33a36c308", size = 69329, upload-time = "2025-08-22T13:42:41.311Z" }, - { url = "https://files.pythonhosted.org/packages/78/be/15f8f5a0b0b2e668e756a152257d26370132c97f2f1943329b08f057eff0/lazy_object_proxy-1.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:563d2ec8e4d4b68ee7848c5ab4d6057a6d703cb7963b342968bb8758dda33a23", size = 70690, upload-time = "2025-08-22T13:42:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/5d/aa/f02be9bbfb270e13ee608c2b28b8771f20a5f64356c6d9317b20043c6129/lazy_object_proxy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:53c7fd99eb156bbb82cbc5d5188891d8fdd805ba6c1e3b92b90092da2a837073", size = 26563, upload-time = "2025-08-22T13:42:43.685Z" }, - { url = "https://files.pythonhosted.org/packages/f4/26/b74c791008841f8ad896c7f293415136c66cc27e7c7577de4ee68040c110/lazy_object_proxy-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:86fd61cb2ba249b9f436d789d1356deae69ad3231dc3c0f17293ac535162672e", size = 26745, upload-time = "2025-08-22T13:42:44.982Z" }, - { url = "https://files.pythonhosted.org/packages/9b/52/641870d309e5d1fb1ea7d462a818ca727e43bfa431d8c34b173eb090348c/lazy_object_proxy-1.12.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:81d1852fb30fab81696f93db1b1e55a5d1ff7940838191062f5f56987d5fcc3e", size = 71537, upload-time = "2025-08-22T13:42:46.141Z" }, - { url = "https://files.pythonhosted.org/packages/47/b6/919118e99d51c5e76e8bf5a27df406884921c0acf2c7b8a3b38d847ab3e9/lazy_object_proxy-1.12.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9045646d83f6c2664c1330904b245ae2371b5c57a3195e4028aedc9f999655", size = 71141, upload-time = "2025-08-22T13:42:47.375Z" }, - { url = "https://files.pythonhosted.org/packages/e5/47/1d20e626567b41de085cf4d4fb3661a56c159feaa73c825917b3b4d4f806/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:67f07ab742f1adfb3966c40f630baaa7902be4222a17941f3d85fd1dae5565ff", size = 69449, upload-time = "2025-08-22T13:42:48.49Z" }, - { url = "https://files.pythonhosted.org/packages/58/8d/25c20ff1a1a8426d9af2d0b6f29f6388005fc8cd10d6ee71f48bff86fdd0/lazy_object_proxy-1.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:75ba769017b944fcacbf6a80c18b2761a1795b03f8899acdad1f1c39db4409be", size = 70744, upload-time = "2025-08-22T13:42:49.608Z" }, - { url = "https://files.pythonhosted.org/packages/c0/67/8ec9abe15c4f8a4bcc6e65160a2c667240d025cbb6591b879bea55625263/lazy_object_proxy-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:7b22c2bbfb155706b928ac4d74c1a63ac8552a55ba7fff4445155523ea4067e1", size = 26568, upload-time = "2025-08-22T13:42:57.719Z" }, - { url = "https://files.pythonhosted.org/packages/23/12/cd2235463f3469fd6c62d41d92b7f120e8134f76e52421413a0ad16d493e/lazy_object_proxy-1.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4a79b909aa16bde8ae606f06e6bbc9d3219d2e57fb3e0076e17879072b742c65", size = 27391, upload-time = "2025-08-22T13:42:50.62Z" }, - { url = "https://files.pythonhosted.org/packages/60/9e/f1c53e39bbebad2e8609c67d0830cc275f694d0ea23d78e8f6db526c12d3/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:338ab2f132276203e404951205fe80c3fd59429b3a724e7b662b2eb539bb1be9", size = 80552, upload-time = "2025-08-22T13:42:51.731Z" }, - { url = "https://files.pythonhosted.org/packages/4c/b6/6c513693448dcb317d9d8c91d91f47addc09553613379e504435b4cc8b3e/lazy_object_proxy-1.12.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c40b3c9faee2e32bfce0df4ae63f4e73529766893258eca78548bac801c8f66", size = 82857, upload-time = "2025-08-22T13:42:53.225Z" }, - { url = "https://files.pythonhosted.org/packages/12/1c/d9c4aaa4c75da11eb7c22c43d7c90a53b4fca0e27784a5ab207768debea7/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:717484c309df78cedf48396e420fa57fc8a2b1f06ea889df7248fdd156e58847", size = 80833, upload-time = "2025-08-22T13:42:54.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/ae/29117275aac7d7d78ae4f5a4787f36ff33262499d486ac0bf3e0b97889f6/lazy_object_proxy-1.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a6b7ea5ea1ffe15059eb44bcbcb258f97bcb40e139b88152c40d07b1a1dfc9ac", size = 79516, upload-time = "2025-08-22T13:42:55.812Z" }, - { url = "https://files.pythonhosted.org/packages/19/40/b4e48b2c38c69392ae702ae7afa7b6551e0ca5d38263198b7c79de8b3bdf/lazy_object_proxy-1.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:08c465fb5cd23527512f9bd7b4c7ba6cec33e28aad36fbbe46bf7b858f9f3f7f", size = 27656, upload-time = "2025-08-22T13:42:56.793Z" }, -] - [[package]] name = "libcst" version = "1.5.0" @@ -2861,6 +2913,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/b9/9ffe7abb527813a7568e46dc62edc5c1df8f100690cbe7b7829d07c2201e/lmnr-0.7.29-py3-none-any.whl", hash = "sha256:b7f09e73aa6d8cdf358eac80a1edbea951e879247f6738eb6dbe1c0d239dd645", size = 266860, upload-time = "2026-01-14T16:55:15.981Z" }, ] +[[package]] +name = "lupa" +version = "2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b8/1c/191c3e6ec6502e3dbe25a53e27f69a5daeac3e56de1f73c0138224171ead/lupa-2.6.tar.gz", hash = "sha256:9a770a6e89576be3447668d7ced312cd6fd41d3c13c2462c9dc2c2ab570e45d9", size = 7240282, upload-time = "2025-10-24T07:20:29.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/86/ce243390535c39d53ea17ccf0240815e6e457e413e40428a658ea4ee4b8d/lupa-2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47ce718817ef1cc0c40d87c3d5ae56a800d61af00fbc0fad1ca9be12df2f3b56", size = 951707, upload-time = "2025-10-24T07:18:03.884Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/cedea5e6cbeb54396fdcc55f6b741696f3f036d23cfaf986d50d680446da/lupa-2.6-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7aba985b15b101495aa4b07112cdc08baa0c545390d560ad5cfde2e9e34f4d58", size = 1916703, upload-time = "2025-10-24T07:18:05.6Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/3d6b5f9a8588c01a4d88129284c726017b2089f3a3fd3ba8bd977292fea0/lupa-2.6-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:b766f62f95b2739f2248977d29b0722e589dcf4f0ccfa827ccbd29f0148bd2e5", size = 985152, upload-time = "2025-10-24T07:18:08.561Z" }, + { url = "https://files.pythonhosted.org/packages/eb/23/9f9a05beee5d5dce9deca4cb07c91c40a90541fc0a8e09db4ee670da550f/lupa-2.6-cp312-cp312-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:00a934c23331f94cb51760097ebfab14b005d55a6b30a2b480e3c53dd2fa290d", size = 1159599, upload-time = "2025-10-24T07:18:10.346Z" }, + { url = "https://files.pythonhosted.org/packages/40/4e/e7c0583083db9d7f1fd023800a9767d8e4391e8330d56c2373d890ac971b/lupa-2.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21de9f38bd475303e34a042b7081aabdf50bd9bafd36ce4faea2f90fd9f15c31", size = 1038686, upload-time = "2025-10-24T07:18:12.112Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9f/5a4f7d959d4feba5e203ff0c31889e74d1ca3153122be4a46dca7d92bf7c/lupa-2.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf3bda96d3fc41237e964a69c23647d50d4e28421111360274d4799832c560e9", size = 2071956, upload-time = "2025-10-24T07:18:14.572Z" }, + { url = "https://files.pythonhosted.org/packages/92/34/2f4f13ca65d01169b1720176aedc4af17bc19ee834598c7292db232cb6dc/lupa-2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a76ead245da54801a81053794aa3975f213221f6542d14ec4b859ee2e7e0323", size = 1057199, upload-time = "2025-10-24T07:18:16.379Z" }, + { url = "https://files.pythonhosted.org/packages/35/2a/5f7d2eebec6993b0dcd428e0184ad71afb06a45ba13e717f6501bfed1da3/lupa-2.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8dd0861741caa20886ddbda0a121d8e52fb9b5bb153d82fa9bba796962bf30e8", size = 1173693, upload-time = "2025-10-24T07:18:18.153Z" }, + { url = "https://files.pythonhosted.org/packages/e4/29/089b4d2f8e34417349af3904bb40bec40b65c8731f45e3fd8d497ca573e5/lupa-2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:239e63948b0b23023f81d9a19a395e768ed3da6a299f84e7963b8f813f6e3f9c", size = 2164394, upload-time = "2025-10-24T07:18:20.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1b/79c17b23c921f81468a111cad843b076a17ef4b684c4a8dff32a7969c3f0/lupa-2.6-cp312-cp312-win32.whl", hash = "sha256:325894e1099499e7a6f9c351147661a2011887603c71086d36fe0f964d52d1ce", size = 1420647, upload-time = "2025-10-24T07:18:23.368Z" }, + { url = "https://files.pythonhosted.org/packages/b8/15/5121e68aad3584e26e1425a5c9a79cd898f8a152292059e128c206ee817c/lupa-2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c735a1ce8ee60edb0fe71d665f1e6b7c55c6021f1d340eb8c865952c602cd36f", size = 1688529, upload-time = "2025-10-24T07:18:25.523Z" }, + { url = "https://files.pythonhosted.org/packages/28/1d/21176b682ca5469001199d8b95fa1737e29957a3d185186e7a8b55345f2e/lupa-2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:663a6e58a0f60e7d212017d6678639ac8df0119bc13c2145029dcba084391310", size = 947232, upload-time = "2025-10-24T07:18:27.878Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4c/d327befb684660ca13cf79cd1f1d604331808f9f1b6fb6bf57832f8edf80/lupa-2.6-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:d1f5afda5c20b1f3217a80e9bc1b77037f8a6eb11612fd3ada19065303c8f380", size = 1908625, upload-time = "2025-10-24T07:18:29.944Z" }, + { url = "https://files.pythonhosted.org/packages/66/8e/ad22b0a19454dfd08662237a84c792d6d420d36b061f239e084f29d1a4f3/lupa-2.6-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:26f2b3c085fe76e9119e48c1013c1cccdc1f51585d456858290475aa38e7089e", size = 981057, upload-time = "2025-10-24T07:18:31.553Z" }, + { url = "https://files.pythonhosted.org/packages/5c/48/74859073ab276bd0566c719f9ca0108b0cfc1956ca0d68678d117d47d155/lupa-2.6-cp313-cp313-manylinux2010_i686.manylinux_2_12_i686.manylinux_2_28_i686.whl", hash = "sha256:60d2f902c7b96fb8ab98493dcff315e7bb4d0b44dc9dd76eb37de575025d5685", size = 1156227, upload-time = "2025-10-24T07:18:33.981Z" }, + { url = "https://files.pythonhosted.org/packages/09/6c/0e9ded061916877253c2266074060eb71ed99fb21d73c8c114a76725bce2/lupa-2.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a02d25dee3a3250967c36590128d9220ae02f2eda166a24279da0b481519cbff", size = 1035752, upload-time = "2025-10-24T07:18:36.32Z" }, + { url = "https://files.pythonhosted.org/packages/dd/ef/f8c32e454ef9f3fe909f6c7d57a39f950996c37a3deb7b391fec7903dab7/lupa-2.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6eae1ee16b886b8914ff292dbefbf2f48abfbdee94b33a88d1d5475e02423203", size = 2069009, upload-time = "2025-10-24T07:18:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/53/dc/15b80c226a5225815a890ee1c11f07968e0aba7a852df41e8ae6fe285063/lupa-2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0edd5073a4ee74ab36f74fe61450148e6044f3952b8d21248581f3c5d1a58be", size = 1056301, upload-time = "2025-10-24T07:18:40.165Z" }, + { url = "https://files.pythonhosted.org/packages/31/14/2086c1425c985acfb30997a67e90c39457122df41324d3c179d6ee2292c6/lupa-2.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0c53ee9f22a8a17e7d4266ad48e86f43771951797042dd51d1494aaa4f5f3f0a", size = 1170673, upload-time = "2025-10-24T07:18:42.426Z" }, + { url = "https://files.pythonhosted.org/packages/10/e5/b216c054cf86576c0191bf9a9f05de6f7e8e07164897d95eea0078dca9b2/lupa-2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:de7c0f157a9064a400d828789191a96da7f4ce889969a588b87ec80de9b14772", size = 2162227, upload-time = "2025-10-24T07:18:46.112Z" }, + { url = "https://files.pythonhosted.org/packages/59/2f/33ecb5bedf4f3bc297ceacb7f016ff951331d352f58e7e791589609ea306/lupa-2.6-cp313-cp313-win32.whl", hash = "sha256:ee9523941ae0a87b5b703417720c5d78f72d2f5bc23883a2ea80a949a3ed9e75", size = 1419558, upload-time = "2025-10-24T07:18:48.371Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b4/55e885834c847ea610e111d87b9ed4768f0afdaeebc00cd46810f25029f6/lupa-2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b1335a5835b0a25ebdbc75cf0bda195e54d133e4d994877ef025e218c2e59db9", size = 1683424, upload-time = "2025-10-24T07:18:50.976Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -3507,25 +3589,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/e1/0a6560bab7fb7b5a88d35a505b859c6d969cb2fa2681b568eb5d95019dec/openai-2.8.0-py3-none-any.whl", hash = "sha256:ba975e347f6add2fe13529ccb94d54a578280e960765e5224c34b08d7e029ddf", size = 1022692, upload-time = "2025-11-13T18:15:23.621Z" }, ] -[[package]] -name = "openapi-core" -version = "0.22.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "isodate" }, - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "more-itertools" }, - { name = "openapi-schema-validator" }, - { name = "openapi-spec-validator" }, - { name = "typing-extensions" }, - { name = "werkzeug" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/65/ee75f25b9459a02df6f713f8ffde5dacb57b8b4e45145cde4cab28b5abba/openapi_core-0.22.0.tar.gz", hash = "sha256:b30490dfa74e3aac2276105525590135212352f5dd7e5acf8f62f6a89ed6f2d0", size = 109242, upload-time = "2025-12-22T19:19:49.608Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5b/8e/a1bf9e7d1b170122aa33b6cf2c788b68824712427deb19795eb7db1b8dd5/openapi_core-0.22.0-py3-none-any.whl", hash = "sha256:8fb7c325f2db4ef6c60584b1870f90eeb3183aa47e30643715c5003b7677a149", size = 108384, upload-time = "2025-12-22T19:19:47.904Z" }, -] - [[package]] name = "openapi-pydantic" version = "0.5.1" @@ -3538,35 +3601,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/12/cf/03675d8bd8ecbf4445504d8071adab19f5f993676795708e36402ab38263/openapi_pydantic-0.5.1-py3-none-any.whl", hash = "sha256:a3a09ef4586f5bd760a8df7f43028b60cafb6d9f61de2acba9574766255ab146", size = 96381, upload-time = "2025-01-08T19:29:25.275Z" }, ] -[[package]] -name = "openapi-schema-validator" -version = "0.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-specifications" }, - { name = "rfc3339-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/5507ad3325169347cd8ced61c232ff3df70e2b250c49f0fe140edb4973c6/openapi_schema_validator-0.6.3.tar.gz", hash = "sha256:f37bace4fc2a5d96692f4f8b31dc0f8d7400fd04f3a937798eaf880d425de6ee", size = 11550, upload-time = "2025-01-10T18:08:22.268Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/21/c6/ad0fba32775ae749016829dace42ed80f4407b171da41313d1a3a5f102e4/openapi_schema_validator-0.6.3-py3-none-any.whl", hash = "sha256:f3b9870f4e556b5a62a1c39da72a6b4b16f3ad9c73dc80084b1b11e74ba148a3", size = 8755, upload-time = "2025-01-10T18:08:19.758Z" }, -] - -[[package]] -name = "openapi-spec-validator" -version = "0.7.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "jsonschema" }, - { name = "jsonschema-path" }, - { name = "lazy-object-proxy" }, - { name = "openapi-schema-validator" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/af/fe2d7618d6eae6fb3a82766a44ed87cd8d6d82b4564ed1c7cfb0f6378e91/openapi_spec_validator-0.7.2.tar.gz", hash = "sha256:cc029309b5c5dbc7859df0372d55e9d1ff43e96d678b9ba087f7c56fc586f734", size = 36855, upload-time = "2025-06-07T14:48:56.299Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/dd/b3fd642260cb17532f66cc1e8250f3507d1e580483e209dc1e9d13bd980d/openapi_spec_validator-0.7.2-py3-none-any.whl", hash = "sha256:4bbdc0894ec85f1d1bea1d6d9c8b2c3c8d7ccaa13577ef40da9c006c9fd0eb60", size = 39713, upload-time = "2025-06-07T14:48:54.077Z" }, -] - [[package]] name = "openhands-aci" version = "0.3.3" @@ -3608,8 +3642,8 @@ wheels = [ [[package]] name = "openhands-agent-server" -version = "1.11.5" -source = { registry = "https://pypi.org/simple" } +version = "1.12.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-agent-server&branch=openhands%2Fissue-2228-sdk-settings-schema#5356a26e909b660d280de312cdc0409037c9ff48" } dependencies = [ { name = "aiosqlite" }, { name = "alembic" }, @@ -3622,10 +3656,6 @@ dependencies = [ { name = "websockets" }, { name = "wsproto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/12/546ec8e0fe22e04c5bbca36ab8c860bbdaafca29b88a382ff5ebcc06657f/openhands_agent_server-1.11.5.tar.gz", hash = "sha256:b61366d727c61ab9b7fcd66faab53f230f8ef0928c1177a388d2c5c4be6ebbd0", size = 70384, upload-time = "2026-02-20T22:16:44.772Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/24/6e35036b3e44878684f43acf8fa4f8e14a5578be30841458bf985fbbf566/openhands_agent_server-1.11.5-py3-none-any.whl", hash = "sha256:8bae7063f232791d58a5c31919f58b557f7cce60e6295773985c7dadc556cb9e", size = 84930, upload-time = "2026-02-20T22:16:45.821Z" }, -] [[package]] name = "openhands-ai" @@ -3662,6 +3692,7 @@ dependencies = [ { name = "libtmux" }, { name = "litellm" }, { name = "lmnr" }, + { name = "mcp" }, { name = "memory-profiler" }, { name = "numpy" }, { name = "openai" }, @@ -3767,7 +3798,7 @@ requires-dist = [ { name = "docker" }, { name = "e2b-code-interpreter", marker = "extra == 'third-party-runtimes'", specifier = ">=2" }, { name = "fastapi" }, - { name = "fastmcp", specifier = ">=2.12.4,<2.12.5" }, + { name = "fastmcp", specifier = ">=3.0.0,<4" }, { name = "google-api-python-client", specifier = ">=2.164" }, { name = "google-auth-httplib2" }, { name = "google-auth-oauthlib" }, @@ -3785,14 +3816,15 @@ requires-dist = [ { name = "libtmux", specifier = ">=0.46.2" }, { name = "litellm", specifier = ">=1.74.3" }, { name = "lmnr", specifier = ">=0.7.20" }, + { name = "mcp", specifier = ">=1.25" }, { name = "memory-profiler", specifier = ">=0.61" }, { name = "modal", marker = "extra == 'third-party-runtimes'", specifier = ">=0.66.26,<1.2" }, { name = "numpy" }, { name = "openai", specifier = "==2.8" }, { name = "openhands-aci", specifier = "==0.3.3" }, - { name = "openhands-agent-server", specifier = "==1.11.5" }, - { name = "openhands-sdk", specifier = "==1.11.5" }, - { name = "openhands-tools", specifier = "==1.11.5" }, + { name = "openhands-agent-server", git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-agent-server&branch=openhands%2Fissue-2228-sdk-settings-schema" }, + { name = "openhands-sdk", git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-sdk&branch=openhands%2Fissue-2228-sdk-settings-schema" }, + { name = "openhands-tools", git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-tools&branch=openhands%2Fissue-2228-sdk-settings-schema" }, { name = "opentelemetry-api", specifier = ">=1.33.1" }, { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.33.1" }, { name = "pathspec", specifier = ">=0.12.1" }, @@ -3870,10 +3902,12 @@ test = [ [[package]] name = "openhands-sdk" -version = "1.11.5" -source = { registry = "https://pypi.org/simple" } +version = "1.12.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-sdk&branch=openhands%2Fissue-2228-sdk-settings-schema#5356a26e909b660d280de312cdc0409037c9ff48" } dependencies = [ + { name = "agent-client-protocol" }, { name = "deprecation" }, + { name = "fakeredis", extra = ["lua"] }, { name = "fastmcp" }, { name = "filelock" }, { name = "httpx" }, @@ -3885,15 +3919,11 @@ dependencies = [ { name = "tenacity" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3a/fc/cfb73768099be94c9f92b2e160f29d1f6d3a4dd84fea5e33a2b0984449cc/openhands_sdk-1.11.5.tar.gz", hash = "sha256:dd6225876b7b8dbb6c608559f2718c3d0bf44d0bb741e990b185c6cdc5150c5a", size = 295069, upload-time = "2026-02-20T22:16:47.102Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/6b/21df2a5b9ed756a48566d96ad491c81609af804b271c370106a4ced4ed5c/openhands_sdk-1.11.5-py3-none-any.whl", hash = "sha256:f949cd540cbecc339d90fb0cca2a5f29e1b62566b82b5aee82ef40f259d14e60", size = 377527, upload-time = "2026-02-20T22:16:48.165Z" }, -] [[package]] name = "openhands-tools" -version = "1.11.5" -source = { registry = "https://pypi.org/simple" } +version = "1.12.0" +source = { git = "https://github.com/OpenHands/software-agent-sdk.git?subdirectory=openhands-tools&branch=openhands%2Fissue-2228-sdk-settings-schema#5356a26e909b660d280de312cdc0409037c9ff48" } dependencies = [ { name = "bashlex" }, { name = "binaryornot" }, @@ -3905,10 +3935,6 @@ dependencies = [ { name = "pydantic" }, { name = "tom-swe" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/94/bc/d0388c84621c3be21a011d8861cf8917371bcd42a68a87172cd246621e09/openhands_tools-1.11.5.tar.gz", hash = "sha256:d7b1163f6505a51b07147e7d8972062c129ecc46571a71f28d5470355e06650e", size = 101113, upload-time = "2026-02-20T22:16:50.881Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/68/3d/dac03b376b8fea639367d6633f700d8ccba3d535cd2a8e7b17df9918ed5b/openhands_tools-1.11.5-py3-none-any.whl", hash = "sha256:1e981e1e7f3544184fe946cee8eb6bd287010cdef77d83ebac945c9f42df3baf", size = 138837, upload-time = "2026-02-20T22:16:43.173Z" }, -] [[package]] name = "openpyxl" @@ -4576,6 +4602,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/f0/10642828a8dfb741e5f3fbaac830550a518a775c7fff6f04a007259b0548/py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378", size = 98708, upload-time = "2021-11-04T17:17:00.152Z" }, ] +[[package]] +name = "py-key-value-aio" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beartype" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/3c/0397c072a38d4bc580994b42e0c90c5f44f679303489e4376289534735e5/py_key_value_aio-0.4.4.tar.gz", hash = "sha256:e3012e6243ed7cc09bb05457bd4d03b1ba5c2b1ca8700096b3927db79ffbbe55", size = 92300, upload-time = "2026-02-16T21:21:43.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/69/f1b537ee70b7def42d63124a539ed3026a11a3ffc3086947a1ca6e861868/py_key_value_aio-0.4.4-py3-none-any.whl", hash = "sha256:18e17564ecae61b987f909fc2cd41ee2012c84b4b1dcb8c055cf8b4bc1bf3f5d", size = 152291, upload-time = "2026-02-16T21:21:44.241Z" }, +] + +[package.optional-dependencies] +filetree = [ + { name = "aiofile" }, + { name = "anyio" }, +] +keyring = [ + { name = "keyring" }, +] +memory = [ + { name = "cachetools" }, +] + [[package]] name = "pyasn1" version = "0.6.2" @@ -8177,6 +8228,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "soupsieve" version = "2.8.1" @@ -8662,6 +8722,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] +[[package]] +name = "uncalled-for" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/7c/b5b7d8136f872e3f13b0584e576886de0489d7213a12de6bebf29ff6ebfc/uncalled_for-0.2.0.tar.gz", hash = "sha256:b4f8fdbcec328c5a113807d653e041c5094473dd4afa7c34599ace69ccb7e69f", size = 49488, upload-time = "2026-02-27T17:40:58.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/7f/4320d9ce3be404e6310b915c3629fe27bf1e2f438a1a7a3cb0396e32e9a9/uncalled_for-0.2.0-py3-none-any.whl", hash = "sha256:2c0bd338faff5f930918f79e7eb9ff48290df2cb05fcc0b40a7f334e55d4d85f", size = 11351, upload-time = "2026-02-27T17:40:56.804Z" }, +] + [[package]] name = "uri-template" version = "1.3.0" @@ -8851,18 +8920,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, ] -[[package]] -name = "werkzeug" -version = "3.1.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markupsafe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/61/f1/ee81806690a87dab5f5653c1f146c92bc066d7f4cebc603ef88eb9e13957/werkzeug-3.1.6.tar.gz", hash = "sha256:210c6bede5a420a913956b4791a7f4d6843a43b6fcee4dfa08a65e93007d0d25", size = 864736, upload-time = "2026-02-19T15:17:18.884Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, -] - [[package]] name = "whatthepatch" version = "1.0.7"