@ottocode/server 0.1.224 → 0.1.225

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ottocode/server",
3
- "version": "0.1.224",
3
+ "version": "0.1.225",
4
4
  "description": "HTTP API server for ottocode",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -49,8 +49,8 @@
49
49
  "typecheck": "tsc --noEmit"
50
50
  },
51
51
  "dependencies": {
52
- "@ottocode/sdk": "0.1.224",
53
- "@ottocode/database": "0.1.224",
52
+ "@ottocode/sdk": "0.1.225",
53
+ "@ottocode/database": "0.1.225",
54
54
  "drizzle-orm": "^0.44.5",
55
55
  "hono": "^4.9.9",
56
56
  "zod": "^4.3.6"
@@ -447,4 +447,148 @@ export const setuPaths = {
447
447
  },
448
448
  },
449
449
  },
450
+ '/v1/setu/topup/razorpay/estimate': {
451
+ get: {
452
+ tags: ['setu'],
453
+ operationId: 'getRazorpayTopupEstimate',
454
+ summary: 'Get estimated fees for a Razorpay topup',
455
+ parameters: [
456
+ {
457
+ in: 'query',
458
+ name: 'amount',
459
+ required: true,
460
+ schema: { type: 'number' },
461
+ description: 'Amount in USD',
462
+ },
463
+ ],
464
+ responses: {
465
+ 200: {
466
+ description: 'OK',
467
+ content: {
468
+ 'application/json': {
469
+ schema: {
470
+ type: 'object',
471
+ properties: {
472
+ creditAmountUsd: { type: 'number' },
473
+ chargeAmountInr: { type: 'number' },
474
+ feeAmountInr: { type: 'number' },
475
+ currency: { type: 'string' },
476
+ exchangeRate: { type: 'number' },
477
+ },
478
+ },
479
+ },
480
+ },
481
+ },
482
+ },
483
+ },
484
+ },
485
+ '/v1/setu/topup/razorpay': {
486
+ post: {
487
+ tags: ['setu'],
488
+ operationId: 'createRazorpayOrder',
489
+ summary: 'Create a Razorpay order for topping up',
490
+ requestBody: {
491
+ required: true,
492
+ content: {
493
+ 'application/json': {
494
+ schema: {
495
+ type: 'object',
496
+ properties: {
497
+ amount: { type: 'number' },
498
+ },
499
+ required: ['amount'],
500
+ },
501
+ },
502
+ },
503
+ },
504
+ responses: {
505
+ 200: {
506
+ description: 'OK',
507
+ content: {
508
+ 'application/json': {
509
+ schema: {
510
+ type: 'object',
511
+ properties: {
512
+ success: { type: 'boolean' },
513
+ orderId: { type: 'string' },
514
+ amount: { type: 'number' },
515
+ currency: { type: 'string' },
516
+ creditAmountUsd: { type: 'number' },
517
+ keyId: { type: 'string' },
518
+ },
519
+ },
520
+ },
521
+ },
522
+ },
523
+ 401: {
524
+ description: 'Wallet not configured',
525
+ content: {
526
+ 'application/json': {
527
+ schema: {
528
+ type: 'object',
529
+ properties: { error: { type: 'string' } },
530
+ required: ['error'],
531
+ },
532
+ },
533
+ },
534
+ },
535
+ },
536
+ },
537
+ },
538
+ '/v1/setu/topup/razorpay/verify': {
539
+ post: {
540
+ tags: ['setu'],
541
+ operationId: 'verifyRazorpayPayment',
542
+ summary: 'Verify Razorpay payment and credit balance',
543
+ requestBody: {
544
+ required: true,
545
+ content: {
546
+ 'application/json': {
547
+ schema: {
548
+ type: 'object',
549
+ properties: {
550
+ razorpay_order_id: { type: 'string' },
551
+ razorpay_payment_id: { type: 'string' },
552
+ razorpay_signature: { type: 'string' },
553
+ },
554
+ required: [
555
+ 'razorpay_order_id',
556
+ 'razorpay_payment_id',
557
+ 'razorpay_signature',
558
+ ],
559
+ },
560
+ },
561
+ },
562
+ },
563
+ responses: {
564
+ 200: {
565
+ description: 'OK',
566
+ content: {
567
+ 'application/json': {
568
+ schema: {
569
+ type: 'object',
570
+ properties: {
571
+ success: { type: 'boolean' },
572
+ credited: { type: 'number' },
573
+ newBalance: { type: 'number' },
574
+ },
575
+ },
576
+ },
577
+ },
578
+ },
579
+ 401: {
580
+ description: 'Wallet not configured',
581
+ content: {
582
+ 'application/json': {
583
+ schema: {
584
+ type: 'object',
585
+ properties: { error: { type: 'string' } },
586
+ required: ['error'],
587
+ },
588
+ },
589
+ },
590
+ },
591
+ },
592
+ },
593
+ },
450
594
  } as const;
@@ -369,4 +369,120 @@ export function registerSetuRoutes(app: Hono) {
369
369
  return c.json(errorResponse, errorResponse.error.status || 500);
370
370
  }
371
371
  });
372
+
373
+ app.get('/v1/setu/topup/razorpay/estimate', async (c) => {
374
+ try {
375
+ const amount = c.req.query('amount');
376
+ if (!amount) {
377
+ return c.json({ error: 'Missing amount parameter' }, 400);
378
+ }
379
+
380
+ const baseUrl = getSetuBaseUrl();
381
+ const response = await fetch(
382
+ `${baseUrl}/v1/topup/razorpay/estimate?amount=${amount}`,
383
+ {
384
+ method: 'GET',
385
+ headers: { 'Content-Type': 'application/json' },
386
+ },
387
+ );
388
+
389
+ const data = await response.json();
390
+ if (!response.ok) {
391
+ return c.json(data, response.status as 400 | 500);
392
+ }
393
+
394
+ return c.json(data);
395
+ } catch (error) {
396
+ logger.error('Failed to get Razorpay estimate', error);
397
+ const errorResponse = serializeError(error);
398
+ return c.json(errorResponse, errorResponse.error.status || 500);
399
+ }
400
+ });
401
+
402
+ app.post('/v1/setu/topup/razorpay', async (c) => {
403
+ try {
404
+ const privateKey = await getSetuPrivateKey();
405
+ if (!privateKey) {
406
+ return c.json({ error: 'Setu wallet not configured' }, 401);
407
+ }
408
+
409
+ const body = await c.req.json();
410
+ const { amount } = body as { amount: number };
411
+
412
+ if (!amount || typeof amount !== 'number') {
413
+ return c.json({ error: 'Invalid amount' }, 400);
414
+ }
415
+
416
+ const walletHeaders = buildWalletHeaders(privateKey);
417
+ const baseUrl = getSetuBaseUrl();
418
+
419
+ const response = await fetch(`${baseUrl}/v1/topup/razorpay`, {
420
+ method: 'POST',
421
+ headers: {
422
+ 'Content-Type': 'application/json',
423
+ ...walletHeaders,
424
+ },
425
+ body: JSON.stringify({ amount }),
426
+ });
427
+
428
+ const data = await response.json();
429
+ if (!response.ok) {
430
+ return c.json(data, response.status as 400 | 500);
431
+ }
432
+
433
+ return c.json(data);
434
+ } catch (error) {
435
+ logger.error('Failed to create Razorpay order', error);
436
+ const errorResponse = serializeError(error);
437
+ return c.json(errorResponse, errorResponse.error.status || 500);
438
+ }
439
+ });
440
+
441
+ app.post('/v1/setu/topup/razorpay/verify', async (c) => {
442
+ try {
443
+ const privateKey = await getSetuPrivateKey();
444
+ if (!privateKey) {
445
+ return c.json({ error: 'Setu wallet not configured' }, 401);
446
+ }
447
+
448
+ const body = await c.req.json();
449
+ const { razorpay_order_id, razorpay_payment_id, razorpay_signature } =
450
+ body as {
451
+ razorpay_order_id: string;
452
+ razorpay_payment_id: string;
453
+ razorpay_signature: string;
454
+ };
455
+
456
+ if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) {
457
+ return c.json({ error: 'Missing payment details' }, 400);
458
+ }
459
+
460
+ const walletHeaders = buildWalletHeaders(privateKey);
461
+ const baseUrl = getSetuBaseUrl();
462
+
463
+ const response = await fetch(`${baseUrl}/v1/topup/razorpay/verify`, {
464
+ method: 'POST',
465
+ headers: {
466
+ 'Content-Type': 'application/json',
467
+ ...walletHeaders,
468
+ },
469
+ body: JSON.stringify({
470
+ razorpay_order_id,
471
+ razorpay_payment_id,
472
+ razorpay_signature,
473
+ }),
474
+ });
475
+
476
+ const data = await response.json();
477
+ if (!response.ok) {
478
+ return c.json(data, response.status as 400 | 500);
479
+ }
480
+
481
+ return c.json(data);
482
+ } catch (error) {
483
+ logger.error('Failed to verify Razorpay payment', error);
484
+ const errorResponse = serializeError(error);
485
+ return c.json(errorResponse, errorResponse.error.status || 500);
486
+ }
487
+ });
372
488
  }
@@ -7,6 +7,7 @@ export type OauthCodexContinuationInput = {
7
7
  finishReason?: string;
8
8
  rawFinishReason?: string;
9
9
  firstToolSeen: boolean;
10
+ hasTrailingAssistantText: boolean;
10
11
  droppedPseudoToolText: boolean;
11
12
  lastAssistantText: string;
12
13
  };
@@ -40,6 +41,13 @@ function isTruncatedResponse(
40
41
  return rawFinishReason === 'max_output_tokens';
41
42
  }
42
43
 
44
+ function isMissingAssistantSummary(
45
+ input: OauthCodexContinuationInput,
46
+ ): boolean {
47
+ if (!input.firstToolSeen) return false;
48
+ return !input.hasTrailingAssistantText;
49
+ }
50
+
43
51
  const MAX_UNCLEAN_EOF_RETRIES = 1;
44
52
 
45
53
  function isUncleanEof(input: OauthCodexContinuationInput): boolean {
@@ -68,6 +76,10 @@ export function decideOauthCodexContinuation(
68
76
  return { shouldContinue: true, reason: 'truncated' };
69
77
  }
70
78
 
79
+ if (isMissingAssistantSummary(input)) {
80
+ return { shouldContinue: true, reason: 'no-trailing-assistant-text' };
81
+ }
82
+
71
83
  if (
72
84
  isUncleanEof(input) &&
73
85
  input.continuationCount < MAX_UNCLEAN_EOF_RETRIES
@@ -180,7 +180,13 @@ async function runAssistant(opts: RunOpts) {
180
180
  );
181
181
 
182
182
  let _finishObserved = false;
183
+ let _toolActivityObserved = false;
184
+ let _trailingAssistantTextAfterTool = false;
183
185
  const unsubscribeFinish = subscribe(opts.sessionId, (evt) => {
186
+ if (evt.type === 'tool.call' || evt.type === 'tool.result') {
187
+ _toolActivityObserved = true;
188
+ _trailingAssistantTextAfterTool = false;
189
+ }
184
190
  if (evt.type !== 'tool.result') return;
185
191
  try {
186
192
  const name = (evt.payload as { name?: string } | undefined)?.name;
@@ -287,6 +293,12 @@ async function runAssistant(opts: RunOpts) {
287
293
  if (accumulated.trim()) {
288
294
  latestAssistantText = accumulated;
289
295
  }
296
+ if (
297
+ (delta.trim().length > 0 && _toolActivityObserved) ||
298
+ (delta.trim().length > 0 && firstToolSeen())
299
+ ) {
300
+ _trailingAssistantTextAfterTool = true;
301
+ }
290
302
 
291
303
  if (!currentPartId && !accumulated.trim()) {
292
304
  continue;
@@ -404,7 +416,7 @@ async function runAssistant(opts: RunOpts) {
404
416
  }
405
417
 
406
418
  debugLog(
407
- `[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}`,
419
+ `[RUNNER] Stream finished. finishSeen=${_finishObserved}, firstToolSeen=${fs}, trailingAssistantTextAfterTool=${_trailingAssistantTextAfterTool}, finishReason=${streamFinishReason}, rawFinishReason=${streamRawFinishReason}`,
408
420
  );
409
421
 
410
422
  const MAX_CONTINUATIONS = 6;
@@ -418,6 +430,7 @@ async function runAssistant(opts: RunOpts) {
418
430
  finishReason: streamFinishReason,
419
431
  rawFinishReason: streamRawFinishReason,
420
432
  firstToolSeen: fs,
433
+ hasTrailingAssistantText: _trailingAssistantTextAfterTool,
421
434
  droppedPseudoToolText: oauthTextGuard?.dropped ?? false,
422
435
  lastAssistantText: latestAssistantText,
423
436
  });