fix(routes): dedupe direct credentials and clarify binding labels

This commit is contained in:
Brucents
2026-03-20 20:41:32 +08:00
parent 088d0455a9
commit 5ee493778f
10 changed files with 331 additions and 30 deletions
@@ -248,6 +248,55 @@ describe('refreshModelsForAccount credential discovery', () => {
expect(parsed.runtimeHealth?.checkedAt).toMatch(/\d{4}-\d{2}-\d{2}T/);
});
it('does not scan hidden managed tokens for direct apikey connections', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockImplementation(async (_baseUrl: string, token: string) => (
token === 'sk-direct-credential' ? ['gpt-4.1'] : ['legacy-should-not-be-used']
));
const site = await db.insert(schema.sites).values({
name: 'apikey-direct-site',
url: 'https://apikey-direct.example.com',
platform: 'new-api',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'apikey-direct-user',
accessToken: '',
apiToken: 'sk-direct-credential',
status: 'active',
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
}).returning().get();
const hiddenToken = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'legacy-hidden',
token: 'sk-legacy-hidden',
source: 'legacy',
enabled: true,
isDefault: true,
}).returning().get();
const result = await refreshModelsForAccount(account.id);
expect(result).toMatchObject({
accountId: account.id,
refreshed: true,
status: 'success',
modelCount: 1,
modelsPreview: ['gpt-4.1'],
tokenScanned: 0,
discoveredByCredential: true,
});
const tokenRows = await db.select().from(schema.tokenModelAvailability)
.where(eq(schema.tokenModelAvailability.tokenId, hiddenToken.id))
.all();
expect(tokenRows).toHaveLength(0);
});
it('returns structured result when account missing', async () => {
const result = await refreshModelsForAccount(9999);
+61
View File
@@ -85,6 +85,67 @@ describe('rebuildTokenRoutesFromAvailability', () => {
expect(channels[0]?.manualOverride).toBe(false);
});
it('ignores hidden account_tokens for direct apikey connections when rebuilding routes', async () => {
const site = await db.insert(schema.sites).values({
name: 'apikey-legacy-site',
url: 'https://apikey-legacy.example.com',
platform: 'new-api',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'apikey-legacy-user',
accessToken: '',
apiToken: 'sk-direct-credential',
status: 'active',
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
}).returning().get();
const hiddenToken = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'legacy-hidden',
token: 'sk-hidden-legacy-token',
source: 'legacy',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.modelAvailability).values({
accountId: account.id,
modelName: 'gpt-4.1',
available: true,
latencyMs: 200,
checkedAt: '2026-03-20T08:00:00.000Z',
}).run();
await db.insert(schema.tokenModelAvailability).values({
tokenId: hiddenToken.id,
modelName: 'gpt-4.1',
available: true,
latencyMs: 180,
checkedAt: '2026-03-20T08:00:00.000Z',
}).run();
const rebuild = await rebuildTokenRoutesFromAvailability();
expect(rebuild.models).toBe(1);
const route = await db.select().from(schema.tokenRoutes)
.where(eq(schema.tokenRoutes.modelPattern, 'gpt-4.1'))
.get();
expect(route).toBeDefined();
const channels = await db.select().from(schema.routeChannels)
.where(and(
eq(schema.routeChannels.routeId, route!.id),
eq(schema.routeChannels.accountId, account.id),
))
.all();
expect(channels).toHaveLength(1);
expect(channels[0]?.tokenId ?? null).toBeNull();
});
it('creates an exact route with an account-direct channel for oauth model availability', async () => {
const site = await db.insert(schema.sites).values({
name: 'codex-site',
+17 -17
View File
@@ -10,8 +10,8 @@ import {
isUsableAccountToken,
} from './accountTokenService.js';
import {
getCredentialModeFromExtraConfig,
mergeAccountExtraConfig,
requiresManagedAccountTokens,
resolvePlatformUserId,
supportsDirectAccountRoutingConnection,
} from './accountExtraConfig.js';
@@ -141,12 +141,6 @@ function isSiteDisabled(status?: string | null): boolean {
return (status || 'active') === 'disabled';
}
function isApiKeyConnection(account: typeof schema.accounts.$inferSelect): boolean {
const explicit = getCredentialModeFromExtraConfig(account.extraConfig);
if (explicit && explicit !== 'auto') return explicit === 'apikey';
return !(account.accessToken || '').trim();
}
function normalizeModels(models: string[]): string[] {
const normalizedModels: string[] = [];
const seen = new Set<string>();
@@ -821,18 +815,21 @@ export async function refreshModelsForAccount(accountId: number): Promise<ModelR
} catch { }
}
let enabledTokens = await db.select()
.from(schema.accountTokens)
.where(and(
eq(schema.accountTokens.accountId, account.id),
eq(schema.accountTokens.enabled, true),
eq(schema.accountTokens.valueStatus, ACCOUNT_TOKEN_VALUE_STATUS_READY),
))
.all();
const usesManagedTokens = requiresManagedAccountTokens(account);
let enabledTokens = usesManagedTokens
? await db.select()
.from(schema.accountTokens)
.where(and(
eq(schema.accountTokens.accountId, account.id),
eq(schema.accountTokens.enabled, true),
eq(schema.accountTokens.valueStatus, ACCOUNT_TOKEN_VALUE_STATUS_READY),
))
.all()
: [];
enabledTokens = enabledTokens.filter(isUsableAccountToken);
// Last fallback: if still no managed token but account has a legacy apiToken, mirror it into token table.
if (!isApiKeyConnection(account) && enabledTokens.length === 0) {
if (usesManagedTokens && enabledTokens.length === 0) {
const fallback = discoveredApiToken || account.apiToken || null;
if (fallback) {
ensureDefaultTokenForAccount(account.id, fallback, { name: 'default', source: 'legacy' });
@@ -1018,7 +1015,10 @@ export async function rebuildTokenRoutesFromAvailability() {
),
)
.all();
const usableTokenRows = tokenRows.filter((row) => isUsableAccountToken(row.account_tokens));
const usableTokenRows = tokenRows.filter((row) => (
isUsableAccountToken(row.account_tokens)
&& requiresManagedAccountTokens(row.accounts)
));
const accountRows = await db.select().from(schema.modelAvailability)
.innerJoin(schema.accounts, eq(schema.modelAvailability.accountId, schema.accounts.id))