-
Notifications
You must be signed in to change notification settings - Fork 17
feat/non streaming support #270
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
01e1c7a
2f55dee
59364ca
c74d731
e73e71d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { type Request, type Response } from 'express'; | ||
| import { streamText, stepCountIs, tool } from 'ai'; | ||
| import { streamText, generateText, stepCountIs, tool, type GenerateTextResult, type ToolSet } from 'ai'; | ||
| import getRawBody from 'raw-body'; | ||
| import fs from 'node:fs/promises'; | ||
| import path from 'node:path'; | ||
|
|
@@ -31,6 +31,80 @@ type StreamTextOptions = Parameters<typeof streamText>[0]; | |
|
|
||
| const BUSY_ERROR_MESSAGE = '算力繁忙,请切换其他模型或稍后重试'; | ||
|
|
||
| function buildCompletionFromGenerateText(result: GenerateTextResult<any, any>): any { | ||
| const message: any = { role: 'assistant', content: '' }; | ||
| const reasoningParts: string[] = []; | ||
| const toolCalls: any[] = []; | ||
| const toolResults: any[] = []; | ||
|
|
||
| for (const step of result.steps) { | ||
| if (step.reasoningText) reasoningParts.push(step.reasoningText); | ||
| if (step.text) message.content += step.text; | ||
| for (const toolCall of step.toolCalls) { | ||
| toolCalls.push({ | ||
| id: toolCall.toolCallId, | ||
| type: 'function', | ||
| function: { | ||
| name: toolCall.toolName, | ||
| arguments: stringifyToolInput(toolCall.input), | ||
| }, | ||
| }); | ||
| } | ||
| for (const toolResult of step.toolResults) { | ||
| toolResults.push({ | ||
| id: toolResult.toolCallId, | ||
| type: 'function', | ||
| function: { | ||
| name: toolResult.toolName, | ||
| arguments: stringifyToolInput(toolResult.input), | ||
| result: toolResult.output, | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| if (reasoningParts.length) message.reasoning_content = reasoningParts.join(''); | ||
| if (toolCalls.length) message.tool_calls = toolCalls; | ||
| if (toolResults.length) message.tool_calls_result = toolResults; | ||
|
|
||
| const { inputTokens, outputTokens, totalTokens } = result.totalUsage; | ||
| return { | ||
| id: result.response?.id ?? `chatcmpl-${Date.now()}`, | ||
| object: 'chat.completion', | ||
| created: Date.now(), | ||
| model: result.response?.modelId ?? '', | ||
|
Comment on lines
+72
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- applicable repository knowledge files ---'
find /tmp/coderabbit-repo-knowledge/opentiny-genui-sdk-b1ee012d -type f -path '*/.md' -print | sort
printf '%s\n' '--- knowledge headers ---'
head -5 /tmp/coderabbit-repo-knowledge/opentiny-genui-sdk-b1ee012d/*/*.md 2>/dev/null || true
printf '%s\n' '--- target outline ---'
ast-grep outline sites/playground/server/src/chat-genui.ts
printf '%s\n' '--- target source ---'
cat -n sites/playground/server/src/chat-genui.ts
printf '%s\n' '--- direct definitions/usages ---'
rg -n -C 4 'buildCompletionFromGenerateText|result\.response|generateText|chat\.completion|created:' sites/playground/server/src sites/playground -g '*.ts' -g '*.tsx' -g '*.md'Repository: opentiny/genui-sdk Length of output: 41862 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- manifests and lockfile references for ai ---'
find . -maxdepth 4 -type f \( -name 'package.json' -o -name 'pnpm-lock.yaml' -o -name 'yarn.lock' -o -name 'package-lock.json' \) -print \
| sort \
| xargs -r rg -n -C 2 '"ai"|/ai@|ai@' || trueRepository: opentiny/genui-sdk Length of output: 8557 🌐 Web query:
💡 Result: In the OpenAI Chat Completions API, the response object contains a field named Citations:
🌐 Web query:
💡 Result: In the Vercel AI SDK, the Citations:
🌐 Web query:
💡 Result: In the Vercel AI SDK, the Citations:
🌐 Web query:
💡 Result: In the Vercel AI SDK, the LanguageModelResponseMetadata type includes a timestamp property that records when the generated response started [1][2]. The structure of LanguageModelResponseMetadata is defined as follows: - id: string (The unique ID for the generated response) [1][2] - timestamp: Date (The timestamp for the start of the generated response) [1][2] - modelId: string (The ID of the model used) [1][2] - messages: Array (The response messages generated) [1][2] - headers?: Record<string, string> (Optional response headers, for providers using HTTP) [1][2] - body?: unknown (Optional response body, for providers using HTTP) [1][2] When using generateText, this metadata is accessible via the response property on the returned GenerateTextResult object [3][4]. For example, you can access the timestamp using result.response.timestamp [4]. Note that while the SDK has evolved, this metadata structure remains consistent across versions, including the Vercel AI SDK 5.x series [5][1][2]. Citations:
🏁 Script executed: #!/bin/bash
set -eu
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for url in \
'https://unpkg.com/ai@5.0.10/dist/index.d.ts' \
'https://unpkg.com/ai@5.0.10/dist/index.d.mts' \
'https://unpkg.com/ai@5.0.10/src/generate-text/generate-text-result.ts' \
'https://raw.githubusercontent.com/vercel/ai/ai@5.0.10/packages/ai/src/generate-text/generate-text-result.ts'
do
printf '\n--- %s ---\n' "$url"
if curl -fsSL --max-time 15 "$url" -o "$tmp/out"; then
rg -n -C 8 'GenerateTextResult|response:|timestamp' "$tmp/out" | head -120 || head -40 "$tmp/out"
else
printf '%s\n' 'unavailable'
fi
doneRepository: opentiny/genui-sdk Length of output: 9179 Use Unix seconds for
🤖 Prompt for AI Agents |
||
| choices: [ | ||
| { | ||
| index: 0, | ||
| message, | ||
| finish_reason: mapFinishReason(result.finishReason), | ||
| }, | ||
| ], | ||
| usage: { | ||
| prompt_tokens: inputTokens, | ||
| completion_tokens: outputTokens, | ||
| total_tokens: totalTokens, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| /** 工具入参序列化为 OpenAI 协议要求的 JSON 字符串 */ | ||
| function stringifyToolInput(input: unknown): string { | ||
| if (typeof input === 'string') return input; | ||
| try { | ||
| return JSON.stringify(input ?? {}); | ||
| } catch { | ||
| return '{}'; | ||
| } | ||
| } | ||
|
|
||
| /** AI SDK 的 finishReason 映射为 OpenAI 协议的取值 */ | ||
| function mapFinishReason(finishReason: string): string { | ||
| if (finishReason === 'tool-calls') return 'tool_calls'; | ||
| if (finishReason === 'content-filter') return 'content_filter'; | ||
| return finishReason; | ||
| } | ||
|
|
||
| function extractStatusCode(error: any): number | undefined { | ||
| if (!error) { | ||
| return undefined; | ||
|
|
@@ -246,6 +320,7 @@ export function createChatGenui() { | |
| const chatGenuiHandler = async (req: Request, res: Response): Promise<void> => { | ||
| const abort = new AbortController(); | ||
| const body = JSON.parse(await getRawBody(req, { encoding: 'utf-8' })); | ||
| const isStreaming = body.stream !== false; | ||
| if (process.env.CHAT_UI_REPLAY_MODE === 'true') { | ||
| res.setHeader('Content-Type', 'text/event-stream'); | ||
| const text = await fs.readFile(path.join(fileURLToPath(import.meta.url), '../replay/replay.txt'), 'utf-8'); | ||
|
|
@@ -387,6 +462,36 @@ export function createChatGenui() { | |
| } | ||
| }); | ||
|
|
||
| if (!isStreaming) { | ||
| const generateOptions = { | ||
| model: model!, | ||
| temperature, | ||
| system: options.system, | ||
| messages: options.messages, | ||
| abortSignal: abort.signal, | ||
| tools: tools as ToolSet, | ||
| toolChoice: 'auto' as const, | ||
| stopWhen: stepCountIs(maxSteps), | ||
| ...(providerOptions ? { providerOptions } : {}), | ||
| }; | ||
|
|
||
| try { | ||
| const result = await generateText(generateOptions); | ||
| if (abort.signal.aborted) { | ||
| res.status(499).json({ message: 'Request aborted', type: 'AbortedError', param: null, code: 499 }); | ||
| return; | ||
| } | ||
| res.json(buildCompletionFromGenerateText(result)); | ||
| } catch (error: any) { | ||
| const statusCode = error?.statusCode ?? 500; | ||
| const message = error?.message || 'Internal Server Error'; | ||
| console.error('Error in chat-genui generateText:', error); | ||
| const errorResponse = { message, type: 'Internal Server Error', param: null, code: 'Internal Server Error' }; | ||
| res.status(statusCode).json(errorResponse); | ||
| } | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| const stream = streamText(options); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Complete the non-streaming error lifecycle.
If
client.chat()rejects, Lines 129-140 do not run.chatRequest()only setsSTATUS.ERROR.GenuiChat.vuetherefore does not receive itsonFinisherror payload, so it does not add the error message or save the conversation. Route non-streaming request failures through the same completion-error contract as streaming requests.🤖 Prompt for AI Agents