Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
98 changes: 98 additions & 0 deletions app/admin-api/credentials/models/route.ts
Original file line number Diff line number Diff line change
@@ -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<Response> => {
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<Response> => {
const authError = await getAdminSessionErrorResponse(request);

if (authError) return authError;

return toResponse();
};

export const POST = async (request: Request): Promise<Response> => {
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<Response> => {
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]);
};
66 changes: 12 additions & 54 deletions app/api-test/api-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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] ??
'',
};
};

Expand All @@ -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] ?? '');
Expand All @@ -144,23 +112,13 @@ const ApiTest = () => {
<Select
className="w-full"
id="testCredential"
options={[
{
label: apiTestText('followCurrent'),
value: followCurrentCredentialValue,
},
...context.credentialOptions.map((credential) => ({
label: `${credential.filename} · ${credential.email || credential.user_id}`,
value: credential.filename,
})),
]}
value={
context.apiTest.credentialFilename || followCurrentCredentialValue
}
options={context.credentialOptions.map((credential) => ({
label: `${credential.filename} · ${credential.email || credential.user_id}`,
value: credential.filename,
}))}
value={context.apiTest.credentialFilename}
onChange={(value) => {
context.onCredentialChange(
value === followCurrentCredentialValue ? '' : String(value),
);
context.onCredentialChange(String(value));
}}
/>
</div>
Expand Down
4 changes: 3 additions & 1 deletion app/credentials/access-key-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ export const AccessKeyCard = ({
wrap="wrap"
>
{accessKey.credentialFilenames.map((filename) => (
<Tag key={filename}>{filename}</Tag>
<Tag className="access-key-credential-tag" key={filename}>
{filename}
</Tag>
))}
</Flexbox>
</div>
Expand Down
4 changes: 4 additions & 0 deletions app/credentials/credentials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export interface CredentialsState {
form: CredentialFormState;
items: CredentialSummary[];
loading: boolean;
modelRows: Record<string, { error: string | null; models: string[] }>;
modelsLoading: boolean;
revealedSecret: RevealedAccessKeySecret | null;
}

Expand Down Expand Up @@ -149,6 +151,8 @@ export const defaultCredentialsState: CredentialsState = {
},
items: [],
loading: true,
modelRows: {},
modelsLoading: false,
revealedSecret: null,
};

Expand Down
8 changes: 4 additions & 4 deletions app/debug/debug.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1257,11 +1257,11 @@ const Debug = () => {
value={
hasDuration(item.elapsedMs) &&
item.elapsedMs > 0 &&
item.usage?.totalTokens
? Math.round(
(item.usage.totalTokens * 1_000) /
item.usage?.outputTokens
? `${Math.round(
(item.usage.outputTokens * 1_000) /
item.elapsedMs,
)
)} t/s`
: null
}
/>
Expand Down
Loading
Loading