192 lines
5.9 KiB
TypeScript
192 lines
5.9 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import {
|
|
ModelWebSearchError,
|
|
createModelWebSearchAdapter,
|
|
type FrozenSelectedModel,
|
|
} from '../../electron/coding-runtime/pi/model-tools/web-search';
|
|
|
|
const selectedModel: FrozenSelectedModel = {
|
|
accountId: 'niancode-user-models',
|
|
runtimeProviderId: 'makelore-account-1',
|
|
modelId: 'deepseek-v4-pro',
|
|
generation: 7,
|
|
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
|
headers: {
|
|
Authorization: 'Bearer frozen-token',
|
|
'X-Works-Square-AI-Token': 'frozen-token',
|
|
},
|
|
capability: {
|
|
schemaVersion: 1,
|
|
adapter: 'bailian-chat-completions',
|
|
supportsForcedSearch: true,
|
|
sourceMode: 'inline-or-structured',
|
|
billingAuthority: 'model-request',
|
|
},
|
|
};
|
|
|
|
function jsonResponse(body: unknown, init?: ResponseInit): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
...init,
|
|
headers: { 'content-type': 'application/json', ...init?.headers },
|
|
});
|
|
}
|
|
|
|
describe('model Web Search adapter', () => {
|
|
it('uses the frozen Bailian model transport with mandatory search directives', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
|
|
choices: [{
|
|
message: { content: 'Flybird is a Flappy Bird-style game.' },
|
|
}],
|
|
}));
|
|
const adapter = createModelWebSearchAdapter({ fetchImpl });
|
|
|
|
await expect(adapter.search({
|
|
query: ' flybird 玩法 ',
|
|
selectedModel,
|
|
parentTurnId: 'turn-1',
|
|
toolCallId: 'tool-1',
|
|
}, new AbortController().signal)).resolves.toEqual({
|
|
schema: 'makelore-model-tool.v1',
|
|
tool: 'web_search',
|
|
status: 'succeeded',
|
|
modelId: 'deepseek-v4-pro',
|
|
answer: 'Flybird is a Flappy Bird-style game.',
|
|
sources: [],
|
|
sourceMode: 'inline-or-structured',
|
|
});
|
|
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
expect(fetchImpl.mock.calls[0][0]).toBe(
|
|
'http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions',
|
|
);
|
|
expect(fetchImpl.mock.calls[0][1]).toMatchObject({
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: 'Bearer frozen-token',
|
|
'X-Works-Square-AI-Token': 'frozen-token',
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
model: 'deepseek-v4-pro',
|
|
messages: [{ role: 'user', content: 'flybird 玩法' }],
|
|
stream: false,
|
|
enable_search: true,
|
|
search_options: { forced_search: true },
|
|
}),
|
|
});
|
|
});
|
|
|
|
it('normalizes and stably deduplicates structured sources when present', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
|
|
choices: [{
|
|
message: {
|
|
content: 'Grounded answer',
|
|
sources: [
|
|
{ title: ' First ', url: 'https://example.test/page#one' },
|
|
{ title: 'Duplicate', url: 'https://example.test/page#two' },
|
|
{ title: 'Second', url: 'https://second.test/' },
|
|
],
|
|
},
|
|
}],
|
|
}));
|
|
const adapter = createModelWebSearchAdapter({ fetchImpl });
|
|
|
|
const result = await adapter.search({
|
|
query: 'query',
|
|
selectedModel,
|
|
parentTurnId: 'turn-1',
|
|
toolCallId: 'tool-1',
|
|
}, new AbortController().signal);
|
|
|
|
expect(result.sources).toEqual([
|
|
{ title: 'First', url: 'https://example.test/page#one' },
|
|
{ title: 'Second', url: 'https://second.test/' },
|
|
]);
|
|
});
|
|
|
|
it('uses the native Responses tool and normalizes URL citations', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse({
|
|
output_text: 'Current answer',
|
|
output: [{
|
|
type: 'message',
|
|
content: [{
|
|
type: 'output_text',
|
|
text: 'Current answer',
|
|
annotations: [{
|
|
type: 'url_citation',
|
|
title: 'Source',
|
|
url: 'https://example.test/source',
|
|
}],
|
|
}],
|
|
}],
|
|
}));
|
|
const adapter = createModelWebSearchAdapter({ fetchImpl });
|
|
|
|
const result = await adapter.search({
|
|
query: 'latest fact',
|
|
selectedModel: {
|
|
...selectedModel,
|
|
capability: {
|
|
...selectedModel.capability,
|
|
adapter: 'openai-responses',
|
|
sourceMode: 'structured',
|
|
},
|
|
},
|
|
parentTurnId: 'turn-2',
|
|
toolCallId: 'tool-2',
|
|
}, new AbortController().signal);
|
|
|
|
expect(JSON.parse(String((fetchImpl.mock.calls[0][1] as RequestInit).body))).toEqual({
|
|
model: 'deepseek-v4-pro',
|
|
input: 'latest fact',
|
|
tools: [{ type: 'web_search' }],
|
|
tool_choice: 'required',
|
|
});
|
|
expect(fetchImpl.mock.calls[0][0]).toBe(
|
|
'http://127.0.0.1:13210/api/ai-proxy/v1/responses',
|
|
);
|
|
expect(result.sources).toEqual([
|
|
{ title: 'Source', url: 'https://example.test/source' },
|
|
]);
|
|
});
|
|
|
|
it('maps rate limiting without switching model or transport', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>().mockResolvedValue(jsonResponse(
|
|
{ error: { message: 'slow down' } },
|
|
{ status: 429 },
|
|
));
|
|
const adapter = createModelWebSearchAdapter({ fetchImpl });
|
|
|
|
await expect(adapter.search({
|
|
query: 'query',
|
|
selectedModel,
|
|
parentTurnId: 'turn-1',
|
|
toolCallId: 'tool-1',
|
|
}, new AbortController().signal)).rejects.toMatchObject({
|
|
code: 'model_web_search_rate_limited',
|
|
status: 429,
|
|
retryable: false,
|
|
} satisfies Partial<ModelWebSearchError>);
|
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('fails closed before transport for an unsupported capability', async () => {
|
|
const fetchImpl = vi.fn<typeof fetch>();
|
|
const adapter = createModelWebSearchAdapter({ fetchImpl });
|
|
|
|
await expect(adapter.search({
|
|
query: 'query',
|
|
selectedModel: {
|
|
...selectedModel,
|
|
capability: undefined,
|
|
},
|
|
parentTurnId: 'turn-1',
|
|
toolCallId: 'tool-1',
|
|
}, new AbortController().signal)).rejects.toMatchObject({
|
|
code: 'model_web_search_unsupported',
|
|
status: 400,
|
|
} satisfies Partial<ModelWebSearchError>);
|
|
expect(fetchImpl).not.toHaveBeenCalled();
|
|
});
|
|
});
|