Skip to content

Commit 66b36b6

Browse files
committed
updates
1 parent 9e55270 commit 66b36b6

10 files changed

Lines changed: 298 additions & 56 deletions

File tree

apps/sim/providers/cost-policy.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,37 @@ describe('priceModelUsage', () => {
207207

208208
expect(openAIShaped).toEqual(anthropicShaped)
209209
})
210+
211+
it('uses the whole prompt, including cache reads, to select long-context rates', () => {
212+
const cost = priceModelUsage(
213+
'grok-4.5',
214+
{
215+
input: 120_000,
216+
cacheRead: 100_000,
217+
output: 10_000,
218+
contextInputTokens: 220_000,
219+
},
220+
LIST_PRICE_POLICY
221+
)
222+
223+
expect(cost).toMatchObject({ input: 0.54, output: 0.12, total: 0.66 })
224+
})
225+
226+
it('applies priority pricing after long-context and cache tier selection', () => {
227+
const cost = priceModelUsage(
228+
'grok-4.5',
229+
{
230+
input: 120_000,
231+
cacheRead: 100_000,
232+
output: 10_000,
233+
contextInputTokens: 220_000,
234+
serviceTier: 'priority',
235+
},
236+
LIST_PRICE_POLICY
237+
)
238+
239+
expect(cost).toMatchObject({ input: 1.08, output: 0.24, total: 1.32 })
240+
})
210241
})
211242

212243
describe('withoutToolCost', () => {

apps/sim/providers/custom-model.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ describe('custom model config', () => {
6363
parseCustomModelConfig({
6464
provider: 'fireworks',
6565
model: 'nvidia-nemotron-3-super-120b-a12b-fp8',
66+
deployment: 'accounts/acme/deployments/nemotron-super-fp8',
6667
credentials: { mode: 'explicit', apiKey: '{{FIREWORKS_API_KEY}}' },
6768
}).model
6869
).toBe('fireworks/nemotron-3-super-120b-a12b-fp8')
@@ -82,6 +83,46 @@ describe('custom model config', () => {
8283
).toThrow('credentials.mode must be "explicit"')
8384
})
8485

86+
it('requires a valid deployment resource for Fireworks on-demand models', () => {
87+
const base = {
88+
provider: 'fireworks',
89+
model: 'fireworks/qwen3.7-max',
90+
credentials: { mode: 'explicit', apiKey: '{{FIREWORKS_API_KEY}}' },
91+
}
92+
93+
expect(() => parseCustomModelConfig(base)).toThrow('deployment is required')
94+
expect(() => parseCustomModelConfig({ ...base, deployment: 'qwen-prod' })).toThrow(
95+
'must match accounts/<account-id>/deployments/<deployment-id>'
96+
)
97+
expect(
98+
parseCustomModelConfig({
99+
...base,
100+
deployment: 'accounts/acme/deployments/qwen-prod',
101+
})
102+
).toMatchObject({
103+
model: 'fireworks/qwen3.7-max',
104+
deployment: 'accounts/acme/deployments/qwen-prod',
105+
})
106+
})
107+
108+
it('accepts Fireworks priority only for catalog models with a documented priority rate', () => {
109+
expect(
110+
parseCustomModelConfig({
111+
provider: 'fireworks',
112+
model: 'fireworks/minimax-m2.7',
113+
providerOptions: { service_tier: 'priority' },
114+
}).providerOptions
115+
).toEqual({ service_tier: 'priority' })
116+
117+
expect(() =>
118+
parseCustomModelConfig({
119+
provider: 'fireworks',
120+
model: 'fireworks/nemotron-3-ultra-nvfp4',
121+
providerOptions: { service_tier: 'priority' },
122+
})
123+
).toThrow('priority processing is unavailable')
124+
})
125+
85126
it('includes every requested Fireworks model and xAI Grok 4.5 in the JSON schema', () => {
86127
expect(CUSTOM_MODEL_CONFIG_JSON_SCHEMA.properties.model.examples).toEqual(
87128
expect.arrayContaining([

apps/sim/providers/fireworks/index.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,19 @@ describe('fireworksProvider', () => {
156156
expect(result).toMatchObject({ model: 'fireworks/glm-5.2' })
157157
})
158158

159+
it('uses an on-demand deployment as the wire target while retaining the catalog id', async () => {
160+
mockCreate.mockResolvedValueOnce(textResponse('ok'))
161+
162+
const result = await fireworksProvider.executeRequest({
163+
...baseRequest,
164+
model: 'fireworks/qwen3.7-max',
165+
providerModel: 'accounts/acme/deployments/qwen-prod',
166+
})
167+
168+
expect(callBody(0).model).toBe('accounts/acme/deployments/qwen-prod')
169+
expect(result).toMatchObject({ model: 'fireworks/qwen3.7-max' })
170+
})
171+
159172
it('passes custom options and reasoning through while canonical fields win', async () => {
160173
mockCreate.mockResolvedValueOnce(textResponse('ok'))
161174

apps/sim/providers/fireworks/index.ts

Lines changed: 41 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/
66
import type { StreamingExecution } from '@/executor/types'
77
import { MAX_TOOL_ITERATIONS } from '@/providers'
88
import { formatMessagesForProvider } from '@/providers/attachments'
9+
import { addFireworksUsage, priceFireworksUsage } from '@/providers/fireworks/usage'
910
import {
1011
checkForForcedToolUsage,
1112
createReadableStreamFromOpenAIStream,
@@ -30,7 +31,6 @@ import type {
3031
} from '@/providers/types'
3132
import { ProviderError } from '@/providers/types'
3233
import {
33-
calculateCost,
3434
generateSchemaInstructions,
3535
prepareToolExecution,
3636
prepareToolsWithUsageControl,
@@ -90,7 +90,10 @@ export const fireworksProvider: ProviderConfig = {
9090
baseURL: 'https://api.fireworks.ai/inference/v1',
9191
})
9292

93-
const requestedModel = resolveFireworksWireModel(request.model.replace(/^fireworks\//, ''))
93+
const requestedModel =
94+
request.providerModel ?? resolveFireworksWireModel(request.model.replace(/^fireworks\//, ''))
95+
const serviceTier =
96+
request.providerOptions?.service_tier === 'priority' ? 'priority' : 'default'
9497

9598
logger.info('Preparing Fireworks request', {
9699
model: requestedModel,
@@ -182,25 +185,10 @@ export const fireworksProvider: ProviderConfig = {
182185
streamFormat: 'agent-events-v1',
183186
createStream: ({ output, finalizeTiming }) =>
184187
createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => {
188+
const pricedUsage = priceFireworksUsage(request.model, usage, serviceTier)
185189
output.content = content
186-
output.tokens = {
187-
input: usage.prompt_tokens,
188-
output: usage.completion_tokens,
189-
total: usage.total_tokens,
190-
}
191-
192-
// Pricing keys on the catalog id (fireworks/<name>), not the wire
193-
// name — static hosted entries price; dynamic ids stay unpriced.
194-
const costResult = calculateCost(
195-
request.model,
196-
usage.prompt_tokens,
197-
usage.completion_tokens
198-
)
199-
output.cost = {
200-
input: costResult.input,
201-
output: costResult.output,
202-
total: costResult.total,
203-
}
190+
output.tokens = pricedUsage.tokens
191+
output.cost = pricedUsage.cost
204192

205193
finalizeTiming()
206194
}),
@@ -221,11 +209,10 @@ export const fireworksProvider: ProviderConfig = {
221209
const firstResponseTime = Date.now() - initialCallTime
222210

223211
let content = currentResponse.choices[0]?.message?.content || ''
224-
const tokens = {
225-
input: currentResponse.usage?.prompt_tokens || 0,
226-
output: currentResponse.usage?.completion_tokens || 0,
227-
total: currentResponse.usage?.total_tokens || 0,
228-
}
212+
let currentTurnUsage = priceFireworksUsage(request.model, currentResponse.usage, serviceTier)
213+
const usageTotals = priceFireworksUsage(request.model, undefined, serviceTier)
214+
addFireworksUsage(usageTotals, currentTurnUsage)
215+
const { tokens, cost: modelCost } = usageTotals
229216
const toolCalls: FunctionCallResponse[] = []
230217
const toolResults: Record<string, unknown>[] = []
231218
const currentMessages = [...formattedMessages]
@@ -263,7 +250,7 @@ export const fireworksProvider: ProviderConfig = {
263250
timeSegments,
264251
currentResponse,
265252
toolCallsInResponse,
266-
{ model: request.model, provider: 'fireworks' }
253+
{ model: request.model, provider: 'fireworks', cost: currentTurnUsage.cost }
267254
)
268255

269256
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
@@ -416,6 +403,8 @@ export const fireworksProvider: ProviderConfig = {
416403
nextPayload,
417404
request.abortSignal ? { signal: request.abortSignal } : undefined
418405
)
406+
currentTurnUsage = priceFireworksUsage(request.model, currentResponse.usage, serviceTier)
407+
addFireworksUsage(usageTotals, currentTurnUsage)
419408
const nextForcedToolResult = checkForForcedToolUsage(
420409
currentResponse,
421410
nextPayload.tool_choice,
@@ -437,11 +426,6 @@ export const fireworksProvider: ProviderConfig = {
437426
if (currentResponse.choices[0]?.message?.content) {
438427
content = currentResponse.choices[0].message.content
439428
}
440-
if (currentResponse.usage) {
441-
tokens.input += currentResponse.usage.prompt_tokens || 0
442-
tokens.output += currentResponse.usage.completion_tokens || 0
443-
tokens.total += currentResponse.usage.total_tokens || 0
444-
}
445429
iterationCount++
446430
}
447431

@@ -450,6 +434,7 @@ export const fireworksProvider: ProviderConfig = {
450434
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
451435
model: request.model,
452436
provider: 'fireworks',
437+
cost: currentTurnUsage.cost,
453438
})
454439

455440
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
@@ -473,6 +458,12 @@ export const fireworksProvider: ProviderConfig = {
473458
finalPayload,
474459
request.abortSignal ? { signal: request.abortSignal } : undefined
475460
)
461+
const finalTurnUsage = priceFireworksUsage(
462+
request.model,
463+
finalResponse.usage,
464+
serviceTier
465+
)
466+
addFireworksUsage(usageTotals, finalTurnUsage)
476467
const finalEndTime = Date.now()
477468
const finalDuration = finalEndTime - finalStartTime
478469

@@ -488,17 +479,15 @@ export const fireworksProvider: ProviderConfig = {
488479
if (finalResponse.choices[0]?.message?.content) {
489480
content = finalResponse.choices[0].message.content
490481
}
491-
if (finalResponse.usage) {
492-
tokens.input += finalResponse.usage.prompt_tokens || 0
493-
tokens.output += finalResponse.usage.completion_tokens || 0
494-
tokens.total += finalResponse.usage.total_tokens || 0
495-
}
496-
497482
enrichLastModelSegmentFromChatCompletions(
498483
timeSegments,
499484
finalResponse,
500485
finalResponse.choices[0]?.message?.tool_calls,
501-
{ model: request.model, provider: 'fireworks' }
486+
{
487+
model: request.model,
488+
provider: 'fireworks',
489+
cost: finalTurnUsage.cost,
490+
}
502491
)
503492
}
504493
}
@@ -531,6 +520,8 @@ export const fireworksProvider: ProviderConfig = {
531520
finalPayload,
532521
request.abortSignal ? { signal: request.abortSignal } : undefined
533522
)
523+
const finalTurnUsage = priceFireworksUsage(request.model, finalResponse.usage, serviceTier)
524+
addFireworksUsage(usageTotals, finalTurnUsage)
534525
const finalEndTime = Date.now()
535526
const finalDuration = finalEndTime - finalStartTime
536527

@@ -546,29 +537,26 @@ export const fireworksProvider: ProviderConfig = {
546537
if (finalResponse.choices[0]?.message?.content) {
547538
content = finalResponse.choices[0].message.content
548539
}
549-
if (finalResponse.usage) {
550-
tokens.input += finalResponse.usage.prompt_tokens || 0
551-
tokens.output += finalResponse.usage.completion_tokens || 0
552-
tokens.total += finalResponse.usage.total_tokens || 0
553-
}
554-
555540
enrichLastModelSegmentFromChatCompletions(
556541
timeSegments,
557542
finalResponse,
558543
finalResponse.choices[0]?.message?.tool_calls,
559-
{ model: request.model, provider: 'fireworks' }
544+
{
545+
model: request.model,
546+
provider: 'fireworks',
547+
cost: finalTurnUsage.cost,
548+
}
560549
)
561550
}
562551

563552
if (request.stream) {
564-
// Pricing keys on the catalog id (fireworks/<name>), not the wire name.
565-
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
566553
const toolCost = sumToolCosts(toolResults)
567554
const finalCost = {
568-
input: accumulatedCost.input,
569-
output: accumulatedCost.output,
555+
input: modelCost.input,
556+
output: modelCost.output,
570557
toolCost: toolCost || undefined,
571-
total: accumulatedCost.total + toolCost,
558+
total: modelCost.total + toolCost,
559+
pricing: modelCost.pricing,
572560
}
573561

574562
const streamingResult = createStreamingExecution({
@@ -583,14 +571,14 @@ export const fireworksProvider: ProviderConfig = {
583571
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
584572
timeSegments,
585573
},
586-
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
574+
initialTokens: { ...tokens },
587575
initialCost: finalCost,
588576
toolCalls:
589577
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
590578
streamFormat: 'agent-events-v1',
591579
createStream: ({ output, finalizeTiming }) => {
592580
output.content = content
593-
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
581+
output.tokens = { ...tokens }
594582
output.cost = finalCost
595583
finalizeTiming()
596584
return createSettledAgentEventStream(content)
@@ -608,6 +596,7 @@ export const fireworksProvider: ProviderConfig = {
608596
content,
609597
model: request.model,
610598
tokens,
599+
cost: modelCost,
611600
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
612601
toolResults: toolResults.length > 0 ? toolResults : undefined,
613602
timing: {
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { describe, expect, it } from 'vitest'
2+
import {
3+
addFireworksUsage,
4+
createFireworksUsageTotals,
5+
priceFireworksUsage,
6+
} from '@/providers/fireworks/usage'
7+
8+
describe('Fireworks usage pricing', () => {
9+
const usage = {
10+
prompt_tokens: 1000,
11+
completion_tokens: 200,
12+
total_tokens: 1200,
13+
prompt_tokens_details: { cached_tokens: 400 },
14+
}
15+
16+
it('separates cached input and applies the standard catalog rates', () => {
17+
const result = priceFireworksUsage('fireworks/minimax-m2.7', usage)
18+
19+
expect(result.tokens).toEqual({ input: 600, cacheRead: 400, output: 200, total: 1200 })
20+
expect(result.cost).toMatchObject({ input: 0.0002036, output: 0.00024, total: 0.0004436 })
21+
})
22+
23+
it('uses the documented Fireworks priority rates', () => {
24+
const result = priceFireworksUsage('fireworks/minimax-m2.7', usage, 'priority')
25+
26+
expect(result.cost).toMatchObject({ input: 0.000306, output: 0.00036, total: 0.000666 })
27+
})
28+
29+
it('clamps invalid cache usage and accumulates each provider turn independently', () => {
30+
const totals = createFireworksUsageTotals('fireworks/gpt-oss-120b')
31+
const first = priceFireworksUsage('fireworks/gpt-oss-120b', {
32+
prompt_tokens: 100,
33+
completion_tokens: 20,
34+
prompt_tokens_details: { cached_tokens: 500 },
35+
})
36+
const second = priceFireworksUsage('fireworks/gpt-oss-120b', {
37+
prompt_tokens: 50,
38+
completion_tokens: 10,
39+
})
40+
41+
addFireworksUsage(totals, first)
42+
addFireworksUsage(totals, second)
43+
44+
expect(totals.tokens).toEqual({ input: 50, cacheRead: 100, output: 30, total: 180 })
45+
expect(totals.cost.total).toBeCloseTo(first.cost.total + second.cost.total, 8)
46+
})
47+
})

0 commit comments

Comments
 (0)