sliceagent 0.1.6__tar.gz → 0.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sliceagent-0.1.6 → sliceagent-0.1.8}/CHANGELOG.md +32 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/PKG-INFO +1 -1
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/cli.py +40 -10
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/config.py +6 -1
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/llm.py +6 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/onboarding.py +57 -7
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/policy.py +28 -4
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tui.py +10 -4
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bughunt_fixes.py +4 -1
- sliceagent-0.1.8/tests/test_config_journey.py +260 -0
- sliceagent-0.1.6/tests/test_config_journey.py +0 -107
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.dockerignore +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.env.example +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/.gitignore +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/Dockerfile +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/LICENSE +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/NOTICE +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/QUICKSTART.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/README.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/SECURITY.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/install.sh +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/pyproject.toml +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/neocortex.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/sensory_cortex.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/tools.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/README.md +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_agents.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_cli_smoke.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_closure.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_episode.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_flags.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_history.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_memory.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_menu_select.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mining.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_onboarding.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_procman.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_prompt_ab.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_provider_lineup.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_reach.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_records.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_refault.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_repo_map_bounded.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_safety.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_web.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_world_region.py +0 -0
- {sliceagent-0.1.6 → sliceagent-0.1.8}/tests/test_world_state.py +0 -0
|
@@ -5,6 +5,38 @@ this project aims for [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.8] — 2026-07-02
|
|
9
|
+
|
|
10
|
+
Launch-day bug-hunt round: a security fix plus config-robustness and /model correctness.
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
- `run_command` in the default *teenager* mode no longer auto-runs exec/write commands disguised as
|
|
14
|
+
"read-only": `env <program>`, `sort -o FILE`, `date -s`, `tree -o FILE`, `uniq IN OUT`, and
|
|
15
|
+
`git grep --open-files-in-pager` now take the confirm path (arbitrary code-exec / file-overwrite
|
|
16
|
+
confirm-bypass). Read-only siblings like `du -s` / `grep -o` still auto-run.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- Boot no longer cross-wires a prefs-pinned provider's key with the DEFAULT provider's endpoint (an
|
|
20
|
+
env `gpt-5.5` @ OpenAI pin on a DeepSeek-default config used to 401 every call on relaunch).
|
|
21
|
+
- A prefs `provider`/model pin whose `[providers.<id>]` table was removed from config.toml is now
|
|
22
|
+
dropped at boot instead of forcing a model onto the wrong endpoint.
|
|
23
|
+
- The wizard no longer silently ERASES other providers' API keys when config.toml is unparseable —
|
|
24
|
+
the old file is moved to a non-clobbering `.bak` first (keys recoverable).
|
|
25
|
+
- `sliceagent config` / `config --use` / the init wizard no longer crash on a non-UTF-8 config.toml
|
|
26
|
+
or a scalar value under `[providers]` (they degrade).
|
|
27
|
+
- `llm.switch()` (a `/model` provider hop) closes the replaced HTTP client — no more fd leak per hop.
|
|
28
|
+
- The `/model` reasoning menu offers `high` (not a misleading `max`) for OpenRouter models, and offers
|
|
29
|
+
the levels the *target* provider supports when the pick rebinds the endpoint.
|
|
30
|
+
- The wizard's typed provider fallback rejects an unknown choice instead of silently configuring
|
|
31
|
+
OpenRouter.
|
|
32
|
+
|
|
33
|
+
## [0.1.7] — 2026-07-02
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
- `/model`: the current env-configured model row is now labeled `current (env)` instead of the
|
|
37
|
+
provider family name — it could masquerade as a configured provider (e.g. an env `gpt-5.5` on a
|
|
38
|
+
DeepSeek endpoint showed as "deepseek").
|
|
39
|
+
|
|
8
40
|
## [0.1.6] — 2026-07-02
|
|
9
41
|
|
|
10
42
|
The clear config journey: one wizard, two doors, an honest /model.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sliceagent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.8
|
|
4
4
|
Summary: A coding agent with a new context-engineering framework: bounded, deterministic, reconstructed context (slice architecture) — built for long-horizon work.
|
|
5
5
|
Author: TT-Wang
|
|
6
6
|
License-Expression: MIT
|
|
@@ -151,6 +151,21 @@ def _ws_name(path: str) -> str:
|
|
|
151
151
|
return "~" if p == os.path.realpath(os.path.expanduser("~")) else (os.path.basename(p) or p)
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
def _env_from_config(c, pid: str | None = None) -> None:
|
|
155
|
+
"""Populate LLM_API_KEY/LLM_BASE_URL from config (ENV still wins). A prefs-pinned provider `pid`
|
|
156
|
+
binds AS A UNIT: its key with ITS OWN endpoint — an absent base_url means the SDK default (OpenAI),
|
|
157
|
+
NOT the default provider's base_url (that fallback cross-wired an openai key onto the deepseek
|
|
158
|
+
endpoint after `/model`-hop + restart when default_provider pointed elsewhere)."""
|
|
159
|
+
tbl = (c.providers() or {}).get(pid) if pid else None
|
|
160
|
+
if isinstance(tbl, dict) and tbl.get("api_key"):
|
|
161
|
+
key, base = tbl["api_key"], tbl.get("base_url") or ""
|
|
162
|
+
else: # no/unusable pin → the default provider's own pairing
|
|
163
|
+
key, base = c.api_key, c.base_url
|
|
164
|
+
for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
|
|
165
|
+
if not os.environ.get(_env) and _val:
|
|
166
|
+
os.environ[_env] = _val
|
|
167
|
+
|
|
168
|
+
|
|
154
169
|
def main() -> None:
|
|
155
170
|
# subcommands (onboarding / discovery) are handled BEFORE any key gate, so `sliceagent init` runs on a
|
|
156
171
|
# machine with nothing configured yet. A bare `sliceagent` (or one with non-subcommand args) falls through.
|
|
@@ -165,14 +180,6 @@ def main() -> None:
|
|
|
165
180
|
from .config import load_config, load_prefs
|
|
166
181
|
_boot_prefs = load_prefs() # the last /model choice may pin a PROVIDER (endpoint+key), not just a model
|
|
167
182
|
|
|
168
|
-
def _env_from_config(c, pid: str | None = None) -> None:
|
|
169
|
-
tbl = (c.providers() or {}).get(pid) if pid else None
|
|
170
|
-
key = (tbl or {}).get("api_key") or c.api_key
|
|
171
|
-
base = ((tbl or {}).get("base_url") if tbl and tbl.get("api_key") else None) or c.base_url
|
|
172
|
-
for _env, _val in (("LLM_API_KEY", key), ("LLM_BASE_URL", base)):
|
|
173
|
-
if not os.environ.get(_env) and _val:
|
|
174
|
-
os.environ[_env] = _val
|
|
175
|
-
|
|
176
183
|
def _key_present() -> bool:
|
|
177
184
|
return bool(os.environ.get("LLM_API_KEY") or os.environ.get("OPENAI_API_KEY")
|
|
178
185
|
or os.environ.get("ANTHROPIC_API_KEY") or os.environ.get("DEEPSEEK_API_KEY")
|
|
@@ -189,6 +196,14 @@ def main() -> None:
|
|
|
189
196
|
return run_init() == 0
|
|
190
197
|
|
|
191
198
|
cfg = load_config()
|
|
199
|
+
# A prefs `provider` pin whose [providers.<id>] table was since removed from config.toml is STALE:
|
|
200
|
+
# its key/endpoint already fall back to the default provider (see _env_from_config), but its pinned
|
|
201
|
+
# MODEL would still win at the resolution below and get sent to the wrong endpoint (model_not_found
|
|
202
|
+
# every turn). Drop the whole stale pin so model/endpoint resolve together from the live config.
|
|
203
|
+
if _boot_prefs.get("provider") and _boot_prefs["provider"] not in (cfg.providers() or {}):
|
|
204
|
+
from .config import save_prefs as _sp
|
|
205
|
+
_sp({"provider": None, "model": None}) # None DELETES (see save_prefs)
|
|
206
|
+
_boot_prefs.pop("provider", None); _boot_prefs.pop("model", None)
|
|
192
207
|
_env_from_config(cfg, _boot_prefs.get("provider"))
|
|
193
208
|
if not _key_present() and _first_run_setup("Welcome! No API key configured yet"):
|
|
194
209
|
cfg = load_config() # the wizard just wrote ~/.sliceagent/config.toml — pick it up
|
|
@@ -468,6 +483,18 @@ def main() -> None:
|
|
|
468
483
|
ans = input(f" ⚠ allow {name} {str(detail)[:60]!r}? ({reason}) [y]es/[n]o/[a]lways: ").strip().lower()
|
|
469
484
|
return {"y": "yes", "yes": "yes", "a": "always", "always": "always"}.get(ans, "no")
|
|
470
485
|
|
|
486
|
+
def _live_pid():
|
|
487
|
+
"""The configured provider the LIVE llm is actually bound to (endpoint+key match), or None.
|
|
488
|
+
Prefs must pin what's IN EFFECT, not the last menu pick — a typed `/model <name>` keeps the
|
|
489
|
+
endpoint, so blindly re-saving the old pin resurrected a dead endpoint+model pairing at boot."""
|
|
490
|
+
lbase = getattr(llm, "_base_url", "") or ""
|
|
491
|
+
lkey = getattr(llm.client, "api_key", None)
|
|
492
|
+
for p, t in (cfg.providers() or {}).items():
|
|
493
|
+
if isinstance(t, dict) and t.get("api_key") and t["api_key"] == lkey \
|
|
494
|
+
and (t.get("base_url") or "") == lbase:
|
|
495
|
+
return p
|
|
496
|
+
return None
|
|
497
|
+
|
|
471
498
|
def _handle_slash(line): # TUI navigation palette — wired to existing session ops
|
|
472
499
|
nonlocal cfg # /config hot-reloads the config after the in-session wizard
|
|
473
500
|
parts = line.split(maxsplit=1)
|
|
@@ -595,8 +622,9 @@ def main() -> None:
|
|
|
595
622
|
_kw = {"base_url": _tbl.get("base_url") or "", "api_key": _tbl["api_key"]}
|
|
596
623
|
llm.switch(model=_model, reasoning=_reasoning, **_kw)
|
|
597
624
|
_stats["model"] = llm.model
|
|
625
|
+
# pin the provider ACTUALLY in effect (None DELETES a stale pin — see save_prefs)
|
|
598
626
|
save_prefs({"model": llm.model, "reasoning": llm.reasoning,
|
|
599
|
-
|
|
627
|
+
"provider": _pid or _live_pid()})
|
|
600
628
|
note = _reasoning_note(llm)
|
|
601
629
|
_console.print(f" ✓ model → [bold]{llm.model}[/]"
|
|
602
630
|
+ (f" @ [bold]{_pid}[/]" if _pid else "")
|
|
@@ -619,7 +647,9 @@ def main() -> None:
|
|
|
619
647
|
_console.print(" effort must be one of: fast | full | high | max"); return True
|
|
620
648
|
llm.switch(model=name, reasoning=eff)
|
|
621
649
|
_stats["model"] = llm.model
|
|
622
|
-
|
|
650
|
+
# typed /model keeps the endpoint — re-resolve the pin against the LIVE binding so a
|
|
651
|
+
# stale provider pin can't resurrect the old endpoint under this model at next boot
|
|
652
|
+
save_prefs({"model": llm.model, "reasoning": llm.reasoning, "provider": _live_pid()})
|
|
623
653
|
note = _reasoning_note(llm)
|
|
624
654
|
_console.print(f" ✓ model → [bold]{llm.model}[/]"
|
|
625
655
|
+ (f" · reasoning [bold]{llm.reasoning}[/]" if eff else "")
|
|
@@ -52,12 +52,17 @@ def load_prefs() -> dict:
|
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
def save_prefs(updates: dict) -> None:
|
|
55
|
-
"""Merge non-empty `updates` into the prefs sidecar (atomic write)
|
|
55
|
+
"""Merge non-empty `updates` into the prefs sidecar (atomic write); an explicit None DELETES the
|
|
56
|
+
key (a stale `provider` pin must be removable — merge-only let an old endpoint pin resurrect at
|
|
57
|
+
the next boot under a model it doesn't serve). Best-effort; never raises."""
|
|
56
58
|
try:
|
|
57
59
|
import json
|
|
58
60
|
path = _prefs_path()
|
|
59
61
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
60
62
|
cur = load_prefs()
|
|
63
|
+
for k, v in updates.items():
|
|
64
|
+
if v is None:
|
|
65
|
+
cur.pop(k, None)
|
|
61
66
|
cur.update({k: v for k, v in updates.items() if v})
|
|
62
67
|
tmp = path + ".tmp"
|
|
63
68
|
with open(tmp, "w", encoding="utf-8") as f:
|
|
@@ -283,7 +283,13 @@ class OpenAILLM:
|
|
|
283
283
|
ckw: dict = {"api_key": new_key}
|
|
284
284
|
if new_base:
|
|
285
285
|
ckw["base_url"] = new_base
|
|
286
|
+
old_client = getattr(self, "client", None)
|
|
286
287
|
self.client = OpenAI(http_client=http_client, timeout=timeout, max_retries=2, **ckw)
|
|
288
|
+
if old_client is not None: # close the replaced connection pool — /model hops leaked fds
|
|
289
|
+
try:
|
|
290
|
+
old_client.close()
|
|
291
|
+
except Exception: # noqa: BLE001 — cleanup must never break the switch
|
|
292
|
+
pass
|
|
287
293
|
self._base_url = new_base or ""
|
|
288
294
|
self._drop_reasoning_effort = False
|
|
289
295
|
if model:
|
|
@@ -140,10 +140,42 @@ def _read_config(path: str) -> dict:
|
|
|
140
140
|
try:
|
|
141
141
|
with open(path, "rb") as f:
|
|
142
142
|
return tomllib.load(f)
|
|
143
|
-
except (OSError, tomllib.TOMLDecodeError):
|
|
143
|
+
except (OSError, tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
|
|
144
|
+
# sibling parity with config.py:_read_toml — a non-UTF-8 or malformed config must DEGRADE, not
|
|
145
|
+
# crash `sliceagent config` / `config --use` / the init wizard with a traceback.
|
|
144
146
|
return {}
|
|
145
147
|
|
|
146
148
|
|
|
149
|
+
def _backup_if_corrupt(path: str) -> "str | None":
|
|
150
|
+
"""A rewrite reads the existing config to MERGE into; but _read_config returns {} for BOTH 'missing'
|
|
151
|
+
and 'unparseable', so a rewrite over a corrupt file would silently ERASE every provider key + section
|
|
152
|
+
it couldn't parse. Guard the writers: if `path` exists and is non-empty but does NOT parse, move it
|
|
153
|
+
aside to a non-clobbering .bak first (the keys survive, recoverable) and return the backup path; else
|
|
154
|
+
None (absent / empty / valid — nothing to protect)."""
|
|
155
|
+
import tomllib
|
|
156
|
+
try:
|
|
157
|
+
with open(path, "rb") as f:
|
|
158
|
+
raw = f.read()
|
|
159
|
+
except OSError:
|
|
160
|
+
return None
|
|
161
|
+
if not raw.strip():
|
|
162
|
+
return None
|
|
163
|
+
try:
|
|
164
|
+
tomllib.loads(raw.decode("utf-8"))
|
|
165
|
+
return None # parses fine → a normal merge is safe
|
|
166
|
+
except (tomllib.TOMLDecodeError, UnicodeDecodeError, ValueError):
|
|
167
|
+
pass
|
|
168
|
+
bak, n = path + ".bak", 0
|
|
169
|
+
while os.path.exists(bak): # never clobber an earlier backup
|
|
170
|
+
n += 1
|
|
171
|
+
bak = f"{path}.bak.{n}"
|
|
172
|
+
try:
|
|
173
|
+
os.replace(path, bak)
|
|
174
|
+
return bak
|
|
175
|
+
except OSError:
|
|
176
|
+
return None
|
|
177
|
+
|
|
178
|
+
|
|
147
179
|
def _atomic_write(path: str, body: str) -> None:
|
|
148
180
|
"""ATOMIC 0600 write (the file holds an API key — never leave it half-written): write a temp in the same
|
|
149
181
|
dir, fsync, then os.replace(); on ANY failure remove the temp so no key-bearing fragment is left behind."""
|
|
@@ -170,8 +202,10 @@ def _atomic_write(path: str, body: str) -> None:
|
|
|
170
202
|
pass
|
|
171
203
|
|
|
172
204
|
|
|
173
|
-
def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> None:
|
|
174
|
-
"""Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest.
|
|
205
|
+
def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: str) -> "str | None":
|
|
206
|
+
"""Merge a provider into the config: add/update [providers.<pid>], set it as the default, keep the rest.
|
|
207
|
+
Returns the path of a backup taken when the existing config was unparseable (else None)."""
|
|
208
|
+
bak = _backup_if_corrupt(path) # never silently erase keys in a corrupt config
|
|
175
209
|
data = _read_config(path)
|
|
176
210
|
provs = data.setdefault("providers", {})
|
|
177
211
|
if not isinstance(provs, dict): # a corrupt non-dict providers must not crash on provs[pid]=
|
|
@@ -186,6 +220,7 @@ def _save_provider(path: str, *, pid: str, model: str, api_key: str, base_url: s
|
|
|
186
220
|
agent["default_provider"] = pid
|
|
187
221
|
agent["model"] = model # keep top-level model in sync (back-compat)
|
|
188
222
|
_atomic_write(path, _emit_toml(data))
|
|
223
|
+
return bak
|
|
189
224
|
|
|
190
225
|
|
|
191
226
|
def _test_key(model: str, api_key: str, base_url: str, llm_factory) -> tuple[bool, str]:
|
|
@@ -250,8 +285,20 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
250
285
|
for k in keys:
|
|
251
286
|
_id, label, _base, model0 = PROVIDERS[k]
|
|
252
287
|
out(f" {k}. {label}" + (f" ({model0})" if model0 else ""))
|
|
253
|
-
|
|
254
|
-
|
|
288
|
+
# an unrecognized answer must NOT silently become provider 1 (a typed "openai" used to
|
|
289
|
+
# configure OpenRouter without a word) — accept a number OR a provider id, else re-ask.
|
|
290
|
+
choice = None
|
|
291
|
+
for _attempt in range(3):
|
|
292
|
+
raw = inp(" > ").strip() or "1"
|
|
293
|
+
if raw in PROVIDERS:
|
|
294
|
+
choice = raw; break
|
|
295
|
+
byname = next((k for k in keys if PROVIDERS[k][0] == raw.lower()), None)
|
|
296
|
+
if byname:
|
|
297
|
+
choice = byname; break
|
|
298
|
+
out(f" not a valid choice: {raw!r} — enter 1-{len(PROVIDERS)} or a provider name")
|
|
299
|
+
if choice is None:
|
|
300
|
+
out(" no valid choice — aborting."); return 1
|
|
301
|
+
pid, label, base_url, model = PROVIDERS[choice]
|
|
255
302
|
if pid == "custom":
|
|
256
303
|
base_url = inp(" Base URL (OpenAI-compatible, e.g. https://host/v1): ").strip()
|
|
257
304
|
# RE-CONFIGURING an ALREADY-SAVED provider (re-running `sliceagent init` to update the model,
|
|
@@ -303,7 +350,9 @@ def run_init(*, inp=input, getpw=None, llm_factory=None, home=None) -> int:
|
|
|
303
350
|
if cont not in ("y", "yes"):
|
|
304
351
|
out(" Not saved. Re-run `sliceagent init` to try again."); return 1
|
|
305
352
|
|
|
306
|
-
_save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
|
|
353
|
+
bak = _save_provider(path, pid=pid, model=model, api_key=key, base_url=base_url)
|
|
354
|
+
if bak:
|
|
355
|
+
out(f" ⚠ the existing config didn't parse — moved it to {bak} (its keys are recoverable there).")
|
|
307
356
|
out(f"\n Saved provider '{pid}' (model {model}) → {path} (0600)")
|
|
308
357
|
out(" Ready. Run: sliceagent\n")
|
|
309
358
|
return 0
|
|
@@ -356,7 +405,8 @@ def run_config(argv=None, *, home=None, env=None) -> int:
|
|
|
356
405
|
out(" providers (* = default · `config --use <id>` to switch):")
|
|
357
406
|
for pid, p in provs.items():
|
|
358
407
|
mark = "*" if pid == default else " "
|
|
359
|
-
|
|
408
|
+
model = p.get("model", "?") if isinstance(p, dict) else "?" # a scalar under [providers] must not crash
|
|
409
|
+
out(f" {mark} {pid} ({model})")
|
|
360
410
|
out(" set values:")
|
|
361
411
|
any_set = False
|
|
362
412
|
for e in REGISTRY:
|
|
@@ -96,11 +96,30 @@ def ask_mutations(name: str, args: dict) -> Optional[ToolDecision]:
|
|
|
96
96
|
# mode runs it without a confirm prompt. Deny-by-default: an unknown verb still asks. The catastrophic floor
|
|
97
97
|
# (no_dangerous_commands) runs BEFORE this, so e.g. `cat /etc/passwd` is still denied.
|
|
98
98
|
_RO_VERBS = frozenset((
|
|
99
|
-
"ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "
|
|
99
|
+
"ls", "pwd", "cat", "head", "tail", "wc", "echo", "which", "type", "printenv", "date",
|
|
100
100
|
"whoami", "hostname", "uname", "id", "groups", "tree", "stat", "file", "du", "df", "realpath",
|
|
101
|
-
"dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "
|
|
101
|
+
"dirname", "basename", "grep", "rg", "egrep", "fgrep", "sort", "nl", "cut", "column",
|
|
102
102
|
"cksum", "md5", "md5sum", "sha1sum", "sha256sum", "true",
|
|
103
103
|
))
|
|
104
|
+
# `env` and `uniq` were REMOVED from the allowlist: `env <program>` executes an arbitrary program, and
|
|
105
|
+
# `uniq [IN] OUT` OVERWRITES its optional second positional — neither is provably read-only, so they now
|
|
106
|
+
# take the normal confirm path. A few remaining allowlisted verbs stay read-only ONLY without a specific
|
|
107
|
+
# WRITE/EXEC option; deny-by-default when that option is present (verb-scoped so read-only siblings like
|
|
108
|
+
# `du -s` / `grep -o` keep auto-running). Value-taking flags match `-o`, `-o=…`, `-oFILE`, `--output=…`.
|
|
109
|
+
_UNSAFE_OPTS = {
|
|
110
|
+
"sort": ("-o", "--output"), # -o FILE / --output=FILE overwrite a file
|
|
111
|
+
"date": ("-s", "--set"), # set the system clock (a mutation)
|
|
112
|
+
"tree": ("-o",), # -o FILE writes the listing to a file
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _has_unsafe_opt(verb: str, toks: list) -> bool:
|
|
117
|
+
bad = _UNSAFE_OPTS.get(verb, ())
|
|
118
|
+
for t in toks[1:]:
|
|
119
|
+
for b in bad:
|
|
120
|
+
if t == b or t.startswith(b + "=") or (len(b) == 2 and len(t) > 2 and t.startswith(b)):
|
|
121
|
+
return True
|
|
122
|
+
return False
|
|
104
123
|
# git subcommands with NO mutating form (excludes branch/tag/config/remote/stash/reflog, which can write).
|
|
105
124
|
_RO_GIT_SUB = frozenset((
|
|
106
125
|
"status", "log", "diff", "show", "rev-parse", "ls-files", "ls-tree", "describe", "blame",
|
|
@@ -127,10 +146,15 @@ def _is_readonly_command(cmd: str) -> bool:
|
|
|
127
146
|
verb = os.path.basename(toks[0])
|
|
128
147
|
if verb == "git":
|
|
129
148
|
sub = next((t for t in toks[1:] if not t.startswith("-")), "")
|
|
130
|
-
|
|
149
|
+
if sub not in _RO_GIT_SUB:
|
|
150
|
+
return False
|
|
151
|
+
# `git grep --open-files-in-pager[=<cmd>]` / `-O[<cmd>]` runs a PAGER program (arbitrary exec).
|
|
152
|
+
if sub == "grep" and any(t.startswith("--open-files-in-pager") or t.startswith("-O") for t in toks):
|
|
153
|
+
return False
|
|
154
|
+
return True
|
|
131
155
|
if verb == "find":
|
|
132
156
|
return not any(t in _FIND_MUTATORS for t in toks)
|
|
133
|
-
return verb in _RO_VERBS
|
|
157
|
+
return verb in _RO_VERBS and not _has_unsafe_opt(verb, toks)
|
|
134
158
|
|
|
135
159
|
|
|
136
160
|
def ask_commands(name: str, args: dict) -> Optional[ToolDecision]:
|
|
@@ -849,8 +849,7 @@ def _model_candidates(llm, cfg):
|
|
|
849
849
|
out.append((m, pid, pid))
|
|
850
850
|
out.sort(key=lambda t: (t[1], t[0]))
|
|
851
851
|
if all(m != llm.model for m, _, _ in out): # an env-overridden current model still shows first
|
|
852
|
-
|
|
853
|
-
out.insert(0, (llm.model, capability(llm.model, base).family, None))
|
|
852
|
+
out.insert(0, (llm.model, "current (env)", None)) # not a configured provider — label honestly
|
|
854
853
|
else:
|
|
855
854
|
known = ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3", "deepseek-chat", "kimi-k2-0905-preview",
|
|
856
855
|
"claude-sonnet-5"]
|
|
@@ -865,12 +864,16 @@ def _model_candidates(llm, cfg):
|
|
|
865
864
|
|
|
866
865
|
def _reasoning_levels(model, base_url):
|
|
867
866
|
"""Reasoning levels valid for a model, derived from its capability (provider-aware). Effort-capable
|
|
868
|
-
models (gpt-5/o-series) expose all four;
|
|
867
|
+
models (gpt-5/o-series) expose all four; OpenRouter exposes fast/full/high (its unified reasoning
|
|
868
|
+
object maps max→high, so offering max would be a lie); others only fast/full (high/max would
|
|
869
|
+
degrade to default)."""
|
|
869
870
|
from .model_catalog import capability
|
|
870
871
|
full4 = [("fast", "minimal reasoning — fastest, cheapest"),
|
|
871
872
|
("full", "provider default reasoning"),
|
|
872
873
|
("high", "deeper reasoning (effort=high, /v1/responses)"),
|
|
873
874
|
("max", "deepest reasoning (effort=xhigh)")]
|
|
875
|
+
if "openrouter" in (base_url or "").lower(): # unified reasoning object honors effort WITH tools
|
|
876
|
+
return full4[:2] + [("high", "deeper reasoning (unified reasoning, effort=high)")]
|
|
874
877
|
if capability(model, base_url).supports_reasoning_effort:
|
|
875
878
|
return full4
|
|
876
879
|
return full4[:2] # fast | full only — the model has no effort knob
|
|
@@ -889,7 +892,10 @@ def select_model_reasoning(llm, cfg, *, pt_input=None, pt_output=None):
|
|
|
889
892
|
if pick is None:
|
|
890
893
|
return None
|
|
891
894
|
model, _grp, pid = cands[pick]
|
|
892
|
-
|
|
895
|
+
if pid: # the pick will REBIND to this provider — offer the levels ITS endpoint supports
|
|
896
|
+
base = ((cfg.providers() or {}).get(pid) or {}).get("base_url") or ""
|
|
897
|
+
else:
|
|
898
|
+
base = getattr(llm, "_base_url", "") if model == llm.model else ""
|
|
893
899
|
levels = _reasoning_levels(model, base)
|
|
894
900
|
lvl_rows = [(name, desc) for name, desc in levels]
|
|
895
901
|
lvl_cur = next((i for i, (n, _) in enumerate(levels) if n == llm.reasoning), -1)
|
|
@@ -918,7 +918,10 @@ def cost_chart_renders_flat_vs_rising():
|
|
|
918
918
|
import sys
|
|
919
919
|
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
920
920
|
sys.path.insert(0, os.path.join(root, "evals"))
|
|
921
|
-
|
|
921
|
+
try:
|
|
922
|
+
import realenv_multiturn as rem # importable WITHOUT the heavy swebench dep
|
|
923
|
+
except ModuleNotFoundError:
|
|
924
|
+
return # evals/ is a local-only dev tree, not shipped in the installed package (CI) — skip, don't fail
|
|
922
925
|
rows = [{"turn": i, "peak_in": 6000, "transcript": 6000 * i} for i in range(1, 13)]
|
|
923
926
|
chart = rem.render_cost_chart(rows)
|
|
924
927
|
assert "sliceagent" in chart and "transcript" in chart and "12×" in chart, chart
|