promptfoo 0.17.4 → 0.17.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/main.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
3
3
  import { join as pathJoin, dirname } from 'path';
4
+ import readline from 'readline';
4
5
 
5
6
  import chalk from 'chalk';
6
7
  import { Command } from 'commander';
@@ -56,7 +57,11 @@ function createDummyFiles(directory: string | null) {
56
57
  writeFileSync(pathJoin(process.cwd(), directory, 'README.md'), DEFAULT_README);
57
58
 
58
59
  if (directory === '.') {
59
- logger.info(chalk.green.bold('Wrote prompts.txt and promptfooconfig.yaml. Open README.md to get started!'));
60
+ logger.info(
61
+ chalk.green.bold(
62
+ 'Wrote prompts.txt and promptfooconfig.yaml. Open README.md to get started!',
63
+ ),
64
+ );
60
65
  } else {
61
66
  logger.info(chalk.green.bold(`Wrote prompts.txt and promptfooconfig.yaml to ./${directory}`));
62
67
  logger.info(chalk.green(`\`cd ${directory}\` and open README.md to get started!`));
@@ -72,20 +77,20 @@ async function main() {
72
77
  pathJoin(pwd, 'promptfooconfig.json'),
73
78
  pathJoin(pwd, 'promptfooconfig.yaml'),
74
79
  ];
75
- let config: Partial<UnifiedConfig> = {};
80
+ let defaultConfig: Partial<UnifiedConfig> = {};
76
81
  for (const path of potentialPaths) {
77
82
  const maybeConfig = await maybeReadConfig(path);
78
83
  if (maybeConfig) {
79
- config = maybeConfig;
84
+ defaultConfig = maybeConfig;
80
85
  break;
81
86
  }
82
87
  }
83
88
 
84
89
  let evaluateOptions: EvaluateOptions = {};
85
- if (config.evaluateOptions) {
86
- evaluateOptions.generateSuggestions = config.evaluateOptions.generateSuggestions;
87
- evaluateOptions.maxConcurrency = config.evaluateOptions.maxConcurrency;
88
- evaluateOptions.showProgressBar = config.evaluateOptions.showProgressBar;
90
+ if (defaultConfig.evaluateOptions) {
91
+ evaluateOptions.generateSuggestions = defaultConfig.evaluateOptions.generateSuggestions;
92
+ evaluateOptions.maxConcurrency = defaultConfig.evaluateOptions.maxConcurrency;
93
+ evaluateOptions.showProgressBar = defaultConfig.evaluateOptions.showProgressBar;
89
94
  }
90
95
 
91
96
  const program = new Command();
@@ -126,20 +131,46 @@ async function main() {
126
131
  program
127
132
  .command('share')
128
133
  .description('Share your most recent result')
129
- .action(async (cmdObj: { port: number } & Command) => {
134
+ .option('-y, --yes', 'Skip confirmation')
135
+ .action(async (cmdObj: { yes: boolean } & Command) => {
130
136
  telemetry.maybeShowNotice();
131
137
  telemetry.record('command_used', {
132
138
  name: 'share',
133
139
  });
134
140
  await telemetry.send();
135
141
 
136
- const latestResults = readLatestResults();
137
- if (!latestResults) {
138
- logger.error('Could not load results. Do you need to run `promptfoo eval` first?');
139
- process.exit(1);
142
+ const createPublicUrl = async () => {
143
+ const latestResults = readLatestResults();
144
+ if (!latestResults) {
145
+ logger.error('Could not load results. Do you need to run `promptfoo eval` first?');
146
+ process.exit(1);
147
+ }
148
+ const url = await createShareableUrl(latestResults.results, latestResults.config);
149
+ logger.info(`View results: ${chalk.greenBright.bold(url)}`);
150
+ };
151
+
152
+ if (cmdObj.yes || process.env.PROMPTFOO_DISABLE_SHARE_WARNING) {
153
+ createPublicUrl();
154
+ } else {
155
+ const reader = readline.createInterface({
156
+ input: process.stdin,
157
+ output: process.stdout,
158
+ });
159
+
160
+ reader.question(
161
+ 'Are you sure you want to create a public URL? [y/N] ',
162
+ async function (answer: string) {
163
+ if (answer.toLowerCase() !== 'yes' && answer.toLowerCase() !== 'y') {
164
+ logger.info('Did not create a public URL.');
165
+ reader.close();
166
+ return;
167
+ }
168
+ reader.close();
169
+
170
+ createPublicUrl();
171
+ },
172
+ );
140
173
  }
141
- const url = await createShareableUrl(latestResults.results, latestResults.config);
142
- logger.info(`View results: ${chalk.greenBright.bold(url)}`);
143
174
  });
144
175
 
145
176
  program
@@ -159,28 +190,32 @@ async function main() {
159
190
  program
160
191
  .command('eval')
161
192
  .description('Evaluate prompts')
162
- .option('-p, --prompts <paths...>', 'Paths to prompt files (.txt)', config.prompts)
193
+ .option('-p, --prompts <paths...>', 'Paths to prompt files (.txt)')
163
194
  .option(
164
195
  '-r, --providers <name or path...>',
165
196
  'One of: openai:chat, openai:completion, openai:<model name>, or path to custom API caller module',
166
197
  )
167
198
  .option(
168
199
  '-c, --config <path>',
169
- 'Path to configuration file. Automatically loads promptfooconfig.js/json/yaml',
200
+ 'Path to configuration file. Automatically loads promptfoodefaultConfig.js/json/yaml',
170
201
  )
171
202
  .option(
172
203
  // TODO(ian): Remove `vars` for v1
173
204
  '-v, --vars, -t, --tests <path>',
174
205
  'Path to CSV with test cases',
175
- config?.commandLineOptions?.vars,
206
+ defaultConfig?.commandLineOptions?.vars,
207
+ )
208
+ .option('-t, --tests <path>', 'Path to CSV with test cases')
209
+ .option(
210
+ '-o, --output <path>',
211
+ 'Path to output file (csv, json, yaml, html)',
212
+ defaultConfig.outputPath,
176
213
  )
177
- .option('-t, --tests <path>', 'Path to CSV with test cases', config?.commandLineOptions?.tests)
178
- .option('-o, --output <path>', 'Path to output file (csv, json, yaml, html)', config.outputPath)
179
214
  .option(
180
215
  '-j, --max-concurrency <number>',
181
216
  'Maximum number of concurrent API calls',
182
- config.evaluateOptions?.maxConcurrency
183
- ? String(config.evaluateOptions.maxConcurrency)
217
+ defaultConfig.evaluateOptions?.maxConcurrency
218
+ ? String(defaultConfig.evaluateOptions.maxConcurrency)
184
219
  : undefined,
185
220
  )
186
221
  .option(
@@ -195,28 +230,28 @@ async function main() {
195
230
  .option(
196
231
  '--prompt-prefix <path>',
197
232
  'This prefix is prepended to every prompt',
198
- config.defaultTest?.options?.prefix,
233
+ defaultConfig.defaultTest?.options?.prefix,
199
234
  )
200
235
  .option(
201
236
  '--prompt-suffix <path>',
202
237
  'This suffix is append to every prompt',
203
- config.defaultTest?.options?.suffix,
238
+ defaultConfig.defaultTest?.options?.suffix,
204
239
  )
205
240
  .option(
206
241
  '--no-write',
207
242
  'Do not write results to promptfoo directory',
208
- config?.commandLineOptions?.write,
243
+ defaultConfig?.commandLineOptions?.write,
209
244
  )
210
245
  .option(
211
246
  '--no-cache',
212
247
  'Do not read or write results to disk cache',
213
- config?.commandLineOptions?.cache,
248
+ defaultConfig?.commandLineOptions?.cache,
214
249
  )
215
250
  .option('--no-progress-bar', 'Do not show progress bar')
216
- .option('--no-table', 'Do not output table in CLI', config?.commandLineOptions?.table)
217
- .option('--share', 'Create a shareable URL', config?.commandLineOptions?.share)
218
- .option('--grader', 'Model that will grade outputs', config?.commandLineOptions?.grader)
219
- .option('--verbose', 'Show debug logs', config?.commandLineOptions?.verbose)
251
+ .option('--no-table', 'Do not output table in CLI', defaultConfig?.commandLineOptions?.table)
252
+ .option('--share', 'Create a shareable URL', defaultConfig?.commandLineOptions?.share)
253
+ .option('--grader', 'Model that will grade outputs', defaultConfig?.commandLineOptions?.grader)
254
+ .option('--verbose', 'Show debug logs', defaultConfig?.commandLineOptions?.verbose)
220
255
  .option('--view [port]', 'View in browser ui')
221
256
  .action(async (cmdObj: CommandLineOptions & Command) => {
222
257
  // Misc settings
@@ -229,15 +264,20 @@ async function main() {
229
264
 
230
265
  // Config parsing
231
266
  const maxConcurrency = parseInt(cmdObj.maxConcurrency || '', 10);
267
+ let fileConfig: Partial<UnifiedConfig> = {};
232
268
  const configPath = cmdObj.config;
233
269
  if (configPath) {
234
- config = await readConfig(configPath);
270
+ fileConfig = await readConfig(configPath);
235
271
  }
236
- config = {
237
- prompts: cmdObj.prompts || config.prompts,
238
- providers: cmdObj.providers || config.providers,
239
- tests: cmdObj.tests || cmdObj.vars || config.tests,
240
- defaultTest: config.defaultTest,
272
+ const config: Partial<UnifiedConfig> = {
273
+ prompts: cmdObj.prompts || fileConfig.prompts || defaultConfig.prompts,
274
+ providers: cmdObj.providers || fileConfig.providers || defaultConfig.providers,
275
+ tests: cmdObj.tests || cmdObj.vars || fileConfig.tests || defaultConfig.tests,
276
+ sharing:
277
+ process.env.PROMPTFOO_DISABLE_SHARING === '1'
278
+ ? false
279
+ : fileConfig.sharing ?? defaultConfig.sharing ?? true,
280
+ defaultTest: fileConfig.defaultTest,
241
281
  };
242
282
 
243
283
  // Validation
@@ -305,7 +345,8 @@ async function main() {
305
345
 
306
346
  const summary = await evaluate(testSuite, options);
307
347
 
308
- const shareableUrl = cmdObj.share ? await createShareableUrl(summary, config) : null;
348
+ const shareableUrl =
349
+ cmdObj.share && config.sharing ? await createShareableUrl(summary, config) : null;
309
350
 
310
351
  if (cmdObj.output) {
311
352
  logger.info(chalk.yellow(`Writing output to ${cmdObj.output}`));
@@ -25,7 +25,7 @@ export class AnthropicCompletionProvider implements ApiProvider {
25
25
  constructor(modelName: string, apiKey?: string, context?: AnthropicCompletionOptions) {
26
26
  this.modelName = modelName;
27
27
  this.apiKey = apiKey || process.env.ANTHROPIC_API_KEY;
28
- this.anthropic = new Anthropic({apiKey: this.apiKey});
28
+ this.anthropic = new Anthropic({ apiKey: this.apiKey });
29
29
  this.options = context || {};
30
30
  }
31
31
 
@@ -0,0 +1,280 @@
1
+ import logger from '../logger';
2
+ import { fetchJsonWithCache } from '../cache';
3
+ import { REQUEST_TIMEOUT_MS } from './shared';
4
+
5
+ import type { ApiProvider, ProviderEmbeddingResponse, ProviderResponse } from '../types.js';
6
+
7
+ interface AzureOpenAiCompletionOptions {
8
+ temperature?: number;
9
+ functions?: {
10
+ name: string;
11
+ description?: string;
12
+ parameters: any;
13
+ }[];
14
+ function_call?: 'none' | 'auto';
15
+ }
16
+
17
+ class AzureOpenAiGenericProvider implements ApiProvider {
18
+ deploymentName: string;
19
+ apiKey?: string;
20
+ apiHost?: string;
21
+
22
+ constructor(deploymentName: string, apiKey?: string) {
23
+ this.deploymentName = deploymentName;
24
+
25
+ this.apiKey = apiKey || process.env.AZURE_OPENAI_API_KEY;
26
+
27
+ this.apiHost = process.env.AZURE_OPENAI_API_HOST;
28
+ }
29
+
30
+ id(): string {
31
+ return `azureopenai:${this.deploymentName}`;
32
+ }
33
+
34
+ toString(): string {
35
+ return `[Azure OpenAI Provider ${this.deploymentName}]`;
36
+ }
37
+
38
+ // @ts-ignore: Prompt is not used in this implementation
39
+ async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
40
+ throw new Error('Not implemented');
41
+ }
42
+ }
43
+
44
+ export class AzureOpenAiEmbeddingProvider extends AzureOpenAiGenericProvider {
45
+ async callEmbeddingApi(text: string): Promise<ProviderEmbeddingResponse> {
46
+ if (!this.apiKey) {
47
+ throw new Error('Azure OpenAI API key must be set for similarity comparison');
48
+ }
49
+ if (!this.apiHost) {
50
+ throw new Error('Azure OpenAI API host must be set');
51
+ }
52
+
53
+ const body = {
54
+ input: text,
55
+ model: this.deploymentName,
56
+ };
57
+ let data,
58
+ cached = false;
59
+ try {
60
+ ({ data, cached } = (await fetchJsonWithCache(
61
+ `https://${this.apiHost}/openai/deployments/${this.deploymentName}/embeddings?api-version=2023-07-01-preview`,
62
+ {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ 'api-key': this.apiKey,
67
+ },
68
+ body: JSON.stringify(body),
69
+ },
70
+ REQUEST_TIMEOUT_MS,
71
+ )) as unknown as any);
72
+ } catch (err) {
73
+ return {
74
+ error: `API call error: ${String(err)}`,
75
+ tokenUsage: {
76
+ total: 0,
77
+ prompt: 0,
78
+ completion: 0,
79
+ },
80
+ };
81
+ }
82
+ logger.debug(`\tAzure OpenAI API response (embeddings): ${JSON.stringify(data)}`);
83
+
84
+ try {
85
+ const embedding = data?.data?.[0]?.embedding;
86
+ if (!embedding) {
87
+ throw new Error('No embedding returned');
88
+ }
89
+ const ret = {
90
+ embedding,
91
+ tokenUsage: cached
92
+ ? { cached: data.usage.total_tokens }
93
+ : {
94
+ total: data.usage.total_tokens,
95
+ prompt: data.usage.prompt_tokens,
96
+ completion: data.usage.completion_tokens,
97
+ },
98
+ };
99
+ return ret;
100
+ } catch (err) {
101
+ return {
102
+ error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
103
+ tokenUsage: {
104
+ total: data?.usage?.total_tokens,
105
+ prompt: data?.usage?.prompt_tokens,
106
+ completion: data?.usage?.completion_tokens,
107
+ },
108
+ };
109
+ }
110
+ }
111
+ }
112
+
113
+ export class AzureOpenAiCompletionProvider extends AzureOpenAiGenericProvider {
114
+ options: AzureOpenAiCompletionOptions;
115
+
116
+ constructor(deploymentName: string, apiKey?: string, context?: AzureOpenAiCompletionOptions) {
117
+ super(deploymentName, apiKey);
118
+ this.options = context || {};
119
+ }
120
+
121
+ async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
122
+ if (!this.apiKey) {
123
+ throw new Error(
124
+ 'Azure OpenAI API key is not set. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
125
+ );
126
+ }
127
+ if (!this.apiHost) {
128
+ throw new Error('Azure OpenAI API host must be set');
129
+ }
130
+
131
+ let stop: string;
132
+ try {
133
+ stop = process.env.OPENAI_STOP
134
+ ? JSON.parse(process.env.OPENAI_STOP)
135
+ : ['<|im_end|>', '<|endoftext|>'];
136
+ } catch (err) {
137
+ throw new Error(`OPENAI_STOP is not a valid JSON string: ${err}`);
138
+ }
139
+ const body = {
140
+ model: this.deploymentName,
141
+ prompt,
142
+ max_tokens: parseInt(process.env.OPENAI_MAX_TOKENS || '1024'),
143
+ temperature:
144
+ options?.temperature ??
145
+ this.options.temperature ??
146
+ parseFloat(process.env.OPENAI_TEMPERATURE || '0'),
147
+ stop,
148
+ };
149
+ logger.debug(`Calling Azure OpenAI API: ${JSON.stringify(body)}`);
150
+ let data,
151
+ cached = false;
152
+ try {
153
+ ({ data, cached } = (await fetchJsonWithCache(
154
+ `https://${this.apiHost}/openai/deployments/${this.deploymentName}/completions?api-version=2023-07-01-preview`,
155
+ {
156
+ method: 'POST',
157
+ headers: {
158
+ 'Content-Type': 'application/json',
159
+ 'api-key': this.apiKey,
160
+ },
161
+ body: JSON.stringify(body),
162
+ },
163
+ REQUEST_TIMEOUT_MS,
164
+ )) as unknown as any);
165
+ } catch (err) {
166
+ return {
167
+ error: `API call error: ${String(err)}`,
168
+ };
169
+ }
170
+ logger.debug(`\tAzure OpenAI API response: ${JSON.stringify(data)}`);
171
+ try {
172
+ return {
173
+ output: data.choices[0].text,
174
+ tokenUsage: cached
175
+ ? { cached: data.usage.total_tokens }
176
+ : {
177
+ total: data.usage.total_tokens,
178
+ prompt: data.usage.prompt_tokens,
179
+ completion: data.usage.completion_tokens,
180
+ },
181
+ };
182
+ } catch (err) {
183
+ return {
184
+ error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
185
+ };
186
+ }
187
+ }
188
+ }
189
+
190
+ export class AzureOpenAiChatCompletionProvider extends AzureOpenAiGenericProvider {
191
+ options: AzureOpenAiCompletionOptions;
192
+
193
+ constructor(deploymentName: string, apiKey?: string, context?: AzureOpenAiCompletionOptions) {
194
+ super(deploymentName, apiKey);
195
+ this.options = context || {};
196
+ }
197
+
198
+ async callApi(prompt: string, options?: AzureOpenAiCompletionOptions): Promise<ProviderResponse> {
199
+ if (!this.apiKey) {
200
+ throw new Error(
201
+ 'Azure OpenAI API key is not set. Set AZURE_OPENAI_API_KEY environment variable or pass it as an argument to the constructor.',
202
+ );
203
+ }
204
+ if (!this.apiHost) {
205
+ throw new Error('Azure OpenAI API host must be set');
206
+ }
207
+
208
+ let messages: { role: string; content: string; name?: string }[];
209
+ try {
210
+ messages = JSON.parse(prompt) as { role: string; content: string }[];
211
+ } catch (err) {
212
+ const trimmedPrompt = prompt.trim();
213
+ if (
214
+ process.env.PROMPTFOO_REQUIRE_JSON_PROMPTS ||
215
+ trimmedPrompt.startsWith('{') ||
216
+ trimmedPrompt.startsWith('[')
217
+ ) {
218
+ throw new Error(
219
+ `Azure OpenAI Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`,
220
+ );
221
+ }
222
+ messages = [{ role: 'user', content: prompt }];
223
+ }
224
+
225
+ const body = {
226
+ model: this.deploymentName,
227
+ messages: messages,
228
+ max_tokens: parseInt(process.env.OPENAI_MAX_TOKENS || '1024'),
229
+ temperature:
230
+ options?.temperature ??
231
+ this.options.temperature ??
232
+ parseFloat(process.env.OPENAI_TEMPERATURE || '0'),
233
+ functions: options?.functions || this.options.functions || undefined,
234
+ function_call: options?.function_call || this.options.function_call || undefined,
235
+ };
236
+ logger.debug(`Calling Azure OpenAI API: ${JSON.stringify(body)}`);
237
+
238
+ let data,
239
+ cached = false;
240
+ try {
241
+ ({ data, cached } = (await fetchJsonWithCache(
242
+ `https://${this.apiHost}/openai/deployments/${this.deploymentName}/chat/completions?api-version=2023-07-01-preview`,
243
+ {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Content-Type': 'application/json',
247
+ 'api-key': this.apiKey,
248
+ },
249
+ body: JSON.stringify(body),
250
+ },
251
+ REQUEST_TIMEOUT_MS,
252
+ )) as unknown as any);
253
+ } catch (err) {
254
+ return {
255
+ error: `API call error: ${String(err)}`,
256
+ };
257
+ }
258
+
259
+ logger.debug(`\tAzure OpenAI API response: ${JSON.stringify(data)}`);
260
+ try {
261
+ const message = data.choices[0].message;
262
+ const output =
263
+ message.content === null ? JSON.stringify(message.function_call) : message.content;
264
+ return {
265
+ output,
266
+ tokenUsage: cached
267
+ ? { cached: data.usage.total_tokens }
268
+ : {
269
+ total: data.usage.total_tokens,
270
+ prompt: data.usage.prompt_tokens,
271
+ completion: data.usage.completion_tokens,
272
+ },
273
+ };
274
+ } catch (err) {
275
+ return {
276
+ error: `API response error: ${String(err)}: ${JSON.stringify(data)}`,
277
+ };
278
+ }
279
+ }
280
+ }
@@ -1,3 +1,5 @@
1
+ import yaml from 'js-yaml';
2
+
1
3
  import logger from '../logger';
2
4
  import { fetchJsonWithCache } from '../cache';
3
5
  import { REQUEST_TIMEOUT_MS } from './shared';
@@ -226,20 +228,34 @@ export class OpenAiChatCompletionProvider extends OpenAiGenericProvider {
226
228
  }
227
229
 
228
230
  let messages: { role: string; content: string; name?: string }[];
229
- try {
230
- messages = JSON.parse(prompt) as { role: string; content: string }[];
231
- } catch (err) {
232
- const trimmedPrompt = prompt.trim();
233
- if (
234
- process.env.PROMPTFOO_REQUIRE_JSON_PROMPTS ||
235
- trimmedPrompt.startsWith('{') ||
236
- trimmedPrompt.startsWith('[')
237
- ) {
231
+ const trimmedPrompt = prompt.trim();
232
+ if (trimmedPrompt.startsWith('- role:')) {
233
+ try {
234
+ // Try YAML
235
+ messages = yaml.load(prompt) as { role: string; content: string }[];
236
+ } catch (err) {
238
237
  throw new Error(
239
- `OpenAI Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`,
238
+ `OpenAI Chat Completion prompt is not a valid YAML string: ${err}\n\n${prompt}`,
240
239
  );
241
240
  }
242
- messages = [{ role: 'user', content: prompt }];
241
+ } else {
242
+ try {
243
+ // Try JSON
244
+ messages = JSON.parse(prompt) as { role: string; content: string }[];
245
+ } catch (err) {
246
+ if (
247
+ process.env.PROMPTFOO_REQUIRE_JSON_PROMPTS ||
248
+ trimmedPrompt.startsWith('{') ||
249
+ trimmedPrompt.startsWith('[')
250
+ ) {
251
+ throw new Error(
252
+ `OpenAI Chat Completion prompt is not a valid JSON string: ${err}\n\n${prompt}`,
253
+ );
254
+ }
255
+
256
+ // Fall back to wrapping the prompt in a user message
257
+ messages = [{ role: 'user', content: prompt }];
258
+ }
243
259
  }
244
260
 
245
261
  const body = {
package/src/providers.ts CHANGED
@@ -6,6 +6,10 @@ import { OpenAiCompletionProvider, OpenAiChatCompletionProvider } from './provid
6
6
  import { AnthropicCompletionProvider } from './providers/anthropic';
7
7
  import { LocalAiCompletionProvider, LocalAiChatProvider } from './providers/localai';
8
8
  import { ScriptCompletionProvider } from './providers/scriptCompletion';
9
+ import {
10
+ AzureOpenAiChatCompletionProvider,
11
+ AzureOpenAiCompletionProvider,
12
+ } from './providers/azureopenai';
9
13
 
10
14
  export async function loadApiProviders(
11
15
  providerPaths: ProviderId | ProviderId[] | RawProviderConfig[],
@@ -68,6 +72,21 @@ export async function loadApiProvider(
68
72
  `Unknown OpenAI model type: ${modelType}. Use one of the following providers: openai:chat:<model name>, openai:completion:<model name>`,
69
73
  );
70
74
  }
75
+ } else if (providerPath?.startsWith('azureopenai:')) {
76
+ // Load Azure OpenAI module
77
+ const options = providerPath.split(':');
78
+ const modelType = options[1];
79
+ const deploymentName = options[2];
80
+
81
+ if (modelType === 'chat') {
82
+ return new AzureOpenAiChatCompletionProvider(deploymentName, undefined, context?.config);
83
+ } else if (modelType === 'completion') {
84
+ return new AzureOpenAiCompletionProvider(deploymentName, undefined, context?.config);
85
+ } else {
86
+ throw new Error(
87
+ `Unknown Azure OpenAI model type: ${modelType}. Use one of the following providers: openai:chat:<model name>, openai:completion:<model name>`,
88
+ );
89
+ }
71
90
  } else if (providerPath?.startsWith('anthropic:')) {
72
91
  // Load Anthropic module
73
92
  const options = providerPath.split(':');
package/src/types.ts CHANGED
@@ -136,6 +136,7 @@ type BaseAssertionTypes =
136
136
  | 'icontains'
137
137
  | 'contains-all'
138
138
  | 'contains-any'
139
+ | 'starts-with'
139
140
  | 'regex'
140
141
  | 'is-json'
141
142
  | 'contains-json'
@@ -230,6 +231,9 @@ export interface TestSuiteConfig {
230
231
 
231
232
  // Path to write output. Writes to console/web viewer if not set.
232
233
  outputPath?: string;
234
+
235
+ // Determines whether or not sharing is enabled.
236
+ sharing?: boolean;
233
237
  }
234
238
 
235
239
  export type UnifiedConfig = TestSuiteConfig & {
package/src/updates.ts CHANGED
@@ -17,6 +17,10 @@ export async function getLatestVersion(packageName: string) {
17
17
  }
18
18
 
19
19
  export async function checkForUpdates(): Promise<boolean> {
20
+ if (process.env.PROMPTFOO_DISABLE_UPDATE) {
21
+ return false;
22
+ }
23
+
20
24
  let latestVersion: string;
21
25
  try {
22
26
  latestVersion = await getLatestVersion('promptfoo');
@@ -218,16 +218,18 @@ export default function ResultsView() {
218
218
  </Button>
219
219
  </Tooltip>
220
220
  )}
221
- <Tooltip title="Generate a unique URL that others can access">
222
- <Button
223
- color="primary"
224
- onClick={handleShareButtonClick}
225
- disabled={shareLoading}
226
- startIcon={shareLoading ? <CircularProgress size={16} /> : <ShareIcon />}
227
- >
228
- Share
229
- </Button>
230
- </Tooltip>
221
+ {config?.sharing && (
222
+ <Tooltip title="Generate a unique URL that others can access">
223
+ <Button
224
+ color="primary"
225
+ onClick={handleShareButtonClick}
226
+ disabled={shareLoading}
227
+ startIcon={shareLoading ? <CircularProgress size={16} /> : <ShareIcon />}
228
+ >
229
+ Share
230
+ </Button>
231
+ </Tooltip>
232
+ )}
231
233
  </ResponsiveStack>
232
234
  </Box>
233
235
  </ResponsiveStack>