You've already forked agentic-coding-workflow
feat(web): chat + llmAdmin api clients with FormData support
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
import { request } from './client'
|
||||
|
||||
export type MessageRole = 'user' | 'assistant'
|
||||
export type MessageStatus = 'pending' | 'ok' | 'error' | 'cancelled'
|
||||
export type TemplateScope = 'system' | 'user'
|
||||
|
||||
export interface Conversation {
|
||||
id: string
|
||||
user_id: string
|
||||
project_id?: string | null
|
||||
workspace_id?: string | null
|
||||
issue_id?: string | null
|
||||
model_id: string
|
||||
system_prompt: string
|
||||
title?: string | null
|
||||
title_generated_at?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Attachment {
|
||||
id: string
|
||||
filename: string
|
||||
mime_type: string
|
||||
size_bytes: number
|
||||
sha256: string
|
||||
}
|
||||
|
||||
export interface ChatMessage {
|
||||
id: number
|
||||
conversation_id: string
|
||||
role: MessageRole
|
||||
content: string
|
||||
thinking?: string | null
|
||||
status: MessageStatus
|
||||
error_message?: string | null
|
||||
prompt_tokens?: number | null
|
||||
completion_tokens?: number | null
|
||||
thinking_tokens?: number | null
|
||||
attachments?: Attachment[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface PromptTemplate {
|
||||
id: string
|
||||
name: string
|
||||
content: string
|
||||
scope: TemplateScope
|
||||
owner_id?: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ConversationDetail {
|
||||
conversation: Conversation
|
||||
messages: ChatMessage[]
|
||||
}
|
||||
|
||||
export interface SendMessageResp {
|
||||
user_message_id: number
|
||||
assistant_message_id: number
|
||||
stream_url: string
|
||||
}
|
||||
|
||||
export interface RetryMessageResp {
|
||||
assistant_message_id: number
|
||||
stream_url: string
|
||||
}
|
||||
|
||||
export interface ListConversationsParams {
|
||||
project_id?: string
|
||||
workspace_id?: string
|
||||
issue_id?: string
|
||||
q?: string
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export interface CreateConversationBody {
|
||||
model_id: string
|
||||
template_id?: string
|
||||
system_prompt?: string
|
||||
project_id?: string
|
||||
workspace_id?: string
|
||||
issue_id?: string
|
||||
}
|
||||
|
||||
export interface UsageDailyItem {
|
||||
day: string
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
thinking_tokens: number
|
||||
cost_usd: number
|
||||
}
|
||||
|
||||
const enc = (v: string) => encodeURIComponent(v)
|
||||
|
||||
function buildQuery(params: object): string {
|
||||
const q = new URLSearchParams()
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v === undefined || v === null || v === '') continue
|
||||
q.set(k, String(v))
|
||||
}
|
||||
const s = q.toString()
|
||||
return s ? '?' + s : ''
|
||||
}
|
||||
|
||||
export const chatApi = {
|
||||
// ===== Conversations =====
|
||||
listConversations: (params: ListConversationsParams = {}) =>
|
||||
request<{ items: Conversation[] }>(`/api/v1/conversations${buildQuery(params)}`).then(
|
||||
(r) => r.items,
|
||||
),
|
||||
createConversation: (body: CreateConversationBody) =>
|
||||
request<Conversation>('/api/v1/conversations', { method: 'POST', body }),
|
||||
getConversation: (id: string, opts: { before?: number; limit?: number } = {}) =>
|
||||
request<ConversationDetail>(
|
||||
`/api/v1/conversations/${enc(id)}${buildQuery({
|
||||
before: opts.before,
|
||||
limit: opts.limit ?? 50,
|
||||
})}`,
|
||||
),
|
||||
updateConversationTitle: (id: string, title: string) =>
|
||||
request<void>(`/api/v1/conversations/${enc(id)}`, {
|
||||
method: 'PATCH',
|
||||
body: { title },
|
||||
}),
|
||||
deleteConversation: (id: string) =>
|
||||
request<void>(`/api/v1/conversations/${enc(id)}`, { method: 'DELETE' }),
|
||||
|
||||
// ===== Messages =====
|
||||
sendMessage: (
|
||||
conversationId: string,
|
||||
body: { content: string; attachment_ids?: string[] },
|
||||
) =>
|
||||
request<SendMessageResp>(`/api/v1/conversations/${enc(conversationId)}/messages`, {
|
||||
method: 'POST',
|
||||
body,
|
||||
}),
|
||||
cancelMessage: (messageId: number) =>
|
||||
request<void>(`/api/v1/messages/${messageId}/cancel`, { method: 'POST' }),
|
||||
retryMessage: (messageId: number) =>
|
||||
request<RetryMessageResp>(`/api/v1/messages/${messageId}/retry`, { method: 'POST' }),
|
||||
|
||||
// ===== Uploads =====
|
||||
uploadAttachment: (file: File) => {
|
||||
const fd = new FormData()
|
||||
fd.append('file', file)
|
||||
return request<Attachment>('/api/v1/uploads', { method: 'POST', body: fd })
|
||||
},
|
||||
|
||||
// ===== Templates =====
|
||||
listTemplates: () =>
|
||||
request<{ items: PromptTemplate[] }>('/api/v1/prompt-templates').then((r) => r.items),
|
||||
createTemplate: (body: { name: string; content: string; scope: TemplateScope }) =>
|
||||
request<PromptTemplate>('/api/v1/prompt-templates', { method: 'POST', body }),
|
||||
updateTemplate: (id: string, body: { name?: string; content?: string }) =>
|
||||
request<void>(`/api/v1/prompt-templates/${enc(id)}`, { method: 'PATCH', body }),
|
||||
deleteTemplate: (id: string) =>
|
||||
request<void>(`/api/v1/prompt-templates/${enc(id)}`, { method: 'DELETE' }),
|
||||
|
||||
// ===== Personal usage =====
|
||||
myDailyUsage: (from: string, to: string) =>
|
||||
request<{ items: UsageDailyItem[] }>(
|
||||
`/api/v1/users/me/usage${buildQuery({ from, to })}`,
|
||||
).then((r) => r.items),
|
||||
}
|
||||
@@ -21,10 +21,11 @@ export function configure(opts: {
|
||||
}
|
||||
|
||||
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||||
const isForm = opts.body instanceof FormData
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Client-Request-ID': crypto.randomUUID(),
|
||||
}
|
||||
if (!isForm) headers['Content-Type'] = 'application/json'
|
||||
const token = authTokenProvider()
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||
|
||||
@@ -39,10 +40,16 @@ export async function request<T>(path: string, opts: RequestOptions = {}): Promi
|
||||
}
|
||||
|
||||
try {
|
||||
const body =
|
||||
opts.body === undefined
|
||||
? undefined
|
||||
: isForm
|
||||
? (opts.body as FormData)
|
||||
: JSON.stringify(opts.body)
|
||||
const resp = await fetch(BASE + path, {
|
||||
method: opts.method || 'GET',
|
||||
headers,
|
||||
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
||||
body,
|
||||
signal: controller.signal,
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { request } from './client'
|
||||
|
||||
export type LLMProvider = 'anthropic' | 'openai' | 'gemini'
|
||||
|
||||
export interface LLMEndpoint {
|
||||
id: string
|
||||
provider: LLMProvider
|
||||
display_name: string
|
||||
base_url: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface ModelCapabilities {
|
||||
image: boolean
|
||||
pdf: boolean
|
||||
audio: boolean
|
||||
reasoning: boolean
|
||||
tools: boolean
|
||||
}
|
||||
|
||||
export interface LLMModel {
|
||||
id: string
|
||||
endpoint_id: string
|
||||
model_id: string
|
||||
display_name: string
|
||||
capabilities: ModelCapabilities
|
||||
context_window: number
|
||||
max_output_tokens: number
|
||||
prompt_price_per_million_usd: number
|
||||
completion_price_per_million_usd: number
|
||||
thinking_price_per_million_usd: number
|
||||
is_title_generator: boolean
|
||||
enabled: boolean
|
||||
sort_order: number
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface CreateEndpointBody {
|
||||
provider: LLMProvider
|
||||
display_name: string
|
||||
base_url: string
|
||||
api_key: string
|
||||
}
|
||||
|
||||
export interface UpdateEndpointBody {
|
||||
display_name?: string
|
||||
base_url?: string
|
||||
api_key?: string
|
||||
}
|
||||
|
||||
export interface CreateModelBody {
|
||||
endpoint_id: string
|
||||
model_id: string
|
||||
display_name: string
|
||||
capabilities?: Partial<ModelCapabilities>
|
||||
context_window: number
|
||||
max_output_tokens: number
|
||||
prompt_price_per_million_usd?: number
|
||||
completion_price_per_million_usd?: number
|
||||
thinking_price_per_million_usd?: number
|
||||
is_title_generator?: boolean
|
||||
enabled?: boolean
|
||||
sort_order?: number
|
||||
}
|
||||
|
||||
export type UpdateModelBody = Partial<Omit<CreateModelBody, 'endpoint_id'>>
|
||||
|
||||
export type AdminUsageGroupBy = 'user' | 'model' | 'day'
|
||||
|
||||
export interface AdminUsageItem {
|
||||
key: string
|
||||
prompt_tokens: number
|
||||
completion_tokens: number
|
||||
thinking_tokens: number
|
||||
cost_usd: number
|
||||
}
|
||||
|
||||
const enc = (v: string) => encodeURIComponent(v)
|
||||
|
||||
function buildQuery(params: object): string {
|
||||
const q = new URLSearchParams()
|
||||
for (const [k, v] of Object.entries(params)) {
|
||||
if (v === undefined || v === null || v === '') continue
|
||||
q.set(k, String(v))
|
||||
}
|
||||
const s = q.toString()
|
||||
return s ? '?' + s : ''
|
||||
}
|
||||
|
||||
export const llmAdminApi = {
|
||||
// ===== Endpoints =====
|
||||
listEndpoints: () =>
|
||||
request<{ items: LLMEndpoint[] }>('/api/v1/admin/llm-endpoints').then((r) => r.items),
|
||||
createEndpoint: (body: CreateEndpointBody) =>
|
||||
request<LLMEndpoint>('/api/v1/admin/llm-endpoints', { method: 'POST', body }),
|
||||
updateEndpoint: (id: string, body: UpdateEndpointBody) =>
|
||||
request<void>(`/api/v1/admin/llm-endpoints/${enc(id)}`, { method: 'PATCH', body }),
|
||||
deleteEndpoint: (id: string) =>
|
||||
request<void>(`/api/v1/admin/llm-endpoints/${enc(id)}`, { method: 'DELETE' }),
|
||||
testEndpoint: (id: string) =>
|
||||
request<void>(`/api/v1/admin/llm-endpoints/${enc(id)}/test`, { method: 'POST' }),
|
||||
|
||||
// ===== Models =====
|
||||
listModels: (onlyEnabled = false) =>
|
||||
request<{ items: LLMModel[] }>(
|
||||
`/api/v1/admin/llm-models${buildQuery({ only_enabled: onlyEnabled })}`,
|
||||
).then((r) => r.items),
|
||||
createModel: (body: CreateModelBody) =>
|
||||
request<LLMModel>('/api/v1/admin/llm-models', { method: 'POST', body }),
|
||||
updateModel: (id: string, body: UpdateModelBody) =>
|
||||
request<void>(`/api/v1/admin/llm-models/${enc(id)}`, { method: 'PATCH', body }),
|
||||
deleteModel: (id: string) =>
|
||||
request<void>(`/api/v1/admin/llm-models/${enc(id)}`, { method: 'DELETE' }),
|
||||
|
||||
// ===== Usage =====
|
||||
adminUsage: (from: string, to: string, groupBy: AdminUsageGroupBy) =>
|
||||
request<{ items: AdminUsageItem[] }>(
|
||||
`/api/v1/admin/usage${buildQuery({ from, to, group_by: groupBy })}`,
|
||||
),
|
||||
}
|
||||
Reference in New Issue
Block a user