UNPKG

genkitx-azure-openai

Version:
404 lines (401 loc) 11.4 kB
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