@paytaca/opencode-plugin 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bundled/proxy.d.ts +1 -1
- package/dist/bundled/proxy.d.ts.map +1 -1
- package/dist/bundled/proxy.js +183 -57
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/bundled/wrapper.d.ts +1 -1
- package/dist/bundled/wrapper.d.ts.map +1 -1
- package/dist/bundled/wrapper.js +7 -2
- package/dist/bundled/wrapper.js.map +1 -1
- package/dist/proxy.d.ts.map +1 -1
- package/dist/proxy.js +3 -59
- package/dist/proxy.js.map +1 -1
- package/package.json +2 -2
package/dist/bundled/proxy.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PROXY_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca AI Proxy\n * \n * Sits between OpenCode and the Django backend.\n * - Auto-starts by OpenCode plugin\n * - On 402, returns SSE typewriter loading sequence + synthetic payment prompt\n * - Stores pending payments; handles \"yes\"/\"no\" approval internally\n * - Uses only Node.js built-in modules\n * \n * Usage: node proxy.js [backend_url] [proxy_port]\n * Example: node proxy.js https://api.paytaca.ai 8001\n */\n\nconst http = require('http');\nconst https = require('https');\nconst { spawn } = require('child_process');\nconst { Transform } = require('stream');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst PROXY_PORT = parseInt(process.argv[3]) || 8001;\nconst BACKEND_URL = process.argv[2] || 'https://api.paytaca.ai';\nconst parsedUrl = new URL(BACKEND_URL);\nconst DJANGO_HOST = parsedUrl.hostname;\nconst DJANGO_PORT = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80);\nconst REQUester = parsedUrl.protocol === 'https:' ? https : http;\n\n// Logging setup: write to file instead of console\nconst LOG_DIR = path.join(os.homedir(), '.opencode-paytaca');\nif (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n}\nconst LOG_FILE = path.join(LOG_DIR, 'proxy.log');\nconst logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });\n\nfunction log(message) {\n const timestamp = new Date().toISOString();\n logStream.write(timestamp + ' [Proxy] ' + message + '\\n');\n}\n\n// Store pending payment requests per wallet hash\n// Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }\n// step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)\nconst pendingPayments = new Map();\n\n// Utility: run shell command and return output\nfunction runCommand(cmd, args = []) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { shell: false });\n let stdout = '';\n let stderr = '';\n \n child.stdout.on('data', (data) => { stdout += data.toString(); });\n child.stderr.on('data', (data) => { stderr += data.toString(); });\n \n child.on('close', (code) => {\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(stderr.trim() || 'Command exited with code ' + code));\n });\n \n child.on('error', (err) => reject(err));\n });\n}\n\n// Get paytaca command from environment or default to 'paytaca'\nconst PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';\n\n// Utility: check if paytaca CLI exists\nasync function checkPaytacaCli() {\n try {\n // Try to run version check\n await runCommand(PAYTACA_CMD, ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Utility: get wallet balance in sats\nasync function getWalletBalance() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);\n if (match) {\n const bch = parseFloat(match[1]);\n return Math.floor(bch * 100000000);\n }\n return null;\n } catch (err) {\n log('Failed to get wallet balance: ' + err.message);\n return null;\n }\n}\n\n// Utility: get receiving address\nasync function getReceivingAddress() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Address:\\s*(bitcoincash:[a-zA-Z0-9]+)/i);\n return match ? match[1] : null;\n } catch {\n return null;\n }\n}\n\n// Utility: check if wallet exists\nasync function checkWallet() {\n try {\n await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Format seconds as MM:SS or HH:MM:SS\nfunction formatDuration(totalSeconds) {\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n if (hours > 0) {\n return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');\n }\n return minutes + ':' + String(secs).padStart(2, '0');\n}\n\n// SSE helper: write a data line\nfunction sseLine(res, data) {\n res.write('data: ' + JSON.stringify(data) + '\\n\\n');\n}\n\n// SSE helper: write [DONE]\nfunction sseDone(res) {\n res.write('data: [DONE]\\n\\n');\n}\n\n// Build and stream SSE tier selection prompt\nasync function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'tier-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n // Loading sequence\n sseLine(res, {\n id: 'tier-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n const hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: 'tier-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-4',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasCli ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: 'tier-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-6',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasWallet ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: 'tier-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n let balanceStr;\n if (balanceSats !== null) {\n const bch = (balanceSats / 100000000).toFixed(8);\n balanceStr = bch + ' BCH';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705 \u2014 ' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n }\n\n // Tier selection\n sseLine(res, {\n id: 'tier-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\uD83D\uDCB3 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],\n });\n\n for (let i = 0; i < tiers.length; i++) {\n const tier = tiers[i];\n const bchAmount = (tier.price_sats / 100000000).toFixed(8);\n const label = '`(' + String(i + 1) + ')` ';\n sseLine(res, {\n id: 'tier-10-' + i,\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes \u2014 PHP ' + tier.price_php.toFixed(2) + ' (' + bchAmount + ' BCH)\\n' }, finish_reason: null }],\n });\n }\n\n sseLine(res, {\n id: 'tier-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'tier-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Build and stream SSE loading sequence + payment prompt\n// Stream SSE notice when the upstream (OpenRouter) account lacks balance to fund\n// the request. Replaces the old single-tier yes/no approval prompt.\nasync function streamLowBalanceNotice(res, modelName) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'lb-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName || 'AI Model',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n sseLine(res, {\n id: 'lb-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F OpenRouter balance is low \u2014 please top up before continuing.\\n' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'lb-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Forward request to Django and return response (buffered, for non-streaming)\nfunction forwardToDjango(req, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardToDjango -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n const elapsed = Date.now() - startTime;\n log('Django responded in ' + elapsed + 'ms: status=' + djangoRes.statusCode + ', bodyLen=' + responseBody.length);\n callback(null, djangoRes.statusCode, djangoRes.headers, responseBody);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Forward streaming request to Django\nfunction forwardStreaming(req, res, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardStreaming -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n const elapsed = Date.now() - startTime;\n log('Django response started in ' + elapsed + 'ms: status=' + djangoRes.statusCode);\n\n if (djangoRes.statusCode === 402) {\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n callback(null, 402, djangoRes.headers, responseBody);\n });\n return;\n }\n\n res.writeHead(djangoRes.statusCode, {\n 'Content-Type': djangoRes.headers['content-type'] || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n if (res.socket) {\n res.socket.setNoDelay(true);\n }\n\n // Buffer SSE data at event boundaries and inject keepalive between events.\n let sseBuffer = '';\n let lastActivity = Date.now();\n let streamingDone = false;\n let doneForwarded = false;\n\n // Watchdog: inject keepalive only when buffer is empty (between complete events)\n const keepaliveTimer = setInterval(() => {\n if (streamingDone || res.writableEnded || res.destroyed) {\n clearInterval(keepaliveTimer);\n return;\n }\n const now = Date.now();\n if (now - lastActivity >= 2000 && sseBuffer.length === 0) {\n try {\n res.write(': keepalive\\n\\n');\n lastActivity = now;\n } catch (err) {\n log('Keepalive write error: ' + err.message);\n clearInterval(keepaliveTimer);\n }\n }\n }, 500);\n\n const cleanup = () => {\n streamingDone = true;\n clearInterval(keepaliveTimer);\n };\n\n var diagCounter = 0;\n djangoRes.on('data', (chunk) => {\n var chunkStr = chunk.toString();\n var chunkIdx = ++diagCounter;\n sseBuffer += chunkStr;\n lastActivity = Date.now();\n\n var okCount = (sseBuffer.match(/:ok/g) || []).length;\n if (okCount > 0) {\n log('CHUNK#' + chunkIdx + ': ' + okCount + ' :ok in buffer (len=' + sseBuffer.length + ')');\n }\n\n // Strip upstream SSE \":ok\" keepalive comments from anywhere in the buffer.\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (okCount > 0) {\n log('AFTER: stripped ' + okCount + ' :ok, buffer len=' + sseBuffer.length);\n }\n\n var extractedCount = 0;\n let idx;\n while ((idx = sseBuffer.indexOf('\\n\\n')) !== -1) {\n const event = sseBuffer.substring(0, idx + 2);\n sseBuffer = sseBuffer.substring(idx + 2);\n const lines = event.split('\\n').filter(l => !/^:/.test(l) && l.length > 0);\n if (lines.length === 0) continue;\n const cleanEvent = lines.join('\\n') + '\\n\\n';\n extractedCount++;\n var dataContent = lines.map(function(l) { return l.replace(/^data: ?/, ''); }).join('');\n if (dataContent === '[DONE]') { doneForwarded = true; }\n var lastChar = dataContent.slice(-1);\n if (dataContent !== '[DONE]' && lastChar !== '}' && lastChar !== ']') {\n log('FLUSH: truncated event #' + extractedCount + ' (len=' + dataContent.length + ', end=' + JSON.stringify(dataContent.slice(-30)) + ')');\n }\n try {\n res.write(cleanEvent);\n } catch (err) {\n cleanup();\n log('Write error: ' + err.message);\n return;\n }\n }\n if (extractedCount > 0) {\n log('EXTRACT: forwarded ' + extractedCount + ' events in chunk#' + chunkIdx + ', buffer remaining len=' + sseBuffer.length);\n }\n });\n\n djangoRes.on('end', () => {\n if (streamingDone) {\n return;\n }\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (sseBuffer) {\n // Ensure the final written data ends with \\n\\n so the client recognizes the event boundary\n if (sseBuffer.length < 2 || sseBuffer.substring(sseBuffer.length - 2) !== '\\n\\n') {\n sseBuffer += '\\n\\n';\n }\n log('END: writing remaining buffer len=' + sseBuffer.length + ' start=' + JSON.stringify(sseBuffer.substring(0, 80)));\n try { res.write(sseBuffer); } catch (e) {}\n }\n if (!doneForwarded) {\n log('Injecting [DONE] \u2014 upstream closed without sending it');\n try { res.write('data: [DONE]\\n\\n'); } catch (e) {}\n }\n cleanup();\n try { res.end(); } catch (e) {}\n log('Streaming response completed' + (doneForwarded ? '' : ' (injected [DONE])'));\n callback(null, djangoRes.statusCode, {}, '');\n });\n\n djangoRes.on('error', (err) => {\n log('Django stream error: ' + err.message);\n if (!streamingDone) {\n cleanup();\n }\n if (!res.writableEnded) {\n try {\n res.end();\n } catch (e) {}\n }\n callback(null, 200, {}, '');\n });\n\n res.on('close', () => {\n cleanup();\n log('Client connection closed');\n });\n\n res.on('error', (err) => {\n cleanup();\n log('Client connection error: ' + err.message);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django streaming request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Force stream=false in body because paytaca pay reads the response as text\nfunction forceNonStreaming(body) {\n try {\n const data = JSON.parse(body);\n data.stream = false;\n return JSON.stringify(data);\n } catch {\n return body;\n }\n}\n\n// Convert a chat.completion JSON object to SSE format\nfunction jsonToSse(res, chatCompletion) {\n const content = chatCompletion.choices?.[0]?.message?.content || '';\n const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';\n const created = chatCompletion.created || Math.floor(Date.now() / 1000);\n\n if (!res.headersSent) {\n try {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n } catch (e) {\n return;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-1',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n } catch (e) {\n }\n\n const chunkSize = 20;\n let chunksWritten = 0;\n for (let i = 0; i < content.length; i += chunkSize) {\n try {\n sseLine(res, {\n id: 'chatcmpl-' + (i + 2),\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { content: content.slice(i, i + chunkSize) }, finish_reason: null }],\n });\n chunksWritten++;\n } catch (e) {\n break;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-done',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n } catch (e) {\n }\n\n try {\n sseDone(res);\n } catch (e) {\n }\n\n try {\n res.end();\n } catch (e) {\n }\n}\n\n// Run paytaca pay internally and return the response\nfunction runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {\n const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');\n const payBody = forceNonStreaming(body);\n\n // Write body to a temp file to avoid CLI arg length limits\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paytaca-pay-'));\n const bodyFile = path.join(tmpDir, 'body.json');\n const configFile = path.join(tmpDir, 'config.json');\n\n try {\n fs.writeFileSync(bodyFile, payBody, 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp body file: ' + err.message));\n }\n\n const config = {\n url,\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, extraHeaders || {}),\n bodyFile,\n confirmed: true,\n };\n\n try {\n fs.writeFileSync(configFile, JSON.stringify(config), 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp config file: ' + err.message));\n }\n\n // Path to the wrapper script\n const wrapperScript = path.join(LOG_DIR, 'paytaca-pay-wrapper.mjs');\n log('Running paytaca pay via wrapper script...');\n\n const child = spawn('node', [wrapperScript, configFile], { shell: false });\n let stdout = '';\n let stderr = '';\n\n child.stdout.on('data', (data) => { \n stdout += data.toString(); \n });\n child.stderr.on('data', (data) => { \n stderr += data.toString(); \n });\n\n child.on('close', (code) => {\n // Clean up temp files\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n\n if (code === 0) {\n try {\n const responseJson = JSON.parse(stdout.trim());\n callback(null, responseJson);\n } catch (err) {\n callback(new Error('Could not parse paytaca pay response: ' + err.message));\n }\n } else {\n callback(new Error(stderr.trim() || 'paytaca pay wrapper exited with code ' + code));\n }\n });\n\n child.on('error', (err) => {\n // Clean up temp files on error\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n callback(new Error('Failed to run paytaca pay wrapper: ' + err.message));\n });\n}\n\n// Extract the last user message content from a chat payload\nfunction getLastUserMessageContent(body) {\n try {\n const data = JSON.parse(body);\n const messages = data.messages || [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'user') {\n const content = messages[i].content;\n if (Array.isArray(content)) {\n const parts = [];\n for (const part of content) {\n if (part && typeof part === 'object' && part.type === 'text') {\n parts.push(part.text || '');\n } else if (typeof part === 'string') {\n parts.push(part);\n } else {\n parts.push(JSON.stringify(part));\n }\n }\n return parts.join('').trim().toLowerCase();\n }\n return String(content || '').trim().toLowerCase();\n }\n }\n return '';\n } catch {\n return '';\n }\n}\n\nasync function handleTimeCreditsCommand(res, walletHash) {\n log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');\n const statusUrl = BACKEND_URL + '/v1/wallet/status';\n const statusRes = await fetch(statusUrl, {\n headers: { 'X-Wallet-Hash': walletHash }\n });\n let content;\n if (statusRes.ok) {\n const statusData = await statusRes.json();\n const sessions = statusData.sessions || [];\n const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);\n const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);\n const parts = [];\n if (activeSessions.length > 0) {\n parts.push('**\u23F1\uFE0F Active Time Credits:**');\n activeSessions.forEach(s => {\n const total = formatDuration(s.time_credits_seconds);\n const remaining = formatDuration(s.time_remaining_seconds);\n const used = formatDuration(s.time_used_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + '** \u2014 ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');\n });\n }\n if (inactiveSessions.length > 0) {\n parts.push('\\n**\u26A0\uFE0F Inactive Model:**');\n inactiveSessions.forEach(s => {\n const remaining = formatDuration(s.time_remaining_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** \u2014 ' + remaining + ' remaining');\n });\n }\n content = parts.length > 0 ? parts.join('\\n') : '\u23F1\uFE0F No active time credits.';\n } else {\n content = '\u23F1\uFE0F Unable to check time credits.';\n }\n\n sseLine(res, {\n id: 'time-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'time-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'time-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\nconst isTimeCmd = (s) => s === 'credits';\nconst isPricingCmd = (s) => s === 'plans';\n\n// List all models grouped by tier (Budget / Premium / Frontier / Other) with prices\nasync function handlePricingCommand(res) {\n log('Pricing command requested');\n let content;\n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (!configRes.ok) {\n throw new Error('config status ' + configRes.status);\n }\n const config = await configRes.json();\n const models = Array.isArray(config.models) ? config.models : [];\n const groups = { budget: [], premium: [], frontier: [], other: [] };\n for (const m of models) {\n const key = String(m.tier || '').toLowerCase();\n const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';\n groups[groupKey].push(m);\n }\n const lines = ['\uD83D\uDCCB Paytaca AI \u2014 Model Pricing'];\n const order = [\n { key: 'budget', label: 'Budget' },\n { key: 'premium', label: 'Premium' },\n { key: 'frontier', label: 'Frontier' },\n { key: 'other', label: 'Other' },\n ];\n let any = false;\n for (const g of order) {\n if (groups[g.key].length === 0) continue;\n any = true;\n lines.push('');\n lines.push(g.label);\n for (const m of groups[g.key]) {\n lines.push('');\n const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];\n if (tiers.length === 0) {\n lines.push('- **' + (m.display_name || m.id) + '**: \u2014 no pricing configured');\n continue;\n }\n const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));\n lines.push('**' + (m.display_name || m.id) + '**:');\n sorted.forEach((t, i) => {\n const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;\n const bch = (sats / 100000000).toFixed(8);\n const php = typeof t.price_php === 'number' ? t.price_php.toFixed(2) : '?.??';\n lines.push(' `(' + String(i + 1) + ')` ' + (t.minutes || 0) + ' minutes \u2014 PHP ' + php + ' (' + bch + ' BCH)');\n });\n }\n }\n if (!any) {\n lines.push('');\n lines.push('No models available.');\n }\n content = lines.join('\\n');\n } catch (err) {\n log('Pricing command failed: ' + err.message);\n content = '\uD83D\uDCCB Unable to fetch pricing.';\n }\n\n sseLine(res, {\n id: 'price-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'price-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'price-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\n// Main proxy server\nconst server = http.createServer(async (req, res) => {\n // Enable CORS\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, X-Model-Id, X-Duration-Minutes, Payment-Signature, Authorization');\n \n if (req.method === 'OPTIONS') {\n res.writeHead(200);\n res.end();\n return;\n }\n \n // Discovery endpoint - fetch from backend to get actual config\n if (req.url === '/v1/config' && req.method === 'GET') {\n try {\n const backendConfig = await fetch(BACKEND_URL + '/v1/config');\n if (backendConfig.ok) {\n const config = await backendConfig.json();\n // Add proxy-specific info\n config.proxy_url = 'http://localhost:' + PROXY_PORT + '/v1';\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n return;\n }\n } catch (err) {\n log('Failed to fetch backend config: ' + err.message);\n }\n \n // Fallback to static values if backend unavailable\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',\n django_url: BACKEND_URL + '/v1',\n payment_address: '',\n default_model: 'deepseek/deepseek-v4-flash',\n default_duration_minutes: 30,\n models: [\n {\n id: 'deepseek/deepseek-v4-flash',\n object: 'model',\n display_name: 'DeepSeek V4 Flash',\n provider: 'openrouter',\n price_tiers: [\n { minutes: 10, price_php: 5.0, price_sats: 45000 },\n { minutes: 30, price_php: 12.0, price_sats: 108000 },\n { minutes: 60, price_php: 20.0, price_sats: 180000 },\n ],\n },\n ],\n context_retention_hours: 2,\n }));\n return;\n }\n \n // All other endpoints \u2014 read body and forward to Django\n let body = '';\n req.on('data', chunk => { body += chunk; });\n req.on('end', async () => {\n try {\n const walletHash = req.headers['x-wallet-hash'];\n const lastContent = getLastUserMessageContent(body);\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n const stripSysRem = (s) => { let r = (s || ''), a = '<system-reminder>', b = '</system-reminder>', i = r.indexOf(a); while (i !== -1) { let j = r.indexOf(b, i); if (j === -1) break; r = r.substring(0, i) + r.substring(j + b.length); i = r.indexOf(a); } return r.trim(); };\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n const redactedHeaders = {};\n for (const [k, v] of Object.entries(req.headers)) {\n const lk = k.toLowerCase();\n redactedHeaders[k] = /authorization|payment-signature|api-?key|secret|token/i.test(lk)\n ? '<redacted>'\n : v;\n }\n log('MISSING X-Wallet-Hash. Received headers: ' + JSON.stringify(redactedHeaders));\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n error: 'X-Wallet-Hash header missing',\n message: 'The X-Wallet-Hash header was not sent by the client. It is injected by the paytaca opencode plugin (provider options.headers / chat.headers). Reinstall or restart the plugin, or run paytaca wallet info and verify the plugin loaded.',\n }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n var pendingPayload = pendingPayments.get(walletHash);\n \n // If there's a pending payment for a different model, clear it so the\n // new request can be forwarded fresh to Django. This prevents the\n // proxy from re-showing a stale payment prompt when the user switches\n // to a different model mid-conversation.\n if (pendingPayload) {\n var reqModel = '';\n try { reqModel = JSON.parse(body).model || ''; } catch (e) {}\n if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {\n pendingPayments.delete(walletHash);\n pendingPayload = null;\n }\n }\n \n if (pendingPayload) {\n // Check for tier selection first\n if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {\n const userInput = stripSysRem(lastContent);\n const timeCmd = userInput?.trim().toLowerCase();\n if (isTimeCmd(timeCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(timeCmd)) {\n await handlePricingCommand(res);\n return;\n }\n let selectedIndex = -1;\n \n // Try to parse user input as a number (1-based)\n const num = parseInt(userInput, 10);\n if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {\n selectedIndex = num - 1;\n } else {\n // Try to match by duration minutes\n for (let i = 0; i < pendingPayload.tiers.length; i++) {\n if (userInput === String(pendingPayload.tiers[i].minutes) ||\n userInput === pendingPayload.tiers[i].minutes + ' minutes' ||\n userInput === pendingPayload.tiers[i].minutes + ' min') {\n selectedIndex = i;\n break;\n }\n }\n }\n \n if (selectedIndex >= 0) {\n const selectedTier = pendingPayload.tiers[selectedIndex];\n pendingPayload.durationMinutes = selectedTier.minutes;\n pendingPayload.step = 'processing';\n \n log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');\n \n // Build extra headers for payment wrapper\n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);\n \n // Check wallet balance before attempting payment\n const currentBalanceSats = await getWalletBalance();\n if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {\n log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');\n pendingPayments.delete(walletHash);\n const addr = await getReceivingAddress();\n const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;\n const neededLine = addr ? '\\n\\n\uD83D\uDCE5 **Fund your wallet:** \\`' + addr + '\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';\n sseLine(res, {\n id: 'balance-err',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n\\n\u274C **Insufficient balance** \u2014 You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\`balance\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'balance-err-done',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n sseDone(res);\n res.end();\n return;\n }\n \n // Keepalive during payment processing\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n pendingPayments.delete(walletHash);\n clearInterval(keepalive);\n\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion);\n } catch (e) {}\n } else {\n try {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(chatCompletion));\n } catch (e) {}\n }\n });\n return;\n } else {\n // Invalid selection \u2014 reshow the prompt\n log('Invalid tier selection for wallet ' + walletHash?.substring(0, 16) + '...');\n await streamTierSelectionPrompt(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', pendingPayload.tiers);\n return;\n }\n }\n \n // Old flow: user responded to a yes/no payment prompt\n if (stripSysRem(lastContent) === 'yes') {\n log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n \n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n if (pendingPayload.durationMinutes) {\n extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);\n }\n \n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n clearInterval(keepalive);\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) {}\n } else {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion);\n } catch (e) {}\n } else {\n try {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(chatCompletion));\n } catch (e) {}\n }\n });\n return;\n \n } else if (stripSysRem(lastContent) === 'no') {\n log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n\n const addr = await getReceivingAddress();\n const fundMsg = addr\n ? 'Fund your wallet: ' + addr\n : 'You can fund your wallet by running: paytaca receive';\n\n const declineCompletion = {\n id: 'payment-declined',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',\n choices: [{\n index: 0,\n message: {\n role: 'assistant',\n content: 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,\n },\n finish_reason: 'stop',\n }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n };\n jsonToSse(res, declineCompletion);\n return;\n \n } else {\n const innerCmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(innerCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(innerCmd)) {\n await handlePricingCommand(res);\n return;\n }\n log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle credits command \u2014 show remaining time credits\n const cmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(cmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n // Handle pricing command \u2014 show all models grouped by tier\n if (isPricingCmd(cmd)) {\n await handlePricingCommand(res);\n return;\n }\n \n let isStreaming = true;\n try { isStreaming = JSON.parse(body).stream !== false; } catch {}\n\n const handleResponse = async (err, statusCode, headers, responseBody) => {\n if (err) {\n if (!res.headersSent) {\n log('Django connection error: ' + err.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Backend unavailable', details: err.message }));\n }\n return;\n }\n\n if (statusCode === 402) {\n let requestModel = null;\n try { requestModel = JSON.parse(body).model || null; } catch (e) {}\n log('402 intercepted for wallet ' + walletHash?.substring(0, 16)\n + ' x-model-id=' + (req.headers['x-model-id'] || 'null')\n + ' body.model=' + (requestModel || 'null'));\n \n // Parse 402 response for model_id and price_tiers\n let modelId = null;\n let displayName = null;\n let tiers = null;\n try {\n const parsed = JSON.parse(responseBody);\n modelId = parsed.model_id || null;\n displayName = parsed.display_name || null;\n tiers = parsed.price_tiers || null;\n log('402 body: model=' + (modelId || 'null')\n + ' display=' + (displayName || 'null')\n + ' tiers=' + (Array.isArray(tiers) ? tiers.length : String(tiers))\n + ' reason=' + (parsed.reason || 'n/a')\n + ' bodyPrefix=' + responseBody.substring(0, 160).replace(/\\n/g, ' '));\n } catch (e) {\n log('Could not parse 402 body: ' + e.message);\n }\n \n pendingPayments.set(walletHash, {\n body: body,\n modelId: modelId,\n displayName: displayName,\n durationMinutes: null,\n tiers: tiers,\n step: tiers ? 'tier_select' : 'approval'\n });\n \n if (tiers && tiers.length > 0) {\n // New flow: show tier selection prompt\n await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);\n return;\n }\n \n // Check session status to determine if this is a renewal\n let isRenewal = false;\n let tokensUsed = 0;\n let tokenLimit = 50000;\n let timeRemainingSeconds = 0;\n \n let statusModelId = modelId;\n let statusSnapshot = null;\n try {\n // Extract model from the original request body if not in 402\n if (!statusModelId) {\n try {\n const bodyParsed = JSON.parse(body);\n statusModelId = bodyParsed.model || null;\n } catch (e) {}\n }\n \n const statusPath = '/v1/wallet/status' + (statusModelId ? '?model_id=' + encodeURIComponent(statusModelId) : '');\n const statusResponse = await new Promise((resolve, reject) => {\n const statusReq = REQUester.get({\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: statusPath,\n headers: { 'X-Wallet-Hash': walletHash }\n }, (res) => {\n let data = '';\n res.on('data', chunk => data += chunk);\n res.on('end', () => {\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve({});\n }\n });\n });\n statusReq.on('error', reject);\n statusReq.setTimeout(5000, () => reject(new Error('timeout')));\n });\n \n if (statusResponse) {\n statusSnapshot = statusResponse;\n tokensUsed = statusResponse.tokens_used || 0;\n tokenLimit = statusResponse.token_limit || 50000;\n timeRemainingSeconds = statusResponse.time_remaining_seconds || 0;\n \n // Renewal if session has been used (tokens > 0 or time > 0) but is now exhausted\n isRenewal = (tokensUsed > 0 || statusResponse.time_used_seconds > 0) &&\n (!statusResponse.session_active || timeRemainingSeconds <= 0);\n }\n } catch (err) {\n log('Failed to check session status: ' + err.message);\n }\n \n log('402 status model=' + (statusModelId || 'null')\n + ' snapshot=' + JSON.stringify(statusSnapshot)\n + ' isRenewal=' + isRenewal\n + ' timeRemaining=' + timeRemainingSeconds\n + ' tokensUsed=' + tokensUsed\n + ' tokenLimit=' + tokenLimit);\n \n await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model');\n } else {\n if (res.headersSent) {\n log('Streaming response completed and already sent');\n pendingPayments.delete(walletHash);\n return;\n }\n\n log('Forwarding normal response to OpenCode: status=' + statusCode + ', bodyLen=' + responseBody.length);\n pendingPayments.delete(walletHash);\n res.writeHead(statusCode, {\n 'Content-Type': headers['content-type'] || 'application/json',\n });\n res.end(responseBody);\n }\n };\n\n if (isStreaming) {\n forwardStreaming(req, res, body, handleResponse);\n } else {\n forwardToDjango(req, body, handleResponse);\n }\n \n } catch (err) {\n log('Error: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Internal proxy error' }));\n }\n });\n});\n\nserver.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n log('Port ' + PROXY_PORT + ' is already in use. Another proxy instance may be running.');\n log('Exiting cleanly (code 0) so the plugin can detect the existing proxy.');\n process.exit(0);\n }\n log('Server error: ' + err.message);\n process.exit(1);\n});\n\nserver.listen(PROXY_PORT, () => {\n log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);\n log('Forwarding to Django at ' + BACKEND_URL);\n log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');\n log('Managed by OpenCode plugin');\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n\nprocess.on('SIGINT', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n";
|
|
1
|
+
export declare const PROXY_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca AI Proxy\n * \n * Sits between OpenCode and the Django backend.\n * - Auto-starts by OpenCode plugin\n * - On 402, returns SSE typewriter loading sequence + synthetic payment prompt\n * - Stores pending payments; handles \"yes\"/\"no\" approval internally\n * - Uses only Node.js built-in modules\n * \n * Usage: node proxy.js [backend_url] [proxy_port]\n * Example: node proxy.js https://api.paytaca.ai 8001\n */\n\nconst http = require('http');\nconst https = require('https');\nconst { spawn } = require('child_process');\nconst { Transform } = require('stream');\nconst fs = require('fs');\nconst path = require('path');\nconst os = require('os');\n\nconst PROXY_PORT = parseInt(process.argv[3]) || 8001;\nconst BACKEND_URL = process.argv[2] || 'https://api.paytaca.ai';\nconst parsedUrl = new URL(BACKEND_URL);\nconst DJANGO_HOST = parsedUrl.hostname;\nconst DJANGO_PORT = parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80);\nconst REQUester = parsedUrl.protocol === 'https:' ? https : http;\n\n// Logging setup: write to file instead of console\nconst LOG_DIR = path.join(os.homedir(), '.opencode-paytaca');\nif (!fs.existsSync(LOG_DIR)) {\n fs.mkdirSync(LOG_DIR, { recursive: true });\n}\nconst LOG_FILE = path.join(LOG_DIR, 'proxy.log');\nconst logStream = fs.createWriteStream(LOG_FILE, { flags: 'a' });\n\nfunction log(message) {\n const timestamp = new Date().toISOString();\n logStream.write(timestamp + ' [Proxy] ' + message + '\\n');\n}\n\n// Store pending payment requests per wallet hash\n// Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }\n// step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)\nconst pendingPayments = new Map();\n\n// Utility: run shell command and return output\nfunction runCommand(cmd, args = []) {\n return new Promise((resolve, reject) => {\n const child = spawn(cmd, args, { shell: false });\n let stdout = '';\n let stderr = '';\n \n child.stdout.on('data', (data) => { stdout += data.toString(); });\n child.stderr.on('data', (data) => { stderr += data.toString(); });\n \n child.on('close', (code) => {\n if (code === 0) resolve(stdout.trim());\n else reject(new Error(stderr.trim() || 'Command exited with code ' + code));\n });\n \n child.on('error', (err) => reject(err));\n });\n}\n\n// Get paytaca command from environment or default to 'paytaca'\nconst PAYTACA_CMD = process.env.PAYTACA_CMD || 'paytaca';\n\n// Utility: check if paytaca CLI exists\nasync function checkPaytacaCli() {\n try {\n // Try to run version check\n await runCommand(PAYTACA_CMD, ['--version']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Utility: get wallet balance in sats\nasync function getWalletBalance() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Balance:\\s*([\\d.]+)\\s*BCH/i);\n if (match) {\n const bch = parseFloat(match[1]);\n return Math.floor(bch * 100000000);\n }\n return null;\n } catch (err) {\n log('Failed to get wallet balance: ' + err.message);\n return null;\n }\n}\n\n// Utility: get receiving address\nasync function getReceivingAddress() {\n try {\n const output = await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n const match = output.match(/Address:\\s*(bitcoincash:[a-zA-Z0-9]+)/i);\n return match ? match[1] : null;\n } catch {\n return null;\n }\n}\n\n// Utility: check if wallet exists\nasync function checkWallet() {\n try {\n await runCommand(PAYTACA_CMD, ['wallet', 'info']);\n return true;\n } catch {\n return false;\n }\n}\n\n// Format seconds as MM:SS or HH:MM:SS\nfunction formatDuration(totalSeconds) {\n const hours = Math.floor(totalSeconds / 3600);\n const minutes = Math.floor((totalSeconds % 3600) / 60);\n const secs = totalSeconds % 60;\n if (hours > 0) {\n return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');\n }\n return minutes + ':' + String(secs).padStart(2, '0');\n}\n\n// SSE helper: write a data line\nfunction sseLine(res, data) {\n res.write('data: ' + JSON.stringify(data) + '\\n\\n');\n}\n\n// SSE helper: write [DONE]\nfunction sseDone(res) {\n res.write('data: [DONE]\\n\\n');\n}\n\n// Stream the tier-selection prompt body (SSE lines) into an in-progress response.\n// When includeRole is false the leading role delta is skipped, so the body can be\n// appended to a stream that already emitted content (e.g. after a payment failure).\nasync function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole) {\n if (includeRole !== false) {\n sseLine(res, {\n id: 'tier-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n }\n\n // Loading sequence\n sseLine(res, {\n id: 'tier-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u23F3 Initializing Paytaca AI provider...\\n' }, finish_reason: null }],\n });\n\n const hasCli = await checkPaytacaCli();\n sseLine(res, {\n id: 'tier-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-4',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasCli ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const hasWallet = hasCli ? await checkWallet() : false;\n sseLine(res, {\n id: 'tier-5',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'tier-6',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: hasWallet ? '\u2705\\n' : '\u274C Not found\\n' }, finish_reason: null }],\n });\n\n const balanceSats = hasWallet ? await getWalletBalance() : null;\n sseLine(res, {\n id: 'tier-7',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],\n });\n\n let balanceStr;\n if (balanceSats !== null) {\n const bch = (balanceSats / 100000000).toFixed(8);\n balanceStr = bch + ' BCH';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u2705 \u2014 ' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n } else {\n balanceStr = 'Unable to check';\n sseLine(res, {\n id: 'tier-8',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n' + balanceStr + '\\n\\n' }, finish_reason: null }],\n });\n }\n\n // Tier selection\n sseLine(res, {\n id: 'tier-9',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\uD83D\uDCB3 Select a plan for **' + (modelName || 'AI Model') + '**\\n\\n' }, finish_reason: null }],\n });\n\n for (let i = 0; i < tiers.length; i++) {\n const tier = tiers[i];\n const bchAmount = (tier.price_sats / 100000000).toFixed(8);\n const label = '`(' + String(i + 1) + ')` ';\n sseLine(res, {\n id: 'tier-10-' + i,\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes \u2014 PHP ' + tier.price_php.toFixed(2) + ' (' + bchAmount + ' BCH)\\n' }, finish_reason: null }],\n });\n }\n\n sseLine(res, {\n id: 'tier-11',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'tier-12',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n}\n\n// Build and stream a full tier-selection prompt (headers + body + [DONE]) to the client.\nasync function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n }\n await streamTierSelectionBody(res, walletHash, modelName, tiers, true);\n sseDone(res);\n res.end();\n}\n\n// Build and stream SSE loading sequence + payment prompt\n// Stream SSE notice when the upstream (OpenRouter) account lacks balance to fund\n// the request. Replaces the old single-tier yes/no approval prompt.\nasync function streamLowBalanceNotice(res, modelName) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'X-Payment-Required': 'true',\n 'Connection': 'keep-alive',\n });\n\n sseLine(res, {\n id: 'lb-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: modelName || 'AI Model',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n\n sseLine(res, {\n id: 'lb-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\u26A0\uFE0F OpenRouter balance is low \u2014 please top up before continuing.\\n' }, finish_reason: 'stop' }],\n });\n\n sseLine(res, {\n id: 'lb-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n\n sseDone(res);\n res.end();\n}\n\n// Forward request to Django and return response (buffered, for non-streaming)\nfunction forwardToDjango(req, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardToDjango -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n const elapsed = Date.now() - startTime;\n log('Django responded in ' + elapsed + 'ms: status=' + djangoRes.statusCode + ', bodyLen=' + responseBody.length);\n callback(null, djangoRes.statusCode, djangoRes.headers, responseBody);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Forward streaming request to Django\nfunction forwardStreaming(req, res, body, callback) {\n const options = {\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: req.url,\n method: req.method,\n headers: {\n 'Content-Type': req.headers['content-type'] || 'application/json',\n 'X-Wallet-Hash': req.headers['x-wallet-hash'] || '',\n 'Content-Length': Buffer.byteLength(body),\n },\n };\n\n const startTime = Date.now();\n log('forwardStreaming -> ' + options.method + ' ' + options.hostname + ':' + options.port + options.path);\n\n let timeoutCleared = false;\n const djangoReq = REQUester.request(options, (djangoRes) => {\n // Response started; clear the connect/first-byte timeout so slow streams aren't killed.\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n\n const elapsed = Date.now() - startTime;\n log('Django response started in ' + elapsed + 'ms: status=' + djangoRes.statusCode);\n\n if (djangoRes.statusCode === 402) {\n let responseBody = '';\n djangoRes.on('data', chunk => { responseBody += chunk; });\n djangoRes.on('end', () => {\n callback(null, 402, djangoRes.headers, responseBody);\n });\n return;\n }\n\n res.writeHead(djangoRes.statusCode, {\n 'Content-Type': djangoRes.headers['content-type'] || 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n\n if (res.socket) {\n res.socket.setNoDelay(true);\n }\n\n // Buffer SSE data at event boundaries and inject keepalive between events.\n let sseBuffer = '';\n let lastActivity = Date.now();\n let streamingDone = false;\n let doneForwarded = false;\n\n // Watchdog: inject keepalive only when buffer is empty (between complete events)\n const keepaliveTimer = setInterval(() => {\n if (streamingDone || res.writableEnded || res.destroyed) {\n clearInterval(keepaliveTimer);\n return;\n }\n const now = Date.now();\n if (now - lastActivity >= 2000 && sseBuffer.length === 0) {\n try {\n res.write(': keepalive\\n\\n');\n lastActivity = now;\n } catch (err) {\n log('Keepalive write error: ' + err.message);\n clearInterval(keepaliveTimer);\n }\n }\n }, 500);\n\n const cleanup = () => {\n streamingDone = true;\n clearInterval(keepaliveTimer);\n };\n\n var diagCounter = 0;\n djangoRes.on('data', (chunk) => {\n var chunkStr = chunk.toString();\n var chunkIdx = ++diagCounter;\n sseBuffer += chunkStr;\n lastActivity = Date.now();\n\n var okCount = (sseBuffer.match(/:ok/g) || []).length;\n if (okCount > 0) {\n log('CHUNK#' + chunkIdx + ': ' + okCount + ' :ok in buffer (len=' + sseBuffer.length + ')');\n }\n\n // Strip upstream SSE \":ok\" keepalive comments from anywhere in the buffer.\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (okCount > 0) {\n log('AFTER: stripped ' + okCount + ' :ok, buffer len=' + sseBuffer.length);\n }\n\n var extractedCount = 0;\n let idx;\n while ((idx = sseBuffer.indexOf('\\n\\n')) !== -1) {\n const event = sseBuffer.substring(0, idx + 2);\n sseBuffer = sseBuffer.substring(idx + 2);\n const lines = event.split('\\n').filter(l => !/^:/.test(l) && l.length > 0);\n if (lines.length === 0) continue;\n const cleanEvent = lines.join('\\n') + '\\n\\n';\n extractedCount++;\n var dataContent = lines.map(function(l) { return l.replace(/^data: ?/, ''); }).join('');\n if (dataContent === '[DONE]') { doneForwarded = true; }\n var lastChar = dataContent.slice(-1);\n if (dataContent !== '[DONE]' && lastChar !== '}' && lastChar !== ']') {\n log('FLUSH: truncated event #' + extractedCount + ' (len=' + dataContent.length + ', end=' + JSON.stringify(dataContent.slice(-30)) + ')');\n }\n try {\n res.write(cleanEvent);\n } catch (err) {\n cleanup();\n log('Write error: ' + err.message);\n return;\n }\n }\n if (extractedCount > 0) {\n log('EXTRACT: forwarded ' + extractedCount + ' events in chunk#' + chunkIdx + ', buffer remaining len=' + sseBuffer.length);\n }\n });\n\n djangoRes.on('end', () => {\n if (streamingDone) {\n return;\n }\n sseBuffer = sseBuffer.replace(/:ok(?:\\n)?/g, '');\n if (sseBuffer) {\n // Ensure the final written data ends with \\n\\n so the client recognizes the event boundary\n if (sseBuffer.length < 2 || sseBuffer.substring(sseBuffer.length - 2) !== '\\n\\n') {\n sseBuffer += '\\n\\n';\n }\n log('END: writing remaining buffer len=' + sseBuffer.length + ' start=' + JSON.stringify(sseBuffer.substring(0, 80)));\n try { res.write(sseBuffer); } catch (e) {}\n }\n if (!doneForwarded) {\n log('Injecting [DONE] \u2014 upstream closed without sending it');\n try { res.write('data: [DONE]\\n\\n'); } catch (e) {}\n }\n cleanup();\n try { res.end(); } catch (e) {}\n log('Streaming response completed' + (doneForwarded ? '' : ' (injected [DONE])'));\n callback(null, djangoRes.statusCode, {}, '');\n });\n\n djangoRes.on('error', (err) => {\n log('Django stream error: ' + err.message);\n if (!streamingDone) {\n cleanup();\n }\n if (!res.writableEnded) {\n try {\n res.end();\n } catch (e) {}\n }\n callback(null, 200, {}, '');\n });\n\n res.on('close', () => {\n cleanup();\n log('Client connection closed');\n });\n\n res.on('error', (err) => {\n cleanup();\n log('Client connection error: ' + err.message);\n });\n });\n\n djangoReq.setTimeout(300000, () => {\n djangoReq.destroy();\n callback(new Error('Django streaming request timed out after 300s'));\n });\n\n djangoReq.on('error', (err) => {\n if (!timeoutCleared) {\n djangoReq.clearTimeout();\n timeoutCleared = true;\n }\n log('Django streaming request error: ' + err.message);\n callback(err);\n });\n\n djangoReq.write(body);\n djangoReq.end();\n}\n\n// Force stream=false in body because paytaca pay reads the response as text\nfunction forceNonStreaming(body) {\n try {\n const data = JSON.parse(body);\n data.stream = false;\n return JSON.stringify(data);\n } catch {\n return body;\n }\n}\n\n// Convert a chat.completion JSON object to SSE format\nfunction jsonToSse(res, chatCompletion, opts) {\n opts = opts || {};\n if (res.destroyed || res.writableEnded) {\n log('jsonToSse: response already destroyed/ended, cannot send SSE');\n return;\n }\n const message = chatCompletion.choices?.[0]?.message || {};\n const content = message.content || '';\n const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : null;\n const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';\n const created = chatCompletion.created || Math.floor(Date.now() / 1000);\n\n if (!res.headersSent) {\n try {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n });\n } catch (e) {\n log('jsonToSse writeHead failed: ' + e.message);\n return;\n }\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-1',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n } catch (e) {\n log('jsonToSse: failed to write role delta: ' + e.message);\n }\n\n const allContent = (opts.prependContent || '') + content;\n const chunkSize = 20;\n let chunksWritten = 0;\n for (let i = 0; i < allContent.length; i += chunkSize) {\n try {\n sseLine(res, {\n id: 'chatcmpl-' + (i + 2),\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { content: allContent.slice(i, i + chunkSize) }, finish_reason: null }],\n });\n chunksWritten++;\n } catch (e) {\n log('jsonToSse: failed to write content chunk ' + (i / chunkSize) + ': ' + e.message);\n break;\n }\n }\n\n let finishReason = 'stop';\n if (toolCalls && toolCalls.length > 0) {\n const toolCallDeltas = [];\n for (let i = 0; i < toolCalls.length; i++) {\n const tc = toolCalls[i] || {};\n const fn = tc.function || {};\n let args = fn.arguments;\n if (args !== undefined && typeof args !== 'string') {\n try { args = JSON.stringify(args); } catch (e) { args = String(args); }\n }\n toolCallDeltas.push({\n index: i,\n id: tc.id || ('call_' + i),\n type: 'function',\n function: {\n name: fn.name || '',\n arguments: args === undefined || args === null ? '' : String(args),\n },\n });\n }\n try {\n sseLine(res, {\n id: 'chatcmpl-tools',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: { tool_calls: toolCallDeltas }, finish_reason: null }],\n });\n } catch (e) {\n log('jsonToSse: failed to write tool_calls: ' + e.message);\n }\n finishReason = 'tool_calls';\n }\n\n try {\n sseLine(res, {\n id: 'chatcmpl-done',\n object: 'chat.completion.chunk',\n created,\n model,\n choices: [{ index: 0, delta: {}, finish_reason: finishReason }],\n usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n } catch (e) {\n log('jsonToSse: failed to write final delta: ' + e.message);\n }\n\n try {\n sseDone(res);\n } catch (e) {\n log('jsonToSse: failed to write [DONE]: ' + e.message);\n }\n\n try {\n res.end();\n } catch (e) {\n log('jsonToSse: res.end() failed: ' + e.message);\n }\n}\n\n// Stream a payment-failure message, then re-show the tier-selection prompt so the\n// user can retry the same or a different plan without sending another message.\n// The pending payment is restored to the tier-select step so the next tier pick is\n// handled by the proxy instead of being forwarded fresh to Django.\nasync function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, message) {\n try {\n sseLine(res, {\n id: 'pay-err',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { content: message }, finish_reason: 'stop' }],\n });\n } catch (e) {\n }\n pendingPayload.step = 'tier_select';\n pendingPayload.durationMinutes = null;\n pendingPayments.set(walletHash, pendingPayload);\n try {\n const tiers = Array.isArray(pendingPayload.tiers) ? pendingPayload.tiers : [];\n if (tiers.length > 0) {\n await streamTierSelectionBody(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', tiers, false);\n }\n sseDone(res);\n res.end();\n } catch (e) {\n try { res.end(); } catch (e2) {}\n }\n}\n\n// Run paytaca pay internally and return the response\nfunction runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {\n const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');\n const payBody = forceNonStreaming(body);\n\n // Write body to a temp file to avoid CLI arg length limits\n const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'paytaca-pay-'));\n const bodyFile = path.join(tmpDir, 'body.json');\n const configFile = path.join(tmpDir, 'config.json');\n\n try {\n fs.writeFileSync(bodyFile, payBody, 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp body file: ' + err.message));\n }\n\n const config = {\n url,\n method: 'POST',\n headers: Object.assign({ 'Content-Type': 'application/json' }, extraHeaders || {}),\n bodyFile,\n confirmed: true,\n };\n\n try {\n fs.writeFileSync(configFile, JSON.stringify(config), 'utf8');\n } catch (err) {\n return callback(new Error('Failed to write temp config file: ' + err.message));\n }\n\n // Path to the wrapper script\n const wrapperScript = path.join(LOG_DIR, 'paytaca-pay-wrapper.mjs');\n log('Running paytaca pay via wrapper script...');\n\n const child = spawn('node', [wrapperScript, configFile], { shell: false });\n let stdout = '';\n let stderr = '';\n\n child.stdout.on('data', (data) => { \n stdout += data.toString(); \n });\n child.stderr.on('data', (data) => { \n stderr += data.toString(); \n });\n\n child.on('close', (code) => {\n // Clean up temp files\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n\n if (code === 0) {\n try {\n const responseJson = JSON.parse(stdout.trim());\n callback(null, responseJson);\n } catch (err) {\n callback(new Error('Could not parse paytaca pay response: ' + err.message));\n }\n } else {\n // Try to extract error from stdout (wrapper writes JSON errors to stdout, not stderr)\n let wrapperErr = stderr.trim();\n if (!wrapperErr) {\n try {\n const parsed = JSON.parse(stdout.trim());\n wrapperErr = parsed.error || 'Unknown error';\n } catch {\n wrapperErr = stdout.trim() || 'paytaca pay wrapper exited with code ' + code;\n }\n }\n callback(new Error(wrapperErr));\n }\n });\n\n child.on('error', (err) => {\n // Clean up temp files on error\n try {\n fs.unlinkSync(bodyFile);\n fs.unlinkSync(configFile);\n fs.rmdirSync(tmpDir);\n } catch {}\n callback(new Error('Failed to run paytaca pay wrapper: ' + err.message));\n });\n}\n\n// Extract the last user message content from a chat payload\nfunction getLastUserMessageContent(body) {\n try {\n const data = JSON.parse(body);\n const messages = data.messages || [];\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === 'user') {\n const content = messages[i].content;\n if (Array.isArray(content)) {\n const parts = [];\n for (const part of content) {\n if (part && typeof part === 'object' && part.type === 'text') {\n parts.push(part.text || '');\n } else if (typeof part === 'string') {\n parts.push(part);\n } else {\n parts.push(JSON.stringify(part));\n }\n }\n return parts.join('').trim().toLowerCase();\n }\n return String(content || '').trim().toLowerCase();\n }\n }\n return '';\n } catch {\n return '';\n }\n}\n\nasync function handleTimeCreditsCommand(res, walletHash) {\n log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');\n const statusUrl = BACKEND_URL + '/v1/wallet/status';\n const statusRes = await fetch(statusUrl, {\n headers: { 'X-Wallet-Hash': walletHash }\n });\n let content;\n if (statusRes.ok) {\n const statusData = await statusRes.json();\n const sessions = statusData.sessions || [];\n const activeSessions = sessions.filter(s => s.time_remaining_seconds > 0 && s.model_active);\n const inactiveSessions = sessions.filter(s => s.time_remaining_seconds > 0 && !s.model_active);\n const parts = [];\n if (activeSessions.length > 0) {\n parts.push('**\u23F1\uFE0F Active Time Credits:**');\n activeSessions.forEach(s => {\n const total = formatDuration(s.time_credits_seconds);\n const remaining = formatDuration(s.time_remaining_seconds);\n const used = formatDuration(s.time_used_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + '** \u2014 ' + remaining + ' remaining of ' + total + ' (' + used + ' used)');\n });\n }\n if (inactiveSessions.length > 0) {\n parts.push('\\n**\u26A0\uFE0F Inactive Model:**');\n inactiveSessions.forEach(s => {\n const remaining = formatDuration(s.time_remaining_seconds);\n parts.push(' - **' + (s.display_name || s.ai_model) + ' (Inactive)** \u2014 ' + remaining + ' remaining');\n });\n }\n content = parts.length > 0 ? parts.join('\\n') : '\u23F1\uFE0F No active time credits.';\n } else {\n content = '\u23F1\uFE0F Unable to check time credits.';\n }\n\n sseLine(res, {\n id: 'time-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'time-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'time-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\nconst isTimeCmd = (s) => s === 'credits';\nconst isPricingCmd = (s) => s === 'plans';\n\n// List all models grouped by tier (Budget / Premium / Frontier / Other) with prices\nasync function handlePricingCommand(res) {\n log('Pricing command requested');\n let content;\n try {\n const configRes = await fetch(BACKEND_URL + '/v1/config');\n if (!configRes.ok) {\n throw new Error('config status ' + configRes.status);\n }\n const config = await configRes.json();\n const models = Array.isArray(config.models) ? config.models : [];\n const groups = { budget: [], premium: [], frontier: [], other: [] };\n for (const m of models) {\n const key = String(m.tier || '').toLowerCase();\n const groupKey = (key === 'budget' || key === 'premium' || key === 'frontier') ? key : 'other';\n groups[groupKey].push(m);\n }\n const lines = ['\uD83D\uDCCB Paytaca AI \u2014 Model Pricing'];\n const order = [\n { key: 'budget', label: 'Budget' },\n { key: 'premium', label: 'Premium' },\n { key: 'frontier', label: 'Frontier' },\n { key: 'other', label: 'Other' },\n ];\n let any = false;\n for (const g of order) {\n if (groups[g.key].length === 0) continue;\n any = true;\n lines.push('');\n lines.push(g.label);\n for (const m of groups[g.key]) {\n lines.push('');\n const tiers = Array.isArray(m.price_tiers) ? m.price_tiers : [];\n if (tiers.length === 0) {\n lines.push('- **' + (m.display_name || m.id) + '**: \u2014 no pricing configured');\n continue;\n }\n const sorted = tiers.slice().sort((a, b) => (a.minutes || 0) - (b.minutes || 0));\n lines.push('**' + (m.display_name || m.id) + '**:');\n sorted.forEach((t, i) => {\n const sats = typeof t.price_sats === 'number' ? t.price_sats : 0;\n const bch = (sats / 100000000).toFixed(8);\n const php = typeof t.price_php === 'number' ? t.price_php.toFixed(2) : '?.??';\n lines.push(' `(' + String(i + 1) + ')` ' + (t.minutes || 0) + ' minutes \u2014 PHP ' + php + ' (' + bch + ' BCH)');\n });\n }\n }\n if (!any) {\n lines.push('');\n lines.push('No models available.');\n }\n content = lines.join('\\n');\n } catch (err) {\n log('Pricing command failed: ' + err.message);\n content = '\uD83D\uDCCB Unable to fetch pricing.';\n }\n\n sseLine(res, {\n id: 'price-1',\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: 'deepseek/deepseek-v4-flash',\n choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],\n });\n sseLine(res, {\n id: 'price-2',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'price-3',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n });\n sseDone(res);\n res.end();\n}\n\n// Main proxy server\nconst server = http.createServer(async (req, res) => {\n // Enable CORS\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, X-Model-Id, X-Duration-Minutes, Payment-Signature, Authorization');\n \n if (req.method === 'OPTIONS') {\n res.writeHead(200);\n res.end();\n return;\n }\n \n // Discovery endpoint - fetch from backend to get actual config\n if (req.url === '/v1/config' && req.method === 'GET') {\n try {\n const backendConfig = await fetch(BACKEND_URL + '/v1/config');\n if (backendConfig.ok) {\n const config = await backendConfig.json();\n // Add proxy-specific info\n config.proxy_url = 'http://localhost:' + PROXY_PORT + '/v1';\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(config));\n return;\n }\n } catch (err) {\n log('Failed to fetch backend config: ' + err.message);\n }\n \n // Fallback to static values if backend unavailable\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',\n django_url: BACKEND_URL + '/v1',\n payment_address: '',\n default_model: 'deepseek/deepseek-v4-flash',\n default_duration_minutes: 30,\n models: [\n {\n id: 'deepseek/deepseek-v4-flash',\n object: 'model',\n display_name: 'DeepSeek V4 Flash',\n provider: 'openrouter',\n price_tiers: [\n { minutes: 10, price_php: 5.0, price_sats: 45000 },\n { minutes: 30, price_php: 12.0, price_sats: 108000 },\n { minutes: 60, price_php: 20.0, price_sats: 180000 },\n ],\n },\n ],\n context_retention_hours: 2,\n }));\n return;\n }\n \n // All other endpoints \u2014 read body and forward to Django\n let body = '';\n req.on('data', chunk => { body += chunk; });\n req.on('end', async () => {\n try {\n const walletHash = req.headers['x-wallet-hash'];\n const lastContent = getLastUserMessageContent(body);\n \n log('Request received: wallet=' + (walletHash?.substring(0, 16) || 'none') + '..., bodyLen=' + body.length + ', pending=' + pendingPayments.has(walletHash));\n \n const stripSysRem = (s) => { let r = (s || ''), a = '<system-reminder>', b = '</system-reminder>', i = r.indexOf(a); while (i !== -1) { let j = r.indexOf(b, i); if (j === -1) break; r = r.substring(0, i) + r.substring(j + b.length); i = r.indexOf(a); } return r.trim(); };\n \n // Guard: wallet hash is required for payment flow\n if (!walletHash) {\n const redactedHeaders = {};\n for (const [k, v] of Object.entries(req.headers)) {\n const lk = k.toLowerCase();\n redactedHeaders[k] = /authorization|payment-signature|api-?key|secret|token/i.test(lk)\n ? '<redacted>'\n : v;\n }\n log('MISSING X-Wallet-Hash. Received headers: ' + JSON.stringify(redactedHeaders));\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({\n error: 'X-Wallet-Hash header missing',\n message: 'The X-Wallet-Hash header was not sent by the client. It is injected by the paytaca opencode plugin (provider options.headers / chat.headers). Reinstall or restart the plugin, or run paytaca wallet info and verify the plugin loaded.',\n }));\n return;\n }\n \n // Check if there's a pending payment for this wallet\n var pendingPayload = pendingPayments.get(walletHash);\n \n // If there's a pending payment for a different model, clear it so the\n // new request can be forwarded fresh to Django. This prevents the\n // proxy from re-showing a stale payment prompt when the user switches\n // to a different model mid-conversation.\n if (pendingPayload) {\n var reqModel = '';\n try { reqModel = JSON.parse(body).model || ''; } catch (e) {}\n if (reqModel && pendingPayload.modelId && reqModel !== pendingPayload.modelId) {\n pendingPayments.delete(walletHash);\n pendingPayload = null;\n }\n }\n \n if (pendingPayload) {\n // Check for tier selection first\n if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {\n const userInput = stripSysRem(lastContent);\n const timeCmd = userInput?.trim().toLowerCase();\n if (isTimeCmd(timeCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(timeCmd)) {\n await handlePricingCommand(res);\n return;\n }\n let selectedIndex = -1;\n \n // Try to parse user input as a number (1-based)\n const num = parseInt(userInput, 10);\n if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {\n selectedIndex = num - 1;\n } else {\n // Try to match by duration minutes\n for (let i = 0; i < pendingPayload.tiers.length; i++) {\n if (userInput === String(pendingPayload.tiers[i].minutes) ||\n userInput === pendingPayload.tiers[i].minutes + ' minutes' ||\n userInput === pendingPayload.tiers[i].minutes + ' min') {\n selectedIndex = i;\n break;\n }\n }\n }\n \n if (selectedIndex >= 0) {\n const selectedTier = pendingPayload.tiers[selectedIndex];\n pendingPayload.durationMinutes = selectedTier.minutes;\n pendingPayload.step = 'processing';\n \n log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');\n \n // Build extra headers for payment wrapper\n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);\n \n // Check wallet balance before attempting payment\n const currentBalanceSats = await getWalletBalance();\n if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {\n log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');\n pendingPayments.delete(walletHash);\n const addr = await getReceivingAddress();\n const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;\n const neededLine = addr ? '\\n\\n\uD83D\uDCE5 **Fund your wallet:** \\`' + addr + '\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';\n sseLine(res, {\n id: 'balance-err',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: { content: '\\n\\n\u274C **Insufficient balance** \u2014 You have **' + (currentBalanceSats / 100000000).toFixed(8) + ' BCH** but need **' + (selectedTier.price_sats / 100000000).toFixed(8) + ' BCH** for this plan. Top up at least **' + neededBch.toFixed(8) + ' BCH** more.' + neededLine + '\\n\\nType \\`balance\\` to re-check or try a different plan:' }, finish_reason: 'stop' }],\n });\n sseLine(res, {\n id: 'balance-err-done',\n object: 'chat.completion.chunk',\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n });\n sseDone(res);\n res.end();\n return;\n }\n \n // Keepalive during payment processing\n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, async (err, responseJson) => {\n pendingPayments.delete(walletHash);\n clearInterval(keepalive);\n\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, '\\n\\n\u274C Payment failed: ' + err.message + '\\n\\n');\n } else if (!res.headersSent) {\n try {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n } catch (e) { log('Failed to send payment error response: ' + e.message); }\n } else {\n log('Cannot send payment failure \u2014 response already ended or destroyed');\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error') + '\\n\\n';\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n if (isTimeout) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send timeout error via SSE: ' + e.message); }\n } else {\n await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, sseContent);\n }\n } else if (!res.headersSent) {\n try {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n } catch (e) { log('Failed to send payment error JSON: ' + e.message); }\n } else {\n log('Cannot send payment error \u2014 response already ended or destroyed');\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n \n if (res.destroyed || res.writableEnded) {\n log('Payment succeeded but response connection is gone \u2014 cannot deliver chat response');\n return;\n }\n\n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion, { prependContent: '\\n\uD83D\uDCB3 Payment successful \u2014 generating your response...\\n\\n' });\n } catch (e) { log('jsonToSse threw: ' + e.message); }\n } else {\n try {\n if (!res.headersSent) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n }\n res.end(JSON.stringify(chatCompletion));\n } catch (e) { log('Failed to send non-streaming response: ' + e.message); }\n }\n });\n return;\n } else {\n // Invalid selection \u2014 reshow the prompt\n log('Invalid tier selection for wallet ' + walletHash?.substring(0, 16) + '...');\n await streamTierSelectionPrompt(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', pendingPayload.tiers);\n return;\n }\n }\n \n // Old flow: user responded to a yes/no payment prompt\n if (stripSysRem(lastContent) === 'yes') {\n log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n \n const extraHeaders = {};\n if (pendingPayload.modelId) {\n extraHeaders['X-Model-Id'] = pendingPayload.modelId;\n }\n if (pendingPayload.durationMinutes) {\n extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);\n }\n \n if (!res.headersSent) {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n 'X-Payment-Processing': 'true',\n });\n }\n const keepalive = setInterval(() => {\n if (res.destroyed || res.writableEnded) { clearInterval(keepalive); return; }\n res.write(': keepalive\\n\\n');\n }, 2000);\n\n runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {\n clearInterval(keepalive);\n if (err) {\n log('paytaca pay failed: ' + err.message);\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n\u274C Payment failed: ' + err.message }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send payment error via SSE: ' + e.message); }\n } else if (!res.headersSent) {\n try {\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));\n } catch (e) { log('Failed to send payment error JSON: ' + e.message); }\n } else {\n log('Cannot send payment failure \u2014 response already ended or destroyed');\n }\n return;\n }\n \n if (!responseJson.success) {\n const isTimeout = responseJson.timeout;\n const sseContent = isTimeout ? '\\n\\n\u23F1\uFE0F Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n\u274C Payment failed: ' + (responseJson.error || 'Unknown error');\n const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';\n const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;\n const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';\n if (res.headersSent && !res.destroyed && !res.writableEnded) {\n try {\n sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });\n sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });\n sseDone(res);\n res.end();\n } catch (e) { log('Failed to send error via SSE: ' + e.message); }\n } else if (!res.headersSent) {\n try {\n res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));\n } catch (e) { log('Failed to send error JSON: ' + e.message); }\n } else {\n log('Cannot send payment error \u2014 response already ended or destroyed');\n }\n return;\n }\n \n const chatCompletion = responseJson?.data || responseJson;\n \n let wasStreaming = false;\n try {\n wasStreaming = JSON.parse(pendingPayload.body).stream === true;\n } catch {}\n \n log('paytaca pay succeeded. Returning chat response.');\n\n if (res.destroyed || res.writableEnded) {\n log('Payment succeeded but response connection is gone \u2014 cannot deliver chat response');\n return;\n }\n \n if (wasStreaming) {\n try {\n jsonToSse(res, chatCompletion, { prependContent: '\\n\uD83D\uDCB3 Payment successful \u2014 generating your response...\\n\\n' });\n } catch (e) { log('jsonToSse threw: ' + e.message); }\n } else {\n try {\n if (!res.headersSent) {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n }\n res.end(JSON.stringify(chatCompletion));\n } catch (e) { log('Failed to send non-streaming response: ' + e.message); }\n }\n });\n return;\n \n } else if (stripSysRem(lastContent) === 'no') {\n log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');\n pendingPayments.delete(walletHash);\n\n const addr = await getReceivingAddress();\n const fundMsg = addr\n ? 'Fund your wallet: ' + addr\n : 'You can fund your wallet by running: paytaca receive';\n\n const declineCompletion = {\n id: 'payment-declined',\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: pendingPayload.modelId || 'deepseek/deepseek-v4-flash',\n choices: [{\n index: 0,\n message: {\n role: 'assistant',\n content: 'Payment declined. Chat cannot continue without funding.\\n\\n' + fundMsg,\n },\n finish_reason: 'stop',\n }],\n usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },\n };\n jsonToSse(res, declineCompletion);\n return;\n \n } else {\n const innerCmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(innerCmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n if (isPricingCmd(innerCmd)) {\n await handlePricingCommand(res);\n return;\n }\n log('New message while payment pending for wallet ' + walletHash?.substring(0, 16) + '...');\n }\n }\n \n // Handle credits command \u2014 show remaining time credits\n const cmd = stripSysRem(lastContent?.trim().toLowerCase());\n if (isTimeCmd(cmd)) {\n await handleTimeCreditsCommand(res, walletHash);\n return;\n }\n // Handle pricing command \u2014 show all models grouped by tier\n if (isPricingCmd(cmd)) {\n await handlePricingCommand(res);\n return;\n }\n \n let isStreaming = true;\n try { isStreaming = JSON.parse(body).stream !== false; } catch {}\n\n const handleResponse = async (err, statusCode, headers, responseBody) => {\n if (err) {\n if (!res.headersSent) {\n log('Django connection error: ' + err.message);\n res.writeHead(502, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Backend unavailable', details: err.message }));\n }\n return;\n }\n\n if (statusCode === 402) {\n let requestModel = null;\n try { requestModel = JSON.parse(body).model || null; } catch (e) {}\n log('402 intercepted for wallet ' + walletHash?.substring(0, 16)\n + ' x-model-id=' + (req.headers['x-model-id'] || 'null')\n + ' body.model=' + (requestModel || 'null'));\n \n // Parse 402 response for model_id and price_tiers\n let modelId = null;\n let displayName = null;\n let tiers = null;\n try {\n const parsed = JSON.parse(responseBody);\n modelId = parsed.model_id || null;\n displayName = parsed.display_name || null;\n tiers = parsed.price_tiers || null;\n log('402 body: model=' + (modelId || 'null')\n + ' display=' + (displayName || 'null')\n + ' tiers=' + (Array.isArray(tiers) ? tiers.length : String(tiers))\n + ' reason=' + (parsed.reason || 'n/a')\n + ' bodyPrefix=' + responseBody.substring(0, 160).replace(/\\n/g, ' '));\n } catch (e) {\n log('Could not parse 402 body: ' + e.message);\n }\n \n pendingPayments.set(walletHash, {\n body: body,\n modelId: modelId,\n displayName: displayName,\n durationMinutes: null,\n tiers: tiers,\n step: tiers ? 'tier_select' : 'approval'\n });\n \n if (tiers && tiers.length > 0) {\n // New flow: show tier selection prompt\n await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);\n return;\n }\n \n // Check session status to determine if this is a renewal\n let isRenewal = false;\n let tokensUsed = 0;\n let tokenLimit = 50000;\n let timeRemainingSeconds = 0;\n \n let statusModelId = modelId;\n let statusSnapshot = null;\n try {\n // Extract model from the original request body if not in 402\n if (!statusModelId) {\n try {\n const bodyParsed = JSON.parse(body);\n statusModelId = bodyParsed.model || null;\n } catch (e) {}\n }\n \n const statusPath = '/v1/wallet/status' + (statusModelId ? '?model_id=' + encodeURIComponent(statusModelId) : '');\n const statusResponse = await new Promise((resolve, reject) => {\n const statusReq = REQUester.get({\n hostname: DJANGO_HOST,\n port: DJANGO_PORT,\n path: statusPath,\n headers: { 'X-Wallet-Hash': walletHash }\n }, (res) => {\n let data = '';\n res.on('data', chunk => data += chunk);\n res.on('end', () => {\n try {\n resolve(JSON.parse(data));\n } catch {\n resolve({});\n }\n });\n });\n statusReq.on('error', reject);\n statusReq.setTimeout(5000, () => reject(new Error('timeout')));\n });\n \n if (statusResponse) {\n statusSnapshot = statusResponse;\n tokensUsed = statusResponse.tokens_used || 0;\n tokenLimit = statusResponse.token_limit || 50000;\n timeRemainingSeconds = statusResponse.time_remaining_seconds || 0;\n \n // Renewal if session has been used (tokens > 0 or time > 0) but is now exhausted\n isRenewal = (tokensUsed > 0 || statusResponse.time_used_seconds > 0) &&\n (!statusResponse.session_active || timeRemainingSeconds <= 0);\n }\n } catch (err) {\n log('Failed to check session status: ' + err.message);\n }\n \n log('402 status model=' + (statusModelId || 'null')\n + ' snapshot=' + JSON.stringify(statusSnapshot)\n + ' isRenewal=' + isRenewal\n + ' timeRemaining=' + timeRemainingSeconds\n + ' tokensUsed=' + tokensUsed\n + ' tokenLimit=' + tokenLimit);\n \n await streamLowBalanceNotice(res, displayName || statusModelId || modelId || 'AI Model');\n } else {\n if (res.headersSent) {\n log('Streaming response completed and already sent');\n pendingPayments.delete(walletHash);\n return;\n }\n\n log('Forwarding normal response to OpenCode: status=' + statusCode + ', bodyLen=' + responseBody.length);\n pendingPayments.delete(walletHash);\n res.writeHead(statusCode, {\n 'Content-Type': headers['content-type'] || 'application/json',\n });\n res.end(responseBody);\n }\n };\n\n if (isStreaming) {\n forwardStreaming(req, res, body, handleResponse);\n } else {\n forwardToDjango(req, body, handleResponse);\n }\n \n } catch (err) {\n log('Error: ' + err.message);\n res.writeHead(500, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Internal proxy error' }));\n }\n });\n});\n\nserver.on('error', (err) => {\n if (err.code === 'EADDRINUSE') {\n log('Port ' + PROXY_PORT + ' is already in use. Another proxy instance may be running.');\n log('Exiting cleanly (code 0) so the plugin can detect the existing proxy.');\n process.exit(0);\n }\n log('Server error: ' + err.message);\n process.exit(1);\n});\n\nserver.listen(PROXY_PORT, () => {\n log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);\n log('Forwarding to ' + BACKEND_URL);\n log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');\n log('Managed by OpenCode plugin');\n});\n\n// Graceful shutdown\nprocess.on('SIGTERM', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n\nprocess.on('SIGINT', () => {\n log('Shutting down proxy...');\n server.close(() => process.exit(0));\n});\n";
|
|
2
2
|
//# sourceMappingURL=proxy.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,oBAAoB,u40DA6/ChC,CAAC"}
|
package/dist/bundled/proxy.js
CHANGED
|
@@ -141,22 +141,19 @@ function sseDone(res) {
|
|
|
141
141
|
res.write('data: [DONE]\\n\\n');
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
model: modelName,
|
|
158
|
-
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
159
|
-
});
|
|
144
|
+
// Stream the tier-selection prompt body (SSE lines) into an in-progress response.
|
|
145
|
+
// When includeRole is false the leading role delta is skipped, so the body can be
|
|
146
|
+
// appended to a stream that already emitted content (e.g. after a payment failure).
|
|
147
|
+
async function streamTierSelectionBody(res, walletHash, modelName, tiers, includeRole) {
|
|
148
|
+
if (includeRole !== false) {
|
|
149
|
+
sseLine(res, {
|
|
150
|
+
id: 'tier-1',
|
|
151
|
+
object: 'chat.completion.chunk',
|
|
152
|
+
created: Math.floor(Date.now() / 1000),
|
|
153
|
+
model: modelName,
|
|
154
|
+
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
155
|
+
});
|
|
156
|
+
}
|
|
160
157
|
|
|
161
158
|
// Loading sequence
|
|
162
159
|
sseLine(res, {
|
|
@@ -244,7 +241,19 @@ async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
|
|
|
244
241
|
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
245
242
|
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
246
243
|
});
|
|
244
|
+
}
|
|
247
245
|
|
|
246
|
+
// Build and stream a full tier-selection prompt (headers + body + [DONE]) to the client.
|
|
247
|
+
async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
|
|
248
|
+
if (!res.headersSent) {
|
|
249
|
+
res.writeHead(200, {
|
|
250
|
+
'Content-Type': 'text/event-stream',
|
|
251
|
+
'Cache-Control': 'no-cache',
|
|
252
|
+
'X-Payment-Required': 'true',
|
|
253
|
+
'Connection': 'keep-alive',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
await streamTierSelectionBody(res, walletHash, modelName, tiers, true);
|
|
248
257
|
sseDone(res);
|
|
249
258
|
res.end();
|
|
250
259
|
}
|
|
@@ -536,8 +545,15 @@ function forceNonStreaming(body) {
|
|
|
536
545
|
}
|
|
537
546
|
|
|
538
547
|
// Convert a chat.completion JSON object to SSE format
|
|
539
|
-
function jsonToSse(res, chatCompletion) {
|
|
540
|
-
|
|
548
|
+
function jsonToSse(res, chatCompletion, opts) {
|
|
549
|
+
opts = opts || {};
|
|
550
|
+
if (res.destroyed || res.writableEnded) {
|
|
551
|
+
log('jsonToSse: response already destroyed/ended, cannot send SSE');
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const message = chatCompletion.choices?.[0]?.message || {};
|
|
555
|
+
const content = message.content || '';
|
|
556
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : null;
|
|
541
557
|
const model = chatCompletion.model || chatCompletion.model_id || 'deepseek/deepseek-v4-flash';
|
|
542
558
|
const created = chatCompletion.created || Math.floor(Date.now() / 1000);
|
|
543
559
|
|
|
@@ -549,6 +565,7 @@ function jsonToSse(res, chatCompletion) {
|
|
|
549
565
|
'Connection': 'keep-alive',
|
|
550
566
|
});
|
|
551
567
|
} catch (e) {
|
|
568
|
+
log('jsonToSse writeHead failed: ' + e.message);
|
|
552
569
|
return;
|
|
553
570
|
}
|
|
554
571
|
}
|
|
@@ -562,45 +579,115 @@ function jsonToSse(res, chatCompletion) {
|
|
|
562
579
|
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
563
580
|
});
|
|
564
581
|
} catch (e) {
|
|
582
|
+
log('jsonToSse: failed to write role delta: ' + e.message);
|
|
565
583
|
}
|
|
566
584
|
|
|
585
|
+
const allContent = (opts.prependContent || '') + content;
|
|
567
586
|
const chunkSize = 20;
|
|
568
587
|
let chunksWritten = 0;
|
|
569
|
-
for (let i = 0; i <
|
|
588
|
+
for (let i = 0; i < allContent.length; i += chunkSize) {
|
|
570
589
|
try {
|
|
571
590
|
sseLine(res, {
|
|
572
591
|
id: 'chatcmpl-' + (i + 2),
|
|
573
592
|
object: 'chat.completion.chunk',
|
|
574
593
|
created,
|
|
575
594
|
model,
|
|
576
|
-
choices: [{ index: 0, delta: { content:
|
|
595
|
+
choices: [{ index: 0, delta: { content: allContent.slice(i, i + chunkSize) }, finish_reason: null }],
|
|
577
596
|
});
|
|
578
597
|
chunksWritten++;
|
|
579
598
|
} catch (e) {
|
|
599
|
+
log('jsonToSse: failed to write content chunk ' + (i / chunkSize) + ': ' + e.message);
|
|
580
600
|
break;
|
|
581
601
|
}
|
|
582
602
|
}
|
|
583
603
|
|
|
604
|
+
let finishReason = 'stop';
|
|
605
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
606
|
+
const toolCallDeltas = [];
|
|
607
|
+
for (let i = 0; i < toolCalls.length; i++) {
|
|
608
|
+
const tc = toolCalls[i] || {};
|
|
609
|
+
const fn = tc.function || {};
|
|
610
|
+
let args = fn.arguments;
|
|
611
|
+
if (args !== undefined && typeof args !== 'string') {
|
|
612
|
+
try { args = JSON.stringify(args); } catch (e) { args = String(args); }
|
|
613
|
+
}
|
|
614
|
+
toolCallDeltas.push({
|
|
615
|
+
index: i,
|
|
616
|
+
id: tc.id || ('call_' + i),
|
|
617
|
+
type: 'function',
|
|
618
|
+
function: {
|
|
619
|
+
name: fn.name || '',
|
|
620
|
+
arguments: args === undefined || args === null ? '' : String(args),
|
|
621
|
+
},
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
try {
|
|
625
|
+
sseLine(res, {
|
|
626
|
+
id: 'chatcmpl-tools',
|
|
627
|
+
object: 'chat.completion.chunk',
|
|
628
|
+
created,
|
|
629
|
+
model,
|
|
630
|
+
choices: [{ index: 0, delta: { tool_calls: toolCallDeltas }, finish_reason: null }],
|
|
631
|
+
});
|
|
632
|
+
} catch (e) {
|
|
633
|
+
log('jsonToSse: failed to write tool_calls: ' + e.message);
|
|
634
|
+
}
|
|
635
|
+
finishReason = 'tool_calls';
|
|
636
|
+
}
|
|
637
|
+
|
|
584
638
|
try {
|
|
585
639
|
sseLine(res, {
|
|
586
640
|
id: 'chatcmpl-done',
|
|
587
641
|
object: 'chat.completion.chunk',
|
|
588
642
|
created,
|
|
589
643
|
model,
|
|
590
|
-
choices: [{ index: 0, delta: {}, finish_reason:
|
|
644
|
+
choices: [{ index: 0, delta: {}, finish_reason: finishReason }],
|
|
591
645
|
usage: chatCompletion.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
592
646
|
});
|
|
593
647
|
} catch (e) {
|
|
648
|
+
log('jsonToSse: failed to write final delta: ' + e.message);
|
|
594
649
|
}
|
|
595
650
|
|
|
596
651
|
try {
|
|
597
652
|
sseDone(res);
|
|
598
653
|
} catch (e) {
|
|
654
|
+
log('jsonToSse: failed to write [DONE]: ' + e.message);
|
|
599
655
|
}
|
|
600
656
|
|
|
601
657
|
try {
|
|
602
658
|
res.end();
|
|
603
659
|
} catch (e) {
|
|
660
|
+
log('jsonToSse: res.end() failed: ' + e.message);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Stream a payment-failure message, then re-show the tier-selection prompt so the
|
|
665
|
+
// user can retry the same or a different plan without sending another message.
|
|
666
|
+
// The pending payment is restored to the tier-select step so the next tier pick is
|
|
667
|
+
// handled by the proxy instead of being forwarded fresh to Django.
|
|
668
|
+
async function streamPaymentFailureAndRetry(res, walletHash, pendingPayload, message) {
|
|
669
|
+
try {
|
|
670
|
+
sseLine(res, {
|
|
671
|
+
id: 'pay-err',
|
|
672
|
+
object: 'chat.completion.chunk',
|
|
673
|
+
created: Math.floor(Date.now() / 1000),
|
|
674
|
+
model: 'deepseek/deepseek-v4-flash',
|
|
675
|
+
choices: [{ index: 0, delta: { content: message }, finish_reason: 'stop' }],
|
|
676
|
+
});
|
|
677
|
+
} catch (e) {
|
|
678
|
+
}
|
|
679
|
+
pendingPayload.step = 'tier_select';
|
|
680
|
+
pendingPayload.durationMinutes = null;
|
|
681
|
+
pendingPayments.set(walletHash, pendingPayload);
|
|
682
|
+
try {
|
|
683
|
+
const tiers = Array.isArray(pendingPayload.tiers) ? pendingPayload.tiers : [];
|
|
684
|
+
if (tiers.length > 0) {
|
|
685
|
+
await streamTierSelectionBody(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', tiers, false);
|
|
686
|
+
}
|
|
687
|
+
sseDone(res);
|
|
688
|
+
res.end();
|
|
689
|
+
} catch (e) {
|
|
690
|
+
try { res.end(); } catch (e2) {}
|
|
604
691
|
}
|
|
605
692
|
}
|
|
606
693
|
|
|
@@ -665,7 +752,17 @@ function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {
|
|
|
665
752
|
callback(new Error('Could not parse paytaca pay response: ' + err.message));
|
|
666
753
|
}
|
|
667
754
|
} else {
|
|
668
|
-
|
|
755
|
+
// Try to extract error from stdout (wrapper writes JSON errors to stdout, not stderr)
|
|
756
|
+
let wrapperErr = stderr.trim();
|
|
757
|
+
if (!wrapperErr) {
|
|
758
|
+
try {
|
|
759
|
+
const parsed = JSON.parse(stdout.trim());
|
|
760
|
+
wrapperErr = parsed.error || 'Unknown error';
|
|
761
|
+
} catch {
|
|
762
|
+
wrapperErr = stdout.trim() || 'paytaca pay wrapper exited with code ' + code;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
callback(new Error(wrapperErr));
|
|
669
766
|
}
|
|
670
767
|
});
|
|
671
768
|
|
|
@@ -1031,42 +1128,49 @@ const server = http.createServer(async (req, res) => {
|
|
|
1031
1128
|
res.write(': keepalive\\n\\n');
|
|
1032
1129
|
}, 2000);
|
|
1033
1130
|
|
|
1034
|
-
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
|
|
1131
|
+
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, async (err, responseJson) => {
|
|
1035
1132
|
pendingPayments.delete(walletHash);
|
|
1036
1133
|
clearInterval(keepalive);
|
|
1037
1134
|
|
|
1038
1135
|
if (err) {
|
|
1039
1136
|
log('paytaca pay failed: ' + err.message);
|
|
1040
|
-
if (res.headersSent) {
|
|
1137
|
+
if (res.headersSent && !res.destroyed && !res.writableEnded) {
|
|
1138
|
+
await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, '\\n\\n❌ Payment failed: ' + err.message + '\\n\\n');
|
|
1139
|
+
} else if (!res.headersSent) {
|
|
1041
1140
|
try {
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
res.end();
|
|
1046
|
-
} catch (e) {}
|
|
1141
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1142
|
+
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1143
|
+
} catch (e) { log('Failed to send payment error response: ' + e.message); }
|
|
1047
1144
|
} else {
|
|
1048
|
-
|
|
1049
|
-
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1145
|
+
log('Cannot send payment failure — response already ended or destroyed');
|
|
1050
1146
|
}
|
|
1051
1147
|
return;
|
|
1052
1148
|
}
|
|
1053
1149
|
|
|
1054
1150
|
if (!responseJson.success) {
|
|
1055
1151
|
const isTimeout = responseJson.timeout;
|
|
1056
|
-
const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error');
|
|
1152
|
+
const sseContent = isTimeout ? '\\n\\n⏱️ Response timed out. Your payment was processed \\u2014 check credits with \\'credits\\' and try again' : '\\n\\n❌ Payment failed: ' + (responseJson.error || 'Unknown error') + '\\n\\n';
|
|
1057
1153
|
const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
|
|
1058
1154
|
const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
|
|
1059
1155
|
const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';
|
|
1060
|
-
if (res.headersSent) {
|
|
1156
|
+
if (res.headersSent && !res.destroyed && !res.writableEnded) {
|
|
1157
|
+
if (isTimeout) {
|
|
1158
|
+
try {
|
|
1159
|
+
sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
|
|
1160
|
+
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1161
|
+
sseDone(res);
|
|
1162
|
+
res.end();
|
|
1163
|
+
} catch (e) { log('Failed to send timeout error via SSE: ' + e.message); }
|
|
1164
|
+
} else {
|
|
1165
|
+
await streamPaymentFailureAndRetry(res, walletHash, pendingPayload, sseContent);
|
|
1166
|
+
}
|
|
1167
|
+
} else if (!res.headersSent) {
|
|
1061
1168
|
try {
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
res.end();
|
|
1066
|
-
} catch (e) {}
|
|
1169
|
+
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
1170
|
+
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1171
|
+
} catch (e) { log('Failed to send payment error JSON: ' + e.message); }
|
|
1067
1172
|
} else {
|
|
1068
|
-
|
|
1069
|
-
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1173
|
+
log('Cannot send payment error — response already ended or destroyed');
|
|
1070
1174
|
}
|
|
1071
1175
|
return;
|
|
1072
1176
|
}
|
|
@@ -1080,15 +1184,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
1080
1184
|
|
|
1081
1185
|
log('paytaca pay succeeded. Returning chat response.');
|
|
1082
1186
|
|
|
1187
|
+
if (res.destroyed || res.writableEnded) {
|
|
1188
|
+
log('Payment succeeded but response connection is gone — cannot deliver chat response');
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1083
1192
|
if (wasStreaming) {
|
|
1084
1193
|
try {
|
|
1085
|
-
jsonToSse(res, chatCompletion);
|
|
1086
|
-
} catch (e) {}
|
|
1194
|
+
jsonToSse(res, chatCompletion, { prependContent: '\\n💳 Payment successful — generating your response...\\n\\n' });
|
|
1195
|
+
} catch (e) { log('jsonToSse threw: ' + e.message); }
|
|
1087
1196
|
} else {
|
|
1088
1197
|
try {
|
|
1089
|
-
res.
|
|
1198
|
+
if (!res.headersSent) {
|
|
1199
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1200
|
+
}
|
|
1090
1201
|
res.end(JSON.stringify(chatCompletion));
|
|
1091
|
-
} catch (e) {}
|
|
1202
|
+
} catch (e) { log('Failed to send non-streaming response: ' + e.message); }
|
|
1092
1203
|
}
|
|
1093
1204
|
});
|
|
1094
1205
|
return;
|
|
@@ -1130,16 +1241,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
1130
1241
|
clearInterval(keepalive);
|
|
1131
1242
|
if (err) {
|
|
1132
1243
|
log('paytaca pay failed: ' + err.message);
|
|
1133
|
-
if (res.headersSent) {
|
|
1244
|
+
if (res.headersSent && !res.destroyed && !res.writableEnded) {
|
|
1134
1245
|
try {
|
|
1135
1246
|
sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: '\\n\\n❌ Payment failed: ' + err.message }, finish_reason: 'stop' }] });
|
|
1136
1247
|
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1137
1248
|
sseDone(res);
|
|
1138
1249
|
res.end();
|
|
1139
|
-
} catch (e) {}
|
|
1250
|
+
} catch (e) { log('Failed to send payment error via SSE: ' + e.message); }
|
|
1251
|
+
} else if (!res.headersSent) {
|
|
1252
|
+
try {
|
|
1253
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1254
|
+
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1255
|
+
} catch (e) { log('Failed to send payment error JSON: ' + e.message); }
|
|
1140
1256
|
} else {
|
|
1141
|
-
|
|
1142
|
-
res.end(JSON.stringify({ error: 'Payment failed', message: err.message, details: 'Please check your wallet balance and try again.' }));
|
|
1257
|
+
log('Cannot send payment failure — response already ended or destroyed');
|
|
1143
1258
|
}
|
|
1144
1259
|
return;
|
|
1145
1260
|
}
|
|
@@ -1150,16 +1265,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
1150
1265
|
const errLabel = isTimeout ? 'Response timeout' : 'Payment failed';
|
|
1151
1266
|
const errMsg = isTimeout ? 'Response timed out. Payment was processed.' : responseJson.error;
|
|
1152
1267
|
const errDetails = isTimeout ? 'Try again or check credits with \\'credits\\'.' : 'Please check your balance and try again.';
|
|
1153
|
-
if (res.headersSent) {
|
|
1268
|
+
if (res.headersSent && !res.destroyed && !res.writableEnded) {
|
|
1154
1269
|
try {
|
|
1155
1270
|
sseLine(res, { id: 'pay-err', object: 'chat.completion.chunk', created: Math.floor(Date.now() / 1000), model: 'deepseek/deepseek-v4-flash', choices: [{ index: 0, delta: { content: sseContent }, finish_reason: 'stop' }] });
|
|
1156
1271
|
sseLine(res, { id: 'pay-err-done', object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }] });
|
|
1157
1272
|
sseDone(res);
|
|
1158
1273
|
res.end();
|
|
1159
|
-
} catch (e) {}
|
|
1274
|
+
} catch (e) { log('Failed to send error via SSE: ' + e.message); }
|
|
1275
|
+
} else if (!res.headersSent) {
|
|
1276
|
+
try {
|
|
1277
|
+
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
1278
|
+
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1279
|
+
} catch (e) { log('Failed to send error JSON: ' + e.message); }
|
|
1160
1280
|
} else {
|
|
1161
|
-
|
|
1162
|
-
res.end(JSON.stringify({ error: errLabel, message: errMsg, details: errDetails }));
|
|
1281
|
+
log('Cannot send payment error — response already ended or destroyed');
|
|
1163
1282
|
}
|
|
1164
1283
|
return;
|
|
1165
1284
|
}
|
|
@@ -1172,16 +1291,23 @@ const server = http.createServer(async (req, res) => {
|
|
|
1172
1291
|
} catch {}
|
|
1173
1292
|
|
|
1174
1293
|
log('paytaca pay succeeded. Returning chat response.');
|
|
1294
|
+
|
|
1295
|
+
if (res.destroyed || res.writableEnded) {
|
|
1296
|
+
log('Payment succeeded but response connection is gone — cannot deliver chat response');
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1175
1299
|
|
|
1176
1300
|
if (wasStreaming) {
|
|
1177
1301
|
try {
|
|
1178
|
-
jsonToSse(res, chatCompletion);
|
|
1179
|
-
} catch (e) {}
|
|
1302
|
+
jsonToSse(res, chatCompletion, { prependContent: '\\n💳 Payment successful — generating your response...\\n\\n' });
|
|
1303
|
+
} catch (e) { log('jsonToSse threw: ' + e.message); }
|
|
1180
1304
|
} else {
|
|
1181
1305
|
try {
|
|
1182
|
-
res.
|
|
1306
|
+
if (!res.headersSent) {
|
|
1307
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1308
|
+
}
|
|
1183
1309
|
res.end(JSON.stringify(chatCompletion));
|
|
1184
|
-
} catch (e) {}
|
|
1310
|
+
} catch (e) { log('Failed to send non-streaming response: ' + e.message); }
|
|
1185
1311
|
}
|
|
1186
1312
|
});
|
|
1187
1313
|
return;
|
|
@@ -1395,7 +1521,7 @@ server.on('error', (err) => {
|
|
|
1395
1521
|
|
|
1396
1522
|
server.listen(PROXY_PORT, () => {
|
|
1397
1523
|
log('Paytaca AI Proxy running on http://localhost:' + PROXY_PORT);
|
|
1398
|
-
log('Forwarding to
|
|
1524
|
+
log('Forwarding to ' + BACKEND_URL);
|
|
1399
1525
|
log('Discovery: http://localhost:' + PROXY_PORT + '/v1/config');
|
|
1400
1526
|
log('Managed by OpenCode plugin');
|
|
1401
1527
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../../src/bundled/proxy.ts"],"names":[],"mappings":";AAAA,0DAA0D;AAC1D,6DAA6D;;;AAEhD,QAAA,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6/CnC,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(
|
|
1
|
+
export declare const WRAPPER_SCRIPT_CONTENT = "#!/usr/bin/env node\n/**\n * Paytaca Pay Wrapper \u2014 handles large request bodies by reading from a file.\n * Imports paytaca-cli modules directly (avoids CLI argument size limits).\n */\n\nimport { readFileSync } from 'fs';\nimport { execSync } from 'child_process';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// How long to wait for the server to respond before treating the payment as timed out.\n// Default 240s so heavy non-streaming generations (large context / long output) can\n// complete; override with PAYTACA_PAY_TIMEOUT_MS.\nconst PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);\n\n// Find paytaca-cli installation\nfunction findPaytacaCliPath() {\n const possiblePaths = [];\n \n // Try to get global npm root\n try {\n const globalPath = execSync('npm root -g', { encoding: 'utf8' }).trim();\n possiblePaths.push(\n join(globalPath, 'paytaca-cli'),\n join(globalPath, 'opencode-plugin', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Common global locations\n possiblePaths.push(\n '/usr/lib/node_modules/paytaca-cli',\n '/usr/local/lib/node_modules/paytaca-cli',\n '/opt/homebrew/lib/node_modules/paytaca-cli',\n );\n \n // Try current file's node_modules (for bundled installs)\n try {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = dirname(currentFile);\n possiblePaths.push(\n join(currentDir, '..', 'node_modules', 'paytaca-cli'),\n join(currentDir, '..', '..', 'node_modules', 'paytaca-cli'),\n );\n } catch {}\n \n // Find first valid path\n for (const basePath of possiblePaths) {\n try {\n const walletPath = join(basePath, 'dist', 'wallet', 'index.js');\n readFileSync(walletPath);\n return basePath;\n } catch {}\n }\n \n throw new Error('paytaca-cli not found. Try reinstalling opencode-plugin: npm install @paytaca/opencode-plugin');\n}\n\n// Load paytaca-cli modules\nlet loadMnemonic, loadWallet, LibauthHDWallet, X402Payer, parsePaymentRequiredJson, selectBchPaymentRequirements, BCH_DERIVATION_PATH;\n\ntry {\n const basePath = findPaytacaCliPath();\n \n ({ loadMnemonic, loadWallet } = await import(join(basePath, 'dist', 'wallet', 'index.js')));\n ({ LibauthHDWallet } = await import(join(basePath, 'dist', 'wallet', 'keys.js')));\n ({ X402Payer } = await import(join(basePath, 'dist', 'wallet', 'x402.js')));\n ({ parsePaymentRequiredJson, selectBchPaymentRequirements } = await import(join(basePath, 'dist', 'utils', 'x402.js')));\n ({ BCH_DERIVATION_PATH } = await import(join(basePath, 'dist', 'utils', 'network.js')));\n} catch (err) {\n console.log(JSON.stringify({ success: false, error: 'Failed to load paytaca-cli: ' + err.message }));\n process.exit(1);\n}\n\nasync function main() {\n const configPath = process.argv[2];\n if (!configPath) {\n console.log(JSON.stringify({ success: false, error: 'Usage: node paytaca-pay-wrapper.mjs <config.json>' }));\n process.exit(1);\n }\n\n const config = JSON.parse(readFileSync(configPath, 'utf8'));\n const { url, method, headers, bodyFile, chipnet, confirmed } = config;\n\n const body = readFileSync(bodyFile, 'utf8');\n\n const data = loadMnemonic();\n if (!data) {\n console.log(JSON.stringify({ success: false, error: 'No wallet found. Run paytaca wallet create first.' }));\n process.exit(1);\n }\n\n const wallet = loadWallet();\n const isChipnet = Boolean(chipnet);\n const bchWallet = wallet.forNetwork(isChipnet);\n const hdWallet = new LibauthHDWallet(data.mnemonic, BCH_DERIVATION_PATH, isChipnet ? 'chipnet' : 'mainnet');\n const x402Payer = new X402Payer({ hdWallet, addressIndex: 0 });\n\n try {\n const result = await executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed);\n console.log(JSON.stringify(result, null, 2));\n } catch (err) {\n console.log(JSON.stringify({ success: false, error: err.message || String(err) }, null, 2));\n process.exit(1);\n }\n}\n\nasync function executePay(url, method, headers, body, bchWallet, x402Payer, isChipnet, confirmed) {\n const response = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n\n const responseHeaders = {};\n response.headers.forEach((value, key) => { responseHeaders[key] = value; });\n const responseText = await response.text();\n let responseData;\n try { responseData = JSON.parse(responseText); } catch { responseData = responseText; }\n\n if (response.status === 402) {\n const paymentRequired = parsePaymentRequiredJson(responseData);\n if (!paymentRequired) {\n return { success: false, status: 402, error: 'Could not parse PaymentRequired from 402 response body' };\n }\n const requirements = selectBchPaymentRequirements(paymentRequired, isChipnet ? 'chipnet' : 'mainnet');\n if (!requirements) {\n return {\n success: false, status: 402, error: 'Server does not accept BCH payment',\n data: { acceptedSchemes: paymentRequired.accepts.map(a => ({ scheme: a.scheme, network: a.network })) },\n };\n }\n\n const payerAddress = x402Payer.getPayerAddress();\n const address = requirements.payTo;\n const amountBch = Number(requirements.amount) / 1e8;\n const changeAddressSet = bchWallet.getAddressSetAt(0);\n const changeAddress = changeAddressSet.change;\n\n if (!confirmed) {\n return {\n success: false, status: 402, error: 'Payment not confirmed.',\n payment: { required: true, amount: requirements.amount, payTo: address },\n };\n }\n\n const sendResult = await bchWallet.sendBch(amountBch, address, changeAddress);\n if (!sendResult.success) {\n return { success: false, status: 402, payment: { required: true, error: sendResult.error }, error: sendResult.error };\n }\n\n const txid = sendResult.txid;\n const paymentPayload = await x402Payer.createPaymentPayload(requirements, paymentRequired.resource.url, txid, 0, requirements.amount);\n headers['PAYMENT-SIGNATURE'] = JSON.stringify(paymentPayload);\n\n let retryResponse;\n try {\n retryResponse = await fetch(url, {\n method,\n headers,\n body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,\n signal: AbortSignal.timeout(PAY_TIMEOUT_MS),\n });\n } catch (e) {\n if (e.name === 'AbortError') {\n return { success: false, timeout: true, error: 'Response timed out from server.' };\n }\n throw e;\n }\n const retryResponseHeaders = {};\n retryResponse.headers.forEach((value, key) => { retryResponseHeaders[key] = value; });\n const retryResponseText = await retryResponse.text();\n let retryResponseData;\n try { retryResponseData = JSON.parse(retryResponseText); } catch { retryResponseData = retryResponseText; }\n\n return {\n success: retryResponse.ok,\n status: retryResponse.status,\n statusText: retryResponse.statusText,\n headers: retryResponseHeaders,\n data: retryResponseData,\n payment: { required: true, txid, recipientAddress: address },\n };\n }\n\n return {\n success: response.ok,\n status: response.status,\n statusText: response.statusText,\n headers: responseHeaders,\n data: responseData,\n payment: { required: false },\n };\n}\n\nmain();\n";
|
|
2
2
|
//# sourceMappingURL=wrapper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,
|
|
1
|
+
{"version":3,"file":"wrapper.d.ts","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,sBAAsB,ozOAqMlC,CAAC"}
|
package/dist/bundled/wrapper.js
CHANGED
|
@@ -15,6 +15,11 @@ import { execSync } from 'child_process';
|
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { dirname, join } from 'path';
|
|
17
17
|
|
|
18
|
+
// How long to wait for the server to respond before treating the payment as timed out.
|
|
19
|
+
// Default 240s so heavy non-streaming generations (large context / long output) can
|
|
20
|
+
// complete; override with PAYTACA_PAY_TIMEOUT_MS.
|
|
21
|
+
const PAY_TIMEOUT_MS = Number(process.env.PAYTACA_PAY_TIMEOUT_MS || 240000);
|
|
22
|
+
|
|
18
23
|
// Find paytaca-cli installation
|
|
19
24
|
function findPaytacaCliPath() {
|
|
20
25
|
const possiblePaths = [];
|
|
@@ -111,7 +116,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
|
|
|
111
116
|
method,
|
|
112
117
|
headers,
|
|
113
118
|
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
114
|
-
signal: AbortSignal.timeout(
|
|
119
|
+
signal: AbortSignal.timeout(PAY_TIMEOUT_MS),
|
|
115
120
|
});
|
|
116
121
|
|
|
117
122
|
const responseHeaders = {};
|
|
@@ -161,7 +166,7 @@ async function executePay(url, method, headers, body, bchWallet, x402Payer, isCh
|
|
|
161
166
|
method,
|
|
162
167
|
headers,
|
|
163
168
|
body: ['POST', 'PUT', 'PATCH'].includes(method) ? body : undefined,
|
|
164
|
-
signal: AbortSignal.timeout(
|
|
169
|
+
signal: AbortSignal.timeout(PAY_TIMEOUT_MS),
|
|
165
170
|
});
|
|
166
171
|
} catch (e) {
|
|
167
172
|
if (e.name === 'AbortError') {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG
|
|
1
|
+
{"version":3,"file":"wrapper.js","sourceRoot":"","sources":["../../src/bundled/wrapper.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,4EAA4E;AAC5E,mFAAmF;;;AAEtE,QAAA,sBAAsB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqMrC,CAAC"}
|
package/dist/proxy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AA4D5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAYpE;AAuCD,wBAAsB,iBAAiB,CAAC,SAAS,GAAE,MAAa,EAAE,OAAO,GAAE,MAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAOzG;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOrD;AAED,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAiBlH;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC,CA+GtF;AAqBD,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAU5G"}
|
package/dist/proxy.js
CHANGED
|
@@ -46,20 +46,16 @@ const child_process_1 = require("child_process");
|
|
|
46
46
|
const config_1 = require("./config");
|
|
47
47
|
const proxy_1 = require("./bundled/proxy");
|
|
48
48
|
const wrapper_1 = require("./bundled/wrapper");
|
|
49
|
-
// Get path to paytaca binary (multi-strategy resolution)
|
|
50
49
|
function getPaytacaCommand() {
|
|
51
|
-
// Priority 1: Local node_modules (via require.resolve)
|
|
52
50
|
try {
|
|
53
51
|
const paytacaCliPkg = require.resolve('paytaca-cli/package.json');
|
|
54
52
|
return path.resolve(path.dirname(paytacaCliPkg), 'bin', 'paytaca.js');
|
|
55
53
|
}
|
|
56
54
|
catch { }
|
|
57
|
-
// Priority 2: Local .bin symlink
|
|
58
55
|
const localPaytaca = path.join(__dirname, '..', 'node_modules', '.bin', 'paytaca');
|
|
59
56
|
if (fs.existsSync(localPaytaca)) {
|
|
60
57
|
return localPaytaca;
|
|
61
58
|
}
|
|
62
|
-
// Priority 3: Global npm root
|
|
63
59
|
try {
|
|
64
60
|
const globalRoot = (0, child_process_1.execSync)('npm root -g', { encoding: 'utf8' }).trim();
|
|
65
61
|
const pathsToCheck = [
|
|
@@ -73,7 +69,6 @@ function getPaytacaCommand() {
|
|
|
73
69
|
}
|
|
74
70
|
}
|
|
75
71
|
catch { }
|
|
76
|
-
// Priority 4: Common global installation paths
|
|
77
72
|
const commonPaths = [
|
|
78
73
|
'/usr/lib/node_modules/paytaca-cli/bin/paytaca.js',
|
|
79
74
|
'/usr/local/lib/node_modules/paytaca-cli/bin/paytaca.js',
|
|
@@ -84,7 +79,6 @@ function getPaytacaCommand() {
|
|
|
84
79
|
return p;
|
|
85
80
|
}
|
|
86
81
|
}
|
|
87
|
-
// Priority 5: which/where on PATH
|
|
88
82
|
try {
|
|
89
83
|
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
90
84
|
const result = (0, child_process_1.execSync)(`${which} paytaca`, { encoding: 'utf8' }).trim().split('\n')[0];
|
|
@@ -93,15 +87,10 @@ function getPaytacaCommand() {
|
|
|
93
87
|
}
|
|
94
88
|
}
|
|
95
89
|
catch { }
|
|
96
|
-
// Priority 6: Bare command (rely on PATH at runtime)
|
|
97
90
|
return 'paytaca';
|
|
98
91
|
}
|
|
99
92
|
async function isPortAvailable(port) {
|
|
100
93
|
return new Promise((resolve) => {
|
|
101
|
-
// Bind EXACTLY like the proxy does (no host -> wildcard / dual-stack).
|
|
102
|
-
// Probing only 127.0.0.1 would miss an existing proxy listener on the
|
|
103
|
-
// IPv6 wildcard address (Node's default listen()), making findAvailablePort
|
|
104
|
-
// hand out a port that's actually in use (-> EADDRINUSE on spawn).
|
|
105
94
|
const server = require('net').createServer();
|
|
106
95
|
server.once('error', () => {
|
|
107
96
|
resolve(false);
|
|
@@ -113,8 +102,6 @@ async function isPortAvailable(port) {
|
|
|
113
102
|
server.listen(port);
|
|
114
103
|
});
|
|
115
104
|
}
|
|
116
|
-
// Wait until a port is free (or the timeout elapses). Used after killing an
|
|
117
|
-
// outdated proxy so the replacement can bind the same port.
|
|
118
105
|
async function waitForPortFree(port, timeout = 10000) {
|
|
119
106
|
const start = Date.now();
|
|
120
107
|
while (Date.now() - start < timeout) {
|
|
@@ -125,13 +112,9 @@ async function waitForPortFree(port, timeout = 10000) {
|
|
|
125
112
|
}
|
|
126
113
|
return false;
|
|
127
114
|
}
|
|
128
|
-
// Hash of the bundled proxy script so we can detect when the code changed and
|
|
129
|
-
// restart a running proxy instead of silently reusing stale code.
|
|
130
115
|
function scriptHash() {
|
|
131
116
|
return crypto.createHash('sha1').update(proxy_1.PROXY_SCRIPT_CONTENT).digest('hex');
|
|
132
117
|
}
|
|
133
|
-
// Persist the pid file + config so every future lookup points at the current
|
|
134
|
-
// (known-good) proxy. Centralizes the bookkeeping.
|
|
135
118
|
function persistProxyInfo(configDir, config, port, pid, currentHash) {
|
|
136
119
|
fs.writeFileSync((0, config_1.getPidFile)(configDir), pid.toString());
|
|
137
120
|
config.proxyPort = port;
|
|
@@ -139,20 +122,14 @@ function persistProxyInfo(configDir, config, port, pid, currentHash) {
|
|
|
139
122
|
config.proxyScriptHash = currentHash;
|
|
140
123
|
(0, config_1.saveConfig)(configDir, config);
|
|
141
124
|
}
|
|
142
|
-
// Reuse an existing (fresh) proxy: correct pid file + config and hand its
|
|
143
|
-
// port back to the caller.
|
|
144
125
|
async function reuseExistingProxy(configDir, config, status, currentHash) {
|
|
145
126
|
persistProxyInfo(configDir, config, status.port, status.pid, currentHash);
|
|
146
127
|
return { port: status.port, pid: status.pid };
|
|
147
128
|
}
|
|
148
|
-
// If a healthy proxy running the current code already exists (e.g. another
|
|
149
|
-
// opencode window won the spawn race), adopt it instead of spawning a second
|
|
150
|
-
// one. Returns null when no suitable proxy is found.
|
|
151
129
|
async function tryAdoptExistingFreshProxy(configDir, config, currentHash) {
|
|
152
130
|
const status = await getProxyStatus(configDir);
|
|
153
131
|
if (status.running && status.pid && status.port && status.pid > 0 &&
|
|
154
132
|
(0, config_1.loadConfig)(configDir).proxyScriptHash === currentHash) {
|
|
155
|
-
console.log(`[paytaca] Using existing proxy (pid ${status.pid}, port ${status.port}) that started during this session.`);
|
|
156
133
|
return await reuseExistingProxy(configDir, config, { port: status.port, pid: status.pid }, currentHash);
|
|
157
134
|
}
|
|
158
135
|
return null;
|
|
@@ -187,7 +164,6 @@ async function getProxyStatus(configDir) {
|
|
|
187
164
|
}
|
|
188
165
|
}
|
|
189
166
|
catch {
|
|
190
|
-
// Invalid PID file
|
|
191
167
|
}
|
|
192
168
|
return { running: false };
|
|
193
169
|
}
|
|
@@ -195,51 +171,33 @@ async function startProxy(configDir, config) {
|
|
|
195
171
|
const proxyScript = (0, config_1.getProxyScript)(configDir);
|
|
196
172
|
const logFile = (0, config_1.getLogFile)(configDir);
|
|
197
173
|
const wrapperScript = (0, config_1.getWrapperScript)(configDir);
|
|
198
|
-
// Ensure config directory exists
|
|
199
174
|
(0, config_1.ensureConfigDir)(configDir);
|
|
200
|
-
// Detect if the running proxy was started from different code than this build
|
|
201
175
|
const currentHash = scriptHash();
|
|
202
176
|
const persistedConfig = (0, config_1.loadConfig)(configDir);
|
|
203
|
-
// Check if proxy already running
|
|
204
177
|
let existingStatus = await getProxyStatus(configDir);
|
|
205
|
-
// A running proxy is stale if it was spawned before the current code hash
|
|
206
|
-
// was persisted (i.e. the bundled script has changed since it was started).
|
|
207
178
|
const needsRestart = existingStatus.running &&
|
|
208
179
|
(existingStatus.pid || 0) > 0 &&
|
|
209
180
|
persistedConfig.proxyScriptHash !== currentHash;
|
|
210
|
-
// Always rewrite proxy script so code changes take effect on next restart
|
|
211
181
|
fs.writeFileSync(proxyScript, proxy_1.PROXY_SCRIPT_CONTENT, 'utf8');
|
|
212
182
|
fs.chmodSync(proxyScript, '755');
|
|
213
183
|
if (existingStatus.running && existingStatus.pid && existingStatus.port && !needsRestart) {
|
|
214
184
|
return await reuseExistingProxy(configDir, config, { port: existingStatus.port, pid: existingStatus.pid }, currentHash);
|
|
215
185
|
}
|
|
216
|
-
// A proxy is running but with outdated code — shut it down so the
|
|
217
|
-
// replacement can take over (fixes stale code persisting across restarts).
|
|
218
186
|
if (existingStatus.running && existingStatus.pid) {
|
|
219
|
-
console.log(`[paytaca] Proxy (pid ${existingStatus.pid}) runs outdated code; restarting with updated script...`);
|
|
220
187
|
try {
|
|
221
188
|
process.kill(existingStatus.pid);
|
|
222
189
|
}
|
|
223
|
-
catch
|
|
224
|
-
console.log(`[paytaca] Could not signal proxy ${existingStatus.pid}: ${e.message}`);
|
|
225
|
-
}
|
|
190
|
+
catch { }
|
|
226
191
|
if (existingStatus.port) {
|
|
227
|
-
|
|
228
|
-
if (!freed) {
|
|
229
|
-
console.log(`[paytaca] Port ${existingStatus.port} still busy after kill; will use next available port.`);
|
|
230
|
-
}
|
|
192
|
+
await waitForPortFree(existingStatus.port);
|
|
231
193
|
}
|
|
232
194
|
}
|
|
233
|
-
// Another window may have started a fresh proxy while we were deciding
|
|
234
|
-
// (e.g. it won the same-port spawn race) — adopt it rather than double-spawn.
|
|
235
195
|
const adopted = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
|
|
236
196
|
if (adopted) {
|
|
237
197
|
return adopted;
|
|
238
198
|
}
|
|
239
|
-
// Write bundled wrapper script to config directory (shared across attempts)
|
|
240
199
|
fs.writeFileSync(wrapperScript, wrapper_1.WRAPPER_SCRIPT_CONTENT, 'utf8');
|
|
241
200
|
fs.chmodSync(wrapperScript, '755');
|
|
242
|
-
// Start proxy (detached to avoid receiving SIGINT from terminal)
|
|
243
201
|
const paytacaCmd = getPaytacaCommand();
|
|
244
202
|
const MAX_ATTEMPTS = 3;
|
|
245
203
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
@@ -258,22 +216,15 @@ async function startProxy(configDir, config) {
|
|
|
258
216
|
});
|
|
259
217
|
proxy.unref();
|
|
260
218
|
if (!proxy.pid) {
|
|
261
|
-
console.log(`[paytaca] Spawn attempt ${attempt} failed (no PID); retrying...`);
|
|
262
219
|
continue;
|
|
263
220
|
}
|
|
264
|
-
// Track whether OUR child exited — if so the port is served by another
|
|
265
|
-
// proxy that won the bind race, and we should adopt it instead.
|
|
266
221
|
let childExited = false;
|
|
267
222
|
proxy.once('exit', () => { childExited = true; });
|
|
268
|
-
// Handle proxy output
|
|
269
223
|
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
|
270
224
|
proxy.stdout?.pipe(logStream);
|
|
271
225
|
proxy.stderr?.pipe(logStream);
|
|
272
226
|
const ready = await waitForProxy(port, 15000);
|
|
273
227
|
if (childExited) {
|
|
274
|
-
// Our child lost the bind race (EADDRINUSE) or crashed immediately.
|
|
275
|
-
// Adopt an existing fresh proxy if there is one; otherwise try again.
|
|
276
|
-
console.log(`[paytaca] Spawned proxy (pid ${proxy.pid}) exited early on port ${port}; checking for an existing proxy...`);
|
|
277
228
|
const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
|
|
278
229
|
if (a) {
|
|
279
230
|
return a;
|
|
@@ -281,8 +232,6 @@ async function startProxy(configDir, config) {
|
|
|
281
232
|
continue;
|
|
282
233
|
}
|
|
283
234
|
if (ready) {
|
|
284
|
-
// Confirm our child is genuinely the one serving (it could have exited
|
|
285
|
-
// between the readiness poll and here). Persist only when healthy.
|
|
286
235
|
if (childExited) {
|
|
287
236
|
const a = await tryAdoptExistingFreshProxy(configDir, config, currentHash);
|
|
288
237
|
if (a) {
|
|
@@ -291,14 +240,11 @@ async function startProxy(configDir, config) {
|
|
|
291
240
|
continue;
|
|
292
241
|
}
|
|
293
242
|
persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
|
|
294
|
-
console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
|
|
295
243
|
return { port, pid: proxy.pid };
|
|
296
244
|
}
|
|
297
|
-
// Child alive but never became ready — give it one more window, then retry.
|
|
298
245
|
const ready2 = await waitForProxy(port, 15000);
|
|
299
246
|
if (ready2 && !childExited) {
|
|
300
247
|
persistProxyInfo(configDir, config, port, proxy.pid, currentHash);
|
|
301
|
-
console.log(`[paytaca] Proxy started on port ${port} (pid ${proxy.pid}).`);
|
|
302
248
|
return { port, pid: proxy.pid };
|
|
303
249
|
}
|
|
304
250
|
if (childExited) {
|
|
@@ -307,12 +253,12 @@ async function startProxy(configDir, config) {
|
|
|
307
253
|
return a;
|
|
308
254
|
}
|
|
309
255
|
}
|
|
310
|
-
console.log(`[paytaca] Spawn attempt ${attempt} not ready on port ${port}; retrying...`);
|
|
311
256
|
try {
|
|
312
257
|
process.kill(proxy.pid);
|
|
313
258
|
}
|
|
314
259
|
catch { }
|
|
315
260
|
}
|
|
261
|
+
console.error(`[paytaca] Proxy failed to start within ${MAX_ATTEMPTS} attempts`);
|
|
316
262
|
throw new Error(`Proxy failed to start within ${MAX_ATTEMPTS} attempts`);
|
|
317
263
|
}
|
|
318
264
|
async function waitForProxy(port, timeout = 30000) {
|
|
@@ -327,11 +273,9 @@ async function waitForProxy(port, timeout = 30000) {
|
|
|
327
273
|
}
|
|
328
274
|
}
|
|
329
275
|
catch {
|
|
330
|
-
// Not ready yet
|
|
331
276
|
}
|
|
332
277
|
await new Promise(resolve => setTimeout(resolve, 500));
|
|
333
278
|
}
|
|
334
|
-
console.log(`[paytaca] Proxy did not become ready on port ${port} within ${timeout}ms`);
|
|
335
279
|
return false;
|
|
336
280
|
}
|
|
337
281
|
async function getProxyConfig(configDir) {
|
package/dist/proxy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"proxy.js","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@paytaca/opencode-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "OpenCode plugin for Paytaca AI - AI inference provider powered by Bitcoin Cash micropayments",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"license": "MIT",
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@opencode-ai/plugin": "^1.17.8",
|
|
36
|
-
"paytaca-cli": "^0.4.
|
|
36
|
+
"paytaca-cli": "^0.4.1"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/node": "^20.0.0",
|