sliceagent 0.1.5__tar.gz → 0.1.7__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.5 → sliceagent-0.1.7}/CHANGELOG.md +24 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/PKG-INFO +1 -1
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/__init__.py +1 -1
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/cli.py +51 -18
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/llm.py +27 -5
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/tui.py +40 -22
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bughunt_fixes.py +4 -4
- sliceagent-0.1.7/tests/test_config_journey.py +119 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.dockerignore +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.env.example +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/dependabot.yml +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/workflows/ci.yml +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.github/workflows/publish.yml +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/.gitignore +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/CODE_OF_CONDUCT.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/CONTRIBUTING.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/Dockerfile +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/LICENSE +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/NOTICE +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/QUICKSTART.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/README.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/SECURITY.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/examples/plugins/hello/__init__.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/examples/plugins/hello/plugin.toml +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/install.sh +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/pyproject.toml +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/scripts/gen_config_reference.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/scripts/run_tests.sh +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/sliceagent.toml.example +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/__main__.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/access.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/agents.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/background_review.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/binsniff.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/clock.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/code_grep.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/code_index.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/config.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/context_overflow.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/envspec.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/errors.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/events.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/finding_types.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/flags.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/fuzzy.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/guardrails.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/guidance.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/hippocampus.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/hooks.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/interfaces.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/loop.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/mcp_client.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/mcp_security.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/memory.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/metrics.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/model_catalog.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/monitor.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/neocortex.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/onboarding.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/oracle.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/pagetable.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/pfc.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/plugins.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/policy.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/procman.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/prompt.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/records.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/recovery.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/regions.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/registry.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/retriever.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/safety.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/sandbox.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/scheduler.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/search_index.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/seed.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/sensory_cortex.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/session.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/skill_provenance.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/skill_usage.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/skills.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/subagent.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/subdir_hints.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/swap.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/taskstate.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/telemetry.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/terminal.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/text_utils.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/tool_summary.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/tools.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/src/sliceagent/web.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/README.md +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_active_focus.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_agents.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_ask_echo.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_ask_user.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_background_review.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_binary_view.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bound_is_relevance.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_breadth_e2e.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_2b.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_breadth_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_current_request.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_linenum.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_loop_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_memory_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_p0a.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_p0b.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_p0c.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_selfreview.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_selfreview_siblings.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_skills_h1.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_slice_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_subagent_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_bugfix_tools_wave.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_cache_manifest.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_checkpoint.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_cli_smoke.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_closure.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_code_grep.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_code_index.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_consolidate.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_context_overflow.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_conversation.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_coresidency.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_echo_before_blocking.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_episode.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_errors_backoff.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_esc_sentinel.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_exec_env.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_finding_types.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_flags.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_fuzzy.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_ghost_index.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_guardrails.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_guidance.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_history.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_invariant_fixes.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_live_composer.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_llm_streaming.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_llm_usage_cache.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_llm_watchdog.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_log_redaction.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_loop_overflow.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_mcp_output_cap.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_mcp_runtime.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_memory.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_memory_persist.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_menu_select.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_metrics.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_mining.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_mission_tier.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_model_catalog.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_monitor.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_onboarding.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_pageout_compaction.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_permission_patterns.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_pinned_composer.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_plan_tier.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_procman.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_product_features.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_progress_status.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_prompt_ab.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_provenance.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_provider_lineup.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_proxy_interrupt.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_reach.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_readonly_subagent.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_recall_search.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_records.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_refault.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_registry_validation.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_reliability.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_repo_map_bounded.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_requirements.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_review_fixes.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_route_lexical.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_safety.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_seal_markdown.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_search_index.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_skill_meta.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_skill_metadata.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_subdir_hints.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_task_agnostic.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_task_state_roundtrip.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_telemetry_convergence.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_terminal.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tool_dedup.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tool_result_ok.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tools_robust.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_topic_switch.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_trajectory.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tui.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tui_menus.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tui_render.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_tui_widgets.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_turn_budget.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_verification_agent.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_web.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_workspace.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_world_region.py +0 -0
- {sliceagent-0.1.5 → sliceagent-0.1.7}/tests/test_world_state.py +0 -0
|
@@ -5,6 +5,30 @@ this project aims for [Semantic Versioning](https://semver.org/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [0.1.7] — 2026-07-02
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `/model`: the current env-configured model row is now labeled `current (env)` instead of the
|
|
12
|
+
provider family name — it could masquerade as a configured provider (e.g. an env `gpt-5.5` on a
|
|
13
|
+
DeepSeek endpoint showed as "deepseek").
|
|
14
|
+
|
|
15
|
+
## [0.1.6] — 2026-07-02
|
|
16
|
+
|
|
17
|
+
The clear config journey: one wizard, two doors, an honest /model.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **`/config`** — manage LLM providers *inside* sliceagent: the same wizard as first-run onboarding
|
|
21
|
+
(provider → model → key → live test), then the config hot-reloads and the new provider shows up
|
|
22
|
+
in `/model` immediately.
|
|
23
|
+
- **`/model` switches providers for real** — the menu lists ONLY configured providers' models
|
|
24
|
+
(saved model + suggestions, labeled by provider), and picking one switches **model + endpoint +
|
|
25
|
+
key together** (the old menu changed the model string but never the endpoint). The last-picked
|
|
26
|
+
provider is remembered across sessions.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
- Typed `/model <name>` stays same-endpoint (documented as such); the mismatch warning now points
|
|
30
|
+
at `/config` + the `/model` menu instead of `config --use`.
|
|
31
|
+
|
|
8
32
|
## [0.1.5] — 2026-07-02
|
|
9
33
|
|
|
10
34
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sliceagent
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.7
|
|
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
|
|
@@ -134,8 +134,8 @@ def _reasoning_note(llm) -> str:
|
|
|
134
134
|
label = {"openai": "an OpenAI", "deepseek": "a DeepSeek",
|
|
135
135
|
"moonshot": "a Moonshot", "anthropic": "an Anthropic"}.get(home, f"a {home}")
|
|
136
136
|
return (f"note: {llm.model} looks like {label} model, but you're connected to {base or '(default)'}"
|
|
137
|
-
f" — it will likely fail on your next message.
|
|
138
|
-
f"
|
|
137
|
+
f" — it will likely fail on your next message. Add {label} provider with /config, then pick "
|
|
138
|
+
f"its model from the /model menu (the menu switches the endpoint too; typed /model doesn't).")
|
|
139
139
|
eff = (getattr(llm, "reasoning", "full") or "full").lower()
|
|
140
140
|
if eff == "full":
|
|
141
141
|
return ""
|
|
@@ -162,10 +162,14 @@ def main() -> None:
|
|
|
162
162
|
_load_env()
|
|
163
163
|
# config-persisted key/endpoint (written by `sliceagent init`) populate the env BEFORE the gate, so a
|
|
164
164
|
# configured user never has to export anything; ENV still wins for one-off overrides.
|
|
165
|
-
from .config import load_config
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
from .config import load_config, load_prefs
|
|
166
|
+
_boot_prefs = load_prefs() # the last /model choice may pin a PROVIDER (endpoint+key), not just a model
|
|
167
|
+
|
|
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)):
|
|
169
173
|
if not os.environ.get(_env) and _val:
|
|
170
174
|
os.environ[_env] = _val
|
|
171
175
|
|
|
@@ -185,7 +189,7 @@ def main() -> None:
|
|
|
185
189
|
return run_init() == 0
|
|
186
190
|
|
|
187
191
|
cfg = load_config()
|
|
188
|
-
_env_from_config(cfg)
|
|
192
|
+
_env_from_config(cfg, _boot_prefs.get("provider"))
|
|
189
193
|
if not _key_present() and _first_run_setup("Welcome! No API key configured yet"):
|
|
190
194
|
cfg = load_config() # the wizard just wrote ~/.sliceagent/config.toml — pick it up
|
|
191
195
|
_env_from_config(cfg)
|
|
@@ -465,12 +469,33 @@ def main() -> None:
|
|
|
465
469
|
return {"y": "yes", "yes": "yes", "a": "always", "always": "always"}.get(ans, "no")
|
|
466
470
|
|
|
467
471
|
def _handle_slash(line): # TUI navigation palette — wired to existing session ops
|
|
472
|
+
nonlocal cfg # /config hot-reloads the config after the in-session wizard
|
|
468
473
|
parts = line.split(maxsplit=1)
|
|
469
474
|
cmd, arg = parts[0], (parts[1].strip() if len(parts) > 1 else "")
|
|
470
475
|
if cmd == "/help":
|
|
471
|
-
_console.print("commands: /model · /mode · /cwd · /learn · /plan · /cost · /threads ·
|
|
472
|
-
"/help · /exit\n (type / for the menu · /
|
|
473
|
-
"
|
|
476
|
+
_console.print("commands: /config · /model · /mode · /cwd · /learn · /plan · /cost · /threads · "
|
|
477
|
+
"/plugins · /mcp · /help · /exit\n (type / for the menu · /config adds LLM "
|
|
478
|
+
"providers · /cwd <path> switches workspace · Esc = undo last turn · "
|
|
479
|
+
"say \"review my changes\" for code_review · @path pins a file)")
|
|
480
|
+
elif cmd == "/config":
|
|
481
|
+
# THE clear user journey: providers are managed INSIDE sliceagent — same wizard as first-run
|
|
482
|
+
# onboarding (provider → model → key → live test), then the new provider shows up in /model.
|
|
483
|
+
if sys.stdin.isatty() and not use_live:
|
|
484
|
+
from .onboarding import run_init
|
|
485
|
+
try:
|
|
486
|
+
rc = run_init()
|
|
487
|
+
except (EOFError, KeyboardInterrupt):
|
|
488
|
+
rc = 1
|
|
489
|
+
if rc == 0:
|
|
490
|
+
from .config import load_config as _reload
|
|
491
|
+
cfg = _reload() # hot-reload: /model now sees the new provider
|
|
492
|
+
provs = cfg.providers()
|
|
493
|
+
_console.print(" providers configured: "
|
|
494
|
+
+ (", ".join(f"[bold]{k}[/] ({v.get('model', '?')})"
|
|
495
|
+
for k, v in provs.items()) or "(none)"))
|
|
496
|
+
_console.print(" switch with [bold]/model[/] — it lists your configured providers' models.")
|
|
497
|
+
else:
|
|
498
|
+
_console.print(" /config needs an interactive terminal — run `sliceagent init` instead.")
|
|
474
499
|
elif cmd == "/plan":
|
|
475
500
|
s = session.active() if session.active_id else None
|
|
476
501
|
plan = getattr(s, "plan", None) if s else None
|
|
@@ -562,23 +587,31 @@ def main() -> None:
|
|
|
562
587
|
from .tui import select_model_reasoning
|
|
563
588
|
choice = select_model_reasoning(llm, cfg)
|
|
564
589
|
if choice:
|
|
565
|
-
|
|
590
|
+
_model, _reasoning, _pid = choice
|
|
591
|
+
_kw = {}
|
|
592
|
+
if _pid: # a CONFIGURED provider → switch endpoint+key too
|
|
593
|
+
_tbl = (cfg.providers() or {}).get(_pid) or {}
|
|
594
|
+
if _tbl.get("api_key"):
|
|
595
|
+
_kw = {"base_url": _tbl.get("base_url") or "", "api_key": _tbl["api_key"]}
|
|
596
|
+
llm.switch(model=_model, reasoning=_reasoning, **_kw)
|
|
566
597
|
_stats["model"] = llm.model
|
|
567
|
-
save_prefs({"model": llm.model, "reasoning": llm.reasoning
|
|
598
|
+
save_prefs({"model": llm.model, "reasoning": llm.reasoning,
|
|
599
|
+
**({"provider": _pid} if _pid else {})})
|
|
568
600
|
note = _reasoning_note(llm)
|
|
569
|
-
_console.print(f" ✓ model → [bold]{llm.model}[/]
|
|
601
|
+
_console.print(f" ✓ model → [bold]{llm.model}[/]"
|
|
602
|
+
+ (f" @ [bold]{_pid}[/]" if _pid else "")
|
|
603
|
+
+ f" · reasoning [bold]{llm.reasoning}[/] (saved)"
|
|
570
604
|
+ (f"\n {note}" if note else ""))
|
|
571
605
|
elif not arg:
|
|
572
606
|
_console.print(f" model: [bold]{llm.model}[/] · reasoning: [bold]{llm.reasoning}[/]"
|
|
573
607
|
f" · net: {getattr(llm, 'proxy_used', 'direct')}")
|
|
574
|
-
|
|
575
|
-
"kimi-k2-0905-preview", "claude-sonnet-4-6")
|
|
576
|
-
_console.print(" switch: /model <name> [fast|full|high|max]")
|
|
577
|
-
_console.print(" known: " + ", ".join(known))
|
|
608
|
+
_console.print(" switch: /model <name> [fast|full|high|max] (same endpoint)")
|
|
578
609
|
provs = cfg.providers()
|
|
579
610
|
if provs:
|
|
580
|
-
_console.print(" providers (
|
|
611
|
+
_console.print(" configured providers (pick via the /model menu to switch endpoint too): "
|
|
581
612
|
+ ", ".join(f"{k}={v.get('model', '?')}" for k, v in provs.items()))
|
|
613
|
+
else:
|
|
614
|
+
_console.print(" no providers configured yet — add one with [bold]/config[/]")
|
|
582
615
|
else:
|
|
583
616
|
name, *rest = arg.split()
|
|
584
617
|
eff = rest[0].lower() if rest else None
|
|
@@ -220,6 +220,7 @@ class OpenAILLM:
|
|
|
220
220
|
# the SDK's own (cleaner) timeout fires first when it can, and SIGALRM only catches the stalls
|
|
221
221
|
# the read-timeout misses (silent mid-response connections).
|
|
222
222
|
self._hard_timeout = max(int(timeout) + 15, 30)
|
|
223
|
+
self._timeout = timeout # kept for live endpoint switches (switch())
|
|
223
224
|
# No built-in default model — the user picks (parallels the CLI's model gate; a silent
|
|
224
225
|
# fallback here would contradict it for library/embedding callers).
|
|
225
226
|
self.model = model or os.environ.get("AGENT_MODEL") or ""
|
|
@@ -259,11 +260,32 @@ class OpenAILLM:
|
|
|
259
260
|
# thereafter reasoning_effort is dropped when tools are present (graceful degrade, no re-400).
|
|
260
261
|
self._drop_reasoning_effort = False
|
|
261
262
|
|
|
262
|
-
def switch(self, *, model: str | None = None, reasoning: str | None = None
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
the
|
|
266
|
-
|
|
263
|
+
def switch(self, *, model: str | None = None, reasoning: str | None = None,
|
|
264
|
+
base_url: str | None = None, api_key: str | None = None) -> None:
|
|
265
|
+
"""Live-switch model / reasoning — and, when `base_url`/`api_key` are given, the PROVIDER too:
|
|
266
|
+
the client is rebuilt against the new endpoint (with the proxy re-chosen for it), so /model can
|
|
267
|
+
hop between configured providers in one action. Mutates in place — the loop passes this same llm
|
|
268
|
+
object every turn, so the change applies from the next turn on. base_url semantics: None = keep
|
|
269
|
+
the current endpoint; "" = the SDK's default endpoint (OpenAI). Resets the
|
|
270
|
+
reasoning_effort+tools degrade memory (a different model/provider may support the pairing)."""
|
|
271
|
+
if base_url is not None or api_key:
|
|
272
|
+
import httpx
|
|
273
|
+
from openai import OpenAI
|
|
274
|
+
new_base = self._base_url if base_url is None else base_url
|
|
275
|
+
new_key = api_key or getattr(self.client, "api_key", None)
|
|
276
|
+
proxy = _choose_proxy(new_base, os.environ.get("AGENT_PROXY")
|
|
277
|
+
or os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY"))
|
|
278
|
+
use_proxy = bool(proxy) and proxy != "none"
|
|
279
|
+
self.proxy_used = proxy if use_proxy else "direct"
|
|
280
|
+
timeout = getattr(self, "_timeout", 60.0)
|
|
281
|
+
http_client = (httpx.Client(proxy=proxy, timeout=timeout) if use_proxy
|
|
282
|
+
else httpx.Client(timeout=timeout))
|
|
283
|
+
ckw: dict = {"api_key": new_key}
|
|
284
|
+
if new_base:
|
|
285
|
+
ckw["base_url"] = new_base
|
|
286
|
+
self.client = OpenAI(http_client=http_client, timeout=timeout, max_retries=2, **ckw)
|
|
287
|
+
self._base_url = new_base or ""
|
|
288
|
+
self._drop_reasoning_effort = False
|
|
267
289
|
if model:
|
|
268
290
|
self.model = model
|
|
269
291
|
self._drop_reasoning_effort = False
|
|
@@ -824,25 +824,41 @@ def run_selector(title, rows, *, current=-1, hint="↑↓ move · Enter select
|
|
|
824
824
|
|
|
825
825
|
|
|
826
826
|
def _model_candidates(llm, cfg):
|
|
827
|
-
"""Models to offer in the /model menu
|
|
828
|
-
|
|
827
|
+
"""Models to offer in the /model menu — CONFIGURED providers only (the user's clear-journey rule:
|
|
828
|
+
the switcher shows what you can actually use). Each configured provider (has an api_key)
|
|
829
|
+
contributes its saved model + its wizard suggestions, labeled with the provider id; picking one
|
|
830
|
+
lets the CLI switch model+endpoint+key together. Returns [(model, group_label, provider_id)].
|
|
831
|
+
provider_id None = the current env-configured model (no provider table to rebind to). Fallback
|
|
832
|
+
when NO providers are configured (pure env setup): current model + a small known set."""
|
|
829
833
|
from .model_catalog import capability
|
|
830
|
-
known = ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3", "deepseek-chat", "kimi-k2-0905-preview", "claude-sonnet-4-6"]
|
|
831
|
-
prov = []
|
|
832
834
|
try:
|
|
833
|
-
for
|
|
834
|
-
|
|
835
|
-
if m:
|
|
836
|
-
prov.append(m)
|
|
835
|
+
provs = {pid: t for pid, t in (cfg.providers() or {}).items()
|
|
836
|
+
if isinstance(t, dict) and t.get("api_key")}
|
|
837
837
|
except Exception: # noqa: BLE001 — a malformed providers table must not break the menu
|
|
838
|
-
|
|
838
|
+
provs = {}
|
|
839
839
|
out, seen = [], set()
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
840
|
+
if provs:
|
|
841
|
+
try:
|
|
842
|
+
from .onboarding import MODEL_SUGGESTIONS
|
|
843
|
+
except Exception: # noqa: BLE001
|
|
844
|
+
MODEL_SUGGESTIONS = {}
|
|
845
|
+
for pid, tbl in provs.items():
|
|
846
|
+
for m in [tbl.get("model")] + list(MODEL_SUGGESTIONS.get(pid, [])):
|
|
847
|
+
if m and (pid, m) not in seen:
|
|
848
|
+
seen.add((pid, m))
|
|
849
|
+
out.append((m, pid, pid))
|
|
850
|
+
out.sort(key=lambda t: (t[1], t[0]))
|
|
851
|
+
if all(m != llm.model for m, _, _ in out): # an env-overridden current model still shows first
|
|
852
|
+
out.insert(0, (llm.model, "current (env)", None)) # not a configured provider — label honestly
|
|
853
|
+
else:
|
|
854
|
+
known = ["gpt-5.5", "gpt-5", "gpt-5-mini", "o3", "deepseek-chat", "kimi-k2-0905-preview",
|
|
855
|
+
"claude-sonnet-5"]
|
|
856
|
+
for m in [llm.model] + known:
|
|
857
|
+
if m and m not in seen:
|
|
858
|
+
seen.add(m)
|
|
859
|
+
base = getattr(llm, "_base_url", "") if m == llm.model else ""
|
|
860
|
+
out.append((m, capability(m, base).family, None))
|
|
861
|
+
out.sort(key=lambda t: (t[1], t[0]))
|
|
846
862
|
return out
|
|
847
863
|
|
|
848
864
|
|
|
@@ -860,17 +876,18 @@ def _reasoning_levels(model, base_url):
|
|
|
860
876
|
|
|
861
877
|
|
|
862
878
|
def select_model_reasoning(llm, cfg, *, pt_input=None, pt_output=None):
|
|
863
|
-
"""Two-tier picker: choose a model (
|
|
864
|
-
model supports). Returns (model, reasoning)
|
|
879
|
+
"""Two-tier picker: choose a model (from CONFIGURED providers) then its reasoning level (only the
|
|
880
|
+
levels that model supports). Returns (model, reasoning, provider_id) — provider_id is the configured
|
|
881
|
+
provider to switch endpoint+key to (None = keep the current endpoint) — or None if cancelled."""
|
|
865
882
|
cands = _model_candidates(llm, cfg)
|
|
866
|
-
rows = [(m, f"provider: {
|
|
867
|
-
cur_idx = next((i for i, (m, _) in enumerate(cands) if m == llm.model), -1)
|
|
883
|
+
rows = [(m, f"provider: {grp}") for m, grp, _pid in cands]
|
|
884
|
+
cur_idx = next((i for i, (m, _, _) in enumerate(cands) if m == llm.model), -1)
|
|
868
885
|
pick = run_selector("Select model", rows, current=cur_idx,
|
|
869
886
|
hint="↑↓ move · Enter choose model → reasoning · Esc cancel",
|
|
870
887
|
pt_input=pt_input, pt_output=pt_output)
|
|
871
888
|
if pick is None:
|
|
872
889
|
return None
|
|
873
|
-
model = cands[pick]
|
|
890
|
+
model, _grp, pid = cands[pick]
|
|
874
891
|
base = getattr(llm, "_base_url", "") if model == llm.model else ""
|
|
875
892
|
levels = _reasoning_levels(model, base)
|
|
876
893
|
lvl_rows = [(name, desc) for name, desc in levels]
|
|
@@ -879,12 +896,13 @@ def select_model_reasoning(llm, cfg, *, pt_input=None, pt_output=None):
|
|
|
879
896
|
hint="↑↓ move · Enter select · Esc keep current",
|
|
880
897
|
pt_input=pt_input, pt_output=pt_output)
|
|
881
898
|
reasoning = levels[lpick][0] if lpick is not None else llm.reasoning # Esc on step 2 = keep current
|
|
882
|
-
return (model, reasoning)
|
|
899
|
+
return (model, reasoning, pid)
|
|
883
900
|
|
|
884
901
|
|
|
885
902
|
# ── input layer (prompt_toolkit) ─────────────────────────────────────────────────────────────
|
|
886
903
|
_SLASH = {
|
|
887
|
-
"/
|
|
904
|
+
"/config": "add / update LLM providers (the setup wizard, in-session) — then /model to switch",
|
|
905
|
+
"/model": "switch model + reasoning — menu lists YOUR configured providers (switches endpoint too)",
|
|
888
906
|
"/mode": "permission mode — opens a menu (baby-sitter · teenager · let-it-go)",
|
|
889
907
|
"/cwd": "switch workspace root (/cwd <path>) — re-roots repo map, file tools & commands",
|
|
890
908
|
"/learn": "turn what you just did into a reusable SKILL (/learn [name])",
|
|
@@ -816,11 +816,11 @@ def model_menu_is_provider_aware():
|
|
|
816
816
|
def providers(self):
|
|
817
817
|
return {}
|
|
818
818
|
|
|
819
|
-
cands = t._model_candidates(_LLM(), _CFG())
|
|
820
|
-
models = [m for m,
|
|
819
|
+
cands = t._model_candidates(_LLM(), _CFG()) # 3-tuples since the configured-only journey
|
|
820
|
+
models = [m for m, _grp, _pid in cands]
|
|
821
821
|
assert "deepseek-chat" in models and "gpt-5.5" in models # current + known both present
|
|
822
822
|
assert len(models) == len(set(models)) # deduped
|
|
823
|
-
fams = [fam for _, fam in cands]
|
|
823
|
+
fams = [fam for _, fam, _pid in cands]
|
|
824
824
|
assert fams == sorted(fams) # grouped (sorted) by provider family
|
|
825
825
|
|
|
826
826
|
|
|
@@ -1337,7 +1337,7 @@ def model_switch_warns_when_the_endpoint_cant_serve_it():
|
|
|
1337
1337
|
self.model, self._base_url, self.reasoning = model, base_url, reasoning
|
|
1338
1338
|
|
|
1339
1339
|
note = _reasoning_note(LLM("gpt-5.5", "https://api.deepseek.com/v1", "full"))
|
|
1340
|
-
assert "OpenAI" in note and "deepseek" in note.lower() and "
|
|
1340
|
+
assert "OpenAI" in note and "deepseek" in note.lower() and "/config" in note, note
|
|
1341
1341
|
# same provider, no mismatch → must stay silent (no false positive)
|
|
1342
1342
|
assert _reasoning_note(LLM("deepseek-chat", "https://api.deepseek.com/v1", "full")) == ""
|
|
1343
1343
|
# real OpenAI → completely unaffected
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""The clear config journey (user-designed): /config manages providers in-session, and /model shows
|
|
2
|
+
ONLY configured providers' models — picking one switches model + endpoint + key together.
|
|
3
|
+
Offline: stub objects, no network, no pytest. Run: PYTHONPATH=src python tests/test_config_journey.py
|
|
4
|
+
"""
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
|
|
9
|
+
|
|
10
|
+
from sliceagent.config import Config # noqa: E402
|
|
11
|
+
from sliceagent.llm import OpenAILLM # noqa: E402
|
|
12
|
+
from sliceagent.tui import _SLASH, _model_candidates, select_model_reasoning # noqa: E402
|
|
13
|
+
|
|
14
|
+
CHECKS = []
|
|
15
|
+
def check(fn):
|
|
16
|
+
CHECKS.append(fn)
|
|
17
|
+
return fn
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _Llm:
|
|
21
|
+
model = "deepseek-chat"
|
|
22
|
+
_base_url = "https://api.deepseek.com/v1"
|
|
23
|
+
reasoning = "full"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_CFG = Config({"providers": {
|
|
27
|
+
"deepseek": {"api_key": "k1", "base_url": "https://api.deepseek.com/v1", "model": "deepseek-chat"},
|
|
28
|
+
"openrouter": {"api_key": "k2", "base_url": "https://openrouter.ai/api/v1", "model": "openai/gpt-5.5"},
|
|
29
|
+
"keyless": {"model": "phantom-model"}, # no api_key → NOT usable → must not appear
|
|
30
|
+
}})
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@check
|
|
34
|
+
def model_menu_lists_only_configured_providers():
|
|
35
|
+
cands = _model_candidates(_Llm(), _CFG)
|
|
36
|
+
pids = {pid for _, _, pid in cands if pid}
|
|
37
|
+
models = [m for m, _, _ in cands]
|
|
38
|
+
assert pids == {"deepseek", "openrouter"}, f"only keyed providers may appear, got {pids}"
|
|
39
|
+
assert "phantom-model" not in models, "a provider without a key must not contribute models"
|
|
40
|
+
assert "deepseek-chat" in models and "openai/gpt-5.5" in models
|
|
41
|
+
assert "anthropic/claude-sonnet-5" in models, "configured providers contribute their suggestions too"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@check
|
|
45
|
+
def every_candidate_carries_its_provider_for_endpoint_switch():
|
|
46
|
+
for m, _grp, pid in _model_candidates(_Llm(), _CFG):
|
|
47
|
+
assert pid in ("deepseek", "openrouter"), f"{m} must map to a configured provider, got {pid!r}"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@check
|
|
51
|
+
def env_current_model_is_labeled_honestly_not_as_a_provider():
|
|
52
|
+
class _EnvLlm: # env model NOT in any configured provider's list
|
|
53
|
+
model = "gpt-5.5"
|
|
54
|
+
_base_url = "https://api.deepseek.com/v1" # deepseek endpoint must NOT make it say "deepseek"
|
|
55
|
+
reasoning = "full"
|
|
56
|
+
cands = _model_candidates(_EnvLlm(), _CFG)
|
|
57
|
+
m, grp, pid = cands[0]
|
|
58
|
+
assert m == "gpt-5.5" and pid is None
|
|
59
|
+
assert grp == "current (env)", f"env model must not masquerade as a configured provider, got {grp!r}"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@check
|
|
63
|
+
def no_providers_configured_falls_back_to_known_set():
|
|
64
|
+
cands = _model_candidates(_Llm(), Config({}))
|
|
65
|
+
assert cands, "env-only setups still need a menu"
|
|
66
|
+
assert all(pid is None for _, _, pid in cands), "fallback entries have no provider to rebind to"
|
|
67
|
+
assert any(m == "deepseek-chat" for m, _, _ in cands)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@check
|
|
71
|
+
def switch_rebinds_endpoint_and_key():
|
|
72
|
+
saved = {k: os.environ.pop(k, None) for k in ("AGENT_PROXY", "HTTPS_PROXY", "HTTP_PROXY")}
|
|
73
|
+
try:
|
|
74
|
+
llm = OpenAILLM(model="deepseek-chat", api_key="k1", base_url="https://api.deepseek.com/v1")
|
|
75
|
+
assert llm._base_url == "https://api.deepseek.com/v1"
|
|
76
|
+
llm.switch(model="openai/gpt-5.5", base_url="https://openrouter.ai/api/v1", api_key="k2")
|
|
77
|
+
assert llm.model == "openai/gpt-5.5"
|
|
78
|
+
assert llm._base_url == "https://openrouter.ai/api/v1"
|
|
79
|
+
assert str(llm.client.base_url).startswith("https://openrouter.ai/api/v1"), \
|
|
80
|
+
"the OpenAI client itself must be rebuilt against the new endpoint"
|
|
81
|
+
assert llm.client.api_key == "k2", "the key must follow the provider"
|
|
82
|
+
# base_url="" → SDK default endpoint (OpenAI)
|
|
83
|
+
llm.switch(model="gpt-5.5", base_url="", api_key="k3")
|
|
84
|
+
assert llm._base_url == "" and "openrouter" not in str(llm.client.base_url)
|
|
85
|
+
# model-only switch keeps the endpoint (the classic /model <name> path)
|
|
86
|
+
llm.switch(model="gpt-5")
|
|
87
|
+
assert llm.model == "gpt-5" and llm.client.api_key == "k3"
|
|
88
|
+
finally:
|
|
89
|
+
for k, v in saved.items():
|
|
90
|
+
if v is not None:
|
|
91
|
+
os.environ[k] = v
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@check
|
|
95
|
+
def selector_returns_the_provider_triple():
|
|
96
|
+
from prompt_toolkit.input.defaults import create_pipe_input
|
|
97
|
+
from prompt_toolkit.output import DummyOutput
|
|
98
|
+
with create_pipe_input() as pinp:
|
|
99
|
+
pinp.send_text("\r\r") # Enter (model) + Enter (reasoning)
|
|
100
|
+
got = select_model_reasoning(_Llm(), _CFG, pt_input=pinp, pt_output=DummyOutput())
|
|
101
|
+
assert got is not None and len(got) == 3, f"expected (model, reasoning, pid), got {got!r}"
|
|
102
|
+
model, _reasoning, pid = got
|
|
103
|
+
assert pid in ("deepseek", "openrouter") and isinstance(model, str)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@check
|
|
107
|
+
def config_command_is_in_the_slash_palette():
|
|
108
|
+
assert "/config" in _SLASH, "/config must be discoverable in the palette"
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
ok = 0
|
|
113
|
+
for fn in CHECKS:
|
|
114
|
+
try:
|
|
115
|
+
fn(); ok += 1; print(f"PASS {fn.__name__}")
|
|
116
|
+
except Exception as e: # noqa: BLE001
|
|
117
|
+
print(f"FAIL {fn.__name__}: {type(e).__name__}: {e}")
|
|
118
|
+
print(f"\n{ok}/{len(CHECKS)} passed")
|
|
119
|
+
sys.exit(0 if ok == len(CHECKS) else 1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|