siliconflow-serverless-client
Version:

168 lines (146 loc) • 5.02 kB
text/typescript
export const TARGET_URL_HEADER = "x-siliconflow-target-url";
export const DEFAULT_PROXY_ROUTE = "/api/example/sdk";
const SILICONFLOW_API_KEY = process.env.SILICONFLOW_API_KEY;
export type HeaderValue = string | string[] | undefined | null;
const SILICONFLOW_URL_REG_EXP =
/(\.|^)siliconflow\.(com)$|192\.168\.1\.102||10\.8\.0\.\d/;
/**
* The proxy behavior that is passed to the proxy handler. This is a subset of
* request objects that are used by different frameworks, like Express and NextJS.
*/
export interface ProxyBehavior<ResponseType> {
id: string;
method: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
respondWith(status: number, data: string | any): ResponseType;
getHeaders(): Record<string, HeaderValue>;
getHeader(name: string): HeaderValue;
sendHeader(name: string, value: string): void;
getBody(): Promise<string | undefined>;
resolveApiKey?: () => Promise<string | undefined>;
}
/**
* Utility to get a header value as `string` from a Headers object.
*
* @private
* @param request the header value.
* @returns the header value as `string` or `undefined` if the header is not set.
*/
function singleHeaderValue(value: HeaderValue): string | undefined {
if (!value) {
return undefined;
}
if (Array.isArray(value)) {
return value[0];
}
return value;
}
function HeaderValueHandle(value: HeaderValue): string | undefined {
let token: string | undefined;
if (!value) {
return undefined;
}
if (typeof value === "string" && value.includes("authjs.session-token")) {
const cookiesArray = value.split(";");
for (const cookie of cookiesArray) {
if (cookie.includes("authjs.session-token")) {
token = cookie.split("=")?.[1];
break;
}
}
}
return token;
}
function getSILICONFLOWKey(): string | undefined {
if (SILICONFLOW_API_KEY) {
return SILICONFLOW_API_KEY;
}
return undefined;
}
const EXCLUDED_HEADERS = ["content-length", "content-encoding"];
/**
* A request handler that proxies the request to the fal-serverless
* endpoint. This is useful so client-side calls to the fal-serverless endpoint
* can be made without CORS issues and the correct credentials can be added
* effortlessly.
*
* @param behavior the request proxy behavior.
* @returns Promise<any> the promise that will be resolved once the request is done.
*/
export async function handleRequest<ResponseType>(
behavior: ProxyBehavior<ResponseType>,
) {
// console.log("[behavior]", behavior.getHeaders());
const targetUrl = singleHeaderValue(behavior.getHeader(TARGET_URL_HEADER));
if (!targetUrl) {
return behavior.respondWith(
400,
`Missing the ${TARGET_URL_HEADER} header`,
);
}
const urlHost = new URL(targetUrl).host;
if (!SILICONFLOW_URL_REG_EXP.test(urlHost)) {
return behavior.respondWith(412, `Invalid ${TARGET_URL_HEADER} header`);
}
const falKey = behavior.resolveApiKey
? await behavior.resolveApiKey()
: getSILICONFLOWKey();
if (!falKey) {
return behavior.respondWith(
401,
"Missing SILICONFLOWcost credentials",
);
}
// pass over headers prefixed with x-fal-*
const headers: Record<string, HeaderValue> = {};
Object.keys(behavior.getHeaders()).forEach((key) => {
// if (key.toLowerCase().startsWith("x-SILICONFLOW-")) {
headers[key.toLowerCase()] = behavior.getHeader(key);
// }
});
// console.log("[headers1]", headers);
const proxyUserAgent = `@SILICONFLOW-ai/serverless-proxy/${behavior.id}`;
// console.log("[targetUrl]", HeaderValueHandle(behavior.getHeader("cookie")));
const userAgent = singleHeaderValue(behavior.getHeader("user-agent"));
const res = await fetch(targetUrl, {
method: behavior.method,
headers: {
...headers,
authorization:
singleHeaderValue(behavior.getHeader("authorization")) ??
`Bearer ${falKey}`,
accept: "application/json",
"content-type": "application/json",
"user-agent": userAgent,
"x-SILICONFLOW-client-proxy": proxyUserAgent,
} as HeadersInit,
body:
behavior.method?.toUpperCase() === "GET"
? undefined
: await behavior.getBody(),
});
// console.log("[res]", res);
// copy headers from fal to the proxied response
res.headers.forEach((value, key) => {
if (!EXCLUDED_HEADERS.includes(key.toLowerCase())) {
behavior.sendHeader(key, value);
}
});
if (res.headers.get("content-type")?.includes("application/json")) {
const data = await res.json();
return behavior.respondWith(res.status, data);
}
const data = await res.text();
return behavior.respondWith(res.status, data);
}
export function fromHeaders(
headers: Headers,
): Record<string, string | string[]> {
// TODO once Header.entries() is available, use that instead
// Object.fromEntries(headers.entries());
const result: Record<string, string | string[]> = {};
headers.forEach((value, key) => {
result[key] = value;
});
return result;
}