fix: update Gemini model to 2.5-flash and add API auto-detection in enhance

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 <noreply@anthropic.com>
This commit is contained in:
yusyus
2026-02-24 06:52:55 +03:00
parent 63968c05f6
commit 5ae57d192a
3 changed files with 246 additions and 20 deletions

View File

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

View File

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

View File

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