From 5ae57d192ac659aacdd3c27b6d0c440248590df5 Mon Sep 17 00:00:00 2001 From: yusyus Date: Tue, 24 Feb 2026 06:52:55 +0300 Subject: [PATCH] fix: update Gemini model to 2.5-flash and add API auto-detection in enhance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix 1 — gemini.py: replace deprecated gemini-2.0-flash-exp (404 errors) with gemini-2.5-flash (stable, GA, Google's recommended replacement). Closes #290. Fix 2 — enhance dispatcher: implement the documented auto-detection that was missing from the code. skill-seekers enhance now correctly routes: - ANTHROPIC_API_KEY set → Claude API mode (enhance_skill.py) - GOOGLE_API_KEY set → Gemini API mode - OPENAI_API_KEY set → OpenAI API mode - No API keys → LOCAL mode (Claude Code Max, free) Use --mode LOCAL to force local mode even when an API key is present. 9 new tests cover _detect_api_target() priority logic and main() routing (API delegation, --mode LOCAL override, no-key fallback). Co-Authored-By: Claude Sonnet 4.6 --- src/skill_seekers/cli/adaptors/gemini.py | 10 +- src/skill_seekers/cli/enhance_skill_local.py | 111 ++++++++++++-- tests/test_enhance_skill_local.py | 145 +++++++++++++++++++ 3 files changed, 246 insertions(+), 20 deletions(-) diff --git a/src/skill_seekers/cli/adaptors/gemini.py b/src/skill_seekers/cli/adaptors/gemini.py index af74a8a..3e58f1b 100644 --- a/src/skill_seekers/cli/adaptors/gemini.py +++ b/src/skill_seekers/cli/adaptors/gemini.py @@ -3,7 +3,7 @@ Google Gemini Adaptor Implements platform-specific handling for Google Gemini skills. -Uses Gemini Files API for grounding and Gemini 2.0 Flash for enhancement. +Uses Gemini Files API for grounding and Gemini 2.5 Flash for enhancement. """ import json @@ -23,7 +23,7 @@ class GeminiAdaptor(SkillAdaptor): - Plain markdown format (no YAML frontmatter) - tar.gz packaging for Gemini Files API - Upload to Google AI Studio / Files API - - AI enhancement using Gemini 2.0 Flash + - AI enhancement using Gemini 2.5 Flash """ PLATFORM = "gemini" @@ -279,7 +279,7 @@ See the references directory for complete documentation with examples and best p def supports_enhancement(self) -> bool: """ - Gemini supports AI enhancement via Gemini 2.0 Flash. + Gemini supports AI enhancement via Gemini 2.5 Flash. Returns: True @@ -288,7 +288,7 @@ See the references directory for complete documentation with examples and best p def enhance(self, skill_dir: Path, api_key: str) -> bool: """ - Enhance SKILL.md using Gemini 2.0 Flash API. + Enhance SKILL.md using Gemini 2.5 Flash API. Args: skill_dir: Path to skill directory @@ -338,7 +338,7 @@ See the references directory for complete documentation with examples and best p try: genai.configure(api_key=api_key) - model = genai.GenerativeModel("gemini-2.0-flash-exp") + model = genai.GenerativeModel("gemini-2.5-flash") response = model.generate_content(prompt) diff --git a/src/skill_seekers/cli/enhance_skill_local.py b/src/skill_seekers/cli/enhance_skill_local.py index a69f837..1ddc59f 100644 --- a/src/skill_seekers/cli/enhance_skill_local.py +++ b/src/skill_seekers/cli/enhance_skill_local.py @@ -1205,46 +1205,119 @@ except Exception as e: return False +def _detect_api_target() -> tuple[str, str] | None: + """ + Auto-detect which API platform to use for enhancement based on env vars. + + Priority: ANTHROPIC_API_KEY > GOOGLE_API_KEY > OPENAI_API_KEY + + Returns: + (target, api_key) tuple if an API key is found, else None. + """ + anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.environ.get( + "ANTHROPIC_AUTH_TOKEN" + ) + if anthropic_key: + return ("claude", anthropic_key) + + google_key = os.environ.get("GOOGLE_API_KEY") + if google_key: + return ("gemini", google_key) + + openai_key = os.environ.get("OPENAI_API_KEY") + if openai_key: + return ("openai", openai_key) + + return None + + +def _run_api_enhance(target: str, api_key: str) -> None: + """Delegate to enhance_skill.main() for API-mode enhancement.""" + import sys + + from skill_seekers.cli.enhance_skill import main as api_main + + # Find the skill_directory positional arg (first non-flag arg after argv[0]) + skill_dir = None + dry_run = False + i = 1 + while i < len(sys.argv): + arg = sys.argv[i] + if arg == "--dry-run": + dry_run = True + elif arg in ("--mode",): + i += 1 # skip value + elif not arg.startswith("-") and skill_dir is None: + skill_dir = arg + i += 1 + + if not skill_dir: + print("❌ Error: skill_directory is required") + sys.exit(1) + + new_argv = [sys.argv[0], skill_dir, "--target", target, "--api-key", api_key] + if dry_run: + new_argv.append("--dry-run") + sys.argv = new_argv + api_main() + + def main(): import argparse parser = argparse.ArgumentParser( - description="Enhance a skill with a local coding agent (no API key)", + description="Enhance a skill using AI (auto-detects API or local agent mode)", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" +Auto-detection (no flags needed): + If ANTHROPIC_API_KEY is set → Claude API mode + If GOOGLE_API_KEY is set → Gemini API mode + If OPENAI_API_KEY is set → OpenAI API mode + Otherwise → LOCAL mode (Claude Code Max, free) + Examples: - # Headless mode (default - runs in foreground, waits for completion, auto-force) + # Auto-detect mode based on env vars (recommended) skill-seekers enhance output/react/ - # Background mode (runs in background, returns immediately) - skill-seekers enhance output/react/ --background + # Force LOCAL mode even if API keys are set + skill-seekers enhance output/react/ --mode LOCAL - # Daemon mode (persistent background process, fully detached) - skill-seekers enhance output/react/ --daemon + # LOCAL: background mode (runs in background, returns immediately) + skill-seekers enhance output/react/ --mode LOCAL --background - # Disable force mode (ask for confirmations) - skill-seekers enhance output/react/ --no-force + # LOCAL: daemon mode (persistent background process, fully detached) + skill-seekers enhance output/react/ --mode LOCAL --daemon - # Interactive mode (opens terminal window) - skill-seekers enhance output/react/ --interactive-enhancement + # LOCAL: interactive mode (opens terminal window) + skill-seekers enhance output/react/ --mode LOCAL --interactive-enhancement - # Custom timeout - skill-seekers enhance output/react/ --timeout 1200 + # LOCAL: custom timeout + skill-seekers enhance output/react/ --mode LOCAL --timeout 1200 -Mode Comparison: +LOCAL Mode Comparison: - headless: Runs local agent CLI directly, BLOCKS until done (default) - background: Runs in background thread, returns immediately - daemon: Fully detached process, continues after parent exits - terminal: Opens new terminal window (interactive) -Force Mode (Default ON): - By default, all modes skip confirmations (auto-yes). +Force Mode (LOCAL only, Default ON): + By default, all LOCAL modes skip confirmations (auto-yes). Use --no-force to enable confirmation prompts. """, ) parser.add_argument("skill_directory", help="Path to skill directory (e.g., output/react/)") + parser.add_argument( + "--mode", + choices=["LOCAL", "API"], + help=( + "Force enhancement mode. LOCAL uses a local coding agent (free). " + "API uses the platform API (requires API key). " + "Default: auto-detect from environment variables." + ), + ) + parser.add_argument( "--agent", choices=sorted(list(AGENT_PRESETS.keys()) + ["custom"]), @@ -1290,6 +1363,14 @@ Force Mode (Default ON): args = parser.parse_args() + # Auto-detect API mode unless --mode LOCAL is explicitly set + if getattr(args, "mode", None) != "LOCAL": + api_target = _detect_api_target() + if api_target is not None: + target, api_key = api_target + _run_api_enhance(target, api_key) + return + # Validate mutually exclusive options mode_count = sum([args.interactive_enhancement, args.background, args.daemon]) if mode_count > 1: diff --git a/tests/test_enhance_skill_local.py b/tests/test_enhance_skill_local.py index 557dd25..601db7e 100644 --- a/tests/test_enhance_skill_local.py +++ b/tests/test_enhance_skill_local.py @@ -596,3 +596,148 @@ class TestRunBackground: # Should have returned quickly (not waited for the slow thread) assert result is True assert elapsed < 0.4, f"_run_background took {elapsed:.2f}s - should return immediately" + + +class TestEnhanceDispatcher: + """Test auto-detection of API vs LOCAL mode in enhance main().""" + + def test_detect_api_target_anthropic(self, monkeypatch): + """ANTHROPIC_API_KEY detected as claude target.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test") + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + result = _detect_api_target() + assert result == ("claude", "sk-ant-test") + + def test_detect_api_target_google(self, monkeypatch): + """GOOGLE_API_KEY detected as gemini target when no Anthropic key.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.setenv("GOOGLE_API_KEY", "AIza-test") + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + result = _detect_api_target() + assert result == ("gemini", "AIza-test") + + def test_detect_api_target_openai(self, monkeypatch): + """OPENAI_API_KEY detected as openai target when no higher-priority key.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-test") + result = _detect_api_target() + assert result == ("openai", "sk-openai-test") + + def test_detect_api_target_none(self, monkeypatch): + """Returns None when no API keys are set.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + result = _detect_api_target() + assert result is None + + def test_detect_api_target_anthropic_priority(self, monkeypatch): + """ANTHROPIC_API_KEY takes priority over GOOGLE_API_KEY.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test") + monkeypatch.setenv("GOOGLE_API_KEY", "AIza-test") + monkeypatch.setenv("OPENAI_API_KEY", "sk-openai-test") + result = _detect_api_target() + assert result == ("claude", "sk-ant-test") + + def test_detect_api_target_auth_token_fallback(self, monkeypatch): + """ANTHROPIC_AUTH_TOKEN is used when ANTHROPIC_API_KEY is absent.""" + from skill_seekers.cli.enhance_skill_local import _detect_api_target + + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "sk-auth-test") + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + result = _detect_api_target() + assert result == ("claude", "sk-auth-test") + + def test_main_delegates_to_api_when_key_set(self, monkeypatch, tmp_path): + """main() calls _run_api_enhance when an API key is detected.""" + import sys + from skill_seekers.cli.enhance_skill_local import main + + skill_dir = _make_skill_dir(tmp_path) + monkeypatch.setenv("GOOGLE_API_KEY", "AIza-test") + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setattr(sys, "argv", ["enhance", str(skill_dir)]) + + called_with = {} + + def fake_run_api(target, api_key): + called_with["target"] = target + called_with["api_key"] = api_key + + monkeypatch.setattr( + "skill_seekers.cli.enhance_skill_local._run_api_enhance", fake_run_api + ) + main() + assert called_with == {"target": "gemini", "api_key": "AIza-test"} + + def test_main_uses_local_when_mode_local(self, monkeypatch, tmp_path): + """main() stays in LOCAL mode when --mode LOCAL is passed.""" + import sys + from skill_seekers.cli.enhance_skill_local import main + + skill_dir = _make_skill_dir(tmp_path) + monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-test") + monkeypatch.setattr(sys, "argv", ["enhance", str(skill_dir), "--mode", "LOCAL"]) + + api_called = [] + monkeypatch.setattr( + "skill_seekers.cli.enhance_skill_local._run_api_enhance", + lambda *a: api_called.append(a), + ) + + # LocalSkillEnhancer.run will fail without a real agent, just verify + # _run_api_enhance was NOT called + with patch("skill_seekers.cli.enhance_skill_local.LocalSkillEnhancer") as mock_enhancer: + mock_instance = MagicMock() + mock_instance.run.return_value = True + mock_enhancer.return_value = mock_instance + with pytest.raises(SystemExit): + main() + + assert api_called == [], "_run_api_enhance should not be called in LOCAL mode" + + def test_main_uses_local_when_no_api_keys(self, monkeypatch, tmp_path): + """main() uses LOCAL mode when no API keys are present.""" + import sys + from skill_seekers.cli.enhance_skill_local import main + + skill_dir = _make_skill_dir(tmp_path) + monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) + monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("OPENAI_API_KEY", raising=False) + monkeypatch.setattr(sys, "argv", ["enhance", str(skill_dir)]) + + api_called = [] + monkeypatch.setattr( + "skill_seekers.cli.enhance_skill_local._run_api_enhance", + lambda *a: api_called.append(a), + ) + + with patch("skill_seekers.cli.enhance_skill_local.LocalSkillEnhancer") as mock_enhancer: + mock_instance = MagicMock() + mock_instance.run.return_value = True + mock_enhancer.return_value = mock_instance + with pytest.raises(SystemExit): + main() + + assert api_called == [], "_run_api_enhance should not be called without API keys"