chore: import upstream snapshot with attribution
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,121 @@
|
||||
// Shared mock setup and lifecycle hooks for all HTTP provider test files.
|
||||
import { afterAll, afterEach, beforeEach, vi } from 'vitest';
|
||||
import { fetchWithCache } from '../../../src/cache';
|
||||
import { runPython } from '../../../src/python/pythonUtils';
|
||||
import { maybeLoadConfigFromExternalFile, maybeLoadFromExternalFile } from '../../../src/util/file';
|
||||
import { functionCache } from '../../../src/util/functions/loadFunction';
|
||||
|
||||
// Mock console.warn to prevent test noise
|
||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(function () {});
|
||||
|
||||
vi.mock('../../../src/cache', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../../src/cache')>('../../../src/cache');
|
||||
return {
|
||||
...actual,
|
||||
fetchWithCache: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/util/fetch/index.ts', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../../src/util/fetch/index.ts')>(
|
||||
'../../../src/util/fetch/index.ts',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
fetchWithRetries: vi.fn(),
|
||||
fetchWithTimeout: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/util/file', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../../src/util/file')>('../../../src/util/file');
|
||||
return {
|
||||
...actual,
|
||||
maybeLoadFromExternalFile: vi.fn((input) => input),
|
||||
maybeLoadConfigFromExternalFile: vi.fn((input) => input),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/esm', async (importOriginal) => {
|
||||
return {
|
||||
...(await importOriginal()),
|
||||
|
||||
importModule: vi.fn(async (_modulePath: string, functionName?: string) => {
|
||||
const mockModule: Record<string, ReturnType<typeof vi.fn>> = {
|
||||
default: vi.fn((data) => data.defaultField),
|
||||
parseResponse: vi.fn((data) => data.specificField),
|
||||
};
|
||||
if (functionName) {
|
||||
if (!(functionName in mockModule)) {
|
||||
throw new Error(
|
||||
`importModule mock: unexpected functionName "${functionName}". ` +
|
||||
'Add it to the mockModule in setup.ts or use mockResolvedValueOnce.',
|
||||
);
|
||||
}
|
||||
return mockModule[functionName];
|
||||
}
|
||||
return mockModule.default;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/cliState', async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import('../../../src/cliState')>('../../../src/cliState');
|
||||
const mockState = { basePath: '/mock/base/path', config: {} };
|
||||
return {
|
||||
...actual,
|
||||
...mockState,
|
||||
default: mockState,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../../src/python/pythonUtils', async () => {
|
||||
const actual = await vi.importActual<typeof import('../../../src/python/pythonUtils')>(
|
||||
'../../../src/python/pythonUtils',
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
runPython: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
// Mock jks-js module for JKS keystore tests in auth.test.ts - don't use importOriginal as the native module may fail to load
|
||||
vi.mock('jks-js', () => ({
|
||||
toPem: vi.fn(),
|
||||
default: {
|
||||
toPem: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
afterAll(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(fetchWithCache).mockReset();
|
||||
vi.mocked(maybeLoadFromExternalFile).mockReset();
|
||||
vi.mocked(maybeLoadConfigFromExternalFile).mockReset();
|
||||
vi.mocked(runPython).mockReset();
|
||||
vi.mocked(fetchWithCache).mockImplementation(() => {
|
||||
throw new Error(
|
||||
'fetchWithCache called without mockResolvedValueOnce setup. ' +
|
||||
'Add vi.mocked(fetchWithCache).mockResolvedValueOnce({...}) before calling provider.callApi().',
|
||||
);
|
||||
});
|
||||
vi.mocked(maybeLoadFromExternalFile).mockImplementation(function (input: unknown) {
|
||||
return input;
|
||||
});
|
||||
vi.mocked(maybeLoadConfigFromExternalFile).mockImplementation(function (input: unknown) {
|
||||
return input;
|
||||
});
|
||||
Object.keys(functionCache).forEach((key) => {
|
||||
delete functionCache[key];
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
@@ -0,0 +1,663 @@
|
||||
// Tools and template variable tests: tool_choice handling, transformToolsFormat integration.
|
||||
import './setup';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { fetchWithCache } from '../../../src/cache';
|
||||
import logger from '../../../src/logger';
|
||||
import { HttpProvider } from '../../../src/providers/http';
|
||||
|
||||
describe('tools and tool_choice template variables', () => {
|
||||
const mockUrl = 'http://example.com/api';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should make tools available as a template variable', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const tools = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
description: 'Get weather for a location',
|
||||
parameters: { type: 'object', properties: { location: { type: 'string' } } },
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
await provider.callApi('test prompt', {
|
||||
vars: {},
|
||||
prompt: {
|
||||
raw: 'test prompt',
|
||||
label: 'test',
|
||||
config: { tools },
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools,
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should make tool_choice available as a template variable', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const tool_choice = { type: 'function', function: { name: 'get_weather' } };
|
||||
|
||||
await provider.callApi('test prompt', {
|
||||
vars: {},
|
||||
prompt: {
|
||||
raw: 'test prompt',
|
||||
label: 'test',
|
||||
config: { tool_choice },
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tool_choice,
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle both tools and tool_choice together', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
const tools = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'report_scores',
|
||||
parameters: { type: 'object', properties: { score: { type: 'integer' } } },
|
||||
},
|
||||
},
|
||||
];
|
||||
const tool_choice = { type: 'function', function: { name: 'report_scores' } };
|
||||
|
||||
await provider.callApi('test prompt', {
|
||||
vars: {},
|
||||
prompt: {
|
||||
raw: 'test prompt',
|
||||
label: 'test',
|
||||
config: { tools, tool_choice },
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools,
|
||||
tool_choice,
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle undefined tools gracefully', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// No tools or tool_choice in config
|
||||
await provider.callApi('test prompt', {
|
||||
vars: {},
|
||||
prompt: {
|
||||
raw: 'test prompt',
|
||||
label: 'test',
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('transformToolsFormat integration', () => {
|
||||
const mockUrl = 'http://example.com/api';
|
||||
|
||||
it('should pass through OpenAI tools unchanged when format is openai', async () => {
|
||||
const openaiTools = [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
description: 'Get weather for a location',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
location: { type: 'string' },
|
||||
},
|
||||
required: ['location'],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
},
|
||||
tools: openaiTools,
|
||||
transformToolsFormat: 'openai',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: openaiTools,
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform OpenAI tools to anthropic format', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'search',
|
||||
description: 'Search the web',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
transformToolsFormat: 'anthropic',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: [
|
||||
{
|
||||
name: 'search',
|
||||
description: 'Search the web',
|
||||
input_schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform tool_choice to openai format', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
description: 'Get weather',
|
||||
},
|
||||
},
|
||||
],
|
||||
tool_choice: { type: 'function', function: { name: 'get_weather' } },
|
||||
transformToolsFormat: 'openai',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: [
|
||||
{
|
||||
type: 'function',
|
||||
function: {
|
||||
name: 'get_weather',
|
||||
description: 'Get weather',
|
||||
},
|
||||
},
|
||||
],
|
||||
tool_choice: { type: 'function', function: { name: 'get_weather' } },
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should transform tool_choice mode required to anthropic format', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
tools: [{ type: 'function', function: { name: 'my_tool' } }],
|
||||
tool_choice: 'required',
|
||||
transformToolsFormat: 'anthropic',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: [
|
||||
{
|
||||
name: 'my_tool',
|
||||
input_schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
tool_choice: { type: 'any' },
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should pass through non-OpenAI tools unchanged', async () => {
|
||||
const anthropicTools = [
|
||||
{
|
||||
name: 'existing_tool',
|
||||
description: 'Already in Anthropic format',
|
||||
input_schema: { type: 'object' },
|
||||
},
|
||||
];
|
||||
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
},
|
||||
tools: anthropicTools,
|
||||
transformToolsFormat: 'anthropic', // Won't transform - not in OpenAI format
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: anthropicTools, // Unchanged
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use tools from prompt.config over provider config', async () => {
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
},
|
||||
tools: [{ type: 'function', function: { name: 'provider_tool' } }],
|
||||
transformToolsFormat: 'anthropic',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
// prompt.config.tools should override provider config
|
||||
await provider.callApi('test prompt', {
|
||||
vars: {},
|
||||
prompt: {
|
||||
raw: 'test prompt',
|
||||
label: 'test',
|
||||
config: {
|
||||
tools: [
|
||||
{ type: 'function', function: { name: 'prompt_tool', description: 'From prompt' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fetchWithCache).toHaveBeenCalledWith(
|
||||
mockUrl,
|
||||
expect.objectContaining({
|
||||
body: JSON.stringify({
|
||||
messages: 'test prompt',
|
||||
tools: [
|
||||
{
|
||||
name: 'prompt_tool',
|
||||
description: 'From prompt',
|
||||
input_schema: { type: 'object', properties: {} },
|
||||
},
|
||||
],
|
||||
}),
|
||||
}),
|
||||
expect.any(Number),
|
||||
'text',
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should warn when tool_choice is set but tools is empty', async () => {
|
||||
const loggerWarnSpy = vi.spyOn(logger, 'warn');
|
||||
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
tool_choice: 'required',
|
||||
transformToolsFormat: 'openai',
|
||||
// No tools configured
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(loggerWarnSpy).toHaveBeenCalled();
|
||||
expect(loggerWarnSpy.mock.calls[0][0]).toEqual(
|
||||
expect.stringContaining('tool_choice is set but tools is empty'),
|
||||
);
|
||||
|
||||
loggerWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should warn when tool_choice is set but tools array is empty', async () => {
|
||||
const loggerWarnSpy = vi.spyOn(logger, 'warn');
|
||||
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
tools: [], // Empty array
|
||||
tool_choice: 'auto',
|
||||
transformToolsFormat: 'openai',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(loggerWarnSpy).toHaveBeenCalled();
|
||||
expect(loggerWarnSpy.mock.calls[0][0]).toEqual(
|
||||
expect.stringContaining('tool_choice is set but tools is empty'),
|
||||
);
|
||||
|
||||
loggerWarnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not warn when both tools and tool_choice are set', async () => {
|
||||
const loggerWarnSpy = vi.spyOn(logger, 'warn');
|
||||
|
||||
const provider = new HttpProvider(mockUrl, {
|
||||
config: {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: {
|
||||
messages: '{{ prompt }}',
|
||||
tools: '{{ tools }}',
|
||||
tool_choice: '{{ tool_choice }}',
|
||||
},
|
||||
tools: [{ normalized: true, name: 'my_tool' }],
|
||||
tool_choice: 'auto',
|
||||
transformToolsFormat: 'openai',
|
||||
},
|
||||
});
|
||||
|
||||
const mockResponse = {
|
||||
data: JSON.stringify({ result: 'success' }),
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
cached: false,
|
||||
};
|
||||
vi.mocked(fetchWithCache).mockResolvedValueOnce(mockResponse);
|
||||
|
||||
await provider.callApi('test prompt');
|
||||
|
||||
expect(
|
||||
loggerWarnSpy.mock.calls.some(([message]) =>
|
||||
String(message).includes('tool_choice is set but tools is empty'),
|
||||
),
|
||||
).toBe(false);
|
||||
|
||||
loggerWarnSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user