feat: 支持 OpenAI Responses API

This commit is contained in:
Zipper_Wang
2026-08-25 17:15:07 +08:00
parent 8e5b9790f6
commit 5ff67edaef
9 changed files with 539 additions and 57 deletions
+391 -52
View File
@@ -24,6 +24,7 @@ interface AIProviderMetadataFile {
type AIMessagePart = { type: 'text'; text: string } | { type: 'image'; dataUrl: string }
type AIMessage = { role: string; content: string | AIMessagePart[] }
type AIChatDeltaHandler = (delta: string) => void
type AIRequestResult = {
data: string
usage?: { input?: number; output?: number; total?: number; estimated?: boolean }
@@ -33,6 +34,28 @@ interface OpenAIResponsePayload {
choices?: Array<{ message?: { content?: string } }>
usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
}
interface OpenAIStreamPayload {
error?: { message?: string }
choices?: Array<{ delta?: { content?: string } }>
usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number }
}
interface OpenAIResponsesPayload {
error?: { message?: string }
incomplete_details?: { reason?: string }
output_text?: string
output?: Array<{
type?: string
content?: Array<{ type?: string; text?: string }>
}>
usage?: { input_tokens?: number; output_tokens?: number; total_tokens?: number }
}
interface OpenAIResponsesStreamPayload {
type?: string
delta?: string
message?: string
error?: { message?: string }
response?: OpenAIResponsesPayload
}
interface AnthropicResponsePayload {
error?: { message?: string }
content?: Array<{ type?: string; text?: string }>
@@ -232,7 +255,8 @@ export class AIProviderService {
async chat(
messages: Array<{ role: string; content: string }>,
options?: AIChatRequestOptions,
signal?: AbortSignal
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<{
success: boolean
data?: string
@@ -240,7 +264,7 @@ export class AIProviderService {
error?: string
}> {
try {
return { success: true, ...(await this.request(messages, options, false, signal)) }
return { success: true, ...(await this.request(messages, options, false, signal, onDelta)) }
} catch (error) {
if (signal?.aborted) throw error
return { success: false, error: safeAIError(error) }
@@ -321,12 +345,13 @@ export class AIProviderService {
messages: AIMessage[],
options?: AIChatRequestOptions,
testing = false,
signal?: AbortSignal
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<{
data: string
usage?: { input?: number; output?: number; total?: number; estimated?: boolean }
}> {
if (options?.apiKey) return this.requestLegacy(messages, options, signal)
if (options?.apiKey) return this.requestLegacy(messages, options, signal, onDelta)
const resolved = this.resolveProvider(options)
const provider = options?.timeoutMs
? {
@@ -334,7 +359,15 @@ export class AIProviderService {
advanced: { ...resolved.provider.advanced, timeoutMs: options.timeoutMs }
}
: resolved.provider
return requestProvider(provider, resolved.key, resolved.model, messages, testing, signal)
return requestProvider(
provider,
resolved.key,
resolved.model,
messages,
testing,
signal,
onDelta
)
}
private resolveProvider(options?: { providerId?: string; modelId?: string }): {
@@ -357,7 +390,8 @@ export class AIProviderService {
private async requestLegacy(
messages: AIMessage[],
options: AIChatRequestOptions,
signal?: AbortSignal
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
const provider = deepSeekProvider(options.baseURL, options.model)
return requestOpenAICompatible(
@@ -366,7 +400,8 @@ export class AIProviderService {
options.model || provider.defaultModel,
messages,
false,
signal
signal,
onDelta
)
}
@@ -540,6 +575,11 @@ function validateProvider(provider: AIProviderConfig): string | undefined {
return '默认模型不在模型列表中'
if (provider.auth.type === 'custom-header' && !provider.auth.headerName?.trim())
return '请填写自定义认证字段'
if (
provider.advanced.apiProtocol &&
!['chat-completions', 'responses'].includes(provider.advanced.apiProtocol)
)
return 'OpenAI API 接口配置不正确'
return undefined
}
@@ -555,17 +595,26 @@ function buildHeaders(provider: AIProviderSummary, apiKey: string): Record<strin
return headers
}
function hasHeader(headers: Record<string, string>, name: string): boolean {
const normalizedName = name.toLowerCase()
return Object.keys(headers).some((header) => header.toLowerCase() === normalizedName)
}
function requestProvider(
provider: AIProviderSummary,
apiKey: string,
model: string,
messages: AIMessage[],
testing = false,
signal?: AbortSignal
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
return provider.type === 'anthropic-messages'
? requestAnthropic(provider, apiKey, model, messages, testing, signal)
: requestOpenAICompatible(provider, apiKey, model, messages, testing, signal)
if (provider.type === 'anthropic-messages') {
return requestAnthropic(provider, apiKey, model, messages, testing, signal)
}
return provider.advanced.apiProtocol === 'responses'
? requestOpenAIResponses(provider, apiKey, model, messages, testing, signal, onDelta)
: requestOpenAICompatible(provider, apiKey, model, messages, testing, signal, onDelta)
}
function toOpenAIMessages(messages: AIMessage[]): Array<{ role: string; content: unknown }> {
@@ -582,6 +631,46 @@ function toOpenAIMessages(messages: AIMessage[]): Array<{ role: string; content:
}))
}
function toOpenAIResponsesRequest(messages: AIMessage[]): {
instructions?: string
input: Array<{ role: string; content: unknown[] }>
} {
const instructions = messages
.filter((message) => message.role === 'system')
.map((message) =>
typeof message.content === 'string'
? message.content
: message.content
.filter((part) => part.type === 'text')
.map((part) => (part.type === 'text' ? part.text : ''))
.join('\n')
)
.filter(Boolean)
.join('\n\n')
const input = messages
.filter((message) => message.role !== 'system')
.map((message) => ({
role: message.role === 'assistant' ? 'assistant' : 'user',
content:
typeof message.content === 'string'
? [
{
type: message.role === 'assistant' ? 'output_text' : 'input_text',
text: message.content
}
]
: message.content.map((part) =>
part.type === 'text'
? {
type: message.role === 'assistant' ? 'output_text' : 'input_text',
text: part.text
}
: { type: 'input_image', image_url: part.dataUrl }
)
}))
return { instructions: instructions || undefined, input }
}
function toAnthropicMessages(messages: AIMessage[]): Array<{ role: string; content: unknown }> {
return messages
.filter((message) => message.role !== 'system')
@@ -607,12 +696,13 @@ async function requestOpenAICompatible(
model: string,
messages: AIMessage[],
testing = false,
signal?: AbortSignal
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
const endpoint = provider.baseUrl.endsWith('/chat/completions')
? provider.baseUrl
: `${provider.baseUrl.replace(/\/+$/, '')}/chat/completions`
const response = await fetchWithTimeout(
return fetchWithTimeout(
endpoint,
{
method: 'POST',
@@ -621,25 +711,83 @@ async function requestOpenAICompatible(
model,
messages: toOpenAIMessages(messages),
temperature: provider.advanced.temperature,
max_tokens: testing ? 8 : provider.advanced.maxTokens
max_tokens: testing ? 8 : provider.advanced.maxTokens,
...(provider.advanced.stream ? { stream: true } : {})
})
},
provider.advanced.timeoutMs,
signal
signal,
async (response) => {
if (!response.ok) {
const payload = await parseJsonResponse<OpenAIResponsePayload>(response)
throw new Error(payload.error?.message || `AI 请求失败 (${response.status})`)
}
if (
provider.advanced.stream &&
!response.headers.get('content-type')?.toLowerCase().includes('application/json')
) {
return parseOpenAIStream(response, onDelta)
}
const payload = await parseJsonResponse<OpenAIResponsePayload>(response)
return {
data: String(payload.choices?.[0]?.message?.content || ''),
usage: toOpenAIUsage(payload.usage)
}
}
)
const payload = await parseJsonResponse<OpenAIResponsePayload>(response)
if (!response.ok) throw new Error(payload.error?.message || `AI 请求失败 (${response.status})`)
return {
data: String(payload.choices?.[0]?.message?.content || ''),
usage: payload.usage
? {
input: payload.usage.prompt_tokens,
output: payload.usage.completion_tokens,
total: payload.usage.total_tokens,
estimated: false
}
: undefined
}
async function requestOpenAIResponses(
provider: AIProviderSummary,
apiKey: string,
model: string,
messages: AIMessage[],
testing = false,
signal?: AbortSignal,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
const normalizedBaseUrl = provider.baseUrl.replace(/\/+$/, '')
const endpoint = normalizedBaseUrl.endsWith('/responses')
? normalizedBaseUrl
: normalizedBaseUrl.endsWith('/chat/completions')
? `${normalizedBaseUrl.slice(0, -'/chat/completions'.length)}/responses`
: `${normalizedBaseUrl}/responses`
const request = toOpenAIResponsesRequest(messages)
const headers = buildHeaders(provider, apiKey)
if (provider.advanced.stream && !hasHeader(headers, 'accept')) {
headers.accept = 'text/event-stream'
}
return fetchWithTimeout(
endpoint,
{
method: 'POST',
headers,
body: JSON.stringify({
model,
instructions: request.instructions,
input: request.input,
temperature: provider.advanced.temperature,
max_output_tokens: testing ? 128 : provider.advanced.maxTokens,
store: false,
...(provider.advanced.stream ? { stream: true } : {})
})
},
provider.advanced.timeoutMs,
signal,
async (response) => {
if (!response.ok) {
const payload = await parseJsonResponse<OpenAIResponsesPayload>(response)
throw new Error(payload.error?.message || `AI 请求失败 (${response.status})`)
}
if (provider.advanced.stream) return parseOpenAIResponsesStream(response, onDelta)
const payload = await parseJsonResponse<OpenAIResponsesPayload>(response)
if (payload.error?.message) throw new Error(payload.error.message)
return {
data: extractOpenAIResponsesText(payload),
usage: toOpenAIResponsesUsage(payload.usage)
}
}
)
}
async function requestAnthropic(
@@ -667,7 +815,7 @@ async function requestAnthropic(
const endpoint = provider.baseUrl.endsWith('/messages')
? provider.baseUrl
: `${provider.baseUrl.replace(/\/+$/, '')}/messages`
const response = await fetchWithTimeout(
return fetchWithTimeout(
endpoint,
{
method: 'POST',
@@ -681,35 +829,39 @@ async function requestAnthropic(
})
},
provider.advanced.timeoutMs,
signal
signal,
async (response) => {
const payload = await parseJsonResponse<AnthropicResponsePayload>(response)
if (!response.ok)
throw new Error(payload.error?.message || `Anthropic 请求失败 (${response.status})`)
return {
data: Array.isArray(payload.content)
? payload.content
.filter((item) => item.type === 'text')
.map((item) => item.text || '')
.join('\n')
: '',
usage: payload.usage
? {
input: payload.usage.input_tokens,
output: payload.usage.output_tokens,
total:
Number(payload.usage.input_tokens || 0) + Number(payload.usage.output_tokens || 0),
estimated: false
}
: undefined
}
}
)
const payload = await parseJsonResponse<AnthropicResponsePayload>(response)
if (!response.ok)
throw new Error(payload.error?.message || `Anthropic 请求失败 (${response.status})`)
return {
data: Array.isArray(payload.content)
? payload.content
.filter((item) => item.type === 'text')
.map((item) => item.text || '')
.join('\n')
: '',
usage: payload.usage
? {
input: payload.usage.input_tokens,
output: payload.usage.output_tokens,
total: Number(payload.usage.input_tokens || 0) + Number(payload.usage.output_tokens || 0),
estimated: false
}
: undefined
}
}
async function fetchWithTimeout(
async function fetchWithTimeout<T>(
url: string,
init: RequestInit,
timeoutMs: number,
signal?: AbortSignal
): Promise<Response> {
signal: AbortSignal | undefined,
consume: (response: Response) => Promise<T>
): Promise<T> {
const controller = new AbortController()
let timedOut = false
const abortFromCaller = (): void =>
@@ -724,7 +876,8 @@ async function fetchWithTimeout(
Math.max(1_000, timeoutMs || 120_000)
)
try {
return await fetch(url, { ...init, signal: controller.signal })
const response = await fetch(url, { ...init, signal: controller.signal })
return await consume(response)
} catch (error) {
if (signal?.aborted) throw new DOMException('AI request cancelled', 'AbortError')
if (timedOut) throw new DOMException('AI request timed out', 'TimeoutError')
@@ -735,6 +888,192 @@ async function fetchWithTimeout(
}
}
async function parseOpenAIStream(
response: Response,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
let data = ''
let usage: AIRequestResult['usage']
const stream = await readSSE(response, (eventData) => {
let payload: OpenAIStreamPayload
try {
payload = JSON.parse(eventData) as OpenAIStreamPayload
} catch {
throw new Error('模型服务返回了无法解析的流式数据')
}
if (payload.error?.message) throw new Error(payload.error.message)
if (payload.usage) usage = toOpenAIUsage(payload.usage)
const delta = payload.choices?.[0]?.delta?.content
if (typeof delta === 'string' && delta) {
data += delta
onDelta?.(delta)
}
})
if (!stream.sawData) {
let payload: OpenAIResponsePayload
try {
payload = JSON.parse(stream.rawBody) as OpenAIResponsePayload
} catch {
throw new Error('模型服务返回了无法解析的流式数据')
}
if (payload.error?.message) throw new Error(payload.error.message)
const content = String(payload.choices?.[0]?.message?.content || '')
if (content) onDelta?.(content)
return { data: content, usage: toOpenAIUsage(payload.usage) }
}
return { data, usage }
}
async function parseOpenAIResponsesStream(
response: Response,
onDelta?: AIChatDeltaHandler
): Promise<AIRequestResult> {
let data = ''
let completedResponse: OpenAIResponsesPayload | undefined
let usage: AIRequestResult['usage']
const stream = await readSSE(response, (eventData) => {
let payload: OpenAIResponsesStreamPayload
try {
payload = JSON.parse(eventData) as OpenAIResponsesStreamPayload
} catch {
throw new Error('模型服务返回了无法解析的 Responses 流式数据')
}
const errorMessage = payload.error?.message || payload.response?.error?.message
if (errorMessage) throw new Error(errorMessage)
if (payload.response?.usage) usage = toOpenAIResponsesUsage(payload.response.usage)
if (payload.type === 'response.output_text.delta' && typeof payload.delta === 'string') {
data += payload.delta
onDelta?.(payload.delta)
}
if (payload.type === 'response.completed') completedResponse = payload.response
if (payload.type === 'response.failed' || payload.type === 'response.incomplete') {
const reason = payload.response?.incomplete_details?.reason
throw new Error(reason ? `模型响应未完成:${reason}` : payload.message || '模型响应未完成')
}
})
if (!stream.sawData) {
let payload: OpenAIResponsesPayload
try {
payload = JSON.parse(stream.rawBody) as OpenAIResponsesPayload
} catch {
throw new Error('模型服务返回了无法解析的 Responses 流式数据')
}
if (payload.error?.message) throw new Error(payload.error.message)
const content = extractOpenAIResponsesText(payload)
if (content) onDelta?.(content)
return { data: content, usage: toOpenAIResponsesUsage(payload.usage) }
}
if (!data && completedResponse) {
data = extractOpenAIResponsesText(completedResponse)
if (data) onDelta?.(data)
}
return { data, usage }
}
async function readSSE(
response: Response,
onEvent: (eventData: string) => void
): Promise<{ sawData: boolean; rawBody: string }> {
if (!response.body) throw new Error('模型服务未返回可读取的流式响应')
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
let rawBody = ''
let sawData = false
let finished = false
const consumeEvent = (event: string): boolean => {
const eventData = event
.split(/\r\n|\r|\n/)
.filter((line) => line.startsWith('data:'))
.map((line) => {
const value = line.slice(5)
return value.startsWith(' ') ? value.slice(1) : value
})
.join('\n')
if (!eventData) return false
sawData = true
rawBody = ''
if (eventData.trim() === '[DONE]') return true
onEvent(eventData)
return false
}
const consumeBuffer = (): void => {
while (!finished) {
const boundary = /(?:\r\n|\r|\n){2}/.exec(buffer)
if (!boundary) return
const event = buffer.slice(0, boundary.index)
buffer = buffer.slice(boundary.index + boundary[0].length)
finished = consumeEvent(event)
}
}
try {
while (!finished) {
const chunk = await reader.read()
if (chunk.done) {
const tail = decoder.decode()
buffer += tail
if (!sawData) rawBody += tail
consumeBuffer()
break
}
const text = decoder.decode(chunk.value, { stream: true })
buffer += text
if (!sawData) rawBody += text
consumeBuffer()
}
if (!finished && buffer.trim()) finished = consumeEvent(buffer)
if (finished) await reader.cancel().catch(() => undefined)
} catch (error) {
await reader.cancel(error).catch(() => undefined)
throw error
} finally {
reader.releaseLock()
}
return { sawData, rawBody }
}
function extractOpenAIResponsesText(payload: OpenAIResponsesPayload): string {
if (typeof payload.output_text === 'string') return payload.output_text
return (payload.output || [])
.flatMap((item) => item.content || [])
.filter((item) => item.type === 'output_text')
.map((item) => item.text || '')
.join('')
}
function toOpenAIResponsesUsage(
usage: OpenAIResponsesPayload['usage']
): AIRequestResult['usage'] | undefined {
return usage
? {
input: usage.input_tokens,
output: usage.output_tokens,
total: usage.total_tokens,
estimated: false
}
: undefined
}
function toOpenAIUsage(
usage: OpenAIResponsePayload['usage']
): AIRequestResult['usage'] | undefined {
return usage
? {
input: usage.prompt_tokens,
output: usage.completion_tokens,
total: usage.total_tokens,
estimated: false
}
: undefined
}
async function parseJsonResponse<T>(response: Response): Promise<T> {
const body = await response.text()
try {
@@ -128,6 +128,34 @@ const isAbortError = (error: unknown): boolean =>
(error instanceof DOMException && error.name === 'AbortError') ||
(error instanceof Error && error.name === 'AbortError')
const createDeltaBatcher = (
publish: (delta: string) => void,
intervalMs = 40
): { push: (delta: string) => void; close: (flushPending: boolean) => void } => {
let pending = ''
let timer: ReturnType<typeof setTimeout> | undefined
const flush = (): void => {
timer = undefined
if (!pending) return
const delta = pending
pending = ''
publish(delta)
}
return {
push: (delta: string): void => {
if (!delta) return
pending += delta
if (!timer) timer = setTimeout(flush, intervalMs)
},
close: (flushPending: boolean): void => {
if (timer) clearTimeout(timer)
timer = undefined
if (flushPending) flush()
else pending = ''
}
}
}
/**
* Main-process search orchestrator. It owns the only transition from raw
* candidates to Final Evidence; the AI and Renderer never receive a wider
@@ -748,6 +776,23 @@ export class AiSearchPipelineService {
timings: snapshotTimings()
})
const aiGenerationStartedAt = Date.now()
const answerDeltaBatcher = createDeltaBatcher((answerDelta) =>
emit({
stage: 'ai_generating',
status: 'running',
message: '正在生成带来源的回答',
answerDelta,
plan,
modelName: aiConfig.modelName,
stats: {
matchedMessages: evidenceBuild.candidateCount,
evidenceCount: evidence.length,
contextEvidenceCount: evidence.length,
tokenEstimate
},
timings: snapshotTimings()
})
)
const answer = await this.chatForSearchRequest(
request.requestId,
aiConfig.providerId,
@@ -760,8 +805,9 @@ export class AiSearchPipelineService {
},
{ role: 'user', content: prompt }
],
signal
)
signal,
answerDeltaBatcher.push
).finally(() => answerDeltaBatcher.close(!signal.aborted))
signal.throwIfAborted()
timings.aiGenerationMs = Date.now() - aiGenerationStartedAt
if (!answer.success || !answer.data) {
@@ -964,13 +1010,14 @@ export class AiSearchPipelineService {
providerId: string | undefined,
modelId: string,
messages: Array<{ role: string; content: string }>,
signal: AbortSignal
signal: AbortSignal,
onDelta?: (delta: string) => void
): ReturnType<AIProviderService['chat']> {
if (!this.canUseAiForRequest(requestId, providerId)) {
return { success: false, error: '当前搜索请求未授权向该 AI 服务发送内容' }
}
signal.throwIfAborted()
return this.aiProvider.chat(messages, { providerId, modelId }, signal)
return this.aiProvider.chat(messages, { providerId, modelId }, signal, onDelta)
}
private clearPendingAuthorization(requestId: string): void {
@@ -261,6 +261,8 @@ export function AISearchWorkspace({
window.api.onAiSearchProgress((progress) => {
if (progress.requestId !== searchRequestIdRef.current) return
setSearchProgress((current) => ({ ...current, [progress.stage]: progress }))
const answerDelta = progress.answerDelta
if (answerDelta) setAnswer((current) => current + answerDelta)
if (progress.agentTrace) {
setAgentTrace((current) =>
current.some((item) => item.sequence === progress.agentTrace?.sequence)
@@ -846,6 +848,19 @@ export function AISearchWorkspace({
</div>
</section>
</div>
{answer && (
<section
className="ai-search-summary-block ai-search-streaming-answer"
aria-label="正在生成的回答"
aria-live="polite"
>
<div className="ai-search-section-heading">
<span />
回答生成中
</div>
<div className="ai-search-answer">{renderMarkdown(answer)}</div>
</section>
)}
</div>
)
}
@@ -288,6 +288,44 @@ export function AIProviderEditor({
}
/>
</label>
{provider.type !== 'anthropic-messages' && (
<>
<label className="wide">
OpenAI API 接口
<select
value={provider.advanced.apiProtocol || 'chat-completions'}
onChange={(event) =>
patch({
advanced: {
...provider.advanced,
apiProtocol: event.target
.value as AIProviderConfig['advanced']['apiProtocol']
}
})
}
>
<option value="chat-completions">Chat Completions(兼容模式)</option>
<option value="responses">Responses API</option>
</select>
<small>Codex 或仅支持 /responses 的中转服务请选择 Responses API。</small>
</label>
<label className="wide ai-provider-stream-toggle">
<span className="ai-provider-stream-toggle-control">
<input
type="checkbox"
checked={Boolean(provider.advanced.stream)}
onChange={(event) =>
patch({
advanced: { ...provider.advanced, stream: event.target.checked }
})
}
/>
启用流式响应(stream: true)
</span>
<small>问问微信会边生成边显示,其他功能仍会等待完整结果。</small>
</label>
</>
)}
<label className="wide">
额外 Headers(JSON)
<textarea
@@ -92,6 +92,13 @@ export function createProviderFromPreset(presetId = 'deepseek'): AIProviderConfi
}
],
defaultModel: preset.model,
advanced: { timeoutMs: 120000, temperature: 0.7, maxTokens: 4096, extraHeaders: {} }
advanced: {
timeoutMs: 120000,
temperature: 0.7,
maxTokens: 4096,
stream: false,
apiProtocol: 'chat-completions',
extraHeaders: {}
}
}
}
+22
View File
@@ -1076,6 +1076,27 @@
white-space: pre-wrap;
}
.ai-search-streaming-answer {
width: 100%;
}
.ai-search-streaming-answer .ai-search-answer::after {
content: '';
display: inline-block;
width: 2px;
height: 1em;
margin-left: 3px;
background: var(--wxex-ai);
vertical-align: -0.12em;
animation: ai-search-stream-cursor 0.8s steps(2, start) infinite;
}
@keyframes ai-search-stream-cursor {
50% {
opacity: 0;
}
}
.ai-search-answer-evidence,
.ai-search-trace {
display: flex;
@@ -1577,6 +1598,7 @@
.ai-search-result,
.ai-search-summary-block,
.ai-search-answer > *,
.ai-search-streaming-answer .ai-search-answer::after,
.ai-search-evidence-card,
.ai-search-history-popover,
.ai-search-pipeline-step.active .ai-search-pipeline-mark,
+9
View File
@@ -1807,6 +1807,15 @@
min-height: 90px;
resize: vertical;
}
.ai-provider-stream-toggle-control {
display: flex;
align-items: center;
gap: 7px;
}
.ai-provider-stream-toggle small {
color: #66706b;
font-weight: 400;
}
.ai-provider-preview pre {
overflow: auto;
max-height: 280px;
+4
View File
@@ -7,6 +7,8 @@ export type AIProviderType =
export type AIAuthType = 'bearer' | 'x-api-key' | 'custom-header' | 'none'
export type AIOpenAIProtocol = 'chat-completions' | 'responses'
export interface AIProviderAuth {
type: AIAuthType
headerName?: string
@@ -34,6 +36,8 @@ export interface AIProviderAdvancedSettings {
timeoutMs: number
temperature?: number
maxTokens?: number
stream?: boolean
apiProtocol?: AIOpenAIProtocol
extraHeaders: Record<string, string>
}
+1
View File
@@ -71,6 +71,7 @@ export interface AiSearchProgressEvent {
stage: AiSearchProgressStage
status: AiSearchProgressStatus
message: string
answerDelta?: string
plan?: AiSearchPlan
stats?: {
knowledgeMessageCount?: number