@baishuyun/coze-provider 0.0.17 → 0.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.
@@ -14,6 +14,7 @@ import { CozeChatConfig, IGenerateResult } from './types';
14
14
  import { ServerSentEventTransformStream } from 'parse-sse';
15
15
  import { TextDecoderStream } from 'stream/web';
16
16
  import { convertPromptToMessages } from './utils/prompt-to-model-msg';
17
+ import { accumulateSSEResponse } from './utils/accumulate-sse-response';
17
18
  import { createErrorStream } from './utils/create-error-stream';
18
19
  import { LLMChunkInjectorTransformStream } from './stream-trasnformer/llm-chunk-injector-transform-stream';
19
20
  import MCPChunkFilterTransformStream from './stream-trasnformer/mcp-chunk-filter-transform-stream';
@@ -39,26 +40,74 @@ export class CozeLanguageModel implements LanguageModelV2 {
39
40
  this.config = config;
40
41
  }
41
42
 
42
- // @not-support
43
- // coze 目前不支持非流式接口,为了兼容接口规范,仍然实现了 doGenerate 方法
44
43
  async doGenerate(options: LanguageModelV2CallOptions): Promise<IGenerateResult> {
45
- return BuildWarningGenerateResult(
46
- 'Coze provider does not support non-streaming calls, please use streaming interface'
47
- );
44
+ const messages = convertPromptToMessages(options.prompt);
45
+ const warnings: LanguageModelV2CallWarning[] = [];
46
+
47
+ const url = this.config.url({ path: '/chat' });
48
+
49
+ const user_id = options.headers ? options.headers['x-user-id'] : '';
50
+ const user_var = options.headers ? options.headers['x-user-var'] : '';
51
+
52
+ let customVar;
53
+ if (user_var) {
54
+ try {
55
+ customVar = JSON.parse(user_var);
56
+ } catch (e) {
57
+ console.warn('x-user-var parse error', e);
58
+ }
59
+ }
48
60
 
49
- const botIdFromProvider = options.providerOptions?.bot?.botId;
50
- if (!botIdFromProvider) {
51
- return BuildWarningGenerateResult('bot id missing in provider options');
61
+ const body = {
62
+ bot_id: this.config.botId,
63
+ auto_save_history: false,
64
+ user_id: user_id || '7543191937730215936',
65
+ stream: true,
66
+ additional_messages: messages,
67
+ };
68
+
69
+ if (customVar) {
70
+ (body as any).custom_variables = customVar;
52
71
  }
53
72
 
54
- const response = await createCozeNonStreamingChat(
55
- botIdFromProvider as string,
56
- options,
57
- this.config
58
- );
73
+ const headers = this.config.headers();
74
+ const abortSignal = options.abortSignal;
59
75
 
60
- const result = await BuildGenerateResultByChatRes(response, this.config);
61
- return result;
76
+ const response = await fetch(url, {
77
+ method: 'POST',
78
+ headers: headers,
79
+ credentials: 'include',
80
+ body: JSON.stringify(body),
81
+ signal: abortSignal,
82
+ });
83
+
84
+ if (!response.ok || !response.body) {
85
+ const rawError = await response.text();
86
+ let errorDetail;
87
+ try {
88
+ errorDetail = JSON.parse(rawError);
89
+ } catch {
90
+ errorDetail = rawError;
91
+ }
92
+
93
+ const errorMessage =
94
+ typeof errorDetail === 'object' && errorDetail?.msg
95
+ ? errorDetail.msg
96
+ : `Coze API error: ${response.status} ${response.statusText}`;
97
+
98
+ return BuildWarningGenerateResult(errorMessage);
99
+ }
100
+
101
+ const { content, finishReason, usage } = await accumulateSSEResponse(response.body);
102
+
103
+ return {
104
+ content,
105
+ finishReason,
106
+ usage,
107
+ request: { body },
108
+ response: { body: null },
109
+ warnings,
110
+ };
62
111
  }
63
112
 
64
113
  isVisibleMsgPart(evtData: any) {
@@ -81,7 +130,7 @@ export class CozeLanguageModel implements LanguageModelV2 {
81
130
  path: '/chat',
82
131
  });
83
132
 
84
- const user_id = (options.headers ? options.headers['x-user-id'] : '') || Date.now().toString();
133
+ const user_id = options.headers ? options.headers['x-user-id'] : '';
85
134
  const user_var = options.headers ? options.headers['x-user-var'] : '';
86
135
  // const user_token = options.headers ? options.headers['x-user-token'] : '';
87
136
 
@@ -97,22 +146,17 @@ export class CozeLanguageModel implements LanguageModelV2 {
97
146
  const body = {
98
147
  bot_id: this.config.botId,
99
148
  auto_save_history: false,
100
- user_id: '7543191937730215936', // user_id,
149
+ user_id: user_id || '7543191937730215936',
101
150
  stream: true,
102
151
  additional_messages: messages,
103
152
  };
104
153
 
105
154
  if (customVar) {
106
- (body as any).custom_var = customVar;
155
+ (body as any).custom_variables = customVar;
107
156
  }
108
157
 
109
158
  const headers = this.config.headers();
110
159
 
111
- // console.log('Request headers for Coze API:', JSON.stringify(headers));
112
- // console.log('Request body for Coze API:', JSON.stringify(body));
113
- // console.log('Request URL for Coze API:', url);
114
-
115
- // Support abort signal for proper cancellation when clients disconnect
116
160
  const abortSignal = options.abortSignal;
117
161
 
118
162
  const response = await fetch(url, {
@@ -1,15 +1,18 @@
1
1
  import { LanguageModelV2StreamPart } from '@ai-sdk/provider';
2
2
  import { JSONParser } from '@streamparser/json';
3
- import { createTextInfoEnqueuer } from '../utils/enqueue-text-info';
3
+ import { createTextInfoEnqueuer, TextInfoEnqueuer } from '../utils/enqueue-text-info';
4
4
  import { rebuildFieldsJsonStructure } from '../utils/fields-json-rebuild';
5
5
 
6
6
  export const createFieldsJsonTransformStream = (enableJsonParser: boolean) => {
7
7
  let parser: JSONParser;
8
+ let enqueueTextDelta: TextInfoEnqueuer;
8
9
  let id: string;
9
10
  let isFirstChunk = true;
11
+ // 当 LLM 返回的不是 JSON 而是普通文本时,切换到直通模式
12
+ let passthroughMode = false;
10
13
 
11
14
  const chunkGuard = (chunk: LanguageModelV2StreamPart): boolean => {
12
- if (!enableJsonParser) {
15
+ if (!enableJsonParser || passthroughMode) {
13
16
  return false;
14
17
  }
15
18
 
@@ -33,12 +36,10 @@ export const createFieldsJsonTransformStream = (enableJsonParser: boolean) => {
33
36
  chunk: LanguageModelV2StreamPart,
34
37
  controller: TransformStreamDefaultController<LanguageModelV2StreamPart>
35
38
  ) => {
36
- if (!parser) {
39
+ if (!parser && !passthroughMode) {
37
40
  return;
38
41
  }
39
42
 
40
- // console.log("chunk arrived", chunk);
41
-
42
43
  try {
43
44
  if (chunkGuard(chunk)) {
44
45
  if ('id' in chunk && 'delta' in chunk) {
@@ -66,13 +67,10 @@ export const createFieldsJsonTransformStream = (enableJsonParser: boolean) => {
66
67
  paths: ['$.*'],
67
68
  });
68
69
 
69
- const enqueueTextDelta = enableJsonParser
70
+ enqueueTextDelta = enableJsonParser
70
71
  ? createTextInfoEnqueuer(controller)
71
72
  : (content: string) => {};
72
73
 
73
- // manualChunkForfieldsBeginTips(controller);
74
- // enqueueTextDelta("开始构建字段");
75
-
76
74
  parser.onValue = (parsedInfo: any) => {
77
75
  const value = parsedInfo.value;
78
76
 
@@ -90,17 +88,11 @@ export const createFieldsJsonTransformStream = (enableJsonParser: boolean) => {
90
88
  );
91
89
  };
92
90
 
93
- let errorLogged = false;
94
91
  parser.onError = (err: any) => {
95
- if (!errorLogged) {
96
- console.log('is enabled', enableJsonParser);
97
- console.error('JsonWidgetStream: JSON Parsing Error:', err);
98
- errorLogged = true;
99
- // enqueueTextDelta('error', {
100
- // type: 'agent-error',
101
- // error: '操作超时,请刷新重试',
102
- // });
103
- }
92
+ console.error('JsonWidgetStream: JSON Parsing Error:', err);
93
+
94
+ // LLM 返回的不是 JSON,切换到直通模式,后续 text-delta 直接透传
95
+ passthroughMode = true;
104
96
  };
105
97
 
106
98
  parser.onEnd = () => {
@@ -0,0 +1,83 @@
1
+ import {
2
+ LanguageModelV2Content,
3
+ LanguageModelV2FinishReason,
4
+ LanguageModelV2Usage,
5
+ } from '@ai-sdk/provider';
6
+ import { ServerSentEventTransformStream } from 'parse-sse';
7
+ import { TextDecoderStream } from 'stream/web';
8
+
9
+ export interface AccumulatedSSEResponse {
10
+ content: LanguageModelV2Content[];
11
+ finishReason: LanguageModelV2FinishReason;
12
+ usage: LanguageModelV2Usage;
13
+ }
14
+
15
+ /**
16
+ * 从字节流中解码 SSE 事件并累积完整响应内容
17
+ */
18
+ export async function accumulateSSEResponse(
19
+ body: ReadableStream<Uint8Array>
20
+ ): Promise<AccumulatedSSEResponse> {
21
+ const sseStream = body
22
+ .pipeThrough(
23
+ new TextDecoderStream() as ReadableWritablePair<
24
+ string | undefined,
25
+ Uint8Array<ArrayBufferLike>
26
+ >
27
+ )
28
+ .pipeThrough(new ServerSentEventTransformStream());
29
+
30
+ let textContent = '';
31
+ let reasoningContent = '';
32
+ let finishReason: LanguageModelV2FinishReason = 'stop';
33
+ let usage: LanguageModelV2Usage = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
34
+
35
+ const reader = sseStream.getReader();
36
+ try {
37
+ while (true) {
38
+ const { done, value: evt } = await reader.read();
39
+ if (done) break;
40
+ if (!evt) continue;
41
+
42
+ let evtData;
43
+ try {
44
+ evtData = JSON.parse(evt.data);
45
+ } catch {
46
+ continue;
47
+ }
48
+
49
+ const event = evt.type;
50
+
51
+ if (event === 'conversation.message.delta') {
52
+ if (evtData.reasoning_content) {
53
+ reasoningContent += evtData.reasoning_content;
54
+ } else if (evtData.content) {
55
+ textContent += evtData.content;
56
+ }
57
+ } else if (event === 'conversation.chat.completed' || event === 'done') {
58
+ finishReason = 'stop';
59
+ if (evtData.usage) {
60
+ usage = {
61
+ inputTokens: evtData.usage.input_count || 0,
62
+ outputTokens: evtData.usage.output_count || 0,
63
+ totalTokens: evtData.usage.total_count || 0,
64
+ };
65
+ }
66
+ } else if (event === 'error') {
67
+ finishReason = 'error';
68
+ }
69
+ }
70
+ } finally {
71
+ reader.releaseLock();
72
+ }
73
+
74
+ const content: LanguageModelV2Content[] = [];
75
+ if (reasoningContent) {
76
+ content.push({ type: 'reasoning', text: reasoningContent });
77
+ }
78
+ if (textContent) {
79
+ content.push({ type: 'text', text: textContent });
80
+ }
81
+
82
+ return { content, finishReason, usage };
83
+ }