diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 84ec25ae..01b519db 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -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": { @@ -76,6 +86,7 @@ }, "devDependencies": { "@types/express": "^5.0.6", - "hono": "^4.7.0" + "hono": "^4.7.0", + "vite": "^6.0.0" } } diff --git a/packages/sdk/src/lib/vite/index.ts b/packages/sdk/src/lib/vite/index.ts new file mode 100644 index 00000000..0943a741 --- /dev/null +++ b/packages/sdk/src/lib/vite/index.ts @@ -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((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); + }, + }; +} diff --git a/packages/sdk/tsdown.config.ts b/packages/sdk/tsdown.config.ts index 552ad0d1..9f24c7ca 100644 --- a/packages/sdk/tsdown.config.ts +++ b/packages/sdk/tsdown.config.ts @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eff9f28..b12eed96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -566,6 +566,9 @@ importers: hono: specifier: ^4.7.0 version: 4.10.7 + vite: + specifier: ^6.0.0 + version: 6.4.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) standalone: dependencies: @@ -7564,6 +7567,46 @@ packages: vite: optional: true + vite@6.4.1: + resolution: {integrity: sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + jiti: '>=1.21.0' + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vite@7.2.6: resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -16266,6 +16309,23 @@ snapshots: - supports-color - typescript + vite@6.4.1(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.4 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.8.2 + vite@7.2.6(@types/node@24.10.4)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: esbuild: 0.25.12