Files
promptfoo--promptfoo/plugins/promptfoo/skills/promptfoo-provider-setup/scripts/openapi-operation-to-config.mjs
T
wehub-resource-sync 0d3cb498a3
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
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

943 lines
31 KiB
JavaScript

import fs from 'node:fs';
import * as yaml from 'js-yaml';
const HTTP_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete']);
const PROMPT_FIELDS = new Set(['message', 'prompt', 'q', 'query', 'question', 'input', 'text']);
function usage(message) {
if (message) {
console.error(message);
}
console.error(
'Usage: node openapi-operation-to-config.mjs --spec openapi.yaml --operation-id op --base-url-env API_BASE_URL [--token-env API_TOKEN] [--auth-header Authorization] [--auth-prefix Bearer|none|custom] [--label label] [--output promptfooconfig.yaml]',
);
process.exit(1);
}
function parseArgs(argv) {
const args = {};
for (let index = 0; index < argv.length; index += 2) {
const key = argv[index];
const value = argv[index + 1];
if (!key?.startsWith('--') || value === undefined) {
usage(`Invalid argument near ${key ?? '<end>'}`);
}
args[key.slice(2)] = value;
}
return args;
}
function asRecord(value, context) {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
usage(`${context} must be an object`);
}
return value;
}
function resolveRef(document, schema, seen = new Set()) {
if (!schema?.$ref) {
return schema;
}
if (!schema.$ref.startsWith('#/')) {
usage(`Only local OpenAPI refs are supported: ${schema.$ref}`);
}
if (seen.has(schema.$ref)) {
usage(`Circular OpenAPI ref detected: ${schema.$ref}`);
}
const activeRefs = new Set(seen);
activeRefs.add(schema.$ref);
const parts = schema.$ref
.replace(/^#\//, '')
.split('/')
.map((part) => part.replaceAll('~1', '/').replaceAll('~0', '~'));
let current = document;
for (const part of parts) {
const currentRecord = asRecord(current, schema.$ref);
if (!(part in currentRecord)) {
usage(`OpenAPI ref not found: ${schema.$ref}`);
}
current = currentRecord[part];
}
return resolveRef(document, current, activeRefs);
}
function getJsonMediaEntry(document, content) {
const contentRecord = asRecord(content || {}, 'content');
const exactJson = contentRecord['application/json'];
const entry = exactJson
? ['application/json', exactJson]
: Object.entries(contentRecord).find(([mediaType]) => isJsonMediaType(mediaType));
return entry
? { mediaType: entry[0], media: asRecord(resolveRef(document, entry[1]), entry[0]) }
: undefined;
}
function getRequestMediaEntry(document, content) {
const contentRecord = asRecord(content || {}, 'content');
const jsonEntry = getJsonMediaEntry(document, contentRecord);
if (jsonEntry) {
return jsonEntry;
}
const formEntry = Object.entries(contentRecord).find(([mediaType]) =>
isFormUrlEncodedMediaType(mediaType),
);
if (formEntry) {
return {
mediaType: formEntry[0],
media: asRecord(resolveRef(document, formEntry[1]), formEntry[0]),
};
}
const multipartEntry = Object.entries(contentRecord).find(([mediaType]) =>
isMultipartMediaType(mediaType),
);
if (multipartEntry) {
return {
mediaType: multipartEntry[0],
media: asRecord(resolveRef(document, multipartEntry[1]), multipartEntry[0]),
};
}
const textEntry = Object.entries(contentRecord).find(([mediaType]) => isTextMediaType(mediaType));
return textEntry
? { mediaType: textEntry[0], media: asRecord(resolveRef(document, textEntry[1]), textEntry[0]) }
: undefined;
}
function isJsonMediaType(mediaType) {
const normalized = mediaType.split(';', 1)[0].trim().toLowerCase();
return (
normalized === 'application/json' ||
normalized.endsWith('/json') ||
normalized.endsWith('+json')
);
}
function isFormUrlEncodedMediaType(mediaType) {
return mediaType.split(';', 1)[0].trim().toLowerCase() === 'application/x-www-form-urlencoded';
}
function isMultipartMediaType(mediaType) {
return mediaType.split(';', 1)[0].trim().toLowerCase() === 'multipart/form-data';
}
function isTextMediaType(mediaType) {
return mediaType.split(';', 1)[0].trim().toLowerCase().startsWith('text/');
}
function schemaFromMedia(document, media) {
return media?.schema ? resolveRef(document, media.schema) : undefined;
}
function findOperation(document, operationId) {
const paths = asRecord(document.paths, 'paths');
for (const [pathTemplate, pathItem] of Object.entries(paths)) {
const pathItemRecord = asRecord(pathItem, pathTemplate);
for (const [method, operation] of Object.entries(pathItemRecord)) {
if (
HTTP_METHODS.has(method) &&
asRecord(operation, `${method} ${pathTemplate}`).operationId === operationId
) {
return {
pathTemplate,
pathItem: pathItemRecord,
method: method.toUpperCase(),
operation: asRecord(operation, operationId),
};
}
}
}
usage(`Operation not found: ${operationId}`);
}
function effectiveParameters(document, pathItem, operation) {
const parameters = [
...(Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
...(Array.isArray(operation.parameters) ? operation.parameters : []),
].map((parameter) => asRecord(resolveRef(document, parameter), 'parameter'));
const byLocationAndName = new Map();
for (const parameter of parameters) {
byLocationAndName.set(`${parameter.in}:${parameter.name}`, parameter);
}
return [...byLocationAndName.values()];
}
function schemaProperties(document, schema) {
const resolved = asRecord(resolveRef(document, schema || { type: 'object' }), 'schema');
const variantProperties = schemaVariant(resolved)
? schemaProperties(document, schemaVariant(resolved))
: {};
const allOfProperties = Array.isArray(resolved.allOf)
? Object.assign({}, ...resolved.allOf.map((part) => schemaProperties(document, part)))
: {};
return {
...variantProperties,
...allOfProperties,
...asRecord(resolved.properties || {}, 'schema.properties'),
};
}
function schemaRequired(document, schema) {
const resolved = asRecord(resolveRef(document, schema || { type: 'object' }), 'schema');
const variantRequired = schemaVariant(resolved)
? schemaRequired(document, schemaVariant(resolved))
: [];
const allOfRequired = Array.isArray(resolved.allOf)
? resolved.allOf.flatMap((part) => schemaRequired(document, part))
: [];
const required = Array.isArray(resolved.required) ? resolved.required : [];
return [...new Set([...variantRequired, ...allOfRequired, ...required])];
}
function schemaVariant(schema) {
if (Array.isArray(schema.oneOf) && schema.oneOf.length > 0) {
return schema.oneOf[0];
}
if (Array.isArray(schema.anyOf) && schema.anyOf.length > 0) {
return schema.anyOf[0];
}
return undefined;
}
function schemaType(schema) {
if (Array.isArray(schema.type)) {
return schema.type.find((type) => type !== 'null');
}
if (schema.type) {
return schema.type;
}
if (schema.properties) {
return 'object';
}
if (schema.items) {
return 'array';
}
return undefined;
}
function isScalarSchema(document, schema) {
if (!schema || typeof schema !== 'object') {
return false;
}
const resolved = asRecord(resolveRef(document, schema), 'schema');
const variant = schemaVariant(resolved);
if (variant) {
return isScalarSchema(document, variant);
}
const type = schemaType(resolved);
return ['boolean', 'integer', 'number', 'string'].includes(type);
}
function schemaArrayItems(document, schema) {
if (!schema || typeof schema !== 'object') {
return undefined;
}
const resolved = asRecord(resolveRef(document, schema), 'schema');
const variant = schemaVariant(resolved);
if (variant) {
return schemaArrayItems(document, variant);
}
if (Array.isArray(resolved.allOf)) {
for (const part of resolved.allOf) {
const items = schemaArrayItems(document, part);
if (items) {
return items;
}
}
}
return schemaType(resolved) === 'array' ? resolveRef(document, resolved.items || {}) : undefined;
}
function stringSample(schema, name) {
const format = typeof schema.format === 'string' ? schema.format.toLowerCase() : '';
if (format === 'email' || name.endsWith('_email') || name === 'email') {
return 'user@example.com';
}
if (format === 'uuid') {
return '00000000-0000-4000-8000-000000000000';
}
if (format === 'uri' || format === 'url' || format === 'uri-reference') {
return 'https://example.test/resource';
}
if (format === 'date-time') {
return '2026-04-17T00:00:00Z';
}
if (format === 'date') {
return '2026-04-17';
}
if (format === 'time') {
return '12:00:00';
}
return sampleValue(name);
}
function orderedProperties(document, schema) {
const resolved = asRecord(schema || { type: 'object' }, 'schema');
const properties = schemaProperties(document, resolved);
const required = schemaRequired(document, resolved);
return [...required, ...Object.keys(properties).filter((name) => !required.includes(name))];
}
function schemaHasFlag(document, schema, flag) {
if (!schema || typeof schema !== 'object') {
return false;
}
const resolved = asRecord(resolveRef(document, schema), 'schema');
if (resolved[flag] === true) {
return true;
}
const variant = schemaVariant(resolved);
if (variant && schemaHasFlag(document, variant, flag)) {
return true;
}
return Array.isArray(resolved.allOf)
? resolved.allOf.some((part) => schemaHasFlag(document, part, flag))
: false;
}
function isReadOnlySchema(document, schema) {
return schemaHasFlag(document, schema, 'readOnly');
}
function isWriteOnlySchema(document, schema) {
return schemaHasFlag(document, schema, 'writeOnly');
}
function isMultipartFileSchema(document, schema) {
if (!schema || typeof schema !== 'object') {
return false;
}
const resolved = asRecord(resolveRef(document, schema), 'schema');
const variant = schemaVariant(resolved);
if (variant && isMultipartFileSchema(document, variant)) {
return true;
}
if (
Array.isArray(resolved.allOf) &&
resolved.allOf.some((part) => isMultipartFileSchema(document, part))
) {
return true;
}
const type = schemaType(resolved);
const format = typeof resolved.format === 'string' ? resolved.format.toLowerCase() : '';
if (type === 'array') {
return isMultipartFileSchema(document, resolved.items);
}
return type === 'string' && ['binary', 'base64'].includes(format);
}
function responseOutputField(document, properties) {
const responseProperties = Object.fromEntries(
Object.entries(properties).filter(([_name, schema]) => !isWriteOnlySchema(document, schema)),
);
for (const candidate of [
'output',
'answer',
'response',
'result',
'text',
'content',
'message',
]) {
if (candidate in responseProperties) {
return candidate;
}
}
return Object.keys(responseProperties)[0];
}
function responseAccessor(base, field) {
return /^[A-Za-z_$][0-9A-Za-z_$]*$/.test(field)
? `${base}.${field}`
: `${base}[${JSON.stringify(field)}]`;
}
function successResponse(document, operation) {
const responses = asRecord(operation.responses || {}, 'responses');
const status = responses['200']
? '200'
: Object.keys(responses)
.filter((candidate) => /^2\d\d$/.test(candidate))
.sort()[0];
return {
status,
response: status
? asRecord(resolveRef(document, responses[status]), `responses.${status}`)
: {},
};
}
function sampleValue(name) {
if (PROMPT_FIELDS.has(name)) {
return 'Say exactly PONG.';
}
if (name.endsWith('_id')) {
return `sample-${name.replaceAll('_', '-')}`;
}
return `sample-${name}`;
}
function isPlainRecord(value) {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}
function firstExample(document, examples) {
if (Array.isArray(examples) && examples.length > 0) {
return examples[0];
}
if (!isPlainRecord(examples)) {
return undefined;
}
const first = Object.values(examples)[0];
if (!isPlainRecord(first)) {
return undefined;
}
const resolved = resolveRef(document, first);
return isPlainRecord(resolved) && 'value' in resolved ? resolved.value : undefined;
}
function objectSample(document, object, context) {
if (!isPlainRecord(object)) {
return undefined;
}
const resolved = asRecord(resolveRef(document, object), context);
if ('example' in resolved && resolved.example !== null) {
return resolved.example;
}
return firstExample(document, resolved.examples);
}
function schemaSample(document, schema, name, depth = 0) {
if (PROMPT_FIELDS.has(name)) {
return 'Say exactly PONG.';
}
if (!schema || typeof schema !== 'object') {
return sampleValue(name);
}
const resolved = asRecord(resolveRef(document, schema), `${name} schema`);
if ('const' in resolved && resolved.const !== null) {
return resolved.const;
}
if ('example' in resolved && resolved.example !== null) {
return resolved.example;
}
if ('default' in resolved && resolved.default !== null) {
return resolved.default;
}
if (
Array.isArray(resolved.examples) &&
resolved.examples.length > 0 &&
resolved.examples[0] !== null
) {
return resolved.examples[0];
}
if (Array.isArray(resolved.enum) && resolved.enum.length > 0) {
return resolved.enum[0];
}
const variant = schemaVariant(resolved);
if (variant) {
return schemaSample(document, variant, name, depth + 1);
}
if (depth > 4) {
return sampleValue(name);
}
const type = schemaType(resolved);
if (type === 'integer' || type === 'number') {
return 1;
}
if (type === 'boolean') {
return true;
}
if (type === 'array') {
return [schemaSample(document, resolved.items, name, depth + 1)];
}
if (type === 'object') {
const properties = schemaProperties(document, resolved);
const names = orderedProperties(document, resolved).slice(0, 6);
return Object.fromEntries(
names.map((propertyName) => [
propertyName,
schemaSample(document, properties[propertyName], propertyName, depth + 1),
]),
);
}
if (type === 'string' || typeof resolved.format === 'string') {
return stringSample(resolved, name);
}
return sampleValue(name);
}
function parameterSample(document, parameter) {
if (PROMPT_FIELDS.has(parameter.name)) {
return 'Say exactly PONG.';
}
const example = objectSample(document, parameter, `${parameter.name} parameter`);
return example === undefined
? schemaSample(
document,
parameter.schema,
parameter.in === 'header' ? varName(parameter.name) : parameter.name,
)
: example;
}
function inputTemplate(name) {
return PROMPT_FIELDS.has(name) ? '{{prompt}}' : `{{${varName(name)}}}`;
}
function encodedInputTemplate(name) {
return PROMPT_FIELDS.has(name) ? '{{prompt | urlencode}}' : `{{${varName(name)} | urlencode}}`;
}
function formEncodedBody(fields) {
return fields
.map((name) => `${encodeURIComponent(name)}=${encodedInputTemplate(name)}`)
.join('&');
}
function multipartPart(document, name, schema) {
if (isMultipartFileSchema(document, schema)) {
return {
kind: 'file',
name,
filename: `promptfoo-${varName(name)}.pdf`,
source: {
type: 'generated',
generator: 'basic-document',
format: 'pdf',
text: `Promptfoo generated document for ${inputTemplate(name)}.`,
},
};
}
return { kind: 'field', name, value: inputTemplate(name) };
}
function varName(name) {
return name
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1_$2')
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
.replaceAll(/[^A-Za-z0-9_]/g, '_')
.replace(/^(\d)/, '_$1')
.toLowerCase();
}
// Parameters with these names (or these suffixes) are treated as credentials
// even when they are modeled as plain header/query/cookie parameters rather
// than as OpenAPI securitySchemes. Copying their literal `example` values into
// the generated config could leak real tokens, so the helper forces
// `{{env.<UPPER_NAME>}}` placeholders for them instead.
const CREDENTIAL_PARAM_NAMES = new Set([
'authorization',
'bearer',
'token',
'access_token',
'auth_token',
'api_key',
'apikey',
'x_api_key',
'x_auth_token',
'x_access_token',
'secret',
'password',
'csrf_token',
'xsrf_token',
'session',
'sessionid',
'session_id',
'sid',
]);
const CREDENTIAL_SUFFIX_REGEX =
/(^|_)(api_key|apikey|auth_token|access_token|bearer|password|secret|token|authorization)$/;
function isCredentialParamName(name) {
const normalized = varName(name);
return CREDENTIAL_PARAM_NAMES.has(normalized) || CREDENTIAL_SUFFIX_REGEX.test(normalized);
}
function credentialEnvName(paramName) {
return varName(paramName).toUpperCase();
}
function credentialPlaceholder(paramName) {
return `{{env.${credentialEnvName(paramName)}}}`;
}
function authFromScheme(scheme) {
if (scheme.type === 'apiKey' && typeof scheme.name === 'string') {
if (scheme.in === 'header') {
return { location: 'header', name: scheme.name, prefix: 'none' };
}
if (scheme.in === 'query') {
return { location: 'query', name: scheme.name, prefix: 'none' };
}
if (scheme.in === 'cookie') {
return { location: 'cookie', name: scheme.name, prefix: 'none' };
}
}
if (scheme.type === 'http' && typeof scheme.scheme === 'string') {
const httpScheme = scheme.scheme.toLowerCase();
if (httpScheme === 'bearer') {
return { location: 'header', name: 'Authorization', prefix: 'Bearer' };
}
if (httpScheme === 'basic') {
return { location: 'header', name: 'Authorization', prefix: 'Basic' };
}
}
if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect') {
return { location: 'header', name: 'Authorization', prefix: 'Bearer' };
}
return undefined;
}
function inferAuths(document, operation) {
const components = asRecord(document.components || {}, 'components');
const securitySchemes = asRecord(components.securitySchemes || {}, 'components.securitySchemes');
const hasOperationSecurity = Array.isArray(operation.security);
const hasDocumentSecurity = Array.isArray(document.security);
const securityRequirements = hasOperationSecurity
? operation.security
: hasDocumentSecurity
? document.security
: undefined;
if (!securityRequirements) {
return undefined;
}
if (securityRequirements.length === 0) {
return [];
}
let partiallySupportedAuths;
for (const requirement of securityRequirements) {
const requirementRecord = asRecord(requirement, 'security requirement');
const requirementNames = Object.keys(requirementRecord);
if (requirementNames.length === 0) {
return [];
}
const auths = [];
for (const name of requirementNames) {
if (!(name in securitySchemes)) {
continue;
}
const scheme = asRecord(
resolveRef(document, securitySchemes[name]),
`securitySchemes.${name}`,
);
const auth = authFromScheme(scheme);
if (!auth) {
continue;
}
auths.push(auth);
}
if (auths.length === requirementNames.length) {
return auths;
}
if (auths.length > 0 && !partiallySupportedAuths) {
partiallySupportedAuths = auths;
}
}
return partiallySupportedAuths ?? [];
}
function authValue(tokenEnv, prefix) {
return prefix === 'none' ? `{{env.${tokenEnv}}}` : `${prefix} {{env.${tokenEnv}}}`;
}
function appendCookieHeader(headers, name, value) {
const cookie = `${name}=${value}`;
headers.Cookie = headers.Cookie ? `${headers.Cookie}; ${cookie}` : cookie;
}
function authConfigs(args, document, operation) {
if (args['auth-header']) {
const prefix = args['auth-prefix'] ?? 'Bearer';
return {
auths: [{ location: 'header', name: args['auth-header'], prefix }],
};
}
const inferred = inferAuths(document, operation);
const auths =
inferred === undefined
? [{ location: 'header', name: 'Authorization', prefix: 'Bearer' }]
: inferred;
return {
auths: auths.map((auth) => ({
...auth,
// --auth-prefix only overrides schemes that already carry a prefix (Bearer/Basic/etc.);
// API-key-style auths with prefix 'none' stay untouched so the emitted header value is raw.
prefix: args['auth-prefix'] && auth.prefix !== 'none' ? args['auth-prefix'] : auth.prefix,
})),
};
}
const args = parseArgs(process.argv.slice(2));
if (!args.spec || !args['operation-id'] || !args['base-url-env']) {
usage('Missing --spec, --operation-id, or --base-url-env');
}
const document = yaml.load(fs.readFileSync(args.spec, 'utf8'), {
// Preserve js-yaml v4's support for YAML merge keys in user-supplied specs.
schema: yaml.CORE_SCHEMA.withTags(yaml.mergeTag),
});
const { pathTemplate, pathItem, method, operation } = findOperation(
asRecord(document, 'OpenAPI document'),
args['operation-id'],
);
const pathVars = [...pathTemplate.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
const promptPath = pathTemplate.replace(/\{([^}]+)\}/g, (_match, name) =>
encodedInputTemplate(name),
);
const parameters = effectiveParameters(document, pathItem, operation);
const parameterSamples = Object.fromEntries(
parameters
.filter((parameter) => typeof parameter.name === 'string')
.map((parameter) => [varName(parameter.name), parameterSample(document, parameter)]),
);
const queryFields = parameters
.filter((parameter) => parameter.in === 'query')
.map((parameter) => parameter.name)
.filter((name) => typeof name === 'string');
const headerFields = parameters
.filter((parameter) => parameter.in === 'header')
.map((parameter) => parameter.name)
.filter((name) => typeof name === 'string');
const cookieFields = parameters
.filter((parameter) => parameter.in === 'cookie')
.map((parameter) => parameter.name)
.filter((name) => typeof name === 'string');
const requestBody = asRecord(resolveRef(document, operation.requestBody || {}), 'requestBody');
const requestMediaEntry = getRequestMediaEntry(
document,
asRecord(requestBody.content || {}, 'requestBody.content'),
);
const requestMedia = requestMediaEntry?.media;
const requestExample = objectSample(document, requestMedia, 'requestBody application/json');
const requestExampleIsArray = Array.isArray(requestExample);
const requestExampleArrayItemIsObject = requestExampleIsArray && isPlainRecord(requestExample[0]);
const requestExampleIsScalar =
requestExample !== undefined && !requestExampleIsArray && !isPlainRecord(requestExample);
const requestSchema = schemaFromMedia(document, requestMedia);
const requestIsFormUrlEncoded = isFormUrlEncodedMediaType(requestMediaEntry?.mediaType || '');
const requestIsMultipart = isMultipartMediaType(requestMediaEntry?.mediaType || '');
const requestIsText = isTextMediaType(requestMediaEntry?.mediaType || '');
const requestArrayItemSchema =
requestSchema && !requestIsFormUrlEncoded && !requestIsMultipart && !requestIsText
? schemaArrayItems(document, requestSchema)
: undefined;
const requestArrayItemProperties = requestArrayItemSchema
? schemaProperties(document, requestArrayItemSchema)
: {};
const requestArrayItemIsObject = Object.keys(requestArrayItemProperties).length > 0;
const requestBodyIsArray = Boolean(
!requestIsFormUrlEncoded &&
!requestIsMultipart &&
!requestIsText &&
(requestArrayItemSchema || requestExampleIsArray),
);
const requestBodyArrayItemIsObject = requestArrayItemSchema
? requestArrayItemIsObject
: requestExampleArrayItemIsObject;
const effectiveRequestSchema = requestArrayItemSchema || requestSchema;
const requestBodyIsScalar =
(!effectiveRequestSchema && requestExampleIsScalar) ||
Boolean(
effectiveRequestSchema &&
!requestBodyIsArray &&
!requestIsFormUrlEncoded &&
!requestIsMultipart &&
isScalarSchema(document, effectiveRequestSchema),
);
const requestProperties =
requestArrayItemSchema && !requestArrayItemIsObject
? { message: requestArrayItemSchema }
: requestBodyIsScalar && effectiveRequestSchema
? { message: effectiveRequestSchema }
: effectiveRequestSchema
? schemaProperties(document, effectiveRequestSchema)
: {};
const requestExampleFields = isPlainRecord(requestExample)
? requestExample
: Array.isArray(requestExample) && isPlainRecord(requestExample[0])
? requestExample[0]
: Array.isArray(requestExample) && requestExample.length > 0
? { message: requestExample[0] }
: requestExampleIsScalar
? { message: requestExample }
: {};
const bodyFields = effectiveRequestSchema
? (requestIsText || requestBodyIsScalar || (requestArrayItemSchema && !requestArrayItemIsObject)
? ['message']
: orderedProperties(document, effectiveRequestSchema)
).filter(
(name) =>
!pathVars.includes(name) &&
!queryFields.includes(name) &&
!isReadOnlySchema(document, requestProperties[name]),
)
: Object.keys(requestExampleFields).filter(
(name) => !pathVars.includes(name) && !queryFields.includes(name),
);
const { status: responseStatus, response } = successResponse(document, operation);
const responseContent = asRecord(
response.content || {},
`responses.${responseStatus ?? 'success'}.content`,
);
const responseMediaEntry = getJsonMediaEntry(document, responseContent);
const responseMedia = responseMediaEntry?.media;
const responseExample = objectSample(document, responseMedia, 'response application/json');
const responseExampleIsArray = Array.isArray(responseExample);
const responseExampleFields = isPlainRecord(responseExample)
? responseExample
: responseExampleIsArray && isPlainRecord(responseExample[0])
? responseExample[0]
: {};
const responseSchema = schemaFromMedia(document, responseMedia);
const responseArrayItemSchema = responseSchema
? schemaArrayItems(document, responseSchema)
: undefined;
const responseIsArray = Boolean(responseArrayItemSchema || responseExampleIsArray);
const responseProperties = responseArrayItemSchema
? schemaProperties(document, responseArrayItemSchema)
: responseSchema
? schemaProperties(document, responseSchema)
: responseExampleFields;
const responseField = responseOutputField(document, responseProperties);
const credentialHeaderFields = headerFields.filter(isCredentialParamName);
const credentialQueryFields = queryFields.filter(isCredentialParamName);
const credentialCookieFields = cookieFields.filter(isCredentialParamName);
const credentialParamNames = new Set([
...credentialHeaderFields,
...credentialQueryFields,
...credentialCookieFields,
]);
const headers =
requestMediaEntry && !requestIsMultipart
? { 'Content-Type': requestMediaEntry?.mediaType || 'application/json' }
: {};
for (const name of headerFields) {
headers[name] = credentialParamNames.has(name)
? credentialPlaceholder(name)
: `{{${varName(name)}}}`;
}
for (const name of cookieFields) {
appendCookieHeader(
headers,
name,
credentialParamNames.has(name) ? credentialPlaceholder(name) : `{{${varName(name)}}}`,
);
}
const queryParams = Object.fromEntries(
queryFields.map((name) => [
name,
credentialParamNames.has(name) ? credentialPlaceholder(name) : inputTemplate(name),
]),
);
if (args['token-env']) {
for (const auth of authConfigs(args, document, operation).auths) {
const value = authValue(args['token-env'], auth.prefix);
if (auth.location === 'query') {
queryParams[auth.name] = value;
} else if (auth.location === 'cookie') {
appendCookieHeader(headers, auth.name, value);
} else {
headers[auth.name] = value;
}
}
}
const providerConfig = {
url: `{{env.${args['base-url-env']}}}${promptPath}`,
method,
stateful: false,
};
if (Object.keys(headers).length > 0) {
providerConfig.headers = headers;
}
if (bodyFields.length > 0) {
if (requestIsMultipart) {
providerConfig.multipart = {
parts: bodyFields.map((name) => multipartPart(document, name, requestProperties[name])),
};
} else if (requestIsText || requestBodyIsScalar) {
providerConfig.body = inputTemplate(bodyFields[0]);
} else if (requestBodyIsArray) {
providerConfig.body = [
requestBodyArrayItemIsObject
? Object.fromEntries(bodyFields.map((name) => [name, inputTemplate(name)]))
: inputTemplate(bodyFields[0]),
];
} else {
providerConfig.body = requestIsFormUrlEncoded
? formEncodedBody(bodyFields)
: Object.fromEntries(bodyFields.map((name) => [name, inputTemplate(name)]));
}
}
if (Object.keys(queryParams).length > 0) {
providerConfig.queryParams = queryParams;
}
if (responseField) {
providerConfig.transformResponse = responseAccessor(
responseIsArray ? 'json[0]' : 'json',
responseField,
);
}
const nonCredentialQueryFields = queryFields.filter((name) => !credentialParamNames.has(name));
const nonCredentialHeaderFields = headerFields.filter((name) => !credentialParamNames.has(name));
const nonCredentialCookieFields = cookieFields.filter((name) => !credentialParamNames.has(name));
const varNames = [...pathVars, ...nonCredentialQueryFields, ...bodyFields]
.filter((name) => inputTemplate(name) !== '{{prompt}}')
.map(varName);
varNames.push(...nonCredentialHeaderFields.map(varName), ...nonCredentialCookieFields.map(varName));
if (!varNames.includes('message')) {
varNames.push('message');
}
const vars = Object.fromEntries([...new Set(varNames)].map((name) => [name, sampleValue(name)]));
for (const [name, value] of Object.entries(parameterSamples)) {
if (name in vars) {
vars[name] = value;
}
}
for (const name of bodyFields) {
const key = varName(name);
if (key in vars) {
vars[key] = PROMPT_FIELDS.has(name)
? schemaSample(document, requestProperties[name], name)
: (requestExampleFields[name] ?? schemaSample(document, requestProperties[name], name));
}
}
// The canned "Say exactly PONG." prompt is only meaningful when the request
// carries a prompt field (path/query/body) — otherwise the message never reaches
// the target and `contains: PONG` fails by construction. Fall back to a safer
// response-shape assertion.
const promptReachesTarget =
pathVars.some((name) => PROMPT_FIELDS.has(name)) ||
queryFields.some((name) => PROMPT_FIELDS.has(name)) ||
bodyFields.some((name) => PROMPT_FIELDS.has(name)) ||
(bodyFields.length > 0 && (requestIsText || requestBodyIsScalar));
const responseIsJson = Boolean(responseMediaEntry);
const smokeAssert = promptReachesTarget
? [{ type: 'contains', value: 'PONG' }]
: responseIsJson
? [{ type: 'is-json' }]
: [{ type: 'javascript', value: "typeof output === 'string' && output.length > 0" }];
const config = {
description: args.description || `Provider setup generated from ${args['operation-id']}`,
prompts: ['{{message}}'],
providers: [{ id: 'https', label: args.label || args['operation-id'], config: providerConfig }],
tests: [
{
description: promptReachesTarget
? `${args['operation-id']} returns text`
: `${args['operation-id']} responds successfully`,
vars,
assert: smokeAssert,
},
],
};
const output = `# yaml-language-server: $schema=https://promptfoo.dev/config-schema.json\n${yaml.dump(config, { lineWidth: 100, noRefs: true })}`;
if (args.output) {
fs.writeFileSync(args.output, output);
} else {
process.stdout.write(output);
}