-
Notifications
You must be signed in to change notification settings - Fork 838
Support OpenAI reasoning models with intelligent pattern detection #882
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
base: master
Are you sure you want to change the base?
Changes from 13 commits
3c6f522
2be919e
f4ac0ae
17b9876
9062561
f4b3fd9
79d378e
65ec6b2
712eb8e
520c45c
08b7cca
a3bf9a5
f878898
96cbd20
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -5,7 +5,38 @@ 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, 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 | ||||||||||||||||||||||||||||||||||||||
|
@@ -65,10 +96,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 | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
@@ -116,37 +153,74 @@ export async function generateAnswersWithChatgptApiCompat( | |||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||
const { controller, messageListener, disconnectListener } = setAbortController(port) | ||||||||||||||||||||||||||||||||||||||
const model = getModelValue(session) | ||||||||||||||||||||||||||||||||||||||
const isReasoningModel = isUsingReasoningModel(session) | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
const config = await getUserConfig() | ||||||||||||||||||||||||||||||||||||||
const prompt = getConversationPairs( | ||||||||||||||||||||||||||||||||||||||
session.conversationRecords.slice(-config.maxConversationContextLength), | ||||||||||||||||||||||||||||||||||||||
false, | ||||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||||
prompt.push({ role: 'user', content: question }) | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Filter messages based on model type | ||||||||||||||||||||||||||||||||||||||
const filteredPrompt = isReasoningModel | ||||||||||||||||||||||||||||||||||||||
? 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 | ||||||||||||||||||||||||||||||||||||||
const requestBody = { | ||||||||||||||||||||||||||||||||||||||
messages: filteredPrompt, | ||||||||||||||||||||||||||||||||||||||
model, | ||||||||||||||||||||||||||||||||||||||
...extraBody, | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (isReasoningModel) { | ||||||||||||||||||||||||||||||||||||||
// Reasoning models use max_completion_tokens instead of max_tokens | ||||||||||||||||||||||||||||||||||||||
requestBody.max_completion_tokens = config.maxResponseTokenLength | ||||||||||||||||||||||||||||||||||||||
// Reasoning models don't support streaming during beta | ||||||||||||||||||||||||||||||||||||||
requestBody.stream = false | ||||||||||||||||||||||||||||||||||||||
// 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 | ||||||||||||||||||||||||||||||||||||||
// 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 | ||||||||||||||||||||||||||||||||||||||
requestBody.max_tokens = config.maxResponseTokenLength | ||||||||||||||||||||||||||||||||||||||
requestBody.temperature = config.temperature | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Validate API key | ||||||||||||||||||||||||||||||||||||||
if (!apiKey || typeof apiKey !== 'string' || !apiKey.trim()) { | ||||||||||||||||||||||||||||||||||||||
throw new Error('Invalid API key provided') | ||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The API key validation throws a generic Error which may not provide clear guidance to users about the specific issue. Consider throwing a more specific error type or providing a more descriptive error message that helps users understand what constitutes a valid API key.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
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({ | ||||||||||||||||||||||||||||||||||||||
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 +236,57 @@ 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 (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 | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (data.choices[0]?.finish_reason) { | ||||||||||||||||||||||||||||||||||||||
finish() | ||||||||||||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||||||||||||
let content = choice.message?.content ?? choice.text | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Handle structured response arrays for reasoning models | ||||||||||||||||||||||||||||||||||||||
if (Array.isArray(content)) { | ||||||||||||||||||||||||||||||||||||||
content = extractContentFromArray(content) | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// 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 }) | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
// Only finish when we have a proper finish reason | ||||||||||||||||||||||||||||||||||||||
if (choice.finish_reason) { | ||||||||||||||||||||||||||||||||||||||
finish() | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} 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 | ||||||||||||||||||||||||||||||||||||||
if (delta !== undefined) { | ||||||||||||||||||||||||||||||||||||||
answer += delta | ||||||||||||||||||||||||||||||||||||||
} else if (content) { | ||||||||||||||||||||||||||||||||||||||
answer = content | ||||||||||||||||||||||||||||||||||||||
} else if (text) { | ||||||||||||||||||||||||||||||||||||||
answer += text | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
port.postMessage({ answer, done: false, session: null }) | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
if (choice.finish_reason) { | ||||||||||||||||||||||||||||||||||||||
finish() | ||||||||||||||||||||||||||||||||||||||
return | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||
async onStart() {}, | ||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -164,3 +164,24 @@ export function isInApiModeGroup(apiModeGroup, configOrSession) { | |||||
const [, { value: groupValue }] = foundGroup | ||||||
return groupValue === apiModeGroup | ||||||
} | ||||||
|
||||||
export function isUsingReasoningModel(configOrSession) { | ||||||
const modelValue = getModelValue(configOrSession) | ||||||
if (!modelValue) return false | ||||||
|
||||||
// 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)) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The regex pattern includes suffixes like 'turbo', 'instruct', 'nano', 'small', 'medium', 'large' that may not be valid for reasoning models. Consider using a more restrictive pattern that only includes confirmed OpenAI reasoning model suffixes like 'preview' and 'mini' to prevent false positives.
Suggested change
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||
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 | ||||||
} | ||||||
|
||||||
return false | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The finish function checks and sets the 'finished' flag without synchronization, which could lead to race conditions in concurrent scenarios. Consider using atomic operations or proper synchronization to ensure thread safety.
Copilot uses AI. Check for mistakes.