Skip to content

Commit 96e8adc

Browse files
Add FreeAPI provider support across OpenCode templates
1 parent 4bf0f0f commit 96e8adc

5 files changed

Lines changed: 607 additions & 9 deletions

File tree

workspaces/AdvancedHostDANGER/main.tf

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ variable "mks_key_endpoint" {
4242
type = string
4343
}
4444

45+
variable "freeapi_base_url" {
46+
default = ""
47+
description = "Base URL OpenAI-compatible para FreeAPI (ej. $TF_VAR_freeapi_base_url)."
48+
type = string
49+
}
50+
51+
variable "freeapi_key_endpoint" {
52+
default = ""
53+
description = "Endpoint para solicitar API keys de FreeAPI."
54+
type = string
55+
}
56+
4557
data "coder_parameter" "persist_home_storage" {
4658
name = "02_01_persist_home_storage"
4759
display_name = "[Storage] Persistir home en el host"
@@ -124,6 +136,15 @@ data "coder_parameter" "opencode_api_key" {
124136
mutable = true
125137
}
126138

139+
data "coder_parameter" "autoprovision_freeapi_key" {
140+
name = "04_autoprovision_freeapi_key"
141+
display_name = "[AI/FreeAPI] Provisionar API key automáticamente"
142+
description = "Si hay FREEAPI_BASE_URL/FREEAPI_KEY_ENDPOINT, solicita una key y configura FreeAPI en OpenCode."
143+
type = "bool"
144+
default = true
145+
mutable = true
146+
}
147+
127148
data "coder_parameter" "claude_token" {
128149
name = "04_claude_token"
129150
display_name = "[AI/Claude] Token"
@@ -158,12 +179,15 @@ locals {
158179
projects_mount_host_path = local.persist_projects_storage && local.workspace_storage_root != "" ? local.workspace_storage_projects : ""
159180
opencode_default_base_url = trimspace(var.opencode_default_base_url)
160181
mks_key_endpoint = trimspace(var.mks_key_endpoint)
182+
freeapi_base_url = trimspace(var.freeapi_base_url)
183+
freeapi_key_endpoint = trimspace(var.freeapi_key_endpoint)
161184
home_volume_resolved = "coder-${data.coder_workspace.me.id}-home"
162185
repo_url = trimspace(data.coder_parameter.git_repo_url.value)
163186
repo_name = local.repo_url != "" ? trimsuffix(basename(local.repo_url), ".git") : ""
164187
default_repo_path = local.repo_name != "" ? "/home/coder/Projects/${local.repo_name}" : "/home/coder/Projects"
165188
openai_base_url = trimspace(data.coder_parameter.opencode_provider_url.value)
166189
openai_api_key = trimspace(data.coder_parameter.opencode_api_key.value)
190+
auto_provision_freeapi_key = data.coder_parameter.autoprovision_freeapi_key.value
167191
claude_token = trimspace(data.coder_parameter.claude_token.value)
168192
install_claude = local.claude_token != ""
169193
vscode_extensions_default = [
@@ -397,6 +421,32 @@ CHROMEWRAP
397421
fi
398422
fi
399423
424+
# Autoprovisionar clave FreeAPI si está habilitado y hay endpoint/base URL
425+
freeapi_auto_flag="$${AUTO_PROVISION_FREEAPI_API_KEY:-true}"
426+
if printf '%s' "$freeapi_auto_flag" | grep -Eq '^(1|true|TRUE|yes|on)$'; then
427+
FREEAPI_BASE_URL="$${FREEAPI_BASE_URL:-}"
428+
export FREEAPI_BASE_URL
429+
freeapi_payload=""
430+
if [ -z "$${FREEAPI_API_KEY:-}" ]; then
431+
FREEAPI_ENDPOINT="$${FREEAPI_KEY_ENDPOINT:-}"
432+
if [ -z "$FREEAPI_ENDPOINT" ]; then
433+
echo "FREEAPI_KEY_ENDPOINT no configurado; omitiendo autoprovision de key FreeAPI" >&2
434+
else
435+
freeapi_alias="freeapi-$(tr -dc 0-9 </dev/urandom 2>/dev/null | head -c 8 | sed 's/^$/00000000/')"
436+
freeapi_payload=$(printf '{"email":"%s","alias":"%s"}' "$${CODER_USER_EMAIL:-}" "$freeapi_alias")
437+
freeapi_resp=$(curl -fsSL -X POST "$FREEAPI_ENDPOINT" -H "Content-Type: application/json" -d "$freeapi_payload" 2>/dev/null || true)
438+
freeapi_key=$(printf '%s' "$freeapi_resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("key",""))' 2>/dev/null || true)
439+
if [ -n "$freeapi_key" ]; then
440+
FREEAPI_API_KEY="$freeapi_key"
441+
export FREEAPI_API_KEY
442+
mkdir -p /home/coder/.opencode
443+
printf "%s" "$freeapi_key" > /home/coder/.opencode/.latest_freeapi_key || true
444+
printf "%s" "$freeapi_payload" > /home/coder/.opencode/.latest_freeapi_request || true
445+
fi
446+
fi
447+
fi
448+
fi
449+
400450
# Script para regenerar y aplicar nueva key de MakeSpace
401451
sudo tee /usr/local/bin/gen_mks_litellm_key >/dev/null <<'GENMKS'
402452
#!/usr/bin/env bash
@@ -456,7 +506,7 @@ GENMKS
456506
sudo chmod +x /usr/local/bin/gen_mks_litellm_key || true
457507
458508
# Config inicial de OpenCode (opcional)
459-
if [ -n "$${OPENCODE_PROVIDER_URL:-}" ] && [ -n "$${OPENCODE_API_KEY:-}" ]; then
509+
if [ -n "$${OPENCODE_PROVIDER_URL:-}" ] && [ -n "$${OPENCODE_API_KEY:-}" ] || [ -n "$${FREEAPI_API_KEY:-}" ]; then
460510
mkdir -p /home/coder/.opencode
461511
cat > /home/coder/.opencode/opencode.json <<'JSONCFG'
462512
{
@@ -731,6 +781,64 @@ GENMKS
731781
JSONCFG
732782
sed -i "s|OPENCODE_PROVIDER_URL_VALUE|$${OPENCODE_PROVIDER_URL}|g" /home/coder/.opencode/opencode.json
733783
sed -i "s|OPENCODE_API_KEY_VALUE|$${OPENCODE_API_KEY}|g" /home/coder/.opencode/opencode.json
784+
FREEAPI_BASE="$${FREEAPI_BASE_URL:-}"
785+
FREEAPI_KEY="$${FREEAPI_API_KEY:-}"
786+
FREEAPI_BASE="$${FREEAPI_BASE%/}"
787+
if [ -n "$FREEAPI_BASE" ]; then
788+
FREEAPI_BASE_URL="$FREEAPI_BASE" FREEAPI_API_KEY="$FREEAPI_KEY" python3 - <<'PY'
789+
import json, os, urllib.request
790+
path = "/home/coder/.opencode/opencode.json"
791+
base_url = (os.environ.get("FREEAPI_BASE_URL") or "").strip().rstrip("/")
792+
api_key = (os.environ.get("FREEAPI_API_KEY") or "").strip()
793+
if not base_url:
794+
raise SystemExit(0)
795+
with open(path, "r", encoding="utf-8") as f:
796+
data = json.load(f)
797+
def norm_model_id(raw):
798+
if not isinstance(raw, str):
799+
return ""
800+
s = raw.strip()
801+
if not s:
802+
return ""
803+
if "//" in s:
804+
s = s.split("//", 1)[1]
805+
if "/" in s:
806+
s = s.rsplit("/", 1)[-1]
807+
return s
808+
model_ids = []
809+
for p in ("/v1/models", "/models"):
810+
try:
811+
headers = {"Accept": "application/json"}
812+
if api_key:
813+
headers["Authorization"] = f"Bearer {api_key}"
814+
req = urllib.request.Request(f"{base_url}{p}", headers=headers)
815+
with urllib.request.urlopen(req, timeout=10) as resp:
816+
payload = json.loads(resp.read().decode("utf-8", "replace"))
817+
items = payload.get("data", payload if isinstance(payload, list) else [])
818+
for item in items:
819+
if not isinstance(item, dict):
820+
continue
821+
mid = norm_model_id(item.get("id"))
822+
if mid.endswith("-ha"):
823+
model_ids.append(mid)
824+
if model_ids:
825+
break
826+
except Exception:
827+
continue
828+
models = {}
829+
for mid in sorted(set(model_ids)):
830+
models[mid] = {"name": mid}
831+
provider = data.setdefault("provider", {})
832+
provider["freeapi"] = {
833+
"npm": "@ai-sdk/openai-compatible",
834+
"name": "FreeAPI",
835+
"options": {"baseURL": base_url, "api_key": api_key},
836+
"models": models,
837+
}
838+
with open(path, "w", encoding="utf-8") as f:
839+
json.dump(data, f, indent=2, ensure_ascii=False)
840+
PY
841+
fi
734842
ln -sf /home/coder/.opencode/opencode.json /home/coder/.opencode/config.json || true
735843
chown -R "$USER:$USER" /home/coder/.opencode || true
736844
fi
@@ -777,8 +885,11 @@ CONTINUECFG
777885
OPENCODE_API_KEY = local.openai_api_key
778886
OPENCODE_DEFAULT_BASE_URL = local.opencode_default_base_url
779887
MKS_KEY_ENDPOINT = local.mks_key_endpoint
888+
FREEAPI_BASE_URL = local.freeapi_base_url
889+
FREEAPI_KEY_ENDPOINT = local.freeapi_key_endpoint
780890
MKS_BASE_URL = local.openai_base_url
781891
MKS_API_KEY = local.openai_api_key
892+
AUTO_PROVISION_FREEAPI_API_KEY = tostring(local.auto_provision_freeapi_key)
782893
INSTALL_CLAUDE = tostring(local.install_claude)
783894
CODER_USER_EMAIL = data.coder_workspace_owner.me.email
784895
DEFAULT_REPO_PATH = local.default_repo_path

workspaces/Developer/main.tf

Lines changed: 137 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ variable "mks_key_endpoint" {
4242
type = string
4343
}
4444

45+
variable "freeapi_base_url" {
46+
default = ""
47+
description = "Base URL OpenAI-compatible para FreeAPI (ej. $TF_VAR_freeapi_base_url)."
48+
type = string
49+
}
50+
51+
variable "freeapi_key_endpoint" {
52+
default = ""
53+
description = "Endpoint para solicitar API keys de FreeAPI."
54+
type = string
55+
}
56+
4557

4658
# ================================
4759
# Parámetros visibles en Coder
@@ -155,6 +167,15 @@ data "coder_parameter" "p05_autoprovision_mks_key" {
155167
mutable = true
156168
}
157169

170+
data "coder_parameter" "p05_autoprovision_freeapi_key" {
171+
name = "05_autoprovision_freeapi_key"
172+
display_name = "[AI/FreeAPI] Provisionar API_KEY automáticamente"
173+
description = "Si hay FREEAPI_BASE_URL/FREEAPI_KEY_ENDPOINT, solicita una key y configura FreeAPI en OpenCode."
174+
type = "bool"
175+
default = true
176+
mutable = true
177+
}
178+
158179
data "coder_parameter" "p05_opencode_provider_url" {
159180
name = "05_opencode_provider_url"
160181
display_name = "[AI/OpenAI] Base URL (opcional)"
@@ -199,6 +220,8 @@ locals {
199220
projects_mount_host_path = local.persist_projects_storage && local.workspace_storage_root != "" ? local.workspace_storage_projects : ""
200221
opencode_default_base_url = trimspace(var.opencode_default_base_url)
201222
mks_key_endpoint = trimspace(var.mks_key_endpoint)
223+
freeapi_base_url = trimspace(var.freeapi_base_url)
224+
freeapi_key_endpoint = trimspace(var.freeapi_key_endpoint)
202225
home_volume_resolved = "coder-${data.coder_workspace.me.id}-home"
203226
docker_data_volume_name = "coder-${data.coder_workspace.me.id}-docker-data"
204227
repo_url = trimspace(data.coder_parameter.p04_git_repo_url.value)
@@ -207,6 +230,7 @@ locals {
207230
openai_base_url = trimspace(data.coder_parameter.p05_opencode_provider_url.value)
208231
openai_api_key = trimspace(data.coder_parameter.p05_opencode_api_key.value)
209232
auto_provision_mks_key = data.coder_parameter.p05_autoprovision_mks_key.value
233+
auto_provision_freeapi_key = data.coder_parameter.p05_autoprovision_freeapi_key.value
210234
claude_token = trimspace(data.coder_parameter.p05_claude_token.value)
211235
install_claude = trimspace(data.coder_parameter.p05_claude_token.value) != ""
212236
vscode_extensions_default = [
@@ -440,6 +464,32 @@ JSONCFG
440464
fi
441465
fi
442466
467+
# Autoprovisionar clave FreeAPI si está habilitado y hay endpoint/base URL
468+
freeapi_auto_flag="$${AUTO_PROVISION_FREEAPI_API_KEY:-true}"
469+
if printf '%s' "$freeapi_auto_flag" | grep -Eq '^(1|true|TRUE|yes|on)$'; then
470+
FREEAPI_BASE_URL="$${FREEAPI_BASE_URL:-}"
471+
export FREEAPI_BASE_URL
472+
freeapi_payload=""
473+
if [ -z "$${FREEAPI_API_KEY:-}" ]; then
474+
FREEAPI_ENDPOINT="$${FREEAPI_KEY_ENDPOINT:-}"
475+
if [ -z "$FREEAPI_ENDPOINT" ]; then
476+
echo "FREEAPI_KEY_ENDPOINT no configurado; omitiendo autoprovision de key FreeAPI" >&2
477+
else
478+
freeapi_alias="freeapi-$(tr -dc 0-9 </dev/urandom 2>/dev/null | head -c 8 | sed 's/^$/00000000/')"
479+
freeapi_payload=$(printf '{"email":"%s","alias":"%s"}' "$${CODER_USER_EMAIL:-}" "$freeapi_alias")
480+
freeapi_resp=$(curl -fsSL -X POST "$FREEAPI_ENDPOINT" -H "Content-Type: application/json" -d "$freeapi_payload" 2>/dev/null || true)
481+
freeapi_key=$(printf '%s' "$freeapi_resp" | python3 -c 'import sys,json;print(json.load(sys.stdin).get("key",""))' 2>/dev/null || true)
482+
if [ -n "$freeapi_key" ]; then
483+
FREEAPI_API_KEY="$freeapi_key"
484+
export FREEAPI_API_KEY
485+
mkdir -p /home/coder/.opencode
486+
printf "%s" "$freeapi_key" > /home/coder/.opencode/.latest_freeapi_key || true
487+
printf "%s" "$freeapi_payload" > /home/coder/.opencode/.latest_freeapi_request || true
488+
fi
489+
fi
490+
fi
491+
fi
492+
443493
# Propagar variables a nuevas shells interactivas
444494
if [ -n "$${OPENCODE_PROVIDER_URL:-}" ]; then
445495
MKS_BASE_URL="$${MKS_BASE_URL:-$OPENCODE_PROVIDER_URL}"
@@ -461,6 +511,16 @@ JSONCFG
461511
echo "export OPENCODE_API_KEY=\"$OPENCODE_API_KEY\"" >> ~/.bashrc
462512
fi
463513
fi
514+
if [ -n "$${FREEAPI_BASE_URL:-}" ]; then
515+
if ! grep -q "FREEAPI_BASE_URL=" ~/.bashrc 2>/dev/null; then
516+
echo "export FREEAPI_BASE_URL=\"$FREEAPI_BASE_URL\"" >> ~/.bashrc
517+
fi
518+
fi
519+
if [ -n "$${FREEAPI_API_KEY:-}" ]; then
520+
if ! grep -q "FREEAPI_API_KEY=" ~/.bashrc 2>/dev/null; then
521+
echo "export FREEAPI_API_KEY=\"$FREEAPI_API_KEY\"" >> ~/.bashrc
522+
fi
523+
fi
464524
465525
# Instalar OpenCode CLI si Claude está activo (sin módulo)
466526
if printf '%s' "$${INSTALL_CLAUDE:-false}" | grep -Eq '^(1|true|TRUE|yes|on)$'; then
@@ -544,7 +604,7 @@ GENMKS
544604
sudo chmod +x /usr/local/bin/gen_mks_litellm_key || true
545605
546606
# Config inicial de OpenCode (opcional)
547-
if [ -n "$${OPENCODE_API_KEY:-}" ]; then
607+
if [ -n "$${OPENCODE_API_KEY:-}" ] || [ -n "$${FREEAPI_API_KEY:-}" ]; then
548608
mkdir -p /home/coder/.opencode
549609
cat > /home/coder/.opencode/opencode.json <<'JSONCFG'
550610
{
@@ -787,13 +847,16 @@ GENMKS
787847
"api_key": "OPENCODE_API_KEY_VALUE"
788848
},
789849
"models": {
790-
"devstral:24b": { "name": "Devstral 24b" },
791-
"qwen2.5-coder:14b": { "name": "Qwen2.5 Coder 14b" },
792-
"qwen2.5-coder:7b": { "name": "Qwen2.5 Coder 7b" },
850+
"devstral-small-2:24b": { "name": "Devstral Small 2 24b" },
851+
"opencoder-8b-base": { "name": "OpenCoder 8b Base" },
852+
"qwen2.5-coder:7b-base": { "name": "Qwen2.5 Coder 7b Base" },
793853
"qwen3-coder:30b": { "name": "Qwen3 Coder 30b" },
854+
"qwen3.5:27b": { "name": "Qwen3.5 27b" },
855+
"qwen3:32b": { "name": "Qwen3 32b" },
856+
"qwen3:14b": { "name": "Qwen3 14b" },
857+
"qwen3:8b": { "name": "Qwen3 8b" },
794858
"gpt-oss:20b": { "name": "GPT-OSS 20b" },
795-
"magistral:24b": { "name": "Magistral 24b" },
796-
"mistral-small3.1:24b": { "name": "Mistral Small3.1 24b" }
859+
"gpt-oss-safeguard:latest": { "name": "GPT-OSS Safeguard" }
797860
}
798861
},
799862
"google": {
@@ -829,6 +892,71 @@ JSONCFG
829892
base_url="$${OPENCODE_PROVIDER_URL:-$${OPENCODE_DEFAULT_BASE_URL:-}}"
830893
sed -i "s|OPENCODE_PROVIDER_URL_VALUE|$base_url|g" /home/coder/.opencode/opencode.json
831894
sed -i "s|OPENCODE_API_KEY_VALUE|$${OPENCODE_API_KEY}|g" /home/coder/.opencode/opencode.json
895+
FREEAPI_BASE="$${FREEAPI_BASE_URL:-}"
896+
FREEAPI_KEY="$${FREEAPI_API_KEY:-}"
897+
FREEAPI_BASE="$${FREEAPI_BASE%/}"
898+
if [ -n "$FREEAPI_BASE" ]; then
899+
FREEAPI_BASE_URL="$FREEAPI_BASE" FREEAPI_API_KEY="$FREEAPI_KEY" python3 - <<'PY'
900+
import json, os, urllib.request
901+
902+
path = "/home/coder/.opencode/opencode.json"
903+
base_url = (os.environ.get("FREEAPI_BASE_URL") or "").strip().rstrip("/")
904+
api_key = (os.environ.get("FREEAPI_API_KEY") or "").strip()
905+
if not base_url:
906+
raise SystemExit(0)
907+
908+
with open(path, "r", encoding="utf-8") as f:
909+
data = json.load(f)
910+
911+
def norm_model_id(raw):
912+
if not isinstance(raw, str):
913+
return ""
914+
s = raw.strip()
915+
if not s:
916+
return ""
917+
if "//" in s:
918+
s = s.split("//", 1)[1]
919+
if "/" in s:
920+
s = s.rsplit("/", 1)[-1]
921+
return s
922+
923+
model_ids = []
924+
for p in ("/v1/models", "/models"):
925+
try:
926+
headers = {"Accept": "application/json"}
927+
if api_key:
928+
headers["Authorization"] = f"Bearer {api_key}"
929+
req = urllib.request.Request(f"{base_url}{p}", headers=headers)
930+
with urllib.request.urlopen(req, timeout=10) as resp:
931+
payload = json.loads(resp.read().decode("utf-8", "replace"))
932+
items = payload.get("data", payload if isinstance(payload, list) else [])
933+
for item in items:
934+
if not isinstance(item, dict):
935+
continue
936+
mid = norm_model_id(item.get("id"))
937+
if mid.endswith("-ha"):
938+
model_ids.append(mid)
939+
if model_ids:
940+
break
941+
except Exception:
942+
continue
943+
944+
models = {}
945+
for mid in sorted(set(model_ids)):
946+
models[mid] = {"name": mid}
947+
948+
provider = data.setdefault("provider", {})
949+
provider["freeapi"] = {
950+
"npm": "@ai-sdk/openai-compatible",
951+
"name": "FreeAPI",
952+
"options": {"baseURL": base_url, "api_key": api_key},
953+
"models": models,
954+
}
955+
956+
with open(path, "w", encoding="utf-8") as f:
957+
json.dump(data, f, indent=2, ensure_ascii=False)
958+
PY
959+
fi
832960
chown -R "$USER:$USER" /home/coder/.opencode || true
833961
fi
834962
@@ -895,9 +1023,12 @@ JSONCFG
8951023
OPENCODE_API_KEY = local.openai_api_key
8961024
OPENCODE_DEFAULT_BASE_URL = local.opencode_default_base_url
8971025
MKS_KEY_ENDPOINT = local.mks_key_endpoint
1026+
FREEAPI_BASE_URL = local.freeapi_base_url
1027+
FREEAPI_KEY_ENDPOINT = local.freeapi_key_endpoint
8981028
MKS_BASE_URL = local.openai_base_url
8991029
MKS_API_KEY = local.openai_api_key
9001030
AUTO_PROVISION_MKS_API_KEY = tostring(local.auto_provision_mks_key)
1031+
AUTO_PROVISION_FREEAPI_API_KEY = tostring(local.auto_provision_freeapi_key)
9011032
CODER_USER_EMAIL = data.coder_workspace_owner.me.email
9021033
INSTALL_CLAUDE = tostring(local.install_claude)
9031034
AGENTAPI_CHAT_BASE_PATH = local.install_claude ? "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.id}/apps/ccw/chat" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.id}/apps/opencode/chat"

0 commit comments

Comments
 (0)