Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,444 changes: 560 additions & 884 deletions package-lock.json

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,20 @@
"shadcn:add": "npx shadcn@latest add"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^1.0.7",
"@ai-sdk/anthropic": "^1.1.15",
"@ai-sdk/google": "^1.0.7",
"@ai-sdk/openai": "^0.0.34",
"@ai-sdk/openai-compatible": "^0.2.12",
"@ai-sdk/provider": "^1.0.7",
"@ai-sdk/amazon-bedrock": "^4.0.37",
"@ai-sdk/anthropic": "^3.0.29",
"@ai-sdk/azure": "^3.0.0",
"@ai-sdk/google": "^3.0.16",
"@ai-sdk/openai": "^3.0.21",
"@ai-sdk/openai-compatible": "^2.0.21",
"@ai-sdk/provider": "^3.0.5",
"@ai-sdk/provider-utils": "^4.0.10",
"@aws-sdk/client-bedrock-runtime": "^3.751.0",
"@aws-sdk/client-rds-data": "^3.825.0",
"@aws-sdk/client-s3": "3.338.0",
"@aws-sdk/s3-presigned-post": "3.338.0",
"@aws-sdk/s3-request-presigner": "3.338.0",
"@aws-sdk/signature-v4-crt": "3.338.0",
"@dqbd/tiktoken": "^1.0.16",
"@emotion/react": "^11.11.1",
"@google/generative-ai": "^0.21.0",
Expand Down Expand Up @@ -80,7 +83,7 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@t3-oss/env-nextjs": "^0.2.1",
"@t3-oss/env-nextjs": "^0.13.8",
"@tabler/icons-react": "^2.47.0",
"@tanstack/react-form": "^0.29.2",
"@tanstack/react-query": "^5.28.9",
Expand All @@ -95,7 +98,7 @@
"@vercel/edge-config": "^0.2.0",
"@vercel/kv": "^0.2.1",
"@vercel/speed-insights": "^1.1.0",
"ai": "^4.3.19",
"ai": "^6.0.33",
"axios": "^1.6.0",
"class-variance-authority": "^0.7.1",
"cmdk": "^1.1.1",
Expand All @@ -116,8 +119,7 @@
"jsonwebtoken": "^9.0.2",
"jwks-rsa": "^3.2.0",
"keycloak-js": "^26.2.0",
"langchain": "^0.2.1",
"langsmith": "^0.1.25",
"langsmith": "^0.1.68",
"lodash": "^4.17.21",
"lucide-react": "^0.447.0",
"mantine-datatable": "^6.0.0",
Expand All @@ -126,8 +128,8 @@
"next-themes": "^0.4.6",
"oidc-client-ts": "^3.2.1",
"ollama": "^0.5.6",
"ollama-ai-provider": "^0.10.0",
"openai": "^4.42.0",
"ollama-ai-provider-v2": "^2.0.0",
"openai": "^6.16.0",
"postgres": "^3.4.7",
"posthog-js": "^1.258.5",
"react": "^18.2.0",
Expand Down Expand Up @@ -157,7 +159,7 @@
"uuid": "^13.0.0",
"vaul": "^1.1.2",
"vercel": "^31.0.1",
"zod": "^3.25.76"
"zod": "^4.3.5"
},
"devDependencies": {
"@next/bundle-analyzer": "^13.4.3",
Expand Down
12 changes: 6 additions & 6 deletions src/app/api/chat/anthropic/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createAnthropic } from '@ai-sdk/anthropic'
import { generateText, smoothStream, streamText, type CoreMessage } from 'ai'
import { generateText, smoothStream, streamText, type ModelMessage } from 'ai'
import { type ChatBody, type Conversation } from '~/types/chat'
import { type AuthenticatedRequest } from '~/utils/appRouterAuth'
import { decryptKeyIfNeeded } from '~/utils/crypto'
Expand Down Expand Up @@ -74,7 +74,7 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {
getAnthropicRequestConfig(conversation)

if (chatBody.stream) {
const result = await streamText({
const result = streamText({
model: anthropic(modelId),
temperature: conversation.temperature,
messages: convertConversationToVercelAISDKv3(conversation),
Expand All @@ -86,9 +86,9 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {

if (isThinking) {
console.log('Using Claude with reasoning enabled')
const response = result.toDataStreamResponse({
const response = result.toUIMessageStreamResponse({
sendReasoning: true,
getErrorMessage: () => {
onError: () => {
return `An error occurred while streaming the response.`
},
})
Expand Down Expand Up @@ -149,8 +149,8 @@ export const POST = withCourseAccessFromRequest('any')(handler)

function convertConversationToVercelAISDKv3(
conversation: Conversation,
): CoreMessage[] {
const coreMessages: CoreMessage[] = []
): ModelMessage[] {
const coreMessages: ModelMessage[] = []

const systemMessage = conversation.messages.findLast(
(msg) => msg.latestSystemMessage !== undefined,
Expand Down
49 changes: 35 additions & 14 deletions src/app/api/chat/openAI/route.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { createOpenAI } from '@ai-sdk/openai'
import { generateText, streamText, type CoreMessage } from 'ai'
import { generateText, streamText, type ModelMessage } from 'ai'
import { NextResponse } from 'next/server'
import OpenAI from 'openai'
import { type AuthenticatedRequest } from '~/utils/appRouterAuth'
import { uiMessageStreamResponseToTextWithThink } from '~/app/utils/streaming/reasoningToThink'
import { decrypt } from '~/utils/crypto'
import { withCourseAccessFromRequest } from '~/app/api/authorization'

Expand Down Expand Up @@ -81,30 +82,43 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {

const openAIClient = createOpenAI({
apiKey,
baseURL: 'https://api.openai.com/v1', // Default OpenAI API URL
compatibility: 'strict', // Use strict mode for OpenAI API

// Default OpenAI API URL
baseURL: 'https://api.openai.com/v1',
})

const openAIModel = openAIClient(model)
const openAIModel = openAIClient.responses(model)

const commonParams = {
const commonParams: Record<string, unknown> = {
model: openAIModel,
messages: messages as CoreMessage[],
temperature: 0.7, // You might want to make this configurable
maxTokens: 8192,
messages: messages as ModelMessage[],
maxOutputTokens: 8192,
}
if (supportsTemperature(model)) {
commonParams.temperature = 0.7
} else {
commonParams.providerOptions = { openai: { reasoningSummary: 'auto' } }
}

if (stream) {
const result = await streamText(commonParams as any)
const response = result.toTextStreamResponse()
return new NextResponse(response.body, {
status: response.status,
headers: response.headers,
const result = streamText(commonParams as any)
const uiResponse = result.toUIMessageStreamResponse({
sendReasoning: true,
onError: () => `An error occurred while streaming the response.`,
})
const textResponse = uiMessageStreamResponseToTextWithThink(uiResponse)
return new NextResponse(textResponse.body, {
status: textResponse.status,
headers: textResponse.headers,
})
} else {
const result = await generateText(commonParams as any)
const formatted =
result.reasoningText && result.reasoningText.length > 0
? `<think>${result.reasoningText}</think>\n${result.text}`
: result.text
return NextResponse.json({
choices: [{ message: { content: result.text } }],
choices: [{ message: { content: formatted } }],
})
}
} catch (error) {
Expand All @@ -118,3 +132,10 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {
}

export const POST = withCourseAccessFromRequest('any')(handler)

function supportsTemperature(modelId: string): boolean {
const id = modelId.toLowerCase()
if (id.startsWith('gpt-5')) return false
if (id.startsWith('o')) return false
return true
}
8 changes: 4 additions & 4 deletions src/app/api/chat/vlm/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,17 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {
const openai = createOpenAI({
baseURL: process.env.NCSA_HOSTED_VLM_BASE_URL,
apiKey: 'non-empty',
compatibility: 'compatible', // strict/compatible - enable 'strict' when using the OpenAI API
})

const messages =
convertConversationToCoreMessagesWithoutSystem(conversation)
// const messages = convertToCoreMessages(conversation)
console.log('⭐️ messages', JSON.stringify(messages, null, 2))

// Use .chat() to use Chat Completions API instead of Responses API
if (stream) {
const result = await streamText({
model: openai(conversation.model.id) as any,
const result = streamText({
model: openai.chat(conversation.model.id) as any,
temperature: conversation.temperature,
messages,
})
Expand All @@ -40,7 +40,7 @@ async function handler(req: AuthenticatedRequest): Promise<NextResponse> {
})
} else {
const result = await generateText({
model: openai(conversation.model.id) as any,
model: openai.chat(conversation.model.id) as any,
temperature: conversation.temperature,
messages,
})
Expand Down
28 changes: 15 additions & 13 deletions src/app/utils/anthropic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type CoreMessage, generateText, streamText, smoothStream } from 'ai'
import { type ModelMessage, generateText, streamText, smoothStream } from 'ai'
import { type Conversation } from '~/types/chat'
import { createAnthropic } from '@ai-sdk/anthropic'
import {
Expand Down Expand Up @@ -55,7 +55,7 @@ export async function runAnthropicChat(
model: model,
messages: convertConversationToVercelAISDKv3(conversation),
temperature: conversation.temperature,
maxTokens: 4096,
maxOutputTokens: 4096,
...(isAnthropicModelWithThinking && {
providerOptions: {
anthropic: {
Expand Down Expand Up @@ -85,7 +85,7 @@ async function handleStreamingResponse(
commonParams: any,
isThinkingEnabled: boolean,
): Promise<Response> {
const result = await streamText({
const result = streamText({
...commonParams,
experimental_transform: [
smoothStream({
Expand All @@ -99,9 +99,9 @@ async function handleStreamingResponse(
}

// Get the original stream response
const originalResponse = result.toDataStreamResponse({
const originalResponse = result.toUIMessageStreamResponse({
sendReasoning: true,
getErrorMessage: () => {
onError: () => {
return `An error occurred while streaming the response.`
},
})
Expand Down Expand Up @@ -272,13 +272,13 @@ async function handleNonStreamingResponse(
isThinkingEnabled: boolean,
): Promise<Response> {
if (isThinkingEnabled) {
const { text, reasoning } = await generateText({
const { text, reasoningText } = await generateText({
...commonParams,
})

// Format the response with reasoning wrapped in <think> tags at the beginning
const formattedContent = reasoning
? `<think>${reasoning}</think>\n${text}`
const formattedContent = reasoningText
? `<think>${reasoningText}</think>\n${text}`
: text

return new Response(
Expand Down Expand Up @@ -306,8 +306,8 @@ async function handleNonStreamingResponse(
*/
function convertConversationToVercelAISDKv3(
conversation: Conversation,
): CoreMessage[] {
const coreMessages: CoreMessage[] = []
): ModelMessage[] {
const coreMessages: ModelMessage[] = []

const systemMessage = conversation.messages.findLast(
(msg) => msg.latestSystemMessage !== undefined,
Expand All @@ -328,16 +328,18 @@ function convertConversationToVercelAISDKv3(
} else if (Array.isArray(message.content)) {
// Handle both text and file content
const textParts: string[] = []

message.content.forEach((c) => {
if (c.type === 'text') {
textParts.push(c.text || '')
} else if (c.type === 'file') {
// Convert file content to text representation for Anthropic
textParts.push(`[File: ${c.fileName || 'unknown'} (${c.fileType || 'unknown type'}, ${c.fileSize ? Math.round(c.fileSize / 1024) + 'KB' : 'unknown size'})]`)
textParts.push(
`[File: ${c.fileName || 'unknown'} (${c.fileType || 'unknown type'}, ${c.fileSize ? Math.round(c.fileSize / 1024) + 'KB' : 'unknown size'})]`,
)
}
})

content = textParts.join('\n')
} else {
content = message.content as string
Expand Down
Loading