Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions frontend/src/api/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,41 @@ export function testChat(model: string, content: string, accountId?: string) {
})
}


export type RawApiFormat = 'chat' | 'messages' | 'responses'

/** Build a minimal non-streaming body for the selected public API format. */
export function buildRawPayload(format: RawApiFormat, model: string, content: string) {
const text = content || '只回复OK'
if (format === 'responses') {
return { model, input: text, stream: false }
}
if (format === 'messages') {
return {
model,
max_tokens: 1024,
stream: false,
messages: [{ role: 'user', content: text }],
}
}
return {
model,
stream: false,
messages: [{ role: 'user', content: text }],
}
}

/** POST to a public /v1 route (chat completions, messages, or responses). */
export function testRawApi(path: string, body: unknown, accountId?: string) {
const headers: Record<string, string> = {}
if (accountId) headers['X-Qoder-Account'] = accountId
return api(path, {
method: 'POST',
headers,
body: JSON.stringify(body),
})
}

export type StreamChatContentPart =
| { type: 'text'; text: string }
| { type: 'image_url'; image_url: { url: string } }
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/access/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export type ComposerOption = {
icon?: ReactNode
}

function CompactSelect({
export function CompactSelect({
ariaLabel,
value,
onChange,
Expand Down Expand Up @@ -174,7 +174,7 @@ export function ChatComposer({

return (
<div className="p-3 sm:p-4">
<div className="overflow-hidden rounded-2xl bg-surface-secondary/80">
<div className="card overflow-hidden rounded-2xl bg-surface">
{attachments.length ? (
<div className="flex flex-wrap gap-2 px-3 pt-3">
{attachments.map((file) => (
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/access/ChatHistoryRail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ChatHistoryRail({

if (collapsed) {
return (
<div className="flex w-10 shrink-0 flex-col items-center gap-2 border-r border-separator/40 py-3">
<div className="flex w-10 shrink-0 flex-col items-center gap-2 border-r border-separator/70 bg-surface-secondary py-3">
<Button
isIconOnly
size="sm"
Expand Down Expand Up @@ -69,7 +69,7 @@ export function ChatHistoryRail({
return (
<aside
ref={railRef}
className="flex w-[13.5rem] shrink-0 flex-col border-r border-separator/40 bg-surface-secondary/30"
className="flex w-[13.5rem] shrink-0 flex-col border-r border-separator/70 bg-surface-secondary"
aria-label={t('chatHistoryTitle')}
>
<div className="flex items-center gap-1 px-2 py-2.5">
Expand Down
75 changes: 67 additions & 8 deletions frontend/src/components/access/RawApiPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { CopyIcon } from '@/components/motion/CopyIcon'
import { EmptyPanel } from '@/components/ui/EmptyPanel'
import { FilterToggle } from '@/components/ui/FilterToggle'
import { PageAlert } from '@/components/ui/PageAlert'
import { CompactSelect, type ComposerOption } from '@/components/access/ChatComposer'
import type { RawApiFormat } from '@/api/overview'
import { Button, Chip, Skeleton, TextArea } from '@heroui/react'
import { BracketsCurly, CheckCircle, Clock, PaperPlaneTilt } from '@phosphor-icons/react'
import { useI18n } from '@/hooks/useI18n'

type RequestState = 'idle' | 'loading' | 'success' | 'error'

export function RawApiPanel({
chatPath,
format,
onFormatChange,
requestPath,
prompt,
onPromptChange,
onTest,
Expand All @@ -20,8 +25,18 @@ export function RawApiPanel({
copiedCurl,
onCopyCurl,
canSend,
accountValue,
onAccountChange,
accountOptions,
modelValue,
onModelChange,
modelOptions,
modelsLoading,
disabled,
}: {
chatPath: string
format: RawApiFormat
onFormatChange: (value: RawApiFormat) => void
requestPath: string
prompt: string
onPromptChange: (value: string) => void
onTest: () => void
Expand All @@ -33,22 +48,65 @@ export function RawApiPanel({
copiedCurl: boolean
onCopyCurl: () => void
canSend: boolean
accountValue: string
onAccountChange: (value: string) => void
accountOptions: ComposerOption[]
modelValue: string
onModelChange: (value: string) => void
modelOptions: ComposerOption[]
modelsLoading?: boolean
disabled?: boolean
}) {
const { t } = useI18n()

return (
<div className="space-y-4">
<p className="px-1 text-caption leading-5 text-muted">{t('nativeApiHint')}</p>
<p className="px-1 text-caption leading-5 text-muted">{t('rawFormatHint')}</p>

<div className="overflow-hidden rounded-3xl bg-surface shadow-surface">
<div className="card overflow-hidden rounded-3xl bg-surface">
<div className="grid xl:grid-cols-[minmax(440px,.92fr)_minmax(0,1.08fr)]">
<div className="xl:border-r xl:border-separator/50">
<div className="px-5 py-4 sm:px-6">
<h3 className="font-semibold">{t('requestBuilder')}</h3>
<p className="mt-0.5 text-caption text-muted">POST {chatPath}</p>
<p className="mt-0.5 text-caption text-muted">POST {requestPath}</p>
</div>

<div className="space-y-5 px-5 pb-5 sm:px-6 sm:pb-6">
<div className="space-y-2">
<div className="text-body font-medium text-muted">{t('rawFormatLabel')}</div>
<FilterToggle
ariaLabel={t('rawFormatLabel')}
value={format}
onChange={(value) => onFormatChange(value as RawApiFormat)}
options={[
{ id: 'chat', label: t('rawFormatChat') },
{ id: 'messages', label: t('rawFormatMessages') },
{ id: 'responses', label: t('rawFormatResponses') },
]}
/>
</div>

<div className="flex flex-wrap items-center gap-2">
<CompactSelect
ariaLabel={t('account')}
value={accountValue}
onChange={onAccountChange}
options={accountOptions}
isDisabled={disabled || requestState === 'loading'}
tone="muted"
className="w-auto max-w-[9rem] min-w-0"
/>
<CompactSelect
ariaLabel={t('model')}
value={modelValue}
onChange={onModelChange}
placeholder={modelsLoading ? t('requesting') : t('model')}
options={modelOptions}
isDisabled={disabled || requestState === 'loading' || modelsLoading || !modelOptions.length}
className="w-auto max-w-[12rem] min-w-0"
/>
</div>

<div className="space-y-3">
<div className="flex items-center justify-between gap-4">
<div className="text-body font-medium text-muted">{t('prompt')}</div>
Expand All @@ -62,14 +120,15 @@ export function RawApiPanel({
onChange={(event) => onPromptChange(event.target.value)}
placeholder={t('promptPlaceholder')}
aria-label={t('prompt')}
disabled={disabled || requestState === 'loading'}
/>
<p className="text-caption leading-5 text-muted">{t('promptHelper')}</p>
</div>

<Button
fullWidth
isPending={requestState === 'loading'}
isDisabled={!canSend}
isDisabled={!canSend || disabled}
onPress={onTest}
>
<PaperPlaneTilt size={16} />
Expand All @@ -79,7 +138,7 @@ export function RawApiPanel({
</div>

<div
className="flex min-h-[480px] min-w-0 flex-col bg-surface-secondary/40"
className="flex min-h-[480px] min-w-0 flex-col bg-surface-secondary"
aria-live="polite"
aria-busy={requestState === 'loading'}
>
Expand Down Expand Up @@ -142,7 +201,7 @@ export function RawApiPanel({
</div>
</div>

<section className="overflow-hidden rounded-3xl bg-surface shadow-surface">
<section className="card overflow-hidden rounded-3xl bg-surface">
<div className="flex items-center justify-between gap-4 px-5 py-3.5">
<div className="min-w-0">
<div className="text-body font-semibold">{t('curlExample')}</div>
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/i18n/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,11 @@ export const messages: Record<Lang, Dict> = {
chatHistoryExpand: 'Show history',
chatHistoryCollapse: 'Hide history',
nativeApiHint: 'Non-streaming request, raw JSON response, and a matching curl command.',
rawFormatLabel: 'API format',
rawFormatChat: 'Chat',
rawFormatMessages: 'Messages',
rawFormatResponses: 'Responses',
rawFormatHint: 'Chat Completions, Anthropic Messages, and OpenAI Responses are all supported.',
rawApiShow: 'Show raw API',
rawApiHide: 'Hide raw API',
chatAttach: 'Attach file',
Expand Down Expand Up @@ -1398,6 +1403,11 @@ export const messages: Record<Lang, Dict> = {
chatHistoryExpand: '展开历史',
chatHistoryCollapse: '收起历史',
nativeApiHint: '非流式请求、原始 JSON 响应,以及对应的 curl 命令。',
rawFormatLabel: 'API 格式',
rawFormatChat: 'Chat',
rawFormatMessages: 'Messages',
rawFormatResponses: 'Responses',
rawFormatHint: '支持 Chat Completions、Anthropic Messages 与 OpenAI Responses 三种格式。',
rawApiShow: '展开原始 API',
rawApiHide: '收起原始 API',
chatAttach: '添加附件',
Expand Down
41 changes: 29 additions & 12 deletions frontend/src/pages/AccessPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { Button, Chip, Tab, Tabs } from '@heroui/react'
import { ArrowsClockwise } from '@phosphor-icons/react'
import { useI18n } from '@/hooks/useI18n'
import { useOverview } from '@/hooks/useOverview'
import { fetchAccounts, fetchModels, streamChat, testChat } from '@/api/overview'
import { buildRawPayload, fetchAccounts, fetchModels, streamChat, testRawApi, type RawApiFormat } from '@/api/overview'
import type { ModelInfo, Overview } from '@/api/types'
import type { StreamToolCall } from '@/api/overview'
import { absUrl } from '@/lib/url'
Expand Down Expand Up @@ -97,6 +97,7 @@ export function AccessPage() {
const { t } = useI18n()
const { overview, loading } = useOverview()
const [accessTab, setAccessTab] = useState<AccessTab>('chat')
const [rawFormat, setRawFormat] = useState<RawApiFormat>('chat')
const [historyCollapsed, setHistoryCollapsed] = useState(true)
const [sessions, setSessions] = useState<ChatSession[]>(() => [createSession(initialDemoMessages())])
const [activeSessionId, setActiveSessionId] = useState('')
Expand All @@ -116,7 +117,14 @@ export function AccessPage() {
}, [])
const base = absUrl(overview?.access?.openai_base_url || '/v1')
const chatPath = overview?.access?.chat_completions || '/v1/chat/completions'
const chatEndpoint = absUrl(chatPath)
const messagesPath = overview?.access?.messages || '/v1/messages'
const responsesPath = overview?.access?.responses || '/v1/responses'
const rawPath = rawFormat === 'messages'
? messagesPath
: rawFormat === 'responses'
? responsesPath
: chatPath
const rawEndpoint = absUrl(rawPath)
const [model, setModel] = useState('')
const [accountId, setAccountId] = useState('')
const [accountCatalog, setAccountCatalog] = useState<{ accountId: string; models: ModelInfo[]; error: string } | null>(null)
Expand Down Expand Up @@ -170,19 +178,18 @@ export function AccessPage() {
account.ready === true || account.hot === true || account.status === 'ready' || account.status === 'hot'
))

const payload = useMemo(() => ({
model: selectedModel,
stream: false,
messages: [{ role: 'user', content: prompt || '只回复OK' }],
}), [prompt, selectedModel])
const payload = useMemo(
() => buildRawPayload(rawFormat, selectedModel, prompt),
[prompt, rawFormat, selectedModel],
)

const curl = useMemo(
() => `curl -sS ${shellQuote(chatEndpoint)} \\
() => `curl -sS ${shellQuote(rawEndpoint)} \\
-H "Authorization: Bearer $CLI2API_API_KEY" \\
-H ${shellQuote('Content-Type: application/json')}${selectedAccount ? ` \\
-H ${shellQuote(`X-Qoder-Account: ${selectedAccount}`)}` : ''} \\
-d ${shellQuote(JSON.stringify(payload))}`,
[chatEndpoint, payload, selectedAccount],
[payload, rawEndpoint, selectedAccount],
)

if (loading && !overview) return <AccessPageSkeleton />
Expand All @@ -199,7 +206,7 @@ export function AccessPage() {
setElapsedMs(null)
setOutput('')
try {
const data = await testChat(payload.model, payload.messages[0].content, selectedAccount || undefined)
const data = await testRawApi(rawPath, payload, selectedAccount || undefined)
setOutput(JSON.stringify(data, null, 2))
setRequestState('success')
} catch (error) {
Expand Down Expand Up @@ -468,7 +475,7 @@ export function AccessPage() {
</Tabs.ListContainer>

<Tabs.Panel id="chat" className="pt-5">
<section className="overflow-hidden rounded-3xl bg-surface shadow-surface">
<section className="card overflow-hidden rounded-3xl bg-surface">
<div className="flex min-h-[640px] md:min-h-[72vh]">
<ChatHistoryRail
collapsed={historyCollapsed}
Expand Down Expand Up @@ -509,7 +516,9 @@ export function AccessPage() {

<Tabs.Panel id="raw" className="space-y-5 pt-5">
<RawApiPanel
chatPath={chatPath}
format={rawFormat}
onFormatChange={setRawFormat}
requestPath={rawPath}
prompt={prompt}
onPromptChange={setPrompt}
onTest={() => void onTest()}
Expand All @@ -521,6 +530,14 @@ export function AccessPage() {
copiedCurl={copied === 'curl'}
onCopyCurl={() => void copy(curl, 'curl')}
canSend={Boolean(prompt.trim() && selectedModel && !modelsLoading)}
accountValue={selectedAccount || 'auto'}
onAccountChange={(next) => setAccountId(next === 'auto' ? '' : next)}
accountOptions={accountOptions}
modelValue={selectedModel}
onModelChange={setModel}
modelOptions={modelOptions}
modelsLoading={modelsLoading}
disabled={!accounts.length && demoMode() == null}
/>
<EndpointList access={overview?.access} />
</Tabs.Panel>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

Loading
Loading