fix: conversation items (#6815)
This commit is contained in:
parent
e46200868e
commit
f07e43cfe0
4
.github/workflows/jan-server-web-ci-dev.yml
vendored
4
.github/workflows/jan-server-web-ci-dev.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
build-and-preview:
|
build-and-preview:
|
||||||
runs-on: [ubuntu-24-04-docker]
|
runs-on: [ubuntu-24-04-docker]
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api-dev.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api-dev.menlo.ai/v1"
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
contents: write
|
||||||
@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
run: |
|
run: |
|
||||||
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
||||||
|
|
||||||
- name: Push docker image
|
- name: Push docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|||||||
4
.github/workflows/jan-server-web-ci-prod.yml
vendored
4
.github/workflows/jan-server-web-ci-prod.yml
vendored
@ -13,7 +13,7 @@ jobs:
|
|||||||
deployments: write
|
deployments: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api.menlo.ai/v1"
|
||||||
GA_MEASUREMENT_ID: "G-YK53MX8M8M"
|
GA_MEASUREMENT_ID: "G-YK53MX8M8M"
|
||||||
CLOUDFLARE_PROJECT_NAME: "jan-server-web"
|
CLOUDFLARE_PROJECT_NAME: "jan-server-web"
|
||||||
steps:
|
steps:
|
||||||
@ -43,7 +43,7 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: make config-yarn && yarn install && yarn build:core && make build-web-app
|
run: make config-yarn && yarn install && yarn build:core && make build-web-app
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: ${{ env.JAN_API_BASE }}
|
MENLO_PLATFORM_BASE_URL: ${{ env.MENLO_PLATFORM_BASE_URL }}
|
||||||
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
|
GA_MEASUREMENT_ID: ${{ env.GA_MEASUREMENT_ID }}
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages Production
|
- name: Publish to Cloudflare Pages Production
|
||||||
|
|||||||
4
.github/workflows/jan-server-web-ci-stag.yml
vendored
4
.github/workflows/jan-server-web-ci-stag.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
|||||||
build-and-preview:
|
build-and-preview:
|
||||||
runs-on: [ubuntu-24-04-docker]
|
runs-on: [ubuntu-24-04-docker]
|
||||||
env:
|
env:
|
||||||
JAN_API_BASE: "https://api-stag.menlo.ai/v1"
|
MENLO_PLATFORM_BASE_URL: "https://api-stag.menlo.ai/v1"
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
contents: write
|
contents: write
|
||||||
@ -52,7 +52,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build docker image
|
- name: Build docker image
|
||||||
run: |
|
run: |
|
||||||
docker build --build-arg JAN_API_BASE=${{ env.JAN_API_BASE }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
docker build --build-arg MENLO_PLATFORM_BASE_URL=${{ env.MENLO_PLATFORM_BASE_URL }} -t ${{ steps.vars.outputs.FULL_IMAGE }} .
|
||||||
|
|
||||||
- name: Push docker image
|
- name: Push docker image
|
||||||
if: github.event_name == 'push'
|
if: github.event_name == 'push'
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
# Stage 1: Build stage with Node.js and Yarn v4
|
# Stage 1: Build stage with Node.js and Yarn v4
|
||||||
FROM node:20-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
ARG JAN_API_BASE=https://api-dev.jan.ai/v1
|
ARG MENLO_PLATFORM_BASE_URL=https://api-dev.menlo.ai/v1
|
||||||
ENV JAN_API_BASE=$JAN_API_BASE
|
ENV MENLO_PLATFORM_BASE_URL=$MENLO_PLATFORM_BASE_URL
|
||||||
|
|
||||||
# Install build dependencies
|
# Install build dependencies
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import {
|
|||||||
ListConversationItemsResponse
|
ListConversationItemsResponse
|
||||||
} from './types'
|
} from './types'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export class RemoteApi {
|
export class RemoteApi {
|
||||||
private authService: JanAuthService
|
private authService: JanAuthService
|
||||||
@ -28,7 +28,7 @@ export class RemoteApi {
|
|||||||
async createConversation(
|
async createConversation(
|
||||||
data: Conversation
|
data: Conversation
|
||||||
): Promise<ConversationResponse> {
|
): Promise<ConversationResponse> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
||||||
url,
|
url,
|
||||||
@ -43,7 +43,7 @@ export class RemoteApi {
|
|||||||
conversationId: string,
|
conversationId: string,
|
||||||
data: Conversation
|
data: Conversation
|
||||||
): Promise<ConversationResponse> {
|
): Promise<ConversationResponse> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
return this.authService.makeAuthenticatedRequest<ConversationResponse>(
|
||||||
url,
|
url,
|
||||||
@ -70,7 +70,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATIONS}${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
|
return this.authService.makeAuthenticatedRequest<ListConversationsResponse>(
|
||||||
url,
|
url,
|
||||||
@ -114,7 +114,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteConversation(conversationId: string): Promise<void> {
|
async deleteConversation(conversationId: string): Promise<void> {
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_BY_ID(conversationId)}`
|
||||||
|
|
||||||
await this.authService.makeAuthenticatedRequest(
|
await this.authService.makeAuthenticatedRequest(
|
||||||
url,
|
url,
|
||||||
@ -141,7 +141,7 @@ export class RemoteApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryString = queryParams.toString()
|
const queryString = queryParams.toString()
|
||||||
const url = `${JAN_API_BASE}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
|
const url = `${MENLO_PLATFORM_BASE_URL}${CONVERSATION_API_ROUTES.CONVERSATION_ITEMS(conversationId)}${queryString ? `?${queryString}` : ''}`
|
||||||
|
|
||||||
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
|
return this.authService.makeAuthenticatedRequest<ListConversationItemsResponse>(
|
||||||
url,
|
url,
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export interface ConversationResponse {
|
|||||||
id: string
|
id: string
|
||||||
object: 'conversation'
|
object: 'conversation'
|
||||||
title?: string
|
title?: string
|
||||||
created_at: number
|
created_at: number | string
|
||||||
metadata: ConversationMetadata
|
metadata: ConversationMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,6 +50,7 @@ export interface ConversationItemAnnotation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationItemContent {
|
export interface ConversationItemContent {
|
||||||
|
type?: string
|
||||||
file?: {
|
file?: {
|
||||||
file_id?: string
|
file_id?: string
|
||||||
mime_type?: string
|
mime_type?: string
|
||||||
@ -62,23 +63,50 @@ export interface ConversationItemContent {
|
|||||||
file_id?: string
|
file_id?: string
|
||||||
url?: string
|
url?: string
|
||||||
}
|
}
|
||||||
|
image_file?: {
|
||||||
|
file_id?: string
|
||||||
|
mime_type?: string
|
||||||
|
}
|
||||||
input_text?: string
|
input_text?: string
|
||||||
output_text?: {
|
output_text?: {
|
||||||
annotations?: ConversationItemAnnotation[]
|
annotations?: ConversationItemAnnotation[]
|
||||||
text?: string
|
text?: string
|
||||||
}
|
}
|
||||||
reasoning_content?: string
|
|
||||||
text?: {
|
text?: {
|
||||||
value?: string
|
value?: string
|
||||||
|
text?: string
|
||||||
}
|
}
|
||||||
type?: string
|
reasoning_content?: string
|
||||||
|
tool_calls?: Array<{
|
||||||
|
id?: string
|
||||||
|
type?: string
|
||||||
|
function?: {
|
||||||
|
name?: string
|
||||||
|
arguments?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
tool_call_id?: string
|
||||||
|
tool_result?: {
|
||||||
|
content?: Array<{
|
||||||
|
type?: string
|
||||||
|
text?: string
|
||||||
|
output_text?: {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
}>
|
||||||
|
output_text?: {
|
||||||
|
text?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text_result?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConversationItem {
|
export interface ConversationItem {
|
||||||
content?: ConversationItemContent[]
|
content?: ConversationItemContent[]
|
||||||
created_at: number
|
created_at: number | string
|
||||||
id: string
|
id: string
|
||||||
object: string
|
object: string
|
||||||
|
metadata?: Record<string, unknown>
|
||||||
role: string
|
role: string
|
||||||
status?: string
|
status?: string
|
||||||
type?: string
|
type?: string
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
|
import { Thread, ThreadAssistantInfo, ThreadMessage, ContentType } from '@janhq/core'
|
||||||
import { Conversation, ConversationResponse, ConversationItem } from './types'
|
import { Conversation, ConversationResponse, ConversationItem, ConversationItemContent, ConversationMetadata } from './types'
|
||||||
import { DEFAULT_ASSISTANT } from './const'
|
import { DEFAULT_ASSISTANT } from './const'
|
||||||
|
|
||||||
export class ObjectParser {
|
export class ObjectParser {
|
||||||
@ -7,7 +7,7 @@ export class ObjectParser {
|
|||||||
const modelName = thread.assistants?.[0]?.model?.id || undefined
|
const modelName = thread.assistants?.[0]?.model?.id || undefined
|
||||||
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
|
const modelProvider = thread.assistants?.[0]?.model?.engine || undefined
|
||||||
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
|
const isFavorite = thread.metadata?.is_favorite?.toString() || 'false'
|
||||||
let metadata = {}
|
let metadata: ConversationMetadata = {}
|
||||||
if (modelName && modelProvider) {
|
if (modelName && modelProvider) {
|
||||||
metadata = {
|
metadata = {
|
||||||
model_id: modelName,
|
model_id: modelName,
|
||||||
@ -23,15 +23,14 @@ export class ObjectParser {
|
|||||||
|
|
||||||
static conversationToThread(conversation: ConversationResponse): Thread {
|
static conversationToThread(conversation: ConversationResponse): Thread {
|
||||||
const assistants: ThreadAssistantInfo[] = []
|
const assistants: ThreadAssistantInfo[] = []
|
||||||
if (
|
const metadata: ConversationMetadata = conversation.metadata || {}
|
||||||
conversation.metadata?.model_id &&
|
|
||||||
conversation.metadata?.model_provider
|
if (metadata.model_id && metadata.model_provider) {
|
||||||
) {
|
|
||||||
assistants.push({
|
assistants.push({
|
||||||
...DEFAULT_ASSISTANT,
|
...DEFAULT_ASSISTANT,
|
||||||
model: {
|
model: {
|
||||||
id: conversation.metadata.model_id,
|
id: metadata.model_id,
|
||||||
engine: conversation.metadata.model_provider,
|
engine: metadata.model_provider,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@ -44,16 +43,18 @@ export class ObjectParser {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFavorite = conversation.metadata?.is_favorite === 'true'
|
const isFavorite = metadata.is_favorite === 'true'
|
||||||
|
const createdAtMs = parseTimestamp(conversation.created_at)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: conversation.id,
|
id: conversation.id,
|
||||||
title: conversation.title || '',
|
title: conversation.title || '',
|
||||||
assistants,
|
assistants,
|
||||||
created: conversation.created_at,
|
created: createdAtMs,
|
||||||
updated: conversation.created_at,
|
updated: createdAtMs,
|
||||||
model: {
|
model: {
|
||||||
id: conversation.metadata.model_id,
|
id: metadata.model_id,
|
||||||
provider: conversation.metadata.model_provider,
|
provider: metadata.model_provider,
|
||||||
},
|
},
|
||||||
isFavorite,
|
isFavorite,
|
||||||
metadata: { is_favorite: isFavorite },
|
metadata: { is_favorite: isFavorite },
|
||||||
@ -65,74 +66,70 @@ export class ObjectParser {
|
|||||||
threadId: string
|
threadId: string
|
||||||
): ThreadMessage {
|
): ThreadMessage {
|
||||||
// Extract text content and metadata from the item
|
// Extract text content and metadata from the item
|
||||||
let textContent = ''
|
const textSegments: string[] = []
|
||||||
let reasoningContent = ''
|
const reasoningSegments: string[] = []
|
||||||
const imageUrls: string[] = []
|
const imageUrls: string[] = []
|
||||||
let toolCalls: any[] = []
|
let toolCalls: any[] = []
|
||||||
let finishReason = ''
|
|
||||||
|
|
||||||
if (item.content && item.content.length > 0) {
|
if (item.content && item.content.length > 0) {
|
||||||
for (const content of item.content) {
|
for (const content of item.content) {
|
||||||
// Handle text content
|
extractContentByType(content, {
|
||||||
if (content.text?.value) {
|
onText: (value) => {
|
||||||
textContent = content.text.value
|
if (value) {
|
||||||
}
|
textSegments.push(value)
|
||||||
// Handle output_text for assistant messages
|
}
|
||||||
if (content.output_text?.text) {
|
},
|
||||||
textContent = content.output_text.text
|
onReasoning: (value) => {
|
||||||
}
|
if (value) {
|
||||||
// Handle reasoning content
|
reasoningSegments.push(value)
|
||||||
if (content.reasoning_content) {
|
}
|
||||||
reasoningContent = content.reasoning_content
|
},
|
||||||
}
|
onImage: (url) => {
|
||||||
// Handle image content
|
if (url) {
|
||||||
if (content.image?.url) {
|
imageUrls.push(url)
|
||||||
imageUrls.push(content.image.url)
|
}
|
||||||
}
|
},
|
||||||
// Extract finish_reason
|
onToolCalls: (calls) => {
|
||||||
if (content.finish_reason) {
|
toolCalls = calls.map((toolCall) => {
|
||||||
finishReason = content.finish_reason
|
const callId = toolCall.id || 'unknown'
|
||||||
}
|
const rawArgs = toolCall.function?.arguments
|
||||||
}
|
const normalizedArgs =
|
||||||
}
|
typeof rawArgs === 'string'
|
||||||
|
? rawArgs
|
||||||
// Handle tool calls parsing for assistant messages
|
: JSON.stringify(rawArgs ?? {})
|
||||||
if (item.role === 'assistant' && finishReason === 'tool_calls') {
|
return {
|
||||||
try {
|
id: callId,
|
||||||
// Tool calls are embedded as JSON string in textContent
|
tool_call_id: callId,
|
||||||
const toolCallMatch = textContent.match(/\[.*\]/)
|
tool: {
|
||||||
if (toolCallMatch) {
|
id: callId,
|
||||||
const toolCallsData = JSON.parse(toolCallMatch[0])
|
function: {
|
||||||
toolCalls = toolCallsData.map((toolCall: any) => ({
|
name: toolCall.function?.name || 'unknown',
|
||||||
tool: {
|
arguments: normalizedArgs,
|
||||||
id: toolCall.id || 'unknown',
|
},
|
||||||
function: {
|
type: toolCall.type || 'function',
|
||||||
name: toolCall.function?.name || 'unknown',
|
},
|
||||||
arguments: toolCall.function?.arguments || '{}'
|
response: {
|
||||||
},
|
error: '',
|
||||||
type: toolCall.type || 'function'
|
content: [],
|
||||||
},
|
},
|
||||||
response: {
|
state: 'pending',
|
||||||
error: '',
|
}
|
||||||
content: []
|
})
|
||||||
},
|
},
|
||||||
state: 'ready'
|
})
|
||||||
}))
|
|
||||||
// Remove tool calls JSON from text content, keep only reasoning
|
|
||||||
textContent = ''
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to parse tool calls:', error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format final content with reasoning if present
|
// Format final content with reasoning if present
|
||||||
let finalTextValue = ''
|
let finalTextValue = ''
|
||||||
if (reasoningContent) {
|
if (reasoningSegments.length > 0) {
|
||||||
finalTextValue = `<think>${reasoningContent}</think>`
|
finalTextValue += `<think>${reasoningSegments.join('\n')}</think>`
|
||||||
}
|
}
|
||||||
if (textContent) {
|
if (textSegments.length > 0) {
|
||||||
finalTextValue += textContent
|
if (finalTextValue) {
|
||||||
|
finalTextValue += '\n'
|
||||||
|
}
|
||||||
|
finalTextValue += textSegments.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build content array for ThreadMessage
|
// Build content array for ThreadMessage
|
||||||
@ -157,22 +154,26 @@ export class ObjectParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build metadata
|
// Build metadata
|
||||||
const metadata: any = {}
|
const metadata: any = { ...(item.metadata || {}) }
|
||||||
if (toolCalls.length > 0) {
|
if (toolCalls.length > 0) {
|
||||||
metadata.tool_calls = toolCalls
|
metadata.tool_calls = toolCalls
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createdAtMs = parseTimestamp(item.created_at)
|
||||||
|
|
||||||
// Map status from server format to frontend format
|
// Map status from server format to frontend format
|
||||||
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
|
const mappedStatus = item.status === 'completed' ? 'ready' : item.status || 'ready'
|
||||||
|
|
||||||
|
const role = item.role === 'user' || item.role === 'assistant' ? item.role : 'assistant'
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
id: item.id,
|
id: item.id,
|
||||||
object: 'thread.message',
|
object: 'thread.message',
|
||||||
thread_id: threadId,
|
thread_id: threadId,
|
||||||
role: item.role as 'user' | 'assistant',
|
role,
|
||||||
content: messageContent,
|
content: messageContent,
|
||||||
created_at: item.created_at * 1000, // Convert to milliseconds
|
created_at: createdAtMs,
|
||||||
completed_at: 0,
|
completed_at: 0,
|
||||||
status: mappedStatus,
|
status: mappedStatus,
|
||||||
metadata,
|
metadata,
|
||||||
@ -201,25 +202,46 @@ export const combineConversationItemsToMessages = (
|
|||||||
): ThreadMessage[] => {
|
): ThreadMessage[] => {
|
||||||
const messages: ThreadMessage[] = []
|
const messages: ThreadMessage[] = []
|
||||||
const toolResponseMap = new Map<string, any>()
|
const toolResponseMap = new Map<string, any>()
|
||||||
|
const sortedItems = [...items].sort(
|
||||||
|
(a, b) => parseTimestamp(a.created_at) - parseTimestamp(b.created_at)
|
||||||
|
)
|
||||||
|
|
||||||
// First pass: collect tool responses
|
// First pass: collect tool responses
|
||||||
for (const item of items) {
|
for (const item of sortedItems) {
|
||||||
if (item.role === 'tool') {
|
if (item.role === 'tool') {
|
||||||
const toolContent = item.content?.[0]?.text?.value || ''
|
for (const content of item.content ?? []) {
|
||||||
toolResponseMap.set(item.id, {
|
const toolCallId = content.tool_call_id || item.id
|
||||||
error: '',
|
const toolResultText =
|
||||||
content: [
|
content.tool_result?.output_text?.text ||
|
||||||
{
|
(Array.isArray(content.tool_result?.content)
|
||||||
type: 'text',
|
? content.tool_result?.content
|
||||||
text: toolContent
|
?.map((entry) => entry.text || entry.output_text?.text)
|
||||||
}
|
.filter((text): text is string => Boolean(text))
|
||||||
]
|
.join('\n')
|
||||||
})
|
: undefined)
|
||||||
|
const toolContent =
|
||||||
|
content.text?.text ||
|
||||||
|
content.text?.value ||
|
||||||
|
content.output_text?.text ||
|
||||||
|
content.input_text ||
|
||||||
|
content.text_result ||
|
||||||
|
toolResultText ||
|
||||||
|
''
|
||||||
|
toolResponseMap.set(toolCallId, {
|
||||||
|
error: '',
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: 'text',
|
||||||
|
text: toolContent,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Second pass: build messages and merge tool responses
|
// Second pass: build messages and merge tool responses
|
||||||
for (const item of items) {
|
for (const item of sortedItems) {
|
||||||
// Skip tool messages as they will be merged into assistant messages
|
// Skip tool messages as they will be merged into assistant messages
|
||||||
if (item.role === 'tool') {
|
if (item.role === 'tool') {
|
||||||
continue
|
continue
|
||||||
@ -228,14 +250,35 @@ export const combineConversationItemsToMessages = (
|
|||||||
const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
|
const message = ObjectParser.conversationItemToThreadMessage(item, threadId)
|
||||||
|
|
||||||
// If this is an assistant message with tool calls, merge tool responses
|
// If this is an assistant message with tool calls, merge tool responses
|
||||||
if (message.role === 'assistant' && message.metadata?.tool_calls && Array.isArray(message.metadata.tool_calls)) {
|
if (
|
||||||
|
message.role === 'assistant' &&
|
||||||
|
message.metadata?.tool_calls &&
|
||||||
|
Array.isArray(message.metadata.tool_calls)
|
||||||
|
) {
|
||||||
const toolCalls = message.metadata.tool_calls as any[]
|
const toolCalls = message.metadata.tool_calls as any[]
|
||||||
let toolResponseIndex = 0
|
|
||||||
|
|
||||||
for (const [responseId, responseData] of toolResponseMap.entries()) {
|
for (const toolCall of toolCalls) {
|
||||||
if (toolResponseIndex < toolCalls.length) {
|
const callId = toolCall.tool_call_id || toolCall.id || toolCall.tool?.id
|
||||||
toolCalls[toolResponseIndex].response = responseData
|
let responseKey: string | undefined
|
||||||
toolResponseIndex++
|
let response: any = null
|
||||||
|
|
||||||
|
if (callId && toolResponseMap.has(callId)) {
|
||||||
|
responseKey = callId
|
||||||
|
response = toolResponseMap.get(callId)
|
||||||
|
} else {
|
||||||
|
const iterator = toolResponseMap.entries().next()
|
||||||
|
if (!iterator.done) {
|
||||||
|
responseKey = iterator.value[0]
|
||||||
|
response = iterator.value[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
toolCall.response = response
|
||||||
|
toolCall.state = 'succeeded'
|
||||||
|
if (responseKey) {
|
||||||
|
toolResponseMap.delete(responseKey)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -245,3 +288,79 @@ export const combineConversationItemsToMessages = (
|
|||||||
|
|
||||||
return messages
|
return messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const parseTimestamp = (value: number | string | undefined): number => {
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
// Distinguish between seconds and milliseconds
|
||||||
|
return value > 1e12 ? value : value * 1000
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const parsed = Date.parse(value)
|
||||||
|
return Number.isNaN(parsed) ? Date.now() : parsed
|
||||||
|
}
|
||||||
|
return Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractContentByType = (
|
||||||
|
content: ConversationItemContent,
|
||||||
|
handlers: {
|
||||||
|
onText: (value: string) => void
|
||||||
|
onReasoning: (value: string) => void
|
||||||
|
onImage: (url: string) => void
|
||||||
|
onToolCalls: (calls: NonNullable<ConversationItemContent['tool_calls']>) => void
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const type = content.type || ''
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'input_text':
|
||||||
|
handlers.onText(content.input_text || '')
|
||||||
|
break
|
||||||
|
case 'text':
|
||||||
|
handlers.onText(content.text?.text || content.text?.value || '')
|
||||||
|
break
|
||||||
|
case 'output_text':
|
||||||
|
handlers.onText(content.output_text?.text || '')
|
||||||
|
break
|
||||||
|
case 'reasoning_content':
|
||||||
|
handlers.onReasoning(content.reasoning_content || '')
|
||||||
|
break
|
||||||
|
case 'image':
|
||||||
|
case 'image_url':
|
||||||
|
if (content.image?.url) {
|
||||||
|
handlers.onImage(content.image.url)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'tool_calls':
|
||||||
|
if (content.tool_calls && Array.isArray(content.tool_calls)) {
|
||||||
|
handlers.onToolCalls(content.tool_calls)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case 'tool_result':
|
||||||
|
if (content.tool_result?.output_text?.text) {
|
||||||
|
handlers.onText(content.tool_result.output_text.text)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
// Fallback for legacy fields without explicit type
|
||||||
|
if (content.text?.value || content.text?.text) {
|
||||||
|
handlers.onText(content.text.value || content.text.text || '')
|
||||||
|
}
|
||||||
|
if (content.text_result) {
|
||||||
|
handlers.onText(content.text_result)
|
||||||
|
}
|
||||||
|
if (content.output_text?.text) {
|
||||||
|
handlers.onText(content.output_text.text)
|
||||||
|
}
|
||||||
|
if (content.reasoning_content) {
|
||||||
|
handlers.onReasoning(content.reasoning_content)
|
||||||
|
}
|
||||||
|
if (content.image?.url) {
|
||||||
|
handlers.onImage(content.image.url)
|
||||||
|
}
|
||||||
|
if (content.tool_calls && Array.isArray(content.tool_calls)) {
|
||||||
|
handlers.onToolCalls(content.tool_calls)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { ApiError } from '../shared/types/errors'
|
|||||||
import { JAN_API_ROUTES } from './const'
|
import { JAN_API_ROUTES } from './const'
|
||||||
import { JanModel, janProviderStore } from './store'
|
import { JanModel, janProviderStore } from './store'
|
||||||
|
|
||||||
// JAN_API_BASE is defined in vite.config.ts
|
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
const TEMPORARY_CHAT_ID = 'temporary-chat'
|
const TEMPORARY_CHAT_ID = 'temporary-chat'
|
||||||
@ -20,7 +20,7 @@ const TEMPORARY_CHAT_ID = 'temporary-chat'
|
|||||||
*/
|
*/
|
||||||
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
|
function getChatCompletionConfig(request: JanChatCompletionRequest, stream: boolean = false) {
|
||||||
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
|
const isTemporaryChat = request.conversation_id === TEMPORARY_CHAT_ID
|
||||||
const endpoint = `${JAN_API_BASE}${JAN_API_ROUTES.CHAT_COMPLETIONS}`
|
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.CHAT_COMPLETIONS}`
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
...request,
|
...request,
|
||||||
@ -162,7 +162,7 @@ export class JanApiClient {
|
|||||||
|
|
||||||
this.modelsFetchPromise = (async () => {
|
this.modelsFetchPromise = (async () => {
|
||||||
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
|
const response = await this.authService.makeAuthenticatedRequest<JanModelsResponse>(
|
||||||
`${JAN_API_BASE}${JAN_API_ROUTES.MODELS}`
|
`${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODELS}`
|
||||||
)
|
)
|
||||||
|
|
||||||
const summaries = response.data || []
|
const summaries = response.data || []
|
||||||
@ -327,7 +327,7 @@ export class JanApiClient {
|
|||||||
|
|
||||||
private async fetchSupportedParameters(modelId: string): Promise<string[]> {
|
private async fetchSupportedParameters(modelId: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
const endpoint = `${JAN_API_BASE}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}`
|
const endpoint = `${MENLO_PLATFORM_BASE_URL}${JAN_API_ROUTES.MODEL_CATALOGS}/${this.encodeModelIdForCatalog(modelId)}`
|
||||||
const catalog = await this.authService.makeAuthenticatedRequest<JanModelCatalogResponse>(endpoint)
|
const catalog = await this.authService.makeAuthenticatedRequest<JanModelCatalogResponse>(endpoint)
|
||||||
return this.extractSupportedParameters(catalog)
|
return this.extractSupportedParameters(catalog)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import { JanMCPOAuthProvider } from './oauth-provider'
|
|||||||
import { WebSearchButton } from './components'
|
import { WebSearchButton } from './components'
|
||||||
import type { ComponentType } from 'react'
|
import type { ComponentType } from 'react'
|
||||||
|
|
||||||
// JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1')
|
// MENLO_PLATFORM_BASE_URL is defined in vite.config.ts (defaults to 'https://api-dev.menlo.ai/jan/v1')
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export default class MCPExtensionWeb extends MCPExtension {
|
export default class MCPExtensionWeb extends MCPExtension {
|
||||||
private mcpEndpoint = '/mcp'
|
private mcpEndpoint = '/mcp'
|
||||||
@ -77,7 +77,7 @@ export default class MCPExtensionWeb extends MCPExtension {
|
|||||||
|
|
||||||
// Create transport with OAuth provider (handles token refresh automatically)
|
// Create transport with OAuth provider (handles token refresh automatically)
|
||||||
const transport = new StreamableHTTPClientTransport(
|
const transport = new StreamableHTTPClientTransport(
|
||||||
new URL(`${JAN_API_BASE}${this.mcpEndpoint}`),
|
new URL(`${MENLO_PLATFORM_BASE_URL}${this.mcpEndpoint}`),
|
||||||
{
|
{
|
||||||
authProvider: this.oauthProvider
|
authProvider: this.oauthProvider
|
||||||
// No sessionId needed - server will generate one automatically
|
// No sessionId needed - server will generate one automatically
|
||||||
|
|||||||
@ -6,13 +6,13 @@
|
|||||||
import { AuthTokens } from './types'
|
import { AuthTokens } from './types'
|
||||||
import { AUTH_ENDPOINTS } from './const'
|
import { AUTH_ENDPOINTS } from './const'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user on server
|
* Logout user on server
|
||||||
*/
|
*/
|
||||||
export async function logoutUser(): Promise<void> {
|
export async function logoutUser(): Promise<void> {
|
||||||
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.LOGOUT}`, {
|
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.LOGOUT}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -29,7 +29,7 @@ export async function logoutUser(): Promise<void> {
|
|||||||
* Guest login
|
* Guest login
|
||||||
*/
|
*/
|
||||||
export async function guestLogin(): Promise<AuthTokens> {
|
export async function guestLogin(): Promise<AuthTokens> {
|
||||||
const response = await fetch(`${JAN_API_BASE}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
|
const response = await fetch(`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.GUEST_LOGIN}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -51,7 +51,7 @@ export async function guestLogin(): Promise<AuthTokens> {
|
|||||||
*/
|
*/
|
||||||
export async function refreshToken(): Promise<AuthTokens> {
|
export async function refreshToken(): Promise<AuthTokens> {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${JAN_API_BASE}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
|
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.REFRESH_TOKEN}`,
|
||||||
{
|
{
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
|
|||||||
@ -5,10 +5,10 @@
|
|||||||
|
|
||||||
import { AuthTokens, LoginUrlResponse } from './types'
|
import { AuthTokens, LoginUrlResponse } from './types'
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
|
export async function getLoginUrl(endpoint: string): Promise<LoginUrlResponse> {
|
||||||
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
|
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
@ -30,7 +30,7 @@ export async function handleOAuthCallback(
|
|||||||
code: string,
|
code: string,
|
||||||
state?: string
|
state?: string
|
||||||
): Promise<AuthTokens> {
|
): Promise<AuthTokens> {
|
||||||
const response: Response = await fetch(`${JAN_API_BASE}${endpoint}`, {
|
const response: Response = await fetch(`${MENLO_PLATFORM_BASE_URL}${endpoint}`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
* Handles authentication flows for any OAuth provider
|
* Handles authentication flows for any OAuth provider
|
||||||
*/
|
*/
|
||||||
|
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
|
|
||||||
import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types'
|
import { User, AuthState, AuthBroadcastMessage, AuthTokens } from './types'
|
||||||
import {
|
import {
|
||||||
@ -429,7 +429,7 @@ export class JanAuthService {
|
|||||||
private async fetchUserProfile(): Promise<User | null> {
|
private async fetchUserProfile(): Promise<User | null> {
|
||||||
try {
|
try {
|
||||||
return await this.makeAuthenticatedRequest<User>(
|
return await this.makeAuthenticatedRequest<User>(
|
||||||
`${JAN_API_BASE}${AUTH_ENDPOINTS.ME}`
|
`${MENLO_PLATFORM_BASE_URL}${AUTH_ENDPOINTS.ME}`
|
||||||
)
|
)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch user profile:', error)
|
console.error('Failed to fetch user profile:', error)
|
||||||
|
|||||||
2
extensions-web/src/types/global.d.ts
vendored
2
extensions-web/src/types/global.d.ts
vendored
@ -1,5 +1,5 @@
|
|||||||
export {}
|
export {}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
declare const JAN_API_BASE: string
|
declare const MENLO_PLATFORM_BASE_URL: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,6 @@ export default defineConfig({
|
|||||||
emptyOutDir: false // Don't clean the output directory
|
emptyOutDir: false // Don't clean the output directory
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'),
|
MENLO_PLATFORM_BASE_URL: JSON.stringify(process.env.MENLO_PLATFORM_BASE_URL || 'https://api-dev.menlo.ai/v1'),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user