Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion packages/proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ We offer built in integrations for the following libraries:
- [Express](./src/express/README.md)
- [Next.js](./src/nextjs/README.md)

You can also create your own custom integration. See docs here.
You can also [create your own custom adapter](#custom-adapters).


## Supported Endpoints
Expand All @@ -33,4 +33,38 @@ The proxy supports all model endpoints, apart from the realtime models.
4. Proxy forwards the request to `https://api.decart.ai`
5. Proxy returns the response to the client

## Custom Adapters

You can create a proxy adapter for any HTTP framework by implementing the `ProxyBehavior` interface with the framework specific implementation, and passing it to `handleRequest()`.

### Example: Fastify Adapter

```typescript
import {
handleRequest,
type DecartProxyOptions,
type ProxyBehavior,
} from "@decartai/proxy";
import type { FastifyRequest, FastifyReply } from "fastify";

export function createDecartProxy(options?: DecartProxyOptions) {
return async (request: FastifyRequest, reply: FastifyReply) => {
const behavior: ProxyBehavior = {
integration: "fastify",
baseUrl: options?.baseUrl,
method: request.method,
getHeaders: () => request.headers as Record<string, string>,
getHeader: (name) => request.headers[name],
getRequestBody: async () => JSON.stringify(request.body),
sendHeader: (name, value) => reply.header(name, value),
respondWith: (status, data) => reply.status(status).send(data),
getRequestPath: () => request.url,
sendResponse: async (response) => {
reply.status(response.status).send(response.body);
},
};

await handleRequest(behavior);
};
}
```
6 changes: 3 additions & 3 deletions packages/proxy/src/core/proxy-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,10 @@ export async function handleRequest<ResponseType>(behavior: ProxyBehavior<Respon
const requestPath = behavior.getRequestPath();
const targetUrl = new URL(requestPath, baseUrl);

// pass over headers prefixed with x-decart-*
const adapterId = behavior.id ?? "custom";
const proxyUserAgent = behavior.integration
? `@decart-ai/server-proxy/${behavior.id} (integration: ${behavior.integration})`
: `@decart-ai/server-proxy/${behavior.id}`;
? `@decart-ai/server-proxy/${adapterId} (integration: ${behavior.integration})`
: `@decart-ai/server-proxy/${adapterId}`;

const userAgent = singleHeaderValue(behavior.getHeader("user-agent"));
const requestBody = await behavior.getRequestBody();
Expand Down
78 changes: 76 additions & 2 deletions packages/proxy/src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,92 @@ export type DecartProxyOptions = {

export type HeaderValue = string | string[] | undefined | null;

export interface ProxyBehavior<ResponseType> {
id: string;
export interface ProxyBehavior<ResponseType = void> {
/**
* Internal identifier for built-in adapters. Custom adapters should use
* `integration` instead to identify themselves.
*/
id?: string;

/**
* HTTP method of the incoming request (GET, POST, etc.)
*/
method: string;

/**
* Optional API key for authenticating with the Decart API.
* If not provided, falls back to the DECART_API_KEY environment variable.
*/
apiKey?: string;

/**
* Optional base URL for the Decart API.
* Defaults to "https://api.decart.ai"
*/
baseUrl?: string;

/**
* Optional integration identifier included in the User-Agent header.
*/
integration?: string;

/**
* Send an error response to the client.
* Called when the proxy encounters an error (e.g., missing API key).
*
* @param status - HTTP status code
* @param data - Error message or data to send
* @returns The framework's response type
*/
// biome-ignore lint/suspicious/noExplicitAny: data can be any type
respondWith(status: number, data: string | any): ResponseType;

/**
* Forward the successful proxy response to the client.
* Handle the response body as appropriate for your framework.
*
* @param response - The fetch Response from the Decart API
* @returns Promise resolving to the framework's response type
*/
sendResponse(response: Response): Promise<ResponseType>;

/**
* Get all request headers as a record.
* Header names should be lowercase for consistent matching.
*
* @returns Record of header names to values
*/
getHeaders(): Record<string, HeaderValue>;

/**
* Get a specific request header value.
*
* @param name - Header name
* @returns The header value, or undefined if not present
*/
getHeader(name: string): HeaderValue;

/**
* Set a response header to forward to the client.
*
* @param name - Header name
* @param value - Header value
*/
sendHeader(name: string, value: string): void;

/**
* Get the request body.
* Return as ArrayBuffer to preserve binary data (e.g., for FormData/multipart).
*
* @returns Promise resolving to the request body, or undefined if no body
*/
getRequestBody(): Promise<string | ArrayBuffer | undefined>;

/**
* Get the request path to proxy to the Decart API.
* Should return the path after your proxy route (e.g., "/v1/generate/lucy-pro-t2i").
*
* @returns The request path
*/
getRequestPath(): string;
}
4 changes: 2 additions & 2 deletions packages/proxy/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { handleRequest } from "./core/proxy-handler";
export type { DecartProxyOptions } from "./core/types";
export { DEFAULT_PROXY_ROUTE, fromHeaders, handleRequest } from "./core/proxy-handler";
export type { DecartProxyOptions, HeaderValue, ProxyBehavior } from "./core/types";
Loading