Add custom headers support for API Management integration

This change adds support for custom HTTP headers in AI inference requests,
enabling integration with API Management platforms (Azure APIM, AWS API
Gateway, Kong, etc.) and custom request routing/tracking.

Features:
- New 'custom-headers' input supporting both YAML and JSON formats
- Auto-detection of input format for better UX
- Header name validation (alphanumeric, hyphens, underscores)
- Automatic masking of sensitive headers in logs
- Full backward compatibility (optional parameter)

Changes:
- Added parseCustomHeaders() function in helpers.ts
- Updated InferenceRequest interface with optional customHeaders field
- Modified simpleInference() and mcpInference() to pass headers to OpenAI client
- Added 18 comprehensive test cases
- Updated documentation with examples and use cases

All 80 tests passing. Zero breaking changes.
This commit is contained in:
Yonatan Golick
2026-01-18 11:24:13 +02:00
parent 63993128d7
commit 6d144ac474
11 changed files with 691 additions and 102 deletions
Generated Vendored
+153 -85
View File
@@ -58308,6 +58308,7 @@ async function simpleInference(request) {
const client = new OpenAI({
apiKey: request.token,
baseURL: request.endpoint,
defaultHeaders: request.customHeaders || {},
});
const chatCompletionRequest = {
messages: request.messages,
@@ -58334,6 +58335,7 @@ async function mcpInference(request, githubMcpClient) {
const client = new OpenAI({
apiKey: request.token,
baseURL: request.endpoint,
defaultHeaders: request.customHeaders || {},
});
// Start with the pre-processed messages
const messages = [...request.messages];
@@ -58437,90 +58439,6 @@ async function chatCompletion(client, params, context) {
}
}
/**
* Helper function to load content from a file or use fallback input
* @param filePathInput - Input name for the file path
* @param contentInput - Input name for the direct content
* @param defaultValue - Default value to use if neither file nor content is provided
* @returns The loaded content
*/
function loadContentFromFileOrInput(filePathInput, contentInput, defaultValue) {
const filePath = coreExports.getInput(filePathInput);
const contentString = coreExports.getInput(contentInput);
if (filePath !== undefined && filePath !== '') {
if (!fs.existsSync(filePath)) {
throw new Error(`File for ${filePathInput} was not found: ${filePath}`);
}
return fs.readFileSync(filePath, 'utf-8');
}
else if (contentString !== undefined && contentString !== '') {
return contentString;
}
else if (defaultValue !== undefined) {
return defaultValue;
}
else {
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`);
}
}
/**
* Build messages array from either prompt config or legacy format
*/
function buildMessages(promptConfig, systemPrompt, prompt) {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map(msg => ({
role: msg.role,
content: msg.content,
}));
}
else {
// Use legacy format
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant',
},
{ role: 'user', content: prompt || '' },
];
}
}
/**
* Build response format object for API from prompt config
*/
function buildResponseFormat(promptConfig) {
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
try {
const schema = JSON.parse(promptConfig.jsonSchema);
return {
type: 'json_schema',
json_schema: schema,
};
}
catch (error) {
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
return undefined;
}
/**
* Build complete InferenceRequest from prompt config and inputs
*/
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token) {
const messages = buildMessages(promptConfig, systemPrompt, prompt);
const responseFormat = buildResponseFormat(promptConfig);
return {
messages,
modelName,
temperature,
topP,
maxTokens,
endpoint,
token,
responseFormat,
};
}
/*! js-yaml 4.1.1 https://github.com/nodeca/js-yaml @license MIT */
function isNothing(subject) {
return (typeof subject === 'undefined') || (subject === null);
@@ -61328,6 +61246,153 @@ var loader = {
};
var load = loader.load;
/**
* Helper function to load content from a file or use fallback input
* @param filePathInput - Input name for the file path
* @param contentInput - Input name for the direct content
* @param defaultValue - Default value to use if neither file nor content is provided
* @returns The loaded content
*/
function loadContentFromFileOrInput(filePathInput, contentInput, defaultValue) {
const filePath = coreExports.getInput(filePathInput);
const contentString = coreExports.getInput(contentInput);
if (filePath !== undefined && filePath !== '') {
if (!fs.existsSync(filePath)) {
throw new Error(`File for ${filePathInput} was not found: ${filePath}`);
}
return fs.readFileSync(filePath, 'utf-8');
}
else if (contentString !== undefined && contentString !== '') {
return contentString;
}
else if (defaultValue !== undefined) {
return defaultValue;
}
else {
throw new Error(`Neither ${filePathInput} nor ${contentInput} was set`);
}
}
/**
* Build messages array from either prompt config or legacy format
*/
function buildMessages(promptConfig, systemPrompt, prompt) {
if (promptConfig?.messages && promptConfig.messages.length > 0) {
// Use new message format
return promptConfig.messages.map(msg => ({
role: msg.role,
content: msg.content,
}));
}
else {
// Use legacy format
return [
{
role: 'system',
content: systemPrompt || 'You are a helpful assistant',
},
{ role: 'user', content: prompt || '' },
];
}
}
/**
* Build response format object for API from prompt config
*/
function buildResponseFormat(promptConfig) {
if (promptConfig?.responseFormat === 'json_schema' && promptConfig.jsonSchema) {
try {
const schema = JSON.parse(promptConfig.jsonSchema);
return {
type: 'json_schema',
json_schema: schema,
};
}
catch (error) {
throw new Error(`Invalid JSON schema: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
}
return undefined;
}
/**
* Parse custom headers from YAML or JSON format
* @param input - String in YAML or JSON format containing headers
* @returns Record of header names to values, or empty object if invalid
*/
function parseCustomHeaders(input) {
if (!input || input.trim() === '') {
return {};
}
const trimmedInput = input.trim();
try {
// Try JSON first (check if it starts with { or [)
if (trimmedInput.startsWith('{') || trimmedInput.startsWith('[')) {
const parsed = JSON.parse(trimmedInput);
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
coreExports.warning('Custom headers JSON must be an object, not an array');
return {};
}
return validateAndMaskHeaders(parsed);
}
// Try YAML
const parsed = load(trimmedInput);
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
coreExports.warning('Custom headers YAML must be an object');
return {};
}
return validateAndMaskHeaders(parsed);
}
catch (error) {
coreExports.warning(`Failed to parse custom headers: ${error instanceof Error ? error.message : 'Unknown error'}`);
return {};
}
}
/**
* Validate header names and mask sensitive values in logs
* @param headers - Raw headers object
* @returns Validated headers with string values
*/
function validateAndMaskHeaders(headers) {
const validHeaders = {};
const sensitivePatterns = ['key', 'token', 'secret', 'password', 'authorization'];
for (const [name, value] of Object.entries(headers)) {
// Validate header name (basic HTTP header name validation)
if (!/^[a-zA-Z0-9\-_]+$/.test(name)) {
coreExports.warning(`Skipping invalid header name: ${name} (only alphanumeric, hyphens, and underscores allowed)`);
continue;
}
// Convert value to string
const stringValue = String(value);
validHeaders[name] = stringValue;
// Mask sensitive headers in logs
const lowerName = name.toLowerCase();
const isSensitive = sensitivePatterns.some(pattern => lowerName.includes(pattern));
if (isSensitive) {
coreExports.info(`Custom header added: ${name}: ***MASKED***`);
}
else {
coreExports.info(`Custom header added: ${name}: ${stringValue}`);
}
}
return validHeaders;
}
/**
* Build complete InferenceRequest from prompt config and inputs
*/
function buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, temperature, topP, maxTokens, endpoint, token, customHeaders) {
const messages = buildMessages(promptConfig, systemPrompt, prompt);
const responseFormat = buildResponseFormat(promptConfig);
return {
messages,
modelName,
temperature,
topP,
maxTokens,
endpoint,
token,
responseFormat,
customHeaders,
};
}
/**
* Parse template variables from YAML input string
*/
@@ -61478,8 +61543,11 @@ async function run() {
const githubMcpToken = coreExports.getInput('github-mcp-token') || token;
const githubMcpToolsets = coreExports.getInput('github-mcp-toolsets');
const endpoint = coreExports.getInput('endpoint');
// Parse custom headers
const customHeadersInput = coreExports.getInput('custom-headers');
const customHeaders = parseCustomHeaders(customHeadersInput);
// Build the inference request with pre-processed messages and response format
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token);
const inferenceRequest = buildInferenceRequest(promptConfig, systemPrompt, prompt, modelName, promptConfig?.modelParameters?.temperature, promptConfig?.modelParameters?.topP, maxTokens, endpoint, token, customHeaders);
const enableMcp = coreExports.getBooleanInput('enable-github-mcp') || false;
let modelResponse = null;
if (enableMcp) {
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long