genkitx-azure-openai
Version:
Genkit AI framework plugin for Azure OpenAI APIs.
404 lines (401 loc) • 11.4 kB
JavaScript
import { app } from "@azure/functions";
import { UserFacingError } from "genkit";
import {
getCallableJSON,
getHttpStatus
} from "genkit/context";
function buildCorsHeaders(corsOptions, requestOrigin) {
if (corsOptions === false) {
return {};
}
const opts = corsOptions === true || corsOptions === void 0 ? {} : corsOptions;
const headers = {
"Content-Type": "application/json"
};
const origin = opts.origin ?? "*";
if (Array.isArray(origin)) {
if (requestOrigin && origin.includes(requestOrigin)) {
headers["Access-Control-Allow-Origin"] = requestOrigin;
}
} else {
headers["Access-Control-Allow-Origin"] = origin;
}
const methods = opts.methods ?? ["POST", "OPTIONS"];
headers["Access-Control-Allow-Methods"] = methods.join(", ");
const allowedHeaders = opts.allowedHeaders ?? [
"Content-Type",
"Authorization"
];
headers["Access-Control-Allow-Headers"] = allowedHeaders.join(", ");
if (opts.exposedHeaders && opts.exposedHeaders.length > 0) {
headers["Access-Control-Expose-Headers"] = opts.exposedHeaders.join(", ");
}
if (opts.credentials) {
headers["Access-Control-Allow-Credentials"] = "true";
}
const maxAge = opts.maxAge ?? 86400;
headers["Access-Control-Max-Age"] = String(maxAge);
return headers;
}
async function parseRequestBody(request) {
let bodyText;
try {
bodyText = await request.text();
} catch {
return {};
}
if (!bodyText) {
return {};
}
try {
const parsed = JSON.parse(bodyText);
if (parsed && typeof parsed === "object" && "data" in parsed) {
return parsed.data;
}
return parsed;
} catch {
throw new UserFacingError(
"INVALID_ARGUMENT",
"Invalid JSON in request body"
);
}
}
function getRequestOrigin(request) {
return request.headers.get("origin") || void 0;
}
function normalizeHeaders(request) {
const result = {};
request.headers.forEach((value, key) => {
result[key.toLowerCase()] = value;
});
return result;
}
function getQueryParams(request) {
const result = {};
const url = new URL(request.url);
url.searchParams.forEach((value, key) => {
result[key] = value;
});
return result;
}
function toRequestData(request, input) {
return {
method: request.method,
headers: normalizeHeaders(request),
input
};
}
function onCallGenkit(optsOrFlow, flowArg) {
let opts;
let flow;
if (arguments.length === 1) {
opts = {};
flow = optsOrFlow;
} else {
opts = optsOrFlow;
flow = flowArg;
}
const flowName = flow.__action?.name || "unknown";
function buildAzureFunctionsContext(request, azureContext) {
return {
azureFunctions: {
request: {
url: request.url,
headers: normalizeHeaders(request),
query: getQueryParams(request),
params: request.params
},
context: {
functionName: azureContext.functionName,
invocationId: azureContext.invocationId
}
}
};
}
async function resolveActionContext(request, azureContext, input) {
const azureFunctionsContext = buildAzureFunctionsContext(
request,
azureContext
);
if (opts.contextProvider) {
const requestData = toRequestData(request, input);
const providerContext = await opts.contextProvider(requestData);
return { ...azureFunctionsContext, ...providerContext };
}
return azureFunctionsContext;
}
async function buildErrorResponse(error, corsHeaders) {
if (opts.onError) {
const customError = await opts.onError(
error instanceof Error ? error : new Error(String(error))
);
return {
status: customError.statusCode,
headers: corsHeaders,
jsonBody: {
error: {
status: "INTERNAL",
message: customError.message
}
}
};
}
return {
status: getHttpStatus(error),
headers: corsHeaders,
jsonBody: getCallableJSON(error)
};
}
async function standardHandler(request, azureContext) {
const requestOrigin = getRequestOrigin(request);
const corsHeaders = buildCorsHeaders(opts.cors, requestOrigin);
if (request.method === "OPTIONS") {
return {
status: 204,
headers: corsHeaders
};
}
if (opts.debug) {
azureContext.log(
`[${flowName}] Request: ${request.method} ${request.url}`
);
azureContext.log(
`[${flowName}] Headers:`,
JSON.stringify(normalizeHeaders(request), null, 2)
);
}
try {
const input = await parseRequestBody(request);
const actionContext = await resolveActionContext(
request,
azureContext,
input
);
if (opts.debug) {
azureContext.log(`[${flowName}] Running flow with input:`, input);
}
const runResult = await flow.run(input, { context: actionContext });
const result = runResult.result;
if (opts.debug) {
azureContext.log(`[${flowName}] Flow completed successfully`);
}
return {
status: 200,
headers: corsHeaders,
jsonBody: {
result
}
};
} catch (error) {
azureContext.error(`[${flowName}] Error:`, error);
return buildErrorResponse(error, corsHeaders);
}
}
async function streamingHandler(request, azureContext) {
const requestOrigin = getRequestOrigin(request);
const corsHeaders = buildCorsHeaders(opts.cors, requestOrigin);
if (request.method === "OPTIONS") {
return {
status: 204,
headers: corsHeaders
};
}
if (opts.debug) {
azureContext.log(
`[${flowName}] Stream request: ${request.method} ${request.url}`
);
}
try {
const input = await parseRequestBody(request);
const actionContext = await resolveActionContext(
request,
azureContext,
input
);
const acceptHeader = request.headers.get("accept") || "";
const clientWantsStreaming = acceptHeader.includes("text/event-stream");
if (clientWantsStreaming) {
const encoder = new TextEncoder();
const readableStream = new ReadableStream({
async start(controller) {
try {
const { stream, output } = flow.stream(input, {
context: actionContext
});
for await (const chunk of stream) {
const sseData = `data: ${JSON.stringify({ message: chunk })}
`;
controller.enqueue(encoder.encode(sseData));
}
const result = await output;
const sseFinal = `data: ${JSON.stringify({ result })}
`;
controller.enqueue(encoder.encode(sseFinal));
controller.close();
if (opts.debug) {
azureContext.log(
`[${flowName}] Streaming flow completed successfully`
);
}
} catch (error) {
azureContext.error(`[${flowName}] Stream error:`, error);
const errorData = `data: ${JSON.stringify(getCallableJSON(error))}
`;
controller.enqueue(encoder.encode(errorData));
controller.close();
}
}
});
return {
status: 200,
headers: {
...corsHeaders,
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive"
},
body: readableStream
};
} else {
const runResult = await flow.run(input, {
context: actionContext
});
const result = runResult.result;
return {
status: 200,
headers: corsHeaders,
jsonBody: { result }
};
}
} catch (error) {
azureContext.error(`[${flowName}] Stream error:`, error);
return buildErrorResponse(error, corsHeaders);
}
}
const handler = opts.streaming ? streamingHandler : standardHandler;
const callableFunction = {
handler,
flow,
flowName,
run: async (input, options) => {
const runResult = await flow.run(input, {
context: options?.context
});
return runResult.result;
},
stream: (input, options) => {
return flow.stream(input, {
context: options?.context
});
}
};
const methods = opts.httpMethods ?? ["POST", "OPTIONS"];
const authLevel = opts.authLevel ?? "anonymous";
app.http(flowName, {
methods,
authLevel,
...opts.route ? { route: opts.route } : {},
handler
});
return callableFunction;
}
function requireApiKey(headerName, expectedValueOrValidator) {
const lowerHeaderName = headerName.toLowerCase();
return async (request) => {
const apiKey = request.headers[lowerHeaderName];
if (!apiKey) {
throw new UserFacingError(
"UNAUTHENTICATED",
`Missing required header: ${headerName}`
);
}
if (typeof expectedValueOrValidator === "string") {
if (apiKey !== expectedValueOrValidator) {
throw new UserFacingError("PERMISSION_DENIED", "Invalid API key");
}
} else {
await expectedValueOrValidator(apiKey);
}
return {
auth: { apiKey }
};
};
}
function requireBearerToken(validateToken) {
return async (request) => {
const authHeader = request.headers["authorization"];
if (!authHeader) {
throw new UserFacingError(
"UNAUTHENTICATED",
"Missing Authorization header"
);
}
const match = authHeader.match(/^Bearer\s+(.+)$/i);
if (!match) {
throw new UserFacingError(
"UNAUTHENTICATED",
"Invalid Authorization header format. Expected: Bearer <token>"
);
}
const token = match[1];
return await validateToken(token);
};
}
function requireHeader(headerName, expectedValue) {
const lowerHeaderName = headerName.toLowerCase();
return async (request) => {
const value = request.headers[lowerHeaderName];
if (!value) {
throw new UserFacingError(
"UNAUTHENTICATED",
`Missing required header: ${headerName}`
);
}
if (expectedValue !== void 0 && value !== expectedValue) {
throw new UserFacingError(
"PERMISSION_DENIED",
`Invalid value for header: ${headerName}`
);
}
return {};
};
}
function allowAll() {
return async () => ({});
}
function allOf(...providers) {
return async (request) => {
let mergedContext = {};
for (const provider of providers) {
const context = await provider(request);
mergedContext = { ...mergedContext, ...context };
}
return mergedContext;
};
}
function anyOf(...providers) {
return async (request) => {
let lastError;
for (const provider of providers) {
try {
const context = await provider(request);
return context;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
}
}
throw lastError || new UserFacingError("UNAUTHENTICATED", "Unauthorized");
};
}
var azure_functions_default = onCallGenkit;
export {
allOf,
allowAll,
anyOf,
azure_functions_default as default,
onCallGenkit,
requireApiKey,
requireBearerToken,
requireHeader
};
//# sourceMappingURL=azure_functions.mjs.map