Skip to content

Commit 26ed0f5

Browse files
authored
Merge pull request #1 from Lagyu/feature/integrate-openai-recent-update
Feature/integrate OpenAI recent update
2 parents 1144bf9 + 0126f3a commit 26ed0f5

File tree

1 file changed

+90
-65
lines changed

1 file changed

+90
-65
lines changed

src/api/providers/openai.ts

Lines changed: 90 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -376,47 +376,55 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
376376

377377
// Use Responses API when selected (non-streaming convenience method)
378378
if (flavor === "responses") {
379-
// Build a single-turn formatted string input (Developer/User style) for Responses API
380-
const formattedInput = this._formatResponsesSingleMessage(
381-
{
382-
role: "user",
383-
content: [{ type: "text", text: prompt }],
384-
} as Anthropic.Messages.MessageParam,
385-
/*includeRole*/ true,
386-
)
379+
// Build structured single-turn input
387380
const payload: Record<string, unknown> = {
388381
model: model.id,
389-
input: formattedInput,
382+
input: [
383+
{
384+
role: "user",
385+
content: [{ type: "input_text", text: prompt }],
386+
},
387+
],
388+
stream: false,
389+
store: false,
390390
}
391391

392-
// Reasoning effort (Responses)
392+
// Reasoning effort (support "minimal"; include summary: "auto" unless disabled)
393393
const effort = (this.options.reasoningEffort || model.reasoningEffort) as
394394
| "minimal"
395395
| "low"
396396
| "medium"
397397
| "high"
398398
| undefined
399-
if (this.options.enableReasoningEffort && effort && effort !== "minimal") {
400-
payload.reasoning = { effort }
399+
if (this.options.enableReasoningEffort && effort) {
400+
;(
401+
payload as { reasoning?: { effort: "minimal" | "low" | "medium" | "high"; summary?: "auto" } }
402+
).reasoning = {
403+
effort,
404+
...(this.options.enableGpt5ReasoningSummary !== false ? { summary: "auto" as const } : {}),
405+
}
401406
}
402407

403-
// Temperature if set
404-
if (this.options.modelTemperature !== undefined) {
405-
payload.temperature = this.options.modelTemperature
408+
// Temperature if supported and set
409+
if (modelInfo.supportsTemperature !== false && this.options.modelTemperature !== undefined) {
410+
;(payload as Record<string, unknown>).temperature = this.options.modelTemperature
406411
}
407412

408-
// Verbosity via text.verbosity - include only when explicitly specified
409-
if (this.options.verbosity) {
410-
payload.text = { verbosity: this.options.verbosity as "low" | "medium" | "high" }
413+
// Verbosity via text.verbosity - include only when supported
414+
if (this.options.verbosity && modelInfo.supportsVerbosity) {
415+
;(payload as { text?: { verbosity: "low" | "medium" | "high" } }).text = {
416+
verbosity: this.options.verbosity as "low" | "medium" | "high",
417+
}
411418
}
412419

413420
// max_output_tokens
414421
if (this.options.includeMaxTokens === true) {
415-
payload.max_output_tokens = this.options.modelMaxTokens || modelInfo.maxTokens
422+
;(payload as Record<string, unknown>).max_output_tokens =
423+
this.options.modelMaxTokens || modelInfo.maxTokens
416424
}
417425

418426
const response = await this._responsesCreateWithRetries(payload, {
419-
usedArrayInput: false,
427+
usedArrayInput: true,
420428
lastUserMessage: undefined,
421429
previousId: undefined,
422430
systemPrompt: "",
@@ -736,47 +744,40 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
736744
const modelId = this.options.openAiModelId ?? ""
737745
const nonStreaming = !(this.options.openAiStreamingEnabled ?? true)
738746

739-
// Build Responses payload (align with OpenAI Native Responses API formatting)
740-
// Azure- and Responses-compatible multimodal handling:
741-
// - Use array input ONLY when the latest user message contains images (initial turn)
742-
// - When previous_response_id is present, send only the latest user turn:
743-
// • Text-only => single string "User: ...", no Developer preface
744-
// • With images => one-item array containing only the latest user content (no Developer preface)
745-
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user")
746-
const lastUserHasImages =
747-
!!lastUserMessage &&
748-
Array.isArray(lastUserMessage.content) &&
749-
lastUserMessage.content.some((b: unknown) => (b as { type?: string } | undefined)?.type === "image")
750-
751-
// Conversation continuity (parity with OpenAiNativeHandler.prepareGpt5Input)
747+
// Determine conversation continuity id (skip when explicitly suppressed)
752748
const previousId = metadata?.suppressPreviousResponseId
753749
? undefined
754750
: (metadata?.previousResponseId ?? this.lastResponseId)
755751

752+
// Prepare Responses API input per test expectations:
753+
// - Non-minimal text-only => single string with Developer/User lines
754+
// - Minimal (previous_response_id) => single string "User: ..." when last user has no images
755+
// - Image cases => structured array; inject Developer preface as first item (non-minimal only)
756+
const lastUserMessage = [...messages].reverse().find((m) => m.role === "user")
757+
const lastUserHasImages =
758+
!!lastUserMessage &&
759+
Array.isArray(lastUserMessage.content) &&
760+
lastUserMessage.content.some((b: any) => (b as any)?.type === "image")
756761
const minimalInputMode = Boolean(previousId)
757762

758763
let inputPayload: unknown
759764
if (minimalInputMode && lastUserMessage) {
760-
// Minimal-mode: only the latest user message (no Developer preface)
765+
// Minimal mode: only latest user turn
761766
if (lastUserHasImages) {
762-
// Single-item array with just the latest user content
763767
inputPayload = this._toResponsesInput([lastUserMessage])
764768
} else {
765-
// Single message string "User: ..."
766769
inputPayload = this._formatResponsesSingleMessage(lastUserMessage, true)
767770
}
768771
} else if (lastUserHasImages && lastUserMessage) {
769-
// Initial turn with images: include Developer preface and minimal prior context to preserve continuity
772+
// Initial turn with images: include Developer preface and minimal context
770773
const lastAssistantMessage = [...messages].reverse().find((m) => m.role === "assistant")
771-
772774
const messagesForArray = messages.filter((m) => {
773775
if (m.role === "assistant") {
774776
return lastAssistantMessage ? m === lastAssistantMessage : false
775777
}
776778
if (m.role === "user") {
777779
const hasImage =
778-
Array.isArray(m.content) &&
779-
m.content.some((b: unknown) => (b as { type?: string } | undefined)?.type === "image")
780+
Array.isArray(m.content) && m.content.some((b: any) => (b as any)?.type === "image")
780781
return hasImage || m === lastUserMessage
781782
}
782783
return false
@@ -789,19 +790,20 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
789790
}
790791
inputPayload = [developerPreface, ...arrayInput]
791792
} else {
792-
// Pure text history: full compact transcript (includes both user and assistant turns)
793+
// Pure text history: compact transcript string
793794
inputPayload = this._formatResponsesInput(systemPrompt, messages)
794795
}
795-
const usedArrayInput = Array.isArray(inputPayload)
796796

797+
// Build base payload: use top-level instructions; default to storing unless explicitly disabled
797798
const basePayload: Record<string, unknown> = {
798799
model: modelId,
799800
input: inputPayload,
800801
...(previousId ? { previous_response_id: previousId } : {}),
802+
instructions: systemPrompt,
803+
store: metadata?.store !== false,
801804
}
802805

803-
// Reasoning effort (Responses expects: reasoning: { effort, summary? })
804-
// Parity with native: support "minimal" and include summary: "auto" unless explicitly disabled
806+
// Reasoning effort (support "minimal"; include summary: "auto" unless disabled)
805807
if (this.options.enableReasoningEffort && (this.options.reasoningEffort || openAiParams?.reasoningEffort)) {
806808
const effort = (this.options.reasoningEffort || openAiParams?.reasoningEffort) as
807809
| "minimal"
@@ -811,25 +813,25 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
811813
| undefined
812814
if (effort) {
813815
;(
814-
basePayload as {
815-
reasoning?: { effort: "minimal" | "low" | "medium" | "high"; summary?: "auto" }
816-
}
816+
basePayload as { reasoning?: { effort: "minimal" | "low" | "medium" | "high"; summary?: "auto" } }
817817
).reasoning = {
818818
effort,
819819
...(this.options.enableGpt5ReasoningSummary !== false ? { summary: "auto" as const } : {}),
820820
}
821821
}
822822
}
823823

824-
// Temperature (only include when explicitly set by the user)
824+
// Temperature: include only if model supports it
825825
const deepseekReasoner = modelId.includes("deepseek-reasoner") || (this.options.openAiR1FormatEnabled ?? false)
826-
if (this.options.modelTemperature !== undefined) {
827-
basePayload.temperature = this.options.modelTemperature
828-
} else if (deepseekReasoner) {
829-
basePayload.temperature = DEEP_SEEK_DEFAULT_TEMPERATURE
826+
if (modelInfo.supportsTemperature !== false) {
827+
if (this.options.modelTemperature !== undefined) {
828+
;(basePayload as Record<string, unknown>).temperature = this.options.modelTemperature
829+
} else if (deepseekReasoner) {
830+
;(basePayload as Record<string, unknown>).temperature = DEEP_SEEK_DEFAULT_TEMPERATURE
831+
}
830832
}
831833

832-
// Verbosity: include only when explicitly specified in settings
834+
// Verbosity: include when provided; retry logic removes it on 400
833835
if (this.options.verbosity) {
834836
;(basePayload as { text?: { verbosity: "low" | "medium" | "high" } }).text = {
835837
verbosity: this.options.verbosity as "low" | "medium" | "high",
@@ -844,7 +846,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
844846
// Non-streaming path
845847
if (nonStreaming) {
846848
const response = await this._responsesCreateWithRetries(basePayload, {
847-
usedArrayInput,
849+
usedArrayInput: Array.isArray(inputPayload),
848850
lastUserMessage,
849851
previousId,
850852
systemPrompt,
@@ -857,7 +859,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
857859
// Streaming path (auto-fallback to non-streaming result if provider ignores stream flag)
858860
const streamingPayload: Record<string, unknown> = { ...basePayload, stream: true }
859861
const maybeStream = await this._responsesCreateWithRetries(streamingPayload, {
860-
usedArrayInput,
862+
usedArrayInput: Array.isArray(inputPayload),
861863
lastUserMessage,
862864
previousId,
863865
systemPrompt,
@@ -925,30 +927,53 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
925927

926928
private _toResponsesInput(anthropicMessages: Anthropic.Messages.MessageParam[]): Array<{
927929
role: "user" | "assistant"
928-
content: Array<{ type: "input_text"; text: string } | { type: "input_image"; image_url: string }>
930+
content: Array<
931+
| { type: "input_text"; text: string }
932+
| { type: "input_image"; image_url: string }
933+
| { type: "output_text"; text: string }
934+
>
929935
}> {
930936
const input: Array<{
931937
role: "user" | "assistant"
932-
content: Array<{ type: "input_text"; text: string } | { type: "input_image"; image_url: string }>
938+
content: Array<
939+
| { type: "input_text"; text: string }
940+
| { type: "input_image"; image_url: string }
941+
| { type: "output_text"; text: string }
942+
>
933943
}> = []
934944

935945
for (const msg of anthropicMessages) {
936946
const role = msg.role === "assistant" ? "assistant" : "user"
937-
const parts: Array<{ type: "input_text"; text: string } | { type: "input_image"; image_url: string }> = []
947+
const parts: Array<
948+
| { type: "input_text"; text: string }
949+
| { type: "input_image"; image_url: string }
950+
| { type: "output_text"; text: string }
951+
> = []
938952

939953
if (typeof msg.content === "string") {
940954
if (msg.content.length > 0) {
941-
parts.push({ type: "input_text", text: msg.content })
955+
if (role === "assistant") {
956+
parts.push({ type: "output_text", text: msg.content })
957+
} else {
958+
parts.push({ type: "input_text", text: msg.content })
959+
}
942960
}
943-
} else {
961+
} else if (Array.isArray(msg.content)) {
944962
for (const block of msg.content) {
945963
if (block.type === "text") {
946-
parts.push({ type: "input_text", text: block.text })
964+
if (role === "assistant") {
965+
parts.push({ type: "output_text", text: block.text })
966+
} else {
967+
parts.push({ type: "input_text", text: block.text })
968+
}
947969
} else if (block.type === "image") {
948-
parts.push({
949-
type: "input_image",
950-
image_url: `data:${block.source.media_type};base64,${block.source.data}`,
951-
})
970+
// Images are treated as user input; ignore images on assistant turns
971+
if (role === "user") {
972+
parts.push({
973+
type: "input_image",
974+
image_url: `data:${block.source.media_type};base64,${block.source.data}`,
975+
})
976+
}
952977
}
953978
// tool_use/tool_result are omitted in this minimal mapping (can be added as needed)
954979
}

0 commit comments

Comments
 (0)