diff --git a/.env.example b/.env.example index c728a26..067deb6 100644 --- a/.env.example +++ b/.env.example @@ -8,7 +8,8 @@ CODEBUDDY_LOG_LEVEL=INFO # Optional upstream overrides # CODEBUDDY_API_ENDPOINT=https://copilot.tencent.com # CODEBUDDY_INTERNET_ENVIRONMENT=ioa -# CODEBUDDY_MODELS=glm-5.1,glm-5.0,glm-5.0-turbo + +# Models are discovered from each saved credential. No model-list setting is needed. # Optional file-backend data directory. Default: .codebuddy_data # CODEBUDDY_STORAGE_FILE_DIR=.codebuddy_data diff --git a/README.md b/README.md index da9d83c..ddcc981 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,8 @@ Current persisted runtime settings: - `CODEBUDDY_AUTH_MODE` - `CODEBUDDY_INTERNET_ENVIRONMENT` - `CODEBUDDY_LOG_LEVEL` -- `CODEBUDDY_MODELS` + +Models are discovered from CodeBuddy for each active credential. The credentials page shows one row per credential with its supported models and a refresh action. `/v1/models` only merges models from credentials bound to the requesting API key. ## Logging diff --git a/app/admin-api/credentials/models/route.ts b/app/admin-api/credentials/models/route.ts new file mode 100644 index 0000000..6b82cb9 --- /dev/null +++ b/app/admin-api/credentials/models/route.ts @@ -0,0 +1,98 @@ +import { getAdminSessionErrorResponse } from '@/lib/server/admin/session'; +import { + findEligibleCredentialRecordByFilename, + getCredentialSupportedModels, + listEligibleCredentialRecords, + updateCredentialSupportedModels, +} from '@/lib/server/domain/credentials'; +import { getModelsByCredential } from '@/lib/server/proxy/codebuddy'; +import { getJsonBody } from '@/lib/server/shared/http'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-dynamic'; + +const toResponse = async (filenames?: string[]): Promise => { + const credentials = await listEligibleCredentialRecords(filenames); + const models = Object.fromEntries( + credentials.map((credential) => [ + credential.filename, + { + error: null, + models: getCredentialSupportedModels(credential.data).map((id) => ({ + id, + })), + }, + ]), + ); + + return Response.json({ models }); +}; + +export const GET = async (request: Request): Promise => { + const authError = await getAdminSessionErrorResponse(request); + + if (authError) return authError; + + return toResponse(); +}; + +export const POST = async (request: Request): Promise => { + const authError = await getAdminSessionErrorResponse(request); + + if (authError) return authError; + + const body = await getJsonBody<{ filename?: unknown }>(request); + const filename = + typeof body.filename === 'string' ? body.filename.trim() : ''; + + if (!filename || !(await findEligibleCredentialRecordByFilename(filename))) { + return Response.json( + { error: { message: 'Credential is unavailable' } }, + { status: 404 }, + ); + } + + const models = await getModelsByCredential( + await listEligibleCredentialRecords([filename]), + ); + const value = models[filename]; + + if (value && !value.error) { + await updateCredentialSupportedModels( + filename, + value.models.map((model) => model.id), + ); + } + + return Response.json({ models }); +}; + +export const PUT = async (request: Request): Promise => { + const authError = await getAdminSessionErrorResponse(request); + + if (authError) return authError; + + const body = await getJsonBody<{ filename?: unknown; models?: unknown }>( + request, + ); + const filename = + typeof body.filename === 'string' ? body.filename.trim() : ''; + + if (!filename || !(await findEligibleCredentialRecordByFilename(filename))) { + return Response.json( + { error: { message: 'Credential is unavailable' } }, + { status: 404 }, + ); + } + + const models = + typeof body.models === 'string' + ? body.models + .split(/[\n,]/) + .map((model) => model.trim()) + .filter(Boolean) + : []; + await updateCredentialSupportedModels(filename, models); + + return toResponse([filename]); +}; diff --git a/app/api-test/api-test.tsx b/app/api-test/api-test.tsx index f4a49bb..64c06b0 100644 --- a/app/api-test/api-test.tsx +++ b/app/api-test/api-test.tsx @@ -8,39 +8,6 @@ import { useTranslations } from 'next-intl'; import { createContext, useContext } from 'react'; import type { AdminConsoleInitialData } from '@/app/page-data'; -const defaultModels = [ - 'glm-5.1', - 'glm-5.0', - 'glm-5.0-turbo', - 'glm-5v-turbo', - 'glm-4.7', - 'minimax-m3-play', - 'minimax-m2.7', - 'minimax-m2.5', - 'kimi-k2.6', - 'kimi-k2.5', - 'hy3-preview-agent', - 'deepseek-v4-pro', - 'deepseek-v4-flash', - 'deepseek-v3-2-volc', - 'glm-5.1-ioa', - 'glm-5.0-ioa', - 'glm-5.0-turbo-ioa', - 'glm-5v-turbo-ioa', - 'glm-4.7-ioa', - 'minimax-m3-ioa', - 'minimax-m2.7-ioa', - 'minimax-m2.5-ioa', - 'kimi-k2.6-ioa', - 'kimi-k2.5-ioa', - 'hy3-preview-agent-ioa', - 'deepseek-v4-pro-ioa', - 'deepseek-v4-flash-ioa', - 'deepseek-v3-2-volc-ioa', -] as const; - -const followCurrentCredentialValue = '__follow_current_rotation__'; - export interface ApiTestController { apiTest: { credentialFilename: string; @@ -99,10 +66,11 @@ export const createApiTestState = ( credentialFilename: currentCredential?.filename ?? validCredentials[0]?.filename ?? '', model: - initialData.modelSettings - .split(',') - .map((model) => model.trim()) - .find(Boolean) ?? '', + initialData.credentialModels[ + currentCredential?.filename ?? validCredentials[0]?.filename ?? '' + ]?.[0] ?? + initialData.models[0] ?? + '', }; }; @@ -122,7 +90,7 @@ const useApiTest = (): ApiTestController => { const ApiTest = () => { const context = useApiTest(); const apiTestText = useTranslations('Admin.apiTest'); - const models = context.models.length ? context.models : [...defaultModels]; + const models = context.models; const model = models.includes(context.apiTest.model) ? context.apiTest.model : (models[0] ?? ''); @@ -144,23 +112,13 @@ const ApiTest = () => {