← Files PromptfooARCHIVED FILE

skills/promptfoo-provider-setup/scripts/openapi-operation-to-config.mjs

30.1 KB · Oct 3, 2026 · 06:37 UTC

↓ Download file

import fs from 'node:fs';

import { responseTransform, smokeAssertions } from './response-contract.mjs';

import * as yaml from './vendor/js-yaml.mjs';

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] [--smoke-assert text] [--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 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 (responseMediaEntry) {
  providerConfig.transformResponse = responseTransform({
    field: responseField,
    array: responseIsArray,
    schema: responseField ? responseProperties[responseField] : responseSchema,
    resolveRef: (schema) => resolveRef(document, schema),
  });
}

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));
  }
}
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: `${args['operation-id']} responds successfully`,
      vars,
      assert: smokeAssertions(Boolean(responseMediaEntry), args['smoke-assert']),
    },
  ],
};

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);
}

SHA-256: 2138c6a9032a341355219e19976182ad0afad86014d25f14fbf6d1fe0598b357