Skip to content

Commit 194bb9a

Browse files
committed
updates
1 parent 3e880c1 commit 194bb9a

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
@@ -161,6 +161,19 @@ describe('fireworksProvider', () => {
161161
expect(result).toMatchObject({ model: 'fireworks/glm-5.2' })
162162
})
163163

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

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
isFunctionToolCall,
3636
prepareToolExecution,
@@ -91,7 +91,10 @@ export const fireworksProvider: ProviderConfig = {
9191
baseURL: 'https://api.fireworks.ai/inference/v1',
9292
})
9393

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

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

206194
finalizeTiming()
207195
}),
@@ -222,11 +210,10 @@ export const fireworksProvider: ProviderConfig = {
222210
const firstResponseTime = Date.now() - initialCallTime
223211

224212
let content = currentResponse.choices[0]?.message?.content || ''
225-
const tokens = {
226-
input: currentResponse.usage?.prompt_tokens || 0,
227-
output: currentResponse.usage?.completion_tokens || 0,
228-
total: currentResponse.usage?.total_tokens || 0,
229-
}
213+
let currentTurnUsage = priceFireworksUsage(request.model, currentResponse.usage, serviceTier)
214+
const usageTotals = priceFireworksUsage(request.model, undefined, serviceTier)
215+
addFireworksUsage(usageTotals, currentTurnUsage)
216+
const { tokens, cost: modelCost } = usageTotals
230217
const toolCalls: FunctionCallResponse[] = []
231218
const toolResults: Record<string, unknown>[] = []
232219
const currentMessages = [...formattedMessages]
@@ -265,7 +252,7 @@ export const fireworksProvider: ProviderConfig = {
265252
timeSegments,
266253
currentResponse,
267254
toolCallsInResponse,
268-
{ model: request.model, provider: 'fireworks' }
255+
{ model: request.model, provider: 'fireworks', cost: currentTurnUsage.cost }
269256
)
270257

271258
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
@@ -418,6 +405,8 @@ export const fireworksProvider: ProviderConfig = {
418405
nextPayload,
419406
request.abortSignal ? { signal: request.abortSignal } : undefined
420407
)
408+
currentTurnUsage = priceFireworksUsage(request.model, currentResponse.usage, serviceTier)
409+
addFireworksUsage(usageTotals, currentTurnUsage)
421410
const nextForcedToolResult = checkForForcedToolUsage(
422411
currentResponse,
423412
nextPayload.tool_choice,
@@ -439,11 +428,6 @@ export const fireworksProvider: ProviderConfig = {
439428
if (currentResponse.choices[0]?.message?.content) {
440429
content = currentResponse.choices[0].message.content
441430
}
442-
if (currentResponse.usage) {
443-
tokens.input += currentResponse.usage.prompt_tokens || 0
444-
tokens.output += currentResponse.usage.completion_tokens || 0
445-
tokens.total += currentResponse.usage.total_tokens || 0
446-
}
447431
iterationCount++
448432
}
449433

@@ -453,6 +437,7 @@ export const fireworksProvider: ProviderConfig = {
453437
enrichLastModelSegmentFromChatCompletions(timeSegments, currentResponse, pendingToolCalls, {
454438
model: request.model,
455439
provider: 'fireworks',
440+
cost: currentTurnUsage.cost,
456441
})
457442

458443
if (pendingToolCalls?.length && !(request.responseFormat && hasActiveTools)) {
@@ -476,6 +461,12 @@ export const fireworksProvider: ProviderConfig = {
476461
finalPayload,
477462
request.abortSignal ? { signal: request.abortSignal } : undefined
478463
)
464+
const finalTurnUsage = priceFireworksUsage(
465+
request.model,
466+
finalResponse.usage,
467+
serviceTier
468+
)
469+
addFireworksUsage(usageTotals, finalTurnUsage)
479470
const finalEndTime = Date.now()
480471
const finalDuration = finalEndTime - finalStartTime
481472

@@ -491,17 +482,15 @@ export const fireworksProvider: ProviderConfig = {
491482
if (finalResponse.choices[0]?.message?.content) {
492483
content = finalResponse.choices[0].message.content
493484
}
494-
if (finalResponse.usage) {
495-
tokens.input += finalResponse.usage.prompt_tokens || 0
496-
tokens.output += finalResponse.usage.completion_tokens || 0
497-
tokens.total += finalResponse.usage.total_tokens || 0
498-
}
499-
500485
enrichLastModelSegmentFromChatCompletions(
501486
timeSegments,
502487
finalResponse,
503488
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
504-
{ model: request.model, provider: 'fireworks' }
489+
{
490+
model: request.model,
491+
provider: 'fireworks',
492+
cost: finalTurnUsage.cost,
493+
}
505494
)
506495
}
507496
}
@@ -534,6 +523,8 @@ export const fireworksProvider: ProviderConfig = {
534523
finalPayload,
535524
request.abortSignal ? { signal: request.abortSignal } : undefined
536525
)
526+
const finalTurnUsage = priceFireworksUsage(request.model, finalResponse.usage, serviceTier)
527+
addFireworksUsage(usageTotals, finalTurnUsage)
537528
const finalEndTime = Date.now()
538529
const finalDuration = finalEndTime - finalStartTime
539530

@@ -549,29 +540,26 @@ export const fireworksProvider: ProviderConfig = {
549540
if (finalResponse.choices[0]?.message?.content) {
550541
content = finalResponse.choices[0].message.content
551542
}
552-
if (finalResponse.usage) {
553-
tokens.input += finalResponse.usage.prompt_tokens || 0
554-
tokens.output += finalResponse.usage.completion_tokens || 0
555-
tokens.total += finalResponse.usage.total_tokens || 0
556-
}
557-
558543
enrichLastModelSegmentFromChatCompletions(
559544
timeSegments,
560545
finalResponse,
561546
finalResponse.choices[0]?.message?.tool_calls?.filter(isFunctionToolCall),
562-
{ model: request.model, provider: 'fireworks' }
547+
{
548+
model: request.model,
549+
provider: 'fireworks',
550+
cost: finalTurnUsage.cost,
551+
}
563552
)
564553
}
565554

566555
if (request.stream) {
567-
// Pricing keys on the catalog id (fireworks/<name>), not the wire name.
568-
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
569556
const toolCost = sumToolCosts(toolResults)
570557
const finalCost = {
571-
input: accumulatedCost.input,
572-
output: accumulatedCost.output,
558+
input: modelCost.input,
559+
output: modelCost.output,
573560
toolCost: toolCost || undefined,
574-
total: accumulatedCost.total + toolCost,
561+
total: modelCost.total + toolCost,
562+
pricing: modelCost.pricing,
575563
}
576564

577565
const streamingResult = createStreamingExecution({
@@ -586,14 +574,14 @@ export const fireworksProvider: ProviderConfig = {
586574
iterations: timeSegments.filter((segment) => segment.type === 'model').length,
587575
timeSegments,
588576
},
589-
initialTokens: { input: tokens.input, output: tokens.output, total: tokens.total },
577+
initialTokens: { ...tokens },
590578
initialCost: finalCost,
591579
toolCalls:
592580
toolCalls.length > 0 ? { list: toolCalls, count: toolCalls.length } : undefined,
593581
streamFormat: 'agent-events-v1',
594582
createStream: ({ output, finalizeTiming }) => {
595583
output.content = content
596-
output.tokens = { input: tokens.input, output: tokens.output, total: tokens.total }
584+
output.tokens = { ...tokens }
597585
output.cost = finalCost
598586
finalizeTiming()
599587
return createSettledAgentEventStream(content)
@@ -611,6 +599,7 @@ export const fireworksProvider: ProviderConfig = {
611599
content,
612600
model: request.model,
613601
tokens,
602+
cost: modelCost,
614603
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
615604
toolResults: toolResults.length > 0 ? toolResults : undefined,
616605
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)