fix(daemon): skip chat probe for SenseAudio media models (#3181)

This commit is contained in:
初晨 2026-05-28 16:52:34 +08:00 committed by GitHub
parent a4c447eea6
commit 693176457e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 217 additions and 0 deletions

View file

@ -859,6 +859,118 @@ async function validateLocalOpenAiModel(
};
}
function isSenseAudioNonChatModel(model: string): boolean {
return (
model.startsWith('senseaudio-image-') ||
model.startsWith('doubao-seedream-') ||
model === 'sensenova-u1-fast' ||
model.startsWith('doubao-seedance-') ||
model.startsWith('senseaudio-asr-') ||
model.startsWith('senseaudio-tts-') ||
model.startsWith('senseaudio-music-')
);
}
async function validateSenseAudioNonChatModel(
input: ProviderTestRequest,
signal: AbortSignal,
start: number,
requestInit: Pick<RequestInit, 'dispatcher'> = {},
): Promise<ConnectionTestResponse | null> {
if (input.protocol !== 'senseaudio' || !isSenseAudioNonChatModel(input.model)) {
return null;
}
const url = appendVersionedApiPath(String(input.baseUrl), '/models');
let response: Response;
try {
response = await fetch(url, {
...requestInit,
method: 'GET',
headers: { authorization: `Bearer ${String(input.apiKey)}` },
signal,
redirect: 'error',
});
} catch (err) {
const latencyMs = Date.now() - start;
const kind = networkErrorToKind(err);
return {
ok: false,
kind,
latencyMs,
model: input.model,
detail: redactSecrets(err instanceof Error ? err.message : String(err), [
input.apiKey,
]),
};
}
const latencyMs = Date.now() - start;
let rawText = '';
let data: unknown = {};
let parseError: unknown = null;
try {
rawText = await response.text();
} catch {
rawText = '';
}
try {
data = rawText ? JSON.parse(rawText) : {};
} catch (err) {
parseError = err;
}
if (parseError && response.ok) {
return {
ok: false,
kind: 'unknown',
latencyMs,
model: input.model,
status: response.status,
detail: redactSecrets(
parseError instanceof Error ? parseError.message : String(parseError),
[input.apiKey],
),
};
}
if (!response.ok) {
const redactedDetail = redactSecrets(
extractProviderErrorDetail(data, rawText).slice(0, 240),
[input.apiKey],
);
return {
ok: false,
kind: statusToKind(response.status, redactedDetail),
latencyMs,
model: input.model,
status: response.status,
detail: redactedDetail,
};
}
const modelIds = extractOpenAiModelIds(data);
if (!modelIds.includes(input.model)) {
return {
ok: false,
kind: 'not_found_model',
latencyMs,
model: input.model,
status: response.status,
detail: `Model "${input.model}" is not reported by SenseAudio /models.`,
};
}
return {
ok: true,
kind: 'success',
latencyMs,
model: input.model,
status: response.status,
detail: 'SenseAudio model is available, but this media model is not chat-testable from Settings.',
};
}
interface ProviderCallShape {
url: string;
headers: Record<string, string>;
@ -1084,6 +1196,14 @@ export async function testProviderConnection(
);
if (modelError) return modelError;
const senseAudioNonChatResult = await validateSenseAudioNonChatModel(
input,
controller.signal,
start,
proxyDispatcher.requestInit,
);
if (senseAudioNonChatResult) return senseAudioNonChatResult;
const requestInit = {
...proxyDispatcher.requestInit,
method: 'POST',

View file

@ -632,6 +632,103 @@ describe('POST /api/test/connection provider mode', () => {
);
});
it('checks SenseAudio non-chat model availability without probing chat completions', async () => {
const fetchMock = passThroughOrUpstream((url) => {
if (url === 'https://api.senseaudio.cn/v1/models') {
return jsonResponse({
data: [
{ id: 'doubao-1-5-pro-32k-250115' },
{ id: 'senseaudio-image-2.0-260319' },
],
});
}
return jsonResponse({ error: { message: 'unexpected endpoint' } }, { status: 500 });
});
vi.stubGlobal('fetch', fetchMock);
const res = await realFetch(`${baseUrl}/api/test/connection`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
mode: 'provider',
protocol: 'senseaudio',
baseUrl: 'https://api.senseaudio.cn',
apiKey: 'sense-key',
model: 'senseaudio-image-2.0-260319',
}),
});
const body = (await res.json()) as Record<string, unknown>;
expect(body.ok).toBe(true);
expect(body.kind).toBe('success');
expect(body.detail).toContain('not chat-testable');
expect(fetchMock).toHaveBeenCalledWith(
'https://api.senseaudio.cn/v1/models',
expect.objectContaining({ method: 'GET' }),
);
expect(fetchMock).not.toHaveBeenCalledWith(
'https://api.senseaudio.cn/v1/chat/completions',
expect.anything(),
);
});
it('returns not_found_model when a SenseAudio non-chat model is absent from /models', async () => {
vi.stubGlobal(
'fetch',
passThroughOrUpstream((url) => {
expect(url).toBe('https://api.senseaudio.cn/v1/models');
return jsonResponse({
data: [{ id: 'senseaudio-image-1.0-260319' }],
});
}),
);
const res = await realFetch(`${baseUrl}/api/test/connection`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
mode: 'provider',
protocol: 'senseaudio',
baseUrl: 'https://api.senseaudio.cn',
apiKey: 'sense-key',
model: 'senseaudio-image-2.0-260319',
}),
});
const body = (await res.json()) as Record<string, unknown>;
expect(body.ok).toBe(false);
expect(body.kind).toBe('not_found_model');
expect(body.detail).toContain('not reported by SenseAudio /models');
});
it('keeps SenseAudio chat models on the chat completions smoke test', async () => {
const fetchMock = passThroughOrUpstream((url) => {
if (url === 'https://api.senseaudio.cn/v1/chat/completions') {
return jsonResponse({
choices: [{ message: { content: 'ok' }, finish_reason: 'stop' }],
});
}
return jsonResponse({ error: { message: 'unexpected endpoint' } }, { status: 500 });
});
vi.stubGlobal('fetch', fetchMock);
const res = await realFetch(`${baseUrl}/api/test/connection`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
mode: 'provider',
protocol: 'senseaudio',
baseUrl: 'https://api.senseaudio.cn',
apiKey: 'sense-key',
model: 'doubao-1-5-pro-32k-250115',
}),
});
const body = (await res.json()) as Record<string, unknown>;
expect(body.ok).toBe(true);
expect(fetchMock).toHaveBeenCalledWith(
'https://api.senseaudio.cn/v1/chat/completions',
expect.objectContaining({ method: 'POST' }),
);
});
it('maps a 404 to not_found_model', async () => {
vi.stubGlobal(
'fetch',