iop/agent-client/pi/install.sh

222 lines
6.4 KiB
Bash
Executable file

#!/usr/bin/env bash
set -euo pipefail
PI_DIR="$HOME/.pi/agent"
SETTINGS="$PI_DIR/settings.json"
MODELS="$PI_DIR/models.json"
PYTHON_BIN="/usr/bin/python3"
if [[ ! -x "$PYTHON_BIN" ]]; then
PYTHON_BIN="$(command -v python3 || true)"
fi
if [[ -z "$PYTHON_BIN" || ! -x "$PYTHON_BIN" ]]; then
echo "python3 를 찾을 수 없습니다."
exit 1
fi
mkdir -p "$PI_DIR"
"$PYTHON_BIN" - "$SETTINGS" "$MODELS" <<'PY'
import json
import pathlib
import subprocess
import sys
import time
settings_path = pathlib.Path(sys.argv[1]).expanduser()
models_path = pathlib.Path(sys.argv[2]).expanduser()
def read_json(path):
if not path.exists():
return {}
return json.loads(path.read_text() or "{}")
def write_json(path, value):
if path.exists():
backup = path.with_name(f"{path.name}.bak-{time.strftime('%Y%m%d%H%M%S')}")
backup.write_bytes(path.read_bytes())
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n")
def run_helper(path):
expanded = pathlib.Path(path).expanduser()
if not expanded.exists():
return ""
try:
result = subprocess.run(
[str(expanded)],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
timeout=10,
)
except Exception:
return ""
return result.stdout.strip()
def merge_desired_models(existing_models, desired_models):
existing_by_id = {}
for existing in existing_models:
if isinstance(existing, dict) and isinstance(existing.get("id"), str):
existing_by_id.setdefault(existing["id"], existing)
merged_models = []
for desired in desired_models:
merged = dict(existing_by_id.get(desired["id"], {}))
merged.update(desired)
merged_models.append(merged)
return merged_models
def zero_cost():
return {"input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0}
settings = read_json(settings_path)
models_doc = read_json(models_path)
providers = models_doc.setdefault("providers", {})
seulgivibe_provider = providers.setdefault("seulgivibe-claude", {})
seulgivibe_api_key = seulgivibe_provider.get("apiKey") or run_helper("~/.claude/anthropic_key.sh")
seulgivibe_provider.update(
{
"baseUrl": "https://seulgivibe.lgudax.cool/anthropic",
"api": "anthropic-messages",
"authHeader": True,
"compat": {"supportsEagerToolInputStreaming": False},
}
)
if seulgivibe_api_key:
seulgivibe_provider["apiKey"] = seulgivibe_api_key
seulgivibe_provider["models"] = merge_desired_models(
seulgivibe_provider.get("models", []),
[
{
"id": "claude-sonnet-4-5",
"name": "Seulgivibe Claude Sonnet 4.5",
"reasoning": True,
"input": ["text"],
"contextWindow": 200000,
"maxTokens": 64000,
"cost": zero_cost(),
},
{
"id": "claude-opus-4-8",
"name": "Seulgivibe Claude Opus 4.8",
"reasoning": True,
"thinkingLevelMap": {"xhigh": "xhigh"},
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 128000,
"compat": {"forceAdaptiveThinking": True, "supportsTemperature": False},
"cost": zero_cost(),
},
{
"id": "claude-fable-5",
"name": "Seulgivibe Claude Fable 5",
"reasoning": True,
"thinkingLevelMap": {"xhigh": "xhigh"},
"input": ["text"],
"contextWindow": 1000000,
"maxTokens": 128000,
"compat": {"forceAdaptiveThinking": True, "supportsTemperature": False},
"cost": zero_cost(),
},
],
)
legacy_openai_provider = providers.get("seulgivibe-openai")
if not isinstance(legacy_openai_provider, dict):
legacy_openai_provider = {}
seulgivibe_codex_provider = providers.setdefault("seulgivibe-codex", {})
seulgivibe_codex_api_key = (
seulgivibe_codex_provider.get("apiKey")
or legacy_openai_provider.get("apiKey")
or run_helper("~/.claude/anthropic_key.sh")
)
seulgivibe_codex_provider.update(
{
"baseUrl": "https://seulgivibe.lgudax.cool/openai/v1",
"api": "openai-responses",
}
)
if seulgivibe_codex_api_key:
seulgivibe_codex_provider["apiKey"] = seulgivibe_codex_api_key
seulgivibe_codex_provider["models"] = merge_desired_models(
seulgivibe_codex_provider.get("models", []),
[
{
"id": "gpt-5.1",
"name": "Seulgivibe Codex GPT-5.1",
"reasoning": True,
"thinkingLevelMap": {"off": "none"},
"input": ["text"],
"contextWindow": 400000,
"maxTokens": 128000,
"cost": zero_cost(),
},
{
"id": "gpt-5.5",
"name": "Seulgivibe Codex GPT-5.5",
"reasoning": True,
"thinkingLevelMap": {"off": "none", "xhigh": "xhigh", "minimal": None},
"input": ["text"],
"contextWindow": 272000,
"maxTokens": 128000,
"cost": zero_cost(),
},
],
)
providers.pop("seulgivibe-openai", None)
settings.setdefault("defaultProvider", "seulgivibe-codex")
settings.setdefault("defaultModel", "gpt-5.5")
settings.setdefault("defaultThinkingLevel", "high")
settings.setdefault("hideThinkingBlock", False)
settings["httpIdleTimeoutMs"] = 300000
retry = settings.setdefault("retry", {})
provider_retry = retry.setdefault("provider", {})
provider_retry["timeoutMs"] = 300000
provider_retry["maxRetries"] = 0
provider_retry["maxRetryDelayMs"] = 60000
enabled = settings.setdefault("enabledModels", [])
if not isinstance(enabled, list):
enabled = []
desired_enabled = [
"seulgivibe-codex/gpt-5.1:medium",
"seulgivibe-codex/gpt-5.5:xhigh",
"seulgivibe-claude/claude-sonnet-4-5",
"seulgivibe-claude/claude-opus-4-8",
"seulgivibe-claude/claude-fable-5",
]
normalized_enabled = []
seen_enabled = set()
for item in enabled:
if not isinstance(item, str):
continue
if item.startswith("seulgivibe-openai/"):
continue
if item.startswith("seulgivibe-codex/") or item.startswith("seulgivibe-claude/"):
continue
if item not in seen_enabled:
normalized_enabled.append(item)
seen_enabled.add(item)
for item in desired_enabled:
if item not in seen_enabled:
normalized_enabled.append(item)
seen_enabled.add(item)
settings["enabledModels"] = normalized_enabled
write_json(settings_path, settings)
write_json(models_path, models_doc)
PY
echo "완료: Pi session에서 /reload 하거나 Pi를 재시작하세요."