Compare commits

...

12 Commits

Author SHA1 Message Date
9qeklajc
4c31bf9767 enforce payment finalization 2026-02-01 23:01:47 +01:00
shroominic
1b3b206a20 Merge pull request #336 from Routstr/prevent-payout-race-contition
prevent payout race condition
2026-01-30 10:49:44 +08:00
shroominic
b9b477e5eb Merge pull request #335 from Routstr/fix-recurring-payout-error
Fix recurring payout error
2026-01-30 10:06:37 +08:00
Shroominic
ace8cf960c prevent payout race condition 2026-01-30 10:05:37 +08:00
Shroominic
3396e0cd47 fix recurring payout error 2026-01-30 09:59:59 +08:00
shroominic
855d60b4a5 Merge pull request #330 from Routstr/fix-provider-model-pricing
fix provider model pricing
2026-01-29 10:58:27 +08:00
shroominic
27ace348b5 Merge pull request #329 from Routstr/fix-multiple-custom-models
fix: unable to add multiple custom models
2026-01-29 10:58:14 +08:00
Shroominic
c8f8857f03 fckng prettier again 2026-01-28 18:12:54 +08:00
Shroominic
c9650441bb fix build error 2026-01-28 18:11:07 +08:00
Shroominic
5ce9c2217f fix fmt 2026-01-28 18:04:56 +08:00
Shroominic
89b8488ab8 prettier 2026-01-28 17:58:24 +08:00
Shroominic
2ee917fa31 fix not being able to add multiple custom models when provider does not have Provided Models 2026-01-28 17:55:06 +08:00
4 changed files with 112 additions and 172 deletions

View File

@@ -291,9 +291,7 @@ async def raw_send_to_lnurl(
lnurl_data["callback_url"], final_amount
)
melt_quote_resp = await wallet.melt_quote(
invoice=bolt11_invoice, amount_msat=final_amount
)
melt_quote_resp = await wallet.melt_quote(invoice=bolt11_invoice)
if amount:
proofs, _ = await wallet.select_to_send(proofs, amount, set_reserved=True)

View File

@@ -427,7 +427,11 @@ class BaseUpstreamProvider:
)
async def handle_streaming_chat_completion(
self, response: httpx.Response, key: ApiKey, max_cost_for_model: int
self,
response: httpx.Response,
key: ApiKey,
max_cost_for_model: int,
background_tasks: BackgroundTasks,
) -> StreamingResponse:
"""Handle streaming chat completion responses with token usage tracking and cost adjustment.
@@ -534,7 +538,14 @@ class BaseUpstreamProvider:
}
yield f"data: {json.dumps(usage_chunk_data)}\n\n".encode()
usage_finalized = True
except Exception:
except Exception as e:
logger.exception(
"Error during usage finalization",
extra={
"key_hash": key.hashed_key[:8] + "...",
"error": str(e),
},
)
# Fallback: yield original usage chunk if adjustment fails
yield f"data: {json.dumps(usage_chunk_data)}\n\n".encode()
@@ -555,7 +566,9 @@ class BaseUpstreamProvider:
raise
finally:
if not usage_finalized:
await finalize_db_only()
# Create a background task to ensure finalization happens
# even if the generator is closed early
background_tasks.add_task(finalize_db_only)
# Remove inaccurate encoding headers from upstream response
response_headers = dict(response.headers)
@@ -1136,12 +1149,12 @@ class BaseUpstreamProvider:
)
if is_streaming and response.status_code == 200:
result = await self.handle_streaming_chat_completion(
response, key, max_cost_for_model
)
background_tasks = BackgroundTasks()
background_tasks.add_task(response.aclose)
background_tasks.add_task(client.aclose)
result = await self.handle_streaming_chat_completion(
response, key, max_cost_for_model, background_tasks
)
result.background = background_tasks
return result

View File

@@ -319,6 +319,7 @@ async def periodic_payout() -> None:
wallet, mint_url, unit, not_reserved=True
)
proofs = await slow_filter_spend_proofs(proofs, wallet)
await asyncio.sleep(5)
user_balance = await db.balances_for_mint_and_unit(
session, mint_url, unit
)
@@ -344,8 +345,6 @@ async def periodic_payout() -> None:
"amount_received": amount_received,
},
)
await asyncio.sleep(5)
except Exception as e:
logger.error(
f"Error sending payout: {type(e).__name__}",

View File

@@ -286,7 +286,9 @@ function ProviderBalance({
<div className='rounded-lg border-2 border-gray-200 p-2 dark:border-gray-800'>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodeURIComponent(invoiceData.payment_request)}`}
src={`https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=${encodeURIComponent(
invoiceData.payment_request
)}`}
alt='Lightning Invoice QR Code'
className='h-64 w-64'
/>
@@ -933,25 +935,71 @@ export default function ProvidersPage() {
</div>
) : providerModels &&
viewingModels === provider.id ? (
providerModels.remote_models.length === 0 ? (
// No provided models - show custom models directly without tabs
<div className='space-y-2'>
{providerModels.db_models.length === 0 ? (
<div className='flex flex-col items-center justify-center gap-2 py-4'>
<Tabs
defaultValue={
providerModels.remote_models.length > 0
? 'provided'
: 'custom'
}
className='w-full'
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger
value='provided'
className='text-xs sm:text-sm'
>
<span className='hidden sm:inline'>
Provided Models
</span>
<span className='sm:hidden'>Provided</span>
<Badge
variant='secondary'
className='ml-1 text-xs sm:ml-2'
>
{providerModels.remote_models.length}
</Badge>
</TabsTrigger>
<TabsTrigger
value='custom'
className='text-xs sm:text-sm'
>
<span className='hidden sm:inline'>
Custom Models
</span>
<span className='sm:hidden'>Custom</span>
<Badge
variant='secondary'
className='ml-1 text-xs sm:ml-2'
>
{providerModels.db_models.length}
</Badge>
</TabsTrigger>
</TabsList>
<TabsContent
value='custom'
className='mt-4 space-y-2'
>
<div className='flex items-center justify-between'>
{providerModels.db_models.length > 0 && (
<div className='text-muted-foreground text-sm'>
No models configured. Add custom models
to use this provider.
Custom models override or extend the
provider&apos;s catalog.
</div>
<Button
variant='outline'
size='sm'
onClick={() =>
handleAddModel(provider.id)
}
>
<Plus className='mr-2 h-4 w-4' />
Add Custom Model
</Button>
)}
<Button
variant='outline'
size='sm'
onClick={() =>
handleAddModel(provider.id)
}
>
<Plus className='mr-2 h-4 w-4' />
Add
</Button>
</div>
{providerModels.db_models.length === 0 ? (
<div className='text-muted-foreground py-4 text-center text-sm'>
No custom models configured
</div>
) : (
<div className='space-y-2'>
@@ -1005,98 +1053,27 @@ export default function ProvidersPage() {
))}
</div>
)}
</div>
) : (
// Has provided models - show tabs
<Tabs
defaultValue='provided'
className='w-full'
</TabsContent>
<TabsContent
value='provided'
className='mt-4 space-y-2'
>
<TabsList className='grid w-full grid-cols-2'>
<TabsTrigger
value='provided'
className='text-xs sm:text-sm'
>
<span className='hidden sm:inline'>
Provided Models
</span>
<span className='sm:hidden'>
Provided
</span>
<Badge
variant='secondary'
className='ml-1 text-xs sm:ml-2'
>
{providerModels.remote_models.length}
</Badge>
</TabsTrigger>
<TabsTrigger
value='custom'
className='text-xs sm:text-sm'
>
<span className='hidden sm:inline'>
Custom Models
</span>
<span className='sm:hidden'>Custom</span>
<Badge
variant='secondary'
className='ml-1 text-xs sm:ml-2'
>
{providerModels.db_models.length}
</Badge>
</TabsTrigger>
</TabsList>
<TabsContent
value='custom'
className='mt-4 space-y-2'
>
<div className='flex items-center justify-between'>
{providerModels.db_models.length > 0 && (
<div className='text-muted-foreground text-sm'>
Custom models override or extend the
provider&apos;s catalog.
</div>
)}
<Button
variant='outline'
size='sm'
onClick={() =>
handleAddModel(provider.id)
}
>
<Plus className='mr-2 h-4 w-4' />
Add
</Button>
</div>
{providerModels.db_models.length === 0 ? (
<div className='text-muted-foreground py-4 text-center text-sm'>
No custom models configured
{providerModels.remote_models.length > 0 ? (
<>
<div className='text-muted-foreground mb-3 text-sm'>
Models automatically discovered from the
provider&apos;s catalog.
</div>
) : (
<div className='space-y-2'>
{providerModels.db_models.map(
{providerModels.remote_models.map(
(model) => (
<div
key={model.id}
className='hover:bg-accent flex flex-col gap-2 rounded-lg border p-3 transition-colors sm:flex-row sm:items-center sm:justify-between'
>
<div className='min-w-0 flex-1'>
<div className='flex flex-col gap-1 sm:flex-row sm:items-center sm:gap-2'>
<span className='truncate font-mono text-sm font-medium'>
{model.id}
</span>
<Badge
variant={
model.enabled
? 'default'
: 'secondary'
}
className='w-fit text-xs'
>
{model.enabled
? 'Enabled'
: 'Disabled'}
</Badge>
<div className='truncate font-mono text-sm font-medium'>
{model.id}
</div>
<div className='text-muted-foreground mt-1 text-xs break-words'>
{model.description ||
@@ -1109,79 +1086,32 @@ export default function ProvidersPage() {
tokens
</div>
<Button
variant='ghost'
size='icon'
className='h-8 w-8'
variant='outline'
size='sm'
className='h-7 text-xs'
onClick={() =>
handleEditModel(
handleOverrideModel(
provider.id,
model
)
}
>
<Pencil className='h-4 w-4' />
<Plus className='mr-1 h-3 w-3' />
Override
</Button>
</div>
</div>
)
)}
</div>
)}
</TabsContent>
<TabsContent
value='provided'
className='mt-4 space-y-2'
>
{providerModels.remote_models.length >
0 && (
<div className='text-muted-foreground mb-3 text-sm'>
Models automatically discovered from the
provider&apos;s catalog.
</div>
)}
<div className='space-y-2'>
{providerModels.remote_models.map(
(model) => (
<div
key={model.id}
className='hover:bg-accent flex flex-col gap-2 rounded-lg border p-3 transition-colors sm:flex-row sm:items-center sm:justify-between'
>
<div className='min-w-0 flex-1'>
<div className='truncate font-mono text-sm font-medium'>
{model.id}
</div>
<div className='text-muted-foreground mt-1 text-xs break-words'>
{model.description ||
model.name}
</div>
</div>
<div className='flex items-center gap-2'>
<div className='text-muted-foreground text-xs whitespace-nowrap'>
{model.context_length?.toLocaleString()}{' '}
tokens
</div>
<Button
variant='outline'
size='sm'
className='h-7 text-xs'
onClick={() =>
handleOverrideModel(
provider.id,
model
)
}
>
<Plus className='mr-1 h-3 w-3' />
Override
</Button>
</div>
</div>
)
)}
</>
) : (
<div className='text-muted-foreground py-4 text-center text-sm'>
No provided models available
</div>
</TabsContent>
</Tabs>
)
)}
</TabsContent>
</Tabs>
) : null}
</div>
)}