@veltra/ai 1.4.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/chat/index.d.ts +3 -3
  2. package/dist/chat/transports/index.d.ts +2 -2
  3. package/dist/chat/transports/openai.d.ts +16 -12
  4. package/dist/chat/transports/openai.js +56 -15
  5. package/dist/chat/transports/openai.js.map +1 -1
  6. package/dist/chat/types.d.ts +41 -2
  7. package/dist/chat/use-chat.d.ts +26 -1
  8. package/dist/chat/use-chat.js +132 -13
  9. package/dist/chat/use-chat.js.map +1 -1
  10. package/dist/components/ai-chat/ai-chat.js +222 -41
  11. package/dist/components/ai-chat/ai-chat.js.map +1 -1
  12. package/dist/components/ai-chat/ask-question.js +2 -2
  13. package/dist/components/ai-chat/ask-question.js.map +1 -1
  14. package/dist/components/ai-chat/chat-input.js +81 -44
  15. package/dist/components/ai-chat/chat-input.js.map +1 -1
  16. package/dist/components/ai-chat/di.js.map +1 -1
  17. package/dist/components/ai-chat/message-item.js +101 -23
  18. package/dist/components/ai-chat/message-item.js.map +1 -1
  19. package/dist/components/ai-chat/message-list.js +217 -32
  20. package/dist/components/ai-chat/message-list.js.map +1 -1
  21. package/dist/components/ai-chat/model-picker.js +140 -0
  22. package/dist/components/ai-chat/model-picker.js.map +1 -0
  23. package/dist/components/ai-chat/queue-list.js +70 -0
  24. package/dist/components/ai-chat/queue-list.js.map +1 -0
  25. package/dist/components/ai-chat/side-panel.js +96 -0
  26. package/dist/components/ai-chat/side-panel.js.map +1 -0
  27. package/dist/components/ai-chat/style.d.ts +6 -0
  28. package/dist/components/ai-chat/style.js +6 -0
  29. package/dist/components/ai-chat/style2.css +552 -41
  30. package/dist/components/ai-chat/tool-call.js +61 -27
  31. package/dist/components/ai-chat/tool-call.js.map +1 -1
  32. package/dist/components/ai-orb/ai-orb.js +94 -0
  33. package/dist/components/ai-orb/ai-orb.js.map +1 -0
  34. package/dist/components/ai-orb/orb-renderer.js +411 -0
  35. package/dist/components/ai-orb/orb-renderer.js.map +1 -0
  36. package/dist/components/ai-orb/style.d.ts +1 -0
  37. package/dist/components/ai-orb/style.js +1 -0
  38. package/dist/components/ai-orb/style2.css +23 -0
  39. package/dist/index.d.ts +8 -6
  40. package/dist/index.js +2 -2
  41. package/dist/providers/types.d.ts +47 -0
  42. package/dist/style.js +1 -0
  43. package/dist/tools/ask-question/ask-question.d.ts +2 -17
  44. package/dist/tools/ask-question/index.d.ts +2 -2
  45. package/dist/tools/index.d.ts +3 -2
  46. package/dist/tools/index.js +10 -0
  47. package/dist/tools/index.js.map +1 -0
  48. package/dist/types/ai-chat.d.ts +35 -7
  49. package/dist/types/ai-orb.d.ts +27 -0
  50. package/dist/types/index.d.ts +2 -1
  51. package/package.json +8 -8
  52. package/src/chat/__test__/use-chat.test.ts +295 -9
  53. package/src/chat/transports/__test__/openai.test.ts +317 -0
  54. package/src/chat/transports/openai.ts +88 -26
  55. package/src/chat/types.ts +41 -1
  56. package/src/chat/use-chat.ts +177 -14
  57. package/src/components/ai-chat/__test__/ai-chat.test.ts +690 -12
  58. package/src/components/ai-chat/ai-chat.vue +241 -31
  59. package/src/components/ai-chat/chat-input.vue +78 -31
  60. package/src/components/ai-chat/di.ts +2 -0
  61. package/src/components/ai-chat/message-item.vue +98 -11
  62. package/src/components/ai-chat/message-list.vue +261 -21
  63. package/src/components/ai-chat/model-picker.vue +177 -0
  64. package/src/components/ai-chat/queue-list.vue +63 -0
  65. package/src/components/ai-chat/side-panel.vue +76 -0
  66. package/src/components/ai-chat/style.scss +621 -43
  67. package/src/components/ai-chat/style.ts +6 -0
  68. package/src/components/ai-chat/tool-call.vue +86 -42
  69. package/src/components/ai-orb/__test__/ai-orb.test.ts +251 -0
  70. package/src/components/ai-orb/ai-orb.vue +107 -0
  71. package/src/components/ai-orb/index.ts +1 -0
  72. package/src/components/ai-orb/orb-renderer.ts +505 -0
  73. package/src/components/ai-orb/style.scss +10 -0
  74. package/src/components/ai-orb/style.ts +1 -0
  75. package/src/components/index.ts +1 -0
  76. package/src/index.ts +8 -1
  77. package/src/providers/index.ts +1 -0
  78. package/src/providers/types.ts +46 -0
  79. package/src/style.ts +1 -0
  80. package/src/tools/ask-question/__test__/ask-question.test.ts +4 -4
  81. package/src/tools/index.ts +14 -1
  82. package/src/types/ai-chat.ts +35 -6
  83. package/src/types/ai-orb.ts +27 -0
  84. package/src/types/index.ts +1 -0
  85. package/dist/components/ai-chat/ai-chat.vue.d.ts +0 -36
  86. package/dist/components/ai-chat/index.d.ts +0 -2
  87. package/dist/components/index.d.ts +0 -3
@@ -0,0 +1,317 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+
3
+ import type { ChatTransportHandlers } from '../../types'
4
+ import { createOpenAITransport } from '../openai'
5
+
6
+ const baseHandlers = (): ChatTransportHandlers => ({
7
+ onTextDelta: vi.fn(),
8
+ onReasoningDelta: vi.fn(),
9
+ onToolCall: vi.fn(),
10
+ onError: vi.fn()
11
+ })
12
+
13
+ afterEach(() => {
14
+ vi.unstubAllGlobals()
15
+ vi.restoreAllMocks()
16
+ })
17
+
18
+ describe('createOpenAITransport', () => {
19
+ it('providers 为空时抛错', () => {
20
+ expect(() => createOpenAITransport({ providers: [] })).toThrow(/providers 不能为空/)
21
+ })
22
+
23
+ it('跨 Provider 模型 id 重复时抛错', () => {
24
+ expect(() =>
25
+ createOpenAITransport({
26
+ providers: [
27
+ {
28
+ id: 'a',
29
+ endpoint: 'https://a.example/v1/chat/completions',
30
+ models: [{ id: 'shared' }]
31
+ },
32
+ { id: 'b', endpoint: 'https://b.example/v1/chat/completions', models: [{ id: 'shared' }] }
33
+ ]
34
+ })
35
+ ).toThrow(/模型 id "shared" 重复/)
36
+ })
37
+
38
+ it('挂载 models 与 defaultModel', () => {
39
+ const transport = createOpenAITransport({
40
+ providers: [
41
+ {
42
+ id: 'openai',
43
+ label: 'OpenAI',
44
+ endpoint: 'https://api.openai.com/v1/chat/completions',
45
+ apiKey: 'sk-test',
46
+ models: [
47
+ { id: 'gpt-4o', label: 'GPT-4o' },
48
+ { id: 'o3-mini', label: 'o3-mini' }
49
+ ]
50
+ }
51
+ ]
52
+ })
53
+
54
+ expect(transport.defaultModel).toBe('gpt-4o')
55
+ expect(transport.models).toEqual([
56
+ { id: 'gpt-4o', label: 'GPT-4o', providerId: 'openai', providerLabel: 'OpenAI' },
57
+ { id: 'o3-mini', label: 'o3-mini', providerId: 'openai', providerLabel: 'OpenAI' }
58
+ ])
59
+ })
60
+
61
+ it('按 model 路由到不同 endpoint,相对路径原样 fetch', async () => {
62
+ const fetchMock = vi.fn(async () => {
63
+ return new Response('data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n', {
64
+ status: 200,
65
+ headers: { 'Content-Type': 'text/event-stream' }
66
+ })
67
+ })
68
+ vi.stubGlobal('fetch', fetchMock)
69
+
70
+ const transport = createOpenAITransport({
71
+ providers: [
72
+ { id: 'proxy', endpoint: '/api/ai/chat', models: [{ id: 'proxy-model' }] },
73
+ {
74
+ id: 'remote',
75
+ endpoint: 'https://api.example.com/v1/chat/completions',
76
+ apiKey: 'sk-remote',
77
+ models: [{ id: 'remote-model' }]
78
+ }
79
+ ]
80
+ })
81
+
82
+ const handlers = baseHandlers()
83
+ await transport(
84
+ {
85
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
86
+ model: 'remote-model',
87
+ signal: new AbortController().signal
88
+ },
89
+ handlers
90
+ )
91
+
92
+ expect(fetchMock).toHaveBeenCalledTimes(1)
93
+ expect(fetchMock.mock.calls[0]![0]).toBe('https://api.example.com/v1/chat/completions')
94
+ const remoteInit = fetchMock.mock.calls[0]![1] as RequestInit
95
+ expect(remoteInit.headers).toMatchObject({ Authorization: 'Bearer sk-remote' })
96
+ expect(JSON.parse(String(remoteInit.body))).toMatchObject({
97
+ model: 'remote-model',
98
+ stream: true
99
+ })
100
+
101
+ await transport(
102
+ {
103
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
104
+ model: 'proxy-model',
105
+ signal: new AbortController().signal
106
+ },
107
+ handlers
108
+ )
109
+
110
+ expect(fetchMock.mock.calls[1]![0]).toBe('/api/ai/chat')
111
+ const proxyInit = fetchMock.mock.calls[1]![1] as RequestInit
112
+ expect((proxyInit.headers as Record<string, string>).Authorization).toBeUndefined()
113
+ })
114
+
115
+ it('无 apiKey 时不带 Authorization', async () => {
116
+ const fetchMock = vi.fn(async () => {
117
+ return new Response('data: [DONE]\n\n', {
118
+ status: 200,
119
+ headers: { 'Content-Type': 'text/event-stream' }
120
+ })
121
+ })
122
+ vi.stubGlobal('fetch', fetchMock)
123
+
124
+ const transport = createOpenAITransport({
125
+ providers: [{ id: 'local', endpoint: '/api/chat', models: [{ id: 'local-model' }] }]
126
+ })
127
+
128
+ await transport(
129
+ {
130
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
131
+ signal: new AbortController().signal
132
+ },
133
+ baseHandlers()
134
+ )
135
+
136
+ const headers = fetchMock.mock.calls[0]![1]!.headers as Record<string, string>
137
+ expect(headers.Authorization).toBeUndefined()
138
+ expect(JSON.parse(String(fetchMock.mock.calls[0]![1]!.body))).toMatchObject({
139
+ model: 'local-model'
140
+ })
141
+ })
142
+
143
+ it('缺省将 reasoningLevel 写入 reasoning_effort', async () => {
144
+ const fetchMock = vi.fn(async () => {
145
+ return new Response('data: [DONE]\n\n', {
146
+ status: 200,
147
+ headers: { 'Content-Type': 'text/event-stream' }
148
+ })
149
+ })
150
+ vi.stubGlobal('fetch', fetchMock)
151
+
152
+ const transport = createOpenAITransport({
153
+ providers: [
154
+ {
155
+ id: 'openai',
156
+ endpoint: 'https://api.openai.com/v1/chat/completions',
157
+ apiKey: 'sk',
158
+ models: [{ id: 'o3-mini' }]
159
+ }
160
+ ]
161
+ })
162
+
163
+ await transport(
164
+ {
165
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
166
+ model: 'o3-mini',
167
+ reasoningLevel: 'high',
168
+ signal: new AbortController().signal
169
+ },
170
+ baseHandlers()
171
+ )
172
+
173
+ expect(JSON.parse(String(fetchMock.mock.calls[0]![1]!.body))).toMatchObject({
174
+ model: 'o3-mini',
175
+ reasoning_effort: 'high'
176
+ })
177
+ })
178
+
179
+ it('applyReasoning 可自定义写入字段', async () => {
180
+ const fetchMock = vi.fn(async () => {
181
+ return new Response('data: [DONE]\n\n', {
182
+ status: 200,
183
+ headers: { 'Content-Type': 'text/event-stream' }
184
+ })
185
+ })
186
+ vi.stubGlobal('fetch', fetchMock)
187
+
188
+ const transport = createOpenAITransport({
189
+ providers: [
190
+ {
191
+ id: 'custom',
192
+ endpoint: 'https://custom.example/chat',
193
+ applyReasoning: (level, body) => {
194
+ body.thinking = { budget: level }
195
+ },
196
+ models: [{ id: 'custom-model' }]
197
+ }
198
+ ]
199
+ })
200
+
201
+ await transport(
202
+ {
203
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
204
+ reasoningLevel: '8k',
205
+ signal: new AbortController().signal
206
+ },
207
+ baseHandlers()
208
+ )
209
+
210
+ expect(JSON.parse(String(fetchMock.mock.calls[0]![1]!.body))).toMatchObject({
211
+ thinking: { budget: '8k' }
212
+ })
213
+ expect(JSON.parse(String(fetchMock.mock.calls[0]![1]!.body)).reasoning_effort).toBeUndefined()
214
+ })
215
+
216
+ it('未知 model 时通过 onError 报错且不发请求', async () => {
217
+ const fetchMock = vi.fn()
218
+ vi.stubGlobal('fetch', fetchMock)
219
+ const handlers = baseHandlers()
220
+
221
+ const transport = createOpenAITransport({
222
+ providers: [{ id: 'a', endpoint: 'https://a.example/chat', models: [{ id: 'known' }] }]
223
+ })
224
+
225
+ await transport(
226
+ {
227
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
228
+ model: 'unknown',
229
+ signal: new AbortController().signal
230
+ },
231
+ handlers
232
+ )
233
+
234
+ expect(fetchMock).not.toHaveBeenCalled()
235
+ expect(handlers.onError).toHaveBeenCalledWith(
236
+ expect.objectContaining({ message: expect.stringContaining('unknown') })
237
+ )
238
+ })
239
+
240
+ it('工具调用参数分片跨 chunk 累积为一次完整 onToolCall', async () => {
241
+ // 同一工具调用的多个分片共享 index:0,不得被误判为"新一轮"而提前 flush
242
+ const sse = String.raw`data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"calculate","arguments":""}}]}}]}
243
+
244
+ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"ex"}}]}}]}
245
+
246
+ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"pression\":\"1+1\"}"}}]}}]}
247
+
248
+ data: [DONE]
249
+ `
250
+ vi.stubGlobal(
251
+ 'fetch',
252
+ vi.fn(async () => new Response(sse, { status: 200 }))
253
+ )
254
+
255
+ const transport = createOpenAITransport({
256
+ providers: [{ id: 'a', endpoint: 'https://a.example/chat', models: [{ id: 'm' }] }]
257
+ })
258
+ const handlers = baseHandlers()
259
+
260
+ await transport(
261
+ {
262
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
263
+ signal: new AbortController().signal
264
+ },
265
+ handlers
266
+ )
267
+
268
+ expect(handlers.onToolCall).toHaveBeenCalledTimes(1)
269
+ expect(handlers.onToolCall).toHaveBeenCalledWith({
270
+ id: 'call_1',
271
+ name: 'calculate',
272
+ arguments: '{"expression":"1+1"}'
273
+ })
274
+ })
275
+
276
+ it('多个工具调用按 index 顺序完整抛出', async () => {
277
+ const sse = String.raw`data: {"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"alpha","arguments":""}}]}}]}
278
+
279
+ data: {"choices":[{"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"x\":1}"}}]}}]}
280
+
281
+ data: {"choices":[{"delta":{"tool_calls":[{"index":1,"id":"call_2","function":{"name":"beta","arguments":""}}]}}]}
282
+
283
+ data: {"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"{\"y\":2}"}}]}}]}
284
+
285
+ data: [DONE]
286
+ `
287
+ vi.stubGlobal(
288
+ 'fetch',
289
+ vi.fn(async () => new Response(sse, { status: 200 }))
290
+ )
291
+
292
+ const transport = createOpenAITransport({
293
+ providers: [{ id: 'a', endpoint: 'https://a.example/chat', models: [{ id: 'm' }] }]
294
+ })
295
+ const handlers = baseHandlers()
296
+
297
+ await transport(
298
+ {
299
+ messages: [{ id: '1', role: 'user', content: 'hi' }],
300
+ signal: new AbortController().signal
301
+ },
302
+ handlers
303
+ )
304
+
305
+ expect(handlers.onToolCall).toHaveBeenCalledTimes(2)
306
+ expect(handlers.onToolCall).toHaveBeenNthCalledWith(1, {
307
+ id: 'call_1',
308
+ name: 'alpha',
309
+ arguments: '{"x":1}'
310
+ })
311
+ expect(handlers.onToolCall).toHaveBeenNthCalledWith(2, {
312
+ id: 'call_2',
313
+ name: 'beta',
314
+ arguments: '{"y":2}'
315
+ })
316
+ })
317
+ })
@@ -1,3 +1,4 @@
1
+ import type { ChatModelOption, ChatProvider } from '../../providers'
1
2
  import type {
2
3
  ChatMessage,
3
4
  ChatTool,
@@ -6,20 +7,24 @@ import type {
6
7
  ChatTransportRequest
7
8
  } from '../types'
8
9
 
9
- /** createOpenAITransport 的配置项 */
10
+ /** createOpenAITransport 的配置项(多 Provider) */
10
11
  export interface OpenAITransportOptions {
11
- /** chat completions 端点,如 https://api.openai.com/v1/chat/completions */
12
- endpoint: string
13
- /** API Key(Bearer 鉴权) */
14
- apiKey?: string
15
- /** 模型名 */
16
- model: string
17
- /** 额外请求头 */
12
+ /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */
13
+ providers: ChatProvider[]
14
+ /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */
18
15
  headers?: Record<string, string>
19
- /** 额外请求体字段,如 temperature、top_p */
16
+ /** 全局额外请求体字段,如 temperature、top_p */
20
17
  body?: Record<string, unknown>
21
18
  }
22
19
 
20
+ /** 带扁平模型列表元数据的 OpenAI 兼容 transport */
21
+ export type OpenAITransport = ChatTransport & {
22
+ /** 供 UI 使用的扁平模型列表 */
23
+ readonly models: ChatModelOption[]
24
+ /** 默认模型 id(首个 Provider 的首个模型) */
25
+ readonly defaultModel: string
26
+ }
27
+
23
28
  interface OpenAIMessage {
24
29
  role: string
25
30
  content?: unknown
@@ -130,8 +135,8 @@ async function parseSSE(
130
135
  if (Array.isArray(delta.tool_calls)) {
131
136
  for (const tc of delta.tool_calls) {
132
137
  const index: number = tc.index ?? 0
133
- // index 回退说明新的一轮 tool_calls 开始,先把已累积的抛出去
134
- if (index <= lastIndex && accumulated.size > 0) flushToolCalls()
138
+ // 同一调用的参数分片共享 index;index 变大才说明前一个调用已完整,先抛出去
139
+ if (index > lastIndex && accumulated.size > 0) flushToolCalls()
135
140
  lastIndex = Math.max(lastIndex, index)
136
141
 
137
142
  let call = accumulated.get(index)
@@ -178,33 +183,88 @@ async function parseSSE(
178
183
  flushToolCalls()
179
184
  }
180
185
 
186
+ /** 校验 providers 并展开扁平模型列表 */
187
+ function flattenModels(providers: ChatProvider[]): {
188
+ models: ChatModelOption[]
189
+ byModelId: Map<string, ChatProvider>
190
+ } {
191
+ if (!providers.length) {
192
+ throw new Error('[createOpenAITransport] providers 不能为空')
193
+ }
194
+
195
+ const models: ChatModelOption[] = []
196
+ const byModelId = new Map<string, ChatProvider>()
197
+
198
+ for (const provider of providers) {
199
+ if (!provider.models?.length) {
200
+ throw new Error(`[createOpenAITransport] Provider "${provider.id}" 未配置 models`)
201
+ }
202
+ for (const model of provider.models) {
203
+ if (byModelId.has(model.id)) {
204
+ throw new Error(
205
+ `[createOpenAITransport] 模型 id "${model.id}" 重复(跨 Provider 须全局唯一)`
206
+ )
207
+ }
208
+ byModelId.set(model.id, provider)
209
+ models.push({ ...model, providerId: provider.id, providerLabel: provider.label })
210
+ }
211
+ }
212
+
213
+ return { models, byModelId }
214
+ }
215
+
216
+ /** 缺省:写入 OpenAI 兼容的 reasoning_effort */
217
+ function defaultApplyReasoning(level: string, body: Record<string, unknown>) {
218
+ body.reasoning_effort = level
219
+ }
220
+
181
221
  /**
182
222
  * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
183
- * 手写 fetch + SSE 解析,无任何第三方依赖。
223
+ * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。
184
224
  */
185
- export function createOpenAITransport(options: OpenAITransportOptions): ChatTransport {
186
- const { endpoint, apiKey, model, headers, body } = options
225
+ export function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport {
226
+ const { providers, headers: globalHeaders, body: globalBody } = options
227
+ const { models, byModelId } = flattenModels(providers)
228
+ const defaultModel = models[0]!.id
187
229
 
188
- return async (request: ChatTransportRequest, handlers: ChatTransportHandlers) => {
189
- const { messages, systemPrompt, tools, signal } = request
230
+ const send: ChatTransport = async (
231
+ request: ChatTransportRequest,
232
+ handlers: ChatTransportHandlers
233
+ ) => {
234
+ const { messages, systemPrompt, tools, signal, reasoningLevel } = request
235
+ const modelId = request.model ?? defaultModel
236
+ const provider = byModelId.get(modelId)
237
+
238
+ if (!provider) {
239
+ handlers.onError?.(new Error(`未找到模型 "${modelId}" 对应的 Provider`))
240
+ return
241
+ }
242
+
243
+ const requestBody: Record<string, unknown> = {
244
+ ...globalBody,
245
+ model: modelId,
246
+ stream: true,
247
+ messages: toOpenAIMessages(messages, systemPrompt),
248
+ ...(tools?.length ? { tools: toOpenAITools(tools) } : {})
249
+ }
250
+
251
+ if (reasoningLevel) {
252
+ const apply = provider.applyReasoning ?? defaultApplyReasoning
253
+ apply(reasoningLevel, requestBody)
254
+ }
190
255
 
191
256
  let response: Response
192
257
  try {
193
- response = await fetch(endpoint, {
258
+ response = await fetch(provider.endpoint, {
194
259
  method: 'POST',
195
260
  signal,
196
261
  headers: {
197
262
  'Content-Type': 'application/json',
198
- ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
199
- ...headers
263
+ ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}),
264
+ ...globalHeaders,
265
+ ...provider.headers
200
266
  },
201
- body: JSON.stringify({
202
- model,
203
- stream: true,
204
- ...body,
205
- messages: toOpenAIMessages(messages, systemPrompt),
206
- ...(tools?.length ? { tools: toOpenAITools(tools) } : {})
207
- })
267
+ body: JSON.stringify(requestBody)
208
268
  })
209
269
  } catch (error) {
210
270
  if (!signal.aborted) {
@@ -234,4 +294,6 @@ export function createOpenAITransport(options: OpenAITransportOptions): ChatTran
234
294
  }
235
295
  }
236
296
  }
297
+
298
+ return Object.assign(send, { models, defaultModel })
237
299
  }
package/src/chat/types.ts CHANGED
@@ -57,6 +57,16 @@ export interface ChatMessage {
57
57
  status?: 'streaming' | 'done' | 'error' | 'aborted'
58
58
  }
59
59
 
60
+ /** 队列中的待发送消息(会话进行中提交的消息按序排队) */
61
+ export interface ChatQueuedMessage {
62
+ /** 队列项 id */
63
+ id: string
64
+ /** 消息内容 */
65
+ content: string
66
+ /** 携带的附件 */
67
+ attachments?: ChatAttachment[]
68
+ }
69
+
60
70
  /** 工具执行上下文 */
61
71
  export interface ChatToolContext {
62
72
  /** 本次工具调用 */
@@ -85,10 +95,36 @@ export interface ChatTool<A = any> {
85
95
  /**
86
96
  * 自定义工具卡片内容渲染(组件或渲染函数),props 为 ChatToolRenderProps。
87
97
  * 设置后替换卡片 body 的默认参数/结果展示;优先级高于 tool-<name> 插槽。
98
+ * renderTo 为 'panel' 时改为在右侧侧边面板中渲染。
88
99
  */
89
100
  render?: Component
90
- /** 执行完成后是否自动折叠。缺省:设置了 render 时为 false,否则为 true */
101
+ /**
102
+ * 渲染位置。缺省 'inline':render 组件展示在会话内的工具卡片中;
103
+ * 'panel':render 组件展示在对话区右侧的侧边面板(新调用自动打开面板,
104
+ * 工具卡片内仅保留「查看面板」入口,面板与会话区宽度可拖拽调节)。
105
+ * 适合打开后台页面、表单、图表、列表等需要较大交互区域的工具。
106
+ */
107
+ renderTo?: 'inline' | 'panel'
108
+ /**
109
+ * 侧边面板默认宽度(px,最小 320),仅 renderTo: 'panel' 时生效。
110
+ * 聚焦到该工具的调用时应用;缺省时面板打开取「容器宽 - 860」(即默认尽可能大,
111
+ * 给会话区保留 860px);面板已打开时切换聚焦保持当前宽度(含用户拖拽结果)。
112
+ */
113
+ panelWidth?: number
114
+ /**
115
+ * 侧边面板标题,仅 renderTo: 'panel' 时生效。
116
+ * 面板标题通常是「业务对象 + 动作」(如「编辑用户 · 张三」)而非工具名,
117
+ * 可传固定字符串,或传函数按本次调用的参数动态生成。缺省取 label ?? name。
118
+ */
119
+ panelTitle?: string | ((toolCall: ChatToolCall) => string)
120
+ /** 执行完成后是否自动折叠。缺省:renderTo 为 'panel' 时为 true;否则设置了 render 时为 false,否则为 true */
91
121
  autoCollapse?: boolean
122
+ /**
123
+ * 终结工具:执行成功后对话即结束,结果不再回灌模型生成额外文字(工具 UI 即最终答复)。
124
+ * 适合天气卡片这类"UI 即答案"的工具,需配合 render 或 tool-<name> 插槽提供完整结果 UI。
125
+ * 工具结果仍会记录到消息历史供后续轮次使用;执行失败/被拒绝时错误照常回灌模型。
126
+ */
127
+ terminal?: boolean
92
128
  /** 工具实现,返回值(或 Promise 返回值)会被 JSON 序列化后回灌给模型 */
93
129
  execute: (args: A, ctx: ChatToolContext) => unknown
94
130
  }
@@ -107,6 +143,10 @@ export interface ChatTransportRequest {
107
143
  systemPrompt?: string
108
144
  /** 可用工具 */
109
145
  tools?: ChatTool[]
146
+ /** 选中的模型 id(内置 OpenAI transport 按此路由 Provider) */
147
+ model?: string
148
+ /** 选中的推理等级(不透明字符串,由 Provider.applyReasoning 写入请求体) */
149
+ reasoningLevel?: string
110
150
  /** 中断信号 */
111
151
  signal: AbortSignal
112
152
  }