Skip to content

feat: add support for OpenAI gpt-5-chat-latest model #7058

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/types/src/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ export type OpenAiNativeModelId = keyof typeof openAiNativeModels
export const openAiNativeDefaultModelId: OpenAiNativeModelId = "gpt-5-2025-08-07"

export const openAiNativeModels = {
"gpt-5-chat-latest": {
maxTokens: 128000,
contextWindow: 400000,
supportsImages: true,
supportsPromptCache: true,
supportsReasoningEffort: false,
inputPrice: 1.25,
outputPrice: 10.0,
cacheReadsPrice: 0.13,
description: "GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks",
supportsVerbosity: true,
},
"gpt-5-2025-08-07": {
maxTokens: 128000,
contextWindow: 400000,
Expand Down
147 changes: 147 additions & 0 deletions src/api/providers/__tests__/openai-native-gpt5-chat.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { describe, it, expect, vi, beforeEach } from "vitest"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving these tests into the existing "GPT-5 models" describe block in openai-native.spec.ts for better organization. Having all GPT-5 model tests in one place would make it easier to maintain and ensure consistency across similar models.

import { OpenAiNativeHandler } from "../openai-native"
import { ApiHandlerOptions } from "../../../shared/api"
import { Anthropic } from "@anthropic-ai/sdk"

// Mock OpenAI
vi.mock("openai", () => {
return {
default: class MockOpenAI {
responses = {
create: vi.fn(),
}
chat = {
completions: {
create: vi.fn(),
},
}
},
}
})

describe("OpenAiNativeHandler - GPT-5 Chat Latest", () => {
let handler: OpenAiNativeHandler
let mockOptions: ApiHandlerOptions

beforeEach(() => {
vi.clearAllMocks()
mockOptions = {
apiModelId: "gpt-5-chat-latest",
openAiNativeApiKey: "test-api-key",
openAiNativeBaseUrl: "https://api.openai.com",
}
handler = new OpenAiNativeHandler(mockOptions)
})

describe("Model Configuration", () => {
it("should correctly configure gpt-5-chat-latest model", () => {
const model = handler.getModel()

expect(model.id).toBe("gpt-5-chat-latest")
expect(model.info.maxTokens).toBe(128000)
expect(model.info.contextWindow).toBe(400000)
expect(model.info.supportsImages).toBe(true)
expect(model.info.supportsPromptCache).toBe(true)
expect(model.info.supportsReasoningEffort).toBe(false) // Non-reasoning model
expect(model.info.description).toBe(
"GPT-5 Chat Latest: Optimized for conversational AI and non-reasoning tasks",
)
})

it("should not include reasoning effort for gpt-5-chat-latest", () => {
const model = handler.getModel()

// Should not have reasoning parameters since it's a non-reasoning model
expect(model.reasoning).toBeUndefined()
})
})

describe("API Endpoint Selection", () => {
it("should use Responses API for gpt-5-chat-latest", async () => {
// Mock fetch for Responses API
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Hello"}\n\n'),
)
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.done","response":{"id":"test-id","usage":{"input_tokens":10,"output_tokens":5}}}\n\n',
),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
global.fetch = mockFetch

const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello" }]

const stream = handler.createMessage("System prompt", messages)
const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Verify it called the Responses API endpoint
expect(mockFetch).toHaveBeenCalledWith(
"https://api.openai.com/v1/responses",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
Authorization: "Bearer test-api-key",
}),
body: expect.stringContaining('"model":"gpt-5-chat-latest"'),
}),
)

// Verify the request body doesn't include reasoning parameters
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body)
expect(requestBody.reasoning).toBeUndefined()
})
})

describe("Conversation Features", () => {
it("should support conversation continuity with previous_response_id", async () => {
// Mock fetch for Responses API
const mockFetch = vi.fn().mockResolvedValue({
ok: true,
body: new ReadableStream({
start(controller) {
controller.enqueue(
new TextEncoder().encode('data: {"type":"response.text.delta","delta":"Response"}\n\n'),
)
controller.enqueue(
new TextEncoder().encode(
'data: {"type":"response.done","response":{"id":"response-123","usage":{"input_tokens":10,"output_tokens":5}}}\n\n',
),
)
controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"))
controller.close()
},
}),
})
global.fetch = mockFetch

const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Follow-up question" }]

const stream = handler.createMessage("System prompt", messages, {
taskId: "test-task",
previousResponseId: "previous-response-456",
})

const chunks = []
for await (const chunk of stream) {
chunks.push(chunk)
}

// Verify the request includes previous_response_id
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body)
expect(requestBody.previous_response_id).toBe("previous-response-456")
})
})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add a test to verify that completePrompt() throws an error for gpt-5-chat-latest? Since this model uses the Responses API, it doesn't support non-streaming completion. Adding this test would prevent regressions:

Suggested change
})
describe("Unsupported Operations", () => {
it("should throw error for completePrompt since gpt-5-chat-latest uses Responses API", async () => {
await expect(handler.completePrompt("Test prompt")).rejects.toThrow(
"completePrompt is not supported for gpt-5-chat-latest. Use createMessage (Responses API) instead."
)
})
})
})

})
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we add error handling tests similar to other GPT-5 models? For example:

  • API error responses (400, 401, 429, etc.)
  • Network failures
  • Invalid response formats

This would ensure the gpt-5-chat-latest model handles errors consistently with other models.

3 changes: 2 additions & 1 deletion src/api/providers/openai-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1139,7 +1139,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio

private isResponsesApiModel(modelId: string): boolean {
// Both GPT-5 and Codex Mini use the v1/responses endpoint
return modelId.startsWith("gpt-5") || modelId === "codex-mini-latest"
// gpt-5-chat-latest also uses the Responses API

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can also use the completions API

return modelId.startsWith("gpt-5") || modelId === "codex-mini-latest" || modelId === "gpt-5-chat-latest"
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intentional? The condition modelId === "gpt-5-chat-latest" will never be evaluated because modelId.startsWith("gpt-5") already returns true for "gpt-5-chat-latest". The third condition is redundant and can be removed:

Suggested change
return modelId.startsWith("gpt-5") || modelId === "codex-mini-latest" || modelId === "gpt-5-chat-latest"
return modelId.startsWith("gpt-5") || modelId === "codex-mini-latest"

}

private async *handleStreamResponse(
Expand Down
Loading