From 3c6f522d9fb445dc95ac937fe63ffaa0de58f030 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 17:37:34 +0000 Subject: [PATCH 01/13] Initial plan From 2be919e8dbcb2d36a68ca90596fdef94c976a389 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 17:43:53 +0000 Subject: [PATCH 02/13] Add OpenAI o1 series model support with API compatibility Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/config/index.mjs | 7 +++ src/services/apis/openai-api.mjs | 85 +++++++++++++++++++++++--------- src/utils/model-name-convert.mjs | 5 ++ 3 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index fb504aee..23419271 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -65,6 +65,8 @@ export const chatgptApiModelKeys = [ 'chatgptApi4_1', 'chatgptApi4_1_mini', 'chatgptApi4_1_nano', + 'chatgptApiO1Preview', + 'chatgptApiO1Mini', ] export const customApiModelKeys = ['customModel'] export const ollamaApiModelKeys = ['ollamaModel'] @@ -256,6 +258,9 @@ export const Models = { chatgptApi4_1_mini: { value: 'gpt-4.1-mini', desc: 'ChatGPT (GPT-4.1 mini)' }, chatgptApi4_1_nano: { value: 'gpt-4.1-nano', desc: 'ChatGPT (GPT-4.1 nano)' }, + chatgptApiO1Preview: { value: 'o1-preview', desc: 'ChatGPT (o1-preview)' }, + chatgptApiO1Mini: { value: 'o1-mini', desc: 'ChatGPT (o1-mini)' }, + claude2WebFree: { value: '', desc: 'Claude.ai (Web)' }, claude12Api: { value: 'claude-instant-1.2', desc: 'Claude.ai (API, Claude Instant 1.2)' }, claude2Api: { value: 'claude-2.0', desc: 'Claude.ai (API, Claude 2)' }, @@ -541,6 +546,8 @@ export const defaultConfig = { 'openRouter_anthropic_claude_sonnet4', 'openRouter_google_gemini_2_5_pro', 'openRouter_openai_o3', + 'chatgptApiO1Preview', + 'chatgptApiO1Mini', ], customApiModes: [ { diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 2d30c3b1..fc0cdbd3 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -5,7 +5,7 @@ import { fetchSSE } from '../../utils/fetch-sse.mjs' import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs' import { isEmpty } from 'lodash-es' import { getCompletionPromptBase, pushRecord, setAbortController } from './shared.mjs' -import { getModelValue } from '../../utils/model-name-convert.mjs' +import { getModelValue, isUsingO1Model } from '../../utils/model-name-convert.mjs' /** * @param {Browser.Runtime.Port} port @@ -116,13 +116,20 @@ export async function generateAnswersWithChatgptApiCompat( ) { const { controller, messageListener, disconnectListener } = setAbortController(port) const model = getModelValue(session) + const isO1Model = isUsingO1Model(session) const config = await getUserConfig() const prompt = getConversationPairs( session.conversationRecords.slice(-config.maxConversationContextLength), false, ) - prompt.push({ role: 'user', content: question }) + + // Filter out system messages for o1 models (only user and assistant are allowed) + const filteredPrompt = isO1Model + ? prompt.filter((msg) => msg.role === 'user' || msg.role === 'assistant') + : prompt + + filteredPrompt.push({ role: 'user', content: question }) let answer = '' let finished = false @@ -132,6 +139,32 @@ export async function generateAnswersWithChatgptApiCompat( console.debug('conversation history', { content: session.conversationRecords }) port.postMessage({ answer: null, done: true, session: session }) } + + // Build request body with o1-specific parameters + const requestBody = { + messages: filteredPrompt, + model, + ...extraBody, + } + + if (isO1Model) { + // o1 models use max_completion_tokens instead of max_tokens + requestBody.max_completion_tokens = config.maxResponseTokenLength + // o1 models don't support streaming during beta + requestBody.stream = false + // o1 models have fixed parameters during beta + requestBody.temperature = 1 + requestBody.top_p = 1 + requestBody.n = 1 + requestBody.presence_penalty = 0 + requestBody.frequency_penalty = 0 + } else { + // Non-o1 models use the existing behavior + requestBody.stream = true + requestBody.max_tokens = config.maxResponseTokenLength + requestBody.temperature = config.temperature + } + await fetchSSE(`${baseUrl}/chat/completions`, { method: 'POST', signal: controller.signal, @@ -139,14 +172,7 @@ export async function generateAnswersWithChatgptApiCompat( 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}`, }, - body: JSON.stringify({ - messages: prompt, - model, - stream: true, - max_tokens: config.maxResponseTokenLength, - temperature: config.temperature, - ...extraBody, - }), + body: JSON.stringify(requestBody), onMessage(message) { console.debug('sse message', message) if (finished) return @@ -162,21 +188,32 @@ export async function generateAnswersWithChatgptApiCompat( return } - const delta = data.choices[0]?.delta?.content - const content = data.choices[0]?.message?.content - const text = data.choices[0]?.text - if (delta !== undefined) { - answer += delta - } else if (content) { - answer = content - } else if (text) { - answer += text - } - port.postMessage({ answer: answer, done: false, session: null }) + if (isO1Model) { + // For o1 models (non-streaming), get the complete response + const content = data.choices[0]?.message?.content + if (content) { + answer = content + port.postMessage({ answer: answer, done: false, session: null }) + finish() + } + } else { + // For non-o1 models (streaming), handle delta content + const delta = data.choices[0]?.delta?.content + const content = data.choices[0]?.message?.content + const text = data.choices[0]?.text + if (delta !== undefined) { + answer += delta + } else if (content) { + answer = content + } else if (text) { + answer += text + } + port.postMessage({ answer: answer, done: false, session: null }) - if (data.choices[0]?.finish_reason) { - finish() - return + if (data.choices[0]?.finish_reason) { + finish() + return + } } }, async onStart() {}, diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 3f206232..85030887 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -164,3 +164,8 @@ export function isInApiModeGroup(apiModeGroup, configOrSession) { const [, { value: groupValue }] = foundGroup return groupValue === apiModeGroup } + +export function isUsingO1Model(configOrSession) { + const modelValue = getModelValue(configOrSession) + return modelValue && (modelValue === 'o1-preview' || modelValue === 'o1-mini') +} From f4ac0ae1e5d895b841579363559bdb5a7f5ff135 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 18:03:52 +0000 Subject: [PATCH 03/13] Extend reasoning model support and fix error handling in API responses - Add support for o3, o3-mini, o4-mini, gpt-5, gpt-5-mini, gpt-5-nano models - Fix error checking in openai-api.mjs response handling to prevent runtime errors - Refactor duplicated data.choices[0] access pattern with proper null safety - Add isUsingReasoningModel function while maintaining backward compatibility Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/config/index.mjs | 18 ++++++++++++++++++ src/services/apis/openai-api.mjs | 30 +++++++++++++++++++++++------- src/utils/model-name-convert.mjs | 19 +++++++++++++++++-- 3 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index 23419271..14b6744b 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -67,6 +67,12 @@ export const chatgptApiModelKeys = [ 'chatgptApi4_1_nano', 'chatgptApiO1Preview', 'chatgptApiO1Mini', + 'chatgptApiO3Preview', + 'chatgptApiO3Mini', + 'chatgptApiO4Mini', + 'chatgptApiGpt5', + 'chatgptApiGpt5Mini', + 'chatgptApiGpt5Nano', ] export const customApiModelKeys = ['customModel'] export const ollamaApiModelKeys = ['ollamaModel'] @@ -260,6 +266,12 @@ export const Models = { chatgptApiO1Preview: { value: 'o1-preview', desc: 'ChatGPT (o1-preview)' }, chatgptApiO1Mini: { value: 'o1-mini', desc: 'ChatGPT (o1-mini)' }, + chatgptApiO3Preview: { value: 'o3-preview', desc: 'ChatGPT (o3-preview)' }, + chatgptApiO3Mini: { value: 'o3-mini', desc: 'ChatGPT (o3-mini)' }, + chatgptApiO4Mini: { value: 'o4-mini', desc: 'ChatGPT (o4-mini)' }, + chatgptApiGpt5: { value: 'gpt-5', desc: 'ChatGPT (gpt-5)' }, + chatgptApiGpt5Mini: { value: 'gpt-5-mini', desc: 'ChatGPT (gpt-5-mini)' }, + chatgptApiGpt5Nano: { value: 'gpt-5-nano', desc: 'ChatGPT (gpt-5-nano)' }, claude2WebFree: { value: '', desc: 'Claude.ai (Web)' }, claude12Api: { value: 'claude-instant-1.2', desc: 'Claude.ai (API, Claude Instant 1.2)' }, @@ -548,6 +560,12 @@ export const defaultConfig = { 'openRouter_openai_o3', 'chatgptApiO1Preview', 'chatgptApiO1Mini', + 'chatgptApiO3Preview', + 'chatgptApiO3Mini', + 'chatgptApiO4Mini', + 'chatgptApiGpt5', + 'chatgptApiGpt5Mini', + 'chatgptApiGpt5Nano', ], customApiModes: [ { diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index fc0cdbd3..8bb8adcb 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -65,10 +65,16 @@ export async function generateAnswersWithGptCompletionApi(port, question, sessio return } - answer += data.choices[0].text + const choice = data.choices?.[0] + if (!choice) { + console.debug('No choice in response data') + return + } + + answer += choice.text port.postMessage({ answer: answer, done: false, session: null }) - if (data.choices[0]?.finish_reason) { + if (choice.finish_reason) { finish() return } @@ -190,7 +196,12 @@ export async function generateAnswersWithChatgptApiCompat( if (isO1Model) { // For o1 models (non-streaming), get the complete response - const content = data.choices[0]?.message?.content + const choice = data.choices?.[0] + if (!choice) { + console.debug('No choice in response data for o1 model') + return + } + const content = choice.message?.content if (content) { answer = content port.postMessage({ answer: answer, done: false, session: null }) @@ -198,9 +209,14 @@ export async function generateAnswersWithChatgptApiCompat( } } else { // For non-o1 models (streaming), handle delta content - const delta = data.choices[0]?.delta?.content - const content = data.choices[0]?.message?.content - const text = data.choices[0]?.text + const choice = data.choices?.[0] + if (!choice) { + console.debug('No choice in response data') + return + } + const delta = choice.delta?.content + const content = choice.message?.content + const text = choice.text if (delta !== undefined) { answer += delta } else if (content) { @@ -210,7 +226,7 @@ export async function generateAnswersWithChatgptApiCompat( } port.postMessage({ answer: answer, done: false, session: null }) - if (data.choices[0]?.finish_reason) { + if (choice.finish_reason) { finish() return } diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 85030887..4a542a66 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -165,7 +165,22 @@ export function isInApiModeGroup(apiModeGroup, configOrSession) { return groupValue === apiModeGroup } -export function isUsingO1Model(configOrSession) { +export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) - return modelValue && (modelValue === 'o1-preview' || modelValue === 'o1-mini') + return ( + modelValue && + (modelValue === 'o1-preview' || + modelValue === 'o1-mini' || + modelValue === 'o3-preview' || + modelValue === 'o3-mini' || + modelValue === 'o4-mini' || + modelValue === 'gpt-5' || + modelValue === 'gpt-5-mini' || + modelValue === 'gpt-5-nano') + ) +} + +// Keep backward compatibility +export function isUsingO1Model(configOrSession) { + return isUsingReasoningModel(configOrSession) } From 17b9876c1ab703f08d581d2c48f6943f9b371ee1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:01:27 +0000 Subject: [PATCH 04/13] Replace isUsingO1Model with isUsingReasoningModel and remove o1/o3 from default model list Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/config/index.mjs | 4 ---- src/services/apis/openai-api.mjs | 28 ++++++++++++++-------------- src/utils/model-name-convert.mjs | 5 ----- 3 files changed, 14 insertions(+), 23 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index 14b6744b..3467000b 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -558,10 +558,6 @@ export const defaultConfig = { 'openRouter_anthropic_claude_sonnet4', 'openRouter_google_gemini_2_5_pro', 'openRouter_openai_o3', - 'chatgptApiO1Preview', - 'chatgptApiO1Mini', - 'chatgptApiO3Preview', - 'chatgptApiO3Mini', 'chatgptApiO4Mini', 'chatgptApiGpt5', 'chatgptApiGpt5Mini', diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 8bb8adcb..9ac90747 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -5,7 +5,7 @@ import { fetchSSE } from '../../utils/fetch-sse.mjs' import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs' import { isEmpty } from 'lodash-es' import { getCompletionPromptBase, pushRecord, setAbortController } from './shared.mjs' -import { getModelValue, isUsingO1Model } from '../../utils/model-name-convert.mjs' +import { getModelValue, isUsingReasoningModel } from '../../utils/model-name-convert.mjs' /** * @param {Browser.Runtime.Port} port @@ -122,7 +122,7 @@ export async function generateAnswersWithChatgptApiCompat( ) { const { controller, messageListener, disconnectListener } = setAbortController(port) const model = getModelValue(session) - const isO1Model = isUsingO1Model(session) + const isReasoningModel = isUsingReasoningModel(session) const config = await getUserConfig() const prompt = getConversationPairs( @@ -130,8 +130,8 @@ export async function generateAnswersWithChatgptApiCompat( false, ) - // Filter out system messages for o1 models (only user and assistant are allowed) - const filteredPrompt = isO1Model + // Filter out system messages for reasoning models (only user and assistant are allowed) + const filteredPrompt = isReasoningModel ? prompt.filter((msg) => msg.role === 'user' || msg.role === 'assistant') : prompt @@ -146,26 +146,26 @@ export async function generateAnswersWithChatgptApiCompat( port.postMessage({ answer: null, done: true, session: session }) } - // Build request body with o1-specific parameters + // Build request body with reasoning model-specific parameters const requestBody = { messages: filteredPrompt, model, ...extraBody, } - if (isO1Model) { - // o1 models use max_completion_tokens instead of max_tokens + if (isReasoningModel) { + // Reasoning models use max_completion_tokens instead of max_tokens requestBody.max_completion_tokens = config.maxResponseTokenLength - // o1 models don't support streaming during beta + // Reasoning models don't support streaming during beta requestBody.stream = false - // o1 models have fixed parameters during beta + // Reasoning models have fixed parameters during beta requestBody.temperature = 1 requestBody.top_p = 1 requestBody.n = 1 requestBody.presence_penalty = 0 requestBody.frequency_penalty = 0 } else { - // Non-o1 models use the existing behavior + // Non-reasoning models use the existing behavior requestBody.stream = true requestBody.max_tokens = config.maxResponseTokenLength requestBody.temperature = config.temperature @@ -194,11 +194,11 @@ export async function generateAnswersWithChatgptApiCompat( return } - if (isO1Model) { - // For o1 models (non-streaming), get the complete response + if (isReasoningModel) { + // For reasoning models (non-streaming), get the complete response const choice = data.choices?.[0] if (!choice) { - console.debug('No choice in response data for o1 model') + console.debug('No choice in response data for reasoning model') return } const content = choice.message?.content @@ -208,7 +208,7 @@ export async function generateAnswersWithChatgptApiCompat( finish() } } else { - // For non-o1 models (streaming), handle delta content + // For non-reasoning models (streaming), handle delta content const choice = data.choices?.[0] if (!choice) { console.debug('No choice in response data') diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 4a542a66..8d780c41 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -179,8 +179,3 @@ export function isUsingReasoningModel(configOrSession) { modelValue === 'gpt-5-nano') ) } - -// Keep backward compatibility -export function isUsingO1Model(configOrSession) { - return isUsingReasoningModel(configOrSession) -} From 9062561d527850c5923d8bf1c82842ca9f36f4af Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:40:57 +0000 Subject: [PATCH 05/13] Remove old model definitions (o1, o3 series) from codebase Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/config/index.mjs | 8 -------- src/utils/model-name-convert.mjs | 6 +----- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/src/config/index.mjs b/src/config/index.mjs index 3467000b..94d96a93 100644 --- a/src/config/index.mjs +++ b/src/config/index.mjs @@ -65,10 +65,6 @@ export const chatgptApiModelKeys = [ 'chatgptApi4_1', 'chatgptApi4_1_mini', 'chatgptApi4_1_nano', - 'chatgptApiO1Preview', - 'chatgptApiO1Mini', - 'chatgptApiO3Preview', - 'chatgptApiO3Mini', 'chatgptApiO4Mini', 'chatgptApiGpt5', 'chatgptApiGpt5Mini', @@ -264,10 +260,6 @@ export const Models = { chatgptApi4_1_mini: { value: 'gpt-4.1-mini', desc: 'ChatGPT (GPT-4.1 mini)' }, chatgptApi4_1_nano: { value: 'gpt-4.1-nano', desc: 'ChatGPT (GPT-4.1 nano)' }, - chatgptApiO1Preview: { value: 'o1-preview', desc: 'ChatGPT (o1-preview)' }, - chatgptApiO1Mini: { value: 'o1-mini', desc: 'ChatGPT (o1-mini)' }, - chatgptApiO3Preview: { value: 'o3-preview', desc: 'ChatGPT (o3-preview)' }, - chatgptApiO3Mini: { value: 'o3-mini', desc: 'ChatGPT (o3-mini)' }, chatgptApiO4Mini: { value: 'o4-mini', desc: 'ChatGPT (o4-mini)' }, chatgptApiGpt5: { value: 'gpt-5', desc: 'ChatGPT (gpt-5)' }, chatgptApiGpt5Mini: { value: 'gpt-5-mini', desc: 'ChatGPT (gpt-5-mini)' }, diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 8d780c41..69232014 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -169,11 +169,7 @@ export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) return ( modelValue && - (modelValue === 'o1-preview' || - modelValue === 'o1-mini' || - modelValue === 'o3-preview' || - modelValue === 'o3-mini' || - modelValue === 'o4-mini' || + (modelValue === 'o4-mini' || modelValue === 'gpt-5' || modelValue === 'gpt-5-mini' || modelValue === 'gpt-5-nano') From 79d378e5f02d1cb92a4af3ccc5f2b45347de7560 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 19:51:01 +0000 Subject: [PATCH 06/13] Implement smarter reasoning model detection using regex patterns Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/utils/model-name-convert.mjs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 69232014..84f6bbd7 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -167,11 +167,17 @@ export function isInApiModeGroup(apiModeGroup, configOrSession) { export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) - return ( - modelValue && - (modelValue === 'o4-mini' || - modelValue === 'gpt-5' || - modelValue === 'gpt-5-mini' || - modelValue === 'gpt-5-nano') - ) + if (!modelValue) return false + + // Match o[134] pattern with optional dash and suffix (e.g., o1, o1-preview, o3-mini, o4-mini) + if (/^o[134](-|$)/.test(modelValue)) { + return true + } + + // Match gpt-5* pattern but exclude gpt-5-chat-* variants + if (modelValue.startsWith('gpt-5') && !modelValue.startsWith('gpt-5-chat')) { + return true + } + + return false } From 65ec6b2f9af650bf84610e0f1b5959c7c4e51d75 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Sun, 24 Aug 2025 03:58:47 +0800 Subject: [PATCH 07/13] Update src/services/apis/openai-api.mjs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 9ac90747..c8973913 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -130,10 +130,11 @@ export async function generateAnswersWithChatgptApiCompat( false, ) - // Filter out system messages for reasoning models (only user and assistant are allowed) + // Always filter out system messages; for reasoning models, only allow user and assistant + const promptWithoutSystem = prompt.filter((msg) => msg.role !== 'system') const filteredPrompt = isReasoningModel - ? prompt.filter((msg) => msg.role === 'user' || msg.role === 'assistant') - : prompt + ? promptWithoutSystem.filter((msg) => msg.role === 'user' || msg.role === 'assistant') + : promptWithoutSystem filteredPrompt.push({ role: 'user', content: question }) From 712eb8e17e05a5c64fddfe2c41cde79d0e7e6bbb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 23 Aug 2025 20:18:51 +0000 Subject: [PATCH 08/13] Address review comments: fix system message filtering, improve error handling, and enhance model detection Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 20 +++++++++++--------- src/utils/model-name-convert.mjs | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index c8973913..80517beb 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -130,21 +130,21 @@ export async function generateAnswersWithChatgptApiCompat( false, ) - // Always filter out system messages; for reasoning models, only allow user and assistant - const promptWithoutSystem = prompt.filter((msg) => msg.role !== 'system') + // Filter messages based on model type const filteredPrompt = isReasoningModel - ? promptWithoutSystem.filter((msg) => msg.role === 'user' || msg.role === 'assistant') - : promptWithoutSystem + ? prompt.filter((msg) => msg.role === 'user' || msg.role === 'assistant') + : prompt filteredPrompt.push({ role: 'user', content: question }) let answer = '' let finished = false const finish = () => { + if (finished) return finished = true pushRecord(session, question, answer) console.debug('conversation history', { content: session.conversationRecords }) - port.postMessage({ answer: null, done: true, session: session }) + port.postMessage({ answer: null, done: true, session }) } // Build request body with reasoning model-specific parameters @@ -202,10 +202,12 @@ export async function generateAnswersWithChatgptApiCompat( console.debug('No choice in response data for reasoning model') return } - const content = choice.message?.content - if (content) { + const content = choice.message?.content ?? choice.text + if (content !== undefined && content !== null) { answer = content - port.postMessage({ answer: answer, done: false, session: null }) + port.postMessage({ answer, done: false, session: null }) + } + if (choice.finish_reason || content !== undefined) { finish() } } else { @@ -225,7 +227,7 @@ export async function generateAnswersWithChatgptApiCompat( } else if (text) { answer += text } - port.postMessage({ answer: answer, done: false, session: null }) + port.postMessage({ answer, done: false, session: null }) if (choice.finish_reason) { finish() diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 84f6bbd7..2dc7449f 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -169,8 +169,8 @@ export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) if (!modelValue) return false - // Match o[134] pattern with optional dash and suffix (e.g., o1, o1-preview, o3-mini, o4-mini) - if (/^o[134](-|$)/.test(modelValue)) { + // Explicitly match o1, o3, or o4 with optional dash and suffix (e.g., o1, o1-preview, o3-mini, o4-mini) + if (/^(o1|o3|o4)(?:-|$)/.test(modelValue)) { return true } From 520c45c5410725935b1f7dadee771285dab4bd30 Mon Sep 17 00:00:00 2001 From: Peter Dave Hello Date: Sat, 30 Aug 2025 03:51:02 +0800 Subject: [PATCH 09/13] Update src/services/apis/openai-api.mjs Co-authored-by: qodo-merge-pro[bot] <151058649+qodo-merge-pro[bot]@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 80517beb..6dbcaa23 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -202,9 +202,24 @@ export async function generateAnswersWithChatgptApiCompat( console.debug('No choice in response data for reasoning model') return } - const content = choice.message?.content ?? choice.text + let content = choice.message?.content ?? choice.text + if (Array.isArray(content)) { + // Prefer output_text segments; fallback to any string content + const parts = content + .map((p) => { + if (typeof p === 'string') return p + if (p && typeof p === 'object') { + if (typeof p.output_text === 'string') return p.output_text + if (typeof p.text === 'string') return p.text + } + return '' + }) + .filter(Boolean) + content = parts.join('') + } if (content !== undefined && content !== null) { - answer = content + answer = String(content) + } port.postMessage({ answer, done: false, session: null }) } if (choice.finish_reason || content !== undefined) { From 08b7cca21c05eb3010ee092d89fd1584031e0343 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:02:07 +0000 Subject: [PATCH 10/13] Fix syntax error in OpenAI API file to resolve CI build failure Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 6dbcaa23..940c30e4 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -219,7 +219,6 @@ export async function generateAnswersWithChatgptApiCompat( } if (content !== undefined && content !== null) { answer = String(content) - } port.postMessage({ answer, done: false, session: null }) } if (choice.finish_reason || content !== undefined) { From a3bf9a5e6659cd9ce09b0170495e359a1f94f1ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 29 Aug 2025 20:15:40 +0000 Subject: [PATCH 11/13] Address Copilot and Codex bot feedback: fix regex pattern and enhance reasoning model support Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 5 +++++ src/utils/model-name-convert.mjs | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 940c30e4..223c9e17 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -165,6 +165,11 @@ export async function generateAnswersWithChatgptApiCompat( requestBody.n = 1 requestBody.presence_penalty = 0 requestBody.frequency_penalty = 0 + // Disallow tools/functions/function calling in reasoning mode + delete requestBody.tools + delete requestBody.tool_choice + delete requestBody.functions + delete requestBody.function_call } else { // Non-reasoning models use the existing behavior requestBody.stream = true diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index 2dc7449f..de19bc2a 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -169,8 +169,8 @@ export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) if (!modelValue) return false - // Explicitly match o1, o3, or o4 with optional dash and suffix (e.g., o1, o1-preview, o3-mini, o4-mini) - if (/^(o1|o3|o4)(?:-|$)/.test(modelValue)) { + // Explicitly match o1, o3, or o4 as standalone or with a dash and valid suffix (e.g., o1, o1-preview, o3-mini, o4-mini) + if (/^(o1|o3|o4)(?:$|-[a-zA-Z][\w-]*)$/.test(modelValue)) { return true } From f8788987896e0581b43b5d1ddb359d4a74fe00f3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 Aug 2025 09:26:58 +0000 Subject: [PATCH 12/13] Improve reasoning model detection and content processing Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 69 ++++++++++++++++++++++++-------- src/utils/model-name-convert.mjs | 8 +++- 2 files changed, 58 insertions(+), 19 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 223c9e17..69dbfcd3 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -7,6 +7,37 @@ import { isEmpty } from 'lodash-es' import { getCompletionPromptBase, pushRecord, setAbortController } from './shared.mjs' import { getModelValue, isUsingReasoningModel } from '../../utils/model-name-convert.mjs' +/** + * Extract content from structured response arrays for reasoning models + * @param {Array} contentArray - Array of content segments + * @returns {string} - Extracted text content + */ +function extractContentFromArray(contentArray) { + if (!Array.isArray(contentArray)) { + console.debug('Content is not an array, returning empty string') + return '' + } + + try { + const parts = contentArray + .map((part) => { + if (typeof part === 'string') return part + if (part && typeof part === 'object') { + // Prefer output_text segments; fallback to text property + if (typeof part.output_text === 'string') return part.output_text + if (typeof part.text === 'string') return part.text + } + return '' + }) + .filter(Boolean) + + return parts.join('') + } catch (error) { + console.debug('Error extracting content from array:', error) + return '' + } +} + /** * @param {Browser.Runtime.Port} port * @param {string} question @@ -177,12 +208,17 @@ export async function generateAnswersWithChatgptApiCompat( requestBody.temperature = config.temperature } + // Validate API key + if (!apiKey || typeof apiKey !== 'string' || !apiKey.trim()) { + throw new Error('Invalid API key provided') + } + await fetchSSE(`${baseUrl}/chat/completions`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', - Authorization: `Bearer ${apiKey}`, + Authorization: `Bearer ${apiKey.trim()}`, }, body: JSON.stringify(requestBody), onMessage(message) { @@ -207,26 +243,25 @@ export async function generateAnswersWithChatgptApiCompat( console.debug('No choice in response data for reasoning model') return } + let content = choice.message?.content ?? choice.text + + // Handle structured response arrays for reasoning models if (Array.isArray(content)) { - // Prefer output_text segments; fallback to any string content - const parts = content - .map((p) => { - if (typeof p === 'string') return p - if (p && typeof p === 'object') { - if (typeof p.output_text === 'string') return p.output_text - if (typeof p.text === 'string') return p.text - } - return '' - }) - .filter(Boolean) - content = parts.join('') + content = extractContentFromArray(content) } - if (content !== undefined && content !== null) { - answer = String(content) - port.postMessage({ answer, done: false, session: null }) + + // Ensure content is a string and not empty + if (content && typeof content === 'string') { + const trimmedContent = content.trim() + if (trimmedContent) { + answer = trimmedContent + port.postMessage({ answer, done: false, session: null }) + } } - if (choice.finish_reason || content !== undefined) { + + // Only finish when we have a proper finish reason + if (choice.finish_reason) { finish() } } else { diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index de19bc2a..a7941a3a 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -169,12 +169,16 @@ export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) if (!modelValue) return false - // Explicitly match o1, o3, or o4 as standalone or with a dash and valid suffix (e.g., o1, o1-preview, o3-mini, o4-mini) - if (/^(o1|o3|o4)(?:$|-[a-zA-Z][\w-]*)$/.test(modelValue)) { + // Match o1, o3, or o4 models with optional standard OpenAI suffixes + // Allows: o1, o1-preview, o1-mini, o3, o3-mini, o4, o4-mini, etc. + // Prevents: o10, o30, o40, o1x, o3x, o4x, and other invalid patterns + if (/^o[134](?:$|-(?:preview|mini|turbo|instruct|nano|small|medium|large))$/.test(modelValue)) { return true } // Match gpt-5* pattern but exclude gpt-5-chat-* variants + // Allows: gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-preview, gpt-5-turbo + // Prevents: gpt-5-chat-latest, gpt-5-chat, etc. if (modelValue.startsWith('gpt-5') && !modelValue.startsWith('gpt-5-chat')) { return true } From 96cbd20243623e52b51d3ad9a5ed5867a079fcef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 30 Aug 2025 09:46:06 +0000 Subject: [PATCH 13/13] Address automated code review feedback: improve error handling, validation, and code robustness Co-authored-by: PeterDaveHello <3691490+PeterDaveHello@users.noreply.github.com> --- src/services/apis/openai-api.mjs | 44 ++++++++++++++++++++------------ src/utils/model-name-convert.mjs | 14 +++++++--- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/services/apis/openai-api.mjs b/src/services/apis/openai-api.mjs index 69dbfcd3..8ac9614c 100644 --- a/src/services/apis/openai-api.mjs +++ b/src/services/apis/openai-api.mjs @@ -33,7 +33,7 @@ function extractContentFromArray(contentArray) { return parts.join('') } catch (error) { - console.debug('Error extracting content from array:', error) + console.error('Error extracting content from array:', error) return '' } } @@ -162,8 +162,12 @@ export async function generateAnswersWithChatgptApiCompat( ) // Filter messages based on model type + // Reasoning models only support 'user' and 'assistant' roles during beta period const filteredPrompt = isReasoningModel - ? prompt.filter((msg) => msg.role === 'user' || msg.role === 'assistant') + ? prompt.filter((msg) => { + const role = msg?.role + return role === 'user' || role === 'assistant' + }) : prompt filteredPrompt.push({ role: 'user', content: question }) @@ -185,6 +189,7 @@ export async function generateAnswersWithChatgptApiCompat( ...extraBody, } + // Apply model-specific configurations if (isReasoningModel) { // Reasoning models use max_completion_tokens instead of max_tokens requestBody.max_completion_tokens = config.maxResponseTokenLength @@ -196,11 +201,12 @@ export async function generateAnswersWithChatgptApiCompat( requestBody.n = 1 requestBody.presence_penalty = 0 requestBody.frequency_penalty = 0 - // Disallow tools/functions/function calling in reasoning mode + // Remove unsupported parameters for reasoning models delete requestBody.tools delete requestBody.tool_choice delete requestBody.functions delete requestBody.function_call + delete requestBody.max_tokens // Ensure max_tokens is not present } else { // Non-reasoning models use the existing behavior requestBody.stream = true @@ -208,9 +214,11 @@ export async function generateAnswersWithChatgptApiCompat( requestBody.temperature = config.temperature } - // Validate API key + // Validate API key with detailed error message if (!apiKey || typeof apiKey !== 'string' || !apiKey.trim()) { - throw new Error('Invalid API key provided') + throw new Error( + 'Invalid or empty API key provided. Please check your OpenAI API key configuration.', + ) } await fetchSSE(`${baseUrl}/chat/completions`, { @@ -236,14 +244,15 @@ export async function generateAnswersWithChatgptApiCompat( return } + // Validate response structure early + const choice = data.choices?.[0] + if (!choice) { + console.debug('No choice in response data') + return + } + if (isReasoningModel) { // For reasoning models (non-streaming), get the complete response - const choice = data.choices?.[0] - if (!choice) { - console.debug('No choice in response data for reasoning model') - return - } - let content = choice.message?.content ?? choice.text // Handle structured response arrays for reasoning models @@ -258,6 +267,14 @@ export async function generateAnswersWithChatgptApiCompat( answer = trimmedContent port.postMessage({ answer, done: false, session: null }) } + } else if (content) { + // Handle unexpected content types gracefully + console.debug('Unexpected content type for reasoning model:', typeof content) + const stringContent = String(content).trim() + if (stringContent) { + answer = stringContent + port.postMessage({ answer, done: false, session: null }) + } } // Only finish when we have a proper finish reason @@ -266,11 +283,6 @@ export async function generateAnswersWithChatgptApiCompat( } } else { // For non-reasoning models (streaming), handle delta content - const choice = data.choices?.[0] - if (!choice) { - console.debug('No choice in response data') - return - } const delta = choice.delta?.content const content = choice.message?.content const text = choice.text diff --git a/src/utils/model-name-convert.mjs b/src/utils/model-name-convert.mjs index a7941a3a..12f90eac 100644 --- a/src/utils/model-name-convert.mjs +++ b/src/utils/model-name-convert.mjs @@ -167,19 +167,27 @@ export function isInApiModeGroup(apiModeGroup, configOrSession) { export function isUsingReasoningModel(configOrSession) { const modelValue = getModelValue(configOrSession) - if (!modelValue) return false + if (!modelValue || typeof modelValue !== 'string') return false + + // Normalize model value to handle potential whitespace + const normalizedModelValue = modelValue.trim().toLowerCase() // Match o1, o3, or o4 models with optional standard OpenAI suffixes + // Uses word boundaries to prevent false positives like o10, o30, o40 // Allows: o1, o1-preview, o1-mini, o3, o3-mini, o4, o4-mini, etc. // Prevents: o10, o30, o40, o1x, o3x, o4x, and other invalid patterns - if (/^o[134](?:$|-(?:preview|mini|turbo|instruct|nano|small|medium|large))$/.test(modelValue)) { + if ( + /^o[134](?:$|-(?:preview|mini|turbo|instruct|nano|small|medium|large))$/.test( + normalizedModelValue, + ) + ) { return true } // Match gpt-5* pattern but exclude gpt-5-chat-* variants // Allows: gpt-5, gpt-5-mini, gpt-5-nano, gpt-5-preview, gpt-5-turbo // Prevents: gpt-5-chat-latest, gpt-5-chat, etc. - if (modelValue.startsWith('gpt-5') && !modelValue.startsWith('gpt-5-chat')) { + if (normalizedModelValue.startsWith('gpt-5') && !normalizedModelValue.startsWith('gpt-5-chat')) { return true }