diff --git a/packages/responses-server/.eslintignore b/packages/responses-server/.eslintignore new file mode 100644 index 0000000000..9edb9afc9d --- /dev/null +++ b/packages/responses-server/.eslintignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/packages/responses-server/.prettierignore b/packages/responses-server/.prettierignore new file mode 100644 index 0000000000..d95d49a2ec --- /dev/null +++ b/packages/responses-server/.prettierignore @@ -0,0 +1,4 @@ +pnpm-lock.yaml +# In order to avoid code samples to have tabs, they don't display well on npm +README.md +dist \ No newline at end of file diff --git a/packages/responses-server/README.md b/packages/responses-server/README.md new file mode 100644 index 0000000000..ee1e7aee4c --- /dev/null +++ b/packages/responses-server/README.md @@ -0,0 +1,44 @@ +# @huggingface/responses-server + +A lightweight Express.js server supporting Responses API on top of Inference Provider Chat Completion API. + +## 📁 Project Structure + +``` +responses-server/ +├── src/ +│ ├── index.ts +│ ├── server.ts # Express app configuration (e.g. route definition) +│ ├── routes/ # Routes implementation +│ ├── middleware/ # Middlewares (validation + logging) +│ └── schemas/ # Zod validation schemas +├── scripts/ # Utility scripts +├── package.json # Package configuration +``` + +## 🚀 Quick Start + +### Development + +```bash +# Install dependencies +pnpm install + +# Start development server +pnpm dev +``` + +### Run examples + +Some example scripts are implemented in ./examples. + +You can run them using + +```bash +# Run ./examples/text.js +pnpm run example text + +# Run ./examples/multi_turn.js +pnpm run example multi_turn +``` + diff --git a/packages/responses-server/examples/function.js b/packages/responses-server/examples/function.js new file mode 100644 index 0000000000..26893d5449 --- /dev/null +++ b/packages/responses-server/examples/function.js @@ -0,0 +1,32 @@ +import OpenAI from "openai"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const tools = [ + { + type: "function", + name: "get_current_weather", + description: "Get the current weather in a given location", + parameters: { + type: "object", + properties: { + location: { + type: "string", + description: "The city and state, e.g. San Francisco, CA", + }, + unit: { type: "string", enum: ["celsius", "fahrenheit"] }, + }, + required: ["location", "unit"], + }, + }, +]; + +const response = await openai.responses.create({ + model: "meta-llama/Llama-3.3-70B-Instruct", + provider: "cerebras", + tools: tools, + input: "What is the weather like in Boston today?", + tool_choice: "auto", +}); + +console.log(response); diff --git a/packages/responses-server/examples/function_streaming.js b/packages/responses-server/examples/function_streaming.js new file mode 100644 index 0000000000..3c6d557ef0 --- /dev/null +++ b/packages/responses-server/examples/function_streaming.js @@ -0,0 +1,33 @@ +import { OpenAI } from "openai"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const tools = [ + { + type: "function", + name: "get_weather", + description: "Get current temperature for provided coordinates in celsius.", + parameters: { + type: "object", + properties: { + latitude: { type: "number" }, + longitude: { type: "number" }, + }, + required: ["latitude", "longitude"], + additionalProperties: false, + }, + strict: true, + }, +]; + +const stream = await openai.responses.create({ + model: "meta-llama/Llama-3.3-70B-Instruct", + provider: "cerebras", + input: [{ role: "user", content: "What's the weather like in Paris today?" }], + tools, + stream: true, +}); + +for await (const event of stream) { + console.log(event); +} diff --git a/packages/responses-server/examples/image.js b/packages/responses-server/examples/image.js new file mode 100644 index 0000000000..7c729d2440 --- /dev/null +++ b/packages/responses-server/examples/image.js @@ -0,0 +1,23 @@ +import OpenAI from "openai"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const response = await openai.responses.create({ + model: "Qwen/Qwen2.5-VL-7B-Instruct", + input: [ + { + role: "user", + content: [ + { type: "input_text", text: "what is in this image?" }, + { + type: "input_image", + image_url: + "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg", + }, + ], + }, + ], +}); + +console.log(response); +console.log(response.output_text); diff --git a/packages/responses-server/examples/multi_turn.js b/packages/responses-server/examples/multi_turn.js new file mode 100644 index 0000000000..0258805696 --- /dev/null +++ b/packages/responses-server/examples/multi_turn.js @@ -0,0 +1,20 @@ +import OpenAI from "openai"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const response = await openai.responses.create({ + model: "Qwen/Qwen2.5-VL-7B-Instruct", + input: [ + { + role: "developer", + content: "Talk like a pirate.", + }, + { + role: "user", + content: "Are semicolons optional in JavaScript?", + }, + ], +}); + +console.log(response); +console.log(response.output_text); diff --git a/packages/responses-server/examples/streaming.js b/packages/responses-server/examples/streaming.js new file mode 100644 index 0000000000..2d342d67de --- /dev/null +++ b/packages/responses-server/examples/streaming.js @@ -0,0 +1,17 @@ +import { OpenAI } from "openai"; +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const stream = await openai.responses.create({ + model: "Qwen/Qwen2.5-VL-7B-Instruct", + input: [ + { + role: "user", + content: "Say 'double bubble bath' ten times fast.", + }, + ], + stream: true, +}); + +for await (const event of stream) { + console.log(event); +} diff --git a/packages/responses-server/examples/structured_output.js b/packages/responses-server/examples/structured_output.js new file mode 100644 index 0000000000..e1496b2006 --- /dev/null +++ b/packages/responses-server/examples/structured_output.js @@ -0,0 +1,32 @@ +import OpenAI from "openai"; +import { zodTextFormat } from "openai/helpers/zod"; +import { z } from "zod"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const Step = z.object({ + explanation: z.string(), + output: z.string(), +}); + +const MathReasoning = z.object({ + steps: z.array(Step), + final_answer: z.string(), +}); + +const response = await openai.responses.parse({ + model: "Qwen/Qwen2.5-VL-72B-Instruct", + provider: "nebius", + input: [ + { + role: "system", + content: "You are a helpful math tutor. Guide the user through the solution step by step.", + }, + { role: "user", content: "how can I solve 8x + 7 = -23" }, + ], + text: { + format: zodTextFormat(MathReasoning, "math_reasoning"), + }, +}); + +console.log(response.output_parsed); diff --git a/packages/responses-server/examples/structured_output_streaming.js b/packages/responses-server/examples/structured_output_streaming.js new file mode 100644 index 0000000000..bdd8c1cf1e --- /dev/null +++ b/packages/responses-server/examples/structured_output_streaming.js @@ -0,0 +1,36 @@ +import { OpenAI } from "openai"; +import { zodTextFormat } from "openai/helpers/zod"; +import { z } from "zod"; + +const CalendarEvent = z.object({ + name: z.string(), + date: z.string(), + participants: z.array(z.string()), +}); + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); +const stream = openai.responses + .stream({ + model: "Qwen/Qwen2.5-VL-72B-Instruct", + provider: "nebius", + instructions: "Extract the event information.", + input: "Alice and Bob are going to a science fair on Friday.", + text: { + format: zodTextFormat(CalendarEvent, "calendar_event"), + }, + }) + .on("response.refusal.delta", (event) => { + process.stdout.write(event.delta); + }) + .on("response.output_text.delta", (event) => { + process.stdout.write(event.delta); + }) + .on("response.output_text.done", () => { + process.stdout.write("\n"); + }) + .on("response.error", (event) => { + console.error(event.error); + }); + +const result = await stream.finalResponse(); +console.log(result.output_parsed); diff --git a/packages/responses-server/examples/text.js b/packages/responses-server/examples/text.js new file mode 100755 index 0000000000..7abd23a864 --- /dev/null +++ b/packages/responses-server/examples/text.js @@ -0,0 +1,12 @@ +import OpenAI from "openai"; + +const openai = new OpenAI({ baseURL: "http://localhost:3000/v1", apiKey: process.env.HF_TOKEN }); + +const response = await openai.responses.create({ + model: "Qwen/Qwen2.5-VL-7B-Instruct", + instructions: "You are a helpful assistant.", + input: "Tell me a three sentence bedtime story about a unicorn.", +}); + +console.log(response); +console.log(response.output_text); diff --git a/packages/responses-server/package.json b/packages/responses-server/package.json new file mode 100644 index 0000000000..9e30447382 --- /dev/null +++ b/packages/responses-server/package.json @@ -0,0 +1,63 @@ +{ + "name": "@huggingface/responses-server", + "packageManager": "pnpm@10.10.0", + "version": "0.1.0", + "type": "module", + "description": "Server for handling AI responses", + "repository": "https://github.com/huggingface/huggingface.js.git", + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + } + }, + "engines": { + "node": ">=18" + }, + "source": "index.ts", + "scripts": { + "build": "tsup src/*.ts --format cjs,esm --clean && tsc --emitDeclarationOnly --declaration", + "check": "tsc", + "dev": "tsx watch src/index.ts", + "format": "prettier --write .", + "format:check": "prettier --check .", + "lint": "eslint --quiet --fix --ext .cjs,.ts .", + "lint:check": "eslint --ext .cjs,.ts .", + "prepublishOnly": "pnpm run build", + "prepare": "pnpm run build", + "start": "node dist/index.js", + "example": "node scripts/run-example.js" + }, + "files": [ + "src", + "dist", + "tsconfig.json" + ], + "keywords": [ + "huggingface", + "ai", + "llm", + "responses-api", + "server" + ], + "author": "Hugging Face", + "license": "MIT", + "dependencies": { + "@huggingface/inference": "workspace:^", + "@huggingface/tasks": "workspace:^", + "express": "^4.18.2", + "openai": "^5.8.2", + "zod": "^3.22.4" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "tsx": "^4.7.0" + } +} diff --git a/packages/responses-server/pnpm-lock.yaml b/packages/responses-server/pnpm-lock.yaml new file mode 100644 index 0000000000..b3c8d02c5a --- /dev/null +++ b/packages/responses-server/pnpm-lock.yaml @@ -0,0 +1,999 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@huggingface/inference': + specifier: workspace:^ + version: link:../inference + '@huggingface/tasks': + specifier: workspace:^ + version: link:../tasks + express: + specifier: ^4.18.2 + version: 4.21.2 + openai: + specifier: ^5.8.2 + version: 5.8.2(zod@3.25.67) + zod: + specifier: ^3.22.4 + version: 3.25.67 + devDependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.23 + tsx: + specifier: ^4.7.0 + version: 4.20.3 + +packages: + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/express-serve-static-core@4.19.6': + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} + + '@types/express@4.17.23': + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@24.0.7': + resolution: {integrity: sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.5': + resolution: {integrity: sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==} + + '@types/serve-static@1.15.8': + resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + body-parser@1.20.3: + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + + cookie@0.7.1: + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} + engines: {node: '>= 0.6'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + + finalhandler@1.3.1: + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} + engines: {node: '>= 0.8'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + openai@5.8.2: + resolution: {integrity: sha512-8C+nzoHYgyYOXhHGN6r0fcb4SznuEn1R7YZMvlqDbnCuE0FM2mm3T1HiYW6WIcMS/F1Of2up/cSPjLPaWt0X9Q==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.2: + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} + engines: {node: '>= 0.8'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@0.19.0: + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} + engines: {node: '>= 0.8.0'} + + serve-static@1.16.2: + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} + engines: {node: '>= 0.8.0'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tsx@4.20.3: + resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + zod@3.25.67: + resolution: {integrity: sha512-idA2YXwpCdqUSKRCACDE6ItZD9TZzy3OZMtpfLoh6oPR47lipysRrJfjzMqFxQ3uJuUPyUeWe1r9vLH33xO/Qw==} + +snapshots: + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.0.7 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.0.7 + + '@types/express-serve-static-core@4.19.6': + dependencies: + '@types/node': 24.0.7 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 0.17.5 + + '@types/express@4.17.23': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.6 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.8 + + '@types/http-errors@2.0.5': {} + + '@types/mime@1.3.5': {} + + '@types/node@24.0.7': + dependencies: + undici-types: 7.8.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.5': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 24.0.7 + + '@types/serve-static@1.15.8': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.0.7 + '@types/send': 0.17.5 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + array-flatten@1.1.1: {} + + body-parser@1.20.3: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.13.0 + raw-body: 2.5.2 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.0.6: {} + + cookie@0.7.1: {} + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + + encodeurl@1.0.2: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + express@4.21.2: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.1: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + merge-descriptors@1.0.3: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + negotiator@0.6.3: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + openai@5.8.2(zod@3.25.67): + optionalDependencies: + zod: 3.25.67 + + parseurl@1.3.3: {} + + path-to-regexp@0.1.12: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.13.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + resolve-pkg-maps@1.0.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@0.19.0: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.2: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.1: {} + + toidentifier@1.0.1: {} + + tsx@4.20.3: + dependencies: + esbuild: 0.25.5 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + undici-types@7.8.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + vary@1.1.2: {} + + zod@3.25.67: {} diff --git a/packages/responses-server/scripts/run-example.js b/packages/responses-server/scripts/run-example.js new file mode 100644 index 0000000000..8175a5de8d --- /dev/null +++ b/packages/responses-server/scripts/run-example.js @@ -0,0 +1,28 @@ +#!/usr/bin/env node +/** + * AI-generated file using Cursor + Claude 4 + * + * Run an example script + */ +import { spawnSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const [, , exampleName] = process.argv; +if (!exampleName) { + console.error("Usage: run-example.js "); + process.exit(1); +} + +const examplePath = path.resolve(__dirname, "../examples", `${exampleName}.js`); +if (!fs.existsSync(examplePath)) { + console.error(`Example script not found: ${examplePath}`); + process.exit(1); +} + +const result = spawnSync("node", [examplePath], { stdio: "inherit" }); +process.exit(result.status); diff --git a/packages/responses-server/src/index.ts b/packages/responses-server/src/index.ts new file mode 100644 index 0000000000..5572c948ef --- /dev/null +++ b/packages/responses-server/src/index.ts @@ -0,0 +1,26 @@ +import { createApp } from "./server.js"; + +const app = createApp(); +const port = process.env.PORT || 3000; + +// Start server +app.listen(port, () => { + console.log(`🚀 Server started at ${new Date().toISOString()}`); + console.log(`🌐 Server is running on http://localhost:${port}`); + console.log("─".repeat(60)); +}); + +// Graceful shutdown logging +process.on("SIGINT", () => { + console.log("─".repeat(60)); + console.log(`🛑 Server shutting down at ${new Date().toISOString()}`); + process.exit(0); +}); + +process.on("SIGTERM", () => { + console.log("─".repeat(60)); + console.log(`🛑 Server shutting down at ${new Date().toISOString()}`); + process.exit(0); +}); + +export default app; diff --git a/packages/responses-server/src/lib/generateUniqueId.ts b/packages/responses-server/src/lib/generateUniqueId.ts new file mode 100644 index 0000000000..e27fc6cafe --- /dev/null +++ b/packages/responses-server/src/lib/generateUniqueId.ts @@ -0,0 +1,11 @@ +/** + * AI-generated file using Cursor + Claude 4 + * + * Generate a unique ID for the response + */ +import { randomBytes } from "crypto"; + +export function generateUniqueId(prefix?: string): string { + const id = randomBytes(24).toString("hex"); + return prefix ? `${prefix}_${id}` : id; +} diff --git a/packages/responses-server/src/middleware/logging.ts b/packages/responses-server/src/middleware/logging.ts new file mode 100644 index 0000000000..96cf707a7c --- /dev/null +++ b/packages/responses-server/src/middleware/logging.ts @@ -0,0 +1,68 @@ +/** + * AI-generated file using Cursor + Claude 4 + * + * Middleware to log all HTTP requests with duration, status code, method, and route + */ +import { type Request, type Response, type NextFunction } from "express"; + +interface LogContext { + timestamp: string; + method: string; + url: string; + statusCode?: number; + duration?: number; +} + +function formatLogMessage(context: LogContext): string { + const { timestamp, method, url, statusCode, duration } = context; + + if (statusCode === undefined) { + return `[${timestamp}] đŸ“Ĩ ${method} ${url}`; + } + + const statusEmoji = + statusCode >= 200 && statusCode < 300 + ? "✅" + : statusCode >= 400 && statusCode < 500 + ? "âš ī¸" + : statusCode >= 500 + ? "❌" + : "â„šī¸"; + return `[${timestamp}] ${statusEmoji} ${statusCode} ${method} ${url} (${duration}ms)`; +} + +/** + * Middleware to log all HTTP requests with duration, status code, method, and route + */ +export function requestLogger() { + return (req: Request, res: Response, next: NextFunction): void => { + const startTime = Date.now(); + const { method, url } = req; + + // Log incoming request + console.log( + formatLogMessage({ + timestamp: new Date().toISOString(), + method, + url, + }) + ); + + // Listen for when the response finishes + res.on("finish", () => { + const duration = Date.now() - startTime; + + console.log( + formatLogMessage({ + timestamp: new Date().toISOString(), + method, + url, + statusCode: res.statusCode, + duration, + }) + ); + }); + + next(); + }; +} diff --git a/packages/responses-server/src/middleware/validation.ts b/packages/responses-server/src/middleware/validation.ts new file mode 100644 index 0000000000..40c84a43bf --- /dev/null +++ b/packages/responses-server/src/middleware/validation.ts @@ -0,0 +1,42 @@ +/** + * AI-generated file using Cursor + Claude 4 + */ + +import { type Request, type Response, type NextFunction } from "express"; +import { z } from "zod"; + +/** + * Middleware to validate request body against a Zod schema + * @param schema - Zod schema to validate against + * @returns Express middleware function + */ +export function validateBody(schema: T) { + return (req: Request, res: Response, next: NextFunction): void => { + try { + const validatedBody = schema.parse(req.body); + req.body = validatedBody; + next(); + } catch (error) { + if (error instanceof z.ZodError) { + console.log(req.body); + res.status(400).json({ + success: false, + error: error.errors, + details: error.errors, + }); + } else { + res.status(500).json({ + success: false, + error: "Internal server error", + }); + } + } + }; +} + +/** + * Type helper to create a properly typed request with validated body + */ +export interface ValidatedRequest extends Request { + body: T; +} diff --git a/packages/responses-server/src/routes/index.ts b/packages/responses-server/src/routes/index.ts new file mode 100644 index 0000000000..19955457de --- /dev/null +++ b/packages/responses-server/src/routes/index.ts @@ -0,0 +1 @@ +export { postCreateResponse } from "./responses.js"; diff --git a/packages/responses-server/src/routes/responses.ts b/packages/responses-server/src/routes/responses.ts new file mode 100644 index 0000000000..514e843b28 --- /dev/null +++ b/packages/responses-server/src/routes/responses.ts @@ -0,0 +1,426 @@ +import { type Response as ExpressResponse } from "express"; +import { type ValidatedRequest } from "../middleware/validation.js"; +import { type CreateResponseParams } from "../schemas.js"; +import { generateUniqueId } from "../lib/generateUniqueId.js"; +import { InferenceClient } from "@huggingface/inference"; +import type { + ChatCompletionInputMessage, + ChatCompletionInputMessageChunkType, + ChatCompletionInput, +} from "@huggingface/tasks"; + +import type { + Response, + ResponseStreamEvent, + ResponseContentPartAddedEvent, + ResponseOutputMessage, + ResponseFunctionToolCall, +} from "openai/resources/responses/responses"; + +class StreamingError extends Error { + constructor(message: string) { + super(message); + this.name = "StreamingError"; + } +} + +export const postCreateResponse = async ( + req: ValidatedRequest, + res: ExpressResponse +): Promise => { + const apiKey = req.headers.authorization?.split(" ")[1]; + + if (!apiKey) { + res.status(401).json({ + success: false, + error: "Unauthorized", + }); + return; + } + + const client = new InferenceClient(apiKey); + const messages: ChatCompletionInputMessage[] = req.body.instructions + ? [{ role: "system", content: req.body.instructions }] + : []; + + if (Array.isArray(req.body.input)) { + messages.push( + ...req.body.input + .map((item) => ({ + role: item.role, + content: + typeof item.content === "string" + ? item.content + : item.content + .map((content) => { + switch (content.type) { + case "input_image": + return { + type: "image_url" as ChatCompletionInputMessageChunkType, + image_url: { + url: content.image_url, + }, + }; + case "output_text": + return content.text + ? { + type: "text" as ChatCompletionInputMessageChunkType, + text: content.text, + } + : undefined; + case "refusal": + return undefined; + case "input_text": + return { + type: "text" as ChatCompletionInputMessageChunkType, + text: content.text, + }; + } + }) + .filter((item) => item !== undefined), + })) + .filter((message) => message.content?.length !== 0) + ); + } else { + messages.push({ role: "user", content: req.body.input }); + } + + const model = req.body.model.includes("@") ? req.body.model.split("@")[1] : req.body.model; + const provider = req.body.model.includes("@") ? req.body.model.split("@")[0] : undefined; + + const payload: ChatCompletionInput = { + // main params + model: model, + provider: provider, + messages: messages, + stream: req.body.stream, + // options + max_tokens: req.body.max_output_tokens === null ? undefined : req.body.max_output_tokens, + response_format: req.body.text?.format + ? { + type: req.body.text.format.type, + json_schema: + req.body.text.format.type === "json_schema" + ? { + description: req.body.text.format.description, + name: req.body.text.format.name, + schema: req.body.text.format.schema, + strict: req.body.text.format.strict, + } + : undefined, + } + : undefined, + temperature: req.body.temperature, + tool_choice: + typeof req.body.tool_choice === "string" + ? req.body.tool_choice + : req.body.tool_choice + ? { + type: "function", + function: { + name: req.body.tool_choice.name, + }, + } + : undefined, + tools: req.body.tools + ? req.body.tools.map((tool) => ({ + type: tool.type, + function: { + name: tool.name, + parameters: tool.parameters, + description: tool.description, + strict: tool.strict, + }, + })) + : undefined, + top_p: req.body.top_p, + }; + + const responseObject: Omit = { + created_at: new Date().getTime(), + error: null, + id: generateUniqueId("resp"), + instructions: req.body.instructions, + max_output_tokens: req.body.max_output_tokens, + metadata: req.body.metadata, + model: req.body.model, + object: "response", + output: [], + // parallel_tool_calls: req.body.parallel_tool_calls, + status: "in_progress", + text: req.body.text, + tool_choice: req.body.tool_choice ?? "auto", + tools: req.body.tools ?? [], + temperature: req.body.temperature, + top_p: req.body.top_p, + }; + + if (req.body.stream) { + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Connection", "keep-alive"); + let sequenceNumber = 0; + + // Emit events in sequence + const emitEvent = (event: ResponseStreamEvent) => { + res.write(`data: ${JSON.stringify(event)}\n\n`); + }; + + try { + // Response created event + emitEvent({ + type: "response.created", + response: responseObject as Response, + sequence_number: sequenceNumber++, + }); + + // Response in progress event + emitEvent({ + type: "response.in_progress", + response: responseObject as Response, + sequence_number: sequenceNumber++, + }); + + const stream = client.chatCompletionStream(payload); + + for await (const chunk of stream) { + if (chunk.choices[0].delta.content) { + if (responseObject.output.length === 0) { + const outputObject: ResponseOutputMessage = { + id: generateUniqueId("msg"), + type: "message", + role: "assistant", + status: "in_progress", + content: [], + }; + responseObject.output = [outputObject]; + + // Response output item added event + emitEvent({ + type: "response.output_item.added", + output_index: 0, + item: outputObject, + sequence_number: sequenceNumber++, + }); + } + + const outputObject = responseObject.output.at(-1); + if (!outputObject || outputObject.type !== "message") { + throw new StreamingError("Not implemented: only single output item type is supported in streaming mode."); + } + + if (outputObject.content.length === 0) { + // Response content part added event + const contentPart: ResponseContentPartAddedEvent["part"] = { + type: "output_text", + text: "", + annotations: [], + }; + outputObject.content.push(contentPart); + + emitEvent({ + type: "response.content_part.added", + item_id: outputObject.id, + output_index: 0, + content_index: 0, + part: contentPart, + sequence_number: sequenceNumber++, + }); + } + + const contentPart = outputObject.content.at(-1); + if (!contentPart || contentPart.type !== "output_text") { + throw new StreamingError("Not implemented: only output_text is supported in streaming mode."); + } + + if (contentPart.type !== "output_text") { + throw new StreamingError("Not implemented: only output_text is supported in streaming mode."); + } + + // Add text delta + contentPart.text += chunk.choices[0].delta.content; + emitEvent({ + type: "response.output_text.delta", + item_id: outputObject.id, + output_index: 0, + content_index: 0, + delta: chunk.choices[0].delta.content, + sequence_number: sequenceNumber++, + }); + } else if (chunk.choices[0].delta.tool_calls && chunk.choices[0].delta.tool_calls.length > 0) { + if (chunk.choices[0].delta.tool_calls.length > 1) { + throw new StreamingError("Not implemented: only single tool call is supported in streaming mode."); + } + + if (responseObject.output.length === 0) { + if (!chunk.choices[0].delta.tool_calls[0].function.name) { + throw new StreamingError("Tool call function name is required."); + } + + const outputObject: ResponseFunctionToolCall = { + type: "function_call", + id: generateUniqueId("fc"), + call_id: chunk.choices[0].delta.tool_calls[0].id, + name: chunk.choices[0].delta.tool_calls[0].function.name, + arguments: "", + }; + responseObject.output = [outputObject]; + + // Response output item added event + emitEvent({ + type: "response.output_item.added", + output_index: 0, + item: outputObject, + sequence_number: sequenceNumber++, + }); + } + + const outputObject = responseObject.output.at(-1); + if (!outputObject || !outputObject.id || outputObject.type !== "function_call") { + throw new StreamingError("Not implemented: can only support single output item type in streaming mode."); + } + + outputObject.arguments += chunk.choices[0].delta.tool_calls[0].function.arguments; + emitEvent({ + type: "response.function_call_arguments.delta", + item_id: outputObject.id, + output_index: 0, + delta: chunk.choices[0].delta.tool_calls[0].function.arguments, + sequence_number: sequenceNumber++, + }); + } + } + + const lastOutputItem = responseObject.output.at(-1); + + if (lastOutputItem) { + if (lastOutputItem?.type === "message") { + const contentPart = lastOutputItem.content.at(-1); + if (contentPart?.type === "output_text") { + emitEvent({ + type: "response.output_text.done", + item_id: lastOutputItem.id, + output_index: responseObject.output.length - 1, + content_index: lastOutputItem.content.length - 1, + text: contentPart.text, + sequence_number: sequenceNumber++, + }); + + emitEvent({ + type: "response.content_part.done", + item_id: lastOutputItem.id, + output_index: responseObject.output.length - 1, + content_index: lastOutputItem.content.length - 1, + part: contentPart, + sequence_number: sequenceNumber++, + }); + } else { + throw new StreamingError("Not implemented: only output_text is supported in streaming mode."); + } + + // Response output item done event + lastOutputItem.status = "completed"; + emitEvent({ + type: "response.output_item.done", + output_index: responseObject.output.length - 1, + item: lastOutputItem, + sequence_number: sequenceNumber++, + }); + } else if (lastOutputItem?.type === "function_call") { + if (!lastOutputItem.id) { + throw new StreamingError("Function call id is required."); + } + + emitEvent({ + type: "response.function_call_arguments.done", + item_id: lastOutputItem.id, + output_index: responseObject.output.length - 1, + arguments: lastOutputItem.arguments, + sequence_number: sequenceNumber++, + }); + + lastOutputItem.status = "completed"; + emitEvent({ + type: "response.output_item.done", + output_index: responseObject.output.length - 1, + item: lastOutputItem, + sequence_number: sequenceNumber++, + }); + } else { + throw new StreamingError("Not implemented: only message output is supported in streaming mode."); + } + } + + // Response completed event + responseObject.status = "completed"; + emitEvent({ + type: "response.completed", + response: responseObject as Response, + sequence_number: sequenceNumber++, + }); + } catch (streamError) { + console.error("Error in streaming chat completion:", streamError); + + let message = "An error occurred while streaming from inference server."; + if (streamError instanceof StreamingError) { + message = streamError.message; + } else if ( + typeof streamError === "object" && + streamError && + "message" in streamError && + typeof streamError.message === "string" + ) { + message = streamError.message; + } + + emitEvent({ + type: "error", + code: null, + message, + param: null, + sequence_number: sequenceNumber++, + }); + } + res.end(); + return; + } + + try { + const chatCompletionResponse = await client.chatCompletion(payload); + + responseObject.status = "completed"; + responseObject.output = chatCompletionResponse.choices[0].message.content + ? [ + { + id: generateUniqueId("msg"), + type: "message", + role: "assistant", + status: "completed", + content: [ + { + type: "output_text", + text: chatCompletionResponse.choices[0].message.content, + annotations: [], + }, + ], + }, + ] + : chatCompletionResponse.choices[0].message.tool_calls + ? chatCompletionResponse.choices[0].message.tool_calls.map((toolCall) => ({ + type: "function_call", + id: generateUniqueId("fc"), + call_id: toolCall.id, + name: toolCall.function.name, + arguments: toolCall.function.arguments, + status: "completed", + })) + : []; + + res.json(responseObject); + } catch (error) { + console.error(error); + res.status(500).json({ + success: false, + error: error instanceof Error ? error.message : "Unknown error", + }); + } +}; diff --git a/packages/responses-server/src/schemas.ts b/packages/responses-server/src/schemas.ts new file mode 100644 index 0000000000..5fd12020a0 --- /dev/null +++ b/packages/responses-server/src/schemas.ts @@ -0,0 +1,144 @@ +import { z } from "zod"; + +/** + * https://platform.openai.com/docs/api-reference/responses/create + * commented out properties are not supported by the server + */ + +const inputContentSchema = z.array( + z.union([ + z.object({ + type: z.literal("input_text"), + text: z.string(), + }), + z.object({ + type: z.literal("input_image"), + // file_id: z.string().nullable().default(null), + image_url: z.string(), + // detail: z.enum(["auto", "low", "high"]).default("auto"), + }), + // z.object({ + // type: z.literal("input_file"), + // file_data: z.string().nullable().default(null), + // file_id: z.string().nullable().default(null), + // filename: z.string().nullable().default(null), + // }), + ]) +); + +export const createResponseParamsSchema = z.object({ + // background: z.boolean().default(false), + // include: + input: z.union([ + z.string(), + z.array( + z.union([ + z.object({ + content: z.union([z.string(), inputContentSchema]), + role: z.enum(["user", "assistant", "system", "developer"]), + type: z.enum(["message"]).default("message"), + }), + z.object({ + role: z.enum(["user", "system", "developer"]), + status: z.enum(["in_progress", "completed", "incomplete"]).nullable().default(null), + content: inputContentSchema, + type: z.enum(["message"]).default("message"), + }), + z.object({ + id: z.string().optional(), + role: z.enum(["assistant"]), + status: z.enum(["in_progress", "completed", "incomplete"]).optional(), + type: z.enum(["message"]).default("message"), + content: z.array( + z.union([ + z.object({ + type: z.literal("output_text"), + text: z.string(), + annotations: z.array(z.object({})).optional(), // TODO: incomplete + logprobs: z.array(z.object({})).optional(), // TODO: incomplete + }), + z.object({ + type: z.literal("refusal"), + refusal: z.string(), + }), + // TODO: much more objects: File search tool call, Computer tool call, Computer tool call output, Web search tool call, Function tool call, Function tool call output, Reasoning, Image generation call, Code interpreter tool call, Local shell call, Local shell call output, MCP list tools, MCP approval request, MCP approval response, MCP tool call + ]) + ), + }), + // z.object({ + // id: z.string(), + // type: z.enum(["item_reference"]).default("item_reference"), + // }), + ]) + ), + ]), + instructions: z.string().nullable().default(null), + max_output_tokens: z.number().min(0).nullable().default(null), + // max_tool_calls: z.number().min(0).nullable().default(null), + metadata: z + .record(z.string().max(64), z.string().max(512)) + .refine((val) => Object.keys(val).length <= 16, { + message: "Must have at most 16 items", + }) + .nullable() + .default(null), + model: z.string(), + // parallel_tool_calls: z.boolean().default(true), // TODO: how to handle this if chat completion doesn't? + // previous_response_id: z.string().nullable().default(null), + // reasoning: z.object({ + // effort: z.enum(["low", "medium", "high"]).default("medium"), + // summary: z.enum(["auto", "concise", "detailed"]).nullable().default(null), + // }), + // store: z.boolean().default(true), + stream: z.boolean().default(false), + temperature: z.number().min(0).max(2).default(1), + text: z + .object({ + format: z.union([ + z.object({ + type: z.literal("text"), + }), + z.object({ + type: z.literal("json_object"), + }), + z.object({ + type: z.literal("json_schema"), + name: z + .string() + .max(64, "Must be at most 64 characters") + .regex(/^[a-zA-Z0-9_-]+$/, "Only letters, numbers, underscores, and dashes are allowed"), + description: z.string().optional(), + schema: z.record(z.any()), + strict: z.boolean().default(false), + }), + ]), + }) + .optional(), + tool_choice: z + .union([ + z.enum(["auto", "none", "required"]), + z.object({ + type: z.enum(["function"]), + name: z.string(), + }), + // TODO: also hosted tool and MCP tool + ]) + .optional(), + tools: z + .array( + z.object({ + name: z.string(), + parameters: z.record(z.any()), + strict: z.boolean().default(true), + type: z.enum(["function"]), + description: z.string().optional(), + }) + ) + .optional(), + // top_logprobs: z.number().min(0).max(20).nullable().default(null), + top_p: z.number().min(0).max(1).default(1), + // truncation: z.enum(["auto", "disabled"]).default("disabled"), + // user +}); + +export type CreateResponseParams = z.infer; diff --git a/packages/responses-server/src/server.ts b/packages/responses-server/src/server.ts new file mode 100644 index 0000000000..d183b53f24 --- /dev/null +++ b/packages/responses-server/src/server.ts @@ -0,0 +1,22 @@ +import express, { type Express } from "express"; +import { createResponseParamsSchema } from "./schemas.js"; +import { validateBody } from "./middleware/validation.js"; +import { requestLogger } from "./middleware/logging.js"; +import { postCreateResponse } from "./routes/index.js"; + +export const createApp = (): Express => { + const app: Express = express(); + + // Middleware + app.use(requestLogger()); + app.use(express.json()); + + // Routes + app.get("/", (req, res) => { + res.send("hello world"); + }); + + app.post("/v1/responses", validateBody(createResponseParamsSchema), postCreateResponse); + + return app; +}; diff --git a/packages/responses-server/tsconfig.json b/packages/responses-server/tsconfig.json new file mode 100644 index 0000000000..8274efe5ca --- /dev/null +++ b/packages/responses-server/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "lib": ["ES2022", "DOM"], + "module": "CommonJS", + "moduleResolution": "node", + "target": "ES2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "skipLibCheck": true, + "noImplicitOverride": true, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, + "resolveJsonModule": true + }, + "include": ["src", "test"], + "exclude": ["dist"] +} diff --git a/packages/tasks/src/tasks/index.ts b/packages/tasks/src/tasks/index.ts index 7bc1568816..269aa45dee 100644 --- a/packages/tasks/src/tasks/index.ts +++ b/packages/tasks/src/tasks/index.ts @@ -48,16 +48,7 @@ import videoTextToText from "./video-text-to-text/data.js"; export type * from "./audio-classification/inference.js"; export type * from "./automatic-speech-recognition/inference.js"; -export type { - ChatCompletionInput, - ChatCompletionInputMessage, - ChatCompletionOutput, - ChatCompletionOutputComplete, - ChatCompletionOutputMessage, - ChatCompletionStreamOutput, - ChatCompletionStreamOutputChoice, - ChatCompletionStreamOutputDelta, -} from "./chat-completion/inference.js"; +export type * from "./chat-completion/inference.js"; export type * from "./document-question-answering/inference.js"; export type * from "./feature-extraction/inference.js"; export type * from "./fill-mask/inference.js"; diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 08e651bb73..1a6988430d 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -14,3 +14,4 @@ packages: - "packages/ollama-utils" - "packages/mcp-client" - "packages/tiny-agents" + - "packages/responses-server"