Spaces:
Running
Running
fix: Resolve OpenCode integration issues (terminal hang, missing models)
Browse files- engine.py +2 -0
- opencode_terminal.py +1 -1
- providers/opencode_provider.py +97 -0
- static/qaz.html +10 -0
- supabase_proxies.sql +130 -0
engine.py
CHANGED
|
@@ -21,6 +21,7 @@ from providers.gemini_provider import GeminiProvider
|
|
| 21 |
from providers.zai_provider import ZaiProvider
|
| 22 |
from providers.huggingchat_provider import HuggingChatProvider
|
| 23 |
from providers.copilot_provider import CopilotProvider
|
|
|
|
| 24 |
from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
|
| 25 |
from models import ModelInfo
|
| 26 |
from sanitizer import sanitize_response
|
|
@@ -49,6 +50,7 @@ class AIEngine:
|
|
| 49 |
self._providers: dict[str, BaseProvider] = {
|
| 50 |
"g4f": G4FProvider(),
|
| 51 |
"pollinations": PollinationsProvider(),
|
|
|
|
| 52 |
}
|
| 53 |
# Z.ai requires Playwright + Chromium (not available on Vercel serverless)
|
| 54 |
if ZaiProvider.is_available():
|
|
|
|
| 21 |
from providers.zai_provider import ZaiProvider
|
| 22 |
from providers.huggingchat_provider import HuggingChatProvider
|
| 23 |
from providers.copilot_provider import CopilotProvider
|
| 24 |
+
from providers.opencode_provider import OpenCodeProvider
|
| 25 |
from config import MODEL_RANKING, PROVIDER_MODELS, SUPABASE_URL, SUPABASE_KEY
|
| 26 |
from models import ModelInfo
|
| 27 |
from sanitizer import sanitize_response
|
|
|
|
| 50 |
self._providers: dict[str, BaseProvider] = {
|
| 51 |
"g4f": G4FProvider(),
|
| 52 |
"pollinations": PollinationsProvider(),
|
| 53 |
+
"opencode": OpenCodeProvider(),
|
| 54 |
}
|
| 55 |
# Z.ai requires Playwright + Chromium (not available on Vercel serverless)
|
| 56 |
if ZaiProvider.is_available():
|
opencode_terminal.py
CHANGED
|
@@ -108,7 +108,7 @@ class OpenCodeTerminalPortal:
|
|
| 108 |
env['OPENCODE_CONFIG'] = os.path.abspath(self.config.config_path)
|
| 109 |
|
| 110 |
self.process = subprocess.Popen(
|
| 111 |
-
['npx', 'opencode-ai'],
|
| 112 |
cwd=self.config.project_dir,
|
| 113 |
env=env,
|
| 114 |
stdin=subprocess.PIPE,
|
|
|
|
| 108 |
env['OPENCODE_CONFIG'] = os.path.abspath(self.config.config_path)
|
| 109 |
|
| 110 |
self.process = subprocess.Popen(
|
| 111 |
+
['npx', '-y', 'opencode-ai'],
|
| 112 |
cwd=self.config.project_dir,
|
| 113 |
env=env,
|
| 114 |
stdin=subprocess.PIPE,
|
providers/opencode_provider.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import aiohttp
|
| 3 |
+
import json
|
| 4 |
+
import logging
|
| 5 |
+
import asyncio
|
| 6 |
+
from typing import Any, Dict, List
|
| 7 |
+
from .base import BaseProvider
|
| 8 |
+
|
| 9 |
+
logger = logging.getLogger("kai_api.providers.opencode")
|
| 10 |
+
|
| 11 |
+
class OpenCodeProvider(BaseProvider):
|
| 12 |
+
"""
|
| 13 |
+
OpenCode AI Provider.
|
| 14 |
+
Uses the https://opencode.ai/zen/v1 compatible endpoint.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
@property
|
| 18 |
+
def name(self) -> str:
|
| 19 |
+
return "opencode"
|
| 20 |
+
|
| 21 |
+
async def send_message(
|
| 22 |
+
self,
|
| 23 |
+
prompt: str,
|
| 24 |
+
model: str | None = None,
|
| 25 |
+
system_prompt: str | None = None,
|
| 26 |
+
**kwargs: Any,
|
| 27 |
+
) -> Dict[str, Any]:
|
| 28 |
+
"""Send message to OpenCode API."""
|
| 29 |
+
if not model:
|
| 30 |
+
model = "kimi-k2.5-free"
|
| 31 |
+
|
| 32 |
+
# The opencode config suggests:
|
| 33 |
+
# baseURL: "https://opencode.ai/zen/v1"
|
| 34 |
+
# We assume standard OpenAI format
|
| 35 |
+
|
| 36 |
+
url = "https://opencode.ai/zen/v1/chat/completions"
|
| 37 |
+
|
| 38 |
+
messages = []
|
| 39 |
+
if system_prompt:
|
| 40 |
+
messages.append({"role": "system", "content": system_prompt})
|
| 41 |
+
messages.append({"role": "user", "content": prompt})
|
| 42 |
+
|
| 43 |
+
# The config says model: "opencode-zen/{model}"
|
| 44 |
+
api_model = f"opencode-zen/{model}" if "/" not in model else model
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
async with aiohttp.ClientSession() as session:
|
| 48 |
+
async with session.post(
|
| 49 |
+
url,
|
| 50 |
+
json={
|
| 51 |
+
"model": api_model,
|
| 52 |
+
"messages": messages,
|
| 53 |
+
"stream": False
|
| 54 |
+
},
|
| 55 |
+
headers={
|
| 56 |
+
"Content-Type": "application/json",
|
| 57 |
+
# "Authorization": "Bearer ..." # No key needed? We'll assume open for now
|
| 58 |
+
},
|
| 59 |
+
timeout=60
|
| 60 |
+
) as response:
|
| 61 |
+
if response.status != 200:
|
| 62 |
+
text = await response.text()
|
| 63 |
+
logger.error(f"OpenCode API error {response.status}: {text}")
|
| 64 |
+
# If error, try fallback or just raise
|
| 65 |
+
raise ValueError(f"OpenCode API error {response.status}: {text}")
|
| 66 |
+
|
| 67 |
+
data = await response.json()
|
| 68 |
+
|
| 69 |
+
if "choices" in data and len(data["choices"]) > 0:
|
| 70 |
+
content = data["choices"][0]["message"]["content"]
|
| 71 |
+
return {
|
| 72 |
+
"response": content,
|
| 73 |
+
"model": model
|
| 74 |
+
}
|
| 75 |
+
else:
|
| 76 |
+
raise ValueError(f"Invalid response from OpenCode: {data}")
|
| 77 |
+
|
| 78 |
+
except Exception as e:
|
| 79 |
+
logger.error(f"OpenCode request failed: {e}")
|
| 80 |
+
raise
|
| 81 |
+
|
| 82 |
+
def get_available_models(self) -> List[str]:
|
| 83 |
+
# Copied from opencode_terminal.py
|
| 84 |
+
return [
|
| 85 |
+
"kimi-k2.5-free",
|
| 86 |
+
"minimax-m2.5-free",
|
| 87 |
+
"big-pickle",
|
| 88 |
+
"glm-4.7"
|
| 89 |
+
]
|
| 90 |
+
|
| 91 |
+
async def health_check(self) -> bool:
|
| 92 |
+
try:
|
| 93 |
+
# Simple check
|
| 94 |
+
res = await self.send_message("hi", model="kimi-k2.5-free")
|
| 95 |
+
return bool(res and res.get("response"))
|
| 96 |
+
except:
|
| 97 |
+
return False
|
static/qaz.html
CHANGED
|
@@ -1155,6 +1155,16 @@
|
|
| 1155 |
body: JSON.stringify({ model })
|
| 1156 |
});
|
| 1157 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1158 |
const data = await res.json();
|
| 1159 |
if (data.status === 'success' || data.status === 'already_running') {
|
| 1160 |
currentProvider = provider;
|
|
|
|
| 1155 |
body: JSON.stringify({ model })
|
| 1156 |
});
|
| 1157 |
|
| 1158 |
+
if (!res.ok) {
|
| 1159 |
+
const text = await res.text();
|
| 1160 |
+
try {
|
| 1161 |
+
const err = JSON.parse(text);
|
| 1162 |
+
throw new Error(err.detail || err.message || 'Unknown error');
|
| 1163 |
+
} catch (e) {
|
| 1164 |
+
throw new Error(`Failed to start terminal: ${res.status} ${res.statusText}`);
|
| 1165 |
+
}
|
| 1166 |
+
}
|
| 1167 |
+
|
| 1168 |
const data = await res.json();
|
| 1169 |
if (data.status === 'success' || data.status === 'already_running') {
|
| 1170 |
currentProvider = provider;
|
supabase_proxies.sql
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- ============================================
|
| 2 |
+
-- K-AI API Gateway - IP/Proxy Management SQL
|
| 3 |
+
-- ============================================
|
| 4 |
+
-- This script creates tables for managing multiple proxy IPs
|
| 5 |
+
|
| 6 |
+
-- ============================================
|
| 7 |
+
-- Create kaiapi_proxies table for IP management
|
| 8 |
+
-- ============================================
|
| 9 |
+
CREATE TABLE IF NOT EXISTS kaiapi_proxies (
|
| 10 |
+
id SERIAL PRIMARY KEY,
|
| 11 |
+
name VARCHAR(100), -- Optional friendly name (e.g., "USA Proxy 1")
|
| 12 |
+
ip VARCHAR(255) NOT NULL,
|
| 13 |
+
port INTEGER NOT NULL,
|
| 14 |
+
protocol VARCHAR(20) DEFAULT 'http',
|
| 15 |
+
username VARCHAR(255), -- Optional auth
|
| 16 |
+
password VARCHAR(255), -- Optional auth
|
| 17 |
+
country VARCHAR(100),
|
| 18 |
+
city VARCHAR(100),
|
| 19 |
+
is_active BOOLEAN NOT NULL DEFAULT false,
|
| 20 |
+
is_default BOOLEAN NOT NULL DEFAULT false, -- Only one can be default
|
| 21 |
+
last_tested TIMESTAMP WITH TIME ZONE,
|
| 22 |
+
is_working BOOLEAN DEFAULT true,
|
| 23 |
+
response_time_ms INTEGER,
|
| 24 |
+
fail_count INTEGER DEFAULT 0,
|
| 25 |
+
success_count INTEGER DEFAULT 0,
|
| 26 |
+
notes TEXT, -- User notes about this proxy
|
| 27 |
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
| 28 |
+
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
-- Create index for faster lookups
|
| 32 |
+
CREATE INDEX IF NOT EXISTS idx_kaiapi_proxies_active ON kaiapi_proxies(is_active);
|
| 33 |
+
CREATE INDEX IF NOT EXISTS idx_kaiapi_proxies_default ON kaiapi_proxies(is_default) WHERE is_default = true;
|
| 34 |
+
CREATE INDEX IF NOT EXISTS idx_kaiapi_proxies_working ON kaiapi_proxies(is_working);
|
| 35 |
+
|
| 36 |
+
-- ============================================
|
| 37 |
+
-- Insert sample proxies (optional examples)
|
| 38 |
+
-- ============================================
|
| 39 |
+
-- Uncomment to add sample data:
|
| 40 |
+
-- INSERT INTO kaiapi_proxies (name, ip, port, protocol, country, is_active, notes) VALUES
|
| 41 |
+
-- ('US Proxy 1', '192.168.1.100', 8080, 'http', 'United States', true, 'Main proxy'),
|
| 42 |
+
-- ('UK Proxy 1', '10.0.0.50', 3128, 'http', 'United Kingdom', false, 'Backup');
|
| 43 |
+
|
| 44 |
+
-- ============================================
|
| 45 |
+
-- Create trigger to ensure only one default proxy
|
| 46 |
+
-- ============================================
|
| 47 |
+
CREATE OR REPLACE FUNCTION ensure_single_default_proxy()
|
| 48 |
+
RETURNS TRIGGER AS $$
|
| 49 |
+
BEGIN
|
| 50 |
+
IF NEW.is_default = true THEN
|
| 51 |
+
-- Set all other proxies to not default
|
| 52 |
+
UPDATE kaiapi_proxies SET is_default = false WHERE id != NEW.id;
|
| 53 |
+
END IF;
|
| 54 |
+
RETURN NEW;
|
| 55 |
+
END;
|
| 56 |
+
$$ LANGUAGE plpgsql;
|
| 57 |
+
|
| 58 |
+
DROP TRIGGER IF EXISTS trigger_single_default_proxy ON kaiapi_proxies;
|
| 59 |
+
CREATE TRIGGER trigger_single_default_proxy
|
| 60 |
+
BEFORE INSERT OR UPDATE ON kaiapi_proxies
|
| 61 |
+
FOR EACH ROW
|
| 62 |
+
EXECUTE FUNCTION ensure_single_default_proxy();
|
| 63 |
+
|
| 64 |
+
-- ============================================
|
| 65 |
+
-- Create updated_at trigger
|
| 66 |
+
-- ============================================
|
| 67 |
+
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
| 68 |
+
RETURNS TRIGGER AS $$
|
| 69 |
+
BEGIN
|
| 70 |
+
NEW.updated_at = NOW();
|
| 71 |
+
RETURN NEW;
|
| 72 |
+
END;
|
| 73 |
+
$$ LANGUAGE 'plpgsql';
|
| 74 |
+
|
| 75 |
+
DROP TRIGGER IF EXISTS update_kaiapi_proxies_updated_at ON kaiapi_proxies;
|
| 76 |
+
CREATE TRIGGER update_kaiapi_proxies_updated_at
|
| 77 |
+
BEFORE UPDATE ON kaiapi_proxies
|
| 78 |
+
FOR EACH ROW
|
| 79 |
+
EXECUTE FUNCTION update_updated_at_column();
|
| 80 |
+
|
| 81 |
+
-- ============================================
|
| 82 |
+
-- Useful Queries
|
| 83 |
+
-- ============================================
|
| 84 |
+
|
| 85 |
+
-- Get all active proxies:
|
| 86 |
+
-- SELECT * FROM kaiapi_proxies WHERE is_active = true ORDER BY created_at DESC;
|
| 87 |
+
|
| 88 |
+
-- Get default proxy:
|
| 89 |
+
-- SELECT * FROM kaiapi_proxies WHERE is_default = true LIMIT 1;
|
| 90 |
+
|
| 91 |
+
-- Activate a proxy:
|
| 92 |
+
-- UPDATE kaiapi_proxies SET is_active = true WHERE id = 1;
|
| 93 |
+
|
| 94 |
+
-- Deactivate a proxy:
|
| 95 |
+
-- UPDATE kaiapi_proxies SET is_active = false WHERE id = 1;
|
| 96 |
+
|
| 97 |
+
-- Delete a proxy:
|
| 98 |
+
-- DELETE FROM kaiapi_proxies WHERE id = 1;
|
| 99 |
+
|
| 100 |
+
-- Mark proxy as tested:
|
| 101 |
+
-- UPDATE kaiapi_proxies SET
|
| 102 |
+
-- last_tested = NOW(),
|
| 103 |
+
-- is_working = true,
|
| 104 |
+
-- response_time_ms = 500,
|
| 105 |
+
-- success_count = success_count + 1
|
| 106 |
+
-- WHERE id = 1;
|
| 107 |
+
|
| 108 |
+
-- Mark proxy as failed:
|
| 109 |
+
-- UPDATE kaiapi_proxies SET
|
| 110 |
+
-- last_tested = NOW(),
|
| 111 |
+
-- is_working = false,
|
| 112 |
+
-- fail_count = fail_count + 1
|
| 113 |
+
-- WHERE id = 1;
|
| 114 |
+
|
| 115 |
+
-- Get proxy statistics:
|
| 116 |
+
-- SELECT
|
| 117 |
+
-- COUNT(*) as total,
|
| 118 |
+
-- COUNT(*) FILTER (WHERE is_active) as active,
|
| 119 |
+
-- COUNT(*) FILTER (WHERE is_working) as working,
|
| 120 |
+
-- COUNT(*) FILTER (WHERE is_default) as default_proxy
|
| 121 |
+
-- FROM kaiapi_proxies;
|
| 122 |
+
|
| 123 |
+
-- ============================================
|
| 124 |
+
-- Verification
|
| 125 |
+
-- ============================================
|
| 126 |
+
SELECT 'kaiapi_proxies table created successfully' as message;
|
| 127 |
+
SELECT column_name, data_type
|
| 128 |
+
FROM information_schema.columns
|
| 129 |
+
WHERE table_name = 'kaiapi_proxies'
|
| 130 |
+
ORDER BY ordinal_position;
|