fix: conversation items (#6815)

This commit is contained in:
Dinh Long Nguyen 2025-10-24 09:01:31 +07:00 committed by GitHub
parent e46200868e
commit f07e43cfe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 274 additions and 127 deletions

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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 \

View File

@ -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,

View File

@ -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

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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',

View File

@ -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',

View File

@ -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)

View File

@ -1,5 +1,5 @@
export {} export {}
declare global { declare global {
declare const JAN_API_BASE: string declare const MENLO_PLATFORM_BASE_URL: string
} }

View File

@ -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'),
} }
}) })