mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +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,
|
||||
setSessionRuntimeMock,
|
||||
setSelectionMock,
|
||||
settingsSetModelMock,
|
||||
settingsFetchAllMock,
|
||||
} = vi.hoisted(() => ({
|
||||
providersApiMock: {
|
||||
list: vi.fn(),
|
||||
@ -32,6 +34,8 @@ const {
|
||||
},
|
||||
setSessionRuntimeMock: vi.fn(),
|
||||
setSelectionMock: vi.fn(),
|
||||
settingsSetModelMock: vi.fn(),
|
||||
settingsFetchAllMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../api/providers', () => ({
|
||||
@ -59,8 +63,8 @@ vi.mock('./sessionRuntimeStore', () => ({
|
||||
vi.mock('./settingsStore', () => ({
|
||||
useSettingsStore: {
|
||||
getState: () => ({
|
||||
setModel: vi.fn(),
|
||||
fetchAll: vi.fn(),
|
||||
setModel: settingsSetModelMock,
|
||||
fetchAll: settingsFetchAllMock,
|
||||
}),
|
||||
},
|
||||
}))
|
||||
@ -147,4 +151,33 @@ describe('providerStore runtime refresh', () => {
|
||||
expect(setSelectionMock).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 { useSettingsStore } from './settingsStore'
|
||||
import { OFFICIAL_DEFAULT_MODEL_ID } from '../constants/modelCatalog'
|
||||
import {
|
||||
OPENAI_OFFICIAL_DEFAULT_MODEL_ID,
|
||||
OPENAI_OFFICIAL_PROVIDER_ID,
|
||||
} from '../constants/openaiOfficialProvider'
|
||||
import type {
|
||||
SavedProvider,
|
||||
CreateProviderInput,
|
||||
@ -145,12 +149,17 @@ export const useProviderStore = create<ProviderStore>((set, get) => ({
|
||||
await get().fetchProviders()
|
||||
// 更新默认 provider 时,同步刷新默认 model,避免 settings.json 里残留
|
||||
// 旧 provider 的 model id 导致默认选择指向不存在的模型。
|
||||
const provider = get().providers.find((p) => p.id === id)
|
||||
if (provider) {
|
||||
const settings = useSettingsStore.getState()
|
||||
await settings.setModel(provider.models.main)
|
||||
const settings = useSettingsStore.getState()
|
||||
if (id === OPENAI_OFFICIAL_PROVIDER_ID) {
|
||||
await settings.setModel(OPENAI_OFFICIAL_DEFAULT_MODEL_ID)
|
||||
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 () => {
|
||||
|
||||
@ -625,6 +625,65 @@ describe('Models API', () => {
|
||||
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 () => {
|
||||
const { req, url, segments } = makeRequest('GET', '/api/effort')
|
||||
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 { hasOpenAIAuthLogin } from '../../utils/auth.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) ────────────────────
|
||||
|
||||
@ -109,6 +114,15 @@ function buildProviderModelList(models: {
|
||||
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[] {
|
||||
return buildProviderModelList({
|
||||
main: process.env.ANTHROPIC_MODEL?.trim() || '',
|
||||
@ -123,12 +137,7 @@ function getOpenAIAuthModels(): ApiModelInfo[] {
|
||||
return []
|
||||
}
|
||||
|
||||
return OPENAI_CODEX_MODEL_CATALOG.map(model => ({
|
||||
id: model.value,
|
||||
name: model.label,
|
||||
description: model.description,
|
||||
context: '',
|
||||
}))
|
||||
return buildOpenAIModelList()
|
||||
}
|
||||
|
||||
function getStandaloneModelList(): ApiModelInfo[] {
|
||||
@ -189,6 +198,16 @@ export async function handleModelsApi(
|
||||
|
||||
async function handleModelsList(): Promise<Response> {
|
||||
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
|
||||
if (activeProvider) {
|
||||
const modelList = buildProviderModelList(activeProvider.models)
|
||||
@ -204,8 +223,9 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
||||
if (req.method === 'GET') {
|
||||
// Build the full model list: prefer active provider's models, fall back to defaults
|
||||
const { providers, activeId } = await providerService.listProviders()
|
||||
const isOpenAIProviderActive = isOpenAIOfficialProviderId(activeId)
|
||||
const activeProvider = activeId ? providers.find((p) => p.id === activeId) : null
|
||||
const settings = activeProvider
|
||||
const settings = activeProvider || isOpenAIProviderActive
|
||||
? await providerService.getManagedSettings()
|
||||
: await settingsService.getUserSettings()
|
||||
const explicitModel = (settings.model as string) || ''
|
||||
@ -216,7 +236,10 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
||||
let currentModelId: 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.
|
||||
// This avoids leaking global ~/.claude/settings.json model choices into
|
||||
// the active provider flow.
|
||||
@ -237,9 +260,11 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
||||
const lookupId = contextTier ? `${currentModelId}:${contextTier}` : currentModelId
|
||||
|
||||
// Build available models for name lookup
|
||||
const availableModels = activeProvider
|
||||
? buildProviderModelList(activeProvider.models)
|
||||
: getStandaloneModelList()
|
||||
const availableModels = isOpenAIProviderActive
|
||||
? buildOpenAIModelList()
|
||||
: activeProvider
|
||||
? buildProviderModelList(activeProvider.models)
|
||||
: getStandaloneModelList()
|
||||
|
||||
const modelEntry = availableModels.find((m) => m.id === lookupId)
|
||||
|| availableModels.find((m) => m.id === currentModelId)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user