diff --git a/.github/assets/badge-pypi-dark.svg b/.github/assets/badge-pypi-dark.svg
index dce4c53..8e2b2a6 100644
--- a/.github/assets/badge-pypi-dark.svg
+++ b/.github/assets/badge-pypi-dark.svg
@@ -5,5 +5,5 @@
v0.1.3
+ font-size="13" font-weight="700" fill="#ffffff">v0.1.4
\ No newline at end of file
diff --git a/.github/assets/badge-pypi-light.svg b/.github/assets/badge-pypi-light.svg
index a2523be..90962be 100644
--- a/.github/assets/badge-pypi-light.svg
+++ b/.github/assets/badge-pypi-light.svg
@@ -5,5 +5,5 @@
v0.1.3
+ font-size="13" font-weight="700" fill="#ffffff">v0.1.4
\ No newline at end of file
diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py
index c20520a..fe5f84a 100644
--- a/EvoScientist/EvoScientist.py
+++ b/EvoScientist/EvoScientist.py
@@ -59,6 +59,13 @@ _chat_model = None
# /model switch to lag one step (see issue #179).
_chat_model_key: tuple[str | None, str | None] | None = None
+# Auxiliary model for background/helper LLM calls (memory workers + main-agent
+# tool selector). Cached separately from the main model; falls back to the main
+# instance when the auxiliary_* config fields are empty (see
+# _ensure_auxiliary_chat_model).
+_auxiliary_chat_model = None
+_auxiliary_chat_model_key: tuple[str | None, str | None] | None = None
+
# Cache MCP tools by the effective config signature to avoid reconnecting
# to MCP servers on every `/new` when config is unchanged.
_MCP_TOOLS_CACHE_KEY: str | None = None
@@ -124,6 +131,32 @@ def _ensure_chat_model():
return _chat_model
+def _ensure_auxiliary_chat_model():
+ """Return the auxiliary chat model for background/helper LLM calls.
+
+ Resolves ``(cfg.auxiliary_model or cfg.model, cfg.auxiliary_provider or
+ cfg.provider)``. When the auxiliary fields are empty — or resolve to the same
+ ``(model, provider)`` pair as the main model — returns the main
+ ``_ensure_chat_model()`` instance directly, so no second client is built.
+ Otherwise it is cached separately under its own key. Onboard sets the
+ provider alongside the model, so the ``or cfg.provider`` fallback only
+ matters for a model set without an explicit auxiliary provider.
+ """
+ global _auxiliary_chat_model, _auxiliary_chat_model_key
+ from .llm import get_chat_model
+
+ cfg = _ensure_config()
+ aux_model = cfg.auxiliary_model or cfg.model
+ aux_provider = cfg.auxiliary_provider or cfg.provider
+ if (aux_model, aux_provider) == (cfg.model, cfg.provider):
+ return _ensure_chat_model()
+ key = (aux_model, aux_provider)
+ if _auxiliary_chat_model is None or _auxiliary_chat_model_key != key:
+ _auxiliary_chat_model = get_chat_model(model=aux_model, provider=aux_provider)
+ _auxiliary_chat_model_key = key
+ return _auxiliary_chat_model
+
+
def set_chat_model(model: str, provider: str | None = None):
"""Replace the cached chat model with a new one.
@@ -136,6 +169,12 @@ def set_chat_model(model: str, provider: str | None = None):
"""
from .llm import get_chat_model
+ # Invalidate the auxiliary cache too: when auxiliary_* is empty it mirrors
+ # the main model, so a /model switch must let it re-resolve to the new main.
+ global _auxiliary_chat_model, _auxiliary_chat_model_key
+ _auxiliary_chat_model = None
+ _auxiliary_chat_model_key = None
+
key = (model, provider)
if _chat_model is None or _chat_model_key != key:
_replace_chat_model(get_chat_model(model=model, provider=provider), key)
@@ -585,13 +624,26 @@ def _get_default_middleware(
MemoryObservationTarget.AGENT
),
)
+ # Main-agent tool selection may use the auxiliary model; async sub-agents
+ # keep the main model (they do real work, not a one-off helper call).
+ # context_editing stays on the main model — its model only sizes the
+ # context-window trigger for the main agent's own history.
+ tool_selector_model = (
+ model if for_async_subagent else _ensure_auxiliary_chat_model()
+ )
mw = [
ConfigurableModelMiddleware(),
create_context_editing_middleware(model),
ModelFallbackMiddleware(),
ContextOverflowMapperMiddleware(),
ToolErrorHandlerMiddleware(),
- *create_tool_selector_middleware(model=model),
+ *create_tool_selector_middleware(model=tool_selector_model),
+ # Interpreter prompt must land before runtime/memory context, so this
+ # middleware sits ahead of runtime_context in the stack.
+ create_code_interpreter_middleware(
+ timeout=cfg.code_interpreter_timeout,
+ max_result_chars=cfg.code_interpreter_max_result_chars,
+ ),
create_runtime_context_middleware(),
]
if memory_controls.memory_enabled:
@@ -624,12 +676,6 @@ def _get_default_middleware(
mw.append(BackgroundExecutionMiddleware())
- mw.append(
- create_code_interpreter_middleware(
- timeout=cfg.code_interpreter_timeout,
- max_result_chars=cfg.code_interpreter_max_result_chars,
- )
- )
return mw
diff --git a/EvoScientist/config/onboard/steps.py b/EvoScientist/config/onboard/steps.py
index 14d4940..c77cadd 100644
--- a/EvoScientist/config/onboard/steps.py
+++ b/EvoScientist/config/onboard/steps.py
@@ -43,7 +43,7 @@ from .validators import validate_tavily_key
def _step_ui_backend(config: EvoScientistConfig) -> str:
- """Step 0: Select UI backend (Textual TUI, Rich CLI, or browser WebUI).
+ """Step 0: Select UI backend (desktop WebUI, Textual TUI, or Rich CLI).
Args:
config: Current configuration.
@@ -52,9 +52,9 @@ def _step_ui_backend(config: EvoScientistConfig) -> str:
Selected backend name ("tui", "cli", or "webui").
"""
choices = [
+ Choice(title="WebUI (desktop interface, beta)", value="webui"),
Choice(title="TUI (full-screen interface, recommended)", value="tui"),
Choice(title="CLI (classic terminal, lightweight)", value="cli"),
- Choice(title="WebUI (browser interface, beta)", value="webui"),
]
# Map legacy values to current ones
@@ -228,11 +228,20 @@ def _step_webui_port(config: EvoScientistConfig) -> int:
return port
-def _step_provider(config: EvoScientistConfig) -> str:
+def _step_provider(
+ config: EvoScientistConfig,
+ *,
+ label: str | None = None,
+ default_value: str | None = None,
+) -> str:
"""Step 1: Select LLM provider.
Args:
config: Current configuration.
+ label: Optional role label (e.g. "co-pilot") to clarify which model this
+ provider is for. When omitted, the generic main-model prompt is used.
+ default_value: Preselect this provider instead of ``config.provider``
+ (e.g. the auxiliary provider when configuring the co-pilot).
Returns:
Selected provider name.
@@ -297,12 +306,13 @@ def _step_provider(config: EvoScientistConfig) -> str:
),
]
- # Set default based on current config
+ # Set default based on current config (or an explicit override).
valid_providers = {c.value for c in choices}
- default = config.provider if config.provider in valid_providers else "anthropic"
+ preferred = default_value or config.provider
+ default = preferred if preferred in valid_providers else "anthropic"
provider = questionary.select(
- "Select your LLM provider:",
+ f"Select {label} provider:" if label else "Select your LLM provider:",
choices=choices,
default=default,
style=WIZARD_STYLE,
@@ -671,6 +681,8 @@ def _step_model(
provider: str,
*,
ollama_detected_models: list[str] | None = None,
+ label: str | None = None,
+ default_value: str | None = None,
) -> str:
"""Step 3: Select model for the provider.
@@ -678,10 +690,16 @@ def _step_model(
config: Current configuration.
provider: Selected provider name.
ollama_detected_models: Model names detected from a live Ollama server.
+ label: Optional role label (e.g. "co-pilot") for the prompt. When omitted,
+ the generic main-model prompt is used.
+ default_value: Preselect this model instead of ``config.model`` (e.g. the
+ auxiliary model when configuring the co-pilot).
Returns:
Selected model name.
"""
+ model_prompt = f"Select {label} model:" if label else "Select model:"
+ model_default = default_value or config.model
# Ollama: show only what's actually pulled on the server
if provider == "ollama":
if ollama_detected_models:
@@ -692,11 +710,11 @@ def _step_model(
choices.append(Choice(title="Type a model name...", value=_CUSTOM_SENTINEL))
default = ollama_detected_models[0]
- if config.model in ollama_detected_models:
- default = config.model
+ if model_default in ollama_detected_models:
+ default = model_default
selected = questionary.select(
- "Select model:",
+ model_prompt,
choices=choices,
default=default,
style=WIZARD_STYLE,
@@ -742,7 +760,7 @@ def _step_model(
style=WIZARD_STYLE,
qmark=QMARK,
placeholder=FormattedText([("fg:#858585", " e.g. owner/model-name")]),
- default=config.model or "",
+ default=model_default or "",
).ask()
if model is None:
raise KeyboardInterrupt()
@@ -763,14 +781,23 @@ def _step_model(
choices.append(Choice(title=f"{name} ({model_id})", value=name))
choices.append(Choice(title="Type a model name...", value=_CUSTOM_SENTINEL))
- # Determine default
- if config.model in provider_models:
- default = config.model
+ # Determine default. An explicit ``default_value`` override (e.g. a saved
+ # co-pilot model on a re-run) that isn't a registry model is a custom name:
+ # preselect "Type a model name..." and prefill it. A plain ``config.model``
+ # that just isn't in the current provider's list (e.g. the provider was
+ # changed) falls back to the first model, NOT the custom entry.
+ custom_default = (
+ default_value if default_value and default_value not in provider_models else ""
+ )
+ if model_default in provider_models:
+ default = model_default
+ elif custom_default:
+ default = _CUSTOM_SENTINEL
else:
default = provider_models[0]
selected = questionary.select(
- "Select model:",
+ model_prompt,
choices=choices,
default=default,
style=WIZARD_STYLE,
@@ -786,6 +813,7 @@ def _step_model(
model = questionary.text(
"Model name:",
+ default=custom_default,
style=WIZARD_STYLE,
qmark=QMARK,
placeholder=FormattedText([("fg:#858585", " e.g. owner/model-name")]),
@@ -799,6 +827,40 @@ def _step_model(
return model
+def _step_auxiliary_enable(config: EvoScientistConfig) -> bool:
+ """Step 3.25: Choose whether to assemble a co-pilot (auxiliary) model.
+
+ The co-pilot runs background/helper LLM calls — EvoMemory (memory workers)
+ and the main agent's tool selector — so it can be a cheaper/faster model.
+ Returns True when the user picks "Assemble"; the caller then runs the
+ provider/key/model pickers. Returns False to keep the pilot (main model)
+ everywhere.
+ """
+ console.print(
+ " [dim]A cheaper/faster co-pilot for EvoMemory (memory workers).[/dim]"
+ )
+ choice = questionary.select(
+ "Co-pilot (auxiliary model):",
+ choices=[
+ Choice(
+ title="Skip — single pilot (main model handles everything)",
+ value="skip",
+ ),
+ Choice(
+ title="Assemble a co-pilot — separate cheaper/faster model",
+ value="assemble",
+ ),
+ ],
+ default="assemble" if config.auxiliary_model else "skip",
+ style=WIZARD_STYLE,
+ qmark=QMARK,
+ use_indicator=True,
+ ).ask()
+ if choice is None:
+ raise KeyboardInterrupt()
+ return choice == "assemble"
+
+
def _step_reasoning_effort(config: EvoScientistConfig) -> str:
"""Step 3.5: Configure OpenRouter reasoning effort level.
@@ -935,18 +997,23 @@ _RECOMMENDED_SKILLS = [
},
# ── Third-party (K-Dense) ──
{
- "label": "Scientific Skills (147 research & experiment skills, third party by K-Dense)",
+ "label": "Scientific Skills (143 research & experiment skills, third party by K-Dense)",
"source": "K-Dense-AI/scientific-agent-skills@skills",
},
{
- "label": "Scientific Writer (23 writing, review & presentation skills, third party by K-Dense)",
+ "label": "Scientific Writer (27 writing, review & presentation skills, third party by K-Dense)",
"source": "K-Dense-AI/claude-scientific-writer@skills",
},
# ── Third-party (Orchestra Research) ──
{
- "label": "AI Research Skills (85 skills for training, evaluation, deployment, etc., third party by Orchestra Research)",
+ "label": "AI Research Skills (98 skills for training, evaluation, deployment, etc., third party by Orchestra Research)",
"source": "Orchestra-Research/AI-Research-SKILLs",
},
+ # ── Third-party (Google DeepMind) ──
+ {
+ "label": "Science Skills (37 genomics, structural-biology & literature skills, third party by Google DeepMind)",
+ "source": "google-deepmind/science-skills@skills",
+ },
# ── Third-party (Anthropic) ──
{
"label": "Anthropic Skills (co-authoring, design, etc., third party by Anthropic)",
diff --git a/EvoScientist/config/onboard/style.py b/EvoScientist/config/onboard/style.py
index 0fb0f33..4b322b6 100644
--- a/EvoScientist/config/onboard/style.py
+++ b/EvoScientist/config/onboard/style.py
@@ -104,6 +104,21 @@ def _print_header() -> None:
console.print()
+_SECTION_WIDTH = 53 # ~ the Setup Wizard panel width (not the full terminal)
+
+
+def _print_section(title: str) -> None:
+ """Print a compact bold-cyan section divider, e.g. ──── Pilot ────.
+
+ Sized to roughly the Setup Wizard panel width and styled like the section
+ markers used elsewhere in the CLI, rather than a full-width rule.
+ """
+ label = f" {title} "
+ pad = max(2, _SECTION_WIDTH - len(label))
+ left = pad // 2
+ console.print(f"[bold cyan]{'─' * left}{label}{'─' * (pad - left)}[/bold cyan]")
+
+
def _print_step_result(step_name: str, value: str, success: bool = True) -> None:
"""Print a completed step result inline.
diff --git a/EvoScientist/config/onboard/wizard.py b/EvoScientist/config/onboard/wizard.py
index 8e74efe..21008dc 100644
--- a/EvoScientist/config/onboard/wizard.py
+++ b/EvoScientist/config/onboard/wizard.py
@@ -18,6 +18,7 @@ from ..settings import (
from .channels import _step_channels
from .steps import (
_step_anthropic_auth_mode,
+ _step_auxiliary_enable,
_step_base_url,
_step_langgraph_dev_port,
_step_mcp_servers,
@@ -40,6 +41,7 @@ from .style import (
CONFIRM_STYLE,
QMARK,
_print_header,
+ _print_section,
_print_step_skipped,
console,
)
@@ -50,6 +52,7 @@ STEPS = [
"Provider",
"API Key",
"Model",
+ "Auxiliary Model",
"Tavily Key",
"Workspace",
"Thinking",
@@ -145,6 +148,7 @@ _SECTION_LABELS: list[tuple[str, str]] = [
("port", "LangGraph server port"),
("provider", "LLM provider + auth + API key"),
("model", "Model + reasoning effort"),
+ ("auxiliary_model", "Auxiliary model (optional)"),
("tavily", "Tavily search key"),
("workspace", "Workspace mode"),
("thinking", "Thinking panel"),
@@ -453,6 +457,7 @@ def run_onboard(
if "provider" in sections_to_run:
from .prompter import GoBack
+ _print_section("EvoScientist · Pilot (Main model)")
_require("provider", "LLM provider")
# Provider sub-loop: auth_mode can raise GoBack to re-pick provider.
# We snapshot config at the top of each iteration so a GoBack can
@@ -665,6 +670,63 @@ def run_onboard(
config.reasoning_effort = _step_reasoning_effort(config)
_autosave(config)
+ if "auxiliary_model" in sections_to_run:
+ _print_section("Co-pilot (Auxiliary model)")
+ if strict:
+ # Optional; never prompt under --non-interactive. Keep
+ # current (default empty = use main model).
+ _print_step_skipped(
+ "Auxiliary Model",
+ "kept current" if config.auxiliary_model else "not set",
+ )
+ elif _step_auxiliary_enable(config):
+ # Assemble: pick provider -> base URL (custom) -> key -> model,
+ # mirroring the main flow's order. Keys/base URLs are stored
+ # per provider, so when the auxiliary provider matches the main
+ # one they're already set and the user just keeps them (Enter).
+ # Ollama needs no key. Re-runs default to the saved auxiliary
+ # provider/model rather than the main ones.
+ aux_provider = _step_provider(
+ config,
+ label="co-pilot",
+ default_value=config.auxiliary_provider,
+ )
+ config.auxiliary_provider = aux_provider
+ if aux_provider == "custom-openai":
+ config.custom_openai_base_url = _step_base_url(
+ config,
+ current_value=config.custom_openai_base_url
+ or os.environ.get("CUSTOM_OPENAI_BASE_URL", ""),
+ )
+ elif aux_provider == "custom-anthropic":
+ config.custom_anthropic_base_url = _step_base_url(
+ config,
+ current_value=config.custom_anthropic_base_url
+ or os.environ.get("CUSTOM_ANTHROPIC_BASE_URL", ""),
+ )
+ elif aux_provider == "minimax":
+ config.minimax_base_url = _step_minimax_region(config)
+ if aux_provider != "ollama":
+ aux_key_attr = _PROVIDER_KEY_ATTR.get(
+ aux_provider, "openai_api_key"
+ )
+ new_aux_key = _step_provider_api_key(
+ config, aux_provider, skip_validation
+ )
+ if new_aux_key is not None:
+ setattr(config, aux_key_attr, new_aux_key)
+ config.auxiliary_model = _step_model(
+ config,
+ aux_provider,
+ label="co-pilot",
+ default_value=config.auxiliary_model,
+ )
+ else:
+ # Skip: single driver — clear any prior auxiliary config.
+ config.auxiliary_provider = ""
+ config.auxiliary_model = ""
+ _autosave(config)
+
if "tavily" in sections_to_run:
preset_tavily = _preset("tavily_key")
if preset_tavily is not None:
diff --git a/EvoScientist/config/settings.py b/EvoScientist/config/settings.py
index f897021..a14f7a3 100644
--- a/EvoScientist/config/settings.py
+++ b/EvoScientist/config/settings.py
@@ -95,6 +95,8 @@ class EvoScientistConfig:
tavily_api_key: Tavily API key for web search.
provider: Default LLM provider ('anthropic', 'openai', 'google-genai', or 'nvidia').
model: Default model name (short name or full ID).
+ auxiliary_provider: Provider for auxiliary_model (empty = use main provider).
+ auxiliary_model: Model for memory workers + tool selector (empty = use main model).
default_mode: Default workspace mode ('daemon' or 'run').
default_workdir: Default workspace directory (empty = use current working directory).
show_thinking: Whether to show thinking panels in CLI.
@@ -129,6 +131,10 @@ class EvoScientistConfig:
provider: str = "anthropic"
model: str = "claude-sonnet-4-6"
model_fallbacks: str = "" # "model:provider,model:provider" fallback chain
+ # Optional auxiliary model for background/helper LLM calls (memory workers +
+ # tool selector). Empty = fall back to the main model/provider.
+ auxiliary_provider: str = "" # empty = use main provider
+ auxiliary_model: str = "" # empty = use main model
# Async Sub-agent Settings
# When True (default), the EvoSci CLI auto-starts a langgraph dev subprocess
@@ -622,6 +628,8 @@ _ENV_MAPPINGS = {
"ui_backend": "EVOSCIENTIST_UI_BACKEND",
"log_level": "EVOSCIENTIST_LOG_LEVEL",
"model_fallbacks": "EVOSCIENTIST_MODEL_FALLBACKS",
+ "auxiliary_provider": "EVOSCIENTIST_AUXILIARY_PROVIDER",
+ "auxiliary_model": "EVOSCIENTIST_AUXILIARY_MODEL",
"reasoning_effort": "EVOSCIENTIST_REASONING_EFFORT",
"channel_debug_tracing": "EVOSCIENTIST_CHANNEL_DEBUG_TRACING",
"ccproxy_port": "EVOSCIENTIST_CCPROXY_PORT",
diff --git a/EvoScientist/llm/context_window.py b/EvoScientist/llm/context_window.py
index a1d62b9..2e4005f 100644
--- a/EvoScientist/llm/context_window.py
+++ b/EvoScientist/llm/context_window.py
@@ -17,8 +17,9 @@ _KNOWN_MODEL_CONTEXT_WINDOWS: dict[str, int] = {
# Qwen 3.6 open-source variants — exceptions to the ``qwen3.6`` family.
"qwen3.6-27b": 262_000,
"qwen3.6-35b-a3b": 262_000,
- # Qwen 3.7 Max — closed-source flagship (1M).
+ # Qwen 3.7 closed-source tiers — Max flagship and Plus (1M).
"qwen3.7-max": 1_000_000,
+ "qwen3.7-plus": 1_000_000,
# xAI Grok — per-model windows (build-0.1: 256K, 4.3: 1M).
"grok-build-0.1": 256_000,
"grok-4.3": 1_000_000,
diff --git a/EvoScientist/llm/models.py b/EvoScientist/llm/models.py
index 87c8164..d0b021b 100644
--- a/EvoScientist/llm/models.py
+++ b/EvoScientist/llm/models.py
@@ -20,6 +20,7 @@ from .patches import (
_patch_ccproxy_system_to_developer,
_patch_deepseek_reasoning_passback,
_patch_openai_compat_content,
+ _patch_openrouter_strip_responses_reasoning,
)
_MINIMAX_ANTHROPIC_BASE_URL = "https://api.minimaxi.com/anthropic"
@@ -149,6 +150,7 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
("grok-build-0.1", "x-ai/grok-build-0.1", "openrouter"),
("grok-4.3", "x-ai/grok-4.3", "openrouter"),
("qwen3.7-max", "qwen/qwen3.7-max", "openrouter"),
+ ("qwen3.7-plus", "qwen/qwen3.7-plus", "openrouter"),
("qwen3.6-flash", "qwen/qwen3.6-flash", "openrouter"),
("qwen3.5-122b", "qwen/qwen3.5-122b-a10b", "openrouter"),
("deepseek-v4-pro", "deepseek/deepseek-v4-pro", "openrouter"),
@@ -174,11 +176,21 @@ _MODEL_ENTRIES: list[tuple[str, str, str]] = [
("doubao-1.5-pro", "doubao-1.5-pro-256k", "volcengine"),
("doubao-1.5-thinking-pro", "doubao-1.5-thinking-pro", "volcengine"),
# DashScope Coding Plan (阿里云代码计划 — subscription sk-sp-* endpoint)
+ ("qwen3.7-max", "qwen3.7-max", "dashscope-code"),
+ ("qwen3.7-plus", "qwen3.7-plus", "dashscope-code"),
+ ("qwen3.6-max", "qwen3.6-max-preview", "dashscope-code"),
+ ("qwen3.6-plus", "qwen3.6-plus", "dashscope-code"),
+ ("qwen3.6-flash", "qwen3.6-flash", "dashscope-code"),
("qwen3-coder", "qwen3-coder-plus", "dashscope-code"),
("qwen3-coder-next", "qwen3-coder-next", "dashscope-code"),
("qwen3-max", "qwen3-max", "dashscope-code"),
("qwen3.5-plus", "qwen3.5-plus", "dashscope-code"),
# DashScope (阿里云 — Qwen models, default for simple lookups)
+ ("qwen3.7-max", "qwen3.7-max", "dashscope"),
+ ("qwen3.7-plus", "qwen3.7-plus", "dashscope"),
+ ("qwen3.6-max", "qwen3.6-max-preview", "dashscope"),
+ ("qwen3.6-plus", "qwen3.6-plus", "dashscope"),
+ ("qwen3.6-flash", "qwen3.6-flash", "dashscope"),
("qwen3-coder", "qwen3-coder-plus", "dashscope"),
("qwen3-235b", "qwen3-235b-a22b", "dashscope"),
("qwen-max", "qwen-max", "dashscope"),
@@ -403,9 +415,15 @@ def get_chat_model(
api_key = os.environ.get("OPENROUTER_API_KEY", "")
if api_key:
kwargs["api_key"] = api_key
- # Enable reasoning; disable summary to avoid multi-turn schema errors.
+ # Reasoning via `effort` + `summary: "auto"` so a readable reasoning
+ # summary is returned for display. OpenAI-Responses also emits encrypted
+ # reasoning items (`rs_*` id) that can't be replayed on multi-turn
+ # passback (OpenRouter's `/responses` beta is stateless, store=false —
+ # "Item with id 'rs_...' not found"); the patch strips them on passback,
+ # so enabling `summary` is safe. See langchain-ai/langchain#37777.
effort = os.environ.get("EVOSCIENTIST_REASONING_EFFORT", "").strip() or "high"
- kwargs.setdefault("reasoning", {"effort": effort, "summary": "disabled"})
+ kwargs.setdefault("reasoning", {"effort": effort, "summary": "auto"})
+ _patch_openrouter_strip_responses_reasoning()
# Anthropic-routed providers → route through Anthropic provider with base_url
elif provider in _ANTHROPIC_ROUTED_PROVIDERS:
diff --git a/EvoScientist/llm/patches.py b/EvoScientist/llm/patches.py
index 07e1c72..db786d3 100644
--- a/EvoScientist/llm/patches.py
+++ b/EvoScientist/llm/patches.py
@@ -14,6 +14,9 @@ Patches:
- _patch_deepseek_reasoning_passback: re-inject reasoning_content into
outgoing DeepSeek assistant messages for thinking-mode multi-turn /
tool_use scenarios
+ - _patch_openrouter_strip_responses_reasoning: drop OpenAI-Responses
+ encrypted reasoning items (rs_* id) from outgoing OpenRouter messages
+ (store=false → "Item with id rs_... not found")
Utilities:
- _is_ccproxy_codex: detect ccproxy Codex OAuth adapter
@@ -726,6 +729,60 @@ def _patch_openai_capture_reasoning_content() -> None:
_patch_openai_capture_reasoning_content()
+# ---------------------------------------------------------------------------
+# Patch (lazy, OpenRouter only): strip OpenAI-Responses encrypted reasoning
+# items from outgoing assistant messages.
+#
+# OpenRouter's `/responses` beta is stateless — it does not propagate
+# `store=true` / `previous_response_id` upstream. So when a prior turn's
+# assistant message carries a reasoning item with an `rs_*` id (the encrypted
+# Responses reasoning block), replaying it on the next turn fails with HTTP 400:
+# "Item with id 'rs_...' not found. Items are not persisted when `store` is
+# set to false. ... remove this item from your input."
+# (observed against both Azure and OpenAI upstreams — so deepagents'
+# azure-ignore is not sufficient). The only robust fix is to drop these items
+# on passback, exactly as the upstream error instructs. Reasoning DISPLAY is
+# unaffected: it happens when the item is generated, not on passback.
+# Related: langchain-ai/langchain#37777.
+# ---------------------------------------------------------------------------
+_openrouter_reasoning_strip_patched = False
+
+
+def _is_responses_reasoning_item(entry: Any) -> bool:
+ """True for an OpenAI-Responses encrypted reasoning item (`rs_` id / data)."""
+ if not isinstance(entry, dict):
+ return False
+ return str(entry.get("id") or "").startswith("rs_") or bool(entry.get("data"))
+
+
+def _patch_openrouter_strip_responses_reasoning() -> None:
+ global _openrouter_reasoning_strip_patched
+ if _openrouter_reasoning_strip_patched:
+ return
+ try:
+ import langchain_openrouter.chat_models as _mod
+
+ _orig = _mod._convert_message_to_dict
+
+ def _patched(message: Any) -> Any:
+ result = _orig(message)
+ details = (
+ result.get("reasoning_details") if isinstance(result, dict) else None
+ )
+ if isinstance(details, list):
+ kept = [e for e in details if not _is_responses_reasoning_item(e)]
+ if kept:
+ result["reasoning_details"] = kept
+ else:
+ result.pop("reasoning_details", None)
+ return result
+
+ _mod._convert_message_to_dict = _patched
+ _openrouter_reasoning_strip_patched = True
+ except Exception:
+ pass
+
+
# ---------------------------------------------------------------------------
# Patch: DeepSeek thinking mode requires reasoning_content to be passed back
# in all assistant messages for multi-turn + tool_use scenarios.
diff --git a/EvoScientist/middleware/memory_lifecycle.py b/EvoScientist/middleware/memory_lifecycle.py
index c574025..b715a7a 100644
--- a/EvoScientist/middleware/memory_lifecycle.py
+++ b/EvoScientist/middleware/memory_lifecycle.py
@@ -706,11 +706,13 @@ def _build_memory_worker_agent(
"""Create a background memory worker agent for one lifecycle hook."""
from deepagents import create_deep_agent
- from ..EvoScientist import _ensure_chat_model
+ from ..EvoScientist import _ensure_auxiliary_chat_model
agent = create_deep_agent(
name=role.worker_agent_name,
- model=_ensure_chat_model(),
+ # Memory workers are background helper agents — use the auxiliary model
+ # (falls back to the main model when auxiliary_* is unset).
+ model=_ensure_auxiliary_chat_model(),
system_prompt=system_prompt,
tools=[],
backend=_build_memory_worker_backend(
diff --git a/README.md b/README.md
index e91ca6c..1e0d587 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@
-
+
@@ -109,7 +109,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
## ✨ Features
- **🤖 Multi-Agent Team** — 6 sub-agents (plan, research, code, debug, analyze, write) working in concert.
-- **🧠 Persistent Memory** — Context, preferences, and findings survive across sessions.
+- **🧠 Self-Evolving Memory** — User profile and observations auto-distilled each turn, growing across sessions.
- **🌐 Multi-Provider** — Anthropic, OpenAI, Google, MiniMax, NVIDIA — one config to switch.
- **📱 Multi-Channel** — CLI as the hub; Telegram, Slack, Feishu, WeChat, and more — one agent session.
- **🖥️ Browser WebUI (beta)** — Workspace-panel web app, one terminal via `--ui webui`.
@@ -136,6 +136,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
📦 Release Highlights — version changelog
+- **[07 Jun 2026]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — Auxiliary model for background tasks & tool selection, observation-memory lifecycle, Qwen3.7-Max/Plus (DashScope), UI-backend selection, plus an OpenRouter multi-turn reasoning fix.
- **[03 Jun 2026]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — Multimodal handling (image + PDF/doc flatten/hoisting, text-only model fallback), runtime-context middleware, memory middleware → profile files with stream timeline narration, textual CJK-input fix.
- **[02 Jun 2026]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — Browser WebUI mode (beta), `EvoSci deploy` standalone LangGraph server, default model → claude-sonnet-4-6, MiniMax M3, plus sandbox-timeout and async-notifier channel-routing fixes.
- **[19 May 2026]** **[v0.1.1](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.1)** — deepagents 0.6.2 DeltaChannel upgrade, tier-aware skill mounts, status & elapsed-time bar, QQ inline buttons.
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 3f29cdb..653d24d 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -15,7 +15,7 @@
-
+
@@ -117,7 +117,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
## ✨ 特性
- **🤖 多智能体协作** — 6 个子智能体(规划、调研、编码、调试、分析、写作)协同工作。
-- **🧠 持久化记忆** — 上下文、偏好和研究发现跨会话保持。
+- **🧠 自进化记忆** — 用户画像与观察记录每轮自动提炼,跨会话持续进化。
- **🌐 多模型供应商** — Anthropic、OpenAI、Google、MiniMax、NVIDIA——一处配置,随时切换。
- **📱 多渠道接入** — CLI 为中心;Telegram、Slack、飞书、微信等——共享同一智能体会话。
- **🖥️ 浏览器 WebUI(beta)** — 单终端 `--ui webui` 启动带工作区面板的 Web 应用。
@@ -145,6 +145,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
📦 版本更新摘要(changelog)
+- **[2026 年 6 月 7 日]** **[v0.1.4](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.4)** — 辅助模型(后台任务与工具选择)、observation 记忆生命周期、Qwen3.7-Max/Plus(DashScope)、UI 后端选择,以及 OpenRouter 多轮推理修复。
- **[2026 年 6 月 3 日]** **[v0.1.3](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.3)** — 多模态处理(图片 + PDF/文档 flatten/hoisting、纯文本模型回退)、runtime-context 中间件、memory 中间件迁移至 profile 文件 + stream 时间线叙述、textual 中文输入修复。
- **[2026 年 6 月 2 日]** **[v0.1.2](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.2)** — 浏览器 WebUI 模式(beta)、`EvoSci deploy` 独立 LangGraph 服务器、默认模型 → claude-sonnet-4-6、MiniMax M3,以及 sandbox 超时与 async-notifier 渠道路由修复。
- **[2026 年 5 月 19 日]** **[v0.1.1](https://github.com/EvoScientist/EvoScientist/releases/tag/v0.1.1)** — deepagents 0.6.2 DeltaChannel 升级、tier-aware skill mounts、状态/耗时栏、QQ 内联按钮。
diff --git a/pyproject.toml b/pyproject.toml
index 2acabb2..b3bf063 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "EvoScientist"
-version = "0.1.3"
+version = "0.1.4"
description = "EvoScientist: Towards Self-Evolving AI Scientists for End-to-End Scientific Discovery"
readme = "README.md"
requires-python = ">=3.11"
@@ -16,7 +16,7 @@ classifiers = [
"Programming Language :: Python :: 3",
]
dependencies = [
- "deepagents[quickjs]~=0.6.7",
+ "deepagents[quickjs]~=0.6.8",
"langchain>=1.3",
"langchain-anthropic>=1.4",
"langchain-openai>=1.2",
diff --git a/tests/test_ask_user.py b/tests/test_ask_user.py
index 1725fb3..cc21f71 100644
--- a/tests/test_ask_user.py
+++ b/tests/test_ask_user.py
@@ -411,6 +411,8 @@ def test_auto_approve_still_includes_ask_user_middleware(
cfg.enable_ask_user = True
cfg.auto_approve = True
cfg.auto_mode = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
@@ -430,6 +432,8 @@ def test_auto_mode_disables_ask_user_middleware(
cfg.enable_ask_user = True
cfg.auto_approve = True
cfg.auto_mode = True
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
@@ -458,6 +462,8 @@ def test_for_async_subagent_omits_ask_user_middleware(
cfg.enable_ask_user = True
cfg.auto_approve = False
cfg.auto_mode = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
mock_model.return_value = MagicMock(profile={"max_input_tokens": 200_000})
diff --git a/tests/test_async_subagent_factory.py b/tests/test_async_subagent_factory.py
index ba2b741..60758ef 100644
--- a/tests/test_async_subagent_factory.py
+++ b/tests/test_async_subagent_factory.py
@@ -125,6 +125,8 @@ def test_inject_subagent_omits_memory_middleware_when_memory_disabled(
cfg.memory_observations_enabled = False
cfg.memory_observation_writer = MemoryObservationWriter.ALL
cfg.memory_workers_enabled = True
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _inject_subagent_middleware
@@ -153,6 +155,8 @@ def test_inject_subagent_worker_only_observation_writer_keeps_live_tool_off(
cfg.memory_observations_enabled = True
cfg.memory_observation_writer = MemoryObservationWriter.WORKER
cfg.memory_workers_enabled = True
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _inject_subagent_middleware
@@ -191,6 +195,8 @@ def test_all_observation_writer_skips_turn_worker_without_profile_memory(
cfg.memory_observations_enabled = True
cfg.memory_observation_writer = MemoryObservationWriter.ALL
cfg.memory_workers_enabled = True
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
mock_chat.return_value = MagicMock(profile={"max_input_tokens": 200_000})
@@ -259,6 +265,8 @@ def test_async_subagent_mode_filters_ask_user(
cfg.memory_observations_enabled = True
cfg.memory_observation_writer = MemoryObservationWriter.ALL
cfg.memory_workers_enabled = True
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
mock_chat.return_value = MagicMock(profile={"max_input_tokens": 200_000})
diff --git a/tests/test_auxiliary_model.py b/tests/test_auxiliary_model.py
new file mode 100644
index 0000000..37de4fd
--- /dev/null
+++ b/tests/test_auxiliary_model.py
@@ -0,0 +1,159 @@
+"""Tests for the auxiliary-model resolver and its middleware scoping.
+
+Covers ``EvoScientist.EvoScientist._ensure_auxiliary_chat_model`` (fallback to
+the main model when unset) and the wiring in ``_get_default_middleware`` that
+routes the main agent's tool selector to the auxiliary model while keeping
+context editing — and async sub-agents — on the main model.
+"""
+
+from types import SimpleNamespace
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+import EvoScientist.EvoScientist as E
+
+
+@pytest.fixture(autouse=True)
+def _reset_model_caches(monkeypatch):
+ """Isolate the module-level model caches per test."""
+ monkeypatch.setattr(E, "_chat_model", None, raising=False)
+ monkeypatch.setattr(E, "_chat_model_key", None, raising=False)
+ monkeypatch.setattr(E, "_auxiliary_chat_model", None, raising=False)
+ monkeypatch.setattr(E, "_auxiliary_chat_model_key", None, raising=False)
+
+
+def _cfg(**over):
+ base = {
+ "model": "main-m",
+ "provider": "main-p",
+ "auxiliary_model": "",
+ "auxiliary_provider": "",
+ }
+ base.update(over)
+ return SimpleNamespace(**base)
+
+
+class TestAuxiliaryResolver:
+ def test_empty_returns_main_instance(self, monkeypatch):
+ main = object()
+ monkeypatch.setattr(E, "_ensure_config", lambda config=None: _cfg())
+ monkeypatch.setattr(E, "_ensure_chat_model", lambda: main)
+ assert E._ensure_auxiliary_chat_model() is main
+
+ def test_aux_equal_to_main_reuses_main_instance(self, monkeypatch):
+ main = object()
+ monkeypatch.setattr(
+ E,
+ "_ensure_config",
+ lambda config=None: _cfg(
+ auxiliary_model="main-m", auxiliary_provider="main-p"
+ ),
+ )
+ monkeypatch.setattr(E, "_ensure_chat_model", lambda: main)
+ assert E._ensure_auxiliary_chat_model() is main
+
+ def test_set_builds_auxiliary(self, monkeypatch):
+ fake = object()
+ get_chat_model = MagicMock(return_value=fake)
+ monkeypatch.setattr(
+ E,
+ "_ensure_config",
+ lambda config=None: _cfg(
+ auxiliary_model="aux-m", auxiliary_provider="aux-p"
+ ),
+ )
+ monkeypatch.setattr("EvoScientist.llm.get_chat_model", get_chat_model)
+ assert E._ensure_auxiliary_chat_model() is fake
+ get_chat_model.assert_called_once_with(model="aux-m", provider="aux-p")
+
+ def test_empty_provider_falls_back_to_main_provider(self, monkeypatch):
+ get_chat_model = MagicMock(return_value=object())
+ monkeypatch.setattr(
+ E,
+ "_ensure_config",
+ lambda config=None: _cfg(auxiliary_model="aux-m", auxiliary_provider=""),
+ )
+ monkeypatch.setattr("EvoScientist.llm.get_chat_model", get_chat_model)
+ E._ensure_auxiliary_chat_model()
+ get_chat_model.assert_called_once_with(model="aux-m", provider="main-p")
+
+ def test_set_chat_model_resets_aux_cache(self, monkeypatch):
+ monkeypatch.setattr(E, "_auxiliary_chat_model", object(), raising=False)
+ monkeypatch.setattr(E, "_auxiliary_chat_model_key", ("x", "y"), raising=False)
+ monkeypatch.setattr(
+ "EvoScientist.llm.get_chat_model", MagicMock(return_value=object())
+ )
+ E.set_chat_model("new-m", "new-p")
+ assert E._auxiliary_chat_model is None
+ assert E._auxiliary_chat_model_key is None
+
+
+def _mock_cfg():
+ cfg = MagicMock()
+ cfg.enable_ask_user = False
+ cfg.auto_mode = False
+ cfg.auto_approve = False
+ cfg.model_fallbacks = None
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
+ return cfg
+
+
+class TestAuxiliaryMiddlewareScope:
+ """``_get_default_middleware`` routes only the right components to aux."""
+
+ def _capture(self):
+ cap: dict[str, object] = {}
+
+ def fake_tool_selector(*args, model=None, **kwargs):
+ cap["tool_selector"] = model
+ return [MagicMock()]
+
+ def fake_context_editing(model=None, *args, **kwargs):
+ cap["context_editing"] = model
+ return MagicMock()
+
+ return cap, fake_tool_selector, fake_context_editing
+
+ def test_main_agent_tool_selector_aux_context_editing_main(self):
+ cap, fake_ts, fake_ce = self._capture()
+ main_model, aux_model = object(), object()
+ with (
+ patch.object(E, "_ensure_config", return_value=_mock_cfg()),
+ patch.object(E, "_ensure_chat_model", return_value=main_model),
+ patch.object(E, "_ensure_auxiliary_chat_model", return_value=aux_model),
+ patch(
+ "EvoScientist.middleware.create_tool_selector_middleware",
+ side_effect=fake_ts,
+ ),
+ patch(
+ "EvoScientist.middleware.create_context_editing_middleware",
+ side_effect=fake_ce,
+ ),
+ ):
+ E._get_default_middleware()
+
+ assert cap["tool_selector"] is aux_model
+ assert cap["context_editing"] is main_model
+
+ def test_async_subagent_tool_selector_stays_main(self):
+ cap, fake_ts, fake_ce = self._capture()
+ main_model, aux_model = object(), object()
+ with (
+ patch.object(E, "_ensure_config", return_value=_mock_cfg()),
+ patch.object(E, "_ensure_chat_model", return_value=main_model),
+ patch.object(E, "_ensure_auxiliary_chat_model", return_value=aux_model),
+ patch(
+ "EvoScientist.middleware.create_tool_selector_middleware",
+ side_effect=fake_ts,
+ ),
+ patch(
+ "EvoScientist.middleware.create_context_editing_middleware",
+ side_effect=fake_ce,
+ ),
+ ):
+ E._get_default_middleware(for_async_subagent=True)
+
+ assert cap["tool_selector"] is main_model
+ assert cap["context_editing"] is main_model
diff --git a/tests/test_config.py b/tests/test_config.py
index c955dfe..da8a55d 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -50,6 +50,8 @@ def temp_config_dir(tmp_path, monkeypatch):
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
+ "EVOSCIENTIST_AUXILIARY_MODEL",
+ "EVOSCIENTIST_AUXILIARY_PROVIDER",
]:
monkeypatch.delenv(key, raising=False)
return config_dir
@@ -69,6 +71,8 @@ def clean_env(monkeypatch):
"EVOSCIENTIST_MEMORY_OBSERVATIONS_ENABLED",
"EVOSCIENTIST_MEMORY_OBSERVATION_WRITER",
"EVOSCIENTIST_MEMORY_WORKERS_ENABLED",
+ "EVOSCIENTIST_AUXILIARY_MODEL",
+ "EVOSCIENTIST_AUXILIARY_PROVIDER",
]:
monkeypatch.delenv(key, raising=False)
@@ -590,3 +594,39 @@ class TestApplyConfigToEnv:
apply_config_to_env(config)
assert os.environ.get("OLLAMA_BASE_URL") == "http://existing:11434"
+
+
+class TestAuxiliaryModelConfig:
+ """auxiliary_model / auxiliary_provider config fields (plain str, optional)."""
+
+ def test_defaults_empty(self):
+ cfg = EvoScientistConfig()
+ assert cfg.auxiliary_model == ""
+ assert cfg.auxiliary_provider == ""
+
+ def test_save_and_load_round_trip(self, temp_config_dir, clean_env):
+ save_config(
+ EvoScientistConfig(
+ auxiliary_model="claude-haiku-4-5",
+ auxiliary_provider="anthropic",
+ )
+ )
+ loaded = load_config()
+ assert loaded.auxiliary_model == "claude-haiku-4-5"
+ assert loaded.auxiliary_provider == "anthropic"
+
+ def test_get_set_value(self, temp_config_dir, clean_env):
+ save_config(EvoScientistConfig())
+ assert set_config_value("auxiliary_model", "qwen3.6-flash") is True
+ assert set_config_value("auxiliary_provider", "dashscope") is True
+ assert get_config_value("auxiliary_model") == "qwen3.6-flash"
+ assert get_config_value("auxiliary_provider") == "dashscope"
+
+ def test_env_overrides_file(self, temp_config_dir, monkeypatch):
+ save_config(EvoScientistConfig(auxiliary_model="claude-haiku-4-5"))
+ monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_MODEL", "gpt-5.5")
+ monkeypatch.setenv("EVOSCIENTIST_AUXILIARY_PROVIDER", "openai")
+
+ config = get_effective_config()
+ assert config.auxiliary_model == "gpt-5.5"
+ assert config.auxiliary_provider == "openai"
diff --git a/tests/test_context_editing_middleware.py b/tests/test_context_editing_middleware.py
index 929b4ed..c7db267 100644
--- a/tests/test_context_editing_middleware.py
+++ b/tests/test_context_editing_middleware.py
@@ -114,6 +114,8 @@ def test_default_middleware_includes_context_editing(mock_config, mock_model, mo
cfg = MagicMock()
cfg.enable_ask_user = False
cfg.auto_approve = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _get_default_middleware
@@ -148,6 +150,8 @@ def test_context_editing_before_overflow_mapper(mock_config, mock_model, mock_ts
cfg = MagicMock()
cfg.enable_ask_user = False
cfg.auto_approve = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _get_default_middleware
diff --git a/tests/test_llm.py b/tests/test_llm.py
index 9d797a3..63f7a4a 100644
--- a/tests/test_llm.py
+++ b/tests/test_llm.py
@@ -402,13 +402,15 @@ class TestThirdPartyRouting:
"""OpenRouter should use native 'openrouter' provider via init_chat_model."""
mock_init.return_value = "mock_model"
monkeypatch.setenv("OPENROUTER_API_KEY", "or-key-456")
+ # Assert the DEFAULT effort, so isolate from any leaked env override.
+ monkeypatch.delenv("EVOSCIENTIST_REASONING_EFFORT", raising=False)
get_chat_model("x-ai/grok-4.3", provider="openrouter")
call_kwargs = mock_init.call_args[1]
assert call_kwargs["model_provider"] == "openrouter"
assert call_kwargs["api_key"] == "or-key-456"
- assert call_kwargs["reasoning"] == {"effort": "high", "summary": "disabled"}
+ assert call_kwargs["reasoning"] == {"effort": "high", "summary": "auto"}
@patch("EvoScientist.llm.models.init_chat_model")
def test_openrouter_reasoning_user_override(self, mock_init, monkeypatch):
@@ -435,7 +437,7 @@ class TestThirdPartyRouting:
get_chat_model("x-ai/grok-4.3", provider="openrouter")
call_kwargs = mock_init.call_args[1]
- assert call_kwargs["reasoning"] == {"effort": "medium", "summary": "disabled"}
+ assert call_kwargs["reasoning"] == {"effort": "medium", "summary": "auto"}
@patch("EvoScientist.llm.models.init_chat_model")
def test_custom_routes_through_openai(self, mock_init, monkeypatch):
@@ -1994,6 +1996,142 @@ class TestPatchOpenAICaptureReasoningContent:
assert msg.additional_kwargs.get("reasoning_content") == "use the tool"
+class TestIsResponsesReasoningItem:
+ """_is_responses_reasoning_item flags encrypted OpenAI-Responses items."""
+
+ def test_rs_id_is_responses_item(self):
+ from EvoScientist.llm.patches import _is_responses_reasoning_item
+
+ assert _is_responses_reasoning_item({"id": "rs_09363d42", "type": "x"})
+
+ def test_encrypted_data_is_responses_item(self):
+ from EvoScientist.llm.patches import _is_responses_reasoning_item
+
+ assert _is_responses_reasoning_item({"data": "gAAAAAB...", "type": "x"})
+
+ def test_plain_text_reasoning_is_not_responses_item(self):
+ from EvoScientist.llm.patches import _is_responses_reasoning_item
+
+ assert not _is_responses_reasoning_item(
+ {"type": "reasoning.text", "text": "thinking", "index": 0}
+ )
+ assert not _is_responses_reasoning_item("not a dict")
+
+
+class TestPatchOpenrouterStripResponsesReasoning:
+ """OpenAI-Responses encrypted reasoning items (`rs_` id / encrypted data)
+ are stripped from outgoing OpenRouter assistant messages, preventing the
+ multi-turn "Item with id 'rs_...' not found" 400 (store=false; #37777).
+ """
+
+ def _apply(self):
+ import langchain_openrouter.chat_models as mod
+
+ import EvoScientist.llm.patches as patches
+
+ orig = mod._convert_message_to_dict
+ orig_flag = patches._openrouter_reasoning_strip_patched
+ patches._openrouter_reasoning_strip_patched = False
+ patches._patch_openrouter_strip_responses_reasoning()
+ return patches, mod, orig, orig_flag
+
+ @staticmethod
+ def _restore(patches, mod, orig, orig_flag):
+ mod._convert_message_to_dict = orig
+ patches._openrouter_reasoning_strip_patched = orig_flag
+
+ def test_strips_encrypted_item_drops_key_when_empty(self):
+ from langchain_core.messages import AIMessage
+
+ patches, mod, orig, orig_flag = self._apply()
+ try:
+ msg = AIMessage(
+ content="done",
+ additional_kwargs={
+ "reasoning_details": [
+ {
+ "type": "reasoning.summary",
+ "format": "openai-responses-v1",
+ "id": "rs_09363d42b054",
+ "data": "gAAAAAB...",
+ "summary": "real reasoning text",
+ "index": 0,
+ }
+ ],
+ },
+ )
+ result = mod._convert_message_to_dict(msg)
+ # sole entry was an rs_ item → reasoning_details removed entirely.
+ assert "reasoning_details" not in result
+ finally:
+ self._restore(patches, mod, orig, orig_flag)
+
+ def test_keeps_plain_text_reasoning(self):
+ from langchain_core.messages import AIMessage
+
+ patches, mod, orig, orig_flag = self._apply()
+ try:
+ msg = AIMessage(
+ content="done",
+ additional_kwargs={
+ "reasoning_details": [
+ {"type": "reasoning.text", "text": "thinking", "index": 0},
+ {"id": "rs_abc", "data": "blob", "index": 1},
+ ],
+ },
+ )
+ result = mod._convert_message_to_dict(msg)
+ kept = result["reasoning_details"]
+ assert len(kept) == 1
+ assert kept[0]["type"] == "reasoning.text"
+ finally:
+ self._restore(patches, mod, orig, orig_flag)
+
+ def test_does_not_mutate_original_message(self):
+ from langchain_core.messages import AIMessage
+
+ patches, mod, orig, orig_flag = self._apply()
+ try:
+ details = [{"id": "rs_abc", "data": "blob"}]
+ msg = AIMessage(
+ content="x", additional_kwargs={"reasoning_details": details}
+ )
+ mod._convert_message_to_dict(msg)
+ # stored history untouched — we filter a fresh list, not in place.
+ assert details == [{"id": "rs_abc", "data": "blob"}]
+ finally:
+ self._restore(patches, mod, orig, orig_flag)
+
+ def test_patch_is_idempotent(self):
+ patches, mod, orig, orig_flag = self._apply()
+ try:
+ wrapper = mod._convert_message_to_dict
+ # Second call is guarded by the flag → must not re-wrap.
+ patches._patch_openrouter_strip_responses_reasoning()
+ assert mod._convert_message_to_dict is wrapper
+ finally:
+ self._restore(patches, mod, orig, orig_flag)
+
+ def test_non_dict_entry_is_kept(self):
+ from langchain_core.messages import AIMessage
+
+ patches, mod, orig, orig_flag = self._apply()
+ try:
+ msg = AIMessage(
+ content="done",
+ additional_kwargs={
+ "reasoning_details": [
+ "opaque", # non-dict slipped in → kept, not crashed on
+ {"id": "rs_abc", "data": "blob", "index": 1},
+ ],
+ },
+ )
+ result = mod._convert_message_to_dict(msg)
+ assert result["reasoning_details"] == ["opaque"]
+ finally:
+ self._restore(patches, mod, orig, orig_flag)
+
+
# =============================================================================
# Test _apply_auto_config
# =============================================================================
diff --git a/tests/test_onboard.py b/tests/test_onboard.py
index 0e99a15..b62ca38 100644
--- a/tests/test_onboard.py
+++ b/tests/test_onboard.py
@@ -46,15 +46,16 @@ def _patch_all_questionary(mock_q):
class TestConstants:
- def test_steps_has_twelve_items(self):
- """Test that STEPS contains exactly 12 steps."""
- assert len(STEPS) == 12
+ def test_steps_has_thirteen_items(self):
+ """Test that STEPS contains exactly 13 steps."""
+ assert len(STEPS) == 13
assert STEPS == [
"UI",
"LangGraph Port",
"Provider",
"API Key",
"Model",
+ "Auxiliary Model",
"Tavily Key",
"Workspace",
"Thinking",
@@ -353,6 +354,20 @@ class TestStepProvider:
assert result == "anthropic"
mock_q.select.assert_called_once()
+ def test_default_value_and_label_override(self):
+ """default_value preselects a provider (re-run co-pilot default) and
+ label customizes the prompt text."""
+ from EvoScientist.config.onboard.steps import _step_provider
+
+ config = EvoScientistConfig(provider="anthropic")
+ with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
+ mock_q.select.return_value.ask.return_value = "openai"
+ _step_provider(config, label="co-pilot", default_value="openrouter")
+
+ call = mock_q.select.call_args
+ assert call.kwargs["default"] == "openrouter" # override, not config.provider
+ assert "co-pilot" in call.args[0]
+
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test that _step_provider raises KeyboardInterrupt on cancel."""
from EvoScientist.config.onboard.steps import _step_provider
@@ -378,6 +393,38 @@ class TestStepModel:
assert result == "claude-sonnet-4-6"
+ def test_main_model_not_in_provider_list_defaults_to_first(self):
+ """Reset/main flow: a config.model that isn't in the chosen provider's
+ list (e.g. provider switched to google-genai) defaults to that
+ provider's first model, NOT the custom 'Type a model name...' entry."""
+ from EvoScientist.config.onboard.steps import _step_model
+ from EvoScientist.llm.models import get_models_for_provider
+
+ config = EvoScientistConfig(model="claude-sonnet-4-6")
+ entries = get_models_for_provider("google-genai")
+ with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
+ mock_q.select.return_value.ask.return_value = entries[0][0]
+ _step_model(config, "google-genai")
+
+ default = mock_q.select.call_args.kwargs["default"]
+ assert default == entries[0][0]
+ assert default != "__custom__"
+
+ def test_custom_default_value_preselects_and_prefills(self):
+ """Co-pilot re-run: a saved custom (non-registry) model preselects and
+ prefills the 'Type a model name...' entry."""
+ from EvoScientist.config.onboard.steps import _step_model
+
+ config = EvoScientistConfig()
+ with patch("EvoScientist.config.onboard.steps.questionary") as mock_q:
+ mock_q.select.return_value.ask.return_value = "__custom__"
+ mock_q.text.return_value.ask.return_value = "my-private/model"
+ result = _step_model(config, "openrouter", default_value="my-private/model")
+
+ assert mock_q.select.call_args.kwargs["default"] == "__custom__"
+ assert mock_q.text.call_args.kwargs["default"] == "my-private/model"
+ assert result == "my-private/model"
+
def test_raises_keyboard_interrupt_on_cancel(self):
"""Test that _step_model raises KeyboardInterrupt on cancel."""
from EvoScientist.config.onboard.steps import _step_model
@@ -1143,6 +1190,7 @@ class TestRunOnboard:
"anthropic", # Provider
"api_key", # Anthropic auth mode (API key, not OAuth)
"claude-sonnet-4-6", # Model
+ "skip", # Auxiliary: Skip (single driver)
"daemon", # Workspace mode
True, # Show thinking
]
@@ -1173,6 +1221,101 @@ class TestRunOnboard:
assert final_config.ui_backend == "tui"
assert final_config.default_mode == "daemon"
+ def test_auxiliary_model_enabled_collects_provider_and_key(self):
+ """Enabling the auxiliary step stores its provider, model, and the
+ chosen provider's API key (a different company than the main agent)."""
+ from EvoScientist.config.onboard.wizard import run_onboard
+
+ mock_q = MagicMock()
+ with (
+ _patch_all_questionary(mock_q),
+ patch("EvoScientist.config.onboard.wizard.load_config") as mock_load,
+ patch("EvoScientist.config.onboard.wizard.save_config") as mock_save,
+ patch("EvoScientist.config.onboard.wizard.console"),
+ patch("EvoScientist.config.onboard.steps.console"),
+ patch("EvoScientist.config.onboard.channels.console"),
+ patch("EvoScientist.config.onboard.helpers.console"),
+ patch("EvoScientist.config.onboard.wizard._step_tinytex"),
+ ):
+ mock_load.return_value = EvoScientistConfig()
+
+ mock_q.select.return_value.ask.side_effect = [
+ "tui", # UI backend
+ "anthropic", # Provider
+ "api_key", # Anthropic auth mode
+ "claude-sonnet-4-6", # Model
+ "assemble", # Auxiliary: Assemble
+ "openai", # Auxiliary provider (a different company)
+ "gpt-5.5", # Auxiliary model
+ "daemon", # Workspace mode
+ True, # Show thinking
+ ]
+ mock_q.password.return_value.ask.side_effect = [
+ "", # Main provider API key (keep current)
+ "sk-aux-openai", # Auxiliary provider API key
+ "", # Tavily key (keep current)
+ ]
+ mock_q.confirm.return_value.ask.side_effect = [
+ True, # Save config
+ ]
+ mock_q.text.return_value.ask.side_effect = [
+ "", # Workspace directory
+ ]
+ mock_q.checkbox.return_value.ask.return_value = [] # Skills: skip
+
+ result = run_onboard(skip_validation=True)
+
+ assert result is True
+ final_config = mock_save.call_args_list[-1].args[0]
+ assert final_config.auxiliary_provider == "openai"
+ assert final_config.auxiliary_model == "gpt-5.5"
+ # The auxiliary provider's key is stored in its per-provider field.
+ assert final_config.openai_api_key == "sk-aux-openai"
+ # Main agent is untouched.
+ assert final_config.provider == "anthropic"
+ assert final_config.model == "claude-sonnet-4-6"
+
+ def test_auxiliary_custom_provider_collects_base_url(self):
+ """Regression for the custom-provider fix: a custom auxiliary provider
+ collects its base URL (provider -> base URL -> key -> model order)."""
+ from EvoScientist.config.onboard.wizard import run_onboard
+
+ mock_q = MagicMock()
+ with (
+ _patch_all_questionary(mock_q),
+ patch("EvoScientist.config.onboard.wizard.load_config") as mock_load,
+ patch("EvoScientist.config.onboard.wizard.save_config") as mock_save,
+ patch("EvoScientist.config.onboard.wizard.console"),
+ patch("EvoScientist.config.onboard.steps.console"),
+ patch("EvoScientist.config.onboard.channels.console"),
+ patch("EvoScientist.config.onboard.helpers.console"),
+ ):
+ mock_load.return_value = EvoScientistConfig()
+ mock_q.select.return_value.ask.side_effect = [
+ "assemble", # Auxiliary: Assemble
+ "custom-openai", # Auxiliary provider
+ "gpt-5.5", # Auxiliary model (from the custom-openai registry)
+ ]
+ mock_q.text.return_value.ask.side_effect = [
+ "https://my-endpoint/v1", # Auxiliary base URL (custom provider)
+ ]
+ mock_q.password.return_value.ask.side_effect = [
+ "sk-aux-custom", # Auxiliary provider API key
+ ]
+ mock_q.confirm.return_value.ask.side_effect = [True] # Save
+
+ result = run_onboard(
+ skip_validation=True, only_sections={"auxiliary_model"}
+ )
+
+ assert result is True
+ final_config = mock_save.call_args_list[-1].args[0]
+ assert final_config.auxiliary_provider == "custom-openai"
+ assert final_config.auxiliary_model == "gpt-5.5"
+ # Base URL must be collected for the custom auxiliary provider.
+ assert final_config.custom_openai_base_url == "https://my-endpoint/v1"
+ assert final_config.custom_openai_api_key == "sk-aux-custom"
+
def test_returns_false_on_cancel(self):
"""Test that run_onboard returns False when cancelled."""
from EvoScientist.config.onboard.wizard import run_onboard
@@ -1218,6 +1361,7 @@ class TestRunOnboard:
"anthropic", # Provider
"api_key", # Anthropic auth mode
"claude-sonnet-4-6", # Model
+ "skip", # Auxiliary: Skip (single driver)
"daemon", # Workspace mode
True, # Show thinking
]
@@ -1288,11 +1432,14 @@ class TestRunOnboard:
"anthropic",
"api_key",
"claude-sonnet-4-6",
+ "skip", # Auxiliary: Skip (single driver)
"daemon",
True,
]
mock_q.password.return_value.ask.side_effect = ["", ""]
- mock_q.confirm.return_value.ask.side_effect = [False] # Save? = No
+ mock_q.confirm.return_value.ask.side_effect = [
+ False, # Save? = No
+ ]
mock_q.text.return_value.ask.side_effect = [""]
mock_q.checkbox.return_value.ask.return_value = []
@@ -1335,11 +1482,14 @@ class TestRunOnboard:
"anthropic",
"api_key",
"claude-sonnet-4-6",
+ "skip", # Auxiliary: Skip (single driver)
"daemon",
True,
]
mock_q.password.return_value.ask.side_effect = ["", ""]
- mock_q.confirm.return_value.ask.side_effect = [False] # Save? = No
+ mock_q.confirm.return_value.ask.side_effect = [
+ False, # Save? = No
+ ]
mock_q.text.return_value.ask.side_effect = [""]
mock_q.checkbox.return_value.ask.return_value = []
diff --git a/tests/test_runtime_context_middleware.py b/tests/test_runtime_context_middleware.py
index 7cb560d..a3f7465 100644
--- a/tests/test_runtime_context_middleware.py
+++ b/tests/test_runtime_context_middleware.py
@@ -40,6 +40,8 @@ def _mock_config():
cfg.auto_mode = False
cfg.auto_approve = False
cfg.model_fallbacks = None
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
cfg.code_interpreter_timeout = 60
cfg.code_interpreter_max_result_chars = 6000
return cfg
diff --git a/tests/test_tool_selector_middleware.py b/tests/test_tool_selector_middleware.py
index 007616e..2759614 100644
--- a/tests/test_tool_selector_middleware.py
+++ b/tests/test_tool_selector_middleware.py
@@ -161,6 +161,8 @@ def test_default_middleware_includes_tool_selector(mock_config, mock_model, mock
cfg = MagicMock()
cfg.enable_ask_user = False
cfg.auto_approve = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _get_default_middleware
@@ -196,6 +198,8 @@ def test_tool_selector_ordering(mock_config, mock_model, mock_ts):
cfg = MagicMock()
cfg.enable_ask_user = False
cfg.auto_approve = False
+ cfg.auxiliary_model = ""
+ cfg.auxiliary_provider = ""
mock_config.return_value = cfg
from EvoScientist.EvoScientist import _get_default_middleware
diff --git a/uv.lock b/uv.lock
index 53ced30..7b4cd7f 100644
--- a/uv.lock
+++ b/uv.lock
@@ -810,7 +810,7 @@ wheels = [
[[package]]
name = "deepagents"
-version = "0.6.7"
+version = "0.6.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain" },
@@ -820,9 +820,9 @@ dependencies = [
{ name = "langsmith" },
{ name = "wcmatch" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/51/bb/bb837a2c51631fe9d7eedf6aca7629ddca6336831801e75efcd2f5fa9c27/deepagents-0.6.7.tar.gz", hash = "sha256:af7b5857b28e29a847a4ced4cc7aaa809d33d42107696b1ca5d978d17e96b831", size = 194236, upload-time = "2026-05-30T04:42:14.591Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/97/77/e3b7efd9bff9cd101c085a5a3bf74180c13ab6c41a96f725cd1cb1bf53e8/deepagents-0.6.8.tar.gz", hash = "sha256:70cdd4da920cc420a8a0f729792ec559688bbbff39f7ab1508110cce9f901c06", size = 196927, upload-time = "2026-06-03T17:08:36.724Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/97/25/3a7f23d04778ccb95542f1b1ed39826290916221cc8203d0fb8b26c3edc1/deepagents-0.6.7-py3-none-any.whl", hash = "sha256:3518c1e5f4b9f6588ba39912b668b42b5c864b99a638e94c166d1c7176c7388e", size = 218740, upload-time = "2026-05-30T04:42:13.225Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/19/1b7b76e958ac7f4e40886edc70f67aff4d7188770ab68105c9c48cbeb769/deepagents-0.6.8-py3-none-any.whl", hash = "sha256:087bdc1458202a3436854cf180f7ec059d07d2114a6c232819e9ad6533a5174a", size = 221469, upload-time = "2026-06-03T17:08:35.133Z" },
]
[package.optional-dependencies]
@@ -997,7 +997,7 @@ requires-dist = [
{ name = "certifi", marker = "extra == 'wechat'", specifier = ">=2024.0" },
{ name = "cryptography", marker = "extra == 'all-channels'", specifier = ">=41.0" },
{ name = "cryptography", marker = "extra == 'qq'", specifier = ">=41.0" },
- { name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.7" },
+ { name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.8" },
{ name = "discord-py", marker = "extra == 'all-channels'", specifier = ">=2.3" },
{ name = "discord-py", marker = "extra == 'discord'", specifier = ">=2.3" },
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
@@ -1960,16 +1960,16 @@ wheels = [
[[package]]
name = "langchain"
-version = "1.3.2"
+version = "1.3.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
{ name = "langgraph" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/d5/d0/c7f9d3d26c0e3f8bb146c6d707ee0fc1d30d8da65a59626e8a580085e929/langchain-1.3.2.tar.gz", hash = "sha256:ffd5f204a46b5fa1a38bf89ba3b45ca0902c02d18fa7d2a2eaeaeb1f5bf19d0a", size = 600598, upload-time = "2026-05-26T18:17:57.715Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/3f/034eb6cbef90bfccc89b7f8ed0c1d853dc9cb0bea17c7a269534c647ba3a/langchain-1.3.4.tar.gz", hash = "sha256:d6e0654c22848925534f5c0a706f9be481bb09a619ec60a738fbd1e5502e457a", size = 606617, upload-time = "2026-06-02T20:04:49.411Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f8/82/a54edcd1c48163de5642eb10fa2cb58b13a8889c659964f63f0306b58b1e/langchain-1.3.2-py3-none-any.whl", hash = "sha256:900f6b3f4ee08b9ba3cdbe667dbf42525bd6f66a4a07a7f1db26262673e41ed6", size = 121225, upload-time = "2026-05-26T18:17:56.075Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/29/9ffe99c7dc4891a0215ec59c423bea320f943c08a231bc5bae392a438a83/langchain-1.3.4-py3-none-any.whl", hash = "sha256:e51b05ab23d056bc6bf2d97d8c694fb92d6d5765126fef74565d007c27581672", size = 125286, upload-time = "2026-06-02T20:04:48.13Z" },
]
[[package]]
@@ -2092,14 +2092,14 @@ wheels = [
[[package]]
name = "langchain-protocol"
-version = "0.0.14"
+version = "0.0.16"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/05/bf/efb5e2ed832e4d6d45590e25a9e5191986b291b543bc6a807b48bee070b0/langchain_protocol-0.0.14.tar.gz", hash = "sha256:bc1e8553122e6ede310280462d5813023a172ff2785ccbbdec54d43f3a15e5f2", size = 5862, upload-time = "2026-04-29T16:40:18.657Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/36/e7/8300ba22d968653051fd06e3117d783872dddf3dcebdd6b1d386836eb43c/langchain_protocol-0.0.16.tar.gz", hash = "sha256:806c7cdd951b1c4f692fa40fce60821ff0f221d4360e27673ddf2c2b99c2b7ff", size = 5969, upload-time = "2026-05-28T23:05:11.121Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/c2/e9/06c47ecb2aff08f83dfa30058da3bf86be64862c19569043ed5331bbeecd/langchain_protocol-0.0.14-py3-none-any.whl", hash = "sha256:ffc35089779bd8ca217015180cef5e660fc3b074efdaa0f2e95df73583f1a047", size = 6984, upload-time = "2026-04-29T16:40:17.841Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/9c/06dfcc88d02a6364e8d864c421ddd3736305cb0a6c853f75c302c80fe17c/langchain_protocol-0.0.16-py3-none-any.whl", hash = "sha256:3658c142c5d0fb3a023a4be442ce4c15c6d626aab6135eb79a76dc64ad19c3c3", size = 7037, upload-time = "2026-05-28T23:05:10.163Z" },
]
[[package]]
@@ -2120,7 +2120,7 @@ wheels = [
[[package]]
name = "langgraph"
-version = "1.2.2"
+version = "1.2.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "langchain-core" },
@@ -2130,9 +2130,9 @@ dependencies = [
{ name = "pydantic" },
{ name = "xxhash" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/e6/5a/ffc12434ee8aecab830d58b4d204ddea45073eae7639c963310f671a5bf5/langgraph-1.2.2.tar.gz", hash = "sha256:f54a98458976b3ff0774683867df125fb52d8dbedeb2441d0b0656a51331cee5", size = 695730, upload-time = "2026-05-26T18:07:28.49Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/1c/43/dac5a2621c1e57f8eb7f0703f6f6fe34a5caf62f8f0fb4d2bb395bb454ea/langgraph-1.2.4.tar.gz", hash = "sha256:5df076973a2d23efb13eceb279d1e5b46feebcbbeded0a86a2ef669abd9e4399", size = 720374, upload-time = "2026-06-02T17:07:37.347Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/42/9b/b08d578bba73e25351152dfd3d6d21e81210a5fff1b6f26e56f33197c8f5/langgraph-1.2.2-py3-none-any.whl", hash = "sha256:0a851bf4ba5939c5474a2fd57e6b439b5315283e254e42943bd392c2d71a5e03", size = 236376, upload-time = "2026-05-26T18:07:26.577Z" },
+ { url = "https://files.pythonhosted.org/packages/48/9e/31ca236104966d7bb14ea9e93cfd73350aea8c41008ddf057b65794ed10d/langgraph-1.2.4-py3-none-any.whl", hash = "sha256:ffe3e1e31dce28907640f82525858470f293506d2b272d07ea3b3ce97974b067", size = 245402, upload-time = "2026-06-02T17:07:35.977Z" },
]
[[package]]
@@ -2258,15 +2258,18 @@ wheels = [
[[package]]
name = "langgraph-sdk"
-version = "0.3.13"
+version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "httpx" },
+ { name = "langchain-core" },
+ { name = "langchain-protocol" },
{ name = "orjson" },
+ { name = "websockets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/0e/db/77a45127dddcfea5e4256ba916182903e4c31dc4cfca305b8c386f0a9e53/langgraph_sdk-0.3.13.tar.gz", hash = "sha256:419ca5663eec3cec192ad194ac0647c0c826866b446073eb40f384f950986cd5", size = 196360, upload-time = "2026-04-07T20:34:18.766Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/b4/2b/bd8ac26d4e97f6df88ef05ce5b6a38945a3903e1025d926f4752aa88aa97/langgraph_sdk-0.4.2.tar.gz", hash = "sha256:b88f0f5f6328ac0680d6790614a905b2bcfa257f2276dba4e38f0e86db0aa738", size = 348327, upload-time = "2026-06-01T17:51:19.856Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fe/ef/64d64e9f8eea47ce7b939aa6da6863b674c8d418647813c20111645fcc62/langgraph_sdk-0.3.13-py3-none-any.whl", hash = "sha256:aee09e345c90775f6de9d6f4c7b847cfc652e49055c27a2aed0d981af2af3bd0", size = 96668, upload-time = "2026-04-07T20:34:17.866Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/05/aac507337cceae773c2cc9ab91eb6301963af7aeeb55b4217a00e15aff17/langgraph_sdk-0.4.2-py3-none-any.whl", hash = "sha256:75fa5096c1177ce39c847096a8fe3745ffd480ddb412995f836e9f5f884c43dd", size = 160521, upload-time = "2026-06-01T17:51:18.849Z" },
]
[[package]]
@@ -4671,61 +4674,44 @@ wheels = [
[[package]]
name = "websockets"
-version = "16.0"
+version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
- { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
- { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
- { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
- { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
- { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
- { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
- { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
- { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
- { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
- { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
- { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
- { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
- { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
- { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
- { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
- { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
- { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
- { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
- { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
- { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
- { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
- { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
- { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
- { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
- { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
- { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
- { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
- { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
- { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
- { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
- { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
- { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
- { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
- { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
- { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
- { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
- { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
- { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
- { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
- { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
- { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
- { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
- { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
- { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
- { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
- { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
- { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
- { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
- { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
- { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
+ { url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
+ { url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
+ { url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
+ { url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
+ { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
+ { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
+ { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
+ { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
+ { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },
+ { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" },
+ { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" },
+ { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" },
+ { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" },
+ { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" },
+ { 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]]