Skip to content

Commit d481f51

Browse files
committed
test(openai): cover service tier branches
1 parent 42de997 commit d481f51

3 files changed

Lines changed: 255 additions & 0 deletions

File tree

src/api/providers/__tests__/openai-native.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,29 @@ describe("OpenAiNativeHandler", () => {
122122
})
123123

124124
describe("createMessage", () => {
125+
it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority])(
126+
"should include the selected %s service tier",
127+
async (serviceTier) => {
128+
mockResponsesCreate.mockResolvedValue({
129+
async *[Symbol.asyncIterator]() {},
130+
})
131+
handler = new OpenAiNativeHandler({
132+
...mockOptions,
133+
apiModelId: "gpt-5.6-sol",
134+
openAiNativeServiceTier: serviceTier,
135+
})
136+
137+
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
138+
void chunk
139+
}
140+
141+
expect(mockResponsesCreate).toHaveBeenCalledWith(
142+
expect.objectContaining({ service_tier: serviceTier }),
143+
expect.any(Object),
144+
)
145+
},
146+
)
147+
125148
it("should handle streaming responses via Responses API", async () => {
126149
// Mock fetch for Responses API fallback
127150
const mockFetch = vitest.fn().mockResolvedValue({
@@ -221,6 +244,28 @@ describe("OpenAiNativeHandler", () => {
221244
)
222245
})
223246

247+
it.each([OpenAiServiceTier.Default, OpenAiServiceTier.Priority])(
248+
"should include the selected %s service tier",
249+
async (serviceTier) => {
250+
mockResponsesCreate.mockResolvedValue({ output: [] })
251+
handler = new OpenAiNativeHandler({
252+
...mockOptions,
253+
apiModelId: "gpt-5.6-sol",
254+
openAiNativeServiceTier: serviceTier,
255+
})
256+
257+
await handler.completePrompt("Test prompt")
258+
259+
expect(mockResponsesCreate).toHaveBeenCalledWith(
260+
expect.objectContaining({
261+
stream: false,
262+
service_tier: serviceTier,
263+
}),
264+
expect.any(Object),
265+
)
266+
},
267+
)
268+
224269
it("should handle SDK errors in completePrompt", async () => {
225270
// Mock SDK to throw an error
226271
mockResponsesCreate.mockRejectedValue(new Error("API Error"))
@@ -338,6 +383,27 @@ describe("OpenAiNativeHandler", () => {
338383
])
339384
})
340385

386+
it("should retain standard pricing for an explicitly selected default tier", () => {
387+
const defaultTierHandler = new OpenAiNativeHandler({
388+
...mockOptions,
389+
apiModelId: "gpt-5.4",
390+
openAiNativeServiceTier: OpenAiServiceTier.Default,
391+
})
392+
const model = defaultTierHandler.getModel()
393+
const normalizeUsage = Reflect.get(defaultTierHandler, "normalizeUsage")
394+
395+
const result = Reflect.apply(normalizeUsage, defaultTierHandler, [
396+
{
397+
input_tokens: 100_000,
398+
output_tokens: 1_000,
399+
cache_read_input_tokens: 20_000,
400+
},
401+
model,
402+
]) as { totalCost: number }
403+
404+
expect(result.totalCost).toBeCloseTo(0.22, 6)
405+
})
406+
341407
it("should return GPT-5.3 Chat model info when selected", () => {
342408
const chatHandler = new OpenAiNativeHandler({
343409
...mockOptions,
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { OpenAiServiceTier, providerIdentifiers, type ModelInfo } from "@roo-code/types"
2+
3+
import { render, screen, within } from "@/utils/test-utils"
4+
5+
import { ModelInfoView } from "../ModelInfoView"
6+
7+
vi.mock("@src/i18n/TranslationContext", () => ({
8+
useAppTranslation: () => ({
9+
t: (key: string) =>
10+
({
11+
"settings:serviceTier.pricingTableTitle": "Service tier pricing",
12+
"settings:serviceTier.columns.tier": "Tier",
13+
"settings:serviceTier.columns.input": "Input",
14+
"settings:serviceTier.columns.output": "Output",
15+
"settings:serviceTier.columns.cacheReads": "Cache reads",
16+
"settings:serviceTier.standard": "Standard",
17+
"settings:serviceTier.flex": "Flex",
18+
"settings:serviceTier.priority": "Priority",
19+
})[key] ?? key,
20+
}),
21+
}))
22+
23+
const baseModelInfo: ModelInfo = {
24+
contextWindow: 128_000,
25+
supportsPromptCache: true,
26+
inputPrice: 10,
27+
outputPrice: 20,
28+
cacheReadsPrice: 3,
29+
}
30+
31+
const defaultProps = {
32+
selectedModelId: "gpt-test",
33+
isDescriptionExpanded: false,
34+
setIsDescriptionExpanded: vi.fn(),
35+
}
36+
37+
const getPricingRowValues = (tier: string) => {
38+
const row = screen.getByRole("cell", { name: tier }).closest("tr")
39+
expect(row).not.toBeNull()
40+
return within(row!)
41+
.getAllByRole("cell")
42+
.map((cell) => cell.textContent)
43+
}
44+
45+
describe("ModelInfoView service tier pricing", () => {
46+
it("shows OpenAI Native tier prices with per-field fallback to Standard pricing", () => {
47+
const modelInfo: ModelInfo = {
48+
...baseModelInfo,
49+
tiers: [
50+
{ name: OpenAiServiceTier.Default, contextWindow: 128_000 },
51+
{
52+
name: OpenAiServiceTier.Flex,
53+
contextWindow: 128_000,
54+
inputPrice: 4,
55+
cacheReadsPrice: 1,
56+
},
57+
{
58+
name: OpenAiServiceTier.Priority,
59+
contextWindow: 128_000,
60+
outputPrice: 40,
61+
},
62+
],
63+
}
64+
65+
render(<ModelInfoView {...defaultProps} apiProvider={providerIdentifiers.openaiNative} modelInfo={modelInfo} />)
66+
67+
expect(screen.getByText("Service tier pricing")).toBeInTheDocument()
68+
expect(getPricingRowValues("Standard")).toEqual(["Standard", "$10.00", "$20.00", "$3.00"])
69+
expect(getPricingRowValues("Flex")).toEqual(["Flex", "$4.00", "$20.00", "$1.00"])
70+
expect(getPricingRowValues("Priority")).toEqual(["Priority", "$10.00", "$40.00", "$3.00"])
71+
})
72+
73+
it("only shows the tier pricing table for OpenAI Native models with a non-standard tier", () => {
74+
const tieredModelInfo: ModelInfo = {
75+
...baseModelInfo,
76+
tiers: [{ name: OpenAiServiceTier.Flex, contextWindow: 128_000 }],
77+
}
78+
const { rerender } = render(
79+
<ModelInfoView {...defaultProps} apiProvider="anthropic" modelInfo={tieredModelInfo} />,
80+
)
81+
82+
expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument()
83+
84+
rerender(
85+
<ModelInfoView
86+
{...defaultProps}
87+
apiProvider={providerIdentifiers.openaiNative}
88+
modelInfo={{
89+
...baseModelInfo,
90+
tiers: [{ name: OpenAiServiceTier.Default, contextWindow: 128_000 }],
91+
}}
92+
/>,
93+
)
94+
95+
expect(screen.queryByText("Service tier pricing")).not.toBeInTheDocument()
96+
})
97+
})
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import React from "react"
2+
3+
import { OpenAiServiceTier, providerIdentifiers, type ModelInfo, type ProviderSettings } from "@roo-code/types"
4+
5+
import { fireEvent, render, screen } from "@/utils/test-utils"
6+
7+
import { OpenAI } from "../OpenAI"
8+
9+
vi.mock("@src/i18n/TranslationContext", () => ({
10+
useAppTranslation: () => ({ t: (key: string) => key }),
11+
}))
12+
13+
vi.mock("vscrui", () => ({
14+
Checkbox: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
15+
}))
16+
17+
vi.mock("@src/components/ui", () => ({
18+
Select: ({ children, value, onValueChange }: any) => (
19+
<select aria-label="Service tier" value={value} onChange={(event) => onValueChange(event.target.value)}>
20+
{children}
21+
</select>
22+
),
23+
SelectContent: ({ children }: any) => <>{children}</>,
24+
SelectItem: ({ children, value }: any) => <option value={value}>{children}</option>,
25+
SelectTrigger: () => null,
26+
SelectValue: () => null,
27+
StandardTooltip: ({ children, content }: any) => <span title={content}>{children}</span>,
28+
}))
29+
30+
const baseModelInfo: ModelInfo = {
31+
contextWindow: 128_000,
32+
supportsPromptCache: true,
33+
}
34+
35+
describe("OpenAI service tier selector", () => {
36+
it("shows supported service tiers and persists the selected tier", () => {
37+
const setApiConfigurationField = vi.fn()
38+
const selectedModelInfo: ModelInfo = {
39+
...baseModelInfo,
40+
tiers: [
41+
{ name: OpenAiServiceTier.Default, contextWindow: 128_000 },
42+
{ contextWindow: 128_000 },
43+
{ name: OpenAiServiceTier.Flex, contextWindow: 128_000 },
44+
{ name: OpenAiServiceTier.Priority, contextWindow: 128_000 },
45+
],
46+
}
47+
const apiConfiguration: ProviderSettings = {
48+
apiProvider: providerIdentifiers.openaiNative,
49+
openAiNativeApiKey: "test-api-key",
50+
}
51+
52+
render(
53+
<OpenAI
54+
apiConfiguration={apiConfiguration}
55+
setApiConfigurationField={setApiConfigurationField}
56+
selectedModelInfo={selectedModelInfo}
57+
/>,
58+
)
59+
60+
const selector = screen.getByRole("combobox", { name: "Service tier" })
61+
expect(selector).toHaveValue(OpenAiServiceTier.Default)
62+
expect(screen.getAllByRole("option").map((option) => option.textContent)).toEqual([
63+
"Standard",
64+
"Flex",
65+
"Priority",
66+
])
67+
68+
fireEvent.change(selector, { target: { value: OpenAiServiceTier.Flex } })
69+
expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Flex)
70+
71+
fireEvent.change(selector, { target: { value: OpenAiServiceTier.Priority } })
72+
expect(setApiConfigurationField).toHaveBeenLastCalledWith("openAiNativeServiceTier", OpenAiServiceTier.Priority)
73+
})
74+
75+
it("hides the selector when the model only exposes the default tier", () => {
76+
render(
77+
<OpenAI
78+
apiConfiguration={{
79+
apiProvider: providerIdentifiers.openaiNative,
80+
openAiNativeApiKey: "test-api-key",
81+
}}
82+
setApiConfigurationField={vi.fn()}
83+
selectedModelInfo={{
84+
...baseModelInfo,
85+
tiers: [{ name: OpenAiServiceTier.Default, contextWindow: 128_000 }],
86+
}}
87+
/>,
88+
)
89+
90+
expect(screen.queryByTestId("openai-service-tier")).not.toBeInTheDocument()
91+
})
92+
})

0 commit comments

Comments
 (0)