From 16b59b3e51257bdc1d36d87c50e047ff4df95857 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:22:15 -0700 Subject: [PATCH 1/6] Add Completion Endpoint --- src/index.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/index.ts b/src/index.ts index 2379805..b0c9e02 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,7 +61,7 @@ app.listen(port, () => console.log(`Listening on PORT: ${port}`)) // app.get('/app-health-check', (_req, res) => res.sendStatus(200)) -app.post('/', async (req: Request, res: Response) => { +app.post(['/', '/chat'], async (req: Request, res: Response) => { // console.log('req.headers:', req.headers) // console.log('authorization:', req.headers.authorization) const { messages, system } = req.body @@ -84,6 +84,30 @@ app.post('/', async (req: Request, res: Response) => { pipeUIMessageStreamToResponse({ response: res, stream }) }) +app.post('/completion', async (req: Request, res: Response) => { + const { prompt, system } = req.body + console.log('prompt:', prompt.length, 'system:', system.length) + const result = streamText({ + model: model, + prompt, + system: system || process.env.COMPLETION_INSTRUCTIONS, + maxOutputTokens, + providerOptions, + onEnd({ finishReason, finalStep, text, usage }) { + console.log('finishReason:', finishReason) + console.log('usage:', usage) + console.log('reasoning:', finalStep.reasoningText) + console.log('response:', text) + }, + }) + const stream = createUIMessageStream({ + execute: ({ writer }) => { + writer.merge(toUIMessageStream({ stream: result.stream })) + }, + }) + pipeUIMessageStreamToResponse({ response: res, stream }) +}) + function corsCallback( origin: string | undefined, callback: (err: Error | null, origin?: boolean) => void, From 5161b933dd16ecd4b30be084d0f7f24dd255069c Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Tue, 14 Jul 2026 20:36:49 -0700 Subject: [PATCH 2/6] Cleanup --- src/index.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/index.ts b/src/index.ts index b0c9e02..b7c8ac4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { + consumeStream, streamText, createUIMessageStream, pipeUIMessageStreamToResponse, @@ -85,19 +86,23 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { }) app.post('/completion', async (req: Request, res: Response) => { - const { prompt, system } = req.body - console.log('prompt:', prompt.length, 'system:', system.length) + const { prompt, system, signal } = req.body + console.log('prompt:', prompt?.length, 'system:', system?.length) const result = streamText({ model: model, prompt, system: system || process.env.COMPLETION_INSTRUCTIONS, maxOutputTokens, providerOptions, - onEnd({ finishReason, finalStep, text, usage }) { - console.log('finishReason:', finishReason) - console.log('usage:', usage) + abortSignal: signal, + onError({ error }) { + console.log('error:', error) + }, + onEnd({ finalStep, finishReason, text, usage }) { console.log('reasoning:', finalStep.reasoningText) console.log('response:', text) + console.log('usage:', usage) + console.log('finishReason:', finishReason) }, }) const stream = createUIMessageStream({ @@ -105,7 +110,11 @@ app.post('/completion', async (req: Request, res: Response) => { writer.merge(toUIMessageStream({ stream: result.stream })) }, }) - pipeUIMessageStreamToResponse({ response: res, stream }) + pipeUIMessageStreamToResponse({ + response: res, + stream, + consumeSseStream: consumeStream, + }) }) function corsCallback( From 091be2300fa2e01606e58a86f24065e48c2cf7db Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Tue, 14 Jul 2026 21:57:42 -0700 Subject: [PATCH 3/6] Add /object --- src/index.ts | 42 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index b7c8ac4..7bd2e39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,11 +7,14 @@ import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { + Output, consumeStream, + pipeTextStreamToResponse, streamText, createUIMessageStream, pipeUIMessageStreamToResponse, convertToModelMessages, + toTextStream, toUIMessageStream, } from 'ai' @@ -43,7 +46,11 @@ const maxOutputTokens = process.env.MAX_TOKENS ? Number.parseInt(process.env.MAX_TOKENS) : undefined console.log(`maxOutputTokens: ${maxOutputTokens}`) -console.log(`INSTRUCTIONS: ${process.env.INSTRUCTIONS}`) +console.log( + `CHAT_INSTRUCTIONS: ${process.env.INSTRUCTIONS || process.env.CHAT_INSTRUCTIONS}`, // NOSONAR +) +console.log(`COMPLETION_INSTRUCTIONS: ${process.env.COMPLETION_INSTRUCTIONS}`) +console.log(`OBJECT_INSTRUCTIONS: ${process.env.OBJECT_INSTRUCTIONS}`) const model = getModel() console.log(`Loaded modelId: ${model.modelId}`) @@ -74,7 +81,7 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { const result = streamText({ model: model, messages: modelMessages, - system: system || process.env.INSTRUCTIONS, + system: system || process.env.INSTRUCTIONS || process.env.CHAT_INSTRUCTIONS, maxOutputTokens, providerOptions, }) @@ -86,7 +93,7 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { }) app.post('/completion', async (req: Request, res: Response) => { - const { prompt, system, signal } = req.body + const { prompt, system } = req.body console.log('prompt:', prompt?.length, 'system:', system?.length) const result = streamText({ model: model, @@ -94,8 +101,7 @@ app.post('/completion', async (req: Request, res: Response) => { system: system || process.env.COMPLETION_INSTRUCTIONS, maxOutputTokens, providerOptions, - abortSignal: signal, - onError({ error }) { + onError(error) { console.log('error:', error) }, onEnd({ finalStep, finishReason, text, usage }) { @@ -117,6 +123,32 @@ app.post('/completion', async (req: Request, res: Response) => { }) }) +app.post('/object', async (req: Request, res: Response) => { + const { outputSchema, prompt, system } = req.body + console.log('prompt:', prompt?.length, 'system:', system?.length) + const result = streamText({ + model: model, + prompt, + system: system || process.env.OBJECT_INSTRUCTIONS, + maxOutputTokens, + providerOptions, + output: outputSchema ? Output.object({ schema: outputSchema }) : Output.json(), + onError(error) { + console.log('error:', error) + }, + onEnd({ finalStep, finishReason, text, usage }) { + console.log('reasoning:', finalStep.reasoningText) + console.log('response:', text) + console.log('usage:', usage) + console.log('finishReason:', finishReason) + }, + }) + pipeTextStreamToResponse({ + response: res, + stream: toTextStream({ stream: result.stream }), + }) +}) + function corsCallback( origin: string | undefined, callback: (err: Error | null, origin?: boolean) => void, From 23f9abb19470270fd95bcef0176eb61ae183bbd4 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Wed, 15 Jul 2026 16:47:40 -0700 Subject: [PATCH 4/6] Update README.md --- README.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++---- src/index.ts | 9 ++++++--- 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 47ed6ad..0578466 100644 --- a/README.md +++ b/README.md @@ -42,12 +42,13 @@ To get started [Setup](#setup) and [Configure](#configure) the server. No API Ke [![View Live Demo](https://img.shields.io/badge/view_live_demo-green?style=for-the-badge&logo=chatbot&logoColor=white)](https://cssnr.github.io/vitepress-chat/) -- Client: https://github.com/cssnr/vitepress-chat -- Server: https://github.com/cssnr/chat-server +- Client: +- Server: ### Features - Works with Claude, OpenAI, Gemini and OpenAI Compatible Providers +- Chat, Completion, and Object Endpoints - Live Stream Results to Client - Automatic Input Token Caching - Automatic Retry on API Failures @@ -107,7 +108,9 @@ Environment Variables. | `BASE_URL` | `https://opencode.ai/zen/v1` | OpenAI Compatible Provider Base URL | | [PROVIDER_OPTIONS](#PROVIDER_OPTIONS) | - | Provider Options JSON String | | `MAX_TOKENS` | - | Max Output Tokens | -| `INSTRUCTIONS` | - | Fallback System Instructions | +| `CHAT_INSTRUCTIONS` | - | System Instructions for Chat | +| `COMPLETION_INSTRUCTIONS` | - | System Instructions for Completion | +| `OBJECT_INSTRUCTIONS` | - | System Instructions for Object | | `AI_SDK_LOG_WARNINGS` | - | Disable SDK Warnings | | `CORS_ORIGINS` | - | Allowed CORS Origins (supports \*) | | `PORT` | `3000` | Server Port | @@ -145,6 +148,16 @@ The value is only checked for valid JSON at startup and will fail at runtime if ## Client +### Endpoints + +| Endpoint | Method | Description | +| :------------ | :----: | :------------------------------------------------------------------------------------------------------------------------------------- | +| `/chat` | `POST` | Use with [useChat](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) and [VitePress Chat](https://cssnr.github.io/vitepress-chat/) | +| `/completion` | `POST` | Use with [useCompletion](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-completion) | +| `/object` | `POST` | Use with [useObject](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-object) | + +### Chat + To send System Instructions from the client, add them to the body. ```typescript @@ -157,11 +170,46 @@ const chat = new Chat({ }) ``` +Reference: + +### Completion + +```typescript +import { useCompletion } from '@ai-sdk/react' + +const { completion, complete, isLoading, stop } = useCompletion({ + api: 'https://chat-server.cssnr.com/completion', + headers: { Authorization: 'Basic Abc123=' }, + body: { system: 'You are a helpful assistant.' }, +}) +``` + +Reference: + +### Object + +```typescript +import { useObject } from '@ai-sdk/react' + +const { object, submit } = useObject({ + api: 'https://chat-server.cssnr.com/object', + schema: z.object({ name: z.string(), age: z.number() }), + headers: { Authorization: 'Basic Abc123=' }, +}) + +submit({ + system: 'You are a helpful assistant.', + prompt: 'Extract the name and age from: John is 30 years old.', +}) +``` + +Reference: + ### VitePress Chat Plugin The client is currently available as a VitePress Plugin. -- https://github.com/cssnr/vitepress-chat +- [![View Documentation](https://img.shields.io/badge/view_documentation-blue?style=for-the-badge&logo=googledocs&logoColor=white)](https://cssnr.github.io/vitepress-chat/) diff --git a/src/index.ts b/src/index.ts index 7bd2e39..7ea1dc1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -73,7 +73,7 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { // console.log('req.headers:', req.headers) // console.log('authorization:', req.headers.authorization) const { messages, system } = req.body - // if (system) console.log('system:', system.substring(0, 512)) + // console.log('system:', system?.substring(0, 512)) const modelMessages = await convertToModelMessages(messages) console.log('modelMessages:', modelMessages.length) const stream = createUIMessageStream({ @@ -94,7 +94,8 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { app.post('/completion', async (req: Request, res: Response) => { const { prompt, system } = req.body - console.log('prompt:', prompt?.length, 'system:', system?.length) + console.log('prompt:', prompt?.length) + console.log('system:', system?.length) const result = streamText({ model: model, prompt, @@ -125,7 +126,9 @@ app.post('/completion', async (req: Request, res: Response) => { app.post('/object', async (req: Request, res: Response) => { const { outputSchema, prompt, system } = req.body - console.log('prompt:', prompt?.length, 'system:', system?.length) + console.log('outputSchema:', outputSchema?.length) + console.log('prompt:', prompt?.length) + console.log('system:', system?.length) const result = streamText({ model: model, prompt, From d63e304a1e4ec14775751595f9ac5c33548e19a2 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:45:52 -0700 Subject: [PATCH 5/6] Updates --- README.md | 6 +++++- src/index.ts | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0578466..7d0df3b 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ To send System Instructions from the client, add them to the body. ```typescript const chat = new Chat({ transport: new DefaultChatTransport({ - api: 'https://chat-server.cssnr.com/', + api: 'https://chat-server.cssnr.com/chat', headers: { Authorization: 'Basic Abc123=' }, body: { system: 'You are a helpful assistant.' }, }), @@ -200,6 +200,10 @@ const { object, submit } = useObject({ submit({ system: 'You are a helpful assistant.', prompt: 'Extract the name and age from: John is 30 years old.', + output: { + type: 'object', + properties: { name: { type: 'string' }, age: { type: 'number' } }, + }, }) ``` diff --git a/src/index.ts b/src/index.ts index 7ea1dc1..fe505b8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { Output, consumeStream, + jsonSchema, pipeTextStreamToResponse, streamText, createUIMessageStream, @@ -125,8 +126,8 @@ app.post('/completion', async (req: Request, res: Response) => { }) app.post('/object', async (req: Request, res: Response) => { - const { outputSchema, prompt, system } = req.body - console.log('outputSchema:', outputSchema?.length) + const { output, prompt, system } = req.body + console.log('output:', output ? 'SET' : undefined) console.log('prompt:', prompt?.length) console.log('system:', system?.length) const result = streamText({ @@ -135,7 +136,7 @@ app.post('/object', async (req: Request, res: Response) => { system: system || process.env.OBJECT_INSTRUCTIONS, maxOutputTokens, providerOptions, - output: outputSchema ? Output.object({ schema: outputSchema }) : Output.json(), + output: output ? Output.object({ schema: jsonSchema(output) }) : Output.json(), onError(error) { console.log('error:', error) }, From e641616498074ef0ae70a8ff99111f4ce304ca90 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Wed, 15 Jul 2026 22:33:01 -0700 Subject: [PATCH 6/6] Cleanup --- src/index.ts | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/index.ts b/src/index.ts index fe505b8..6e16701 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,14 +7,15 @@ import { google } from '@ai-sdk/google' import { openai } from '@ai-sdk/openai' import { createOpenAICompatible } from '@ai-sdk/openai-compatible' import { + type GenerateTextEndEvent, Output, consumeStream, + convertToModelMessages, + createUIMessageStream, jsonSchema, pipeTextStreamToResponse, - streamText, - createUIMessageStream, pipeUIMessageStreamToResponse, - convertToModelMessages, + streamText, toTextStream, toUIMessageStream, } from 'ai' @@ -76,7 +77,7 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { const { messages, system } = req.body // console.log('system:', system?.substring(0, 512)) const modelMessages = await convertToModelMessages(messages) - console.log('modelMessages:', modelMessages.length) + // console.log('modelMessages:', modelMessages.length) const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ @@ -85,6 +86,8 @@ app.post(['/', '/chat'], async (req: Request, res: Response) => { system: system || process.env.INSTRUCTIONS || process.env.CHAT_INSTRUCTIONS, maxOutputTokens, providerOptions, + onError: onStreamError, + onEnd: onStreamEnd, }) writer.merge(toUIMessageStream({ stream: result.stream })) }, @@ -103,15 +106,8 @@ app.post('/completion', async (req: Request, res: Response) => { system: system || process.env.COMPLETION_INSTRUCTIONS, maxOutputTokens, providerOptions, - onError(error) { - console.log('error:', error) - }, - onEnd({ finalStep, finishReason, text, usage }) { - console.log('reasoning:', finalStep.reasoningText) - console.log('response:', text) - console.log('usage:', usage) - console.log('finishReason:', finishReason) - }, + onError: onStreamError, + onEnd: onStreamEnd, }) const stream = createUIMessageStream({ execute: ({ writer }) => { @@ -137,15 +133,8 @@ app.post('/object', async (req: Request, res: Response) => { maxOutputTokens, providerOptions, output: output ? Output.object({ schema: jsonSchema(output) }) : Output.json(), - onError(error) { - console.log('error:', error) - }, - onEnd({ finalStep, finishReason, text, usage }) { - console.log('reasoning:', finalStep.reasoningText) - console.log('response:', text) - console.log('usage:', usage) - console.log('finishReason:', finishReason) - }, + onError: onStreamError, + onEnd: onStreamEnd, }) pipeTextStreamToResponse({ response: res, @@ -195,3 +184,14 @@ function getProviderOptions() { console.error('parsing PROVIDER_OPTIONS as JSON') } } + +function onStreamError(error: unknown) { + console.log('error:', error) +} + +function onStreamEnd({ finalStep, finishReason, text, usage }: GenerateTextEndEvent) { + console.log('reasoning:', finalStep.reasoningText) + console.log('response:', text) + console.log('usage:', usage) + console.log('finishReason:', finishReason) +}