codeshift 0.3.7__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- codeshift/__init__.py +2 -2
- codeshift/cli/__init__.py +1 -1
- codeshift/cli/commands/__init__.py +1 -1
- codeshift/cli/commands/auth.py +5 -5
- codeshift/cli/commands/scan.py +2 -5
- codeshift/cli/commands/upgrade.py +2 -7
- codeshift/cli/commands/upgrade_all.py +1 -1
- codeshift/cli/main.py +2 -2
- codeshift/migrator/llm_migrator.py +8 -12
- codeshift/utils/__init__.py +1 -1
- codeshift/utils/api_client.py +11 -11
- codeshift/utils/cache.py +1 -1
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/METADATA +2 -17
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/RECORD +18 -34
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/licenses/LICENSE +1 -1
- codeshift/api/__init__.py +0 -1
- codeshift/api/auth.py +0 -182
- codeshift/api/config.py +0 -73
- codeshift/api/database.py +0 -215
- codeshift/api/main.py +0 -103
- codeshift/api/models/__init__.py +0 -55
- codeshift/api/models/auth.py +0 -108
- codeshift/api/models/billing.py +0 -92
- codeshift/api/models/migrate.py +0 -42
- codeshift/api/models/usage.py +0 -116
- codeshift/api/routers/__init__.py +0 -5
- codeshift/api/routers/auth.py +0 -440
- codeshift/api/routers/billing.py +0 -395
- codeshift/api/routers/migrate.py +0 -304
- codeshift/api/routers/usage.py +0 -291
- codeshift/api/routers/webhooks.py +0 -289
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/WHEEL +0 -0
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/entry_points.txt +0 -0
- {codeshift-0.3.7.dist-info → codeshift-0.4.0.dist-info}/top_level.txt +0 -0
codeshift/api/routers/billing.py
DELETED
|
@@ -1,395 +0,0 @@
|
|
|
1
|
-
"""Billing router for the PyResolve API."""
|
|
2
|
-
|
|
3
|
-
import stripe
|
|
4
|
-
from fastapi import APIRouter, HTTPException, status
|
|
5
|
-
|
|
6
|
-
from codeshift.api.auth import CurrentUser
|
|
7
|
-
from codeshift.api.config import get_settings
|
|
8
|
-
from codeshift.api.database import get_database
|
|
9
|
-
from codeshift.api.models.billing import (
|
|
10
|
-
BillingOverview,
|
|
11
|
-
CheckoutSessionRequest,
|
|
12
|
-
CheckoutSessionResponse,
|
|
13
|
-
PaymentMethodInfo,
|
|
14
|
-
PortalSessionResponse,
|
|
15
|
-
SubscriptionInfo,
|
|
16
|
-
TierInfo,
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
router = APIRouter()
|
|
20
|
-
|
|
21
|
-
# Tier definitions
|
|
22
|
-
TIERS: dict[str, TierInfo] = {
|
|
23
|
-
"free": TierInfo(
|
|
24
|
-
name="free",
|
|
25
|
-
display_name="Free",
|
|
26
|
-
price_monthly=0,
|
|
27
|
-
files_per_month=100,
|
|
28
|
-
llm_calls_per_month=50,
|
|
29
|
-
features=[
|
|
30
|
-
"100 file migrations/month",
|
|
31
|
-
"50 LLM-assisted migrations/month",
|
|
32
|
-
"5 supported libraries",
|
|
33
|
-
"Community support",
|
|
34
|
-
],
|
|
35
|
-
),
|
|
36
|
-
"pro": TierInfo(
|
|
37
|
-
name="pro",
|
|
38
|
-
display_name="Pro",
|
|
39
|
-
price_monthly=1900, # $19.00
|
|
40
|
-
files_per_month=1000,
|
|
41
|
-
llm_calls_per_month=500,
|
|
42
|
-
features=[
|
|
43
|
-
"1,000 file migrations/month",
|
|
44
|
-
"500 LLM-assisted migrations/month",
|
|
45
|
-
"All supported libraries",
|
|
46
|
-
"Priority support",
|
|
47
|
-
"Custom knowledge bases",
|
|
48
|
-
],
|
|
49
|
-
),
|
|
50
|
-
"unlimited": TierInfo(
|
|
51
|
-
name="unlimited",
|
|
52
|
-
display_name="Unlimited",
|
|
53
|
-
price_monthly=4900, # $49.00
|
|
54
|
-
files_per_month=999999999,
|
|
55
|
-
llm_calls_per_month=999999999,
|
|
56
|
-
features=[
|
|
57
|
-
"Unlimited file migrations",
|
|
58
|
-
"Unlimited LLM-assisted migrations",
|
|
59
|
-
"All supported libraries",
|
|
60
|
-
"Priority support",
|
|
61
|
-
"Custom knowledge bases",
|
|
62
|
-
"Usage analytics",
|
|
63
|
-
],
|
|
64
|
-
),
|
|
65
|
-
"enterprise": TierInfo(
|
|
66
|
-
name="enterprise",
|
|
67
|
-
display_name="Enterprise",
|
|
68
|
-
price_monthly=0, # Custom pricing
|
|
69
|
-
files_per_month=999999999,
|
|
70
|
-
llm_calls_per_month=999999999,
|
|
71
|
-
features=[
|
|
72
|
-
"Unlimited migrations",
|
|
73
|
-
"Dedicated support",
|
|
74
|
-
"Custom integrations",
|
|
75
|
-
"SLA guarantees",
|
|
76
|
-
"Self-hosted option",
|
|
77
|
-
"SSO/SAML",
|
|
78
|
-
],
|
|
79
|
-
),
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def get_stripe_client() -> stripe:
|
|
84
|
-
"""Get configured Stripe client."""
|
|
85
|
-
settings = get_settings()
|
|
86
|
-
stripe.api_key = settings.stripe_secret_key
|
|
87
|
-
return stripe
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
@router.get("/tiers", response_model=list[TierInfo])
|
|
91
|
-
async def list_tiers() -> list[TierInfo]:
|
|
92
|
-
"""List all available pricing tiers."""
|
|
93
|
-
return list(TIERS.values())
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
@router.get("/tiers/{tier_name}", response_model=TierInfo)
|
|
97
|
-
async def get_tier(tier_name: str) -> TierInfo:
|
|
98
|
-
"""Get details about a specific tier."""
|
|
99
|
-
tier = TIERS.get(tier_name)
|
|
100
|
-
if not tier:
|
|
101
|
-
raise HTTPException(
|
|
102
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
103
|
-
detail=f"Tier '{tier_name}' not found",
|
|
104
|
-
)
|
|
105
|
-
return tier
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@router.get("/subscription", response_model=SubscriptionInfo)
|
|
109
|
-
async def get_subscription(user: CurrentUser) -> SubscriptionInfo:
|
|
110
|
-
"""Get the current user's subscription information."""
|
|
111
|
-
db = get_database()
|
|
112
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
113
|
-
|
|
114
|
-
if not profile:
|
|
115
|
-
raise HTTPException(
|
|
116
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
117
|
-
detail="User profile not found",
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# If user has a Stripe subscription, fetch details
|
|
121
|
-
subscription_id = profile.get("stripe_subscription_id")
|
|
122
|
-
if subscription_id:
|
|
123
|
-
try:
|
|
124
|
-
stripe_client = get_stripe_client()
|
|
125
|
-
subscription = stripe_client.Subscription.retrieve(subscription_id)
|
|
126
|
-
|
|
127
|
-
return SubscriptionInfo(
|
|
128
|
-
tier=profile.get("tier", "free"),
|
|
129
|
-
status=subscription.status,
|
|
130
|
-
stripe_subscription_id=subscription_id,
|
|
131
|
-
current_period_start=subscription.current_period_start,
|
|
132
|
-
current_period_end=subscription.current_period_end,
|
|
133
|
-
cancel_at_period_end=subscription.cancel_at_period_end,
|
|
134
|
-
)
|
|
135
|
-
except stripe.error.StripeError:
|
|
136
|
-
pass # Fall through to basic response
|
|
137
|
-
|
|
138
|
-
return SubscriptionInfo(
|
|
139
|
-
tier=profile.get("tier", "free"),
|
|
140
|
-
status="active" if profile.get("tier", "free") != "free" else "free",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@router.get("/overview", response_model=BillingOverview)
|
|
145
|
-
async def get_billing_overview(user: CurrentUser) -> BillingOverview:
|
|
146
|
-
"""Get complete billing overview for the current user."""
|
|
147
|
-
db = get_database()
|
|
148
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
149
|
-
|
|
150
|
-
if not profile:
|
|
151
|
-
raise HTTPException(
|
|
152
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
153
|
-
detail="User profile not found",
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
tier = profile.get("tier", "free")
|
|
157
|
-
tier_info = TIERS.get(tier, TIERS["free"])
|
|
158
|
-
|
|
159
|
-
subscription = SubscriptionInfo(
|
|
160
|
-
tier=tier,
|
|
161
|
-
status="active" if tier != "free" else "free",
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
payment_method: PaymentMethodInfo | None = None
|
|
165
|
-
|
|
166
|
-
# Fetch Stripe details if available
|
|
167
|
-
subscription_id = profile.get("stripe_subscription_id")
|
|
168
|
-
|
|
169
|
-
if subscription_id:
|
|
170
|
-
try:
|
|
171
|
-
stripe_client = get_stripe_client()
|
|
172
|
-
sub = stripe_client.Subscription.retrieve(subscription_id)
|
|
173
|
-
|
|
174
|
-
subscription = SubscriptionInfo(
|
|
175
|
-
tier=tier,
|
|
176
|
-
status=sub.status,
|
|
177
|
-
stripe_subscription_id=subscription_id,
|
|
178
|
-
current_period_start=sub.current_period_start,
|
|
179
|
-
current_period_end=sub.current_period_end,
|
|
180
|
-
cancel_at_period_end=sub.cancel_at_period_end,
|
|
181
|
-
)
|
|
182
|
-
|
|
183
|
-
# Get payment method
|
|
184
|
-
if sub.default_payment_method:
|
|
185
|
-
pm = stripe_client.PaymentMethod.retrieve(sub.default_payment_method)
|
|
186
|
-
if pm.type == "card" and pm.card:
|
|
187
|
-
payment_method = PaymentMethodInfo(
|
|
188
|
-
id=pm.id,
|
|
189
|
-
type=pm.type,
|
|
190
|
-
card_brand=pm.card.brand,
|
|
191
|
-
card_last4=pm.card.last4,
|
|
192
|
-
card_exp_month=pm.card.exp_month,
|
|
193
|
-
card_exp_year=pm.card.exp_year,
|
|
194
|
-
)
|
|
195
|
-
except stripe.error.StripeError:
|
|
196
|
-
pass
|
|
197
|
-
|
|
198
|
-
return BillingOverview(
|
|
199
|
-
subscription=subscription,
|
|
200
|
-
tier_info=tier_info,
|
|
201
|
-
payment_method=payment_method,
|
|
202
|
-
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@router.post("/checkout", response_model=CheckoutSessionResponse)
|
|
206
|
-
async def create_checkout_session(
|
|
207
|
-
request: CheckoutSessionRequest,
|
|
208
|
-
user: CurrentUser,
|
|
209
|
-
) -> CheckoutSessionResponse:
|
|
210
|
-
"""Create a Stripe checkout session for upgrading subscription."""
|
|
211
|
-
settings = get_settings()
|
|
212
|
-
stripe_client = get_stripe_client()
|
|
213
|
-
|
|
214
|
-
# Get or create Stripe customer
|
|
215
|
-
db = get_database()
|
|
216
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
217
|
-
|
|
218
|
-
if not profile:
|
|
219
|
-
raise HTTPException(
|
|
220
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
221
|
-
detail="User profile not found",
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
customer_id = profile.get("stripe_customer_id")
|
|
225
|
-
|
|
226
|
-
if not customer_id:
|
|
227
|
-
# Create Stripe customer
|
|
228
|
-
customer = stripe_client.Customer.create(
|
|
229
|
-
email=profile["email"],
|
|
230
|
-
metadata={
|
|
231
|
-
"user_id": user.user_id,
|
|
232
|
-
},
|
|
233
|
-
)
|
|
234
|
-
customer_id = customer.id
|
|
235
|
-
|
|
236
|
-
# Update profile with customer ID
|
|
237
|
-
db.update_profile(user.user_id, {"stripe_customer_id": customer_id})
|
|
238
|
-
|
|
239
|
-
# Get the price ID for the requested tier
|
|
240
|
-
if request.tier == "pro":
|
|
241
|
-
price_id = settings.stripe_price_id_pro
|
|
242
|
-
elif request.tier == "unlimited":
|
|
243
|
-
price_id = settings.stripe_price_id_unlimited
|
|
244
|
-
else:
|
|
245
|
-
raise HTTPException(
|
|
246
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
247
|
-
detail=f"Invalid tier: {request.tier}",
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
if not price_id:
|
|
251
|
-
raise HTTPException(
|
|
252
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
253
|
-
detail="Stripe price not configured",
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
# Create checkout session
|
|
257
|
-
success_url = request.success_url or f"{settings.codeshift_api_url}/billing/success"
|
|
258
|
-
cancel_url = request.cancel_url or f"{settings.codeshift_api_url}/billing/cancel"
|
|
259
|
-
|
|
260
|
-
try:
|
|
261
|
-
session = stripe_client.checkout.Session.create(
|
|
262
|
-
customer=customer_id,
|
|
263
|
-
mode="subscription",
|
|
264
|
-
line_items=[
|
|
265
|
-
{
|
|
266
|
-
"price": price_id,
|
|
267
|
-
"quantity": 1,
|
|
268
|
-
}
|
|
269
|
-
],
|
|
270
|
-
success_url=success_url + "?session_id={CHECKOUT_SESSION_ID}",
|
|
271
|
-
cancel_url=cancel_url,
|
|
272
|
-
metadata={
|
|
273
|
-
"user_id": user.user_id,
|
|
274
|
-
"tier": request.tier,
|
|
275
|
-
},
|
|
276
|
-
)
|
|
277
|
-
except stripe.error.StripeError as e:
|
|
278
|
-
raise HTTPException(
|
|
279
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
280
|
-
detail=f"Failed to create checkout session: {str(e)}",
|
|
281
|
-
) from e
|
|
282
|
-
|
|
283
|
-
return CheckoutSessionResponse(
|
|
284
|
-
checkout_url=session.url,
|
|
285
|
-
session_id=session.id,
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
@router.get("/portal", response_model=PortalSessionResponse)
|
|
290
|
-
async def create_portal_session(user: CurrentUser) -> PortalSessionResponse:
|
|
291
|
-
"""Create a Stripe billing portal session for managing subscription."""
|
|
292
|
-
settings = get_settings()
|
|
293
|
-
stripe_client = get_stripe_client()
|
|
294
|
-
|
|
295
|
-
db = get_database()
|
|
296
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
297
|
-
|
|
298
|
-
if not profile:
|
|
299
|
-
raise HTTPException(
|
|
300
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
301
|
-
detail="User profile not found",
|
|
302
|
-
)
|
|
303
|
-
|
|
304
|
-
customer_id = profile.get("stripe_customer_id")
|
|
305
|
-
|
|
306
|
-
if not customer_id:
|
|
307
|
-
raise HTTPException(
|
|
308
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
309
|
-
detail="No billing account found. Please subscribe first.",
|
|
310
|
-
)
|
|
311
|
-
|
|
312
|
-
try:
|
|
313
|
-
session = stripe_client.billing_portal.Session.create(
|
|
314
|
-
customer=customer_id,
|
|
315
|
-
return_url=f"{settings.codeshift_api_url}/billing",
|
|
316
|
-
)
|
|
317
|
-
except stripe.error.StripeError as e:
|
|
318
|
-
raise HTTPException(
|
|
319
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
320
|
-
detail=f"Failed to create portal session: {str(e)}",
|
|
321
|
-
) from e
|
|
322
|
-
|
|
323
|
-
return PortalSessionResponse(portal_url=session.url)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
@router.post("/cancel")
|
|
327
|
-
async def cancel_subscription(user: CurrentUser) -> dict:
|
|
328
|
-
"""Cancel the current subscription at end of billing period."""
|
|
329
|
-
stripe_client = get_stripe_client()
|
|
330
|
-
|
|
331
|
-
db = get_database()
|
|
332
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
333
|
-
|
|
334
|
-
if not profile:
|
|
335
|
-
raise HTTPException(
|
|
336
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
337
|
-
detail="User profile not found",
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
subscription_id = profile.get("stripe_subscription_id")
|
|
341
|
-
|
|
342
|
-
if not subscription_id:
|
|
343
|
-
raise HTTPException(
|
|
344
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
345
|
-
detail="No active subscription found",
|
|
346
|
-
)
|
|
347
|
-
|
|
348
|
-
try:
|
|
349
|
-
stripe_client.Subscription.modify(
|
|
350
|
-
subscription_id,
|
|
351
|
-
cancel_at_period_end=True,
|
|
352
|
-
)
|
|
353
|
-
except stripe.error.StripeError as e:
|
|
354
|
-
raise HTTPException(
|
|
355
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
356
|
-
detail=f"Failed to cancel subscription: {str(e)}",
|
|
357
|
-
) from e
|
|
358
|
-
|
|
359
|
-
return {"message": "Subscription will be canceled at end of billing period"}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
@router.post("/reactivate")
|
|
363
|
-
async def reactivate_subscription(user: CurrentUser) -> dict:
|
|
364
|
-
"""Reactivate a subscription that was set to cancel."""
|
|
365
|
-
stripe_client = get_stripe_client()
|
|
366
|
-
|
|
367
|
-
db = get_database()
|
|
368
|
-
profile = db.get_profile_by_id(user.user_id)
|
|
369
|
-
|
|
370
|
-
if not profile:
|
|
371
|
-
raise HTTPException(
|
|
372
|
-
status_code=status.HTTP_404_NOT_FOUND,
|
|
373
|
-
detail="User profile not found",
|
|
374
|
-
)
|
|
375
|
-
|
|
376
|
-
subscription_id = profile.get("stripe_subscription_id")
|
|
377
|
-
|
|
378
|
-
if not subscription_id:
|
|
379
|
-
raise HTTPException(
|
|
380
|
-
status_code=status.HTTP_400_BAD_REQUEST,
|
|
381
|
-
detail="No subscription found",
|
|
382
|
-
)
|
|
383
|
-
|
|
384
|
-
try:
|
|
385
|
-
stripe_client.Subscription.modify(
|
|
386
|
-
subscription_id,
|
|
387
|
-
cancel_at_period_end=False,
|
|
388
|
-
)
|
|
389
|
-
except stripe.error.StripeError as e:
|
|
390
|
-
raise HTTPException(
|
|
391
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
392
|
-
detail=f"Failed to reactivate subscription: {str(e)}",
|
|
393
|
-
) from e
|
|
394
|
-
|
|
395
|
-
return {"message": "Subscription reactivated"}
|
codeshift/api/routers/migrate.py
DELETED
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
"""Migration router for LLM-powered code migrations.
|
|
2
|
-
|
|
3
|
-
This router handles all LLM-powered migrations (Tier 2/3).
|
|
4
|
-
The Anthropic API calls are made server-side, ensuring:
|
|
5
|
-
1. Users don't need their own API keys
|
|
6
|
-
2. Usage can be tracked and billed
|
|
7
|
-
3. The LLM prompts remain server-side
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import re
|
|
11
|
-
from typing import Annotated
|
|
12
|
-
|
|
13
|
-
from anthropic import Anthropic
|
|
14
|
-
from fastapi import APIRouter, Depends, HTTPException, status
|
|
15
|
-
|
|
16
|
-
from codeshift.api.auth import AuthenticatedUser, require_tier
|
|
17
|
-
from codeshift.api.config import get_settings
|
|
18
|
-
from codeshift.api.database import get_database
|
|
19
|
-
from codeshift.api.models.migrate import (
|
|
20
|
-
ExplainChangeRequest,
|
|
21
|
-
ExplainChangeResponse,
|
|
22
|
-
MigrateCodeRequest,
|
|
23
|
-
MigrateCodeResponse,
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
router = APIRouter(prefix="/migrate", tags=["migrate"])
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def get_anthropic_client(): # type: ignore[no-untyped-def]
|
|
30
|
-
"""Get the server-side Anthropic client."""
|
|
31
|
-
|
|
32
|
-
settings = get_settings()
|
|
33
|
-
if not settings.anthropic_api_key:
|
|
34
|
-
raise HTTPException(
|
|
35
|
-
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
36
|
-
detail="LLM service not configured",
|
|
37
|
-
)
|
|
38
|
-
|
|
39
|
-
return Anthropic(api_key=settings.anthropic_api_key)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def check_llm_quota(user: AuthenticatedUser, quantity: int = 1) -> None:
|
|
43
|
-
"""Check if user has remaining LLM quota.
|
|
44
|
-
|
|
45
|
-
Raises HTTPException if quota exceeded.
|
|
46
|
-
"""
|
|
47
|
-
db = get_database()
|
|
48
|
-
quota = db.get_user_quota(user.user_id)
|
|
49
|
-
|
|
50
|
-
if not quota:
|
|
51
|
-
raise HTTPException(
|
|
52
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
53
|
-
detail="Could not retrieve quota information",
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Get tier limits
|
|
57
|
-
tier_limits = {
|
|
58
|
-
"free": 0, # Free tier cannot use LLM
|
|
59
|
-
"pro": 100,
|
|
60
|
-
"unlimited": 999999,
|
|
61
|
-
"enterprise": 999999,
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
limit = tier_limits.get(user.tier, 0)
|
|
65
|
-
current_usage = quota.get("llm_calls", 0)
|
|
66
|
-
|
|
67
|
-
if current_usage + quantity > limit:
|
|
68
|
-
raise HTTPException(
|
|
69
|
-
status_code=status.HTTP_402_PAYMENT_REQUIRED,
|
|
70
|
-
detail={
|
|
71
|
-
"error": "LLM quota exceeded",
|
|
72
|
-
"current_usage": current_usage,
|
|
73
|
-
"limit": limit,
|
|
74
|
-
"tier": user.tier,
|
|
75
|
-
"upgrade_url": "https://codeshift.dev/pricing",
|
|
76
|
-
},
|
|
77
|
-
)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def record_llm_usage(user: AuthenticatedUser, library: str, tokens_used: dict) -> None:
|
|
81
|
-
"""Record LLM usage for billing."""
|
|
82
|
-
db = get_database()
|
|
83
|
-
db.record_usage_event(
|
|
84
|
-
user_id=user.user_id,
|
|
85
|
-
event_type="llm_call",
|
|
86
|
-
library=library,
|
|
87
|
-
quantity=1,
|
|
88
|
-
metadata={
|
|
89
|
-
"input_tokens": tokens_used.get("input_tokens", 0),
|
|
90
|
-
"output_tokens": tokens_used.get("output_tokens", 0),
|
|
91
|
-
},
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def extract_code_from_response(content: str) -> str:
|
|
96
|
-
"""Extract Python code from LLM response."""
|
|
97
|
-
# Try to find code block
|
|
98
|
-
code_block_pattern = r"```(?:python)?\n(.*?)```"
|
|
99
|
-
matches = re.findall(code_block_pattern, content, re.DOTALL)
|
|
100
|
-
|
|
101
|
-
if matches:
|
|
102
|
-
# Return the longest code block (likely the full migration)
|
|
103
|
-
longest_match: str = max(matches, key=len)
|
|
104
|
-
return longest_match.strip()
|
|
105
|
-
|
|
106
|
-
# No code block found, assume the entire response is code
|
|
107
|
-
content = content.strip()
|
|
108
|
-
if content.startswith("```"):
|
|
109
|
-
content = content[3:]
|
|
110
|
-
if content.endswith("```"):
|
|
111
|
-
content = content[:-3]
|
|
112
|
-
|
|
113
|
-
return content.strip()
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def validate_syntax(code: str) -> bool:
|
|
117
|
-
"""Quick syntax validation."""
|
|
118
|
-
try:
|
|
119
|
-
compile(code, "<string>", "exec")
|
|
120
|
-
return True
|
|
121
|
-
except SyntaxError:
|
|
122
|
-
return False
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
@router.post("/code", response_model=MigrateCodeResponse)
|
|
126
|
-
async def migrate_code(
|
|
127
|
-
request: MigrateCodeRequest,
|
|
128
|
-
user: Annotated[AuthenticatedUser, Depends(require_tier("pro"))],
|
|
129
|
-
) -> MigrateCodeResponse:
|
|
130
|
-
"""Migrate code using LLM.
|
|
131
|
-
|
|
132
|
-
This endpoint requires Pro tier or higher.
|
|
133
|
-
The LLM call is made server-side using PyResolve's Anthropic API key.
|
|
134
|
-
"""
|
|
135
|
-
# Check quota
|
|
136
|
-
check_llm_quota(user)
|
|
137
|
-
|
|
138
|
-
# Get Anthropic client
|
|
139
|
-
client = get_anthropic_client()
|
|
140
|
-
|
|
141
|
-
# Build the prompt
|
|
142
|
-
system_prompt = f"""You are an expert Python developer specializing in code migrations.
|
|
143
|
-
Your task is to migrate Python code from {request.library} v{request.from_version} to v{request.to_version}.
|
|
144
|
-
|
|
145
|
-
Guidelines:
|
|
146
|
-
1. Only modify code that needs to change for the migration
|
|
147
|
-
2. Preserve all comments, formatting, and code style where possible
|
|
148
|
-
3. Add brief inline comments explaining non-obvious changes
|
|
149
|
-
4. If you're unsure about a change, add a TODO comment
|
|
150
|
-
5. Return ONLY the migrated code, no explanations before or after
|
|
151
|
-
|
|
152
|
-
Important {request.library} migration changes to consider:
|
|
153
|
-
- Config class -> model_config = ConfigDict(...)
|
|
154
|
-
- @validator -> @field_validator with @classmethod
|
|
155
|
-
- @root_validator -> @model_validator with @classmethod
|
|
156
|
-
- .dict() -> .model_dump()
|
|
157
|
-
- .json() -> .model_dump_json()
|
|
158
|
-
- .schema() -> .model_json_schema()
|
|
159
|
-
- .parse_obj() -> .model_validate()
|
|
160
|
-
- .parse_raw() -> .model_validate_json()
|
|
161
|
-
- .copy() -> .model_copy()
|
|
162
|
-
- orm_mode -> from_attributes
|
|
163
|
-
- Field(regex=...) -> Field(pattern=...)
|
|
164
|
-
"""
|
|
165
|
-
|
|
166
|
-
user_prompt = f"""Migrate the following Python code from {request.library} v{request.from_version} to v{request.to_version}.
|
|
167
|
-
|
|
168
|
-
{f"Context: {request.context}" if request.context else ""}
|
|
169
|
-
|
|
170
|
-
Code to migrate:
|
|
171
|
-
```python
|
|
172
|
-
{request.code}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Return only the migrated Python code:"""
|
|
176
|
-
|
|
177
|
-
try:
|
|
178
|
-
response = client.messages.create(
|
|
179
|
-
model="claude-sonnet-4-20250514",
|
|
180
|
-
max_tokens=4096,
|
|
181
|
-
temperature=0.0,
|
|
182
|
-
system=system_prompt,
|
|
183
|
-
messages=[{"role": "user", "content": user_prompt}],
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
# Extract content
|
|
187
|
-
content = ""
|
|
188
|
-
for block in response.content:
|
|
189
|
-
if hasattr(block, "text"):
|
|
190
|
-
content += block.text
|
|
191
|
-
|
|
192
|
-
# Extract code from response
|
|
193
|
-
migrated_code = extract_code_from_response(content)
|
|
194
|
-
|
|
195
|
-
# Validate syntax
|
|
196
|
-
if not validate_syntax(migrated_code):
|
|
197
|
-
return MigrateCodeResponse(
|
|
198
|
-
success=False,
|
|
199
|
-
migrated_code=request.code,
|
|
200
|
-
original_code=request.code,
|
|
201
|
-
error="LLM output has syntax errors",
|
|
202
|
-
usage={
|
|
203
|
-
"input_tokens": response.usage.input_tokens,
|
|
204
|
-
"output_tokens": response.usage.output_tokens,
|
|
205
|
-
},
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
# Record usage
|
|
209
|
-
record_llm_usage(
|
|
210
|
-
user,
|
|
211
|
-
request.library,
|
|
212
|
-
{
|
|
213
|
-
"input_tokens": response.usage.input_tokens,
|
|
214
|
-
"output_tokens": response.usage.output_tokens,
|
|
215
|
-
},
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
return MigrateCodeResponse(
|
|
219
|
-
success=True,
|
|
220
|
-
migrated_code=migrated_code,
|
|
221
|
-
original_code=request.code,
|
|
222
|
-
usage={
|
|
223
|
-
"input_tokens": response.usage.input_tokens,
|
|
224
|
-
"output_tokens": response.usage.output_tokens,
|
|
225
|
-
},
|
|
226
|
-
)
|
|
227
|
-
|
|
228
|
-
except Exception as e:
|
|
229
|
-
return MigrateCodeResponse(
|
|
230
|
-
success=False,
|
|
231
|
-
migrated_code=request.code,
|
|
232
|
-
original_code=request.code,
|
|
233
|
-
error=str(e),
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
@router.post("/explain", response_model=ExplainChangeResponse)
|
|
238
|
-
async def explain_change(
|
|
239
|
-
request: ExplainChangeRequest,
|
|
240
|
-
user: Annotated[AuthenticatedUser, Depends(require_tier("pro"))],
|
|
241
|
-
) -> ExplainChangeResponse:
|
|
242
|
-
"""Explain a migration change using LLM.
|
|
243
|
-
|
|
244
|
-
This endpoint requires Pro tier or higher.
|
|
245
|
-
"""
|
|
246
|
-
# Check quota
|
|
247
|
-
check_llm_quota(user)
|
|
248
|
-
|
|
249
|
-
# Get Anthropic client
|
|
250
|
-
client = get_anthropic_client()
|
|
251
|
-
|
|
252
|
-
system_prompt = """You are an expert Python developer.
|
|
253
|
-
Explain code changes clearly and concisely for other developers.
|
|
254
|
-
Focus on the 'why' not just the 'what'."""
|
|
255
|
-
|
|
256
|
-
user_prompt = f"""Explain the following {request.library} migration change:
|
|
257
|
-
|
|
258
|
-
Original:
|
|
259
|
-
```python
|
|
260
|
-
{request.original_code}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
Migrated:
|
|
264
|
-
```python
|
|
265
|
-
{request.transformed_code}
|
|
266
|
-
```
|
|
267
|
-
|
|
268
|
-
Provide a brief explanation (2-3 sentences) of what changed and why:"""
|
|
269
|
-
|
|
270
|
-
try:
|
|
271
|
-
response = client.messages.create(
|
|
272
|
-
model="claude-sonnet-4-20250514",
|
|
273
|
-
max_tokens=500,
|
|
274
|
-
temperature=0.0,
|
|
275
|
-
system=system_prompt,
|
|
276
|
-
messages=[{"role": "user", "content": user_prompt}],
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
# Extract content
|
|
280
|
-
content = ""
|
|
281
|
-
for block in response.content:
|
|
282
|
-
if hasattr(block, "text"):
|
|
283
|
-
content += block.text
|
|
284
|
-
|
|
285
|
-
# Record usage
|
|
286
|
-
record_llm_usage(
|
|
287
|
-
user,
|
|
288
|
-
request.library,
|
|
289
|
-
{
|
|
290
|
-
"input_tokens": response.usage.input_tokens,
|
|
291
|
-
"output_tokens": response.usage.output_tokens,
|
|
292
|
-
},
|
|
293
|
-
)
|
|
294
|
-
|
|
295
|
-
return ExplainChangeResponse(
|
|
296
|
-
success=True,
|
|
297
|
-
explanation=content.strip(),
|
|
298
|
-
)
|
|
299
|
-
|
|
300
|
-
except Exception as e:
|
|
301
|
-
return ExplainChangeResponse(
|
|
302
|
-
success=False,
|
|
303
|
-
error=str(e),
|
|
304
|
-
)
|