mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-19 13:33:35 +08:00
Expose ChatGPT Official model catalog
When ChatGPT Official is active, the model API now returns the OpenAI Codex catalog and reads or writes the selected GPT model from cc-haha managed settings. Desktop provider activation also recognizes the built-in provider id and resets the current model to the OpenAI default instead of looking for a saved provider record. Constraint: ChatGPT Official is a built-in provider and is not present in providers.json providers[]. Rejected: Reuse the four-slot provider model list | it hides GPT-5.5 and misrepresents the OpenAI catalog as Anthropic slot names. Confidence: high Scope-risk: moderate Tested: bun test src/server/__tests__/settings.test.ts Tested: bun test src/server/__tests__/providers.test.ts --test-name-pattern "ChatGPT Official|getProviderForProxy" Tested: bun test src/server/__tests__/conversation-service.test.ts --test-name-pattern "ChatGPT Official|session-scoped provider" Tested: cd desktop && bun run test src/stores/providerStore.test.ts Tested: git diff --check
This commit is contained in:
parent
67a64c7f80
commit
1ff563d4b0
2
desktop/src/constants/openaiOfficialProvider.ts
Normal file
2
desktop/src/constants/openaiOfficialProvider.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export const OPENAI_OFFICIAL_PROVIDER_ID = 'openai-official'
|
||||||
|
export const OPENAI_OFFICIAL_DEFAULT_MODEL_ID = 'gpt-5.3-codex'
|
||||||
@ -7,6 +7,8 @@ const {
|
|||||||
runtimeStoreState,
|
runtimeStoreState,
|
||||||
setSessionRuntimeMock,
|
setSessionRuntimeMock,
|
||||||
setSelectionMock,
|
setSelectionMock,
|
||||||
|
settingsSetModelMock,
|
||||||
|
settingsFetchAllMock,
|
||||||
} = vi.hoisted(() => ({
|
} = vi.hoisted(() => ({
|
||||||
providersApiMock: {
|
providersApiMock: {
|
||||||
list: vi.fn(),
|
list: vi.fn(),
|
||||||
@ -32,6 +34,8 @@ const {
|
|||||||
},
|
},
|
||||||
setSessionRuntimeMock: vi.fn(),
|
setSessionRuntimeMock: vi.fn(),
|
||||||
setSelectionMock: vi.fn(),
|
setSelectionMock: vi.fn(),
|
||||||
|
settingsSetModelMock: vi.fn(),
|
||||||
|
settingsFetchAllMock: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
vi.mock('../api/providers', () => ({
|
vi.mock('../api/providers', () => ({
|
||||||
@ -59,8 +63,8 @@ vi.mock('./sessionRuntimeStore', () => ({
|
|||||||
vi.mock('./settingsStore', () => ({
|
vi.mock('./settingsStore', () => ({
|
||||||
useSettingsStore: {
|
useSettingsStore: {
|
||||||
getState: () => ({
|
getState: () => ({
|
||||||
setModel: vi.fn(),
|
setModel: settingsSetModelMock,
|
||||||
fetchAll: vi.fn(),
|
fetchAll: settingsFetchAllMock,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
@ -147,4 +151,33 @@ describe('providerStore runtime refresh', () => {
|
|||||||
expect(setSelectionMock).not.toHaveBeenCalled()
|
expect(setSelectionMock).not.toHaveBeenCalled()
|
||||||
expect(setSessionRuntimeMock).not.toHaveBeenCalled()
|
expect(setSessionRuntimeMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('sets the OpenAI default model when activating built-in ChatGPT Official', async () => {
|
||||||
|
providersApiMock.activate.mockResolvedValue({ ok: true })
|
||||||
|
providersApiMock.list.mockResolvedValue({
|
||||||
|
providers: [],
|
||||||
|
activeId: 'openai-official',
|
||||||
|
})
|
||||||
|
|
||||||
|
const { useProviderStore } = await import('./providerStore')
|
||||||
|
await useProviderStore.getState().activateProvider('openai-official')
|
||||||
|
|
||||||
|
expect(settingsSetModelMock).toHaveBeenCalledWith('gpt-5.3-codex')
|
||||||
|
expect(settingsFetchAllMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets the provider main model when activating a saved provider', async () => {
|
||||||
|
const provider = makeProvider()
|
||||||
|
providersApiMock.activate.mockResolvedValue({ ok: true })
|
||||||
|
providersApiMock.list.mockResolvedValue({
|
||||||
|
providers: [provider],
|
||||||
|
activeId: provider.id,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { useProviderStore } = await import('./providerStore')
|
||||||
|
await useProviderStore.getState().activateProvider(provider.id)
|
||||||
|
|
||||||
|
expect(settingsSetModelMock).toHaveBeenCalledWith('model-main')
|
||||||
|
expect(settingsFetchAllMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -6,6 +6,10 @@ import { useChatStore } from './chatStore'
|
|||||||
import { useSessionRuntimeStore } from './sessionRuntimeStore'
|
import { useSessionRuntimeStore } from './sessionRuntimeStore'
|
||||||
import { useSettingsStore } from './settingsStore'
|
import { useSettingsStore } from './settingsStore'
|
||||||
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
|
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
|
||||||
|
import {
|
||||||
|
OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
|
||||||
|
OPENAI_OFFICIAL_PROVIDER_ID,
|
||||||
|
} from '../constants/openaiOfficialProvider'
|
||||||
import type {
|
import type {
|
||||||
SavedProvider,
|
SavedProvider,
|
||||||
CreateProviderInput,
|
CreateProviderInput,
|
||||||
@ -145,12 +149,17 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
|||||||
await get().fetchProviders()
|
await get().fetchProviders()
|
||||||
// 更新默认 provider 时,同步刷新默认 model,避免 settings.json 里残留
|
// 更新默认 provider 时,同步刷新默认 model,避免 settings.json 里残留
|
||||||
// 旧 provider 的 model id 导致默认选择指向不存在的模型。
|
// 旧 provider 的 model id 导致默认选择指向不存在的模型。
|
||||||
const provider = get().providers.find((p) => p.id === id)
|
const settings = useSettingsStore.getState()
|
||||||
if (provider) {
|
if (id === OPENAI_OFFICIAL_PROVIDER_ID) {
|
||||||
const settings = useSettingsStore.getState()
|
await settings.setModel(OPENAI_OFFICIAL_DEFAULT_MODEL_ID)
|
||||||
await settings.setModel(provider.models.main)
|
|
||||||
await settings.fetchAll()
|
await settings.fetchAll()
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const provider = get().providers.find((p) => p.id === id)
|
||||||
|
if (!provider) return
|
||||||
|
await settings.setModel(provider.models.main)
|
||||||
|
await settings.fetchAll()
|
||||||
},
|
},
|
||||||
|
|
||||||
activateOfficial: async () => {
|
activateOfficial: async () => {
|
||||||
|
|||||||
@ -625,6 +625,65 @@ describe('Models API', () => {
|
|||||||
expect(globalSettings.model).toBeUndefined()
|
expect(globalSettings.model).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('GET /api/models should return the OpenAI model catalog when ChatGPT Official is active', async () => {
|
||||||
|
const providerSvc = new ProviderService()
|
||||||
|
await providerSvc.activateProvider('openai-official')
|
||||||
|
|
||||||
|
const { req, url, segments } = makeRequest('GET', '/api/models')
|
||||||
|
const res = await handleModelsApi(req, url, segments)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = await res.json() as {
|
||||||
|
models: Array<{ id: string; name: string }>
|
||||||
|
provider: { id: string; name: string } | null
|
||||||
|
}
|
||||||
|
expect(body.provider).toEqual({
|
||||||
|
id: 'openai-official',
|
||||||
|
name: 'ChatGPT Official',
|
||||||
|
})
|
||||||
|
expect(body.models.map((model) => model.id)).toEqual([
|
||||||
|
'gpt-5.3-codex',
|
||||||
|
'gpt-5.4',
|
||||||
|
'gpt-5.5',
|
||||||
|
'gpt-5.4-mini',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('PUT /api/models/current should persist GPT model to managed settings when ChatGPT Official is active', async () => {
|
||||||
|
const settingsSvc = new SettingsService()
|
||||||
|
const providerSvc = new ProviderService()
|
||||||
|
await settingsSvc.updateUserSettings({ model: 'claude-haiku-4-5' })
|
||||||
|
await providerSvc.activateProvider('openai-official')
|
||||||
|
|
||||||
|
const { req, url, segments } = makeRequest('PUT', '/api/models/current', {
|
||||||
|
modelId: 'gpt-5.5',
|
||||||
|
})
|
||||||
|
const res = await handleModelsApi(req, url, segments)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const managedSettings = await providerSvc.getManagedSettings()
|
||||||
|
expect(managedSettings.model).toBe('gpt-5.5')
|
||||||
|
|
||||||
|
const globalSettings = await settingsSvc.getUserSettings()
|
||||||
|
expect(globalSettings.model).toBe('claude-haiku-4-5')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('GET /api/models/current should read current GPT model from managed settings when ChatGPT Official is active', async () => {
|
||||||
|
const providerSvc = new ProviderService()
|
||||||
|
await providerSvc.activateProvider('openai-official')
|
||||||
|
await providerSvc.updateManagedSettings({ model: 'gpt-5.5' })
|
||||||
|
|
||||||
|
const { req, url, segments } = makeRequest('GET', '/api/models/current')
|
||||||
|
const res = await handleModelsApi(req, url, segments)
|
||||||
|
|
||||||
|
expect(res.status).toBe(200)
|
||||||
|
const body = await res.json()
|
||||||
|
expect(body.model).toMatchObject({
|
||||||
|
id: 'gpt-5.5',
|
||||||
|
name: 'GPT-5.5',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('GET /api/effort should return default effort level', async () => {
|
it('GET /api/effort should return default effort level', async () => {
|
||||||
const { req, url, segments } = makeRequest('GET', '/api/effort')
|
const { req, url, segments } = makeRequest('GET', '/api/effort')
|
||||||
const res = await handleModelsApi(req, url, segments)
|
const res = await handleModelsApi(req, url, segments)
|
||||||
|
|||||||
@ -13,6 +13,11 @@ import { ProviderService } from '../services/providerService.js'
|
|||||||
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
||||||
import { hasOpenAIAuthLogin } from '../../utils/auth.js'
|
import { hasOpenAIAuthLogin } from '../../utils/auth.js'
|
||||||
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
|
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
|
||||||
|
import {
|
||||||
|
OPENAI_OFFICIAL_PROVIDER_ID,
|
||||||
|
OPENAI_OFFICIAL_PROVIDER_NAME,
|
||||||
|
isOpenAIOfficialProviderId,
|
||||||
|
} from '../services/openaiOfficialProvider.js'
|
||||||
|
|
||||||
// ─── Fallback models (used when no provider is configured) ────────────────────
|
// ─── Fallback models (used when no provider is configured) ────────────────────
|
||||||
|
|
||||||
@ -109,6 +114,15 @@ function buildProviderModelList(models: {
|
|||||||
return modelList
|
return modelList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildOpenAIModelList(): ApiModelInfo[] {
|
||||||
|
return OPENAI_CODEX_MODEL_CATALOG.map(model => ({
|
||||||
|
id: model.value,
|
||||||
|
name: model.label,
|
||||||
|
description: model.description,
|
||||||
|
context: '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
function getEnvConfiguredAnthropicModels(): ApiModelInfo[] {
|
function getEnvConfiguredAnthropicModels(): ApiModelInfo[] {
|
||||||
return buildProviderModelList({
|
return buildProviderModelList({
|
||||||
main: process.env.ANTHROPIC_MODEL?.trim() || '',
|
main: process.env.ANTHROPIC_MODEL?.trim() || '',
|
||||||
@ -123,12 +137,7 @@ function getOpenAIAuthModels(): ApiModelInfo[] {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
return OPENAI_CODEX_MODEL_CATALOG.map(model => ({
|
return buildOpenAIModelList()
|
||||||
id: model.value,
|
|
||||||
name: model.label,
|
|
||||||
description: model.description,
|
|
||||||
context: '',
|
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStandaloneModelList(): ApiModelInfo[] {
|
function getStandaloneModelList(): ApiModelInfo[] {
|
||||||
@ -189,6 +198,16 @@ export async function handleModelsApi(
|
|||||||
|
|
||||||
async function handleModelsList(): Promise<Response> {
|
async function handleModelsList(): Promise<Response> {
|
||||||
const { providers, activeId } = await providerService.listProviders()
|
const { providers, activeId } = await providerService.listProviders()
|
||||||
|
if (isOpenAIOfficialProviderId(activeId)) {
|
||||||
|
return Response.json({
|
||||||
|
models: buildOpenAIModelList(),
|
||||||
|
provider: {
|
||||||
|
id: OPENAI_OFFICIAL_PROVIDER_ID,
|
||||||
|
name: OPENAI_OFFICIAL_PROVIDER_NAME,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
|
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
|
||||||
if (activeProvider) {
|
if (activeProvider) {
|
||||||
const modelList = buildProviderModelList(activeProvider.models)
|
const modelList = buildProviderModelList(activeProvider.models)
|
||||||
@ -204,8 +223,9 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
|||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
// Build the full model list: prefer active provider's models, fall back to defaults
|
// Build the full model list: prefer active provider's models, fall back to defaults
|
||||||
const { providers, activeId } = await providerService.listProviders()
|
const { providers, activeId } = await providerService.listProviders()
|
||||||
|
const isOpenAIProviderActive = isOpenAIOfficialProviderId(activeId)
|
||||||
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
|
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
|
||||||
const settings = activeProvider
|
const settings = activeProvider || isOpenAIProviderActive
|
||||||
? await providerService.getManagedSettings()
|
? await providerService.getManagedSettings()
|
||||||
: await settingsService.getUserSettings()
|
: await settingsService.getUserSettings()
|
||||||
const explicitModel = (settings.model as string) || ''
|
const explicitModel = (settings.model as string) || ''
|
||||||
@ -216,7 +236,10 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
|||||||
let currentModelId: string
|
let currentModelId: string
|
||||||
let currentModelName: string
|
let currentModelName: string
|
||||||
|
|
||||||
if (activeProvider) {
|
if (isOpenAIProviderActive) {
|
||||||
|
currentModelId = explicitModel || env.ANTHROPIC_MODEL || 'gpt-5.3-codex'
|
||||||
|
currentModelName = currentModelId
|
||||||
|
} else if (activeProvider) {
|
||||||
// Provider is active — only use the provider-managed cc-haha settings.
|
// Provider is active — only use the provider-managed cc-haha settings.
|
||||||
// This avoids leaking global ~/.claude/settings.json model choices into
|
// This avoids leaking global ~/.claude/settings.json model choices into
|
||||||
// the active provider flow.
|
// the active provider flow.
|
||||||
@ -237,9 +260,11 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
|||||||
const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId
|
const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId
|
||||||
|
|
||||||
// Build available models for name lookup
|
// Build available models for name lookup
|
||||||
const availableModels = activeProvider
|
const availableModels = isOpenAIProviderActive
|
||||||
? buildProviderModelList(activeProvider.models)
|
? buildOpenAIModelList()
|
||||||
: getStandaloneModelList()
|
: activeProvider
|
||||||
|
? buildProviderModelList(activeProvider.models)
|
||||||
|
: getStandaloneModelList()
|
||||||
|
|
||||||
const modelEntry = availableModels.find((m) => m.id === lookupId)
|
const modelEntry = availableModels.find((m) => m.id === lookupId)
|
||||||
|| availableModels.find((m) => m.id === currentModelId)
|
|| availableModels.find((m) => m.id === currentModelId)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user