276 lines
9.5 KiB
TypeScript
276 lines
9.5 KiB
TypeScript
/**
|
|
* Lite Mode API Client - Binary-Free Mode
|
|
*
|
|
* This provides a client for working with NomadArch in Binary-Free Mode,
|
|
* using native session management instead of the OpenCode binary.
|
|
*/
|
|
|
|
import { CODENOMAD_API_BASE } from "./api-client"
|
|
import { getLogger } from "./logger"
|
|
|
|
const log = getLogger("lite-mode")
|
|
|
|
export interface ModeInfo {
|
|
mode: "lite" | "full"
|
|
binaryFreeMode: boolean
|
|
nativeSessions: boolean
|
|
opencodeBinaryAvailable: boolean
|
|
providers: {
|
|
qwen: boolean
|
|
zai: boolean
|
|
zen: boolean
|
|
}
|
|
}
|
|
|
|
export interface NativeSession {
|
|
id: string
|
|
workspaceId: string
|
|
title?: string
|
|
parentId?: string | null
|
|
createdAt: number
|
|
updatedAt: number
|
|
messageIds: string[]
|
|
model?: {
|
|
providerId: string
|
|
modelId: string
|
|
}
|
|
agent?: string
|
|
}
|
|
|
|
export interface NativeMessage {
|
|
id: string
|
|
sessionId: string
|
|
role: "user" | "assistant" | "system" | "tool"
|
|
content?: string
|
|
createdAt: number
|
|
updatedAt: number
|
|
status?: "pending" | "streaming" | "completed" | "error"
|
|
}
|
|
|
|
let modeCache: ModeInfo | null = null
|
|
|
|
/**
|
|
* Get the current running mode (lite or full)
|
|
*/
|
|
export async function getMode(): Promise<ModeInfo> {
|
|
if (modeCache) return modeCache
|
|
|
|
try {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/meta/mode`)
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch mode: ${response.status}`)
|
|
}
|
|
modeCache = await response.json()
|
|
log.info(`Running in ${modeCache?.mode} mode`, { binaryFree: modeCache?.binaryFreeMode })
|
|
return modeCache!
|
|
} catch (error) {
|
|
log.warn("Failed to fetch mode, assuming lite mode", error)
|
|
// Default to lite mode if we can't determine
|
|
return {
|
|
mode: "lite",
|
|
binaryFreeMode: true,
|
|
nativeSessions: true,
|
|
opencodeBinaryAvailable: false,
|
|
providers: { qwen: true, zai: true, zen: true }
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if running in Binary-Free (lite) mode
|
|
*/
|
|
export async function isLiteMode(): Promise<boolean> {
|
|
const mode = await getMode()
|
|
return mode.binaryFreeMode
|
|
}
|
|
|
|
/**
|
|
* Native Session API for Binary-Free Mode
|
|
*/
|
|
export const nativeSessionApi = {
|
|
async listSessions(workspaceId: string): Promise<NativeSession[]> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`)
|
|
if (!response.ok) throw new Error("Failed to list sessions")
|
|
const data = await response.json()
|
|
return data.sessions
|
|
},
|
|
|
|
async createSession(workspaceId: string, options?: {
|
|
title?: string
|
|
parentId?: string
|
|
model?: { providerId: string; modelId: string }
|
|
agent?: string
|
|
}): Promise<NativeSession> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(options ?? {})
|
|
})
|
|
if (!response.ok) throw new Error("Failed to create session")
|
|
const data = await response.json()
|
|
return data.session
|
|
},
|
|
|
|
async getSession(workspaceId: string, sessionId: string): Promise<NativeSession | null> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`)
|
|
if (response.status === 404) return null
|
|
if (!response.ok) throw new Error("Failed to get session")
|
|
const data = await response.json()
|
|
return data.session
|
|
},
|
|
|
|
async updateSession(workspaceId: string, sessionId: string, updates: Partial<NativeSession>): Promise<NativeSession | null> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
|
|
method: "PATCH",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(updates)
|
|
})
|
|
if (response.status === 404) return null
|
|
if (!response.ok) throw new Error("Failed to update session")
|
|
const data = await response.json()
|
|
return data.session
|
|
},
|
|
|
|
async deleteSession(workspaceId: string, sessionId: string): Promise<boolean> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}`, {
|
|
method: "DELETE"
|
|
})
|
|
return response.ok || response.status === 204
|
|
},
|
|
|
|
async forkSession(workspaceId: string, sessionId: string): Promise<NativeSession> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/fork`, {
|
|
method: "POST"
|
|
})
|
|
if (!response.ok) throw new Error("Failed to fork session")
|
|
const data = await response.json()
|
|
return data.session
|
|
},
|
|
|
|
async revertSession(workspaceId: string, sessionId: string, messageId?: string): Promise<NativeSession> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/revert`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ messageId })
|
|
})
|
|
if (!response.ok) throw new Error("Failed to revert session")
|
|
const data = await response.json()
|
|
return data.session
|
|
},
|
|
|
|
async getMessages(workspaceId: string, sessionId: string): Promise<NativeMessage[]> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/messages`)
|
|
if (!response.ok) throw new Error("Failed to get messages")
|
|
const data = await response.json()
|
|
return data.messages
|
|
},
|
|
|
|
/**
|
|
* Import sessions from SDK mode to Native mode
|
|
*/
|
|
async importSessions(workspaceId: string, sessions: Array<{
|
|
id: string
|
|
title?: string
|
|
parentId?: string | null
|
|
createdAt?: number
|
|
updatedAt?: number
|
|
model?: { providerId: string; modelId: string }
|
|
agent?: string
|
|
messages?: Array<{
|
|
id: string
|
|
role: "user" | "assistant" | "system" | "tool"
|
|
content?: string
|
|
createdAt?: number
|
|
}>
|
|
}>): Promise<{ success: boolean; imported: number; skipped: number }> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/import`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ sessions })
|
|
})
|
|
if (!response.ok) throw new Error("Failed to import sessions")
|
|
return response.json()
|
|
},
|
|
|
|
|
|
/**
|
|
* Send a prompt to the session and get a streaming response
|
|
*/
|
|
async* streamPrompt(
|
|
workspaceId: string,
|
|
sessionId: string,
|
|
content: string,
|
|
options?: {
|
|
provider?: "qwen" | "zai" | "zen"
|
|
accessToken?: string
|
|
resourceUrl?: string
|
|
enableTools?: boolean
|
|
}
|
|
): AsyncGenerator<{ type: "content" | "done" | "error"; data?: string }> {
|
|
const response = await fetch(`${CODENOMAD_API_BASE}/api/native/workspaces/${encodeURIComponent(workspaceId)}/sessions/${encodeURIComponent(sessionId)}/prompt`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
content,
|
|
provider: options?.provider ?? "qwen",
|
|
accessToken: options?.accessToken,
|
|
resourceUrl: options?.resourceUrl,
|
|
enableTools: options?.enableTools ?? true
|
|
})
|
|
})
|
|
|
|
if (!response.ok) {
|
|
yield { type: "error", data: `Request failed: ${response.status}` }
|
|
return
|
|
}
|
|
|
|
const reader = response.body?.getReader()
|
|
if (!reader) {
|
|
yield { type: "error", data: "No response body" }
|
|
return
|
|
}
|
|
|
|
const decoder = new TextDecoder()
|
|
let buffer = ""
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split("\n")
|
|
buffer = lines.pop() ?? ""
|
|
|
|
for (const line of lines) {
|
|
if (!line.trim()) continue
|
|
if (line.startsWith("data: ")) {
|
|
const data = line.slice(6)
|
|
if (data === "[DONE]") {
|
|
yield { type: "done" }
|
|
return
|
|
}
|
|
try {
|
|
const parsed = JSON.parse(data)
|
|
if (parsed.error) {
|
|
yield { type: "error", data: parsed.error }
|
|
} else if (parsed.choices?.[0]?.delta?.content) {
|
|
yield { type: "content", data: parsed.choices[0].delta.content }
|
|
}
|
|
} catch {
|
|
// Skip invalid JSON
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
yield { type: "done" }
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clear mode cache (for testing or after config changes)
|
|
*/
|
|
export function clearModeCache(): void {
|
|
modeCache = null
|
|
}
|