Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
5 changes: 4 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,8 @@
:cwd="composerCwd"
:collaboration-modes="availableCollaborationModes"
:selected-collaboration-mode="selectedCollaborationMode"
:models="availableModelIds" :selected-model="composerSelectedModelId"
:models="availableModelIds" :model-reasoning-efforts="availableModelReasoningEfforts"
:selected-model="composerSelectedModelId"
:selected-reasoning-effort="selectedReasoningEffort"
:selected-speed-mode="selectedSpeedMode"
:is-updating-speed-mode="isUpdatingSpeedMode"
Expand Down Expand Up @@ -1027,6 +1028,7 @@
:collaboration-modes="availableCollaborationModes"
:selected-collaboration-mode="selectedCollaborationMode"
:models="availableModelIds"
:model-reasoning-efforts="availableModelReasoningEfforts"
:selected-model="composerSelectedModelId"
:selected-reasoning-effort="selectedReasoningEffort"
:selected-speed-mode="selectedSpeedMode"
Expand Down Expand Up @@ -1418,6 +1420,7 @@ const {
selectedThreadId,
availableCollaborationModes,
availableModelIds,
availableModelReasoningEfforts,
selectedCollaborationMode,
selectedModelId,
selectedReasoningEffort,
Expand Down
92 changes: 91 additions & 1 deletion src/api/codexGateway.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { getAvailableModelIds, getThreadDetail, listDirectoryComposioConnectors, resumeThread, startThreadTurn } from './codexGateway'
import { getAvailableModelIds, getAvailableModels, getCurrentModelConfig, getThreadDetail, listDirectoryComposioConnectors, resumeThread, startThreadTurn } from './codexGateway'

function mockRpcFetch(): { requests: Array<{ method: string, params: Record<string, unknown> }> } {
const requests: Array<{ method: string, params: Record<string, unknown> }> = []
Expand Down Expand Up @@ -59,6 +59,48 @@ describe('startThreadTurn collaboration mode payloads', () => {
},
})
})

it('passes GPT-5.6 ultra reasoning through to Codex', async () => {
const { requests } = mockRpcFetch()

await startThreadTurn('thread-1', 'solve it', [], 'gpt-5.6-sol', 'ultra', undefined, [], 'default')

expect(requests[0].params.effort).toBe('ultra')
expect(requests[0].params.collaborationMode).toEqual({
mode: 'default',
settings: {
model: 'gpt-5.6-sol',
reasoning_effort: 'ultra',
developer_instructions: null,
},
})
})
})

describe('getCurrentModelConfig', () => {
afterEach(() => {
vi.unstubAllGlobals()
})

it.each(['max', 'ultra'] as const)('keeps the GPT-5.6 %s reasoning level', async (reasoningEffort) => {
vi.stubGlobal('fetch', vi.fn(async () => new Response(JSON.stringify({
result: {
config: {
model: 'gpt-5.6-sol',
model_provider: 'openai',
model_reasoning_effort: reasoningEffort,
},
},
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})))

await expect(getCurrentModelConfig()).resolves.toMatchObject({
model: 'gpt-5.6-sol',
reasoningEffort,
})
})
})

describe('listDirectoryComposioConnectors', () => {
Expand Down Expand Up @@ -173,6 +215,54 @@ describe('getAvailableModelIds', () => {
})).resolves.toEqual(['gpt-5.5', 'gpt-5.4-mini'])
expect(requests).toEqual(['/codex-api/provider-models', '/codex-api/rpc'])
})

it('preserves model-specific reasoning metadata from model/list', async () => {
vi.stubGlobal('fetch', vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
const body = typeof init?.body === 'string'
? JSON.parse(init.body) as { method: string }
: { method: '' }
expect(body.method).toBe('model/list')
return new Response(JSON.stringify({
result: {
data: [
{
id: 'gpt-5.6-sol',
supportedReasoningEfforts: [
{ reasoningEffort: 'low' },
{ reasoningEffort: 'max' },
{ reasoningEffort: 'ultra' },
],
defaultReasoningEffort: 'low',
},
{
id: 'gpt-5.5',
supportedReasoningEfforts: [
{ reasoningEffort: 'low' },
{ reasoningEffort: 'xhigh' },
],
defaultReasoningEffort: 'low',
},
],
},
}), {
status: 200,
headers: { 'Content-Type': 'application/json' },
})
}))

await expect(getAvailableModels({ includeProviderModels: false })).resolves.toEqual([
{
id: 'gpt-5.6-sol',
supportedReasoningEfforts: ['low', 'max', 'ultra'],
defaultReasoningEffort: 'low',
},
{
id: 'gpt-5.5',
supportedReasoningEfforts: ['low', 'xhigh'],
defaultReasoningEffort: 'low',
},
])
})
})

describe('getThreadDetail', () => {
Expand Down
71 changes: 56 additions & 15 deletions src/api/codexGateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@ import type {
ConfigReadResponse,
GetAccountRateLimitsResponse,
ModelListResponse,
ReasoningEffort,
ThreadForkResponse,
ThreadListResponse,
ThreadReadResponse,
ThreadResumeResponse,
ThreadStartResponse,
Turn,
} from './appServerDtos'
import { isReasoningEffort } from '../types/codex'
import { extractErrorMessage, normalizeCodexApiError } from './codexErrors'
import {
readActiveTurnIdFromResponse,
Expand Down Expand Up @@ -53,6 +53,7 @@ import type {
UiReviewWorkspaceView,
UiRateLimitSnapshot,
UiRateLimitWindow,
ReasoningEffort,
UiThreadAutomation,
UiThreadAutomationStatus,
} from '../types/codex'
Expand Down Expand Up @@ -252,6 +253,12 @@ type ProviderModelsResponse = {
exclusive?: unknown
}

export type AvailableModel = {
id: string
supportedReasoningEfforts: ReasoningEffort[] | null
defaultReasoningEffort: ReasoningEffort | null
}

const PROVIDER_MODELS_FETCH_TIMEOUT_MS = 5_000

type ResolvedCollaborationModeSettings = {
Expand Down Expand Up @@ -700,10 +707,7 @@ async function enrichThreadMessagesWithFallback(threadId: string, messages: UiMe
}

function normalizeReasoningEffort(value: unknown): ReasoningEffort | '' {
const allowed: ReasoningEffort[] = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh']
return typeof value === 'string' && allowed.includes(value as ReasoningEffort)
? (value as ReasoningEffort)
: ''
return isReasoningEffort(value) ? value : ''
}

function normalizeSpeedMode(value: unknown): SpeedMode {
Expand Down Expand Up @@ -2036,28 +2040,65 @@ async function fetchProviderModelIds(providerId?: string): Promise<{ ids: string
return null
}

export async function getAvailableModelIds(options: { includeProviderModels?: boolean; requireProviderModels?: boolean; providerId?: string } = {}): Promise<string[]> {
function normalizeAvailableModel(value: unknown): AvailableModel | null {
const record = asRecord(value)
if (!record) return null
const id = readString(record.id ?? record.model)?.trim() ?? ''
if (!id) return null

const rawEfforts = record.supportedReasoningEfforts ?? record.supported_reasoning_efforts
const supportedReasoningEfforts = Array.isArray(rawEfforts)
? rawEfforts.flatMap((candidate) => {
const option = asRecord(candidate)
const effort = option
? option.reasoningEffort ?? option.reasoning_effort ?? option.effort
: candidate
return isReasoningEffort(effort) ? [effort] : []
}).filter((effort, index, efforts) => efforts.indexOf(effort) === index)
: null
const rawDefault = record.defaultReasoningEffort ?? record.default_reasoning_effort

return {
id,
supportedReasoningEfforts,
defaultReasoningEffort: isReasoningEffort(rawDefault) ? rawDefault : null,
}
}

function providerAvailableModel(id: string): AvailableModel {
return {
id,
supportedReasoningEfforts: null,
defaultReasoningEffort: null,
}
}

export async function getAvailableModels(options: { includeProviderModels?: boolean; requireProviderModels?: boolean; providerId?: string } = {}): Promise<AvailableModel[]> {
const shouldIncludeProviderModels = options.includeProviderModels !== false
const providerModels = shouldIncludeProviderModels ? await fetchProviderModelIds(options.providerId) : null

if (providerModels?.exclusive || options.requireProviderModels) {
return providerModels?.ids ?? []
return (providerModels?.ids ?? []).map(providerAvailableModel)
}

const payload = await callRpc<ModelListResponse>('model/list', {})
const ids: string[] = []
const models: AvailableModel[] = []
for (const row of payload.data) {
const candidate = row.id || row.model
if (!candidate || ids.includes(candidate)) continue
ids.push(candidate)
const model = normalizeAvailableModel(row)
if (!model || models.some((candidate) => candidate.id === model.id)) continue
models.push(model)
}

if (!shouldIncludeProviderModels || !providerModels) return ids
if (!shouldIncludeProviderModels || !providerModels) return models

for (const candidate of providerModels.ids) {
if (!ids.includes(candidate)) ids.push(candidate)
for (const id of providerModels.ids) {
if (!models.some((candidate) => candidate.id === id)) models.push(providerAvailableModel(id))
}
return ids
return models
}

export async function getAvailableModelIds(options: { includeProviderModels?: boolean; requireProviderModels?: boolean; providerId?: string } = {}): Promise<string[]> {
return (await getAvailableModels(options)).map((model) => model.id)
}

export async function getCurrentModelConfig(): Promise<CurrentModelConfig> {
Expand Down
18 changes: 15 additions & 3 deletions src/components/content/ThreadComposer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -287,12 +287,12 @@
/>

<ComposerDropdown
class="thread-composer-control"
class="thread-composer-control thread-composer-thinking-control"
:model-value="selectedReasoningEffort"
:options="reasoningOptions"
:placeholder="t('Thinking')"
open-direction="up"
:disabled="isComposerConfigDisabled"
:disabled="isComposerConfigDisabled || reasoningOptions.length === 0"
@update:model-value="onReasoningEffortSelect"
/>
</template>
Expand Down Expand Up @@ -438,6 +438,7 @@ const props = defineProps<{
collaborationModes?: CollaborationModeOption[]
selectedCollaborationMode: CollaborationModeKind
models: string[]
modelReasoningEfforts?: Record<string, ReasoningEffort[]>
selectedModel: string
selectedReasoningEffort: ReasoningEffort | ''
selectedSpeedMode: SpeedMode
Expand Down Expand Up @@ -585,14 +586,22 @@ const isAndroid = typeof navigator !== 'undefined' && /Android/i.test(navigator.
const DRAFT_STORAGE_PREFIX = 'codex-web-local.thread-draft.v1.'
let lastActiveThreadId = ''

const reasoningOptions: Array<{ value: ReasoningEffort; label: string }> = [
const reasoningOptionCatalog: Array<{ value: ReasoningEffort; label: string }> = [
{ value: 'none', label: 'None' },
{ value: 'minimal', label: 'Minimal' },
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
{ value: 'xhigh', label: 'Extra high' },
{ value: 'max', label: 'Max' },
{ value: 'ultra', label: 'Ultra' },
Comment thread
qodo-free-for-open-source-projects[bot] marked this conversation as resolved.
]
const reasoningOptions = computed(() => {
const supportedEfforts = props.modelReasoningEfforts?.[props.selectedModel]
if (supportedEfforts === undefined) return reasoningOptionCatalog
const supportedSet = new Set(supportedEfforts)
return reasoningOptionCatalog.filter((option) => supportedSet.has(option.value))
})
function formatModelLabel(modelId: string): string {
return modelId.trim().replace(/^gpt/i, 'GPT')
}
Expand Down Expand Up @@ -2214,6 +2223,9 @@ watch(
@apply truncate;
}

.thread-composer-thinking-control :deep(.composer-dropdown-options) {
@apply max-h-64;
}

.thread-composer-actions {
@apply ml-auto flex min-w-0 items-center gap-2;
Expand Down
Loading