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
12 changes: 7 additions & 5 deletions app/admin-api/credentials/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
listCredentials,
updateCredentialByIndex,
} from '@/lib/server/domain/credentials';
import { refreshCredentialModels } from '@/lib/server/domain/credential-models';
import { getAdminSessionErrorResponse } from '@/lib/server/admin/session';
import { getJsonBody } from '@/lib/server/shared/http';

Expand Down Expand Up @@ -33,12 +34,13 @@ export const POST = async (request: Request): Promise<Response> => {
return Response.json(await updateCredentialByIndex(body.index, body));
}

return Response.json(
await addCredential(
body,
typeof body.filename === 'string' ? body.filename : undefined,
),
const saved = await addCredential(
body,
typeof body.filename === 'string' ? body.filename : undefined,
);
await refreshCredentialModels(saved.filename);

return Response.json(saved);
} catch (error) {
return Response.json(
{
Expand Down
24 changes: 10 additions & 14 deletions app/page-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import type { AdminUsagePreferences } from '@/lib/server/admin/session';
import { listAccessKeys } from '@/lib/server/domain/access-keys';
import { getActiveConfig, getSettingLabels } from '@/lib/server/domain/config';
import {
getCredentialSupportedModels,
getCurrentCredentialInfo,
listEligibleCredentialRecords,
listCredentials,
} from '@/lib/server/domain/credentials';
import {
getModelsByCredential,
getModelsForCredentials,
} from '@/lib/server/proxy/codebuddy';
import { getModelsForCredentials } from '@/lib/server/proxy/codebuddy';
import { getDebugSettings, listDebugLogs } from '@/lib/server/domain/debug';
import { getUsageAnalytics } from '@/lib/server/domain/usage';
import type { AppLocale } from '@/lib/i18n/routing';
Expand Down Expand Up @@ -145,19 +143,17 @@ export const getInitialData = async ({
}
case 'api-test': {
const eligibleCredentials = await listEligibleCredentialRecords();
const [credentials, currentCredential, models, credentialModels] =
await Promise.all([
listCredentials(),
getCurrentCredentialInfo(),
getModelsForCredentials(eligibleCredentials),
getModelsByCredential(eligibleCredentials),
]);
const [credentials, currentCredential, models] = await Promise.all([
listCredentials(),
getCurrentCredentialInfo(),
getModelsForCredentials(eligibleCredentials),
]);

return {
credentialModels: Object.fromEntries(
Object.entries(credentialModels).map(([filename, value]) => [
filename,
value.models.map((model) => model.id),
eligibleCredentials.map((credential) => [
credential.filename,
getCredentialSupportedModels(credential.data),
Comment on lines +154 to +156

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when saved credential models are empty

When an existing credential has not yet persisted supported_models, this now still materializes a per-credential entry with []. The API-test shell reads initialData.credentialModels[filename] ?? initialData.models, so that empty array is treated as authoritative and hides the models discovered by getModelsForCredentials(eligibleCredentials) on the same load; legacy credentials or credentials whose startup refresh has not completed can therefore show an empty model selector even though discovery succeeded. Omit empty entries or populate them from per-credential discovery so the global fallback remains usable.

Useful? React with 👍 / 👎.

]),
),
credentials: credentials.credentials as unknown as CredentialSummary[],
Expand Down
7 changes: 4 additions & 3 deletions app/page-shell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ const AdminPageLayoutContent = ({
modelsLoading: false,
}));
setApiTest((current) => {
if (filename && current.credentialFilename !== filename) {
return current;
}

const selectedFilename = filename ?? current.credentialFilename;
const models = modelRows[selectedFilename]?.models;

Expand Down Expand Up @@ -1044,9 +1048,6 @@ const AdminPageLayoutContent = ({
name: result.data.filename ?? 'unknown',
}),
);
if (!isEditing && result.data.filename) {
await refreshCredentialModels(result.data.filename);
}
await refreshAdminData();
};

Expand Down
17 changes: 15 additions & 2 deletions lib/server/domain/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
readStorageJson,
writeStorageJson,
} from '../storage';
import {
getCredentialSupportedModels,
listEligibleCredentialRecords,
} from './credentials';

export interface RuntimeConfig {
CODEBUDDY_API_ENDPOINT: string;
Expand Down Expand Up @@ -168,7 +172,16 @@ export const getCodeBuddyApiEndpoint = async (): Promise<string> => {
: 'https://copilot.tencent.com';
};

export const getDefaultModel = async (fallback = 'glm-5.1'): Promise<string> =>
fallback;
export const getDefaultModel = async (
fallback = 'glm-5.1',
): Promise<string> => {
const credentials = await listEligibleCredentialRecords();

return (
credentials
.flatMap((credential) => getCredentialSupportedModels(credential.data))
Comment thread
orangeboyChen marked this conversation as resolved.
.sort((left, right) => left.localeCompare(right))[0] ?? fallback
Comment thread
orangeboyChen marked this conversation as resolved.
);
};

export { getConfigDir, getConfigPath, getCredsDir, getFileStorageDir };
35 changes: 26 additions & 9 deletions lib/server/domain/credential-models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
findCredentialRecordByFilename,
getCredentialSupportedModels,
listEligibleCredentialRecords,
updateCredentialSupportedModels,
Expand All @@ -9,6 +10,30 @@ const globalCredentialModelRefreshState = globalThis as typeof globalThis & {
__codebuddy2apiCredentialModelRefresh__?: Promise<void>;
};

export const refreshCredentialModels = async (
filename: string,
): Promise<void> => {
try {
const credential = await findCredentialRecordByFilename(filename);

if (!credential) {
return;
}

const result = await getModelsByCredential([credential]);
const models = result[credential.filename]?.models ?? [];

if (models.length) {
await updateCredentialSupportedModels(
credential.filename,
models.map((model) => model.id),
);
}
} catch (error) {
console.warn('[CodeBuddy2API] Unable to refresh credential models', error);
}
};

export const refreshMissingCredentialModels = (): Promise<void> => {
if (
!globalCredentialModelRefreshState.__codebuddy2apiCredentialModelRefresh__
Expand All @@ -24,15 +49,7 @@ export const refreshMissingCredentialModels = (): Promise<void> => {

await Promise.allSettled(
missingModels.map(async (credential) => {
const result = await getModelsByCredential([credential]);
const models = result[credential.filename]?.models ?? [];

if (models.length) {
await updateCredentialSupportedModels(
credential.filename,
models.map((model) => model.id),
);
}
await refreshCredentialModels(credential.filename);
}),
);
} catch (error) {
Expand Down
12 changes: 11 additions & 1 deletion lib/server/domain/credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -690,16 +690,26 @@ export const resolveCredentialForRequest = async ({
accessKeyId,
affinityKey,
allowedCredentialFilenames,
model,
}: {
accessKeyId?: string;
affinityKey?: string;
allowedCredentialFilenames?: string[];
model?: string;
} = {}): Promise<CredentialRecord | null> => {
const records = await readCredentialRecords();
const requestedModel = model?.trim();
const eligibleRecords = getEligibleRecords(
records,
allowedCredentialFilenames,
);
).filter((record) => {
if (!requestedModel) return true;

const supportedModels = getCredentialSupportedModels(record.data);
return (
supportedModels.length === 0 || supportedModels.includes(requestedModel)
Comment on lines +708 to +710

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when cached model metadata has no match

When every eligible credential has a nonempty but stale supported_models value that omits a newly enabled upstream model, this filter removes all credentials and the proxy returns No valid CodeBuddy credentials found without contacting upstream. Nonempty model metadata is only refreshed manually or when initially absent, so valid explicit-model requests can remain blocked indefinitely after upstream availability changes; if no cached list matches, fall back to the otherwise eligible credentials or refresh the metadata before rejecting the request.

Useful? React with 👍 / 👎.

);
});

if (!eligibleRecords.length) {
return null;
Expand Down
2 changes: 2 additions & 0 deletions lib/server/proxy/codebuddy-auth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { getCodeBuddyApiEndpoint } from '../domain/config';
import { addCredential, type CredentialData } from '../domain/credentials';
import { refreshCredentialModels } from '../domain/credential-models';
import { getAdminSessionErrorResponse } from '../admin/session';

const getAuthStateEndpoint = async (): Promise<string> =>
Expand Down Expand Up @@ -286,6 +287,7 @@ export const pollCodeBuddyAuth = async (
};
const credential = buildCredentialDataFromToken(tokenPayload);
const saved = await addCredential(credential);
await refreshCredentialModels(saved.filename);

return Response.json({
access_token: tokenPayload.access_token,
Expand Down
34 changes: 15 additions & 19 deletions lib/server/proxy/codebuddy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,26 +433,11 @@ const normalizeMessages = (
return applyPromptCacheControl(filtered);
}

let hasSystemMessage = false;

const normalized = filtered.map((message, index) => {
if (message.role === 'system') {
hasSystemMessage = true;
return message;
}

const normalized = filtered.map((message) => {
if (message.role !== 'developer') {
return message;
}

if (index === 0 && !hasSystemMessage) {
hasSystemMessage = true;
return {
...message,
role: 'system',
};
}

return {
...message,
role: 'user',
Expand All @@ -466,12 +451,14 @@ const normalizeMessages = (

export const resolveProxyContext = async (
request: NextRequest,
model?: string,
): Promise<ProxyContext> => {
const accessKey = await resolveRequestAccessKey(request);
const credential = await resolveCredentialForRequest({
accessKeyId: accessKey?.id,
affinityKey: getCredentialAffinityKey(request, accessKey?.id ?? null),
allowedCredentialFilenames: accessKey?.credentialFilenames,
model,
});

if (!credential) {
Expand Down Expand Up @@ -673,10 +660,13 @@ const buildUpstreamBody = async (
context.preferences.firstMessageRoleToSystem,
);
const maxTokens = body.max_tokens ?? body.max_completion_tokens;
const credentialModels = getCredentialSupportedModels(
context.auth.credentialData,
);
const model =
typeof body.model === 'string' && body.model.trim()
? body.model
: await getDefaultModel();
: (credentialModels[0] ?? (await getDefaultModel()));

return {
model,
Expand Down Expand Up @@ -1315,7 +1305,8 @@ export const proxyChatCompletions = async (
}

try {
const resolvedContext = context ?? (await resolveProxyContext(request));
const resolvedContext =
context ?? (await resolveProxyContext(request, body.model));
setDebugTraceCredential(debugTrace, resolvedContext.credentialFilename);
const upstreamBody = await buildUpstreamBody(body, resolvedContext);
const apiEndpoint = await getCodeBuddyApiEndpoint();
Expand Down Expand Up @@ -1430,7 +1421,12 @@ export const proxyResponsesUpstream = async (
debugTrace?: DebugTrace,
): Promise<Response> => {
try {
const resolvedContext = context ?? (await resolveProxyContext(request));
const resolvedContext =
context ??
(await resolveProxyContext(
request,
typeof body.model === 'string' ? body.model : undefined,
));
setDebugTraceCredential(debugTrace, resolvedContext.credentialFilename);
const upstreamBody = {
...body,
Expand Down
22 changes: 19 additions & 3 deletions lib/server/proxy/responses.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NextRequest } from 'next/server';

import { getDefaultModel } from '../domain/config';
import { getCredentialSupportedModels } from '../domain/credentials';
import type { DebugTrace } from '../domain/debug';
import {
proxyChatCompletions,
Expand Down Expand Up @@ -1409,12 +1410,27 @@ export const handleResponsesRequest = async (
requireEligible: true,
},
)
: await resolveProxyContext(request);
: await resolveProxyContext(
request,
typeof body.model === 'string' ? body.model : undefined,
);
Comment thread
orangeboyChen marked this conversation as resolved.

const scopedBody =
!storedPreviousSession &&
(typeof body.model !== 'string' || !body.model.trim())
? {
...body,
model:
getCredentialSupportedModels(
proxyContext.auth.credentialData,
)[0] ?? (await getDefaultModel()),
}
: body;

if (proxyContext.preferences.responsesPassthrough) {
return proxyResponsesUpstream(
request,
body as Record<string, unknown>,
scopedBody as Record<string, unknown>,
proxyContext,
debugTrace,
);
Expand All @@ -1426,7 +1442,7 @@ export const handleResponsesRequest = async (
);

const prepared = await prepareTranscript(
body,
scopedBody,
proxyContext.accessKeyId,
previousSession,
);
Expand Down
4 changes: 2 additions & 2 deletions messages/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@
"credentialResponsesDirectHelp": "When enabled, `/v1/responses` requests using this credential are sent upstream directly instead of being converted into Chat Completions.",
"credentialResponsesProxyTag": "Responses → Chat",
"credentialRoleAsSystem": "Normalize developer messages for upstream",
"credentialRoleAsSystemHelp": "Sends the first developer message as system and later developer messages as user.",
"credentialRoleAsSystemTag": "developer → system",
"credentialRoleAsSystemHelp": "Sends every developer message as user.",
"credentialRoleAsSystemTag": "developer → user",
"credentialRoleKeepDeveloper": "Keep developer",
"credentialSectionTitle": "Saved credentials",
"modelCredential": "Credential",
Expand Down
4 changes: 2 additions & 2 deletions messages/ja-JP.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,8 +233,8 @@
"credentialResponsesDirectHelp": "有効にすると、この認証情報で処理される `/v1/responses` リクエストは Chat Completions へ変換せず上流へ直接送信されます。",
"credentialResponsesProxyTag": "Responses → Chat",
"credentialRoleAsSystem": "developer メッセージを上流向けに正規化",
"credentialRoleAsSystemHelp": "先頭の developer メッセージは system、それ以降の developer メッセージは user として送信します。",
"credentialRoleAsSystemTag": "developer → system",
"credentialRoleAsSystemHelp": "すべての developer メッセージを user として送信します。",
"credentialRoleAsSystemTag": "developer → user",
"credentialRoleKeepDeveloper": "developer を保持",
"credentialSectionTitle": "保存済み認証情報",
"modelCredential": "認証情報",
Expand Down
4 changes: 2 additions & 2 deletions messages/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@
"credentialResponsesDirectHelp": "启用后,使用此凭证的 `/v1/responses` 请求将直接发送到上游,不再转换为 Chat Completions。",
"credentialResponsesProxyTag": "Responses → Chat",
"credentialRoleAsSystem": "转换 developer 消息角色以兼容上游",
"credentialRoleAsSystemHelp": "首条 developer 消息作为 system 发送,其余 developer 消息作为 user 发送。",
"credentialRoleAsSystemTag": "developer → system",
"credentialRoleAsSystemHelp": "所有 developer 消息均作为 user 发送。",
"credentialRoleAsSystemTag": "developer → user",
"credentialRoleKeepDeveloper": "保留 developer",
"credentialSectionTitle": "已保存的凭证",
"modelCredential": "凭据",
Expand Down
Loading
Loading