@paytaca/opencode-plugin 0.1.6 → 0.1.7
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 +363 -47
- package/dist/bundled/proxy.js.map +1 -1
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +102 -16
- package/dist/index.js.map +1 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js.map +1 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js.map +1 -0
- package/dist/wallet.d.ts.map +1 -0
- package/dist/wallet.js.map +1 -0
- package/package.json +6 -1
- package/src/bundled/proxy.ts +0 -937
- package/src/bundled/wrapper.ts +0 -187
- package/src/config.ts +0 -105
- package/src/index.ts +0 -70
- package/src/proxy.ts +0 -283
- package/src/types.ts +0 -26
- package/src/wallet.ts +0 -296
- package/tsconfig.json +0 -19
package/dist/bundled/proxy.js
CHANGED
|
@@ -77,6 +77,8 @@ function checkHeartbeat() {
|
|
|
77
77
|
let heartbeatChecker = null;
|
|
78
78
|
|
|
79
79
|
// Store pending payment requests per wallet hash
|
|
80
|
+
// Each entry: { body, modelId, displayName, durationMinutes, tiers[], step }
|
|
81
|
+
// step: 'tier_select' (user must pick a tier) or 'approval' (yes/no)
|
|
80
82
|
const pendingPayments = new Map();
|
|
81
83
|
|
|
82
84
|
// Utility: run shell command and return output
|
|
@@ -149,6 +151,17 @@ async function checkWallet() {
|
|
|
149
151
|
}
|
|
150
152
|
}
|
|
151
153
|
|
|
154
|
+
// Format seconds as MM:SS or HH:MM:SS
|
|
155
|
+
function formatDuration(totalSeconds) {
|
|
156
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
157
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
158
|
+
const secs = totalSeconds % 60;
|
|
159
|
+
if (hours > 0) {
|
|
160
|
+
return hours + ':' + String(minutes).padStart(2, '0') + ':' + String(secs).padStart(2, '0');
|
|
161
|
+
}
|
|
162
|
+
return minutes + ':' + String(secs).padStart(2, '0');
|
|
163
|
+
}
|
|
164
|
+
|
|
152
165
|
// SSE helper: write a data line
|
|
153
166
|
function sseLine(res, data) {
|
|
154
167
|
res.write('data: ' + JSON.stringify(data) + '\\n\\n');
|
|
@@ -159,8 +172,116 @@ function sseDone(res) {
|
|
|
159
172
|
res.write('data: [DONE]\\n\\n');
|
|
160
173
|
}
|
|
161
174
|
|
|
175
|
+
// Build and stream SSE tier selection prompt
|
|
176
|
+
async function streamTierSelectionPrompt(res, walletHash, modelName, tiers) {
|
|
177
|
+
res.writeHead(200, {
|
|
178
|
+
'Content-Type': 'text/event-stream',
|
|
179
|
+
'Cache-Control': 'no-cache',
|
|
180
|
+
'X-Payment-Required': 'true',
|
|
181
|
+
'Connection': 'keep-alive',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
sseLine(res, {
|
|
185
|
+
id: 'tier-1',
|
|
186
|
+
object: 'chat.completion.chunk',
|
|
187
|
+
created: Math.floor(Date.now() / 1000),
|
|
188
|
+
model: modelName,
|
|
189
|
+
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Loading sequence
|
|
193
|
+
sseLine(res, {
|
|
194
|
+
id: 'tier-2',
|
|
195
|
+
object: 'chat.completion.chunk',
|
|
196
|
+
choices: [{ index: 0, delta: { content: '⏳ Initializing Paytaca AI provider...\\n' }, finish_reason: null }],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const hasCli = await checkPaytacaCli();
|
|
200
|
+
sseLine(res, {
|
|
201
|
+
id: 'tier-3',
|
|
202
|
+
object: 'chat.completion.chunk',
|
|
203
|
+
choices: [{ index: 0, delta: { content: 'Checking Paytaca CLI... ' }, finish_reason: null }],
|
|
204
|
+
});
|
|
205
|
+
sseLine(res, {
|
|
206
|
+
id: 'tier-4',
|
|
207
|
+
object: 'chat.completion.chunk',
|
|
208
|
+
choices: [{ index: 0, delta: { content: hasCli ? '✅\\n' : '❌ Not found\\n' }, finish_reason: null }],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const hasWallet = hasCli ? await checkWallet() : false;
|
|
212
|
+
sseLine(res, {
|
|
213
|
+
id: 'tier-5',
|
|
214
|
+
object: 'chat.completion.chunk',
|
|
215
|
+
choices: [{ index: 0, delta: { content: 'Checking wallet... ' }, finish_reason: null }],
|
|
216
|
+
});
|
|
217
|
+
sseLine(res, {
|
|
218
|
+
id: 'tier-6',
|
|
219
|
+
object: 'chat.completion.chunk',
|
|
220
|
+
choices: [{ index: 0, delta: { content: hasWallet ? '✅\\n' : '❌ Not found\\n' }, finish_reason: null }],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const balanceSats = hasWallet ? await getWalletBalance() : null;
|
|
224
|
+
sseLine(res, {
|
|
225
|
+
id: 'tier-7',
|
|
226
|
+
object: 'chat.completion.chunk',
|
|
227
|
+
choices: [{ index: 0, delta: { content: 'Fetching balance... ' }, finish_reason: null }],
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
let balanceStr;
|
|
231
|
+
if (balanceSats !== null) {
|
|
232
|
+
const bch = (balanceSats / 100000000).toFixed(8);
|
|
233
|
+
balanceStr = bch + ' BCH';
|
|
234
|
+
sseLine(res, {
|
|
235
|
+
id: 'tier-8',
|
|
236
|
+
object: 'chat.completion.chunk',
|
|
237
|
+
choices: [{ index: 0, delta: { content: '✅ — ' + balanceStr + '\\n\\n' }, finish_reason: null }],
|
|
238
|
+
});
|
|
239
|
+
} else {
|
|
240
|
+
balanceStr = 'Unable to check';
|
|
241
|
+
sseLine(res, {
|
|
242
|
+
id: 'tier-8',
|
|
243
|
+
object: 'chat.completion.chunk',
|
|
244
|
+
choices: [{ index: 0, delta: { content: '\\n' + balanceStr + '\\n\\n' }, finish_reason: null }],
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Tier selection
|
|
249
|
+
sseLine(res, {
|
|
250
|
+
id: 'tier-9',
|
|
251
|
+
object: 'chat.completion.chunk',
|
|
252
|
+
choices: [{ index: 0, delta: { content: '💳 Select a plan for ' + (modelName || 'AI Model') + '\\n\\n' }, finish_reason: null }],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
256
|
+
const tier = tiers[i];
|
|
257
|
+
const bchAmount = (tier.price_sats / 100000000).toFixed(8);
|
|
258
|
+
const label = String(i + 1) + '️⃣ ';
|
|
259
|
+
sseLine(res, {
|
|
260
|
+
id: 'tier-10-' + i,
|
|
261
|
+
object: 'chat.completion.chunk',
|
|
262
|
+
choices: [{ index: 0, delta: { content: label + tier.minutes + ' minutes — ₱' + tier.price_php.toFixed(2) + ' (' + bchAmount + ' BCH)\\n' }, finish_reason: null }],
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
sseLine(res, {
|
|
267
|
+
id: 'tier-11',
|
|
268
|
+
object: 'chat.completion.chunk',
|
|
269
|
+
choices: [{ index: 0, delta: { content: '\\nEnter a number (1-' + tiers.length + '), e.g. type ' + tiers[0].minutes + ':' }, finish_reason: 'stop' }],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
sseLine(res, {
|
|
273
|
+
id: 'tier-12',
|
|
274
|
+
object: 'chat.completion.chunk',
|
|
275
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
276
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
sseDone(res);
|
|
280
|
+
res.end();
|
|
281
|
+
}
|
|
282
|
+
|
|
162
283
|
// Build and stream SSE loading sequence + payment prompt
|
|
163
|
-
async function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUsed = 0, tokenLimit = 50000,
|
|
284
|
+
async function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUsed = 0, tokenLimit = 50000, timeRemainingSeconds = 0) {
|
|
164
285
|
res.writeHead(200, {
|
|
165
286
|
'Content-Type': 'text/event-stream',
|
|
166
287
|
'Cache-Control': 'no-cache',
|
|
@@ -291,7 +412,8 @@ async function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUse
|
|
|
291
412
|
}
|
|
292
413
|
|
|
293
414
|
if (isRenewal) {
|
|
294
|
-
const
|
|
415
|
+
const usedMinutes = Math.round(timeRemainingSeconds / 60);
|
|
416
|
+
const remainingAttr = usedMinutes > 0 ? usedMinutes + ' min remaining' : 'depleted';
|
|
295
417
|
sseLine(res, {
|
|
296
418
|
id: baseId + '-11',
|
|
297
419
|
object: 'chat.completion.chunk',
|
|
@@ -300,19 +422,8 @@ async function streamPaymentPrompt(res, walletHash, isRenewal = false, tokensUse
|
|
|
300
422
|
sseLine(res, {
|
|
301
423
|
id: baseId + '-12',
|
|
302
424
|
object: 'chat.completion.chunk',
|
|
303
|
-
choices: [{ index: 0, delta: { content: '
|
|
425
|
+
choices: [{ index: 0, delta: { content: '⏱ Time Credits: ' + remainingAttr + '\\n' }, finish_reason: null }],
|
|
304
426
|
});
|
|
305
|
-
if (carryoverDeadline) {
|
|
306
|
-
const minutesLeft = Math.max(0, Math.floor((new Date(carryoverDeadline) - Date.now()) / 60000));
|
|
307
|
-
const timeStr = minutesLeft > 0
|
|
308
|
-
? minutesLeft + ' min' + (minutesLeft !== 1 ? 's' : '') + ' remaining'
|
|
309
|
-
: 'expired — renew now to keep them';
|
|
310
|
-
sseLine(res, {
|
|
311
|
-
id: baseId + '-12b',
|
|
312
|
-
object: 'chat.completion.chunk',
|
|
313
|
-
choices: [{ index: 0, delta: { content: '⏰ Carryover expires in ' + timeStr + '\\n' }, finish_reason: null }],
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
427
|
}
|
|
317
428
|
|
|
318
429
|
sseLine(res, {
|
|
@@ -476,7 +587,7 @@ function forceNonStreaming(body) {
|
|
|
476
587
|
// Convert a chat.completion JSON object to SSE format
|
|
477
588
|
function jsonToSse(res, chatCompletion) {
|
|
478
589
|
const content = chatCompletion.choices?.[0]?.message?.content || '';
|
|
479
|
-
const model = chatCompletion.model || 'deepseek-ai/DeepSeek-V4-Flash';
|
|
590
|
+
const model = chatCompletion.model || chatCompletion.model_id || 'deepseek-ai/DeepSeek-V4-Flash';
|
|
480
591
|
const created = chatCompletion.created || Math.floor(Date.now() / 1000);
|
|
481
592
|
|
|
482
593
|
|
|
@@ -542,7 +653,7 @@ function jsonToSse(res, chatCompletion) {
|
|
|
542
653
|
}
|
|
543
654
|
|
|
544
655
|
// Run paytaca pay internally and return the response
|
|
545
|
-
function runPaytacaPay(djangoUrl, body, walletHash, callback) {
|
|
656
|
+
function runPaytacaPay(djangoUrl, body, walletHash, extraHeaders, callback) {
|
|
546
657
|
const url = djangoUrl + '/chat/completions?wallet_hash=' + encodeURIComponent(walletHash || '');
|
|
547
658
|
const payBody = forceNonStreaming(body);
|
|
548
659
|
|
|
@@ -560,7 +671,7 @@ function runPaytacaPay(djangoUrl, body, walletHash, callback) {
|
|
|
560
671
|
const config = {
|
|
561
672
|
url,
|
|
562
673
|
method: 'POST',
|
|
563
|
-
headers: { 'Content-Type': 'application/json' },
|
|
674
|
+
headers: Object.assign({ 'Content-Type': 'application/json' }, extraHeaders || {}),
|
|
564
675
|
bodyFile,
|
|
565
676
|
confirmed: true,
|
|
566
677
|
};
|
|
@@ -652,7 +763,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
652
763
|
// Enable CORS
|
|
653
764
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
654
765
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
655
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, Authorization');
|
|
766
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Wallet-Hash, X-Model-Id, X-Duration-Minutes, Payment-Signature, Authorization');
|
|
656
767
|
|
|
657
768
|
if (req.method === 'OPTIONS') {
|
|
658
769
|
res.writeHead(200);
|
|
@@ -681,11 +792,22 @@ const server = http.createServer(async (req, res) => {
|
|
|
681
792
|
res.end(JSON.stringify({
|
|
682
793
|
proxy_url: 'http://localhost:' + PROXY_PORT + '/v1',
|
|
683
794
|
django_url: BACKEND_URL + '/v1',
|
|
684
|
-
cost_sats: 6000,
|
|
685
|
-
cost_bch: '0.00006',
|
|
686
795
|
payment_address: '',
|
|
687
|
-
|
|
688
|
-
|
|
796
|
+
default_model: 'deepseek-ai/DeepSeek-V4-Flash',
|
|
797
|
+
default_duration_minutes: 30,
|
|
798
|
+
models: [
|
|
799
|
+
{
|
|
800
|
+
id: 'deepseek-ai/DeepSeek-V4-Flash',
|
|
801
|
+
object: 'model',
|
|
802
|
+
display_name: 'DeepSeek V4 Flash',
|
|
803
|
+
provider: 'gmi',
|
|
804
|
+
price_tiers: [
|
|
805
|
+
{ minutes: 10, price_php: 5.0, price_sats: 45000 },
|
|
806
|
+
{ minutes: 30, price_php: 12.0, price_sats: 108000 },
|
|
807
|
+
{ minutes: 60, price_php: 20.0, price_sats: 180000 },
|
|
808
|
+
],
|
|
809
|
+
},
|
|
810
|
+
],
|
|
689
811
|
context_retention_hours: 2,
|
|
690
812
|
}));
|
|
691
813
|
return;
|
|
@@ -715,13 +837,131 @@ const server = http.createServer(async (req, res) => {
|
|
|
715
837
|
|
|
716
838
|
|
|
717
839
|
if (pendingPayload) {
|
|
718
|
-
//
|
|
840
|
+
// Check for tier selection first
|
|
841
|
+
if (pendingPayload.step === 'tier_select' && pendingPayload.tiers && pendingPayload.tiers.length > 0) {
|
|
842
|
+
const userInput = lastContent.trim();
|
|
843
|
+
let selectedIndex = -1;
|
|
844
|
+
|
|
845
|
+
// Try to parse user input as a number (1-based)
|
|
846
|
+
const num = parseInt(userInput, 10);
|
|
847
|
+
if (!isNaN(num) && num >= 1 && num <= pendingPayload.tiers.length) {
|
|
848
|
+
selectedIndex = num - 1;
|
|
849
|
+
} else {
|
|
850
|
+
// Try to match by duration minutes
|
|
851
|
+
for (let i = 0; i < pendingPayload.tiers.length; i++) {
|
|
852
|
+
if (userInput === String(pendingPayload.tiers[i].minutes) ||
|
|
853
|
+
userInput === pendingPayload.tiers[i].minutes + ' minutes' ||
|
|
854
|
+
userInput === pendingPayload.tiers[i].minutes + ' min') {
|
|
855
|
+
selectedIndex = i;
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (selectedIndex >= 0) {
|
|
862
|
+
const selectedTier = pendingPayload.tiers[selectedIndex];
|
|
863
|
+
pendingPayload.durationMinutes = selectedTier.minutes;
|
|
864
|
+
pendingPayload.step = 'processing';
|
|
865
|
+
|
|
866
|
+
log('Tier selected: ' + selectedTier.minutes + ' min for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
867
|
+
|
|
868
|
+
// Build extra headers for payment wrapper
|
|
869
|
+
const extraHeaders = {};
|
|
870
|
+
if (pendingPayload.modelId) {
|
|
871
|
+
extraHeaders['X-Model-Id'] = pendingPayload.modelId;
|
|
872
|
+
}
|
|
873
|
+
extraHeaders['X-Duration-Minutes'] = String(selectedTier.minutes);
|
|
874
|
+
|
|
875
|
+
// Check wallet balance before attempting payment
|
|
876
|
+
const currentBalanceSats = await getWalletBalance();
|
|
877
|
+
if (currentBalanceSats !== null && selectedTier.price_sats && currentBalanceSats < selectedTier.price_sats) {
|
|
878
|
+
log('Insufficient balance for wallet ' + walletHash?.substring(0, 16) + '...: ' + currentBalanceSats + ' sats < ' + selectedTier.price_sats + ' sats needed');
|
|
879
|
+
pendingPayments.delete(walletHash);
|
|
880
|
+
const addr = await getReceivingAddress();
|
|
881
|
+
const neededBch = (selectedTier.price_sats - currentBalanceSats) / 100000000;
|
|
882
|
+
const neededLine = addr ? '\\n\\n📥 **Fund your wallet:** \\\`' + addr + '\\\`\\nOr run: paytaca receive (in another terminal) for QR code' : '';
|
|
883
|
+
sseLine(res, {
|
|
884
|
+
id: 'balance-err',
|
|
885
|
+
object: 'chat.completion.chunk',
|
|
886
|
+
choices: [{ index: 0, delta: { content: '\\n\\n❌ **Insufficient balance** — 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' }],
|
|
887
|
+
});
|
|
888
|
+
sseLine(res, {
|
|
889
|
+
id: 'balance-err-done',
|
|
890
|
+
object: 'chat.completion.chunk',
|
|
891
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
892
|
+
});
|
|
893
|
+
sseDone(res);
|
|
894
|
+
res.end();
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
|
|
899
|
+
pendingPayments.delete(walletHash);
|
|
900
|
+
|
|
901
|
+
if (err) {
|
|
902
|
+
log('paytaca pay failed: ' + err.message);
|
|
903
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
904
|
+
res.end(JSON.stringify({
|
|
905
|
+
error: 'Payment failed',
|
|
906
|
+
message: err.message,
|
|
907
|
+
details: 'Please check your wallet balance and try again.'
|
|
908
|
+
}));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
if (!responseJson.success) {
|
|
913
|
+
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
914
|
+
res.end(JSON.stringify({
|
|
915
|
+
error: 'Payment failed',
|
|
916
|
+
message: responseJson.error,
|
|
917
|
+
details: 'Payment was not successful. Please check your balance and try again.'
|
|
918
|
+
}));
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
const chatCompletion = responseJson?.data || responseJson;
|
|
923
|
+
|
|
924
|
+
let wasStreaming = false;
|
|
925
|
+
try {
|
|
926
|
+
wasStreaming = JSON.parse(pendingPayload.body).stream === true;
|
|
927
|
+
} catch {}
|
|
928
|
+
|
|
929
|
+
log('paytaca pay succeeded. Returning chat response.');
|
|
930
|
+
|
|
931
|
+
if (wasStreaming) {
|
|
932
|
+
try {
|
|
933
|
+
jsonToSse(res, chatCompletion);
|
|
934
|
+
} catch (e) {}
|
|
935
|
+
} else {
|
|
936
|
+
try {
|
|
937
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
938
|
+
res.end(JSON.stringify(chatCompletion));
|
|
939
|
+
} catch (e) {}
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
return;
|
|
943
|
+
} else {
|
|
944
|
+
// Invalid selection — reshow the prompt
|
|
945
|
+
log('Invalid tier selection for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
946
|
+
await streamTierSelectionPrompt(res, walletHash, pendingPayload.displayName || pendingPayload.modelId || 'AI Model', pendingPayload.tiers);
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Old flow: user responded to a yes/no payment prompt
|
|
719
952
|
if (lastContent === 'yes') {
|
|
720
|
-
// User approved — run paytaca pay with the stored original payload
|
|
721
953
|
log('Payment approved by wallet ' + walletHash?.substring(0, 16) + '...');
|
|
722
954
|
pendingPayments.delete(walletHash);
|
|
723
955
|
|
|
724
|
-
|
|
956
|
+
const extraHeaders = {};
|
|
957
|
+
if (pendingPayload.modelId) {
|
|
958
|
+
extraHeaders['X-Model-Id'] = pendingPayload.modelId;
|
|
959
|
+
}
|
|
960
|
+
if (pendingPayload.durationMinutes) {
|
|
961
|
+
extraHeaders['X-Duration-Minutes'] = String(pendingPayload.durationMinutes);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
runPaytacaPay(BACKEND_URL + '/v1', pendingPayload.body, walletHash, extraHeaders, (err, responseJson) => {
|
|
725
965
|
if (err) {
|
|
726
966
|
log('paytaca pay failed: ' + err.message);
|
|
727
967
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
@@ -733,8 +973,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
733
973
|
return;
|
|
734
974
|
}
|
|
735
975
|
|
|
736
|
-
|
|
737
|
-
// Check if the response indicates success
|
|
738
976
|
if (!responseJson.success) {
|
|
739
977
|
res.writeHead(responseJson.status || 500, { 'Content-Type': 'application/json' });
|
|
740
978
|
res.end(JSON.stringify({
|
|
@@ -746,12 +984,10 @@ const server = http.createServer(async (req, res) => {
|
|
|
746
984
|
}
|
|
747
985
|
|
|
748
986
|
const chatCompletion = responseJson?.data || responseJson;
|
|
749
|
-
if (chatCompletion.choices) {
|
|
750
|
-
}
|
|
751
987
|
|
|
752
988
|
let wasStreaming = false;
|
|
753
989
|
try {
|
|
754
|
-
wasStreaming = JSON.parse(pendingPayload).stream === true;
|
|
990
|
+
wasStreaming = JSON.parse(pendingPayload.body).stream === true;
|
|
755
991
|
} catch {}
|
|
756
992
|
|
|
757
993
|
log('paytaca pay succeeded. Returning chat response.');
|
|
@@ -759,20 +995,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
759
995
|
if (wasStreaming) {
|
|
760
996
|
try {
|
|
761
997
|
jsonToSse(res, chatCompletion);
|
|
762
|
-
} catch (e) {
|
|
763
|
-
}
|
|
998
|
+
} catch (e) {}
|
|
764
999
|
} else {
|
|
765
1000
|
try {
|
|
766
1001
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
767
1002
|
res.end(JSON.stringify(chatCompletion));
|
|
768
|
-
} catch (e) {
|
|
769
|
-
}
|
|
1003
|
+
} catch (e) {}
|
|
770
1004
|
}
|
|
771
1005
|
});
|
|
772
1006
|
return;
|
|
773
1007
|
|
|
774
1008
|
} else if (lastContent === 'no') {
|
|
775
|
-
// User declined
|
|
776
1009
|
log('Payment declined by wallet ' + walletHash?.substring(0, 16) + '...');
|
|
777
1010
|
pendingPayments.delete(walletHash);
|
|
778
1011
|
|
|
@@ -785,7 +1018,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
785
1018
|
id: 'payment-declined',
|
|
786
1019
|
object: 'chat.completion',
|
|
787
1020
|
created: Math.floor(Date.now() / 1000),
|
|
788
|
-
model: 'deepseek-ai/DeepSeek-V4-Flash',
|
|
1021
|
+
model: pendingPayload.modelId || 'deepseek-ai/DeepSeek-V4-Flash',
|
|
789
1022
|
choices: [{
|
|
790
1023
|
index: 0,
|
|
791
1024
|
message: {
|
|
@@ -804,6 +1037,56 @@ const server = http.createServer(async (req, res) => {
|
|
|
804
1037
|
}
|
|
805
1038
|
}
|
|
806
1039
|
|
|
1040
|
+
// Handle time/credits command — show remaining time credits
|
|
1041
|
+
const cmd = lastContent?.trim().toLowerCase();
|
|
1042
|
+
if (cmd === 'time' || cmd === 'credits') {
|
|
1043
|
+
log('Time command for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
1044
|
+
const statusUrl = BACKEND_URL + '/v1/wallet/status';
|
|
1045
|
+
const statusRes = await fetch(statusUrl, {
|
|
1046
|
+
headers: { 'X-Wallet-Hash': walletHash }
|
|
1047
|
+
});
|
|
1048
|
+
let content;
|
|
1049
|
+
if (statusRes.ok) {
|
|
1050
|
+
const statusData = await statusRes.json();
|
|
1051
|
+
const sessions = statusData.sessions || [];
|
|
1052
|
+
if (sessions.length === 0) {
|
|
1053
|
+
content = '⏱️ No active time credits.';
|
|
1054
|
+
} else {
|
|
1055
|
+
const lines = sessions.map(s => {
|
|
1056
|
+
const total = formatDuration(s.time_credits_seconds);
|
|
1057
|
+
const remaining = formatDuration(s.time_remaining_seconds);
|
|
1058
|
+
const used = formatDuration(s.time_used_seconds);
|
|
1059
|
+
return ' - **' + (s.display_name || s.ai_model) + '** — ' + remaining + ' remaining of ' + total + ' purchased (' + used + ' used)';
|
|
1060
|
+
});
|
|
1061
|
+
content = '⏱️ **Active Time Credits:**\\n' + lines.join('\\n');
|
|
1062
|
+
}
|
|
1063
|
+
} else {
|
|
1064
|
+
content = '⏱️ Unable to check time credits.';
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
sseLine(res, {
|
|
1068
|
+
id: 'time-1',
|
|
1069
|
+
object: 'chat.completion.chunk',
|
|
1070
|
+
created: Math.floor(Date.now() / 1000),
|
|
1071
|
+
model: 'deepseek-ai/DeepSeek-V4-Flash',
|
|
1072
|
+
choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
|
|
1073
|
+
});
|
|
1074
|
+
sseLine(res, {
|
|
1075
|
+
id: 'time-2',
|
|
1076
|
+
object: 'chat.completion.chunk',
|
|
1077
|
+
choices: [{ index: 0, delta: { content: content + '\\n' }, finish_reason: 'stop' }],
|
|
1078
|
+
});
|
|
1079
|
+
sseLine(res, {
|
|
1080
|
+
id: 'time-3',
|
|
1081
|
+
object: 'chat.completion.chunk',
|
|
1082
|
+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
|
|
1083
|
+
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
|
1084
|
+
});
|
|
1085
|
+
sseDone(res);
|
|
1086
|
+
res.end();
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
807
1090
|
let isStreaming = true;
|
|
808
1091
|
try { isStreaming = JSON.parse(body).stream !== false; } catch {}
|
|
809
1092
|
|
|
@@ -819,20 +1102,56 @@ const server = http.createServer(async (req, res) => {
|
|
|
819
1102
|
|
|
820
1103
|
if (statusCode === 402) {
|
|
821
1104
|
log('402 intercepted for wallet ' + walletHash?.substring(0, 16) + '...');
|
|
822
|
-
|
|
1105
|
+
|
|
1106
|
+
// Parse 402 response for model_id and price_tiers
|
|
1107
|
+
let modelId = null;
|
|
1108
|
+
let displayName = null;
|
|
1109
|
+
let tiers = null;
|
|
1110
|
+
try {
|
|
1111
|
+
const parsed = JSON.parse(responseBody);
|
|
1112
|
+
modelId = parsed.model_id || null;
|
|
1113
|
+
displayName = parsed.display_name || null;
|
|
1114
|
+
tiers = parsed.price_tiers || null;
|
|
1115
|
+
} catch (e) {
|
|
1116
|
+
log('Could not parse 402 body: ' + e.message);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
pendingPayments.set(walletHash, {
|
|
1120
|
+
body: body,
|
|
1121
|
+
modelId: modelId,
|
|
1122
|
+
durationMinutes: null,
|
|
1123
|
+
tiers: tiers,
|
|
1124
|
+
step: tiers ? 'tier_select' : 'approval'
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
if (tiers && tiers.length > 0) {
|
|
1128
|
+
// New flow: show tier selection prompt
|
|
1129
|
+
await streamTierSelectionPrompt(res, walletHash, displayName || modelId || 'AI Model', tiers);
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
823
1132
|
|
|
824
1133
|
// Check session status to determine if this is a renewal
|
|
825
1134
|
let isRenewal = false;
|
|
826
1135
|
let tokensUsed = 0;
|
|
827
1136
|
let tokenLimit = 50000;
|
|
828
|
-
let
|
|
1137
|
+
let timeRemainingSeconds = 0;
|
|
829
1138
|
|
|
1139
|
+
let statusModelId = modelId;
|
|
830
1140
|
try {
|
|
1141
|
+
// Extract model from the original request body if not in 402
|
|
1142
|
+
if (!statusModelId) {
|
|
1143
|
+
try {
|
|
1144
|
+
const bodyParsed = JSON.parse(body);
|
|
1145
|
+
statusModelId = bodyParsed.model || null;
|
|
1146
|
+
} catch (e) {}
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const statusPath = '/v1/wallet/status' + (statusModelId ? '?model_id=' + encodeURIComponent(statusModelId) : '');
|
|
831
1150
|
const statusResponse = await new Promise((resolve, reject) => {
|
|
832
1151
|
const statusReq = REQUester.get({
|
|
833
1152
|
hostname: DJANGO_HOST,
|
|
834
1153
|
port: DJANGO_PORT,
|
|
835
|
-
path:
|
|
1154
|
+
path: statusPath,
|
|
836
1155
|
headers: { 'X-Wallet-Hash': walletHash }
|
|
837
1156
|
}, (res) => {
|
|
838
1157
|
let data = '';
|
|
@@ -852,20 +1171,17 @@ const server = http.createServer(async (req, res) => {
|
|
|
852
1171
|
if (statusResponse) {
|
|
853
1172
|
tokensUsed = statusResponse.tokens_used || 0;
|
|
854
1173
|
tokenLimit = statusResponse.token_limit || 50000;
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const hasExpiredSession = !statusResponse.session_active && tokensUsed > 0;
|
|
858
|
-
const carryoverStillValid = (statusResponse.carryover_remaining_minutes || 0) > 0;
|
|
1174
|
+
timeRemainingSeconds = statusResponse.time_remaining_seconds || 0;
|
|
859
1175
|
|
|
860
|
-
// Renewal if
|
|
861
|
-
isRenewal = (statusResponse.
|
|
862
|
-
|
|
1176
|
+
// Renewal if session has been used (tokens > 0 or time > 0) but is now exhausted
|
|
1177
|
+
isRenewal = (tokensUsed > 0 || statusResponse.time_used_seconds > 0) &&
|
|
1178
|
+
(!statusResponse.session_active || timeRemainingSeconds <= 0);
|
|
863
1179
|
}
|
|
864
1180
|
} catch (err) {
|
|
865
1181
|
log('Failed to check session status: ' + err.message);
|
|
866
1182
|
}
|
|
867
1183
|
|
|
868
|
-
await streamPaymentPrompt(res, walletHash, isRenewal, tokensUsed, tokenLimit,
|
|
1184
|
+
await streamPaymentPrompt(res, walletHash, isRenewal, tokensUsed, tokenLimit, timeRemainingSeconds);
|
|
869
1185
|
} else {
|
|
870
1186
|
if (res.headersSent) {
|
|
871
1187
|
log('Streaming response completed and already sent');
|
|
@@ -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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiuCnC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,eAAO,MAAM,mBAAmB,2BAA2B,CAAC;AAC5D,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAGvC,wBAAgB,YAAY,IAAI,MAAM,CAGrC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAIvD;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAiCpD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAIlE;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD;AAED,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpD;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1D;AAGD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAQvD;AAGD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,OAAO,CAOhE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAOA,iBAAe,cAAc,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,QAAQ,CAAC,EAAE,GAAG;;;;kBA+DlC,GAAG;6BA8CQ,GAAG,UAAU,GAAG;GAMlD;;;;;AAED,kBAAoE"}
|