Skip to content

Commit d2f223d

Browse files
authored
feat(models): add GPT-6 Astra and refresh OpenAI model pricing (#7482)
* feat(models): add GPT-6 Astra and tiered pricing * fix(models): preserve provider pricing test seam * fix(models): address catalog review findings
1 parent b341a9d commit d2f223d

17 files changed

Lines changed: 471 additions & 161 deletions

File tree

apps/docs/content/docs/workflows/blocks/agent.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Live tool-call chips stream for **OpenAI, Anthropic, Azure Anthropic, Google, Ve
108108

109109
| Provider | Streamed thinking | Models |
110110
|----------|-------------------|--------|
111-
| OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5-pro`, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5-pro`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `o4-mini`, `o3`, `o3-mini`, `o1` |
111+
| OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `gpt-6-astra`, `gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`, `gpt-5.5-pro`, `gpt-5.5`, `gpt-5.4-pro`, `gpt-5.4`, `gpt-5.4-mini`, `gpt-5.4-nano`, `gpt-5.2-pro`, `gpt-5.2`, `gpt-5.1`, `gpt-5-pro`, `gpt-5`, `gpt-5-mini`, `gpt-5-nano`, `o4-mini`, `o3`, `o3-mini`, `o1` |
112112
| Anthropic | Summaries only — These generations omit full thinking; Sim requests summarized thinking on streaming runs. | `claude-fable-5-1`, `claude-fable-5`, `claude-sonnet-5`, `claude-opus-5`, `claude-opus-4-8`, `claude-opus-4-7`, `claude-opus-4-6`, `claude-sonnet-4-6`, `claude-opus-4-5`, `claude-opus-4-1`, `claude-sonnet-4-5`, `claude-haiku-4-5` |
113113
| Azure OpenAI | Summaries only — Requires OpenAI organization verification; falls back to no summaries. | `azure/gpt-5.4`, `azure/gpt-5.4-mini`, `azure/gpt-5.4-nano`, `azure/gpt-5.2`, `azure/gpt-5.1`, `azure/gpt-5.1-codex`, `azure/gpt-5`, `azure/gpt-5-mini`, `azure/gpt-5-nano`, `azure/o3`, `azure/o4-mini` |
114114
| Azure Anthropic | Summaries only — These generations omit full thinking; Sim requests summarized thinking on streaming runs. | `azure-anthropic/claude-opus-4-6`, `azure-anthropic/claude-opus-4-5`, `azure-anthropic/claude-sonnet-4-5`, `azure-anthropic/claude-opus-4-1`, `azure-anthropic/claude-haiku-4-5` |

apps/sim/app/(landing)/models/(shell)/[provider]/[model]/page.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Fragment } from 'react'
12
import { ChipLink } from '@sim/emcn'
23
import type { Metadata } from 'next'
34
import { notFound } from 'next/navigation'
@@ -200,6 +201,28 @@ export default async function ModelPage({
200201
}
201202
/>
202203
<InfoRow label='Output price' value={`${formatPrice(model.pricing.output)}/1M`} />
204+
{model.pricing.tiers?.map((tier) => {
205+
const threshold = formatTokenCount(tier.aboveInputTokens)
206+
207+
return (
208+
<Fragment key={tier.aboveInputTokens}>
209+
<InfoRow
210+
label={`Input price (> ${threshold})`}
211+
value={`${formatPrice(tier.input)}/1M`}
212+
/>
213+
<InfoRow
214+
label={`Cached input (> ${threshold})`}
215+
value={
216+
tier.cachedInput !== undefined ? `${formatPrice(tier.cachedInput)}/1M` : 'N/A'
217+
}
218+
/>
219+
<InfoRow
220+
label={`Output price (> ${threshold})`}
221+
value={`${formatPrice(tier.output)}/1M`}
222+
/>
223+
</Fragment>
224+
)
225+
})}
203226
<InfoRow
204227
label='Context window'
205228
value={model.contextWindow ? formatTokenCount(model.contextWindow) : 'Unknown'}

apps/sim/app/(landing)/models/components/model-comparison-charts.tsx

Lines changed: 60 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ const FEATURED_COMPARISON_PROVIDER_IDS = ['anthropic', 'openai', 'google']
1313

1414
/** Max latest models pulled from each featured provider. */
1515
const MAX_MODELS_PER_PROVIDER = 4
16+
const CHART_BAR_HEIGHT = 1
17+
const CHART_BAR_END_RADIUS_X = 1
18+
const CHART_BAR_END_RADIUS_Y = 3 / 28
1619

1720
const PROVIDER_ICON_MAP: Record<string, ComponentType<{ className?: string }>> = (() => {
1821
const map: Record<string, ComponentType<{ className?: string }>> = {}
@@ -24,6 +27,14 @@ const PROVIDER_ICON_MAP: Record<string, ComponentType<{ className?: string }>> =
2427
return map
2528
})()
2629

30+
function getRoundedRightBarPath(x: number, width: number): string {
31+
const right = x + width
32+
const radiusX = Math.min(CHART_BAR_END_RADIUS_X, width / 2)
33+
const radiusY = CHART_BAR_END_RADIUS_Y
34+
35+
return `M ${x} 0 H ${right - radiusX} Q ${right} 0 ${right} ${radiusY} V ${CHART_BAR_HEIGHT - radiusY} Q ${right} ${CHART_BAR_HEIGHT} ${right - radiusX} ${CHART_BAR_HEIGHT} H ${x} Z`
36+
}
37+
2738
function selectComparisonModels(models: CatalogModel[]): CatalogModel[] {
2839
const seen = new Set<string>()
2940
const result: CatalogModel[] = []
@@ -96,14 +107,17 @@ function StackedCostChart({ models }: ChartProps) {
96107
Cost
97108
</h3>
98109
<span className='text-[var(--text-muted)] text-sm leading-[150%] tracking-[0.02em]'>
99-
Per 1M tokens
110+
Standard short-context rates per 1M tokens
100111
</span>
101112
</div>
102113

103114
<div className='flex flex-col gap-1.5'>
104115
{data.entries.map(({ model, input, output, total }) => {
105116
const totalPct = data.maxTotal > 0 ? (total / data.maxTotal) * 100 : 0
106117
const inputPct = total > 0 ? (input / total) * 100 : 0
118+
const plottedTotalPct = Math.max(totalPct, 3)
119+
const plottedInputPct = (plottedTotalPct * inputPct) / 100
120+
const plottedOutputPct = plottedTotalPct - plottedInputPct
107121
const color = getProviderColor(model.providerId)
108122

109123
return (
@@ -113,29 +127,34 @@ function StackedCostChart({ models }: ChartProps) {
113127
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--surface-hover)]'
114128
>
115129
<ModelLabel model={model} />
116-
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
117-
<div
118-
className='hidden h-full overflow-hidden rounded-r-[3px] sm:flex'
119-
style={{ width: `${Math.max(totalPct, 3)}%` }}
120-
>
121-
<div
122-
className='h-full'
123-
style={{
124-
width: `${inputPct}%`,
125-
backgroundColor: color,
126-
opacity: 0.8,
127-
}}
128-
/>
129-
<div
130-
className='h-full'
131-
style={{
132-
width: `${100 - inputPct}%`,
133-
backgroundColor: color,
134-
opacity: 0.35,
135-
}}
136-
/>
130+
<div className='flex h-7 min-w-0 flex-1 items-center gap-2.5'>
131+
<div className='hidden h-full min-w-0 flex-1 sm:block'>
132+
<svg
133+
aria-hidden='true'
134+
className='size-full'
135+
preserveAspectRatio='none'
136+
viewBox='0 0 100 1'
137+
>
138+
{plottedInputPct > 0 &&
139+
(plottedOutputPct > 0 ? (
140+
<rect fill={color} fillOpacity={0.8} height='1' width={plottedInputPct} />
141+
) : (
142+
<path
143+
d={getRoundedRightBarPath(0, plottedInputPct)}
144+
fill={color}
145+
fillOpacity={0.8}
146+
/>
147+
))}
148+
{plottedOutputPct > 0 && (
149+
<path
150+
d={getRoundedRightBarPath(plottedInputPct, plottedOutputPct)}
151+
fill={color}
152+
fillOpacity={0.35}
153+
/>
154+
)}
155+
</svg>
137156
</div>
138-
<span className='shrink-0 text-[11px] text-[var(--text-muted)] sm:ml-2.5 sm:text-xs'>
157+
<span className='shrink-0 text-[11px] text-[var(--text-muted)] sm:w-[148px] sm:text-xs'>
139158
{formatPrice(input)} input / {formatPrice(output)} output
140159
</span>
141160
</div>
@@ -184,16 +203,22 @@ function ContextWindowChart({ models }: ChartProps) {
184203
className='-mx-2 flex items-center gap-3 rounded-md px-2 transition-colors hover:bg-[var(--surface-hover)]'
185204
>
186205
<ModelLabel model={model} />
187-
<div className='relative flex h-7 min-w-0 flex-1 items-center'>
188-
<div
189-
className='h-full rounded-r-[3px]'
190-
style={{
191-
width: `${Math.max(pct, 3)}%`,
192-
backgroundColor: color,
193-
opacity: 0.8,
194-
}}
195-
/>
196-
<span className='ml-2.5 shrink-0 text-[11px] text-[var(--text-muted)] sm:text-xs'>
206+
<div className='flex h-7 min-w-0 flex-1 items-center gap-2.5'>
207+
<div className='h-full min-w-0 flex-1'>
208+
<svg
209+
aria-hidden='true'
210+
className='size-full'
211+
preserveAspectRatio='none'
212+
viewBox='0 0 100 1'
213+
>
214+
<path
215+
d={getRoundedRightBarPath(0, Math.max(pct, 3))}
216+
fill={color}
217+
fillOpacity={0.8}
218+
/>
219+
</svg>
220+
</div>
221+
<span className='w-10 shrink-0 text-right text-[11px] text-[var(--text-muted)] tabular-nums sm:text-xs'>
197222
{formatTokenCount(value)}
198223
</span>
199224
</div>
@@ -228,11 +253,11 @@ export function ModelComparisonCharts({ models }: ModelComparisonChartsProps) {
228253

229254
<div className='h-px w-full bg-[var(--border)]' />
230255

231-
<div className='flex flex-col sm:flex-row'>
256+
<div className='flex flex-col lg:flex-row'>
232257
<div className='flex-1 p-6'>
233258
<StackedCostChart models={comparisonModels} />
234259
</div>
235-
<div className='h-px w-full bg-[var(--border)] sm:h-auto sm:w-px' />
260+
<div className='h-px w-full bg-[var(--border)] lg:h-auto lg:w-px' />
236261
<div className='flex-1 p-6'>
237262
<ContextWindowChart models={comparisonModels} />
238263
</div>

apps/sim/app/(landing)/models/components/model-directory.tsx

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -201,14 +201,9 @@ export function ModelDirectory() {
201201
className='size-8 rounded-xl'
202202
iconClassName='size-4'
203203
/>
204-
<div className='min-w-0 flex-1'>
205-
<h4 className='text-[14px] text-[var(--text-primary)] leading-snug'>
206-
{provider.name}
207-
</h4>
208-
<p className='line-clamp-1 text-[12px] text-[var(--text-muted)] leading-[150%]'>
209-
{provider.description}
210-
</p>
211-
</div>
204+
<h4 className='min-w-0 flex-1 text-[14px] text-[var(--text-primary)] leading-snug'>
205+
{provider.name}
206+
</h4>
212207
</div>
213208
))}
214209
</nav>

apps/sim/app/(landing)/models/utils.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { describe, expect, it } from 'vitest'
2-
import { buildModelCapabilityFacts, getEffectiveMaxOutputTokens, getModelBySlug } from './utils'
2+
import {
3+
buildModelCapabilityFacts,
4+
getEffectiveMaxOutputTokens,
5+
getModelBySlug,
6+
getPricingBounds,
7+
getProviderBySlug,
8+
} from '@/app/(landing)/models/utils'
39

410
describe('model catalog capability facts', () => {
511
it.concurrent(
@@ -46,4 +52,15 @@ describe('model catalog capability facts', () => {
4652
expect(researchModel?.bestFor).toContain('research workflows')
4753
expect(generalModel?.bestFor).toBeUndefined()
4854
})
55+
56+
it.concurrent('features the explicitly recommended OpenAI model first', () => {
57+
expect(getProviderBySlug('openai')?.featuredModels[0]?.id).toBe('gpt-6-astra')
58+
})
59+
60+
it.concurrent('includes input-size tiers in structured pricing bounds', () => {
61+
const model = getModelBySlug('openai', 'gpt-6-astra')
62+
63+
expect(model).not.toBeNull()
64+
expect(getPricingBounds(model!.pricing)).toEqual({ lowPrice: 1, highPrice: 75 })
65+
})
4966
})

apps/sim/app/(landing)/models/utils.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ComponentType } from 'react'
22
import { slugify } from '@sim/utils/string'
33
import { type ModelCapabilities, PROVIDER_DEFINITIONS } from '@/providers/models'
4+
import type { ModelPricing } from '@/providers/types'
45

56
const PROVIDER_PREFIXES: Record<string, string[]> = {
67
'azure-openai': ['azure/'],
@@ -95,12 +96,7 @@ const UPDATED_AT_DATE_FORMAT = new Intl.DateTimeFormat('en-US', {
9596
year: 'numeric',
9697
})
9798

98-
export interface PricingInfo {
99-
input: number
100-
cachedInput?: number
101-
output: number
102-
updatedAt: string
103-
}
99+
export type PricingInfo = ModelPricing
104100

105101
export interface CatalogFaq {
106102
question: string
@@ -124,6 +120,7 @@ export interface CatalogModel {
124120
contextWindow: number | null
125121
releaseDate: string | null
126122
deprecated: boolean
123+
recommended: boolean
127124
pricing: PricingInfo
128125
capabilities: ModelCapabilities
129126
capabilityTags: string[]
@@ -444,6 +441,9 @@ function computeModelRelevanceScore(model: CatalogModel): number {
444441
}
445442

446443
function compareModelsByRelevance(a: CatalogModel, b: CatalogModel): number {
444+
const recommendationDifference = Number(b.recommended) - Number(a.recommended)
445+
if (recommendationDifference !== 0) return recommendationDifference
446+
447447
return computeModelRelevanceScore(b) - computeModelRelevanceScore(a)
448448
}
449449

@@ -477,6 +477,7 @@ const rawProviders = Object.values(PROVIDER_DEFINITIONS).map((provider) => {
477477
contextWindow: model.contextWindow ?? null,
478478
releaseDate: model.releaseDate ?? null,
479479
deprecated: !!model.sunset,
480+
recommended: model.recommended ?? false,
480481
pricing: model.pricing,
481482
capabilities: mergedCapabilities,
482483
capabilityTags,
@@ -587,13 +588,15 @@ export const TOP_MODEL_PROVIDERS = MODEL_PROVIDERS_WITH_CATALOGS.slice(0, 8).map
587588
)
588589

589590
export function getPricingBounds(pricing: PricingInfo): { lowPrice: number; highPrice: number } {
591+
const prices = [pricing, ...(pricing.tiers ?? [])].flatMap((tokenPricing) => [
592+
tokenPricing.input,
593+
tokenPricing.output,
594+
...(tokenPricing.cachedInput !== undefined ? [tokenPricing.cachedInput] : []),
595+
])
596+
590597
return {
591-
lowPrice: Math.min(
592-
pricing.input,
593-
pricing.output,
594-
...(pricing.cachedInput !== undefined ? [pricing.cachedInput] : [])
595-
),
596-
highPrice: Math.max(pricing.input, pricing.output),
598+
lowPrice: Math.min(...prices),
599+
highPrice: Math.max(...prices),
597600
}
598601
}
599602

@@ -708,14 +711,20 @@ export function buildProviderFaqs(provider: CatalogProvider): CatalogFaq[] {
708711
}
709712

710713
export function buildModelFaqs(provider: CatalogProvider, model: CatalogModel): CatalogFaq[] {
714+
const pricingTiers = model.pricing.tiers ?? []
711715
const faqs: CatalogFaq[] = [
712716
{
713717
question: `What is ${model.displayName}?`,
714718
answer: `${model.displayName} is a ${provider.name} model available in Sim. ${model.summary}`,
715719
},
716720
{
717721
question: `How much does ${model.displayName} cost?`,
718-
answer: `${model.displayName} is listed at ${formatPrice(model.pricing.input)}/1M input tokens${model.pricing.cachedInput !== undefined ? `, ${formatPrice(model.pricing.cachedInput)}/1M cached input tokens` : ''}, and ${formatPrice(model.pricing.output)}/1M output tokens.`,
722+
answer: `${model.displayName} starts at ${formatPrice(model.pricing.input)}/1M input tokens${model.pricing.cachedInput !== undefined ? `, ${formatPrice(model.pricing.cachedInput)}/1M cached input tokens` : ''}, and ${formatPrice(model.pricing.output)}/1M output tokens.${pricingTiers
723+
.map(
724+
(tier) =>
725+
` Above ${formatTokenCount(tier.aboveInputTokens)} input tokens, the full request is priced at ${formatPrice(tier.input)}/1M input tokens${tier.cachedInput !== undefined ? `, ${formatPrice(tier.cachedInput)}/1M cached input tokens` : ''}, and ${formatPrice(tier.output)}/1M output tokens.`
726+
)
727+
.join('')}`,
719728
},
720729
{
721730
question: `What is the context window for ${model.displayName}?`,

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

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,31 @@ describe('priceModelUsage', () => {
123123
expect(tripled.total).toBeCloseTo(single.total * 3, 8)
124124
})
125125

126+
it('applies the highest matching input-size tier to every token bucket', () => {
127+
const cost = priceModelUsage(
128+
'gpt-5.6-terra',
129+
{
130+
input: 100_000,
131+
output: 100_000,
132+
cacheRead: 100_000,
133+
cacheWrites: [{ tokens: 72_001, inputRateMultiplier: 1.25 }],
134+
},
135+
LIST_PRICE_POLICY
136+
)
137+
138+
expect(cost).toMatchObject({ input: 0.800005, output: 1.8, total: 2.600005 })
139+
})
140+
141+
it('preserves zero-cost behavior for unregistered dynamic models', () => {
142+
const cost = priceModelUsage(
143+
'dynamic-provider/model',
144+
{ input: 300_000, output: 100_000 },
145+
LIST_PRICE_POLICY
146+
)
147+
148+
expect(cost).toMatchObject({ input: 0, output: 0, total: 0 })
149+
})
150+
126151
it('charges nothing when the policy is not billable', () => {
127152
const cost = priceModelUsage(
128153
PRICED_MODEL,

0 commit comments

Comments
 (0)