├── .nvmrc ├── demo.gif ├── .env.example ├── src ├── fixtures │ └── invoice.pdf ├── env.ts ├── client.ts └── server.ts ├── tsconfig.json ├── .vscode └── settings.json ├── package.json ├── tsconfig.base.json ├── README.md ├── .gitignore └── pnpm-lock.yaml /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.11.0 -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KATT/trpc-ai-playground/HEAD/demo.gif -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | ANTHROPIC_API_KEY= 2 | OPENAI_API_KEY= 3 | LMSTUDIO_URL=http://localhost:1234/v1 4 | -------------------------------------------------------------------------------- /src/fixtures/invoice.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KATT/trpc-ai-playground/HEAD/src/fixtures/invoice.pdf -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "lib": ["es2023", "dom"], 5 | "module": "esnext", 6 | "moduleResolution": "bundler", 7 | "target": "esnext", 8 | "types": ["node"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/env.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | import { z } from 'zod'; 3 | 4 | dotenv.config(); 5 | 6 | export const envSchema = z.object({ 7 | ANTHROPIC_API_KEY: z.string().optional(), 8 | OPENAI_API_KEY: z.string().optional(), 9 | LMSTUDIO_URL: z.string().url(), 10 | PORT: z.coerce.number().default(3000), 11 | }); 12 | 13 | const res = envSchema.safeParse(process.env); 14 | 15 | if (!res.success) { 16 | throw new Error('❌ Invalid environment variables - did you miss to `cp .env.example .env`?', { 17 | cause: res.error, 18 | }); 19 | } 20 | 21 | export const env = res.data; 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.formatOnSave": true, 4 | "[typescript]": { 5 | "editor.defaultFormatter": "esbenp.prettier-vscode" 6 | }, 7 | "[javascript]": { 8 | "editor.defaultFormatter": "esbenp.prettier-vscode" 9 | }, 10 | "[json]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | }, 13 | "[jsonc]": { 14 | "editor.defaultFormatter": "esbenp.prettier-vscode" 15 | }, 16 | "typescript.tsdk": "./node_modules/typescript/lib", 17 | "typescript.enablePromptUseWorkspaceTsdk": true 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-playground", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "type": "module", 6 | "scripts": { 7 | "dev-server": "tsx watch src/server.ts", 8 | "dev-client": "tsx watch src/client.ts", 9 | "format": "prettier --write \"**/*\"" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "description": "", 15 | "engines": { 16 | "node": "22.11.0", 17 | "@types/node": "^22.10.9" 18 | }, 19 | "packageManager": "pnpm@9.12.2", 20 | "dependencies": { 21 | "@ai-sdk/anthropic": "^1.1.2", 22 | "@ai-sdk/openai": "^1.1.2", 23 | "@ai-sdk/openai-compatible": "^0.1.2", 24 | "@trpc/client": "next", 25 | "@trpc/server": "next", 26 | "@types/node": "^22.10.9", 27 | "add": "^2.0.6", 28 | "ai": "^4.1.2", 29 | "dotenv": "^16.4.7", 30 | "prettier": "^3.4.2", 31 | "tsx": "^4.19.2", 32 | "typescript": "^5.7.3", 33 | "zod": "^3.24.1", 34 | "zod-form-data": "^2.0.5" 35 | }, 36 | "prettier": { 37 | "semi": true, 38 | "singleQuote": true, 39 | "tabWidth": 2, 40 | "trailingComma": "es5", 41 | "printWidth": 100, 42 | "arrowParens": "avoid" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Strictest", 4 | "compilerOptions": { 5 | "strict": true, 6 | "strictNullChecks": true, 7 | "allowUnusedLabels": false, 8 | "allowUnreachableCode": false, 9 | "exactOptionalPropertyTypes": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitOverride": true, 12 | "noImplicitReturns": true, 13 | "noPropertyAccessFromIndexSignature": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "allowSyntheticDefaultImports": true, 18 | 19 | "isolatedModules": true, 20 | 21 | "allowJs": true, 22 | "checkJs": true, 23 | 24 | "esModuleInterop": true, 25 | "skipLibCheck": true, 26 | "forceConsistentCasingInFileNames": true, 27 | 28 | "lib": ["es2023"], 29 | "module": "esnext", 30 | "target": "esnext", 31 | "moduleResolution": "bundler", 32 | "moduleDetection": "force", 33 | "resolveJsonModule": true, 34 | "noEmit": false, 35 | "incremental": true, 36 | 37 | "declaration": true, 38 | "declarationMap": true, 39 | "sourceMap": true, 40 | "pretty": true, 41 | "preserveWatchOutput": true, 42 | "emitDeclarationOnly": false, 43 | "tsBuildInfoFile": "${configDir}/tsconfig.tsbuildinfo", 44 | "baseUrl": "${configDir}", 45 | "outDir": "${configDir}/dist", 46 | "rootDir": "${configDir}", 47 | "composite": false, 48 | "noEmitOnError": true 49 | }, 50 | "include": ["${configDir}/**/*"] 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AI Playground with Vercel AI SDK 2 | 3 | A project following [Matt Pocock's Vercel AI SDK Tutorial](https://www.aihero.dev/structured-data-from-pdfs-with-vercel-ai-sdk?list=vercel-ai-sdk-tutorial), implementing various AI features using a tRPC server and streaming responses to a client. 4 | 5 | ![Demo GIF](./demo.gif) 6 | 7 | - tRPC server with AI procedures using Vercel's AI SDK 8 | - tRPC client for calling AI procedures 9 | 10 | ## Files 11 | 12 | - [`src/server.ts`](./src/server.ts) - tRPC server with AI procedures 13 | - [`src/client.ts`](./src/client.ts) - CLI client for testing endpoints 14 | - [`src/env.ts`](./src/env.ts) - Environment configuration 15 | - `.env` - Environment variables (copy from [.env.example](./.env.example)) 16 | 17 | ## Features 18 | 19 | > Note: Not all features may work with every model. 20 | 21 | Uncomment the one you want to play with at the bottom of [src/client.ts](./src/client.ts). 22 | 23 | - Text generation and streaming 24 | - Structured data extraction (recipes, user data) 25 | - PDF invoice data extraction 26 | - Image description generation 27 | - Sentiment analysis 28 | 29 | ## Setup 30 | 31 | ```bash 32 | git clone git@github.com:KATT/trpc-ai-playground.git 33 | cd trpc-ai-playground 34 | pnpm install 35 | cp .env.example .env 36 | ``` 37 | 38 | 1. Open `.env` and fill in your API keys. 39 | 2. In terminal 1, run the server: `pnpm dev-server` 40 | 3. In terminal 2, run the client: `pnpm dev-client` 41 | 42 | > [!NOTE] 43 | > 44 | > If you're using Cursor, ensure you use the workspace's version of TypeScript (as Cursor [ships with an old version](https://forum.cursor.com/t/bump-typescript-version-to-5-6/28370) of TypeScript) 45 | > 46 | > `CMD+SHIFT+P` → `TypeScript: Select TypeScript Version` → `Use Workspace Version` 47 | 48 | ## Available Models 49 | 50 | - Claude 3 (Haiku & Sonnet) 51 | - GPT-4 & GPT-3.5 Turbo 52 | - LMStudio (local) 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | # cspell cache 133 | .cspellcache 134 | 135 | # misc 136 | .todo 137 | out-types/ 138 | packages/api/src/routers 139 | .DS_Store 140 | .turbo/ 141 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTRPCClient, 3 | httpLink, 4 | isNonJsonSerializable, 5 | splitLink, 6 | unstable_httpBatchStreamLink, 7 | } from '@trpc/client'; 8 | import { inferRouterInputs } from '@trpc/server'; 9 | import type { AppRouter } from './server'; 10 | import { inspect } from 'util'; 11 | import { readFileSync } from 'fs'; 12 | import path from 'path'; 13 | import { fileURLToPath } from 'url'; 14 | import { dirname } from 'path'; 15 | 16 | const __filename = fileURLToPath(import.meta.url); 17 | const __dirname = dirname(__filename); 18 | 19 | type Inputs = inferRouterInputs; 20 | 21 | // Initialize tRPC client 22 | const url = `http://localhost:${process.env['PORT'] || 3000}`; 23 | const client = createTRPCClient({ 24 | links: [ 25 | splitLink({ 26 | condition: op => isNonJsonSerializable(op.input), 27 | true: httpLink({ 28 | url, 29 | }), 30 | false: unstable_httpBatchStreamLink({ 31 | url, 32 | }), 33 | }), 34 | ], 35 | }); 36 | 37 | const spinner = (() => { 38 | const spinner = ['◜', '◠', '◝', '◞', '◡', '◟']; 39 | let interval: NodeJS.Timeout | null = null; 40 | 41 | return { 42 | start() { 43 | if (interval) { 44 | throw new Error('Loading spinner is already running'); 45 | } 46 | 47 | let first = true; 48 | let currentIndex = 0; 49 | function writeSpinner() { 50 | if (!first) { 51 | process.stdout.write('\b'); 52 | } 53 | process.stdout.write(spinner[currentIndex]!); 54 | currentIndex = (currentIndex + 1) % spinner.length; 55 | first = false; 56 | } 57 | writeSpinner(); 58 | interval = setInterval(writeSpinner, 75); 59 | }, 60 | 61 | stop() { 62 | if (!interval) return; 63 | clearInterval(interval); 64 | interval = null; 65 | process.stdout.write('\b'); // Clear the spinner character 66 | process.stdout.write('\x1B[?25h'); // Show cursor 67 | }, 68 | }; 69 | })(); 70 | 71 | async function askDemo() { 72 | const chat = (function () { 73 | /** 74 | * Client holds state of the chat and passes it to the server on each request 75 | */ 76 | const messages: Inputs['chat']['messages'] = []; 77 | 78 | return { 79 | async ask(question: string) { 80 | process.stdout.write(`Asking: ${question}\n`); 81 | messages.push({ role: 'user', content: question }); 82 | 83 | process.stdout.write('Response: '); 84 | spinner.start(); 85 | const res = await client.chat.query({ 86 | messages, 87 | // model: 'lmstudio-default', 88 | }); 89 | 90 | let allChunks: string[] = []; 91 | 92 | for await (const chunk of res) { 93 | spinner.stop(); 94 | process.stdout.write(chunk); 95 | allChunks.push(chunk); 96 | } 97 | 98 | process.stdout.write('\n\n'); 99 | 100 | messages.push({ role: 'assistant', content: allChunks.join('') }); 101 | 102 | return allChunks.join(''); 103 | }, 104 | }; 105 | })(); 106 | 107 | await chat.ask('What is the capital of France?'); 108 | await chat.ask('What is the capital of the UK?'); 109 | await chat.ask('Summarize the conversation so far.'); 110 | } 111 | 112 | async function promptDemo() { 113 | const prompt = 'What is the capital of France? Give a really long-winded answer.'; 114 | 115 | process.stdout.write(prompt + '\n'); 116 | process.stdout.write('Response: '); 117 | spinner.start(); 118 | const res = await client.prompt.query({ 119 | prompt, 120 | // model: 'lmstudio-default', 121 | }); 122 | 123 | for await (const chunk of res) { 124 | spinner.stop(); 125 | process.stdout.write(chunk); 126 | } 127 | } 128 | 129 | async function structuredRecipeDemo() { 130 | const prompt = 'Give me a recipe for a chocolate cake.'; 131 | 132 | process.stdout.write(prompt + '\n'); 133 | spinner.start(); 134 | const res = await client.recipeObject.query({ prompt }); 135 | 136 | for await (const chunk of res.loading) { 137 | spinner.stop(); 138 | process.stdout.write(chunk); 139 | } 140 | 141 | console.log('\n'); 142 | 143 | spinner.start(); 144 | 145 | const recipe = await res.recipe; 146 | spinner.stop(); 147 | 148 | console.log(inspect(recipe, { depth: null })); 149 | } 150 | 151 | async function structuredRecipeStreamDemo() { 152 | const prompt = 'Give me a recipe for a chocolate cake.'; 153 | 154 | process.stdout.write(prompt + '\n'); 155 | spinner.start(); 156 | const res = await client.recipeStream.query({ prompt }); 157 | 158 | for await (const chunk of res.loading) { 159 | spinner.stop(); 160 | process.stdout.write(chunk); 161 | } 162 | 163 | process.stdout.write('\n\n'); 164 | 165 | spinner.start(); 166 | 167 | let lastNumberOfLinesToClear = 0; 168 | for await (const chunk of res.recipe) { 169 | spinner.stop(); 170 | // Then we get a bunch of chunks with the actual recipe 171 | // We need to clear the previous lines to make the output more readable 172 | if (lastNumberOfLinesToClear > 0) { 173 | process.stdout.moveCursor(0, -lastNumberOfLinesToClear); 174 | process.stdout.clearScreenDown(); 175 | } 176 | 177 | const output = inspect(chunk, { depth: null }); 178 | process.stdout.write(output); 179 | lastNumberOfLinesToClear = output.split('\n').length; 180 | process.stdout.write('\n'); 181 | } 182 | 183 | console.log('\n'); 184 | } 185 | 186 | async function sentimentDemo() { 187 | const texts = [ 188 | "I absolutely love this product! It's amazing!", 189 | 'This is the worst experience ever.', 190 | 'The weather is quite normal today.', 191 | ]; 192 | 193 | for (const text of texts) { 194 | process.stdout.write(`Analyzing: "${text}"\n`); 195 | spinner.start(); 196 | const sentiment = await client.sentiment.query({ text }); 197 | spinner.stop(); 198 | console.log(`Sentiment: ${sentiment}\n`); 199 | } 200 | } 201 | 202 | async function usersFixtureDataDemo() { 203 | const prompt = 'Generate 3 users who work in tech companies'; 204 | 205 | process.stdout.write(`${prompt}\n`); 206 | spinner.start(); 207 | const users = await client.users.query({ prompt }); 208 | spinner.stop(); 209 | console.log(inspect(users, { depth: null, colors: true })); 210 | } 211 | 212 | async function imageDescriptionDemo() { 213 | const imageUrls = [ 214 | 'https://images.unsplash.com/photo-1546527868-ccb7ee7dfa6a?q=80&w=2070&auto=format&fit=crop', 215 | 'https://images.unsplash.com/photo-1472214103451-9374bd1c798e?q=80&w=2070&auto=format&fit=crop', 216 | ]; 217 | 218 | for (const imageUrl of imageUrls) { 219 | process.stdout.write(`Describing image: ${imageUrl}\n`); 220 | 221 | process.stdout.write('Description: '); 222 | spinner.start(); 223 | const description = await client.describeImage.query({ 224 | imageUrl, 225 | model: 'claude-3-5-sonnet-latest', 226 | }); 227 | 228 | for await (const chunk of description) { 229 | spinner.stop(); 230 | process.stdout.write(chunk); 231 | } 232 | process.stdout.write('\n\n'); 233 | } 234 | } 235 | 236 | async function pdfParsingDemo() { 237 | const pdfPath = path.join(__dirname, './fixtures/invoice.pdf'); 238 | process.stdout.write(`Extracting data from ${pdfPath}\n`); 239 | 240 | const fileContent = readFileSync(pdfPath, 'utf-8'); 241 | const formData = new FormData(); 242 | formData.append( 243 | 'fileContent', 244 | new Blob([fileContent], { type: 'application/pdf' }), 245 | 'invoice.pdf' 246 | ); 247 | formData.append('model', 'claude-3-5-sonnet-latest'); 248 | 249 | spinner.start(); 250 | const data = await client.extractInvoice.mutate(formData); 251 | spinner.stop(); 252 | console.log(inspect(data, { depth: null, colors: true })); 253 | } 254 | 255 | try { 256 | // await promptDemo(); 257 | // console.log('\n----\n'); 258 | // await askDemo(); 259 | // console.log('\n----\n'); 260 | // await structuredRecipeDemo(); 261 | // console.log('\n----\n'); 262 | await structuredRecipeStreamDemo(); 263 | // console.log('\n----\n'); 264 | // await sentimentDemo(); 265 | // console.log('\n----\n'); 266 | // await usersFixtureDataDemo(); 267 | // console.log('\n----\n'); 268 | // await imageDescriptionDemo(); 269 | // console.log('\n----\n'); 270 | // await pdfParsingDemo(); 271 | } catch (error) { 272 | console.error(error); 273 | process.exit(1); 274 | } 275 | -------------------------------------------------------------------------------- /src/server.ts: -------------------------------------------------------------------------------- 1 | import { anthropic } from '@ai-sdk/anthropic'; 2 | import { openai } from '@ai-sdk/openai'; 3 | import { createOpenAICompatible } from '@ai-sdk/openai-compatible'; 4 | import { initTRPC } from '@trpc/server'; 5 | import { createHTTPServer } from '@trpc/server/adapters/standalone'; 6 | import { generateObject, LanguageModelV1, streamObject, streamText } from 'ai'; 7 | import { z } from 'zod'; 8 | import { zfd } from 'zod-form-data'; 9 | import { env } from './env'; 10 | 11 | const run = (fn: () => T) => fn(); 12 | 13 | // Available models 14 | const models = (() => { 15 | const lmstudio = createOpenAICompatible({ 16 | name: 'lmstudio', 17 | baseURL: env.LMSTUDIO_URL, 18 | }); 19 | 20 | const record = { 21 | // Requires `ANTHROPIC_API_KEY` 22 | // https://console.anthropic.com/ 23 | 'claude-3-5-haiku-latest': anthropic('claude-3-5-haiku-latest'), 24 | 'claude-3-5-sonnet-latest': anthropic('claude-3-5-sonnet-latest'), 25 | // Requires `OPENAI_API_KEY` 26 | // https://platform.openai.com/ 27 | 'gpt-4-turbo-preview': openai('gpt-4-turbo-preview'), 28 | 'gpt-4': openai('gpt-4'), 29 | 'gpt-3.5-turbo': openai('gpt-3.5-turbo'), 30 | // Requires LMStudio installed and running (free) 31 | // https://lmstudio.ai/ 32 | 'lmstudio-default': lmstudio(''), 33 | } as const satisfies Record; 34 | 35 | function zodEnumFromObjKeys(obj: Record): z.ZodEnum<[K, ...K[]]> { 36 | return z.enum(Object.keys(obj) as [K, ...K[]]); 37 | } 38 | 39 | const schema = zodEnumFromObjKeys(record).default('claude-3-5-haiku-latest'); 40 | 41 | return { 42 | schema, 43 | record, 44 | }; 45 | })(); 46 | 47 | // Initialize tRPC 48 | const t = initTRPC.context>().create(); 49 | const router = t.router; 50 | const publicProcedure = t.procedure; 51 | 52 | const llmProcedure = publicProcedure 53 | .input( 54 | z.object({ 55 | model: models.schema, 56 | }) 57 | ) 58 | .use(opts => { 59 | return opts.next({ 60 | ctx: { 61 | model: models.record[opts.input.model], 62 | }, 63 | }); 64 | }); 65 | 66 | // Define routes 67 | const appRouter = router({ 68 | prompt: llmProcedure 69 | .input( 70 | z.object({ 71 | prompt: z.string(), 72 | }) 73 | ) 74 | .query(opts => { 75 | const response = streamText({ 76 | model: opts.ctx.model, 77 | prompt: opts.input.prompt, 78 | }); 79 | 80 | return response.textStream; 81 | }), 82 | chat: llmProcedure 83 | .input( 84 | z.object({ 85 | messages: z.array( 86 | z.object({ 87 | role: z.enum(['user', 'assistant']), 88 | content: z.string(), 89 | }) 90 | ), 91 | }) 92 | ) 93 | .query(opts => { 94 | const response = streamText({ 95 | model: opts.ctx.model, 96 | messages: [ 97 | // System prompt 98 | { 99 | role: 'system', 100 | content: 101 | 'You respond short riddles without answer or clues. Add an explanation of the riddle after', 102 | }, 103 | ...opts.input.messages, 104 | ], 105 | }); 106 | 107 | return response.textStream; 108 | }), 109 | 110 | recipeObject: llmProcedure 111 | .input( 112 | z.object({ 113 | prompt: z.string(), 114 | }) 115 | ) 116 | .query(async opts => { 117 | return { 118 | /** 119 | * A nice loading message while the full recipe is being generated 120 | */ 121 | loading: run(() => { 122 | const loading = streamText({ 123 | model: opts.ctx.model, 124 | messages: [ 125 | { 126 | role: 'system', 127 | content: [ 128 | 'You are a helpful assistant that generates recipes.', 129 | 'You are just going to give a short message here while a longer LLM is running to generate the actual recipe.', 130 | 'Just 1 line is enough while loading', 131 | 'Emojis are fun!', 132 | ].join('\n'), 133 | }, 134 | { role: 'user', content: opts.input.prompt }, 135 | ], 136 | }); 137 | 138 | return loading.textStream; 139 | }), 140 | /** 141 | * The actual recipe as structured data, kinda slow 142 | */ 143 | recipe: run(async () => { 144 | const schema = z.object({ 145 | recipe: z.object({ 146 | name: z.string(), 147 | ingredients: z.array( 148 | z.object({ 149 | name: z.string(), 150 | amount: z.string(), 151 | }) 152 | ), 153 | steps: z.array(z.string()), 154 | }), 155 | }); 156 | 157 | const res = await generateObject({ 158 | model: opts.ctx.model, 159 | schema, 160 | prompt: opts.input.prompt, 161 | system: 'You are a helpful assistant that generates recipes.', 162 | }); 163 | 164 | return res.object; 165 | }), 166 | }; 167 | }), 168 | 169 | recipeStream: llmProcedure 170 | .input( 171 | z.object({ 172 | prompt: z.string(), 173 | }) 174 | ) 175 | .query(async opts => { 176 | return { 177 | /** 178 | * A nice loading message while the full recipe is being generated 179 | */ 180 | loading: run(() => { 181 | const loading = streamText({ 182 | model: opts.ctx.model, 183 | messages: [ 184 | { 185 | role: 'system', 186 | content: [ 187 | 'You are a helpful assistant that generates recipes.', 188 | 'You are just going to give a short message here while a longer LLM is running to generate the actual recipe.', 189 | 'Just 1 line is enough while loading', 190 | 'Emojis are fun!', 191 | ].join('\n'), 192 | }, 193 | { role: 'user', content: opts.input.prompt }, 194 | ], 195 | }); 196 | return loading.textStream; 197 | }), 198 | recipe: run(async function* () { 199 | const schema = z.object({ 200 | recipe: z.object({ 201 | name: z.string(), 202 | ingredients: z.array( 203 | z.object({ 204 | name: z.string(), 205 | amount: z.string(), 206 | }) 207 | ), 208 | steps: z.array(z.string()), 209 | }), 210 | }); 211 | const stream = streamObject({ 212 | model: opts.ctx.model, 213 | schema, 214 | prompt: opts.input.prompt, 215 | system: 'You are a helpful assistant that generates recipes.', 216 | }); 217 | 218 | for await (const chunk of stream.partialObjectStream) { 219 | // console.log({ 220 | // name: chunk.recipe?.name, 221 | // ingredients: chunk.recipe?.ingredients?.length ?? 0, 222 | // steps: chunk.recipe?.steps?.length ?? 0, 223 | // }); 224 | yield chunk; 225 | // Adds artificial delay to make the output less chaotic 226 | await new Promise(resolve => setTimeout(resolve, 20)); 227 | } 228 | }), 229 | }; 230 | }), 231 | 232 | sentiment: llmProcedure 233 | .input( 234 | z.object({ 235 | text: z.string(), 236 | }) 237 | ) 238 | .query(async opts => { 239 | const res = await generateObject({ 240 | model: opts.ctx.model, 241 | output: 'enum', 242 | enum: ['positive', 'negative', 'neutral'], 243 | prompt: opts.input.text, 244 | system: 'Classify the sentiment of the text as either positive, negative, or neutral.', 245 | }); 246 | 247 | return res.object; 248 | }), 249 | 250 | users: llmProcedure 251 | .input( 252 | z.object({ 253 | prompt: z.string(), 254 | }) 255 | ) 256 | .query(async opts => { 257 | const userSchema = z.object({ 258 | name: z.string().describe('Full name of the user'), 259 | age: z.number().describe('Age of the user between 18 and 80'), 260 | email: z.string().email().describe('A valid email address'), 261 | occupation: z.string().describe("The user's job or profession"), 262 | city: z.string().describe('A city in the UK'), 263 | }); 264 | 265 | const res = await generateObject({ 266 | model: opts.ctx.model, 267 | schema: userSchema, 268 | output: 'array', 269 | prompt: opts.input.prompt, 270 | system: 271 | 'You are generating realistic fake user data for UK residents. Create diverse, believable profiles.', 272 | }); 273 | 274 | return res.object; 275 | }), 276 | 277 | // All of them don't support image generation so not using llmProcedure for this one 278 | describeImage: llmProcedure 279 | .input( 280 | z.object({ 281 | imageUrl: z.string().url(), 282 | }) 283 | ) 284 | .query(async opts => { 285 | const res = streamText({ 286 | model: opts.ctx.model, 287 | system: [ 288 | 'You will receive an image.', 289 | 'Please create an alt text for the image.', 290 | 'Be concise.', 291 | 'Use adjectives only when necessary.', 292 | 'Do not pass 160 characters.', 293 | 'Use simple language.', 294 | ].join(' '), 295 | messages: [ 296 | { 297 | role: 'user', 298 | content: [ 299 | { 300 | type: 'image', 301 | image: new URL(opts.input.imageUrl), 302 | }, 303 | ], 304 | }, 305 | ], 306 | }); 307 | 308 | return res.textStream; 309 | }), 310 | 311 | extractInvoice: publicProcedure 312 | .input( 313 | zfd.formData({ 314 | fileContent: zfd.file(), 315 | model: models.schema, 316 | }) 317 | ) 318 | .use(opts => { 319 | return opts.next({ 320 | ctx: { 321 | model: models.record[opts.input.model], 322 | }, 323 | }); 324 | }) 325 | .mutation(async opts => { 326 | const schema = z 327 | .object({ 328 | total: z.number().describe('The total amount of the invoice.'), 329 | currency: z.string().describe('The currency of the total amount.'), 330 | invoiceNumber: z.string().describe('The invoice number.'), 331 | companyAddress: z 332 | .string() 333 | .describe('The address of the company or person issuing the invoice.'), 334 | companyName: z.string().describe('The name of the company issuing the invoice.'), 335 | invoiceeAddress: z 336 | .string() 337 | .describe('The address of the company or person receiving the invoice.'), 338 | }) 339 | .describe('The extracted data from the invoice.'); 340 | 341 | const res = await generateObject({ 342 | model: opts.ctx.model, 343 | schema, 344 | system: 'You will receive an invoice. Please extract the data from the invoice.', 345 | messages: [ 346 | { 347 | role: 'user', 348 | content: [ 349 | { 350 | type: 'file', 351 | data: await opts.input.fileContent.arrayBuffer(), 352 | mimeType: opts.input.fileContent.type, 353 | }, 354 | ], 355 | }, 356 | ], 357 | }); 358 | 359 | return res.object; 360 | }), 361 | }); 362 | 363 | // Export type definition of API 364 | export type AppRouter = typeof appRouter; 365 | 366 | // Create standalone server 367 | const server = createHTTPServer({ 368 | router: appRouter, 369 | createContext: () => ({}), 370 | onError: opts => { 371 | console.error(opts.error); 372 | }, 373 | }); 374 | 375 | // Start server 376 | const port = env.PORT; 377 | server.listen(port, () => { 378 | console.log(`Server is running on port ${port}`); 379 | }); 380 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@ai-sdk/anthropic': 12 | specifier: ^1.1.2 13 | version: 1.1.2(zod@3.24.1) 14 | '@ai-sdk/openai': 15 | specifier: ^1.1.2 16 | version: 1.1.2(zod@3.24.1) 17 | '@ai-sdk/openai-compatible': 18 | specifier: ^0.1.2 19 | version: 0.1.2(zod@3.24.1) 20 | '@trpc/client': 21 | specifier: next 22 | version: 11.0.0-rc.718(@trpc/server@11.0.0-rc.718(typescript@5.7.3))(typescript@5.7.3) 23 | '@trpc/server': 24 | specifier: next 25 | version: 11.0.0-rc.718(typescript@5.7.3) 26 | '@types/node': 27 | specifier: ^22.10.9 28 | version: 22.10.9 29 | add: 30 | specifier: ^2.0.6 31 | version: 2.0.6 32 | ai: 33 | specifier: ^4.1.2 34 | version: 4.1.2(react@19.0.0)(zod@3.24.1) 35 | dotenv: 36 | specifier: ^16.4.7 37 | version: 16.4.7 38 | prettier: 39 | specifier: ^3.4.2 40 | version: 3.4.2 41 | tsx: 42 | specifier: ^4.19.2 43 | version: 4.19.2 44 | typescript: 45 | specifier: ^5.7.3 46 | version: 5.7.3 47 | zod: 48 | specifier: ^3.24.1 49 | version: 3.24.1 50 | zod-form-data: 51 | specifier: ^2.0.5 52 | version: 2.0.5(zod@3.24.1) 53 | 54 | packages: 55 | 56 | '@ai-sdk/anthropic@1.1.2': 57 | resolution: {integrity: sha512-AR5/zL+N+zVXABocbi8XTsDR44jAbylh77G4VytoE7rQRFTw2Ht/cOxwrTlRZ6RgJeGYABBMnHK9XbxRVOnsfQ==} 58 | engines: {node: '>=18'} 59 | peerDependencies: 60 | zod: ^3.0.0 61 | 62 | '@ai-sdk/openai-compatible@0.1.2': 63 | resolution: {integrity: sha512-5qvWOzJDR30ygNJtnJC7eQghn95krj4KyM61OXan7dN8dtypW69b66oa3XOJVY4Qylbi2aeAvwsNYmTSyM9jbQ==} 64 | engines: {node: '>=18'} 65 | peerDependencies: 66 | zod: ^3.0.0 67 | 68 | '@ai-sdk/openai@1.1.2': 69 | resolution: {integrity: sha512-9rfcwjl4g1/Bdr2SmgFQr+aw81r62MvIKE7QDHMC4ulFd/Hej2oClROSMpDFZHXzs7RGeb32VkRyCHUWWgN3RQ==} 70 | engines: {node: '>=18'} 71 | peerDependencies: 72 | zod: ^3.0.0 73 | 74 | '@ai-sdk/provider-utils@2.1.2': 75 | resolution: {integrity: sha512-ezpQT6kzy/2O4yyn/2YigMqynBYjZIOam3/EMNVzju+Ogj+Z+pf27c/Th78ce0A2ltgrXx6xN14sal/HHZNOOw==} 76 | engines: {node: '>=18'} 77 | peerDependencies: 78 | zod: ^3.0.0 79 | peerDependenciesMeta: 80 | zod: 81 | optional: true 82 | 83 | '@ai-sdk/provider@1.0.6': 84 | resolution: {integrity: sha512-hwj/gFNxpDgEfTaYzCYoslmw01IY9kWLKl/wf8xuPvHtQIzlfXWmmUwc8PnCwxyt8cKzIuV0dfUghCf68HQ0SA==} 85 | engines: {node: '>=18'} 86 | 87 | '@ai-sdk/react@1.1.2': 88 | resolution: {integrity: sha512-bBcRsDaNHzCKSIBbPngMeqbnwZ1RFadXQo9XzHoGrvLANYRwuphGNB8XTXYVLC/eXjoaGVGw2wWf/TYigEnCuA==} 89 | engines: {node: '>=18'} 90 | peerDependencies: 91 | react: ^18 || ^19 || ^19.0.0-rc 92 | zod: ^3.0.0 93 | peerDependenciesMeta: 94 | react: 95 | optional: true 96 | zod: 97 | optional: true 98 | 99 | '@ai-sdk/ui-utils@1.1.2': 100 | resolution: {integrity: sha512-+0kfBF4Y9jmlg1KlbNKIxchmXx9PzuReSpgRNWhpU10vfl1eeer4xK/XL2qHnzAWhsMFe/SVZXJIQObk44zNEQ==} 101 | engines: {node: '>=18'} 102 | peerDependencies: 103 | zod: ^3.0.0 104 | peerDependenciesMeta: 105 | zod: 106 | optional: true 107 | 108 | '@esbuild/aix-ppc64@0.23.1': 109 | resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==} 110 | engines: {node: '>=18'} 111 | cpu: [ppc64] 112 | os: [aix] 113 | 114 | '@esbuild/android-arm64@0.23.1': 115 | resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==} 116 | engines: {node: '>=18'} 117 | cpu: [arm64] 118 | os: [android] 119 | 120 | '@esbuild/android-arm@0.23.1': 121 | resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==} 122 | engines: {node: '>=18'} 123 | cpu: [arm] 124 | os: [android] 125 | 126 | '@esbuild/android-x64@0.23.1': 127 | resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==} 128 | engines: {node: '>=18'} 129 | cpu: [x64] 130 | os: [android] 131 | 132 | '@esbuild/darwin-arm64@0.23.1': 133 | resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==} 134 | engines: {node: '>=18'} 135 | cpu: [arm64] 136 | os: [darwin] 137 | 138 | '@esbuild/darwin-x64@0.23.1': 139 | resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==} 140 | engines: {node: '>=18'} 141 | cpu: [x64] 142 | os: [darwin] 143 | 144 | '@esbuild/freebsd-arm64@0.23.1': 145 | resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==} 146 | engines: {node: '>=18'} 147 | cpu: [arm64] 148 | os: [freebsd] 149 | 150 | '@esbuild/freebsd-x64@0.23.1': 151 | resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==} 152 | engines: {node: '>=18'} 153 | cpu: [x64] 154 | os: [freebsd] 155 | 156 | '@esbuild/linux-arm64@0.23.1': 157 | resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==} 158 | engines: {node: '>=18'} 159 | cpu: [arm64] 160 | os: [linux] 161 | 162 | '@esbuild/linux-arm@0.23.1': 163 | resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==} 164 | engines: {node: '>=18'} 165 | cpu: [arm] 166 | os: [linux] 167 | 168 | '@esbuild/linux-ia32@0.23.1': 169 | resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==} 170 | engines: {node: '>=18'} 171 | cpu: [ia32] 172 | os: [linux] 173 | 174 | '@esbuild/linux-loong64@0.23.1': 175 | resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==} 176 | engines: {node: '>=18'} 177 | cpu: [loong64] 178 | os: [linux] 179 | 180 | '@esbuild/linux-mips64el@0.23.1': 181 | resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==} 182 | engines: {node: '>=18'} 183 | cpu: [mips64el] 184 | os: [linux] 185 | 186 | '@esbuild/linux-ppc64@0.23.1': 187 | resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==} 188 | engines: {node: '>=18'} 189 | cpu: [ppc64] 190 | os: [linux] 191 | 192 | '@esbuild/linux-riscv64@0.23.1': 193 | resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==} 194 | engines: {node: '>=18'} 195 | cpu: [riscv64] 196 | os: [linux] 197 | 198 | '@esbuild/linux-s390x@0.23.1': 199 | resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==} 200 | engines: {node: '>=18'} 201 | cpu: [s390x] 202 | os: [linux] 203 | 204 | '@esbuild/linux-x64@0.23.1': 205 | resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==} 206 | engines: {node: '>=18'} 207 | cpu: [x64] 208 | os: [linux] 209 | 210 | '@esbuild/netbsd-x64@0.23.1': 211 | resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==} 212 | engines: {node: '>=18'} 213 | cpu: [x64] 214 | os: [netbsd] 215 | 216 | '@esbuild/openbsd-arm64@0.23.1': 217 | resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} 218 | engines: {node: '>=18'} 219 | cpu: [arm64] 220 | os: [openbsd] 221 | 222 | '@esbuild/openbsd-x64@0.23.1': 223 | resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==} 224 | engines: {node: '>=18'} 225 | cpu: [x64] 226 | os: [openbsd] 227 | 228 | '@esbuild/sunos-x64@0.23.1': 229 | resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==} 230 | engines: {node: '>=18'} 231 | cpu: [x64] 232 | os: [sunos] 233 | 234 | '@esbuild/win32-arm64@0.23.1': 235 | resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==} 236 | engines: {node: '>=18'} 237 | cpu: [arm64] 238 | os: [win32] 239 | 240 | '@esbuild/win32-ia32@0.23.1': 241 | resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==} 242 | engines: {node: '>=18'} 243 | cpu: [ia32] 244 | os: [win32] 245 | 246 | '@esbuild/win32-x64@0.23.1': 247 | resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==} 248 | engines: {node: '>=18'} 249 | cpu: [x64] 250 | os: [win32] 251 | 252 | '@opentelemetry/api@1.9.0': 253 | resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} 254 | engines: {node: '>=8.0.0'} 255 | 256 | '@trpc/client@11.0.0-rc.718': 257 | resolution: {integrity: sha512-pTeMVFcNSw7akDz7XySsbBckfmoEYVSKLB4TCare4eAmJkFRnuPuFeKvs97hm2hxTQ43j9rrEOQGCRY84Vc4Ew==} 258 | peerDependencies: 259 | '@trpc/server': 11.0.0-rc.718+2018e3914 260 | typescript: '>=5.7.2' 261 | 262 | '@trpc/server@11.0.0-rc.718': 263 | resolution: {integrity: sha512-u9xKci9yEVBjia3X6XhYG9Me3klf0HmyQUAPCzxPYUg8WNvwXEGfTJrIC1bBfst2pjvv3xmXsi16e3tGU16c6g==} 264 | peerDependencies: 265 | typescript: '>=5.7.2' 266 | 267 | '@types/diff-match-patch@1.0.36': 268 | resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==} 269 | 270 | '@types/node@22.10.9': 271 | resolution: {integrity: sha512-Ir6hwgsKyNESl/gLOcEz3krR4CBGgliDqBQ2ma4wIhEx0w+xnoeTq3tdrNw15kU3SxogDjOgv9sqdtLW8mIHaw==} 272 | 273 | add@2.0.6: 274 | resolution: {integrity: sha512-j5QzrmsokwWWp6kUcJQySpbG+xfOBqqKnup3OIk1pz+kB/80SLorZ9V8zHFLO92Lcd+hbvq8bT+zOGoPkmBV0Q==} 275 | 276 | ai@4.1.2: 277 | resolution: {integrity: sha512-11efhPorWFphIpeCgjW6r/jk4wB5RWUGjxayHblBXCq6YEc7o5ki7vlmSnESprsDkMEfmONBWb/xM8pWjR5O2g==} 278 | engines: {node: '>=18'} 279 | peerDependencies: 280 | react: ^18 || ^19 || ^19.0.0-rc 281 | zod: ^3.0.0 282 | peerDependenciesMeta: 283 | react: 284 | optional: true 285 | zod: 286 | optional: true 287 | 288 | chalk@5.4.1: 289 | resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==} 290 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 291 | 292 | dequal@2.0.3: 293 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 294 | engines: {node: '>=6'} 295 | 296 | diff-match-patch@1.0.5: 297 | resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==} 298 | 299 | dotenv@16.4.7: 300 | resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} 301 | engines: {node: '>=12'} 302 | 303 | esbuild@0.23.1: 304 | resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==} 305 | engines: {node: '>=18'} 306 | hasBin: true 307 | 308 | eventsource-parser@3.0.0: 309 | resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} 310 | engines: {node: '>=18.0.0'} 311 | 312 | fsevents@2.3.3: 313 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 314 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 315 | os: [darwin] 316 | 317 | get-tsconfig@4.10.0: 318 | resolution: {integrity: sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==} 319 | 320 | json-schema@0.4.0: 321 | resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} 322 | 323 | jsondiffpatch@0.6.0: 324 | resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==} 325 | engines: {node: ^18.0.0 || >=20.0.0} 326 | hasBin: true 327 | 328 | nanoid@3.3.8: 329 | resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} 330 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 331 | hasBin: true 332 | 333 | prettier@3.4.2: 334 | resolution: {integrity: sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==} 335 | engines: {node: '>=14'} 336 | hasBin: true 337 | 338 | react@19.0.0: 339 | resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} 340 | engines: {node: '>=0.10.0'} 341 | 342 | resolve-pkg-maps@1.0.0: 343 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 344 | 345 | secure-json-parse@2.7.0: 346 | resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==} 347 | 348 | swr@2.3.0: 349 | resolution: {integrity: sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==} 350 | peerDependencies: 351 | react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 352 | 353 | throttleit@2.1.0: 354 | resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==} 355 | engines: {node: '>=18'} 356 | 357 | tsx@4.19.2: 358 | resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} 359 | engines: {node: '>=18.0.0'} 360 | hasBin: true 361 | 362 | typescript@5.7.3: 363 | resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} 364 | engines: {node: '>=14.17'} 365 | hasBin: true 366 | 367 | undici-types@6.20.0: 368 | resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==} 369 | 370 | use-sync-external-store@1.4.0: 371 | resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} 372 | peerDependencies: 373 | react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 374 | 375 | zod-form-data@2.0.5: 376 | resolution: {integrity: sha512-T7dV6lTBCwkd8PyvJVCnjXKpgXomU8gEm/TcvEZY7qNdRhIo9T17HrdlHIK68PzTAYaV2HxR9rgwpTSWv0L+QQ==} 377 | peerDependencies: 378 | zod: '>= 3.11.0' 379 | 380 | zod-to-json-schema@3.24.1: 381 | resolution: {integrity: sha512-3h08nf3Vw3Wl3PK+q3ow/lIil81IT2Oa7YpQyUUDsEWbXveMesdfK1xBd2RhCkynwZndAxixji/7SYJJowr62w==} 382 | peerDependencies: 383 | zod: ^3.24.1 384 | 385 | zod@3.24.1: 386 | resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==} 387 | 388 | snapshots: 389 | 390 | '@ai-sdk/anthropic@1.1.2(zod@3.24.1)': 391 | dependencies: 392 | '@ai-sdk/provider': 1.0.6 393 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 394 | zod: 3.24.1 395 | 396 | '@ai-sdk/openai-compatible@0.1.2(zod@3.24.1)': 397 | dependencies: 398 | '@ai-sdk/provider': 1.0.6 399 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 400 | zod: 3.24.1 401 | 402 | '@ai-sdk/openai@1.1.2(zod@3.24.1)': 403 | dependencies: 404 | '@ai-sdk/provider': 1.0.6 405 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 406 | zod: 3.24.1 407 | 408 | '@ai-sdk/provider-utils@2.1.2(zod@3.24.1)': 409 | dependencies: 410 | '@ai-sdk/provider': 1.0.6 411 | eventsource-parser: 3.0.0 412 | nanoid: 3.3.8 413 | secure-json-parse: 2.7.0 414 | optionalDependencies: 415 | zod: 3.24.1 416 | 417 | '@ai-sdk/provider@1.0.6': 418 | dependencies: 419 | json-schema: 0.4.0 420 | 421 | '@ai-sdk/react@1.1.2(react@19.0.0)(zod@3.24.1)': 422 | dependencies: 423 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 424 | '@ai-sdk/ui-utils': 1.1.2(zod@3.24.1) 425 | swr: 2.3.0(react@19.0.0) 426 | throttleit: 2.1.0 427 | optionalDependencies: 428 | react: 19.0.0 429 | zod: 3.24.1 430 | 431 | '@ai-sdk/ui-utils@1.1.2(zod@3.24.1)': 432 | dependencies: 433 | '@ai-sdk/provider': 1.0.6 434 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 435 | zod-to-json-schema: 3.24.1(zod@3.24.1) 436 | optionalDependencies: 437 | zod: 3.24.1 438 | 439 | '@esbuild/aix-ppc64@0.23.1': 440 | optional: true 441 | 442 | '@esbuild/android-arm64@0.23.1': 443 | optional: true 444 | 445 | '@esbuild/android-arm@0.23.1': 446 | optional: true 447 | 448 | '@esbuild/android-x64@0.23.1': 449 | optional: true 450 | 451 | '@esbuild/darwin-arm64@0.23.1': 452 | optional: true 453 | 454 | '@esbuild/darwin-x64@0.23.1': 455 | optional: true 456 | 457 | '@esbuild/freebsd-arm64@0.23.1': 458 | optional: true 459 | 460 | '@esbuild/freebsd-x64@0.23.1': 461 | optional: true 462 | 463 | '@esbuild/linux-arm64@0.23.1': 464 | optional: true 465 | 466 | '@esbuild/linux-arm@0.23.1': 467 | optional: true 468 | 469 | '@esbuild/linux-ia32@0.23.1': 470 | optional: true 471 | 472 | '@esbuild/linux-loong64@0.23.1': 473 | optional: true 474 | 475 | '@esbuild/linux-mips64el@0.23.1': 476 | optional: true 477 | 478 | '@esbuild/linux-ppc64@0.23.1': 479 | optional: true 480 | 481 | '@esbuild/linux-riscv64@0.23.1': 482 | optional: true 483 | 484 | '@esbuild/linux-s390x@0.23.1': 485 | optional: true 486 | 487 | '@esbuild/linux-x64@0.23.1': 488 | optional: true 489 | 490 | '@esbuild/netbsd-x64@0.23.1': 491 | optional: true 492 | 493 | '@esbuild/openbsd-arm64@0.23.1': 494 | optional: true 495 | 496 | '@esbuild/openbsd-x64@0.23.1': 497 | optional: true 498 | 499 | '@esbuild/sunos-x64@0.23.1': 500 | optional: true 501 | 502 | '@esbuild/win32-arm64@0.23.1': 503 | optional: true 504 | 505 | '@esbuild/win32-ia32@0.23.1': 506 | optional: true 507 | 508 | '@esbuild/win32-x64@0.23.1': 509 | optional: true 510 | 511 | '@opentelemetry/api@1.9.0': {} 512 | 513 | '@trpc/client@11.0.0-rc.718(@trpc/server@11.0.0-rc.718(typescript@5.7.3))(typescript@5.7.3)': 514 | dependencies: 515 | '@trpc/server': 11.0.0-rc.718(typescript@5.7.3) 516 | typescript: 5.7.3 517 | 518 | '@trpc/server@11.0.0-rc.718(typescript@5.7.3)': 519 | dependencies: 520 | typescript: 5.7.3 521 | 522 | '@types/diff-match-patch@1.0.36': {} 523 | 524 | '@types/node@22.10.9': 525 | dependencies: 526 | undici-types: 6.20.0 527 | 528 | add@2.0.6: {} 529 | 530 | ai@4.1.2(react@19.0.0)(zod@3.24.1): 531 | dependencies: 532 | '@ai-sdk/provider': 1.0.6 533 | '@ai-sdk/provider-utils': 2.1.2(zod@3.24.1) 534 | '@ai-sdk/react': 1.1.2(react@19.0.0)(zod@3.24.1) 535 | '@ai-sdk/ui-utils': 1.1.2(zod@3.24.1) 536 | '@opentelemetry/api': 1.9.0 537 | jsondiffpatch: 0.6.0 538 | optionalDependencies: 539 | react: 19.0.0 540 | zod: 3.24.1 541 | 542 | chalk@5.4.1: {} 543 | 544 | dequal@2.0.3: {} 545 | 546 | diff-match-patch@1.0.5: {} 547 | 548 | dotenv@16.4.7: {} 549 | 550 | esbuild@0.23.1: 551 | optionalDependencies: 552 | '@esbuild/aix-ppc64': 0.23.1 553 | '@esbuild/android-arm': 0.23.1 554 | '@esbuild/android-arm64': 0.23.1 555 | '@esbuild/android-x64': 0.23.1 556 | '@esbuild/darwin-arm64': 0.23.1 557 | '@esbuild/darwin-x64': 0.23.1 558 | '@esbuild/freebsd-arm64': 0.23.1 559 | '@esbuild/freebsd-x64': 0.23.1 560 | '@esbuild/linux-arm': 0.23.1 561 | '@esbuild/linux-arm64': 0.23.1 562 | '@esbuild/linux-ia32': 0.23.1 563 | '@esbuild/linux-loong64': 0.23.1 564 | '@esbuild/linux-mips64el': 0.23.1 565 | '@esbuild/linux-ppc64': 0.23.1 566 | '@esbuild/linux-riscv64': 0.23.1 567 | '@esbuild/linux-s390x': 0.23.1 568 | '@esbuild/linux-x64': 0.23.1 569 | '@esbuild/netbsd-x64': 0.23.1 570 | '@esbuild/openbsd-arm64': 0.23.1 571 | '@esbuild/openbsd-x64': 0.23.1 572 | '@esbuild/sunos-x64': 0.23.1 573 | '@esbuild/win32-arm64': 0.23.1 574 | '@esbuild/win32-ia32': 0.23.1 575 | '@esbuild/win32-x64': 0.23.1 576 | 577 | eventsource-parser@3.0.0: {} 578 | 579 | fsevents@2.3.3: 580 | optional: true 581 | 582 | get-tsconfig@4.10.0: 583 | dependencies: 584 | resolve-pkg-maps: 1.0.0 585 | 586 | json-schema@0.4.0: {} 587 | 588 | jsondiffpatch@0.6.0: 589 | dependencies: 590 | '@types/diff-match-patch': 1.0.36 591 | chalk: 5.4.1 592 | diff-match-patch: 1.0.5 593 | 594 | nanoid@3.3.8: {} 595 | 596 | prettier@3.4.2: {} 597 | 598 | react@19.0.0: {} 599 | 600 | resolve-pkg-maps@1.0.0: {} 601 | 602 | secure-json-parse@2.7.0: {} 603 | 604 | swr@2.3.0(react@19.0.0): 605 | dependencies: 606 | dequal: 2.0.3 607 | react: 19.0.0 608 | use-sync-external-store: 1.4.0(react@19.0.0) 609 | 610 | throttleit@2.1.0: {} 611 | 612 | tsx@4.19.2: 613 | dependencies: 614 | esbuild: 0.23.1 615 | get-tsconfig: 4.10.0 616 | optionalDependencies: 617 | fsevents: 2.3.3 618 | 619 | typescript@5.7.3: {} 620 | 621 | undici-types@6.20.0: {} 622 | 623 | use-sync-external-store@1.4.0(react@19.0.0): 624 | dependencies: 625 | react: 19.0.0 626 | 627 | zod-form-data@2.0.5(zod@3.24.1): 628 | dependencies: 629 | zod: 3.24.1 630 | 631 | zod-to-json-schema@3.24.1(zod@3.24.1): 632 | dependencies: 633 | zod: 3.24.1 634 | 635 | zod@3.24.1: {} 636 | --------------------------------------------------------------------------------