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> {
|
export async function request<T>(path: string, opts: RequestOptions = {}): Promise<T> {
|
||||||
|
const isForm = opts.body instanceof FormData
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'X-Client-Request-ID': crypto.randomUUID(),
|
'X-Client-Request-ID': crypto.randomUUID(),
|
||||||
}
|
}
|
||||||
|
if (!isForm) headers['Content-Type'] = 'application/json'
|
||||||
const token = authTokenProvider()
|
const token = authTokenProvider()
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`
|
if (token) headers['Authorization'] = `Bearer ${token}`
|
||||||
|
|
||||||
@@ -39,10 +40,16 @@ export async function request<T>(path: string, opts: RequestOptions = {}): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const body =
|
||||||
|
opts.body === undefined
|
||||||
|
? undefined
|
||||||
|
: isForm
|
||||||
|
? (opts.body as FormData)
|
||||||
|
: JSON.stringify(opts.body)
|
||||||
const resp = await fetch(BASE + path, {
|
const resp = await fetch(BASE + path, {
|
||||||
method: opts.method || 'GET',
|
method: opts.method || 'GET',
|
||||||
headers,
|
headers,
|
||||||
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
body,
|
||||||
signal: controller.signal,
|
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