4ff77b71ff
Addresses maintainer review on the OpenAI-compatible endpoints PR: 1. CLI: accept --backend azure_openai in skillopt_sleep/__main__.py (the documented command was rejected by the argparse choices). 2. Example runner: exit with the child's return code so watchdog/supervisors see a failed sleep run as a failure. 3. Error state: clear last_call_error when a retry recovers; set an explicit "empty response on all N attempts" diagnostic when every attempt returns empty text. 4. Security guard: the managed-identity path now refuses to send an Azure AD bearer token to any endpoint outside *.openai.azure.com / *.cognitiveservices.azure.com — a custom endpoint requires explicit AZURE_OPENAI_AUTH_MODE=openai_compatible + API key. 5. Provider-neutral requests: compat mode sends only the standard contract (max_tokens, default 8192 via SKILLOPT_SLEEP_COMPAT_MAX_TOKENS); provider-specific body fields are opt-in via SKILLOPT_SLEEP_CHAT_EXTRA_BODY (JSON) — the deepseek model-name inference is removed. 6. Docs: removed the unimplemented OPTIMIZER_*/TARGET_* env-var claim; added a configuration reference matching the implementation exactly. 7. Tests: tests/test_azure_openai_compat.py — 17 deterministic no-network unittest cases covering CLI acceptance, compat-vs-Azure client selection, endpoint resolution, the credential guard, request kwargs (opt-in extra body / token cap), retry-success error clearing, empty-response diagnostics, and runner exit-code propagation. Re-verified live against DeepSeek (deepseek-v4-pro, openai_compatible mode) after the rework: client type OpenAI, completion returned, no error state.
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Reference launcher for running SkillOpt-Sleep against an OpenAI-compatible
|
|
endpoint (DeepSeek shown here), plus an Antigravity `session-end` hook.
|
|
|
|
This is a *sanitized example*, not a supported entry point. Adapt the paths and
|
|
provider details to your environment. No API keys are hardcoded — the key is read
|
|
from an .env file or the process environment.
|
|
|
|
Usage:
|
|
python runner.py run # run a full sleep cycle against DeepSeek
|
|
python runner.py dry-run # harvest + replay, report only
|
|
python runner.py session-end # Antigravity Stop-hook: append rollout evidence
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
import json
|
|
import subprocess
|
|
import datetime
|
|
from pathlib import Path
|
|
|
|
# --- Configure these for your environment -----------------------------------
|
|
# Path to a file containing your provider key as `sk-...` (kept out of source).
|
|
PROVIDER_ENV_FILE = Path(os.environ.get("SKILLOPT_PROVIDER_ENV_FILE", "provider.env"))
|
|
# Endpoint + model for the OpenAI-compatible provider.
|
|
PROVIDER_ENDPOINT = os.environ.get("SKILLOPT_PROVIDER_ENDPOINT", "https://api.deepseek.com")
|
|
PROVIDER_MODEL = os.environ.get("SKILLOPT_PROVIDER_MODEL", "deepseek-v4-pro")
|
|
# Project whose SKILL.md files the sleep cycle should evolve.
|
|
PROJECT_DIR = os.environ.get("SKILLOPT_PROJECT_DIR", os.getcwd())
|
|
# Where the session-end hook appends rollout evidence.
|
|
ROLLOUT_LOG = Path(os.environ.get("SKILLOPT_ROLLOUT_LOG", "brain/rollout-evidence.jsonl"))
|
|
# ----------------------------------------------------------------------------
|
|
|
|
|
|
def load_provider_key(env: dict) -> None:
|
|
"""Ensure DEEPSEEK_API_KEY is set, reading it from PROVIDER_ENV_FILE if needed."""
|
|
if env.get("DEEPSEEK_API_KEY"):
|
|
return
|
|
try:
|
|
text = PROVIDER_ENV_FILE.read_text(encoding="utf-8")
|
|
except OSError:
|
|
return
|
|
m = re.search(r"sk-[A-Za-z0-9]+", text)
|
|
if m:
|
|
env["DEEPSEEK_API_KEY"] = m.group(0)
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2:
|
|
print("Usage: runner.py [dry-run|run|status|adopt|session-end]")
|
|
sys.exit(1)
|
|
|
|
command = sys.argv[1]
|
|
|
|
# Antigravity Stop-hook: enrich future nights with task-outcome metadata.
|
|
if command == "session-end":
|
|
ROLLOUT_LOG.parent.mkdir(parents=True, exist_ok=True)
|
|
outcome = {
|
|
"timestamp": datetime.datetime.now().isoformat(),
|
|
"event": "SessionEnd",
|
|
"metadata": "Appended task outcome metadata",
|
|
}
|
|
with open(ROLLOUT_LOG, "a", encoding="utf-8") as f:
|
|
f.write(json.dumps(outcome) + "\n")
|
|
print("Rollout evidence metadata appended.")
|
|
return
|
|
|
|
env = os.environ.copy()
|
|
load_provider_key(env)
|
|
|
|
if env.get("DEEPSEEK_API_KEY"):
|
|
# OpenAI-compatible path — see docs/sleep/openai-compatible-endpoints.md
|
|
backend = "azure_openai"
|
|
env["PYTHONIOENCODING"] = "utf-8"
|
|
env["AZURE_OPENAI_AUTH_MODE"] = "openai_compatible"
|
|
env["AZURE_OPENAI_ENDPOINT"] = PROVIDER_ENDPOINT
|
|
env["AZURE_OPENAI_API_KEY"] = env["DEEPSEEK_API_KEY"]
|
|
# Provider-specific request fields are opt-in, never inferred from the
|
|
# model name. For DeepSeek reasoning models, enable the thinking channel:
|
|
env.setdefault("SKILLOPT_SLEEP_CHAT_EXTRA_BODY",
|
|
json.dumps({"thinking": {"type": "enabled"}}))
|
|
env.setdefault("SKILLOPT_SLEEP_COMPAT_MAX_TOKENS", "8192")
|
|
else:
|
|
# OPTIONAL, UNVERIFIED fallback: route the `claude` CLI backend through a
|
|
# local Anthropic-compatible proxy (e.g. LiteLLM) to reach Gemini. There
|
|
# is no native Gemini backend; this path was not validated. See the doc.
|
|
backend = "claude"
|
|
if "ANTHROPIC_API_KEY" not in env and "GEMINI_API_KEY" in env:
|
|
env["ANTHROPIC_API_KEY"] = env["GEMINI_API_KEY"]
|
|
env.setdefault("ANTHROPIC_BASE_URL", "http://127.0.0.1:4000")
|
|
|
|
args = ["skillopt-sleep", command]
|
|
if command in ("run", "dry-run"):
|
|
args = ["skillopt-sleep", command, "--backend", backend,
|
|
"--model", PROVIDER_MODEL, "--project", PROJECT_DIR]
|
|
|
|
print(f"Running: {' '.join(args)}")
|
|
# Propagate the child's exit code so supervisors (watchdog.py, systemd,
|
|
# Task Scheduler) see a failed sleep run as a failure, not a success.
|
|
proc = subprocess.run(args, env=env, check=False)
|
|
sys.exit(proc.returncode)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|