Skip to content

Commit fe91a99

Browse files
committed
Go directly to fireworks for minimax
1 parent f84d2af commit fe91a99

File tree

5 files changed

+1010
-28
lines changed

5 files changed

+1010
-28
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ CLAUDE_CODE_KEY=dummy_claude_code_key
33
OPEN_ROUTER_API_KEY=dummy_openrouter_key
44
OPENAI_API_KEY=dummy_openai_key
55
ANTHROPIC_API_KEY=dummy_anthropic_key
6+
FIREWORKS_API_KEY=dummy_fireworks_key
67

78
# Database & Server
89
DATABASE_URL=postgresql://manicode_user_local:secretpassword_local@localhost:5432/manicode_db_local

packages/internal/src/env-schema.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const serverEnvSchema = clientEnvSchema.extend({
66
OPEN_ROUTER_API_KEY: z.string().min(1),
77
OPENAI_API_KEY: z.string().min(1),
88
ANTHROPIC_API_KEY: z.string().min(1),
9+
FIREWORKS_API_KEY: z.string().min(1),
910
LINKUP_API_KEY: z.string().min(1),
1011
CONTEXT7_API_KEY: z.string().optional(),
1112
GRAVITY_API_KEY: z.string().min(1),
@@ -48,6 +49,7 @@ export const serverProcessEnv: ServerInput = {
4849
OPEN_ROUTER_API_KEY: process.env.OPEN_ROUTER_API_KEY,
4950
OPENAI_API_KEY: process.env.OPENAI_API_KEY,
5051
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY,
52+
FIREWORKS_API_KEY: process.env.FIREWORKS_API_KEY,
5153
LINKUP_API_KEY: process.env.LINKUP_API_KEY,
5254
CONTEXT7_API_KEY: process.env.CONTEXT7_API_KEY,
5355
GRAVITY_API_KEY: process.env.GRAVITY_API_KEY,

scripts/test-fireworks.ts

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
1+
#!/usr/bin/env bun
2+
3+
/**
4+
* Test script to verify Fireworks AI integration with minimax-m2.5.
5+
*
6+
* Usage:
7+
* # Test 1: Hit Fireworks API directly
8+
* bun scripts/test-fireworks.ts direct
9+
*
10+
* # Test 2: Hit our chat completions endpoint (requires running web server + valid API key)
11+
* CODEBUFF_API_KEY=<key> bun scripts/test-fireworks.ts endpoint
12+
*
13+
* # Run both tests
14+
* CODEBUFF_API_KEY=<key> bun scripts/test-fireworks.ts both
15+
*/
16+
17+
const FIREWORKS_BASE_URL = 'https://api.fireworks.ai/inference/v1'
18+
const FIREWORKS_MODEL = 'accounts/fireworks/models/minimax-m2p5'
19+
const OPENROUTER_MODEL = 'minimax/minimax-m2.5'
20+
21+
// Same pricing constants as web/src/llm-api/fireworks.ts
22+
const FIREWORKS_INPUT_COST_PER_TOKEN = 0.30 / 1_000_000
23+
const FIREWORKS_CACHED_INPUT_COST_PER_TOKEN = 0.03 / 1_000_000
24+
const FIREWORKS_OUTPUT_COST_PER_TOKEN = 1.20 / 1_000_000
25+
26+
function computeCost(usage: Record<string, unknown>): { cost: number; breakdown: string } {
27+
const inputTokens = typeof usage.prompt_tokens === 'number' ? usage.prompt_tokens : 0
28+
const outputTokens = typeof usage.completion_tokens === 'number' ? usage.completion_tokens : 0
29+
const promptDetails = usage.prompt_tokens_details as Record<string, unknown> | undefined
30+
const cachedTokens = typeof promptDetails?.cached_tokens === 'number' ? promptDetails.cached_tokens : 0
31+
const nonCachedInput = Math.max(0, inputTokens - cachedTokens)
32+
33+
const inputCost = nonCachedInput * FIREWORKS_INPUT_COST_PER_TOKEN
34+
const cachedCost = cachedTokens * FIREWORKS_CACHED_INPUT_COST_PER_TOKEN
35+
const outputCost = outputTokens * FIREWORKS_OUTPUT_COST_PER_TOKEN
36+
const totalCost = inputCost + cachedCost + outputCost
37+
38+
const breakdown = [
39+
`${nonCachedInput} input × $0.30/M = $${inputCost.toFixed(8)}`,
40+
`${cachedTokens} cached × $0.03/M = $${cachedCost.toFixed(8)}`,
41+
`${outputTokens} output × $1.20/M = $${outputCost.toFixed(8)}`,
42+
`Total: $${totalCost.toFixed(8)}`,
43+
].join('\n ')
44+
45+
return { cost: totalCost, breakdown }
46+
}
47+
48+
const testPrompt = 'Say "hello world" and nothing else.'
49+
50+
// ─── Direct Fireworks API Test ──────────────────────────────────────────────
51+
52+
async function testFireworksDirect() {
53+
const apiKey = process.env.FIREWORKS_API_KEY
54+
if (!apiKey) {
55+
console.error('❌ FIREWORKS_API_KEY is not set. Add it to .env.local or pass it directly.')
56+
process.exit(1)
57+
}
58+
59+
console.log('── Test 1: Fireworks API (non-streaming) ──')
60+
console.log(`Model: ${FIREWORKS_MODEL}`)
61+
console.log(`Prompt: "${testPrompt}"`)
62+
console.log()
63+
64+
const startTime = Date.now()
65+
const response = await fetch(`${FIREWORKS_BASE_URL}/chat/completions`, {
66+
method: 'POST',
67+
headers: {
68+
Authorization: `Bearer ${apiKey}`,
69+
'Content-Type': 'application/json',
70+
},
71+
body: JSON.stringify({
72+
model: FIREWORKS_MODEL,
73+
messages: [{ role: 'user', content: testPrompt }],
74+
max_tokens: 64,
75+
}),
76+
})
77+
78+
if (!response.ok) {
79+
const errorText = await response.text()
80+
console.error(`❌ Fireworks API returned ${response.status}: ${errorText}`)
81+
process.exit(1)
82+
}
83+
84+
const data = await response.json()
85+
const elapsed = Date.now() - startTime
86+
const content = data.choices?.[0]?.message?.content ?? '<no content>'
87+
const usage = data.usage ?? {}
88+
89+
const { cost, breakdown } = computeCost(usage)
90+
console.log(`✅ Response (${elapsed}ms):`)
91+
console.log(` Content: ${content}`)
92+
console.log(` Model: ${data.model}`)
93+
console.log(` Usage: ${JSON.stringify(usage)}`)
94+
console.log(` Computed cost: $${cost.toFixed(8)}`)
95+
console.log(` ${breakdown}`)
96+
console.log()
97+
98+
// Streaming test
99+
console.log('── Test 1b: Fireworks API (streaming) ──')
100+
const streamStart = Date.now()
101+
const streamResponse = await fetch(`${FIREWORKS_BASE_URL}/chat/completions`, {
102+
method: 'POST',
103+
headers: {
104+
Authorization: `Bearer ${apiKey}`,
105+
'Content-Type': 'application/json',
106+
},
107+
body: JSON.stringify({
108+
model: FIREWORKS_MODEL,
109+
messages: [{ role: 'user', content: testPrompt }],
110+
max_tokens: 64,
111+
stream: true,
112+
stream_options: { include_usage: true },
113+
}),
114+
})
115+
116+
if (!streamResponse.ok) {
117+
const errorText = await streamResponse.text()
118+
console.error(`❌ Fireworks streaming API returned ${streamResponse.status}: ${errorText}`)
119+
process.exit(1)
120+
}
121+
122+
const reader = streamResponse.body?.getReader()
123+
if (!reader) {
124+
console.error('❌ No response body reader')
125+
process.exit(1)
126+
}
127+
128+
const decoder = new TextDecoder()
129+
let streamContent = ''
130+
let streamUsage: Record<string, unknown> | null = null
131+
let chunkCount = 0
132+
133+
let done = false
134+
while (!done) {
135+
const result = await reader.read()
136+
done = result.done
137+
if (done) break
138+
139+
const text = decoder.decode(result.value, { stream: true })
140+
const lines = text.split('\n').filter((l) => l.startsWith('data: '))
141+
142+
for (const line of lines) {
143+
const raw = line.slice('data: '.length)
144+
if (raw === '[DONE]') continue
145+
146+
try {
147+
const chunk = JSON.parse(raw)
148+
chunkCount++
149+
const delta = chunk.choices?.[0]?.delta
150+
if (delta?.content) streamContent += delta.content
151+
if (delta?.reasoning_content) {
152+
console.log(` [reasoning chunk] ${delta.reasoning_content.slice(0, 80)}...`)
153+
}
154+
if (chunk.usage) streamUsage = chunk.usage
155+
} catch {
156+
// skip non-JSON lines
157+
}
158+
}
159+
}
160+
161+
const streamElapsed = Date.now() - streamStart
162+
console.log(`✅ Stream response (${streamElapsed}ms, ${chunkCount} chunks):`)
163+
console.log(` Content: ${streamContent}`)
164+
if (streamUsage) {
165+
const { cost: streamCost, breakdown: streamBreakdown } = computeCost(streamUsage as Record<string, unknown>)
166+
console.log(` Usage: ${JSON.stringify(streamUsage)}`)
167+
console.log(` Computed cost: $${streamCost.toFixed(8)}`)
168+
console.log(` ${streamBreakdown}`)
169+
}
170+
console.log()
171+
}
172+
173+
// ─── Chat Completions Endpoint Test ─────────────────────────────────────────
174+
175+
async function testChatCompletionsEndpoint() {
176+
const codebuffApiKey = process.env.CODEBUFF_API_KEY
177+
if (!codebuffApiKey) {
178+
console.error('❌ CODEBUFF_API_KEY is not set. Pass it as an env var.')
179+
console.error(' Example: CODEBUFF_API_KEY=<key> bun scripts/test-fireworks.ts endpoint')
180+
process.exit(1)
181+
}
182+
183+
const appUrl = process.env.NEXT_PUBLIC_CODEBUFF_APP_URL ?? 'http://localhost:3000'
184+
const endpoint = `${appUrl}/api/v1/chat/completions`
185+
186+
console.log('── Test 2: Chat Completions Endpoint (non-streaming) ──')
187+
console.log(`Endpoint: ${endpoint}`)
188+
console.log(`Model: ${OPENROUTER_MODEL} (should route to Fireworks)`)
189+
console.log(`Prompt: "${testPrompt}"`)
190+
console.log()
191+
192+
// We need a valid run_id. This is tricky without a full setup,
193+
// so we'll just fire the request and check the error to confirm routing.
194+
// If you have a valid run_id, set it via RUN_ID env var.
195+
const runId = process.env.RUN_ID ?? 'test-run-id-fireworks'
196+
197+
const startTime = Date.now()
198+
const response = await fetch(endpoint, {
199+
method: 'POST',
200+
headers: {
201+
Authorization: `Bearer ${codebuffApiKey}`,
202+
'Content-Type': 'application/json',
203+
},
204+
body: JSON.stringify({
205+
model: OPENROUTER_MODEL,
206+
messages: [{ role: 'user', content: testPrompt }],
207+
max_tokens: 64,
208+
stream: false,
209+
codebuff_metadata: {
210+
run_id: runId,
211+
client_id: 'test-fireworks-script',
212+
cost_mode: 'free',
213+
},
214+
}),
215+
})
216+
217+
const elapsed = Date.now() - startTime
218+
const data = await response.json()
219+
220+
if (response.ok) {
221+
const content = data.choices?.[0]?.message?.content ?? '<no content>'
222+
console.log(`✅ Response (${elapsed}ms):`)
223+
console.log(` Content: ${content}`)
224+
console.log(` Model: ${data.model}`)
225+
console.log(` Provider: ${data.provider}`)
226+
console.log(` Usage: ${JSON.stringify(data.usage)}`)
227+
} else {
228+
// Even an auth/validation error confirms the endpoint is reachable
229+
console.log(`⚠️ Response ${response.status} (${elapsed}ms):`)
230+
console.log(` ${JSON.stringify(data)}`)
231+
if (response.status === 400 && data.message?.includes('runId')) {
232+
console.log(' ℹ️ This is expected if you don\'t have a valid run_id.')
233+
console.log(' ℹ️ The request reached the endpoint successfully — routing is wired up.')
234+
} else if (response.status === 401) {
235+
console.log(' ℹ️ Auth failed. Make sure CODEBUFF_API_KEY is valid.')
236+
}
237+
}
238+
console.log()
239+
240+
// Streaming test
241+
console.log('── Test 2b: Chat Completions Endpoint (streaming) ──')
242+
const streamStart = Date.now()
243+
const streamResponse = await fetch(endpoint, {
244+
method: 'POST',
245+
headers: {
246+
Authorization: `Bearer ${codebuffApiKey}`,
247+
'Content-Type': 'application/json',
248+
},
249+
body: JSON.stringify({
250+
model: OPENROUTER_MODEL,
251+
messages: [{ role: 'user', content: testPrompt }],
252+
max_tokens: 64,
253+
stream: true,
254+
codebuff_metadata: {
255+
run_id: runId,
256+
client_id: 'test-fireworks-script',
257+
cost_mode: 'free',
258+
},
259+
}),
260+
})
261+
262+
const streamElapsed = Date.now() - streamStart
263+
264+
if (streamResponse.ok) {
265+
const reader = streamResponse.body?.getReader()
266+
if (!reader) {
267+
console.error('❌ No response body reader')
268+
process.exit(1)
269+
}
270+
271+
const decoder = new TextDecoder()
272+
let streamContent = ''
273+
let chunkCount = 0
274+
275+
let done = false
276+
while (!done) {
277+
const result = await reader.read()
278+
done = result.done
279+
if (done) break
280+
281+
const text = decoder.decode(result.value, { stream: true })
282+
const lines = text.split('\n').filter((l) => l.startsWith('data: '))
283+
284+
for (const line of lines) {
285+
const raw = line.slice('data: '.length)
286+
if (raw === '[DONE]') continue
287+
288+
try {
289+
const chunk = JSON.parse(raw)
290+
chunkCount++
291+
const delta = chunk.choices?.[0]?.delta
292+
if (delta?.content) streamContent += delta.content
293+
} catch {
294+
// skip non-JSON lines
295+
}
296+
}
297+
}
298+
299+
console.log(`✅ Stream response (${streamElapsed}ms, ${chunkCount} chunks):`)
300+
console.log(` Content: ${streamContent}`)
301+
} else {
302+
const data = await streamResponse.json()
303+
console.log(`⚠️ Response ${streamResponse.status} (${streamElapsed}ms):`)
304+
console.log(` ${JSON.stringify(data)}`)
305+
if (streamResponse.status === 400 && data.message?.includes('runId')) {
306+
console.log(' ℹ️ Expected without a valid run_id. Endpoint is reachable and routing works.')
307+
}
308+
}
309+
console.log()
310+
}
311+
312+
// ─── Main ───────────────────────────────────────────────────────────────────
313+
314+
async function main() {
315+
const mode = process.argv[2] ?? 'direct'
316+
317+
console.log('🔥 Fireworks Integration Test')
318+
console.log('='.repeat(50))
319+
console.log()
320+
321+
switch (mode) {
322+
case 'direct':
323+
await testFireworksDirect()
324+
break
325+
case 'endpoint':
326+
await testChatCompletionsEndpoint()
327+
break
328+
case 'both':
329+
await testFireworksDirect()
330+
await testChatCompletionsEndpoint()
331+
break
332+
default:
333+
console.error(`Unknown mode: ${mode}`)
334+
console.error('Usage: bun scripts/test-fireworks.ts [direct|endpoint|both]')
335+
process.exit(1)
336+
}
337+
338+
console.log('Done!')
339+
}
340+
341+
main()

0 commit comments

Comments
 (0)