@@ -18,7 +18,7 @@ const jsonResponse = (status: number, body: Record<string, unknown>) =>
1818 } ) ;
1919
2020type ProviderTarget = {
21- provider : 'openai' | 'anthropic' | 'kimi' ;
21+ provider : 'openai' | 'anthropic' | 'kimi' | 'openrouter' ;
2222 upstreamBaseUrl : string ;
2323 defaultPath : string ;
2424 upstreamApiKey : string ;
@@ -42,6 +42,14 @@ const resolveProviderTarget = (request: Request): ProviderTarget | null => {
4242 upstreamApiKey : String ( process . env . KIMI_API_KEY || '' ) . trim ( ) ,
4343 } ;
4444 }
45+ if ( requestPath . startsWith ( '/ai-proxy/openrouter' ) ) {
46+ return {
47+ provider : 'openrouter' ,
48+ upstreamBaseUrl : 'https://openrouter.ai/api' ,
49+ defaultPath : '/v1/chat/completions' ,
50+ upstreamApiKey : String ( process . env . OPENROUTER_API_KEY || '' ) . trim ( ) ,
51+ } ;
52+ }
4553 if ( requestPath . startsWith ( '/ai-proxy/openai' ) || requestPath === '/ai-proxy' ) {
4654 return {
4755 provider : 'openai' ,
@@ -53,10 +61,16 @@ const resolveProviderTarget = (request: Request): ProviderTarget | null => {
5361 return null ;
5462} ;
5563
56- const resolveForwardPath = ( request : Request , provider : 'openai' | 'anthropic' | 'kimi' , defaultPath : string ) => {
64+ const providerPrefixMap : Record < string , string > = {
65+ anthropic : '/ai-proxy/anthropic' ,
66+ kimi : '/ai-proxy/kimi' ,
67+ openrouter : '/ai-proxy/openrouter' ,
68+ openai : '/ai-proxy/openai' ,
69+ } ;
70+
71+ const resolveForwardPath = ( request : Request , provider : string , defaultPath : string ) => {
5772 const requestPath = new URL ( request . url ) . pathname ;
58- const prefix =
59- provider === 'anthropic' ? '/ai-proxy/anthropic' : provider === 'kimi' ? '/ai-proxy/kimi' : '/ai-proxy/openai' ;
73+ const prefix = providerPrefixMap [ provider ] || '/ai-proxy/openai' ;
6074 const suffix = requestPath . startsWith ( prefix ) ? requestPath . slice ( prefix . length ) : '' ;
6175 return suffix || defaultPath ;
6276} ;
@@ -80,9 +94,10 @@ export const aiProxy = httpActionGeneric(async (ctx, request) => {
8094 }
8195
8296 const subscription = await ctx . runQuery ( anyApi . subscriptions . getByUserId , { userId } ) ;
83- const isPaid = Boolean ( subscription && subscription . plan === 'pro' && subscription . status === 'active' ) ;
84- if ( ! isPaid ) {
85- return jsonResponse ( 402 , { error : 'Active paid subscription required' } ) ;
97+ const creditBalance = subscription ?. creditBalanceCents ?? 0 ;
98+ const hasLegacySub = Boolean ( subscription && subscription . plan === 'pro' && subscription . status === 'active' ) ;
99+ if ( creditBalance <= 0 && ! hasLegacySub ) {
100+ return jsonResponse ( 402 , { error : 'Insufficient credits. Purchase credits to continue.' } ) ;
86101 }
87102
88103 let payload : Record < string , any > ;
@@ -92,10 +107,22 @@ export const aiProxy = httpActionGeneric(async (ctx, request) => {
92107 return jsonResponse ( 400 , { error : 'Invalid JSON payload' } ) ;
93108 }
94109
110+ // Estimate cost: ~$0.03/1K tokens → 0.003 cents/token, minimum 1 cent
111+ const maxTokens = Number ( payload ?. max_tokens || payload ?. maxTokens || 4096 ) ;
112+ const estimatedCostCents = Math . max ( 1 , Math . ceil ( maxTokens * 0.003 ) ) ;
113+
114+ // Deduct credits upfront (best-effort for streaming responses)
115+ if ( creditBalance > 0 ) {
116+ await ctx . runMutation ( anyApi . subscriptions . deductCredits , {
117+ userId,
118+ amountCents : Math . min ( estimatedCostCents , creditBalance ) ,
119+ } ) ;
120+ }
121+
95122 await ctx . runMutation ( anyApi . subscriptions . recordUsage , {
96123 userId,
97124 requestCountIncrement : 1 ,
98- tokenEstimate : Number ( payload ?. max_tokens || payload ?. maxTokens || 0 ) ,
125+ tokenEstimate : maxTokens ,
99126 } ) ;
100127
101128 const forwardPath = resolveForwardPath ( request , providerTarget . provider , providerTarget . defaultPath ) ;
@@ -106,6 +133,10 @@ export const aiProxy = httpActionGeneric(async (ctx, request) => {
106133
107134 if ( providerTarget . provider === 'openai' ) {
108135 upstreamHeaders . set ( 'authorization' , `Bearer ${ providerTarget . upstreamApiKey } ` ) ;
136+ } else if ( providerTarget . provider === 'openrouter' ) {
137+ upstreamHeaders . set ( 'authorization' , `Bearer ${ providerTarget . upstreamApiKey } ` ) ;
138+ upstreamHeaders . set ( 'http-referer' , 'https://parchi.app' ) ;
139+ upstreamHeaders . set ( 'x-title' , 'Parchi' ) ;
109140 } else if ( providerTarget . provider === 'anthropic' ) {
110141 upstreamHeaders . set ( 'x-api-key' , providerTarget . upstreamApiKey ) ;
111142 upstreamHeaders . set ( 'anthropic-version' , request . headers . get ( 'anthropic-version' ) || '2023-06-01' ) ;
0 commit comments