KiWA001 commited on
Commit
02cc5cb
ยท
1 Parent(s): 989ca1a

Add AWS deployment setup with GitHub Actions auto-deploy

Browse files

- Add GitHub Actions workflow for automatic EC2 deployment
- Add disposable mode for OpenCode (auto-reset after 20 messages)
- Add anonymous mode (no credentials stored)
- Add AWS deployment scripts
- Add disposable mode status bar to admin portal
- Add API endpoints for manual reset and status checking
- Update documentation for AWS deployment

This enables:
- Automatic deployment from GitHub to AWS EC2 on every push
- OpenCode runs anonymously without login
- Auto-resets after 20 messages with fresh device identity
- Complete cleanup of all traces between sessions

.github/workflows/deploy.yml ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Deploy to AWS EC2
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+
7
+ jobs:
8
+ deploy:
9
+ runs-on: ubuntu-latest
10
+
11
+ steps:
12
+ - name: Checkout code
13
+ uses: actions/checkout@v3
14
+
15
+ - name: Deploy to EC2
16
+ env:
17
+ PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
18
+ HOST: ${{ secrets.EC2_HOST }}
19
+ USER: ${{ secrets.EC2_USER }}
20
+ run: |
21
+ echo "$PRIVATE_KEY" > private_key.pem
22
+ chmod 600 private_key.pem
23
+
24
+ # Create deployment script
25
+ cat > deploy.sh << 'EOF'
26
+ #!/bin/bash
27
+ set -e
28
+
29
+ cd ~/kai-api
30
+
31
+ # Backup current state
32
+ echo "Creating backup..."
33
+ cp -r . ../kai-api-backup-$(date +%Y%m%d-%H%M%S) 2>/dev/null || true
34
+
35
+ # Pull latest code
36
+ echo "Pulling latest code..."
37
+ git fetch origin
38
+ git reset --hard origin/main
39
+
40
+ # Activate virtual environment
41
+ source venv/bin/activate
42
+
43
+ # Install any new dependencies
44
+ echo "Installing dependencies..."
45
+ pip install -r requirements.txt
46
+
47
+ # Restart the server
48
+ echo "Restarting server..."
49
+ pkill -f uvicorn 2>/dev/null || true
50
+ sleep 2
51
+
52
+ # Start server in background
53
+ nohup ./venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 > /tmp/kai-api.log 2>&1 &
54
+
55
+ echo "โœ… Deployment complete!"
56
+ echo "๐ŸŒ API: http://$(curl -s ifconfig.me):8000"
57
+ EOF
58
+
59
+ chmod +x deploy.sh
60
+
61
+ # Copy and execute deployment script on EC2
62
+ scp -i private_key.pem -o StrictHostKeyChecking=no deploy.sh $USER@$HOST:/tmp/
63
+ ssh -i private_key.pem -o StrictHostKeyChecking=no $USER@$HOST 'bash /tmp/deploy.sh'
64
+
65
+ rm -f private_key.pem deploy.sh
66
+
67
+ - name: Verify deployment
68
+ env:
69
+ HOST: ${{ secrets.EC2_HOST }}
70
+ run: |
71
+ echo "Waiting for server to start..."
72
+ sleep 5
73
+
74
+ # Check if server is responding
75
+ if curl -s http://$HOST:8000/health > /dev/null 2>&1; then
76
+ echo "โœ… Deployment successful! Server is running."
77
+ echo "๐ŸŒ API: http://$HOST:8000"
78
+ echo "๐Ÿ”ง Admin: http://$HOST:8000/qazmlp"
79
+ else
80
+ echo "โš ๏ธ Server might still be starting. Check manually in a minute."
81
+ echo "๐ŸŒ API: http://$HOST:8000"
82
+ fi
AWS_DEPLOYMENT.md ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AWS Deployment Guide for K-AI API
2
+
3
+ This guide will help you deploy the K-AI API to AWS EC2 with OpenCode in **Anonymous + Disposable Mode**.
4
+
5
+ ## Overview
6
+
7
+ - **Platform**: AWS EC2 (Ubuntu 22.04 LTS)
8
+ - **Instance Type**: t3.medium minimum (2 vCPU, 4GB RAM)
9
+ - **Port**: 8000 (HTTP)
10
+ - **Mode**: Anonymous + Disposable (auto-reset after 20 messages)
11
+
12
+ ## Key Features
13
+
14
+ ### ๐Ÿ”’ Anonymous Mode
15
+ - No login required
16
+ - No credentials stored anywhere
17
+ - Fresh device identity for each session
18
+ - Random device/session IDs generated on startup
19
+
20
+ ### ๐Ÿ—‘๏ธ Disposable Mode
21
+ - **Auto-reset after 20 messages** - Complete wipe and fresh start
22
+ - **New chat between each message** - No context carryover
23
+ - **Aggressive cleanup** - Removes ALL traces (config files, cache, temp files)
24
+ - **Appears as new device** - OpenCode sees a completely different device
25
+
26
+ ### ๐Ÿงน What Gets Cleaned
27
+ - All config directories
28
+ - All cache and temp files
29
+ - NPM/npx cache
30
+ - Session directories
31
+ - Any stored identifiers
32
+
33
+ ## Prerequisites
34
+
35
+ 1. **AWS Account** with EC2 access
36
+ 2. **SSH Key Pair** created in AWS
37
+ 3. **Local terminal** with SSH access
38
+
39
+ ## Step 1: Create EC2 Instance
40
+
41
+ 1. Go to AWS Console โ†’ EC2 โ†’ Launch Instance
42
+ 2. **Name**: `kai-api-server`
43
+ 3. **AMI**: Ubuntu 22.04 LTS (64-bit)
44
+ 4. **Instance Type**: t3.medium or larger
45
+ 5. **Key Pair**: Select your existing key pair or create new
46
+ 6. **Network Settings**:
47
+ - Create security group
48
+ - Allow SSH (port 22) from your IP
49
+ - Allow HTTP (port 80) from anywhere
50
+ - Allow Custom TCP (port 8000) from anywhere
51
+
52
+ 7. **Storage**: 20 GB gp3 (minimum)
53
+ 8. Click **Launch Instance**
54
+
55
+ ## Step 2: Get Your EC2 Details
56
+
57
+ Once the instance is running, note down:
58
+ - **Public IPv4 address** (e.g., `54.123.456.789`)
59
+ - **Key pair file path** on your local machine (e.g., `~/Downloads/kai-api-key.pem`)
60
+
61
+ ## Step 3: Deploy to AWS (Easy Method)
62
+
63
+ We provide a simple deployment script. Run this from your local machine:
64
+
65
+ ```bash
66
+ # Make scripts executable
67
+ chmod +x deploy-to-aws.sh aws-setup.sh start-production.sh run-daemon.sh
68
+
69
+ # Run deployment
70
+ ./deploy-to-aws.sh -i ~/Downloads/kai-api-key.pem -h 54.123.456.789 -u ubuntu
71
+ ```
72
+
73
+ This will:
74
+ 1. Upload all files to your EC2 instance
75
+ 2. Run the setup automatically
76
+ 3. Install all dependencies
77
+ 4. Start the server
78
+
79
+ ## Step 4: Access Your API
80
+
81
+ Once deployed:
82
+ - **API**: http://YOUR_EC2_IP:8000
83
+ - **Admin Portal**: http://YOUR_EC2_IP:8000/qazmlp
84
+ - **API Docs**: http://YOUR_EC2_IP:8000/docs
85
+
86
+ ## Managing the Server
87
+
88
+ SSH into your EC2 instance:
89
+ ```bash
90
+ ssh -i ~/Downloads/kai-api-key.pem ubuntu@54.123.456.789
91
+ ```
92
+
93
+ ### Server Commands
94
+
95
+ ```bash
96
+ cd ~/kai-api
97
+
98
+ # Start in foreground (good for testing)
99
+ ./start-production.sh
100
+
101
+ # Run as background daemon
102
+ ./run-daemon.sh start
103
+
104
+ # Check status
105
+ ./run-daemon.sh status
106
+
107
+ # View logs
108
+ ./run-daemon.sh logs
109
+
110
+ # Stop server
111
+ ./run-daemon.sh stop
112
+
113
+ # Restart server
114
+ ./run-daemon.sh restart
115
+ ```
116
+
117
+ ## Using OpenCode in Disposable Mode
118
+
119
+ ### From the Admin Portal (/qazmlp):
120
+
121
+ 1. Go to **Browser Portal** section
122
+ 2. Select **"๐Ÿ–ฅ๏ธ OpenCode (Kimi K2.5 Free)"** from dropdown
123
+ 3. Click **Connect**
124
+ 4. You'll see the **Disposable Mode Status Bar** showing:
125
+ - Message count (0/20)
126
+ - Progress bar
127
+ - Reset timer
128
+ - Manual reset button
129
+
130
+ ### Disposable Mode Features:
131
+
132
+ โœ… **Automatic**:
133
+ - New chat starts before EVERY message (no context carryover)
134
+ - After 20 messages: Complete reset automatically
135
+ - Fresh device identity generated
136
+ - All traces wiped clean
137
+
138
+ โœ… **Manual Control**:
139
+ - Click **"๐Ÿ”„ Reset Now"** anytime to force a reset
140
+ - Status bar shows messages remaining
141
+ - Visual warning when approaching reset (last 5 messages)
142
+
143
+ ### What Happens During Reset:
144
+
145
+ 1. Current session closed
146
+ 2. ALL files cleaned up:
147
+ - Config files
148
+ - Cache files
149
+ - Temp files
150
+ - Session directories
151
+ - NPM cache
152
+ 3. Fresh identity generated
153
+ 4. New session started
154
+ 5. OpenCode sees a **completely new device**
155
+
156
+ ### API Endpoints for Disposable Mode:
157
+
158
+ ```bash
159
+ # Check disposable status
160
+ curl "http://YOUR_EC2_IP:8000/qaz/terminal/status?model=kimi-k2.5-free"
161
+
162
+ # Response:
163
+ # {
164
+ # "status": "success",
165
+ # "data": {
166
+ # "message_count": 5,
167
+ # "max_messages": 20,
168
+ # "messages_remaining": 15,
169
+ # "auto_reset_enabled": true,
170
+ # "new_chat_between_messages": true,
171
+ # "is_running": true,
172
+ # "anonymous_mode": true
173
+ # }
174
+ # }
175
+
176
+ # Manual reset (wipe everything and start fresh)
177
+ curl -X POST http://YOUR_EC2_IP:8000/qaz/terminal/reset \
178
+ -H "Content-Type: application/json" \
179
+ -d '{"model": "kimi-k2.5-free"}'
180
+
181
+ # Response:
182
+ # {
183
+ # "status": "success",
184
+ # "message": "Full disposable reset completed - OpenCode sees a brand new device!",
185
+ # "model": "kimi-k2.5-free"
186
+ # }
187
+ ```
188
+
189
+ ## How It Works
190
+
191
+ ### Message Flow:
192
+ 1. User sends message
193
+ 2. System starts **new chat** (clears context)
194
+ 3. Message sent to OpenCode
195
+ 4. Response received
196
+ 5. Message counter incremented
197
+ 6. If counter >= 20 โ†’ **Full reset triggered**
198
+
199
+ ### Reset Process:
200
+ ```
201
+ [20 messages reached]
202
+ โ†“
203
+ [Close session]
204
+ โ†“
205
+ [Delete ALL files]
206
+ โ†“
207
+ [Clear ALL caches]
208
+ โ†“
209
+ [Generate new identity]
210
+ โ†“
211
+ [Start fresh session]
212
+ โ†“
213
+ [OpenCode sees new device]
214
+ ```
215
+
216
+ ## Security & Privacy
217
+
218
+ ๐Ÿ”’ **Anonymous Mode Guarantees**:
219
+ - No credentials stored
220
+ - No login required
221
+ - No persistent identifiers
222
+ - Fresh identity each session
223
+
224
+ ๐Ÿ—‘๏ธ **Disposable Mode Guarantees**:
225
+ - All traces removed after reset
226
+ - No tracking possible between sessions
227
+ - Context never carries over
228
+ - Appears as completely different device
229
+
230
+ ## Available Free Models
231
+
232
+ All models work in disposable mode:
233
+
234
+ - **kimi-k2.5-free** (Kimi K2.5 Free)
235
+ - **minimax-m2.5-free** (MiniMax M2.5 Free)
236
+ - **big-pickle** (Big Pickle)
237
+ - **glm-4.7** (GLM 4.7)
238
+
239
+ ## Troubleshooting
240
+
241
+ ### Can't connect to EC2:
242
+ ```bash
243
+ chmod 600 ~/Downloads/kai-api-key.pem
244
+ ```
245
+
246
+ ### Server not starting:
247
+ ```bash
248
+ ./run-daemon.sh logs
249
+ # Or run in foreground
250
+ ./start-production.sh
251
+ ```
252
+
253
+ ### OpenCode not resetting:
254
+ - Check status: `curl "http://IP:8000/qaz/terminal/status"`
255
+ - Manual reset: Use the "๐Ÿ”„ Reset Now" button
256
+ - Check logs: `./run-daemon.sh logs`
257
+
258
+ ### Too many resets happening?
259
+ - Default is 20 messages per reset
260
+ - You can change this in `opencode_terminal.py`:
261
+ ```python
262
+ MAX_MESSAGES_BEFORE_RESET = 20 # Change this number
263
+ ```
264
+
265
+ ## Cost Estimation
266
+
267
+ - **t3.medium**: ~$30/month (on-demand)
268
+ - **t3.large**: ~$60/month (on-demand)
269
+ - **Data transfer**: Variable
270
+
271
+ Use Reserved Instances for 30-60% savings.
272
+
273
+ ## Files You Can Modify
274
+
275
+ - `opencode_terminal.py` - Change disposable mode settings
276
+ - `aws-setup.sh` - Change EC2 setup
277
+ - `start-production.sh` - Change how server starts
278
+ - `run-daemon.sh` - Change daemon behavior
279
+
280
+ ## Need Help?
281
+
282
+ 1. Check logs: `./run-daemon.sh logs`
283
+ 2. Verify EC2 security group allows port 8000
284
+ 3. Ensure Node.js 18+ is installed
285
+ 4. Check disposable status endpoint
286
+
287
+ ---
288
+
289
+ ## Summary
290
+
291
+ Your OpenCode integration now:
292
+ โœ… Requires NO login
293
+ โœ… Stores NO credentials
294
+ โœ… Auto-resets every 20 messages
295
+ โœ… Starts new chat between messages
296
+ โœ… Wipes ALL traces on reset
297
+ โœ… Appears as new device to OpenCode
298
+
299
+ **You get fresh free tokens automatically with each reset!**
admin_router.py CHANGED
@@ -1383,3 +1383,46 @@ async def close_terminal(req: dict):
1383
  except Exception as e:
1384
  raise HTTPException(status_code=500, detail=str(e))
1385
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1383
  except Exception as e:
1384
  raise HTTPException(status_code=500, detail=str(e))
1385
 
1386
+
1387
+ @router.post("/terminal/reset")
1388
+ async def reset_terminal(req: dict):
1389
+ """Manually trigger a full disposable reset (wipes all traces and starts fresh)."""
1390
+ try:
1391
+ from opencode_terminal import get_terminal_manager
1392
+
1393
+ model = req.get("model", "kimi-k2.5-free")
1394
+ manager = get_terminal_manager()
1395
+ portal = manager.get_portal(model)
1396
+
1397
+ if not portal.is_running():
1398
+ raise HTTPException(status_code=400, detail="Terminal not running")
1399
+
1400
+ success = await portal.manual_reset()
1401
+
1402
+ return {
1403
+ "status": "success" if success else "error",
1404
+ "message": "Full disposable reset completed - OpenCode sees a brand new device!",
1405
+ "model": model
1406
+ }
1407
+ except Exception as e:
1408
+ raise HTTPException(status_code=500, detail=str(e))
1409
+
1410
+
1411
+ @router.get("/terminal/status")
1412
+ async def get_terminal_status(model: str = "kimi-k2.5-free"):
1413
+ """Get disposable mode status and message count."""
1414
+ try:
1415
+ from opencode_terminal import get_terminal_manager
1416
+
1417
+ manager = get_terminal_manager()
1418
+ portal = manager.get_portal(model)
1419
+
1420
+ status = portal.get_disposable_status()
1421
+
1422
+ return {
1423
+ "status": "success",
1424
+ "data": status
1425
+ }
1426
+ except Exception as e:
1427
+ raise HTTPException(status_code=500, detail=str(e))
1428
+
aws-setup.sh ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # AWS EC2 Deployment Script for K-AI API
3
+ # ======================================
4
+ # This script sets up the K-AI API on an AWS EC2 instance
5
+ # Run this on your EC2 instance after creating it
6
+
7
+ set -e # Exit on error
8
+
9
+ echo "๐Ÿš€ Starting K-AI API deployment on AWS EC2..."
10
+
11
+ # Update system
12
+ echo "๐Ÿ“ฆ Updating system packages..."
13
+ sudo apt-get update && sudo apt-get upgrade -y
14
+
15
+ # Install essential packages
16
+ echo "๐Ÿ”ง Installing dependencies..."
17
+ sudo apt-get install -y \
18
+ git \
19
+ curl \
20
+ wget \
21
+ build-essential \
22
+ python3 \
23
+ python3-pip \
24
+ python3-venv \
25
+ nginx \
26
+ certbot \
27
+ python3-certbot-nginx \
28
+ software-properties-common \
29
+ apt-transport-https \
30
+ ca-certificates \
31
+ gnupg \
32
+ lsb-release
33
+
34
+ # Install Node.js 18.x (required for OpenCode)
35
+ echo "โฌ‡๏ธ Installing Node.js..."
36
+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
37
+ sudo apt-get install -y nodejs
38
+
39
+ # Verify installations
40
+ echo "โœ… Checking installations..."
41
+ python3 --version
42
+ node --version
43
+ npm --version
44
+
45
+ # Install Docker
46
+ echo "๐Ÿณ Installing Docker..."
47
+ sudo apt-get install -y docker.io docker-compose
48
+ sudo systemctl start docker
49
+ sudo systemctl enable docker
50
+ sudo usermod -aG docker $USER
51
+
52
+ # Install Playwright dependencies (for browser portals)
53
+ echo "๐ŸŽญ Installing Playwright dependencies..."
54
+ sudo apt-get install -y \
55
+ libnss3 \
56
+ libatk-bridge2.0-0 \
57
+ libxss1 \
58
+ libgtk-3-0 \
59
+ libgbm1 \
60
+ libasound2
61
+
62
+ # Create app directory
63
+ echo "๐Ÿ“ Creating app directory..."
64
+ mkdir -p ~/kai-api
65
+ cd ~/kai-api
66
+
67
+ # Clone your repository (you'll need to replace this URL)
68
+ echo "๐Ÿ“ฅ Cloning repository..."
69
+ # git clone https://github.com/YOUR_USERNAME/KAI_API.git .
70
+ # OR if using SCP/SFTP to upload files:
71
+ echo "โณ Waiting for application files..."
72
+ echo "Please upload your K-AI API files to ~/kai-api/"
73
+ echo "You can use: scp -r /local/path/to/KAI_API/* ubuntu@YOUR_EC2_IP:~/kai-api/"
74
+
75
+ # Create a flag file to indicate setup is complete
76
+ touch ~/kai-api/.setup-complete
77
+
78
+ echo ""
79
+ echo "=========================================="
80
+ echo "โœ… EC2 Setup Complete!"
81
+ echo "=========================================="
82
+ echo ""
83
+ echo "Next steps:"
84
+ echo "1. Upload your K-AI API files to ~/kai-api/"
85
+ echo "2. Run: cd ~/kai-api && ./start-production.sh"
86
+ echo ""
87
+ echo "Your EC2 instance is ready!"
deploy-to-aws.sh ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # AWS Deployment Script - Automatic Upload and Setup
3
+ # ==================================================
4
+ # This script uploads files to EC2 and sets everything up automatically
5
+
6
+ set -e # Exit on error
7
+
8
+ # Configuration
9
+ EC2_IP="44.201.146.74"
10
+ EC2_USER="ubuntu"
11
+ KEY_PATH="" # Will be set interactively
12
+ REMOTE_DIR="/home/ubuntu/kai-api"
13
+
14
+ # Colors for output
15
+ RED='\033[0;31m'
16
+ GREEN='\033[0;32m'
17
+ YELLOW='\033[1;33m'
18
+ BLUE='\033[0;34m'
19
+ NC='\033[0m' # No Color
20
+
21
+ print_status() {
22
+ echo -e "${BLUE}[INFO]${NC} $1"
23
+ }
24
+
25
+ print_success() {
26
+ echo -e "${GREEN}[SUCCESS]${NC} $1"
27
+ }
28
+
29
+ print_error() {
30
+ echo -e "${RED}[ERROR]${NC} $1"
31
+ }
32
+
33
+ print_warning() {
34
+ echo -e "${YELLOW}[WARNING]${NC} $1"
35
+ }
36
+
37
+ # Check if running from KAI_API directory
38
+ if [ ! -f "main.py" ] || [ ! -d "providers" ]; then
39
+ print_error "Please run this script from the KAI_API directory"
40
+ exit 1
41
+ fi
42
+
43
+ # Get SSH key path
44
+ read -p "Enter path to your .pem key file (e.g., ~/Downloads/kai-api-key.pem): " KEY_PATH
45
+ KEY_PATH="${KEY_PATH/#\~/$HOME}" # Expand ~ to home directory
46
+
47
+ if [ ! -f "$KEY_PATH" ]; then
48
+ print_error "Key file not found: $KEY_PATH"
49
+ exit 1
50
+ fi
51
+
52
+ # Fix key permissions
53
+ chmod 600 "$KEY_PATH"
54
+ print_success "Key permissions set to 600"
55
+
56
+ print_status "Starting deployment to EC2 instance: $EC2_IP"
57
+ print_status "This will upload files and set up the server automatically..."
58
+
59
+ echo ""
60
+ read -p "Press Enter to continue..."
61
+
62
+ # Step 1: Test SSH connection
63
+ print_status "Testing SSH connection..."
64
+ if ssh -i "$KEY_PATH" -o ConnectTimeout=10 -o StrictHostKeyChecking=no "$EC2_USER@$EC2_IP" "echo 'SSH connection successful'" 2>/dev/null; then
65
+ print_success "SSH connection successful"
66
+ else
67
+ print_error "Cannot connect to EC2 instance. Please check:"
68
+ print_error " 1. Instance is running"
69
+ print_error " 2. Security group allows port 22 from your IP"
70
+ print_error " 3. Key file is correct"
71
+ exit 1
72
+ fi
73
+
74
+ # Step 2: Create remote directory
75
+ print_status "Creating remote directory..."
76
+ ssh -i "$KEY_PATH" "$EC2_USER@$EC2_IP" "mkdir -p $REMOTE_DIR"
77
+
78
+ # Step 3: Upload files
79
+ print_status "Uploading files to EC2 (this may take a few minutes)..."
80
+ print_status "Uploading application files..."
81
+
82
+ # Create a list of files to upload (excluding unnecessary files)
83
+ cat > /tmp/upload_list.txt << 'EOF'
84
+ main.py
85
+ config.py
86
+ models.py
87
+ engine.py
88
+ services.py
89
+ db.py
90
+ auth.py
91
+ v1_router.py
92
+ admin_router.py
93
+ copilot_portal.py
94
+ copilot_session.py
95
+ browser_portal.py
96
+ opencode_terminal.py
97
+ proxy_manager.py
98
+ provider_state.py
99
+ provider_sessions.py
100
+ search_engine.py
101
+ error_handling.py
102
+ utils.py
103
+ sanitizer.py
104
+ useragent.py
105
+ requirements.txt
106
+ aws-setup.sh
107
+ start-production.sh
108
+ run-daemon.sh
109
+ Dockerfile
110
+ EOF
111
+
112
+ # Upload the list and files
113
+ rsync -avz --progress \
114
+ -e "ssh -i $KEY_PATH" \
115
+ --files-from=/tmp/upload_list.txt \
116
+ . \
117
+ "$EC2_USER@$EC2_IP:$REMOTE_DIR/"
118
+
119
+ print_status "Uploading providers directory..."
120
+ rsync -avz --progress \
121
+ -e "ssh -i $KEY_PATH" \
122
+ providers/ \
123
+ "$EC2_USER@$EC2_IP:$REMOTE_DIR/providers/"
124
+
125
+ print_status "Uploading static directory..."
126
+ rsync -avz --progress \
127
+ -e "ssh -i $KEY_PATH" \
128
+ static/ \
129
+ "$EC2_USER@$EC2_IP:$REMOTE_DIR/static/"
130
+
131
+ print_success "All files uploaded successfully!"
132
+
133
+ # Step 4: Run setup on EC2
134
+ print_status "Running setup on EC2 instance..."
135
+ ssh -i "$KEY_PATH" "$EC2_USER@$EC2_IP" << 'REMOTE_SCRIPT'
136
+
137
+ cd ~/kai-api
138
+
139
+ echo "=========================================="
140
+ echo "๐Ÿš€ Starting K-AI API Setup"
141
+ echo "=========================================="
142
+
143
+ # Update system
144
+ echo "๐Ÿ“ฆ Updating system packages..."
145
+ sudo apt-get update -y
146
+
147
+ # Install dependencies
148
+ echo "๐Ÿ”ง Installing dependencies..."
149
+ sudo apt-get install -y \
150
+ git curl wget build-essential \
151
+ python3 python3-pip python3-venv \
152
+ nginx certbot python3-certbot-nginx \
153
+ software-properties-common \
154
+ apt-transport-https ca-certificates \
155
+ gnupg lsb-release
156
+
157
+ # Install Node.js 18.x
158
+ echo "โฌ‡๏ธ Installing Node.js..."
159
+ curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
160
+ sudo apt-get install -y nodejs
161
+
162
+ echo "โœ… Node.js version: $(node --version)"
163
+ echo "โœ… npm version: $(npm --version)"
164
+
165
+ # Install Docker
166
+ echo "๐Ÿณ Installing Docker..."
167
+ sudo apt-get install -y docker.io docker-compose
168
+ sudo systemctl start docker
169
+ sudo systemctl enable docker
170
+ sudo usermod -aG docker $USER
171
+
172
+ # Install Playwright dependencies
173
+ echo "๐ŸŽญ Installing Playwright dependencies..."
174
+ sudo apt-get install -y \
175
+ libnss3 libatk-bridge2.0-0 libxss1 \
176
+ libgtk-3-0 libgbm1 libasound2
177
+
178
+ # Create virtual environment
179
+ echo "๐Ÿ Setting up Python virtual environment..."
180
+ python3 -m venv venv
181
+ source venv/bin/activate
182
+
183
+ # Install Python dependencies
184
+ echo "๐Ÿ“ฅ Installing Python dependencies..."
185
+ pip install --upgrade pip
186
+ pip install -r requirements.txt
187
+
188
+ # Install Playwright browsers
189
+ echo "๐ŸŽญ Installing Playwright browsers..."
190
+ playwright install chromium
191
+
192
+ echo "=========================================="
193
+ echo "โœ… Setup Complete!"
194
+ echo "=========================================="
195
+ echo ""
196
+ echo "To start the server, run:"
197
+ echo " cd ~/kai-api && ./start-production.sh"
198
+ echo ""
199
+ echo "Or run as background daemon:"
200
+ echo " cd ~/kai-api && ./run-daemon.sh start"
201
+ echo ""
202
+
203
+ REMOTE_SCRIPT
204
+
205
+ print_success "Setup completed on EC2!"
206
+
207
+ # Step 5: Start the server
208
+ print_status "Starting the server..."
209
+ ssh -i "$KEY_PATH" "$EC2_USER@$EC2_IP" << 'REMOTE_SCRIPT'
210
+
211
+ cd ~/kai-api
212
+
213
+ # Kill any existing processes
214
+ pkill -f uvicorn 2>/dev/null || true
215
+
216
+ # Start the server in background
217
+ nohup ./venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 > /tmp/kai-api.log 2>&1 &
218
+
219
+ # Wait a moment for startup
220
+ sleep 5
221
+
222
+ # Check if it's running
223
+ if pgrep -f uvicorn > /dev/null; then
224
+ echo "โœ… Server is running!"
225
+ echo ""
226
+ echo "๐ŸŒ Your API is available at:"
227
+ echo " http://$(curl -s ifconfig.me):8000"
228
+ echo ""
229
+ echo "๐Ÿ”ง Admin Portal:"
230
+ echo " http://$(curl -s ifconfig.me):8000/qazmlp"
231
+ echo ""
232
+ echo "๐Ÿ“š API Docs:"
233
+ echo " http://$(curl -s ifconfig.me):8000/docs"
234
+ else
235
+ echo "โŒ Server failed to start. Check logs:"
236
+ echo " cat /tmp/kai-api.log"
237
+ fi
238
+
239
+ REMOTE_SCRIPT
240
+
241
+ echo ""
242
+ echo "=========================================="
243
+ print_success "DEPLOYMENT COMPLETE!"
244
+ echo "=========================================="
245
+ echo ""
246
+ echo "๐ŸŒ Your K-AI API is now running at:"
247
+ echo " http://$EC2_IP:8000"
248
+ echo ""
249
+ echo "๐Ÿ”ง Admin Portal (OpenCode Terminal):"
250
+ echo " http://$EC2_IP:8000/qazmlp"
251
+ echo ""
252
+ echo "๐Ÿ“š API Documentation:"
253
+ echo " http://$EC2_IP:8000/docs"
254
+ echo ""
255
+ echo "๐Ÿ“Š To check server status:"
256
+ echo " ssh -i $KEY_PATH $EC2_USER@$EC2_IP 'cd ~/kai-api && ./run-daemon.sh status'"
257
+ echo ""
258
+ echo "๐Ÿ“ To view logs:"
259
+ echo " ssh -i $KEY_PATH $EC2_USER@$EC2_IP 'tail -f /tmp/kai-api.log'"
260
+ echo ""
opencode_terminal.py CHANGED
@@ -3,6 +3,8 @@ OpenCode Terminal Portal
3
  -------------------------
4
  Manages OpenCode terminal TUI as a provider.
5
  Supports free models: Kimi K2.5 Free, MiniMax M2.5 Free, Big Pickle, GLM 4.7
 
 
6
  """
7
 
8
  import asyncio
@@ -10,11 +12,14 @@ import logging
10
  import subprocess
11
  import os
12
  import json
 
 
13
  from typing import Optional, Dict, Any, Callable
14
  from dataclasses import dataclass
15
  from datetime import datetime
16
  import threading
17
  import queue
 
18
 
19
  logger = logging.getLogger("kai_api.terminal_portal")
20
 
@@ -28,6 +33,15 @@ class TerminalConfig:
28
  config_path: str = ".opencode/config.json"
29
 
30
 
 
 
 
 
 
 
 
 
 
31
  # Free models configuration
32
  OPENCODE_MODELS = {
33
  "kimi-k2.5-free": {
@@ -54,7 +68,11 @@ OPENCODE_MODELS = {
54
 
55
 
56
  class OpenCodeTerminalPortal:
57
- """Manages OpenCode terminal TUI session."""
 
 
 
 
58
 
59
  def __init__(self, config: TerminalConfig):
60
  self.config = config
@@ -67,22 +85,49 @@ class OpenCodeTerminalPortal:
67
  self.screenshot_path = f"/tmp/opencode_{config.model}.png"
68
  self.last_activity = None
69
  self._keyboard_active = False
 
 
 
70
 
71
  async def initialize(self):
72
- """Initialize OpenCode terminal session."""
73
  if self.is_initialized:
74
  return
75
 
76
  try:
77
- # Restore auth from Supabase if available
78
- await self.restore_auth()
79
-
80
  logger.info(f"๐Ÿš€ Starting OpenCode terminal with model: {self.config.model}")
 
 
 
 
 
 
81
 
82
- # Ensure config directory exists
83
- os.makedirs(".opencode", exist_ok=True)
 
 
84
 
85
- # Create config file for this model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  config_data = {
87
  "$schema": "https://opencode.ai/config.json",
88
  "theme": "opencode",
@@ -100,40 +145,25 @@ class OpenCodeTerminalPortal:
100
  },
101
  "model": f"opencode-zen/{self.config.model}",
102
  "autoshare": False,
103
- "autoupdate": True
 
 
 
 
104
  }
105
 
106
- # Create config file for this model ONLY if it doesn't exist
107
- # This allows the user to run `npx opencode-ai` manually to login/auth
108
- if not os.path.exists(self.config.config_path):
109
- config_data = {
110
- "$schema": "https://opencode.ai/config.json",
111
- "theme": "opencode",
112
- "provider": {
113
- "opencode-zen": {
114
- "npm": "@ai-sdk/openai-compatible",
115
- "options": {
116
- "baseURL": "https://opencode.ai/zen/v1"
117
- },
118
- "models": {
119
- model: {"name": info["name"]}
120
- for model, info in OPENCODE_MODELS.items()
121
- }
122
- }
123
- },
124
- "model": f"opencode-zen/{self.config.model}",
125
- "autoshare": False,
126
- "autoupdate": True
127
- }
128
-
129
- with open(self.config.config_path, 'w') as f:
130
- json.dump(config_data, f, indent=2)
131
- else:
132
- logger.info(f"Using existing config at {self.config.config_path}")
133
 
134
- # Start OpenCode process
135
  env = os.environ.copy()
136
- env['OPENCODE_CONFIG'] = os.path.abspath(self.config.config_path)
 
 
 
 
137
 
138
  self.process = subprocess.Popen(
139
  ['npx', '-y', 'opencode-ai'],
@@ -141,7 +171,7 @@ class OpenCodeTerminalPortal:
141
  env=env,
142
  stdin=subprocess.PIPE,
143
  stdout=subprocess.PIPE,
144
- stderr=subprocess.PIPE,
145
  text=True,
146
  bufsize=1
147
  )
@@ -152,9 +182,31 @@ class OpenCodeTerminalPortal:
152
  self.output_thread.start()
153
 
154
  self.is_initialized = True
155
- logger.info(f"โœ… OpenCode terminal ready with {self.config.model}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
 
157
- await asyncio.sleep(2) # Give it time to start
158
  await self.take_screenshot()
159
 
160
  except Exception as e:
@@ -181,20 +233,138 @@ class OpenCodeTerminalPortal:
181
  except Exception as e:
182
  logger.error(f"Error reading output: {e}")
183
 
184
- async def send_input(self, text: str):
185
- """Send text input to OpenCode."""
186
  if not self.process or not self.is_initialized:
187
  return False
188
 
189
  try:
 
 
 
 
 
 
 
190
  self.process.stdin.write(text + '\n')
191
  self.process.stdin.flush()
192
  self.last_activity = datetime.now()
 
 
 
 
 
 
 
 
 
 
 
193
  return True
194
  except Exception as e:
195
  logger.error(f"Error sending input: {e}")
196
  return False
197
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  async def send_key(self, key: str):
199
  """Send a special key to OpenCode (e.g., 'ctrl+c', 'enter', 'tab')."""
200
  if not self.process or not self.is_initialized:
@@ -288,7 +458,7 @@ class OpenCodeTerminalPortal:
288
  return self.process.poll() is None
289
 
290
  async def close(self):
291
- """Close the OpenCode terminal."""
292
  try:
293
  if self.process:
294
  # Send exit command
@@ -309,74 +479,47 @@ class OpenCodeTerminalPortal:
309
  self.process = None
310
 
311
  self.is_initialized = False
312
- logger.info("OpenCode terminal closed")
313
-
314
- except Exception as e:
315
- logger.error(f"Error closing OpenCode: {e}")
316
-
317
- async def restore_auth(self):
318
- """Restore auth token from Supabase."""
319
- try:
320
- from db import get_supabase
321
- supabase = get_supabase()
322
- if not supabase:
323
- return
324
-
325
- # Check if we have auth data in Supabase
326
- res = supabase.table("kaiapi_settings").select("value").eq("key", "opencode_auth").execute()
327
- if not res.data:
328
- return
329
-
330
- auth_data = res.data[0]["value"]
331
 
332
- # Paths to check/restore
333
- paths = [
334
- os.path.expanduser("~/.local/share/opencode/auth.json"),
335
- # Fallback paths if needed
336
- ]
 
 
337
 
338
- for path in paths:
339
- os.makedirs(os.path.dirname(path), exist_ok=True)
340
- with open(path, 'w') as f:
341
- json.dump(auth_data, f, indent=2)
342
 
343
- logger.info("โœ… Restored OpenCode auth from Supabase")
344
 
345
  except Exception as e:
346
- logger.warning(f"Failed to restore auth from Supabase: {e}")
347
 
348
  async def sync_auth(self):
349
- """Sync local auth token to Supabase."""
350
- try:
351
- from db import get_supabase
352
- supabase = get_supabase()
353
- if not supabase:
354
- logger.error("Supabase not configured, cannot sync auth")
355
- return False
356
-
357
- # Find auth file
358
- auth_path = os.path.expanduser("~/.local/share/opencode/auth.json")
359
- if not os.path.exists(auth_path):
360
- logger.warning(f"Auth file not found at {auth_path}")
361
- return False
362
-
363
- with open(auth_path, 'r') as f:
364
- auth_data = json.load(f)
365
-
366
- # Save to Supabase
367
- supabase.table("kaiapi_settings").upsert({
368
- "key": "opencode_auth",
369
- "value": auth_data,
370
- "updated_at": datetime.now().isoformat()
371
- }).execute()
372
-
373
- logger.info("โœ… Synced OpenCode auth to Supabase")
374
- return True
375
-
376
- except Exception as e:
377
- logger.error(f"Failed to sync auth to Supabase: {e}")
378
- return False
379
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
 
382
  class TerminalPortalManager:
 
3
  -------------------------
4
  Manages OpenCode terminal TUI as a provider.
5
  Supports free models: Kimi K2.5 Free, MiniMax M2.5 Free, Big Pickle, GLM 4.7
6
+
7
+ ANONYMOUS MODE: No credentials stored, fresh device identity each session
8
  """
9
 
10
  import asyncio
 
12
  import subprocess
13
  import os
14
  import json
15
+ import random
16
+ import string
17
  from typing import Optional, Dict, Any, Callable
18
  from dataclasses import dataclass
19
  from datetime import datetime
20
  import threading
21
  import queue
22
+ import shutil
23
 
24
  logger = logging.getLogger("kai_api.terminal_portal")
25
 
 
33
  config_path: str = ".opencode/config.json"
34
 
35
 
36
+ def generate_anonymous_identity():
37
+ """Generate random device identifiers to appear as different device each time."""
38
+ return {
39
+ "device_id": ''.join(random.choices(string.hexdigits.lower(), k=32)),
40
+ "session_id": ''.join(random.choices(string.hexdigits.lower(), k=16)),
41
+ "fingerprint": ''.join(random.choices(string.hexdigits.lower(), k=24)),
42
+ }
43
+
44
+
45
  # Free models configuration
46
  OPENCODE_MODELS = {
47
  "kimi-k2.5-free": {
 
68
 
69
 
70
  class OpenCodeTerminalPortal:
71
+ """Manages OpenCode terminal TUI session with DISPOSABLE mode."""
72
+
73
+ # Disposable mode settings
74
+ MAX_MESSAGES_BEFORE_RESET = 20 # Auto-reset after 20 messages
75
+ AUTO_NEW_CHAT_BETWEEN_MESSAGES = True # Start new chat between each message
76
 
77
  def __init__(self, config: TerminalConfig):
78
  self.config = config
 
85
  self.screenshot_path = f"/tmp/opencode_{config.model}.png"
86
  self.last_activity = None
87
  self._keyboard_active = False
88
+ self.message_count = 0 # Track messages for auto-reset
89
+ self.current_identity = None # Track current session identity
90
+ self.session_dir = None # Track current session directory
91
 
92
  async def initialize(self):
93
+ """Initialize OpenCode terminal session in ANONYMOUS/DISPOSABLE mode."""
94
  if self.is_initialized:
95
  return
96
 
97
  try:
 
 
 
98
  logger.info(f"๐Ÿš€ Starting OpenCode terminal with model: {self.config.model}")
99
+ logger.info("๐Ÿ”’ Anonymous mode: No credentials stored, fresh device identity")
100
+ logger.info("๐Ÿ—‘๏ธ Disposable mode: Auto-reset after 20 messages, new chat between messages")
101
+
102
+ # Generate fresh anonymous identity
103
+ self.current_identity = generate_anonymous_identity()
104
+ identity = self.current_identity
105
 
106
+ # Create isolated config directory for this session
107
+ self.session_dir = f"/tmp/opencode_session_{identity['session_id'][:8]}"
108
+ config_path = f"{self.session_dir}/config.json"
109
+ os.makedirs(self.session_dir, exist_ok=True)
110
 
111
+ # Remove any existing auth files to ensure anonymous mode
112
+ auth_paths = [
113
+ os.path.expanduser("~/.local/share/opencode/auth.json"),
114
+ os.path.expanduser("~/.local/share/opencode"),
115
+ ".opencode/auth.json",
116
+ "/tmp/opencode_auth.json"
117
+ ]
118
+ for path in auth_paths:
119
+ if os.path.exists(path):
120
+ try:
121
+ if os.path.isfile(path):
122
+ os.remove(path)
123
+ logger.info(f"๐Ÿ—‘๏ธ Removed auth file: {path}")
124
+ elif os.path.isdir(path):
125
+ shutil.rmtree(path)
126
+ logger.info(f"๐Ÿ—‘๏ธ Removed auth directory: {path}")
127
+ except Exception as e:
128
+ logger.warning(f"Could not remove {path}: {e}")
129
+
130
+ # Create anonymous config - NO login required
131
  config_data = {
132
  "$schema": "https://opencode.ai/config.json",
133
  "theme": "opencode",
 
145
  },
146
  "model": f"opencode-zen/{self.config.model}",
147
  "autoshare": False,
148
+ "autoupdate": True,
149
+ # Anonymous mode settings
150
+ "anonymous": True,
151
+ "deviceId": identity["device_id"],
152
+ "sessionId": identity["session_id"]
153
  }
154
 
155
+ with open(config_path, 'w') as f:
156
+ json.dump(config_data, f, indent=2)
157
+
158
+ logger.info(f"โœ… Config created at: {config_path}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
 
160
+ # Start OpenCode process with custom environment
161
  env = os.environ.copy()
162
+ env['OPENCODE_CONFIG'] = os.path.abspath(config_path)
163
+ # Set random terminal identifier
164
+ env['TERM_SESSION_ID'] = identity["session_id"]
165
+ # Prevent any auth persistence
166
+ env['OPENCODE_NO_AUTH'] = '1'
167
 
168
  self.process = subprocess.Popen(
169
  ['npx', '-y', 'opencode-ai'],
 
171
  env=env,
172
  stdin=subprocess.PIPE,
173
  stdout=subprocess.PIPE,
174
+ stderr=subprocess.STDOUT, # Merge stderr into stdout for easier reading
175
  text=True,
176
  bufsize=1
177
  )
 
182
  self.output_thread.start()
183
 
184
  self.is_initialized = True
185
+ logger.info(f"โœ… OpenCode terminal ready (Anonymous mode)")
186
+ logger.info(f"๐Ÿ†” Session ID: {identity['session_id'][:16]}...")
187
+
188
+ # Wait for startup and send initial commands to select free model
189
+ await asyncio.sleep(3)
190
+
191
+ # Auto-select the free model (Ctrl+X then M)
192
+ await self.send_key('ctrl+x')
193
+ await asyncio.sleep(0.5)
194
+ await self.send_input('m')
195
+ await asyncio.sleep(1)
196
+
197
+ # Select model based on config
198
+ model_map = {
199
+ "kimi-k2.5-free": "kimi",
200
+ "minimax-m2.5-free": "minimax",
201
+ "big-pickle": "big pickle",
202
+ "glm-4.7": "glm"
203
+ }
204
+ model_keyword = model_map.get(self.config.model, "kimi")
205
+
206
+ # Send enter to select default (usually Kimi)
207
+ await self.send_key('Enter')
208
+ await asyncio.sleep(0.5)
209
 
 
210
  await self.take_screenshot()
211
 
212
  except Exception as e:
 
233
  except Exception as e:
234
  logger.error(f"Error reading output: {e}")
235
 
236
+ async def send_input(self, text: str, is_message: bool = True):
237
+ """Send text input to OpenCode with disposable mode handling."""
238
  if not self.process or not self.is_initialized:
239
  return False
240
 
241
  try:
242
+ # If this is a user message (not a command), handle disposable mode
243
+ if is_message and self.AUTO_NEW_CHAT_BETWEEN_MESSAGES:
244
+ # Start a new chat before sending the message
245
+ await self._start_new_chat()
246
+ await asyncio.sleep(0.5)
247
+
248
+ # Send the actual message
249
  self.process.stdin.write(text + '\n')
250
  self.process.stdin.flush()
251
  self.last_activity = datetime.now()
252
+
253
+ # Track message count for auto-reset
254
+ if is_message:
255
+ self.message_count += 1
256
+ logger.info(f"๐Ÿ“จ Message {self.message_count}/{self.MAX_MESSAGES_BEFORE_RESET}")
257
+
258
+ # Check if we need to auto-reset
259
+ if self.message_count >= self.MAX_MESSAGES_BEFORE_RESET:
260
+ logger.info("๐Ÿ”„ Auto-reset triggered after 20 messages!")
261
+ await self._full_reset()
262
+
263
  return True
264
  except Exception as e:
265
  logger.error(f"Error sending input: {e}")
266
  return False
267
 
268
+ async def _start_new_chat(self):
269
+ """Start a new chat to avoid context carryover."""
270
+ try:
271
+ # Send Ctrl+N for new chat (or equivalent command)
272
+ logger.info("๐Ÿ†• Starting new chat...")
273
+ self.process.stdin.write('\x0e') # Ctrl+N
274
+ self.process.stdin.flush()
275
+ await asyncio.sleep(0.3)
276
+ except Exception as e:
277
+ logger.warning(f"Could not start new chat: {e}")
278
+
279
+ async def _full_reset(self):
280
+ """Complete reset: wipe everything and start fresh."""
281
+ logger.info("๐Ÿงน PERFORMING FULL DISPOSABLE RESET...")
282
+
283
+ # 1. Close current session
284
+ await self.close()
285
+
286
+ # 2. Aggressive cleanup of ALL traces
287
+ await self._complete_cleanup()
288
+
289
+ # 3. Reset counters
290
+ self.message_count = 0
291
+ self.current_identity = None
292
+ self.session_dir = None
293
+
294
+ # 4. Wait a moment to ensure cleanup
295
+ await asyncio.sleep(2)
296
+
297
+ # 5. Reinitialize with fresh identity
298
+ logger.info("๐Ÿ”„ Starting fresh session...")
299
+ await self.initialize()
300
+
301
+ logger.info("โœ… Full reset complete - OpenCode sees a completely new device!")
302
+
303
+ async def _complete_cleanup(self):
304
+ """Complete cleanup of ALL OpenCode traces."""
305
+ cleanup_paths = [
306
+ # Config directories
307
+ "/tmp/opencode_session_*",
308
+ os.path.expanduser("~/.local/share/opencode"),
309
+ os.path.expanduser("~/.config/opencode"),
310
+ os.path.expanduser("~/.opencode"),
311
+ ".opencode",
312
+
313
+ # Cache and temp files
314
+ os.path.expanduser("~/.cache/opencode"),
315
+ "/tmp/opencode*",
316
+ "/tmp/.opencode*",
317
+
318
+ # Node/npm cache that might have identifiers
319
+ os.path.expanduser("~/.npm/_npx/*opencode*"),
320
+ os.path.expanduser("~/.npm/_logs/*opencode*"),
321
+
322
+ # Any auth files
323
+ os.path.expanduser("~/.local/share/opencode/auth.json"),
324
+ "/tmp/opencode_auth.json",
325
+ "/tmp/kai-opencode-*",
326
+ ]
327
+
328
+ for path_pattern in cleanup_paths:
329
+ try:
330
+ import glob
331
+ matching_paths = glob.glob(path_pattern)
332
+ for path in matching_paths:
333
+ if os.path.exists(path):
334
+ if os.path.isfile(path):
335
+ os.remove(path)
336
+ logger.info(f"๐Ÿ—‘๏ธ Deleted file: {path}")
337
+ elif os.path.isdir(path):
338
+ shutil.rmtree(path, ignore_errors=True)
339
+ logger.info(f"๐Ÿ—‘๏ธ Deleted directory: {path}")
340
+ except Exception as e:
341
+ logger.warning(f"Cleanup warning for {path_pattern}: {e}")
342
+
343
+ # Clear npm/npx cache
344
+ try:
345
+ subprocess.run(["npm", "cache", "clean", "--force"],
346
+ capture_output=True, timeout=10)
347
+ logger.info("๐Ÿงน NPM cache cleared")
348
+ except Exception as e:
349
+ logger.warning(f"Could not clear NPM cache: {e}")
350
+
351
+ # Clear any system-level temporary identifiers
352
+ try:
353
+ # Clear /tmp of any opencode related files
354
+ import glob
355
+ for f in glob.glob("/tmp/*opencode*"):
356
+ try:
357
+ if os.path.isfile(f):
358
+ os.remove(f)
359
+ elif os.path.isdir(f):
360
+ shutil.rmtree(f, ignore_errors=True)
361
+ except:
362
+ pass
363
+ except:
364
+ pass
365
+
366
+ logger.info("๐Ÿงน Complete cleanup finished - No traces left!")
367
+
368
  async def send_key(self, key: str):
369
  """Send a special key to OpenCode (e.g., 'ctrl+c', 'enter', 'tab')."""
370
  if not self.process or not self.is_initialized:
 
458
  return self.process.poll() is None
459
 
460
  async def close(self):
461
+ """Close the OpenCode terminal and cleanup anonymous session."""
462
  try:
463
  if self.process:
464
  # Send exit command
 
479
  self.process = None
480
 
481
  self.is_initialized = False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
 
483
+ # Cleanup session directory to remove any traces
484
+ if self.session_dir and os.path.exists(self.session_dir):
485
+ try:
486
+ shutil.rmtree(self.session_dir, ignore_errors=True)
487
+ logger.info(f"๐Ÿงน Cleaned up session directory: {self.session_dir}")
488
+ except Exception as e:
489
+ logger.warning(f"Session cleanup warning: {e}")
490
 
491
+ # Reset message counter
492
+ self.message_count = 0
 
 
493
 
494
+ logger.info("OpenCode terminal closed (Anonymous session cleaned)")
495
 
496
  except Exception as e:
497
+ logger.error(f"Error closing OpenCode: {e}")
498
 
499
  async def sync_auth(self):
500
+ """DEPRECATED: Anonymous mode - no auth to sync."""
501
+ logger.warning("sync_auth() called but anonymous mode is active - no credentials stored")
502
+ return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
503
 
504
+ async def manual_reset(self):
505
+ """Manually trigger a full disposable reset."""
506
+ logger.info("๐Ÿ”„ Manual reset requested")
507
+ await self._full_reset()
508
+ return True
509
+
510
+ def get_disposable_status(self) -> dict:
511
+ """Get current disposable mode status."""
512
+ return {
513
+ "message_count": self.message_count,
514
+ "max_messages": self.MAX_MESSAGES_BEFORE_RESET,
515
+ "messages_remaining": max(0, self.MAX_MESSAGES_BEFORE_RESET - self.message_count),
516
+ "auto_reset_enabled": True,
517
+ "new_chat_between_messages": self.AUTO_NEW_CHAT_BETWEEN_MESSAGES,
518
+ "is_running": self.is_running(),
519
+ "anonymous_mode": True,
520
+ "session_dir": self.session_dir,
521
+ "device_id": self.current_identity["device_id"][:16] + "..." if self.current_identity else None,
522
+ }
523
 
524
 
525
  class TerminalPortalManager:
run-daemon.sh ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Run K-AI API as a Background Daemon
3
+ # ===================================
4
+ # This script runs the API in the background with auto-restart
5
+
6
+ cd ~/kai-api
7
+
8
+ APP_NAME="kai-api"
9
+ PID_FILE="/tmp/kai-api.pid"
10
+ LOG_FILE="/tmp/kai-api.log"
11
+
12
+ start() {
13
+ if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
14
+ echo "โœ… $APP_NAME is already running (PID: $(cat $PID_FILE))"
15
+ return
16
+ fi
17
+
18
+ echo "๐Ÿš€ Starting $APP_NAME..."
19
+
20
+ # Activate virtual environment
21
+ source venv/bin/activate
22
+
23
+ # Set environment
24
+ export PYTHONUNBUFFERED=1
25
+ export ENVIRONMENT=production
26
+
27
+ # Load .env if exists
28
+ if [ -f ".env" ]; then
29
+ export $(cat .env | grep -v '^#' | xargs)
30
+ fi
31
+
32
+ # Start with nohup
33
+ nohup uvicorn main:app --host 0.0.0.0 --port 8000 --workers 1 > "$LOG_FILE" 2>&1 &
34
+
35
+ # Save PID
36
+ echo $! > "$PID_FILE"
37
+
38
+ echo "โœ… $APP_NAME started (PID: $(cat $PID_FILE))"
39
+ echo "๐Ÿ“„ Logs: $LOG_FILE"
40
+ echo "๐ŸŒ API: http://$(curl -s ifconfig.me):8000"
41
+ echo "๐Ÿ”ง Admin: http://$(curl -s ifconfig.me):8000/qazmlp"
42
+ }
43
+
44
+ stop() {
45
+ if [ ! -f "$PID_FILE" ]; then
46
+ echo "โŒ $APP_NAME is not running"
47
+ return
48
+ fi
49
+
50
+ PID=$(cat "$PID_FILE")
51
+ echo "๐Ÿ›‘ Stopping $APP_NAME (PID: $PID)..."
52
+
53
+ kill $PID 2>/dev/null
54
+ sleep 2
55
+
56
+ if kill -0 $PID 2>/dev/null; then
57
+ echo "๐Ÿ’ฅ Force killing..."
58
+ kill -9 $PID 2>/dev/null
59
+ fi
60
+
61
+ rm -f "$PID_FILE"
62
+ echo "โœ… $APP_NAME stopped"
63
+ }
64
+
65
+ restart() {
66
+ stop
67
+ sleep 2
68
+ start
69
+ }
70
+
71
+ status() {
72
+ if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
73
+ echo "โœ… $APP_NAME is running (PID: $(cat $PID_FILE))"
74
+ echo "๐ŸŒ API: http://$(curl -s ifconfig.me):8000"
75
+ echo ""
76
+ echo "๐Ÿ“Š Recent logs:"
77
+ tail -20 "$LOG_FILE"
78
+ else
79
+ echo "โŒ $APP_NAME is not running"
80
+ rm -f "$PID_FILE"
81
+ fi
82
+ }
83
+
84
+ logs() {
85
+ if [ -f "$LOG_FILE" ]; then
86
+ tail -f "$LOG_FILE"
87
+ else
88
+ echo "โŒ No log file found"
89
+ fi
90
+ }
91
+
92
+ case "${1:-start}" in
93
+ start)
94
+ start
95
+ ;;
96
+ stop)
97
+ stop
98
+ ;;
99
+ restart)
100
+ restart
101
+ ;;
102
+ status)
103
+ status
104
+ ;;
105
+ logs)
106
+ logs
107
+ ;;
108
+ *)
109
+ echo "Usage: $0 {start|stop|restart|status|logs}"
110
+ exit 1
111
+ ;;
112
+ esac
start-production.sh ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # Production Start Script for K-AI API
3
+ # ====================================
4
+ # This script starts the K-AI API in production mode
5
+
6
+ cd ~/kai-api
7
+
8
+ echo "๐Ÿš€ Starting K-AI API in production mode..."
9
+
10
+ # Check if virtual environment exists
11
+ if [ ! -d "venv" ]; then
12
+ echo "๐Ÿ“ฆ Creating virtual environment..."
13
+ python3 -m venv venv
14
+ fi
15
+
16
+ # Activate virtual environment
17
+ source venv/bin/activate
18
+
19
+ # Install/update dependencies
20
+ echo "๐Ÿ“ฅ Installing dependencies..."
21
+ pip install --upgrade pip
22
+ pip install -r requirements.txt
23
+
24
+ # Install Playwright browsers
25
+ echo "๐ŸŽญ Installing Playwright browsers..."
26
+ playwright install chromium
27
+
28
+ # Create necessary directories
29
+ mkdir -p static
30
+ mkdir -p /tmp/opencode_sessions
31
+
32
+ # Set environment variables for production
33
+ export PYTHONUNBUFFERED=1
34
+ export ENVIRONMENT=production
35
+ export PORT=8000
36
+ export HOST=0.0.0.0
37
+
38
+ # Check if .env file exists and source it
39
+ if [ -f ".env" ]; then
40
+ echo "๐Ÿ”‘ Loading environment variables from .env..."
41
+ export $(cat .env | grep -v '^#' | xargs)
42
+ fi
43
+
44
+ echo ""
45
+ echo "========================================"
46
+ echo "๐ŸŒ K-AI API is starting!"
47
+ echo "========================================"
48
+ echo ""
49
+ echo "The API will be available at:"
50
+ echo " โ€ข Local: http://localhost:8000"
51
+ echo " โ€ข Admin: http://localhost:8000/qazmlp"
52
+ echo ""
53
+ echo "To run in background with auto-restart, use:"
54
+ echo " ./run-daemon.sh"
55
+ echo ""
56
+ echo "Starting server..."
57
+ echo ""
58
+
59
+ # Start the server
60
+ exec uvicorn main:app --host $HOST --port $PORT --workers 1
static/qaz.html CHANGED
@@ -805,6 +805,28 @@
805
  </div>
806
  </div>
807
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  <!-- Browser Interface -->
809
  <div id="browser-interface" style="display:none;">
810
  <!-- Toolbar -->
@@ -821,7 +843,7 @@
821
 
822
  <div class="quality-selector">
823
  <span style="font-size:0.75rem;color:var(--text-secondary);">Quality:</span>
824
- <button class="quality-btn active" onclick="setStreamQuality('low')"
825
  id="quality-low">Low</button>
826
  <button class="quality-btn" onclick="setStreamQuality('medium')"
827
  id="quality-medium">Med</button>
@@ -1206,9 +1228,15 @@
1206
  // Update controls for terminal
1207
  document.querySelector('.browser-address-bar').style.visibility = 'hidden';
1208
  document.querySelector('.quality-selector').style.visibility = 'hidden';
1209
- document.getElementById('terminal-sync-btn').style.display = 'inline-block';
1210
  document.getElementById('browser-page-title').textContent = 'OpenCode Terminal';
1211
  document.getElementById('browser-coords').textContent = 'Type commands below';
 
 
 
 
 
 
1212
  }
1213
 
1214
  async function syncTerminalAuth() {
@@ -1267,6 +1295,98 @@
1267
  }, 1000);
1268
  }
1269
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1270
  function showBrowserInterface() {
1271
  document.getElementById('browser-interface').style.display = 'block';
1272
 
 
805
  </div>
806
  </div>
807
 
808
+ <!-- Disposable Mode Status Bar (shown only for terminal) -->
809
+ <div id="disposable-status-bar" style="display:none;padding:10px 16px;background:var(--surface-hover);border-bottom:1px solid var(--border);">
810
+ <div style="display:flex;justify-content:space-between;align-items:center;">
811
+ <div style="display:flex;align-items:center;gap:12px;">
812
+ <span style="font-size:0.875rem;font-weight:600;">๐Ÿ—‘๏ธ Disposable Mode</span>
813
+ <span style="font-size:0.75rem;color:var(--text-secondary);">
814
+ Messages: <span id="msg-count" style="color:var(--accent);font-weight:600;">0</span>/<span id="msg-max">20</span>
815
+ </span>
816
+ <div class="progress-bar" style="width:100px;height:6px;">
817
+ <div id="msg-progress" class="progress-fill" style="width:0%;background:var(--success);"></div>
818
+ </div>
819
+ </div>
820
+ <div style="display:flex;gap:8px;">
821
+ <span id="next-reset-timer" style="font-size:0.75rem;color:var(--text-secondary);"></span>
822
+ <button class="btn btn-secondary" style="padding:4px 12px;font-size:0.75rem;" onclick="manualReset()">๐Ÿ”„ Reset Now</button>
823
+ </div>
824
+ </div>
825
+ <div style="margin-top:6px;font-size:0.75rem;color:var(--text-secondary);">
826
+ ๐Ÿ”’ Anonymous โ€ข ๐Ÿ†• New chat between messages โ€ข ๐Ÿงน Auto-reset after 20 messages
827
+ </div>
828
+ </div>
829
+
830
  <!-- Browser Interface -->
831
  <div id="browser-interface" style="display:none;">
832
  <!-- Toolbar -->
 
843
 
844
  <div class="quality-selector">
845
  <span style="font-size:0.75rem;color:var(--text-secondary);">Quality:</span>
846
+ <button class="quality-btn active" onclick="setStreamQuality('low')
847
  id="quality-low">Low</button>
848
  <button class="quality-btn" onclick="setStreamQuality('medium')"
849
  id="quality-medium">Med</button>
 
1228
  // Update controls for terminal
1229
  document.querySelector('.browser-address-bar').style.visibility = 'hidden';
1230
  document.querySelector('.quality-selector').style.visibility = 'hidden';
1231
+ document.getElementById('terminal-sync-btn').style.display = 'none'; // Hide sync button (anonymous mode)
1232
  document.getElementById('browser-page-title').textContent = 'OpenCode Terminal';
1233
  document.getElementById('browser-coords').textContent = 'Type commands below';
1234
+
1235
+ // Show disposable mode status bar
1236
+ document.getElementById('disposable-status-bar').style.display = 'block';
1237
+
1238
+ // Start polling disposable status
1239
+ startDisposableStatusPolling();
1240
  }
1241
 
1242
  async function syncTerminalAuth() {
 
1295
  }, 1000);
1296
  }
1297
 
1298
+ // Disposable Mode Functions
1299
+ let disposableStatusInterval = null;
1300
+
1301
+ function startDisposableStatusPolling() {
1302
+ if (disposableStatusInterval) clearInterval(disposableStatusInterval);
1303
+
1304
+ // Poll status every 3 seconds
1305
+ disposableStatusInterval = setInterval(async () => {
1306
+ if (currentProvider !== 'opencode') return;
1307
+
1308
+ try {
1309
+ const res = await fetch('/qaz/terminal/status?model=kimi-k2.5-free');
1310
+ const data = await res.json();
1311
+
1312
+ if (data.status === 'success' && data.data) {
1313
+ updateDisposableStatusUI(data.data);
1314
+ }
1315
+ } catch (e) {
1316
+ console.error('Disposable status poll error:', e);
1317
+ }
1318
+ }, 3000);
1319
+
1320
+ // Initial poll
1321
+ fetch('/qaz/terminal/status?model=kimi-k2.5-free')
1322
+ .then(r => r.json())
1323
+ .then(data => {
1324
+ if (data.status === 'success') updateDisposableStatusUI(data.data);
1325
+ })
1326
+ .catch(e => console.error(e));
1327
+ }
1328
+
1329
+ function updateDisposableStatusUI(status) {
1330
+ document.getElementById('msg-count').textContent = status.message_count;
1331
+ document.getElementById('msg-max').textContent = status.max_messages;
1332
+
1333
+ const progress = (status.message_count / status.max_messages) * 100;
1334
+ const progressBar = document.getElementById('msg-progress');
1335
+ progressBar.style.width = progress + '%';
1336
+
1337
+ // Change color based on progress
1338
+ if (progress >= 90) {
1339
+ progressBar.style.background = 'var(--error)';
1340
+ } else if (progress >= 70) {
1341
+ progressBar.style.background = 'var(--warning)';
1342
+ } else {
1343
+ progressBar.style.background = 'var(--success)';
1344
+ }
1345
+
1346
+ const remaining = status.max_messages - status.message_count;
1347
+ document.getElementById('next-reset-timer').textContent =
1348
+ remaining <= 5 ? `โš ๏ธ Reset in ${remaining} messages!` : `${remaining} messages until reset`;
1349
+ }
1350
+
1351
+ async function manualReset() {
1352
+ if (!confirm('๐Ÿ—‘๏ธ This will completely wipe all traces and start fresh. OpenCode will see a brand new device. Continue?')) {
1353
+ return;
1354
+ }
1355
+
1356
+ const btn = event.target;
1357
+ const originalText = btn.textContent;
1358
+ btn.textContent = 'Resetting...';
1359
+ btn.disabled = true;
1360
+
1361
+ try {
1362
+ const res = await fetch('/qaz/terminal/reset', {
1363
+ method: 'POST',
1364
+ headers: { 'Content-Type': 'application/json' },
1365
+ body: JSON.stringify({ model: 'kimi-k2.5-free' })
1366
+ });
1367
+
1368
+ const data = await res.json();
1369
+
1370
+ if (data.status === 'success') {
1371
+ btn.textContent = 'โœ… Reset Complete!';
1372
+ alert('๐Ÿ—‘๏ธ Full disposable reset completed!\n\nOpenCode now sees a completely new device with fresh tokens.');
1373
+ // Clear terminal view
1374
+ document.getElementById('terminal-view').innerHTML = '๐Ÿ”„ Session reset - Starting fresh...';
1375
+ } else {
1376
+ alert('Reset failed: ' + data.message);
1377
+ btn.textContent = 'โŒ Error';
1378
+ }
1379
+ } catch (e) {
1380
+ alert('Reset error: ' + e);
1381
+ btn.textContent = 'โŒ Error';
1382
+ } finally {
1383
+ setTimeout(() => {
1384
+ btn.textContent = originalText;
1385
+ btn.disabled = false;
1386
+ }, 2000);
1387
+ }
1388
+ }
1389
+
1390
  function showBrowserInterface() {
1391
  document.getElementById('browser-interface').style.display = 'block';
1392
 
test_prompt.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import asyncio
3
+ import os
4
+ import logging
5
+ from engine import AIEngine
6
+
7
+ # Configure logging
8
+ logging.basicConfig(level=logging.INFO)
9
+
10
+ async def main():
11
+ print("Initializing Engine...")
12
+ engine = AIEngine()
13
+
14
+ # Mocking system prompt file if it doesn't exist (it should)
15
+
16
+ print("\n--- Testing Chat ---")
17
+ try:
18
+ # We try 'opencode' because we know we settled auth for it,
19
+ # OR we try 'g4f'/pollinations if they are easier.
20
+ # User reported issue on 'gemini-3-flash'.
21
+ # Let's try to mock the provider response handling to see what PROMPT it actually gets?
22
+ # Actually, we can just print the prompt in engine.py by adding a log.
23
+
24
+ # But let's run a real request if possible.
25
+ # If not, at least we see if it crashes.
26
+
27
+ response = await engine.chat(
28
+ prompt="Hello! Are you there?",
29
+ model="gemini", # Friendly name for gemini-pro or similar, mapped in config
30
+ provider="auto"
31
+ )
32
+ print(f"\nResponse: {response['response']}")
33
+ print(f"Model used: {response['model']}")
34
+
35
+ except Exception as e:
36
+ print(f"Error: {e}")
37
+
38
+ if __name__ == "__main__":
39
+ asyncio.run(main())