Skip to content

Commit 33f30fc

Browse files
authored
test ai client (#50)
1 parent 9f87319 commit 33f30fc

File tree

5 files changed

+205
-3
lines changed

5 files changed

+205
-3
lines changed

main.wasp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,12 @@ page NotFoundPage {
109109

110110
//#region Stripe Actions
111111
action createCheckoutSession {
112-
fn: import { createCheckoutSession } from "@src/payment/stripe/operations.ts",
112+
fn: import { createCheckoutSession } from "@src/payment/stripe/operations",
113113
entities: [User]
114114
}
115115

116116
action createCustomerPortalSession {
117-
fn: import { createCustomerPortalSession } from "@src/payment/stripe/operations.ts",
117+
fn: import { createCustomerPortalSession } from "@src/payment/stripe/operations",
118118
entities: [User]
119119
}
120120
//#endregion

package-lock.json

Lines changed: 53 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@
3939
"date-fns": "^3.6.0",
4040
"embla-carousel-react": "^8.6.0",
4141
"input-otp": "^1.4.2",
42+
"limiter": "^3.0.0",
4243
"lodash": "^4.17.21",
4344
"lucide-react": "^0.511.0",
4445
"motion": "^12.12.1",
4546
"next-themes": "^0.4.6",
47+
"openai": "^4.100.0",
4648
"prettier-plugin-tailwindcss": "^0.6.11",
4749
"prismjs": "^1.30.0",
4850
"react": "^18.2.0",

src/ai/openai/service.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import OpenAI from 'openai'
2+
import { RateLimiter } from 'limiter'
3+
4+
// Default configuration
5+
const DEFAULT_MODEL = 'o3-mini'
6+
const DEFAULT_MAX_COMPLETION_TOKENS = 4096
7+
const DEFAULT_RATE_LIMIT_TOKENS_PER_INTERVAL = 3
8+
const DEFAULT_RATE_LIMIT_INTERVAL = 'second'
9+
10+
interface OpenAIServiceConfig {
11+
apiKey?: string
12+
model?: string
13+
maxTokens?: number
14+
rateLimitTokensPerInterval?: number
15+
rateLimitInterval?: 'second' | 'minute' | 'hour' | number
16+
}
17+
18+
export class OpenAIService {
19+
private static instance: OpenAIService
20+
private openai: OpenAI
21+
private limiter: RateLimiter
22+
private model: string
23+
private maxTokens: number
24+
25+
private constructor(config: OpenAIServiceConfig = {}) {
26+
const apiKey = config.apiKey || process.env.OPENAI_API_KEY
27+
if (!apiKey) {
28+
throw new Error(
29+
'OpenAI API key is not provided. Please set OPENAI_API_KEY environment variable or pass it in config.',
30+
)
31+
}
32+
this.openai = new OpenAI({ apiKey })
33+
34+
this.model = config.model || DEFAULT_MODEL
35+
this.maxTokens = config.maxTokens || DEFAULT_MAX_COMPLETION_TOKENS
36+
37+
this.limiter = new RateLimiter({
38+
tokensPerInterval:
39+
config.rateLimitTokensPerInterval ||
40+
DEFAULT_RATE_LIMIT_TOKENS_PER_INTERVAL,
41+
interval: config.rateLimitInterval || DEFAULT_RATE_LIMIT_INTERVAL,
42+
})
43+
}
44+
45+
static getInstance(config?: OpenAIServiceConfig): OpenAIService {
46+
if (!OpenAIService.instance) {
47+
OpenAIService.instance = new OpenAIService(config)
48+
}
49+
return OpenAIService.instance
50+
}
51+
52+
private cleanJsonResponse(response: string): string {
53+
try {
54+
JSON.parse(response)
55+
return response
56+
} catch {
57+
let cleanedResponse = response.trim()
58+
59+
if (cleanedResponse.startsWith('```json')) {
60+
cleanedResponse = cleanedResponse.substring(7)
61+
}
62+
if (cleanedResponse.startsWith('```')) {
63+
cleanedResponse = cleanedResponse.substring(3)
64+
}
65+
if (cleanedResponse.endsWith('```')) {
66+
cleanedResponse = cleanedResponse.substring(
67+
0,
68+
cleanedResponse.length - 3,
69+
)
70+
}
71+
cleanedResponse = cleanedResponse.trim()
72+
73+
try {
74+
JSON.parse(cleanedResponse)
75+
return cleanedResponse
76+
} catch (e) {
77+
console.error(
78+
'Failed to clean JSON response even after basic attempts:',
79+
e,
80+
)
81+
console.error(
82+
'Original problematic response snippet:',
83+
response.substring(0, 500) + '...',
84+
)
85+
return cleanedResponse
86+
}
87+
}
88+
}
89+
90+
async generateOutput(prompt: string, responseType?: string): Promise<string> {
91+
try {
92+
await this.limiter.removeTokens(1)
93+
94+
const needsJsonResponse = !!responseType
95+
96+
console.log('🤖 Making chat completion with model:', this.model)
97+
const completion = await this.openai.responses.create({
98+
model: this.model,
99+
input: prompt,
100+
max_output_tokens: this.maxTokens,
101+
})
102+
103+
const outputText = completion.output_text || ''
104+
console.log('Raw API response content:', outputText)
105+
106+
if (needsJsonResponse) {
107+
if (!outputText) {
108+
console.error('❌ Empty response content from OpenAI API')
109+
throw new Error('Empty response content from OpenAI API')
110+
}
111+
try {
112+
const potentiallyCleanedJson = this.cleanJsonResponse(outputText)
113+
const parsedJson = JSON.parse(potentiallyCleanedJson)
114+
return JSON.stringify(parsedJson)
115+
} catch (e) {
116+
console.error(
117+
'Failed to process or parse AI response as valid JSON:',
118+
e,
119+
)
120+
console.error('Output text that failed parsing:', outputText)
121+
throw new Error(
122+
'Failed to process or parse AI response as valid JSON',
123+
)
124+
}
125+
}
126+
127+
return outputText
128+
} catch (error: any) {
129+
console.error('❌ Error in chat completion:', error)
130+
if (error instanceof OpenAI.APIError) {
131+
let message = `OpenAI API error: ${error.message} (Status: ${error.status}, Type: ${error.type})`
132+
if (error.status === 429) {
133+
message =
134+
'Rate limit exceeded for OpenAI API. Please try again later.'
135+
} else if (error.status === 401) {
136+
message =
137+
'OpenAI API authentication error. Please check your API key.'
138+
} else if (error.status === 400 && error.code === 'invalid_api_key') {
139+
message =
140+
'Invalid OpenAI API key provided. Please verify your API key.'
141+
}
142+
throw new Error(message)
143+
}
144+
throw error
145+
}
146+
}
147+
}

src/landing/LandingPage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default function Landing() {
1010
</h1>
1111
</section>
1212
<section id='subscription'>
13-
<Button asChild>
13+
<Button>
1414
<Link to='/subscription'>Subscribe</Link>
1515
</Button>
1616
</section>

0 commit comments

Comments
 (0)