@veltra/ai 2.2.1 → 2.2.3
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.
- package/dist/chat/fold.js +14 -2
- package/dist/chat/fold.js.map +1 -1
- package/dist/chat/session.d.ts +13 -6
- package/dist/chat/session.js.map +1 -1
- package/dist/chat/transports/openai.d.ts +2 -1
- package/dist/chat/transports/openai.js +20 -213
- package/dist/chat/transports/openai.js.map +1 -1
- package/dist/chat/use-chat-session.js +3 -13
- package/dist/chat/use-chat-session.js.map +1 -1
- package/dist/components/ai-chat/ai-chat.js.map +1 -1
- package/dist/components/ai-chat/approval-banner.js.map +1 -1
- package/dist/components/ai-chat/ask-question.js.map +1 -1
- package/dist/components/ai-chat/chat-input.js.map +1 -1
- package/dist/components/ai-chat/job-bar.js.map +1 -1
- package/dist/components/ai-chat/message-item.js.map +1 -1
- package/dist/components/ai-chat/message-list.js.map +1 -1
- package/dist/components/ai-chat/model-picker.js.map +1 -1
- package/dist/components/ai-chat/queue-list.js.map +1 -1
- package/dist/components/ai-chat/reasoning-picker.js.map +1 -1
- package/dist/components/ai-chat/side-panel.js.map +1 -1
- package/dist/components/ai-chat/tool-call.js.map +1 -1
- package/dist/components/ai-chat/tool-json.js.map +1 -1
- package/dist/components/ai-chat/turn-process.js.map +1 -1
- package/dist/components/ai-orb/ai-orb.js.map +1 -1
- package/package.json +3 -3
- package/src/chat/__test__/bedrock-harness-sim.test.ts +467 -0
- package/src/chat/__test__/fixtures/bedrock/harness.ts +735 -0
- package/src/chat/__test__/fixtures/bedrock/http.ts +117 -0
- package/src/chat/__test__/fixtures/bedrock/types.ts +15 -0
- package/src/chat/__test__/fold.test.ts +26 -1
- package/src/chat/__test__/session.test.ts +1 -1
- package/src/chat/__test__/use-chat.test.ts +6 -3
- package/src/chat/fold.ts +16 -1
- package/src/chat/session.ts +13 -6
- package/src/chat/transports/__test__/openai.test.ts +1 -1
- package/src/chat/transports/openai.ts +25 -288
- package/src/chat/use-chat-session.ts +4 -5
- package/src/components/ai-chat/__test__/ai-chat.test.ts +1 -1
- package/src/components/ai-chat/__test__/tool-icons.test.ts +1 -1
- package/src/components/ai-orb/__test__/ai-orb.test.ts +1 -1
- package/src/tools/ask-question/__test__/ask-question.test.ts +1 -1
package/dist/chat/fold.js
CHANGED
|
@@ -124,15 +124,27 @@ function foldSessionEvent(state, event) {
|
|
|
124
124
|
});
|
|
125
125
|
break;
|
|
126
126
|
}
|
|
127
|
-
case "assistant/message":
|
|
127
|
+
case "assistant/message": {
|
|
128
|
+
const current = next.messages.find((m) => m.id === event.messageId);
|
|
129
|
+
if (event.history && current && current.status === "streaming") {
|
|
130
|
+
const longer = (a, b) => (b?.length ?? 0) > (a?.length ?? 0) ? b : a;
|
|
131
|
+
next.messages = patchMessage(next.messages, event.messageId, {
|
|
132
|
+
role: "assistant",
|
|
133
|
+
content: longer(current.content, event.content),
|
|
134
|
+
reasoning: longer(current.reasoning, event.reasoning),
|
|
135
|
+
toolCalls: event.toolCalls ?? current.toolCalls
|
|
136
|
+
});
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
128
139
|
next.messages = patchMessage(next.messages, event.messageId, {
|
|
129
140
|
role: "assistant",
|
|
130
141
|
status: "done",
|
|
131
142
|
content: event.content,
|
|
132
143
|
reasoning: event.reasoning,
|
|
133
|
-
toolCalls: event.toolCalls ??
|
|
144
|
+
toolCalls: event.toolCalls ?? current?.toolCalls
|
|
134
145
|
});
|
|
135
146
|
break;
|
|
147
|
+
}
|
|
136
148
|
case "tool/call": {
|
|
137
149
|
const call = {
|
|
138
150
|
id: event.callId,
|
package/dist/chat/fold.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fold.js","names":[],"sources":["../../src/chat/fold.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatSessionEvent } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatToolCall } from './types'\n\nfunction eventSeq(event: ChatSessionEvent): number | undefined {\n return 'seq' in event ? event.seq : undefined\n}\n\nexport interface ChatPendingApproval {\n approvalId: string\n toolName: string\n callId?: string\n reason?: string\n rpcId: string\n}\n\n/** 历史回放与实时 onEvent 共用的折叠状态 */\nexport interface ChatFoldState {\n lastSeq: number | null\n messages: ChatMessage[]\n queue: ChatQueuedMessage[]\n jobs: ChatJob[]\n tokenUsage: ChatTokenUsage | null\n projections: Record<string, unknown>\n title: string | null\n running: boolean\n pendingApprovals: ChatPendingApproval[]\n pendingQuestion: { questions: AskQuestionItem[]; rpcId: string } | null\n error: { code: string; message: string } | null\n}\n\nexport function createFoldState(): ChatFoldState {\n return {\n lastSeq: null,\n messages: [],\n queue: [],\n jobs: [],\n tokenUsage: null,\n projections: {},\n title: null,\n running: false,\n pendingApprovals: [],\n pendingQuestion: null,\n error: null\n }\n}\n\nfunction mergeOptionalCount(a?: number, b?: number): number | undefined {\n if (a == null && b == null) return undefined\n return (a ?? 0) + (b ?? 0)\n}\n\nfunction addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {\n const usage: ChatTokenUsage = {\n promptTokens: a.promptTokens + b.promptTokens,\n completionTokens: a.completionTokens + b.completionTokens,\n totalTokens: a.totalTokens + b.totalTokens\n }\n const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n return usage\n}\n\nfunction isTokenUsage(value: unknown): value is ChatTokenUsage {\n if (value == null || typeof value !== 'object') return false\n const usage = value as ChatTokenUsage\n return (\n typeof usage.promptTokens === 'number' &&\n typeof usage.completionTokens === 'number' &&\n typeof usage.totalTokens === 'number'\n )\n}\n\nfunction replaceAt<T>(list: T[], idx: number, item: T): T[] {\n const next = list.slice()\n next[idx] = item\n return next\n}\n\nfunction patchMessage(\n messages: ChatMessage[],\n id: string,\n patch: Partial<ChatMessage>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.id === id)\n if (idx < 0) {\n return [...messages, { id, role: 'assistant', content: '', status: 'streaming', ...patch }]\n }\n return replaceAt(messages, idx, { ...messages[idx]!, ...patch })\n}\n\nfunction appendToolCall(messages: ChatMessage[], call: ChatToolCall): ChatMessage[] {\n const idx = messages.findLastIndex((m) => m.role === 'assistant')\n if (idx < 0) {\n return [\n ...messages,\n {\n id: `tool-host:${call.id}`,\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: [call]\n }\n ]\n }\n const target = messages[idx]!\n return replaceAt(messages, idx, { ...target, toolCalls: [...(target.toolCalls ?? []), call] })\n}\n\nfunction updateToolCall(\n messages: ChatMessage[],\n callId: string,\n patch: Partial<ChatToolCall>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.toolCalls?.some((c) => c.id === callId))\n if (idx < 0) return messages\n const msg = messages[idx]!\n const calls = (msg.toolCalls ?? []).slice()\n const callIdx = calls.findIndex((c) => c.id === callId)\n if (callIdx < 0) return messages\n calls[callIdx] = { ...calls[callIdx]!, ...patch }\n return replaceAt(messages, idx, { ...msg, toolCalls: calls })\n}\n\n/**\n * 纯函数 fold:历史回放与实时 onEvent 走同一套。\n * 后到的更小/重复 seq 不覆盖已应用事件。\n */\nexport function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState {\n const seq = eventSeq(event)\n if (seq != null && state.lastSeq != null && seq <= state.lastSeq) return state\n\n const next: ChatFoldState = {\n ...state,\n lastSeq: seq ?? state.lastSeq,\n projections: { ...state.projections }\n }\n\n switch (event.type) {\n case 'user/message': {\n if (next.messages.some((m) => m.id === event.messageId)) break\n const userMessage: ChatMessage = { id: event.messageId, role: 'user', content: event.content }\n if (event.attachments) userMessage.attachments = event.attachments\n next.messages = [...next.messages, userMessage]\n break\n }\n case 'assistant/chunk': {\n const current = next.messages.find((m) => m.id === event.messageId)\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'streaming',\n content: (current?.content ?? '') + event.delta,\n reasoning: event.reasoningDelta\n ? (current?.reasoning ?? '') + event.reasoningDelta\n : current?.reasoning\n })\n break\n }\n case 'assistant/message': {\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'done',\n content: event.content,\n reasoning: event.reasoning,\n toolCalls: event.toolCalls ?? next.messages.find((m) => m.id === event.messageId)?.toolCalls\n })\n break\n }\n case 'tool/call': {\n const call: ChatToolCall = {\n id: event.callId,\n name: event.name,\n arguments: event.arguments,\n status: 'pending'\n }\n if (event.view !== undefined) call.view = event.view\n next.messages = appendToolCall(next.messages, call)\n break\n }\n case 'tool/result': {\n const patch: Partial<ChatToolCall> = { status: event.status }\n if (event.result !== undefined) patch.result = event.result\n if (event.error !== undefined) patch.error = event.error\n if (event.view !== undefined) patch.view = event.view\n next.messages = updateToolCall(next.messages, event.callId, patch)\n break\n }\n case 'queue/snapshot': {\n next.queue = event.items.slice()\n break\n }\n case 'jobs/snapshot': {\n next.jobs = event.jobs.slice()\n break\n }\n case 'projection': {\n if (event.key === 'tokenUsage' && isTokenUsage(event.value)) {\n next.tokenUsage = next.tokenUsage\n ? addTokenUsage(next.tokenUsage, event.value)\n : event.value\n } else if (event.key === 'title' && typeof event.value === 'string') {\n next.title = event.value\n } else {\n next.projections[event.key] = event.value\n }\n break\n }\n case 'running': {\n next.running = event.running\n break\n }\n case 'finish': {\n next.running = false\n break\n }\n case 'error': {\n next.running = false\n next.error = { code: event.code, message: event.message }\n break\n }\n case 'approval/requested': {\n if (event.callId) {\n next.messages = updateToolCall(next.messages, event.callId, { status: 'awaiting-confirm' })\n }\n next.pendingApprovals = [\n ...next.pendingApprovals,\n {\n approvalId: event.approvalId,\n toolName: event.toolName,\n callId: event.callId,\n reason: event.reason,\n rpcId: event.rpcId\n }\n ]\n break\n }\n case 'approval/resolved': {\n const hit = next.pendingApprovals.find((a) => a.approvalId === event.approvalId)\n next.pendingApprovals = next.pendingApprovals.filter((a) => a.approvalId !== event.approvalId)\n if (hit?.callId && event.outcome === 'rejected') {\n next.messages = updateToolCall(next.messages, hit.callId, { status: 'rejected' })\n }\n break\n }\n case 'question/requested': {\n next.pendingQuestion = { questions: event.questions, rpcId: event.rpcId }\n break\n }\n case 'question/resolved': {\n if (next.pendingQuestion?.rpcId === event.questionRpcId) next.pendingQuestion = null\n break\n }\n }\n\n return next\n}\n"],"mappings":";AAIA,SAAS,SAAS,OAA6C;CAC7D,OAAO,SAAS,QAAQ,MAAM,MAAM,KAAA;AACtC;AAyBA,SAAgB,kBAAiC;CAC/C,OAAO;EACL,SAAS;EACT,UAAU,CAAC;EACX,OAAO,CAAC;EACR,MAAM,CAAC;EACP,YAAY;EACZ,aAAa,CAAC;EACd,OAAO;EACP,SAAS;EACT,kBAAkB,CAAC;EACnB,iBAAiB;EACjB,OAAO;CACT;AACF;AAEA,SAAS,mBAAmB,GAAY,GAAgC;CACtE,IAAI,KAAK,QAAQ,KAAK,MAAM,OAAO,KAAA;CACnC,QAAQ,KAAK,MAAM,KAAK;AAC1B;AAEA,SAAS,cAAc,GAAmB,GAAmC;CAC3E,MAAM,QAAwB;EAC5B,cAAc,EAAE,eAAe,EAAE;EACjC,kBAAkB,EAAE,mBAAmB,EAAE;EACzC,aAAa,EAAE,cAAc,EAAE;CACjC;CACA,MAAM,iBAAiB,mBAAmB,EAAE,gBAAgB,EAAE,cAAc;CAC5E,IAAI,kBAAkB,MAAM,MAAM,iBAAiB;CACnD,MAAM,kBAAkB,mBAAmB,EAAE,iBAAiB,EAAE,eAAe;CAC/E,IAAI,mBAAmB,MAAM,MAAM,kBAAkB;CACrD,MAAM,sBAAsB,mBAAmB,EAAE,qBAAqB,EAAE,mBAAmB;CAC3F,IAAI,uBAAuB,MAAM,MAAM,sBAAsB;CAC7D,OAAO;AACT;AAEA,SAAS,aAAa,OAAyC;CAC7D,IAAI,SAAS,QAAQ,OAAO,UAAU,UAAU,OAAO;CACvD,MAAM,QAAQ;CACd,OACE,OAAO,MAAM,iBAAiB,YAC9B,OAAO,MAAM,qBAAqB,YAClC,OAAO,MAAM,gBAAgB;AAEjC;AAEA,SAAS,UAAa,MAAW,KAAa,MAAc;CAC1D,MAAM,OAAO,KAAK,MAAM;CACxB,KAAK,OAAO;CACZ,OAAO;AACT;AAEA,SAAS,aACP,UACA,IACA,OACe;CACf,MAAM,MAAM,SAAS,WAAW,MAAM,EAAE,OAAO,EAAE;CACjD,IAAI,MAAM,GACR,OAAO,CAAC,GAAG,UAAU;EAAE;EAAI,MAAM;EAAa,SAAS;EAAI,QAAQ;EAAa,GAAG;CAAM,CAAC;CAE5F,OAAO,UAAU,UAAU,KAAK;EAAE,GAAG,SAAS;EAAO,GAAG;CAAM,CAAC;AACjE;AAEA,SAAS,eAAe,UAAyB,MAAmC;CAClF,MAAM,MAAM,SAAS,eAAe,MAAM,EAAE,SAAS,WAAW;CAChE,IAAI,MAAM,GACR,OAAO,CACL,GAAG,UACH;EACE,IAAI,aAAa,KAAK;EACtB,MAAM;EACN,SAAS;EACT,QAAQ;EACR,WAAW,CAAC,IAAI;CAClB,CACF;CAEF,MAAM,SAAS,SAAS;CACxB,OAAO,UAAU,UAAU,KAAK;EAAE,GAAG;EAAQ,WAAW,CAAC,GAAI,OAAO,aAAa,CAAC,GAAI,IAAI;CAAE,CAAC;AAC/F;AAEA,SAAS,eACP,UACA,QACA,OACe;CACf,MAAM,MAAM,SAAS,WAAW,MAAM,EAAE,WAAW,MAAM,MAAM,EAAE,OAAO,MAAM,CAAC;CAC/E,IAAI,MAAM,GAAG,OAAO;CACpB,MAAM,MAAM,SAAS;CACrB,MAAM,SAAS,IAAI,aAAa,CAAC,EAAA,CAAG,MAAM;CAC1C,MAAM,UAAU,MAAM,WAAW,MAAM,EAAE,OAAO,MAAM;CACtD,IAAI,UAAU,GAAG,OAAO;CACxB,MAAM,WAAW;EAAE,GAAG,MAAM;EAAW,GAAG;CAAM;CAChD,OAAO,UAAU,UAAU,KAAK;EAAE,GAAG;EAAK,WAAW;CAAM,CAAC;AAC9D;;;;;AAMA,SAAgB,iBAAiB,OAAsB,OAAwC;CAC7F,MAAM,MAAM,SAAS,KAAK;CAC1B,IAAI,OAAO,QAAQ,MAAM,WAAW,QAAQ,OAAO,MAAM,SAAS,OAAO;CAEzE,MAAM,OAAsB;EAC1B,GAAG;EACH,SAAS,OAAO,MAAM;EACtB,aAAa,EAAE,GAAG,MAAM,YAAY;CACtC;CAEA,QAAQ,MAAM,MAAd;EACE,KAAK,gBAAgB;GACnB,IAAI,KAAK,SAAS,MAAM,MAAM,EAAE,OAAO,MAAM,SAAS,GAAG;GACzD,MAAM,cAA2B;IAAE,IAAI,MAAM;IAAW,MAAM;IAAQ,SAAS,MAAM;GAAQ;GAC7F,IAAI,MAAM,aAAa,YAAY,cAAc,MAAM;GACvD,KAAK,WAAW,CAAC,GAAG,KAAK,UAAU,WAAW;GAC9C;EACF;EACA,KAAK,mBAAmB;GACtB,MAAM,UAAU,KAAK,SAAS,MAAM,MAAM,EAAE,OAAO,MAAM,SAAS;GAClE,KAAK,WAAW,aAAa,KAAK,UAAU,MAAM,WAAW;IAC3D,MAAM;IACN,QAAQ;IACR,UAAU,SAAS,WAAW,MAAM,MAAM;IAC1C,WAAW,MAAM,kBACZ,SAAS,aAAa,MAAM,MAAM,iBACnC,SAAS;GACf,CAAC;GACD;EACF;EACA,KAAK;GACH,KAAK,WAAW,aAAa,KAAK,UAAU,MAAM,WAAW;IAC3D,MAAM;IACN,QAAQ;IACR,SAAS,MAAM;IACf,WAAW,MAAM;IACjB,WAAW,MAAM,aAAa,KAAK,SAAS,MAAM,MAAM,EAAE,OAAO,MAAM,SAAS,CAAC,EAAE;GACrF,CAAC;GACD;EAEF,KAAK,aAAa;GAChB,MAAM,OAAqB;IACzB,IAAI,MAAM;IACV,MAAM,MAAM;IACZ,WAAW,MAAM;IACjB,QAAQ;GACV;GACA,IAAI,MAAM,SAAS,KAAA,GAAW,KAAK,OAAO,MAAM;GAChD,KAAK,WAAW,eAAe,KAAK,UAAU,IAAI;GAClD;EACF;EACA,KAAK,eAAe;GAClB,MAAM,QAA+B,EAAE,QAAQ,MAAM,OAAO;GAC5D,IAAI,MAAM,WAAW,KAAA,GAAW,MAAM,SAAS,MAAM;GACrD,IAAI,MAAM,UAAU,KAAA,GAAW,MAAM,QAAQ,MAAM;GACnD,IAAI,MAAM,SAAS,KAAA,GAAW,MAAM,OAAO,MAAM;GACjD,KAAK,WAAW,eAAe,KAAK,UAAU,MAAM,QAAQ,KAAK;GACjE;EACF;EACA,KAAK;GACH,KAAK,QAAQ,MAAM,MAAM,MAAM;GAC/B;EAEF,KAAK;GACH,KAAK,OAAO,MAAM,KAAK,MAAM;GAC7B;EAEF,KAAK;GACH,IAAI,MAAM,QAAQ,gBAAgB,aAAa,MAAM,KAAK,GACxD,KAAK,aAAa,KAAK,aACnB,cAAc,KAAK,YAAY,MAAM,KAAK,IAC1C,MAAM;QACL,IAAI,MAAM,QAAQ,WAAW,OAAO,MAAM,UAAU,UACzD,KAAK,QAAQ,MAAM;QAEnB,KAAK,YAAY,MAAM,OAAO,MAAM;GAEtC;EAEF,KAAK;GACH,KAAK,UAAU,MAAM;GACrB;EAEF,KAAK;GACH,KAAK,UAAU;GACf;EAEF,KAAK;GACH,KAAK,UAAU;GACf,KAAK,QAAQ;IAAE,MAAM,MAAM;IAAM,SAAS,MAAM;GAAQ;GACxD;EAEF,KAAK;GACH,IAAI,MAAM,QACR,KAAK,WAAW,eAAe,KAAK,UAAU,MAAM,QAAQ,EAAE,QAAQ,mBAAmB,CAAC;GAE5F,KAAK,mBAAmB,CACtB,GAAG,KAAK,kBACR;IACE,YAAY,MAAM;IAClB,UAAU,MAAM;IAChB,QAAQ,MAAM;IACd,QAAQ,MAAM;IACd,OAAO,MAAM;GACf,CACF;GACA;EAEF,KAAK,qBAAqB;GACxB,MAAM,MAAM,KAAK,iBAAiB,MAAM,MAAM,EAAE,eAAe,MAAM,UAAU;GAC/E,KAAK,mBAAmB,KAAK,iBAAiB,QAAQ,MAAM,EAAE,eAAe,MAAM,UAAU;GAC7F,IAAI,KAAK,UAAU,MAAM,YAAY,YACnC,KAAK,WAAW,eAAe,KAAK,UAAU,IAAI,QAAQ,EAAE,QAAQ,WAAW,CAAC;GAElF;EACF;EACA,KAAK;GACH,KAAK,kBAAkB;IAAE,WAAW,MAAM;IAAW,OAAO,MAAM;GAAM;GACxE;EAEF,KAAK,qBACH,IAAI,KAAK,iBAAiB,UAAU,MAAM,eAAe,KAAK,kBAAkB;CAGpF;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"fold.js","names":[],"sources":["../../src/chat/fold.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatSessionEvent } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage, ChatToolCall } from './types'\n\nfunction eventSeq(event: ChatSessionEvent): number | undefined {\n return 'seq' in event ? event.seq : undefined\n}\n\nexport interface ChatPendingApproval {\n approvalId: string\n toolName: string\n callId?: string\n reason?: string\n rpcId: string\n}\n\n/** 历史回放与实时 onEvent 共用的折叠状态 */\nexport interface ChatFoldState {\n lastSeq: number | null\n messages: ChatMessage[]\n queue: ChatQueuedMessage[]\n jobs: ChatJob[]\n tokenUsage: ChatTokenUsage | null\n projections: Record<string, unknown>\n title: string | null\n running: boolean\n pendingApprovals: ChatPendingApproval[]\n pendingQuestion: { questions: AskQuestionItem[]; rpcId: string } | null\n error: { code: string; message: string } | null\n}\n\nexport function createFoldState(): ChatFoldState {\n return {\n lastSeq: null,\n messages: [],\n queue: [],\n jobs: [],\n tokenUsage: null,\n projections: {},\n title: null,\n running: false,\n pendingApprovals: [],\n pendingQuestion: null,\n error: null\n }\n}\n\nfunction mergeOptionalCount(a?: number, b?: number): number | undefined {\n if (a == null && b == null) return undefined\n return (a ?? 0) + (b ?? 0)\n}\n\nfunction addTokenUsage(a: ChatTokenUsage, b: ChatTokenUsage): ChatTokenUsage {\n const usage: ChatTokenUsage = {\n promptTokens: a.promptTokens + b.promptTokens,\n completionTokens: a.completionTokens + b.completionTokens,\n totalTokens: a.totalTokens + b.totalTokens\n }\n const cacheHitTokens = mergeOptionalCount(a.cacheHitTokens, b.cacheHitTokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n const cacheMissTokens = mergeOptionalCount(a.cacheMissTokens, b.cacheMissTokens)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n const cacheCreationTokens = mergeOptionalCount(a.cacheCreationTokens, b.cacheCreationTokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n return usage\n}\n\nfunction isTokenUsage(value: unknown): value is ChatTokenUsage {\n if (value == null || typeof value !== 'object') return false\n const usage = value as ChatTokenUsage\n return (\n typeof usage.promptTokens === 'number' &&\n typeof usage.completionTokens === 'number' &&\n typeof usage.totalTokens === 'number'\n )\n}\n\nfunction replaceAt<T>(list: T[], idx: number, item: T): T[] {\n const next = list.slice()\n next[idx] = item\n return next\n}\n\nfunction patchMessage(\n messages: ChatMessage[],\n id: string,\n patch: Partial<ChatMessage>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.id === id)\n if (idx < 0) {\n return [...messages, { id, role: 'assistant', content: '', status: 'streaming', ...patch }]\n }\n return replaceAt(messages, idx, { ...messages[idx]!, ...patch })\n}\n\nfunction appendToolCall(messages: ChatMessage[], call: ChatToolCall): ChatMessage[] {\n const idx = messages.findLastIndex((m) => m.role === 'assistant')\n if (idx < 0) {\n return [\n ...messages,\n {\n id: `tool-host:${call.id}`,\n role: 'assistant',\n content: '',\n status: 'streaming',\n toolCalls: [call]\n }\n ]\n }\n const target = messages[idx]!\n return replaceAt(messages, idx, { ...target, toolCalls: [...(target.toolCalls ?? []), call] })\n}\n\nfunction updateToolCall(\n messages: ChatMessage[],\n callId: string,\n patch: Partial<ChatToolCall>\n): ChatMessage[] {\n const idx = messages.findIndex((m) => m.toolCalls?.some((c) => c.id === callId))\n if (idx < 0) return messages\n const msg = messages[idx]!\n const calls = (msg.toolCalls ?? []).slice()\n const callIdx = calls.findIndex((c) => c.id === callId)\n if (callIdx < 0) return messages\n calls[callIdx] = { ...calls[callIdx]!, ...patch }\n return replaceAt(messages, idx, { ...msg, toolCalls: calls })\n}\n\n/**\n * 纯函数 fold:历史回放与实时 onEvent 走同一套。\n * 后到的更小/重复 seq 不覆盖已应用事件。\n */\nexport function foldSessionEvent(state: ChatFoldState, event: ChatSessionEvent): ChatFoldState {\n const seq = eventSeq(event)\n if (seq != null && state.lastSeq != null && seq <= state.lastSeq) return state\n\n const next: ChatFoldState = {\n ...state,\n lastSeq: seq ?? state.lastSeq,\n projections: { ...state.projections }\n }\n\n switch (event.type) {\n case 'user/message': {\n if (next.messages.some((m) => m.id === event.messageId)) break\n const userMessage: ChatMessage = { id: event.messageId, role: 'user', content: event.content }\n if (event.attachments) userMessage.attachments = event.attachments\n next.messages = [...next.messages, userMessage]\n break\n }\n case 'assistant/chunk': {\n const current = next.messages.find((m) => m.id === event.messageId)\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'streaming',\n content: (current?.content ?? '') + event.delta,\n reasoning: event.reasoningDelta\n ? (current?.reasoning ?? '') + event.reasoningDelta\n : current?.reasoning\n })\n break\n }\n case 'assistant/message': {\n const current = next.messages.find((m) => m.id === event.messageId)\n if (event.history && current && current.status === 'streaming') {\n // 历史快照命中直播流式中的消息:快照可能落后(甚至为空),不能把消息\n // 降级为 done 或用更短内容覆盖 —— 否则断线补拉会让消息在「完成 / 思考中」\n // 之间来回翻转;终态由直播的 durable 帧(message_text / step 收尾)落定。\n const longer = (a: string | undefined, b: string | undefined) =>\n (b?.length ?? 0) > (a?.length ?? 0) ? b : a\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n content: longer(current.content, event.content),\n reasoning: longer(current.reasoning, event.reasoning),\n toolCalls: event.toolCalls ?? current.toolCalls\n })\n break\n }\n next.messages = patchMessage(next.messages, event.messageId, {\n role: 'assistant',\n status: 'done',\n content: event.content,\n reasoning: event.reasoning,\n toolCalls: event.toolCalls ?? current?.toolCalls\n })\n break\n }\n case 'tool/call': {\n const call: ChatToolCall = {\n id: event.callId,\n name: event.name,\n arguments: event.arguments,\n status: 'pending'\n }\n if (event.view !== undefined) call.view = event.view\n next.messages = appendToolCall(next.messages, call)\n break\n }\n case 'tool/result': {\n const patch: Partial<ChatToolCall> = { status: event.status }\n if (event.result !== undefined) patch.result = event.result\n if (event.error !== undefined) patch.error = event.error\n if (event.view !== undefined) patch.view = event.view\n next.messages = updateToolCall(next.messages, event.callId, patch)\n break\n }\n case 'queue/snapshot': {\n next.queue = event.items.slice()\n break\n }\n case 'jobs/snapshot': {\n next.jobs = event.jobs.slice()\n break\n }\n case 'projection': {\n if (event.key === 'tokenUsage' && isTokenUsage(event.value)) {\n next.tokenUsage = next.tokenUsage\n ? addTokenUsage(next.tokenUsage, event.value)\n : event.value\n } else if (event.key === 'title' && typeof event.value === 'string') {\n next.title = event.value\n } else {\n next.projections[event.key] = event.value\n }\n break\n }\n case 'running': {\n next.running = event.running\n break\n }\n case 'finish': {\n next.running = false\n break\n }\n case 'error': {\n next.running = false\n next.error = { code: event.code, message: event.message }\n break\n }\n case 'approval/requested': {\n if (event.callId) {\n next.messages = updateToolCall(next.messages, event.callId, { status: 'awaiting-confirm' })\n }\n next.pendingApprovals = [\n ...next.pendingApprovals,\n {\n approvalId: event.approvalId,\n toolName: event.toolName,\n callId: event.callId,\n reason: event.reason,\n rpcId: event.rpcId\n }\n ]\n break\n }\n case 'approval/resolved': {\n const hit = next.pendingApprovals.find((a) => a.approvalId === event.approvalId)\n next.pendingApprovals = next.pendingApprovals.filter((a) => a.approvalId !== event.approvalId)\n if (hit?.callId && event.outcome === 'rejected') {\n next.messages = updateToolCall(next.messages, hit.callId, { status: 'rejected' })\n }\n break\n }\n case 'question/requested': {\n next.pendingQuestion = { questions: event.questions, rpcId: event.rpcId }\n break\n }\n case 'question/resolved': {\n if (next.pendingQuestion?.rpcId === event.questionRpcId) next.pendingQuestion = null\n break\n }\n }\n\n return next\n}\n"],"mappings":"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"}
|
package/dist/chat/session.d.ts
CHANGED
|
@@ -2,32 +2,39 @@ import { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from "./type
|
|
|
2
2
|
import { AskQuestionItem } from "../tools/ask-question/ask-question.js";
|
|
3
3
|
import "../tools/index.js";
|
|
4
4
|
//#region src/chat/session.d.ts
|
|
5
|
-
/**
|
|
5
|
+
/** 归一化后的服务端会话事件(协议无关)。
|
|
6
|
+
* seq 为持久帧序号:瞬态/历史回放事件省略(不参与乱序门),durable 终态事件携带。 */
|
|
6
7
|
export type ChatSessionEvent = {
|
|
7
8
|
type: 'user/message';
|
|
8
9
|
messageId: string;
|
|
9
|
-
seq
|
|
10
|
+
seq?: number;
|
|
10
11
|
content: string;
|
|
11
12
|
attachments?: ChatAttachment[];
|
|
12
13
|
} | {
|
|
13
14
|
type: 'assistant/chunk';
|
|
14
15
|
messageId: string;
|
|
15
|
-
seq
|
|
16
|
+
seq?: number;
|
|
16
17
|
delta: string;
|
|
17
18
|
reasoningDelta?: string;
|
|
18
19
|
} | {
|
|
19
20
|
type: 'assistant/message';
|
|
20
21
|
messageId: string;
|
|
21
|
-
seq
|
|
22
|
+
seq?: number;
|
|
22
23
|
content: string;
|
|
23
24
|
reasoning?: string;
|
|
24
25
|
toolCalls?: ChatToolCall[];
|
|
26
|
+
/**
|
|
27
|
+
* 历史回放(REST 拉取/断线补拉)来源标记。
|
|
28
|
+
* 历史快照可能落后于直播流:fold 对流式中的消息不降级为 done、不用更短的
|
|
29
|
+
* 快照覆盖内容;直播(durable 终态帧)始终是状态权威。
|
|
30
|
+
*/
|
|
31
|
+
history?: boolean;
|
|
25
32
|
} | {
|
|
26
33
|
type: 'tool/call';
|
|
27
34
|
callId: string;
|
|
28
35
|
name: string;
|
|
29
36
|
arguments: string;
|
|
30
|
-
seq
|
|
37
|
+
seq?: number;
|
|
31
38
|
view?: unknown;
|
|
32
39
|
} | {
|
|
33
40
|
type: 'tool/result';
|
|
@@ -35,7 +42,7 @@ export type ChatSessionEvent = {
|
|
|
35
42
|
status: 'success' | 'error' | 'rejected';
|
|
36
43
|
result?: string;
|
|
37
44
|
error?: string;
|
|
38
|
-
seq
|
|
45
|
+
seq?: number;
|
|
39
46
|
view?: unknown;
|
|
40
47
|
} | {
|
|
41
48
|
type: 'approval/requested';
|
package/dist/chat/session.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.js","names":[],"sources":["../../src/chat/session.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from './types'\n\n/**
|
|
1
|
+
{"version":3,"file":"session.js","names":[],"sources":["../../src/chat/session.ts"],"sourcesContent":["import type { AskQuestionItem } from '../tools'\nimport type { ChatAttachment, ChatJob, ChatQueuedMessage, ChatToolCall } from './types'\n\n/** 归一化后的服务端会话事件(协议无关)。\n * seq 为持久帧序号:瞬态/历史回放事件省略(不参与乱序门),durable 终态事件携带。 */\nexport type ChatSessionEvent =\n | {\n type: 'user/message'\n messageId: string\n seq?: number\n content: string\n attachments?: ChatAttachment[]\n }\n | {\n type: 'assistant/chunk'\n messageId: string\n seq?: number\n delta: string\n reasoningDelta?: string\n }\n | {\n type: 'assistant/message'\n messageId: string\n seq?: number\n content: string\n reasoning?: string\n toolCalls?: ChatToolCall[]\n /**\n * 历史回放(REST 拉取/断线补拉)来源标记。\n * 历史快照可能落后于直播流:fold 对流式中的消息不降级为 done、不用更短的\n * 快照覆盖内容;直播(durable 终态帧)始终是状态权威。\n */\n history?: boolean\n }\n | {\n type: 'tool/call'\n callId: string\n name: string\n arguments: string\n seq?: number\n view?: unknown\n }\n | {\n type: 'tool/result'\n callId: string\n status: 'success' | 'error' | 'rejected'\n result?: string\n error?: string\n seq?: number\n view?: unknown\n }\n | {\n type: 'approval/requested'\n approvalId: string\n toolName: string\n callId?: string\n reason?: string\n rpcId: string\n }\n | { type: 'approval/resolved'; approvalId: string; outcome: string }\n | { type: 'question/requested'; questions: AskQuestionItem[]; rpcId: string }\n | { type: 'question/resolved'; questionRpcId: string; outcome: 'answered' | 'cancelled' }\n | { type: 'queue/snapshot'; items: ChatQueuedMessage[] }\n | { type: 'jobs/snapshot'; jobs: ChatJob[] }\n | { type: 'projection'; key: string; value: unknown; seq: number }\n | { type: 'running'; running: boolean }\n | { type: 'finish' }\n | { type: 'error'; code: string; message: string }\n\n/** 服务端会话 transport:对象形态,与函数型 ChatTransport 互斥 */\nexport interface ChatSessionTransport {\n readonly kind: 'session'\n open(handlers: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void }): () => void\n send(content: string, attachments?: ChatAttachment[]): Promise<void>\n cancel(): Promise<void>\n respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>\n fetchHistory(beforeSeq?: number): Promise<{ events: ChatSessionEvent[]; hasMore: boolean }>\n selectModel(provider: string, model: string): Promise<void>\n}\n\n/**\n * 协议翻译层:订阅事件流、把动作打到远端、拉历史。\n * 时序校验 / dispose / 断线补拉 / in-flight 去重由 createServerTransport 完成。\n */\nexport interface ChatSessionAdapter {\n subscribe(handlers: { onEvent(event: ChatSessionEvent): void; onDisconnect?(): void }): () => void\n send(content: string, attachments?: ChatAttachment[]): Promise<void>\n cancel(): Promise<void>\n respond(rpcId: string, ok: boolean, value?: unknown): Promise<void>\n fetchHistory(beforeSeq?: number): Promise<{ events: ChatSessionEvent[]; hasMore: boolean }>\n selectModel(provider: string, model: string): Promise<void>\n}\n\nfunction eventSeq(event: ChatSessionEvent): number | undefined {\n return 'seq' in event ? event.seq : undefined\n}\n\n/** 仅带 kind: 'session' 的对象为 true;函数 / null / 无 kind 均为 false */\nexport function isServerTransport(t: unknown): t is ChatSessionTransport {\n return typeof t === 'object' && t !== null && (t as { kind?: unknown }).kind === 'session'\n}\n\nfunction runExclusive(gate: { busy: boolean }, action: () => Promise<void>): Promise<void> {\n if (gate.busy) return Promise.resolve()\n gate.busy = true\n return action().finally(() => {\n gate.busy = false\n })\n}\n\n/**\n * 用 adapter 包出 ChatSessionTransport。\n * 包内完成 seq 校验、disposer、断线补拉、动作 in-flight 去重。\n */\nexport function createServerTransport(adapter: ChatSessionAdapter): ChatSessionTransport {\n let lastSeq: number | undefined\n const sendGate = { busy: false }\n const cancelGate = { busy: false }\n const respondGate = { busy: false }\n const selectModelGate = { busy: false }\n\n const deliver = (event: ChatSessionEvent, onEvent: (e: ChatSessionEvent) => void) => {\n const seq = eventSeq(event)\n if (seq != null && lastSeq != null && seq <= lastSeq) {\n console.warn(`[ChatSessionTransport] 丢弃乱序事件 seq=${seq} lastSeq=${lastSeq}`)\n return\n }\n if (seq != null) lastSeq = seq\n onEvent(event)\n }\n\n return {\n kind: 'session',\n open(handlers) {\n let disposed = false\n const unsubscribe = adapter.subscribe({\n onEvent(event) {\n if (!disposed) deliver(event, handlers.onEvent)\n },\n onDisconnect() {\n if (disposed) return\n void adapter.fetchHistory(lastSeq).then(\n ({ events }) => {\n if (disposed) return\n for (const event of events) deliver(event, handlers.onEvent)\n handlers.onDisconnect?.()\n },\n () => {\n if (!disposed) handlers.onDisconnect?.()\n }\n )\n }\n })\n return () => {\n disposed = true\n unsubscribe()\n }\n },\n send(content, attachments) {\n return runExclusive(sendGate, () => adapter.send(content, attachments))\n },\n cancel() {\n return runExclusive(cancelGate, () => adapter.cancel())\n },\n respond(rpcId, ok, value) {\n return runExclusive(respondGate, () => adapter.respond(rpcId, ok, value))\n },\n fetchHistory(beforeSeq) {\n return adapter.fetchHistory(beforeSeq)\n },\n selectModel(provider, model) {\n return runExclusive(selectModelGate, () => adapter.selectModel(provider, model))\n }\n }\n}\n"],"mappings":";AA6FA,SAAS,SAAS,OAA6C;CAC7D,OAAO,SAAS,QAAQ,MAAM,MAAM,KAAA;AACtC;;AAGA,SAAgB,kBAAkB,GAAuC;CACvE,OAAO,OAAO,MAAM,YAAY,MAAM,QAAS,EAAyB,SAAS;AACnF;AAEA,SAAS,aAAa,MAAyB,QAA4C;CACzF,IAAI,KAAK,MAAM,OAAO,QAAQ,QAAQ;CACtC,KAAK,OAAO;CACZ,OAAO,OAAO,CAAC,CAAC,cAAc;EAC5B,KAAK,OAAO;CACd,CAAC;AACH;;;;;AAMA,SAAgB,sBAAsB,SAAmD;CACvF,IAAI;CACJ,MAAM,WAAW,EAAE,MAAM,MAAM;CAC/B,MAAM,aAAa,EAAE,MAAM,MAAM;CACjC,MAAM,cAAc,EAAE,MAAM,MAAM;CAClC,MAAM,kBAAkB,EAAE,MAAM,MAAM;CAEtC,MAAM,WAAW,OAAyB,YAA2C;EACnF,MAAM,MAAM,SAAS,KAAK;EAC1B,IAAI,OAAO,QAAQ,WAAW,QAAQ,OAAO,SAAS;GACpD,QAAQ,KAAK,qCAAqC,IAAI,WAAW,SAAS;GAC1E;EACF;EACA,IAAI,OAAO,MAAM,UAAU;EAC3B,QAAQ,KAAK;CACf;CAEA,OAAO;EACL,MAAM;EACN,KAAK,UAAU;GACb,IAAI,WAAW;GACf,MAAM,cAAc,QAAQ,UAAU;IACpC,QAAQ,OAAO;KACb,IAAI,CAAC,UAAU,QAAQ,OAAO,SAAS,OAAO;IAChD;IACA,eAAe;KACb,IAAI,UAAU;KACd,QAAa,aAAa,OAAO,CAAC,CAAC,MAChC,EAAE,aAAa;MACd,IAAI,UAAU;MACd,KAAK,MAAM,SAAS,QAAQ,QAAQ,OAAO,SAAS,OAAO;MAC3D,SAAS,eAAe;KAC1B,SACM;MACJ,IAAI,CAAC,UAAU,SAAS,eAAe;KACzC,CACF;IACF;GACF,CAAC;GACD,aAAa;IACX,WAAW;IACX,YAAY;GACd;EACF;EACA,KAAK,SAAS,aAAa;GACzB,OAAO,aAAa,gBAAgB,QAAQ,KAAK,SAAS,WAAW,CAAC;EACxE;EACA,SAAS;GACP,OAAO,aAAa,kBAAkB,QAAQ,OAAO,CAAC;EACxD;EACA,QAAQ,OAAO,IAAI,OAAO;GACxB,OAAO,aAAa,mBAAmB,QAAQ,QAAQ,OAAO,IAAI,KAAK,CAAC;EAC1E;EACA,aAAa,WAAW;GACtB,OAAO,QAAQ,aAAa,SAAS;EACvC;EACA,YAAY,UAAU,OAAO;GAC3B,OAAO,aAAa,uBAAuB,QAAQ,YAAY,UAAU,KAAK,CAAC;EACjF;CACF;AACF"}
|
|
@@ -19,7 +19,8 @@ export type OpenAITransport = ChatTransport & {
|
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
21
|
* 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
|
|
22
|
-
* 按 request.model 路由到对应 Provider 的 endpoint
|
|
22
|
+
* 按 request.model 路由到对应 Provider 的 endpoint;
|
|
23
|
+
* 请求体构造、fetch、SSE 解析与错误处理复用 `@veltra/utils` 通用层。
|
|
23
24
|
*/
|
|
24
25
|
export declare function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport;
|
|
25
26
|
//#endregion
|
|
@@ -1,171 +1,5 @@
|
|
|
1
|
+
import { buildOpenAIBody, requestOpenAIStream } from "@veltra/utils";
|
|
1
2
|
//#region src/chat/transports/openai.ts
|
|
2
|
-
function toOpenAIMessages(messages, systemPrompt) {
|
|
3
|
-
const result = [];
|
|
4
|
-
if (systemPrompt) result.push({
|
|
5
|
-
role: "system",
|
|
6
|
-
content: systemPrompt
|
|
7
|
-
});
|
|
8
|
-
for (const msg of messages) if (msg.role === "user") {
|
|
9
|
-
if (msg.attachments?.length) {
|
|
10
|
-
const parts = [];
|
|
11
|
-
if (msg.content) parts.push({
|
|
12
|
-
type: "text",
|
|
13
|
-
text: msg.content
|
|
14
|
-
});
|
|
15
|
-
for (const att of msg.attachments) parts.push({
|
|
16
|
-
type: "image_url",
|
|
17
|
-
image_url: { url: att.dataUrl }
|
|
18
|
-
});
|
|
19
|
-
result.push({
|
|
20
|
-
role: "user",
|
|
21
|
-
content: parts
|
|
22
|
-
});
|
|
23
|
-
} else result.push({
|
|
24
|
-
role: "user",
|
|
25
|
-
content: msg.content
|
|
26
|
-
});
|
|
27
|
-
} else if (msg.role === "assistant") {
|
|
28
|
-
const item = {
|
|
29
|
-
role: "assistant",
|
|
30
|
-
content: msg.content || ""
|
|
31
|
-
};
|
|
32
|
-
if (msg.toolCalls?.length) item.tool_calls = msg.toolCalls.map((call) => ({
|
|
33
|
-
id: call.id,
|
|
34
|
-
type: "function",
|
|
35
|
-
function: {
|
|
36
|
-
name: call.name,
|
|
37
|
-
arguments: call.arguments
|
|
38
|
-
}
|
|
39
|
-
}));
|
|
40
|
-
result.push(item);
|
|
41
|
-
} else if (msg.role === "tool") result.push({
|
|
42
|
-
role: "tool",
|
|
43
|
-
tool_call_id: msg.toolCallId,
|
|
44
|
-
content: msg.content
|
|
45
|
-
});
|
|
46
|
-
return result;
|
|
47
|
-
}
|
|
48
|
-
function toOpenAITools(tools) {
|
|
49
|
-
return tools.map((tool) => ({
|
|
50
|
-
type: "function",
|
|
51
|
-
function: {
|
|
52
|
-
name: tool.name,
|
|
53
|
-
description: tool.description,
|
|
54
|
-
parameters: tool.parameters
|
|
55
|
-
}
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
function isRecord(value) {
|
|
59
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
60
|
-
}
|
|
61
|
-
/** 非负有限整数;非法或缺省返回 undefined(不把缺失当成 0) */
|
|
62
|
-
function readCount(value) {
|
|
63
|
-
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) return void 0;
|
|
64
|
-
return Math.floor(value);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* 从 OpenAI 兼容 usage 对象解析。
|
|
68
|
-
* 缓存字段只在源数据出现时写入;未命中可在「有命中 + prompt」时由差值得出。
|
|
69
|
-
*/
|
|
70
|
-
function parseOpenAIUsage(raw) {
|
|
71
|
-
if (!isRecord(raw)) return null;
|
|
72
|
-
const promptTokens = readCount(raw.prompt_tokens) ?? readCount(raw.input_tokens);
|
|
73
|
-
const completionTokens = readCount(raw.completion_tokens) ?? readCount(raw.output_tokens);
|
|
74
|
-
if (promptTokens == null && completionTokens == null) return null;
|
|
75
|
-
const prompt = promptTokens ?? 0;
|
|
76
|
-
const completion = completionTokens ?? 0;
|
|
77
|
-
const usage = {
|
|
78
|
-
promptTokens: prompt,
|
|
79
|
-
completionTokens: completion,
|
|
80
|
-
totalTokens: readCount(raw.total_tokens) ?? prompt + completion
|
|
81
|
-
};
|
|
82
|
-
const details = isRecord(raw.prompt_tokens_details) ? raw.prompt_tokens_details : void 0;
|
|
83
|
-
const inputDetails = isRecord(raw.input_tokens_details) ? raw.input_tokens_details : void 0;
|
|
84
|
-
const cacheHitTokens = readCount(details?.cached_tokens) ?? readCount(inputDetails?.cached_tokens) ?? readCount(raw.prompt_cache_hit_tokens) ?? readCount(raw.cache_read_input_tokens) ?? readCount(raw.cached_tokens);
|
|
85
|
-
if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens;
|
|
86
|
-
const cacheMissTokens = readCount(raw.prompt_cache_miss_tokens) ?? (cacheHitTokens != null && promptTokens != null ? Math.max(0, promptTokens - cacheHitTokens) : void 0);
|
|
87
|
-
if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens;
|
|
88
|
-
const cacheCreationTokens = readCount(raw.cache_creation_input_tokens);
|
|
89
|
-
if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens;
|
|
90
|
-
return usage;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* 逐行解析 SSE 流。
|
|
94
|
-
* 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。
|
|
95
|
-
*/
|
|
96
|
-
async function parseSSE(body, signal, handlers) {
|
|
97
|
-
const reader = body.getReader();
|
|
98
|
-
const decoder = new TextDecoder();
|
|
99
|
-
let buffer = "";
|
|
100
|
-
const accumulated = /* @__PURE__ */ new Map();
|
|
101
|
-
let lastIndex = -1;
|
|
102
|
-
const flushToolCalls = () => {
|
|
103
|
-
if (!handlers.onToolCall) return;
|
|
104
|
-
for (const call of accumulated.values()) if (call.id && call.name) handlers.onToolCall({
|
|
105
|
-
id: call.id,
|
|
106
|
-
name: call.name,
|
|
107
|
-
arguments: call.arguments
|
|
108
|
-
});
|
|
109
|
-
accumulated.clear();
|
|
110
|
-
};
|
|
111
|
-
const handleData = (data) => {
|
|
112
|
-
if (data === "[DONE]") return;
|
|
113
|
-
let chunk;
|
|
114
|
-
try {
|
|
115
|
-
chunk = JSON.parse(data);
|
|
116
|
-
} catch {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (!isRecord(chunk)) return;
|
|
120
|
-
const usage = parseOpenAIUsage(chunk.usage);
|
|
121
|
-
if (usage) handlers.onUsage?.(usage);
|
|
122
|
-
const choices = chunk.choices;
|
|
123
|
-
const first = Array.isArray(choices) ? choices[0] : void 0;
|
|
124
|
-
const delta = isRecord(first) ? first.delta : void 0;
|
|
125
|
-
if (!isRecord(delta)) return;
|
|
126
|
-
if (typeof delta.content === "string" && delta.content) handlers.onTextDelta(delta.content);
|
|
127
|
-
const reasoning = delta.reasoning_content ?? delta.reasoning;
|
|
128
|
-
if (typeof reasoning === "string" && reasoning) handlers.onReasoningDelta?.(reasoning);
|
|
129
|
-
if (Array.isArray(delta.tool_calls)) for (const tc of delta.tool_calls) {
|
|
130
|
-
if (!isRecord(tc)) continue;
|
|
131
|
-
const index = typeof tc.index === "number" ? tc.index : 0;
|
|
132
|
-
if (index > lastIndex && accumulated.size > 0) flushToolCalls();
|
|
133
|
-
lastIndex = Math.max(lastIndex, index);
|
|
134
|
-
let call = accumulated.get(index);
|
|
135
|
-
if (!call) {
|
|
136
|
-
call = {
|
|
137
|
-
id: "",
|
|
138
|
-
name: "",
|
|
139
|
-
arguments: ""
|
|
140
|
-
};
|
|
141
|
-
accumulated.set(index, call);
|
|
142
|
-
}
|
|
143
|
-
if (typeof tc.id === "string") call.id = tc.id;
|
|
144
|
-
const fn = isRecord(tc.function) ? tc.function : void 0;
|
|
145
|
-
if (typeof fn?.name === "string") call.name += fn.name;
|
|
146
|
-
if (typeof fn?.arguments === "string") call.arguments += fn.arguments;
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
try {
|
|
150
|
-
const pump = async () => {
|
|
151
|
-
const { done, value } = await reader.read();
|
|
152
|
-
if (done || signal.aborted) return;
|
|
153
|
-
buffer += decoder.decode(value, { stream: true });
|
|
154
|
-
let newlineIndex;
|
|
155
|
-
while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
|
|
156
|
-
const line = buffer.slice(0, newlineIndex).trim();
|
|
157
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
158
|
-
if (line.startsWith("data:")) handleData(line.slice(5).trim());
|
|
159
|
-
}
|
|
160
|
-
return pump();
|
|
161
|
-
};
|
|
162
|
-
await pump();
|
|
163
|
-
if (buffer.trim().startsWith("data:")) handleData(buffer.trim().slice(5).trim());
|
|
164
|
-
} finally {
|
|
165
|
-
reader.releaseLock();
|
|
166
|
-
}
|
|
167
|
-
flushToolCalls();
|
|
168
|
-
}
|
|
169
3
|
/** 校验 providers 并展开扁平模型列表 */
|
|
170
4
|
function flattenModels(providers) {
|
|
171
5
|
if (!providers.length) throw new Error("[createOpenAITransport] providers 不能为空");
|
|
@@ -188,13 +22,10 @@ function flattenModels(providers) {
|
|
|
188
22
|
byModelId
|
|
189
23
|
};
|
|
190
24
|
}
|
|
191
|
-
/** 缺省:写入 OpenAI 兼容的 reasoning_effort */
|
|
192
|
-
function defaultApplyReasoning(level, body) {
|
|
193
|
-
body.reasoning_effort = level;
|
|
194
|
-
}
|
|
195
25
|
/**
|
|
196
26
|
* 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。
|
|
197
|
-
* 按 request.model 路由到对应 Provider 的 endpoint
|
|
27
|
+
* 按 request.model 路由到对应 Provider 的 endpoint;
|
|
28
|
+
* 请求体构造、fetch、SSE 解析与错误处理复用 `@veltra/utils` 通用层。
|
|
198
29
|
*/
|
|
199
30
|
function createOpenAITransport(options) {
|
|
200
31
|
const { providers, headers: globalHeaders, body: globalBody } = options;
|
|
@@ -208,50 +39,26 @@ function createOpenAITransport(options) {
|
|
|
208
39
|
handlers.onError?.(/* @__PURE__ */ new Error(`未找到模型 "${modelId}" 对应的 Provider`));
|
|
209
40
|
return;
|
|
210
41
|
}
|
|
211
|
-
const
|
|
212
|
-
const requestBody = {
|
|
213
|
-
...globalBody,
|
|
42
|
+
const requestBody = buildOpenAIBody({
|
|
214
43
|
model: modelId,
|
|
44
|
+
messages,
|
|
45
|
+
systemPrompt,
|
|
46
|
+
tools,
|
|
215
47
|
stream: true,
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
48
|
+
reasoningLevel,
|
|
49
|
+
applyReasoning: provider.applyReasoning,
|
|
50
|
+
extraBody: globalBody
|
|
51
|
+
});
|
|
52
|
+
await requestOpenAIStream({
|
|
53
|
+
url: provider.endpoint,
|
|
54
|
+
apiKey: provider.apiKey,
|
|
55
|
+
headers: {
|
|
56
|
+
...globalHeaders,
|
|
57
|
+
...provider.headers
|
|
219
58
|
},
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
};
|
|
223
|
-
if (reasoningLevel) (provider.applyReasoning ?? defaultApplyReasoning)(reasoningLevel, requestBody);
|
|
224
|
-
let response;
|
|
225
|
-
try {
|
|
226
|
-
response = await fetch(provider.endpoint, {
|
|
227
|
-
method: "POST",
|
|
228
|
-
signal,
|
|
229
|
-
headers: {
|
|
230
|
-
"Content-Type": "application/json",
|
|
231
|
-
...provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {},
|
|
232
|
-
...globalHeaders,
|
|
233
|
-
...provider.headers
|
|
234
|
-
},
|
|
235
|
-
body: JSON.stringify(requestBody)
|
|
236
|
-
});
|
|
237
|
-
} catch (error) {
|
|
238
|
-
if (!signal.aborted) handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
if (!response.ok) {
|
|
242
|
-
const text = await response.text().catch(() => "");
|
|
243
|
-
handlers.onError?.(/* @__PURE__ */ new Error(`请求失败(${response.status}):${text || response.statusText}`));
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
if (!response.body) {
|
|
247
|
-
handlers.onError?.(/* @__PURE__ */ new Error("响应不包含可读取的流"));
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
try {
|
|
251
|
-
await parseSSE(response.body, signal, handlers);
|
|
252
|
-
} catch (error) {
|
|
253
|
-
if (!signal.aborted) handlers.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
254
|
-
}
|
|
59
|
+
body: requestBody,
|
|
60
|
+
signal
|
|
61
|
+
}, handlers);
|
|
255
62
|
};
|
|
256
63
|
return Object.assign(send, {
|
|
257
64
|
models,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import type { ChatModelOption, ChatProvider } from '../../providers'\nimport type {\n ChatMessage,\n ChatTokenUsage,\n ChatTool,\n ChatTransport,\n ChatTransportHandlers,\n ChatTransportRequest\n} from '../types'\n\n/** createOpenAITransport 的配置项(多 Provider) */\nexport interface OpenAITransportOptions {\n /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */\n providers: ChatProvider[]\n /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */\n headers?: Record<string, string>\n /** 全局额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\n}\n\n/** 带扁平模型列表元数据的 OpenAI 兼容 transport */\nexport type OpenAITransport = ChatTransport & {\n /** 供 UI 使用的扁平模型列表 */\n readonly models: ChatModelOption[]\n /** 默认模型 id(首个 Provider 的首个模型) */\n readonly defaultModel: string\n}\n\ninterface OpenAIMessage {\n role: string\n content?: unknown\n tool_calls?: unknown[]\n tool_call_id?: string\n}\n\nfunction toOpenAIMessages(messages: ChatMessage[], systemPrompt?: string): OpenAIMessage[] {\n const result: OpenAIMessage[] = []\n\n if (systemPrompt) {\n result.push({ role: 'system', content: systemPrompt })\n }\n\n for (const msg of messages) {\n if (msg.role === 'user') {\n if (msg.attachments?.length) {\n const parts: unknown[] = []\n if (msg.content) parts.push({ type: 'text', text: msg.content })\n for (const att of msg.attachments) {\n parts.push({ type: 'image_url', image_url: { url: att.dataUrl } })\n }\n result.push({ role: 'user', content: parts })\n } else {\n result.push({ role: 'user', content: msg.content })\n }\n } else if (msg.role === 'assistant') {\n const item: OpenAIMessage = { role: 'assistant', content: msg.content || '' }\n if (msg.toolCalls?.length) {\n item.tool_calls = msg.toolCalls.map((call) => ({\n id: call.id,\n type: 'function',\n function: { name: call.name, arguments: call.arguments }\n }))\n }\n result.push(item)\n } else if (msg.role === 'tool') {\n result.push({ role: 'tool', tool_call_id: msg.toolCallId, content: msg.content })\n }\n }\n\n return result\n}\n\nfunction toOpenAITools(tools: ChatTool[]): unknown[] {\n return tools.map((tool) => ({\n type: 'function',\n function: { name: tool.name, description: tool.description, parameters: tool.parameters }\n }))\n}\n\ninterface AccumulatedToolCall {\n id: string\n name: string\n arguments: string\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null && !Array.isArray(value)\n}\n\n/** 非负有限整数;非法或缺省返回 undefined(不把缺失当成 0) */\nfunction readCount(value: unknown): number | undefined {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) return undefined\n return Math.floor(value)\n}\n\n/**\n * 从 OpenAI 兼容 usage 对象解析。\n * 缓存字段只在源数据出现时写入;未命中可在「有命中 + prompt」时由差值得出。\n */\nfunction parseOpenAIUsage(raw: unknown): ChatTokenUsage | null {\n if (!isRecord(raw)) return null\n\n const promptTokens = readCount(raw.prompt_tokens) ?? readCount(raw.input_tokens)\n const completionTokens = readCount(raw.completion_tokens) ?? readCount(raw.output_tokens)\n if (promptTokens == null && completionTokens == null) return null\n\n const prompt = promptTokens ?? 0\n const completion = completionTokens ?? 0\n const usage: ChatTokenUsage = {\n promptTokens: prompt,\n completionTokens: completion,\n totalTokens: readCount(raw.total_tokens) ?? prompt + completion\n }\n\n const details = isRecord(raw.prompt_tokens_details) ? raw.prompt_tokens_details : undefined\n const inputDetails = isRecord(raw.input_tokens_details) ? raw.input_tokens_details : undefined\n const cacheHitTokens =\n readCount(details?.cached_tokens) ??\n readCount(inputDetails?.cached_tokens) ??\n readCount(raw.prompt_cache_hit_tokens) ??\n readCount(raw.cache_read_input_tokens) ??\n readCount(raw.cached_tokens)\n if (cacheHitTokens != null) usage.cacheHitTokens = cacheHitTokens\n\n const cacheMissTokens =\n readCount(raw.prompt_cache_miss_tokens) ??\n (cacheHitTokens != null && promptTokens != null\n ? Math.max(0, promptTokens - cacheHitTokens)\n : undefined)\n if (cacheMissTokens != null) usage.cacheMissTokens = cacheMissTokens\n\n const cacheCreationTokens = readCount(raw.cache_creation_input_tokens)\n if (cacheCreationTokens != null) usage.cacheCreationTokens = cacheCreationTokens\n\n return usage\n}\n\n/**\n * 逐行解析 SSE 流。\n * 兼容 OpenAI 及 DeepSeek 等 reasoning_content 风格的兼容端点。\n */\nasync function parseSSE(\n body: ReadableStream<Uint8Array>,\n signal: AbortSignal,\n handlers: ChatTransportHandlers\n): Promise<void> {\n const reader = body.getReader()\n const decoder = new TextDecoder()\n let buffer = ''\n\n // 按 OpenAI 流式协议中 tool_calls 的 index 累积参数片段\n const accumulated = new Map<number, AccumulatedToolCall>()\n let lastIndex = -1\n\n const flushToolCalls = () => {\n if (!handlers.onToolCall) return\n for (const call of accumulated.values()) {\n if (call.id && call.name) {\n handlers.onToolCall({ id: call.id, name: call.name, arguments: call.arguments })\n }\n }\n accumulated.clear()\n }\n\n const handleData = (data: string) => {\n if (data === '[DONE]') return\n\n let chunk: unknown\n try {\n chunk = JSON.parse(data)\n } catch {\n return\n }\n if (!isRecord(chunk)) return\n\n const usage = parseOpenAIUsage(chunk.usage)\n if (usage) handlers.onUsage?.(usage)\n\n const choices = chunk.choices\n const first = Array.isArray(choices) ? choices[0] : undefined\n const delta = isRecord(first) ? first.delta : undefined\n if (!isRecord(delta)) return\n\n if (typeof delta.content === 'string' && delta.content) {\n handlers.onTextDelta(delta.content)\n }\n\n // 兼容 reasoning_content(DeepSeek)与 reasoning(部分 OpenAI 兼容端点)\n const reasoning = delta.reasoning_content ?? delta.reasoning\n if (typeof reasoning === 'string' && reasoning) {\n handlers.onReasoningDelta?.(reasoning)\n }\n\n if (Array.isArray(delta.tool_calls)) {\n for (const tc of delta.tool_calls) {\n if (!isRecord(tc)) continue\n const index = typeof tc.index === 'number' ? tc.index : 0\n // 同一调用的参数分片共享 index;index 变大才说明前一个调用已完整,先抛出去\n if (index > lastIndex && accumulated.size > 0) flushToolCalls()\n lastIndex = Math.max(lastIndex, index)\n\n let call = accumulated.get(index)\n if (!call) {\n call = { id: '', name: '', arguments: '' }\n accumulated.set(index, call)\n }\n if (typeof tc.id === 'string') call.id = tc.id\n const fn = isRecord(tc.function) ? tc.function : undefined\n if (typeof fn?.name === 'string') call.name += fn.name\n if (typeof fn?.arguments === 'string') call.arguments += fn.arguments\n }\n }\n }\n\n try {\n // 递归泵取代替 while 循环,避免 await-in-loop\n const pump = async (): Promise<void> => {\n const { done, value } = await reader.read()\n if (done || signal.aborted) return\n\n buffer += decoder.decode(value, { stream: true })\n\n let newlineIndex: number\n while ((newlineIndex = buffer.indexOf('\\n')) !== -1) {\n const line = buffer.slice(0, newlineIndex).trim()\n buffer = buffer.slice(newlineIndex + 1)\n if (line.startsWith('data:')) {\n handleData(line.slice(5).trim())\n }\n }\n\n return pump()\n }\n\n await pump()\n\n if (buffer.trim().startsWith('data:')) {\n handleData(buffer.trim().slice(5).trim())\n }\n } finally {\n reader.releaseLock()\n }\n\n flushToolCalls()\n}\n\n/** 校验 providers 并展开扁平模型列表 */\nfunction flattenModels(providers: ChatProvider[]): {\n models: ChatModelOption[]\n byModelId: Map<string, ChatProvider>\n} {\n if (!providers.length) {\n throw new Error('[createOpenAITransport] providers 不能为空')\n }\n\n const models: ChatModelOption[] = []\n const byModelId = new Map<string, ChatProvider>()\n\n for (const provider of providers) {\n if (!provider.models?.length) {\n throw new Error(`[createOpenAITransport] Provider \"${provider.id}\" 未配置 models`)\n }\n for (const model of provider.models) {\n if (byModelId.has(model.id)) {\n throw new Error(\n `[createOpenAITransport] 模型 id \"${model.id}\" 重复(跨 Provider 须全局唯一)`\n )\n }\n byModelId.set(model.id, provider)\n models.push({ ...model, providerId: provider.id, providerLabel: provider.label })\n }\n }\n\n return { models, byModelId }\n}\n\n/** 缺省:写入 OpenAI 兼容的 reasoning_effort */\nfunction defaultApplyReasoning(level: string, body: Record<string, unknown>) {\n body.reasoning_effort = level\n}\n\n/**\n * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。\n * 按 request.model 路由到对应 Provider 的 endpoint;手写 fetch + SSE,无第三方依赖。\n */\nexport function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport {\n const { providers, headers: globalHeaders, body: globalBody } = options\n const { models, byModelId } = flattenModels(providers)\n const defaultModel = models[0]!.id\n\n const send: ChatTransport = async (\n request: ChatTransportRequest,\n handlers: ChatTransportHandlers\n ) => {\n const { messages, systemPrompt, tools, signal, reasoningLevel } = request\n const modelId = request.model ?? defaultModel\n const provider = byModelId.get(modelId)\n\n if (!provider) {\n handlers.onError?.(new Error(`未找到模型 \"${modelId}\" 对应的 Provider`))\n return\n }\n\n const extraStreamOptions = isRecord(globalBody?.stream_options)\n ? globalBody.stream_options\n : undefined\n const requestBody: Record<string, unknown> = {\n ...globalBody,\n model: modelId,\n stream: true,\n // 流式默认要末包 usage;OpenAI / DeepSeek 等兼容端点靠这个字段才会返回\n stream_options: { ...extraStreamOptions, include_usage: true },\n messages: toOpenAIMessages(messages, systemPrompt),\n ...(tools?.length ? { tools: toOpenAITools(tools) } : {})\n }\n\n if (reasoningLevel) {\n const apply = provider.applyReasoning ?? defaultApplyReasoning\n apply(reasoningLevel, requestBody)\n }\n\n let response: Response\n try {\n response = await fetch(provider.endpoint, {\n method: 'POST',\n signal,\n headers: {\n 'Content-Type': 'application/json',\n ...(provider.apiKey ? { Authorization: `Bearer ${provider.apiKey}` } : {}),\n ...globalHeaders,\n ...provider.headers\n },\n body: JSON.stringify(requestBody)\n })\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n return\n }\n\n if (!response.ok) {\n const text = await response.text().catch(() => '')\n handlers.onError?.(\n new Error(`请求失败(${response.status}):${text || response.statusText}`)\n )\n return\n }\n\n if (!response.body) {\n handlers.onError?.(new Error('响应不包含可读取的流'))\n return\n }\n\n try {\n await parseSSE(response.body, signal, handlers)\n } catch (error) {\n if (!signal.aborted) {\n handlers.onError?.(error instanceof Error ? error : new Error(String(error)))\n }\n }\n }\n\n return Object.assign(send, { models, defaultModel })\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"openai.js","names":[],"sources":["../../../src/chat/transports/openai.ts"],"sourcesContent":["import { buildOpenAIBody, requestOpenAIStream } from '@veltra/utils'\n\nimport type { ChatModelOption, ChatProvider } from '../../providers'\nimport type { ChatTransport, ChatTransportHandlers, ChatTransportRequest } from '../types'\n\n/** createOpenAITransport 的配置项(多 Provider) */\nexport interface OpenAITransportOptions {\n /** 至少一个 Provider;模型 id 须跨 Provider 全局唯一 */\n providers: ChatProvider[]\n /** 全局额外请求头(与各 Provider headers 合并,Provider 优先) */\n headers?: Record<string, string>\n /** 全局额外请求体字段,如 temperature、top_p */\n body?: Record<string, unknown>\n}\n\n/** 带扁平模型列表元数据的 OpenAI 兼容 transport */\nexport type OpenAITransport = ChatTransport & {\n /** 供 UI 使用的扁平模型列表 */\n readonly models: ChatModelOption[]\n /** 默认模型 id(首个 Provider 的首个模型) */\n readonly defaultModel: string\n}\n\n/** 校验 providers 并展开扁平模型列表 */\nfunction flattenModels(providers: ChatProvider[]): {\n models: ChatModelOption[]\n byModelId: Map<string, ChatProvider>\n} {\n if (!providers.length) {\n throw new Error('[createOpenAITransport] providers 不能为空')\n }\n\n const models: ChatModelOption[] = []\n const byModelId = new Map<string, ChatProvider>()\n\n for (const provider of providers) {\n if (!provider.models?.length) {\n throw new Error(`[createOpenAITransport] Provider \"${provider.id}\" 未配置 models`)\n }\n for (const model of provider.models) {\n if (byModelId.has(model.id)) {\n throw new Error(\n `[createOpenAITransport] 模型 id \"${model.id}\" 重复(跨 Provider 须全局唯一)`\n )\n }\n byModelId.set(model.id, provider)\n models.push({ ...model, providerId: provider.id, providerLabel: provider.label })\n }\n }\n\n return { models, byModelId }\n}\n\n/**\n * 创建 OpenAI 兼容协议的传输层(chat/completions SSE)。\n * 按 request.model 路由到对应 Provider 的 endpoint;\n * 请求体构造、fetch、SSE 解析与错误处理复用 `@veltra/utils` 通用层。\n */\nexport function createOpenAITransport(options: OpenAITransportOptions): OpenAITransport {\n const { providers, headers: globalHeaders, body: globalBody } = options\n const { models, byModelId } = flattenModels(providers)\n const defaultModel = models[0]!.id\n\n const send: ChatTransport = async (\n request: ChatTransportRequest,\n handlers: ChatTransportHandlers\n ) => {\n const { messages, systemPrompt, tools, signal, reasoningLevel } = request\n const modelId = request.model ?? defaultModel\n const provider = byModelId.get(modelId)\n\n if (!provider) {\n handlers.onError?.(new Error(`未找到模型 \"${modelId}\" 对应的 Provider`))\n return\n }\n\n const requestBody = buildOpenAIBody({\n model: modelId,\n messages,\n systemPrompt,\n tools,\n stream: true,\n reasoningLevel,\n applyReasoning: provider.applyReasoning,\n extraBody: globalBody\n })\n\n // fetch + SSE 解析 + 错误处理均由通用层完成;错误经 onError 报告,signal 中断静默\n await requestOpenAIStream(\n {\n url: provider.endpoint,\n apiKey: provider.apiKey,\n headers: { ...globalHeaders, ...provider.headers },\n body: requestBody,\n signal\n },\n handlers\n )\n }\n\n return Object.assign(send, { models, defaultModel })\n}\n"],"mappings":";;;AAwBA,SAAS,cAAc,WAGrB;CACA,IAAI,CAAC,UAAU,QACb,MAAM,IAAI,MAAM,wCAAwC;CAG1D,MAAM,SAA4B,CAAC;CACnC,MAAM,4BAAY,IAAI,IAA0B;CAEhD,KAAK,MAAM,YAAY,WAAW;EAChC,IAAI,CAAC,SAAS,QAAQ,QACpB,MAAM,IAAI,MAAM,qCAAqC,SAAS,GAAG,aAAa;EAEhF,KAAK,MAAM,SAAS,SAAS,QAAQ;GACnC,IAAI,UAAU,IAAI,MAAM,EAAE,GACxB,MAAM,IAAI,MACR,kCAAkC,MAAM,GAAG,uBAC7C;GAEF,UAAU,IAAI,MAAM,IAAI,QAAQ;GAChC,OAAO,KAAK;IAAE,GAAG;IAAO,YAAY,SAAS;IAAI,eAAe,SAAS;GAAM,CAAC;EAClF;CACF;CAEA,OAAO;EAAE;EAAQ;CAAU;AAC7B;;;;;;AAOA,SAAgB,sBAAsB,SAAkD;CACtF,MAAM,EAAE,WAAW,SAAS,eAAe,MAAM,eAAe;CAChE,MAAM,EAAE,QAAQ,cAAc,cAAc,SAAS;CACrD,MAAM,eAAe,OAAO,EAAE,CAAE;CAEhC,MAAM,OAAsB,OAC1B,SACA,aACG;EACH,MAAM,EAAE,UAAU,cAAc,OAAO,QAAQ,mBAAmB;EAClE,MAAM,UAAU,QAAQ,SAAS;EACjC,MAAM,WAAW,UAAU,IAAI,OAAO;EAEtC,IAAI,CAAC,UAAU;GACb,SAAS,0BAAU,IAAI,MAAM,UAAU,QAAQ,eAAe,CAAC;GAC/D;EACF;EAEA,MAAM,cAAc,gBAAgB;GAClC,OAAO;GACP;GACA;GACA;GACA,QAAQ;GACR;GACA,gBAAgB,SAAS;GACzB,WAAW;EACb,CAAC;EAGD,MAAM,oBACJ;GACE,KAAK,SAAS;GACd,QAAQ,SAAS;GACjB,SAAS;IAAE,GAAG;IAAe,GAAG,SAAS;GAAQ;GACjD,MAAM;GACN;EACF,GACA,QACF;CACF;CAEA,OAAO,OAAO,OAAO,MAAM;EAAE;EAAQ;CAAa,CAAC;AACrD"}
|
|
@@ -32,19 +32,9 @@ function createSessionRuntime(surface) {
|
|
|
32
32
|
fold = createFoldState();
|
|
33
33
|
syncFold(fold, surface);
|
|
34
34
|
let disposed = false;
|
|
35
|
-
const dispose = transport.open({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
onDisconnect() {
|
|
40
|
-
if (disposed) return;
|
|
41
|
-
fold = {
|
|
42
|
-
...fold,
|
|
43
|
-
running: false
|
|
44
|
-
};
|
|
45
|
-
surface.running.value = false;
|
|
46
|
-
}
|
|
47
|
-
});
|
|
35
|
+
const dispose = transport.open({ onEvent(event) {
|
|
36
|
+
if (!disposed) apply(event);
|
|
37
|
+
} });
|
|
48
38
|
opened = true;
|
|
49
39
|
onCleanup(() => {
|
|
50
40
|
disposed = true;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-chat-session.js","names":[],"sources":["../../src/chat/use-chat-session.ts"],"sourcesContent":["import type { Ref } from 'vue'\n\nimport type { AskQuestionItem } from '../tools'\nimport {\n createFoldState,\n foldSessionEvent,\n type ChatFoldState,\n type ChatPendingApproval\n} from './fold'\nimport type { ChatSessionEvent, ChatSessionTransport } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from './types'\n\nexport interface SessionChatSurface {\n messages: Ref<ChatMessage[]>\n queue: Ref<ChatQueuedMessage[]>\n jobs: Ref<ChatJob[]>\n tokenUsage: Ref<ChatTokenUsage | null>\n projections: Ref<Record<string, unknown>>\n title: Ref<string | null>\n running: Ref<boolean>\n pendingApprovals: Ref<ChatPendingApproval[]>\n pendingQuestion: Ref<{ questions: AskQuestionItem[]; rpcId: string } | null>\n}\n\n/** 把 fold 状态同步到 useChat 的渲染面 */\nfunction syncFold(fold: ChatFoldState, surface: SessionChatSurface) {\n surface.messages.value = fold.messages\n surface.queue.value = fold.queue\n surface.jobs.value = fold.jobs\n surface.tokenUsage.value = fold.tokenUsage\n surface.projections.value = fold.projections\n surface.title.value = fold.title\n surface.running.value = fold.running\n surface.pendingApprovals.value = fold.pendingApprovals\n surface.pendingQuestion.value = fold.pendingQuestion\n}\n\n/**\n * session transport 的 open / fetchHistory / fold。\n * 历史回放与实时 onEvent 走同一套 fold。\n */\nexport function createSessionRuntime(surface: SessionChatSurface) {\n let fold = createFoldState()\n let opened = false\n\n const apply = (event: ChatSessionEvent) => {\n fold = foldSessionEvent(fold, event)\n syncFold(fold, surface)\n }\n\n return {\n get opened() {\n return opened\n },\n attach(transport: ChatSessionTransport, onCleanup: (fn: () => void) => void) {\n // isServerTransport 只认 kind,不保证 open 存在\n if (typeof transport.open !== 'function') return\n fold = createFoldState()\n syncFold(fold, surface)\n let disposed = false\n const dispose = transport.open({\n onEvent(event) {\n if (!disposed) apply(event)\n }
|
|
1
|
+
{"version":3,"file":"use-chat-session.js","names":[],"sources":["../../src/chat/use-chat-session.ts"],"sourcesContent":["import type { Ref } from 'vue'\n\nimport type { AskQuestionItem } from '../tools'\nimport {\n createFoldState,\n foldSessionEvent,\n type ChatFoldState,\n type ChatPendingApproval\n} from './fold'\nimport type { ChatSessionEvent, ChatSessionTransport } from './session'\nimport type { ChatJob, ChatMessage, ChatQueuedMessage, ChatTokenUsage } from './types'\n\nexport interface SessionChatSurface {\n messages: Ref<ChatMessage[]>\n queue: Ref<ChatQueuedMessage[]>\n jobs: Ref<ChatJob[]>\n tokenUsage: Ref<ChatTokenUsage | null>\n projections: Ref<Record<string, unknown>>\n title: Ref<string | null>\n running: Ref<boolean>\n pendingApprovals: Ref<ChatPendingApproval[]>\n pendingQuestion: Ref<{ questions: AskQuestionItem[]; rpcId: string } | null>\n}\n\n/** 把 fold 状态同步到 useChat 的渲染面 */\nfunction syncFold(fold: ChatFoldState, surface: SessionChatSurface) {\n surface.messages.value = fold.messages\n surface.queue.value = fold.queue\n surface.jobs.value = fold.jobs\n surface.tokenUsage.value = fold.tokenUsage\n surface.projections.value = fold.projections\n surface.title.value = fold.title\n surface.running.value = fold.running\n surface.pendingApprovals.value = fold.pendingApprovals\n surface.pendingQuestion.value = fold.pendingQuestion\n}\n\n/**\n * session transport 的 open / fetchHistory / fold。\n * 历史回放与实时 onEvent 走同一套 fold。\n */\nexport function createSessionRuntime(surface: SessionChatSurface) {\n let fold = createFoldState()\n let opened = false\n\n const apply = (event: ChatSessionEvent) => {\n fold = foldSessionEvent(fold, event)\n syncFold(fold, surface)\n }\n\n return {\n get opened() {\n return opened\n },\n attach(transport: ChatSessionTransport, onCleanup: (fn: () => void) => void) {\n // isServerTransport 只认 kind,不保证 open 存在\n if (typeof transport.open !== 'function') return\n fold = createFoldState()\n syncFold(fold, surface)\n let disposed = false\n // 断线 ≠ 会话停止:不注册 onDisconnect 把 running 置 false(保持乐观,由\n // 重连回放的 prompted / step_started / idle 帧收敛)。曾在此置 false ——\n // 步骤间隙断线会把过程块折叠成「已完成」,重连随即又展开,消息列表在\n // 「完成 / 思考中」之间来回跳。\n const dispose = transport.open({\n onEvent(event) {\n if (!disposed) apply(event)\n }\n })\n opened = true\n onCleanup(() => {\n disposed = true\n opened = false\n dispose()\n })\n void transport.fetchHistory().then(({ events }) => {\n if (disposed) return\n for (const event of events) apply(event)\n })\n },\n resetLocal() {\n fold = createFoldState()\n syncFold(fold, surface)\n }\n }\n}\n"],"mappings":";;;AAyBA,SAAS,SAAS,MAAqB,SAA6B;CAClE,QAAQ,SAAS,QAAQ,KAAK;CAC9B,QAAQ,MAAM,QAAQ,KAAK;CAC3B,QAAQ,KAAK,QAAQ,KAAK;CAC1B,QAAQ,WAAW,QAAQ,KAAK;CAChC,QAAQ,YAAY,QAAQ,KAAK;CACjC,QAAQ,MAAM,QAAQ,KAAK;CAC3B,QAAQ,QAAQ,QAAQ,KAAK;CAC7B,QAAQ,iBAAiB,QAAQ,KAAK;CACtC,QAAQ,gBAAgB,QAAQ,KAAK;AACvC;;;;;AAMA,SAAgB,qBAAqB,SAA6B;CAChE,IAAI,OAAO,gBAAgB;CAC3B,IAAI,SAAS;CAEb,MAAM,SAAS,UAA4B;EACzC,OAAO,iBAAiB,MAAM,KAAK;EACnC,SAAS,MAAM,OAAO;CACxB;CAEA,OAAO;EACL,IAAI,SAAS;GACX,OAAO;EACT;EACA,OAAO,WAAiC,WAAqC;GAE3E,IAAI,OAAO,UAAU,SAAS,YAAY;GAC1C,OAAO,gBAAgB;GACvB,SAAS,MAAM,OAAO;GACtB,IAAI,WAAW;GAKf,MAAM,UAAU,UAAU,KAAK,EAC7B,QAAQ,OAAO;IACb,IAAI,CAAC,UAAU,MAAM,KAAK;GAC5B,EACF,CAAC;GACD,SAAS;GACT,gBAAgB;IACd,WAAW;IACX,SAAS;IACT,QAAQ;GACV,CAAC;GACD,UAAe,aAAa,CAAC,CAAC,MAAM,EAAE,aAAa;IACjD,IAAI,UAAU;IACd,KAAK,MAAM,SAAS,QAAQ,MAAM,KAAK;GACzC,CAAC;EACH;EACA,aAAa;GACX,OAAO,gBAAgB;GACvB,SAAS,MAAM,OAAO;EACxB;CACF;AACF"}
|