Consume SDK AgentSettings schema in OpenHands

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
openhands
2026-03-09 01:18:53 +00:00
parent 9dab5b1bbf
commit a03377698c
11 changed files with 506 additions and 217 deletions

View File

@@ -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",
}),
);
});

View File

@@ -4,14 +4,14 @@ import { DEFAULT_SETTINGS } from "#/services/settings";
import { Provider, Settings } from "#/types/settings";
const MOCK_SDK_SETTINGS_SCHEMA: NonNullable<Settings["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",
@@ -25,7 +25,7 @@ const MOCK_SDK_SETTINGS_SCHEMA: NonNullable<Settings["sdk_settings_schema"]> = {
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<Settings["sdk_settings_schema"]> = {
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<Settings["sdk_settings_schema"]> = {
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<Settings["sdk_settings_schema"]> = {
{ 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,
},
};

View File

@@ -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",
});
});
});

View File

@@ -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
</IMPORTANT_PLANNING_BOUNDARIES>"""
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

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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" }

View File

@@ -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

View File

@@ -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')

View File

@@ -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.

299
uv.lock generated
View File

@@ -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"