mirror of
https://github.com/nexu-io/open-design.git
synced 2026-05-31 19:04:39 +07:00
fix(daemon): skip chat probe for SenseAudio media models (#3181)
This commit is contained in:
parent
a4c447eea6
commit
693176457e
2 changed files with 217 additions and 0 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Reference in a new issue