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
13 changes: 12 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@
"default": "./dist/hono.cjs"
}
},
"./vite": {
"import": {
"types": "./dist/vite.d.mts",
"default": "./dist/vite.mjs"
},
"require": {
"types": "./dist/vite.d.cts",
"default": "./dist/vite.cjs"
}
},
"./*": "./*"
},
"publishConfig": {
Expand All @@ -76,6 +86,7 @@
},
"devDependencies": {
"@types/express": "^5.0.6",
"hono": "^4.7.0"
"hono": "^4.7.0",
"vite": "^6.0.0"
}
}
96 changes: 96 additions & 0 deletions packages/sdk/src/lib/vite/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import type { Plugin, Connect } from 'vite';
import type { LLMOpsClient } from '../../client';
import { Readable } from 'node:stream';

function createMiddleware(
client: LLMOpsClient
): Connect.NextHandleFunction {
const basePath = client.config.basePath;

return async (req, res, next) => {
let urlPath = req.url || '/';

// Only handle requests that match the basePath
if (basePath && basePath !== '/' && !urlPath.startsWith(basePath)) {
return next();
}

// Strip the base path if it exists and is not just '/'
if (basePath && basePath !== '/' && urlPath.startsWith(basePath)) {
urlPath = urlPath.slice(basePath.length) || '/';
}

// Build the full URL with host
const protocol = req.headers['x-forwarded-proto'] || 'http';
const host = req.headers.host || 'localhost';
const url = new URL(urlPath, `${protocol}://${host}`);

// Clone headers from incoming request
const headers = new Headers();
for (const [key, value] of Object.entries(req.headers)) {
if (value) {
headers.set(key, Array.isArray(value) ? value.join(', ') : value);
}
}

// Collect request body for non-GET/HEAD requests
let body: string | undefined;
if (!['GET', 'HEAD'].includes(req.method || 'GET')) {
body = await new Promise<string>((resolve, reject) => {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => resolve(data));
req.on('error', reject);
});
}

const request = new Request(url, {
method: req.method,
headers,
body,
});

const response = await client.handler(request);

// Check if response is 404, pass to next middleware
if (response.status === 404) {
return next();
}

// Set response headers
response.headers?.forEach((value, key) => {
res.setHeader(key, value);
});

res.statusCode = response.status;

// Check if this is a streaming response (SSE)
const contentType = response.headers?.get('content-type');
if (contentType?.includes('text/event-stream') && response.body) {
// For SSE streaming, pipe the body directly to avoid buffering
Readable.fromWeb(
response.body as import('stream/web').ReadableStream
).pipe(res);
} else {
// For non-streaming responses, buffer and send
const responseBody = await response.text();
res.end(responseBody);
}
};
}

export function llmopsPlugin(client: LLMOpsClient): Plugin {
const middleware = createMiddleware(client);

return {
name: 'vite-plugin-llmops',
configureServer(server) {
server.middlewares.use(middleware);
},
configurePreviewServer(server) {
server.middlewares.use(middleware);
},
};
}
1 change: 1 addition & 0 deletions packages/sdk/tsdown.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default defineConfig({
index: 'src/index.ts',
express: 'src/lib/express/index.ts',
hono: 'src/lib/hono/index.ts',
vite: 'src/lib/vite/index.ts',
},
format: ['esm', 'cjs'],
dts: true,
Expand Down
60 changes: 60 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.