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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user