@astack-tech/integrations 0.1.1-beta.6 → 0.2.0-beta.11

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.
@@ -1,309 +1,316 @@
1
- // src/model-provider/deepseek/index.ts
1
+ // src/model-provider/openai-compatible/index.ts
2
2
  import { Component } from "@astack-tech/core";
3
3
  import OpenAI from "openai";
4
- var Deepseek = class extends Component {
4
+ var DEFAULT_BASE_URL = "https://api.openai.com/v1";
5
+ var DEFAULT_NAME = "The model API";
6
+ var DEFAULT_TEMPERATURE = 0.7;
7
+ var FALLBACK_TEXT = "API call failed";
8
+ function isRecord(value) {
9
+ return typeof value === "object" && value !== null && !Array.isArray(value);
10
+ }
11
+ function isOptionalString(value) {
12
+ return value === void 0 || typeof value === "string";
13
+ }
14
+ function isCount(value) {
15
+ return typeof value === "number" && Number.isFinite(value);
16
+ }
17
+ function isTokenUsage(value) {
18
+ return isRecord(value) && isCount(value["completion_tokens"]) && isCount(value["prompt_tokens"]) && isCount(value["total_tokens"]) && (value["prompt_cache_hit_tokens"] === void 0 || isCount(value["prompt_cache_hit_tokens"])) && (value["prompt_cache_miss_tokens"] === void 0 || isCount(value["prompt_cache_miss_tokens"]));
19
+ }
20
+ function isMessage(value) {
21
+ return isRecord(value) && typeof value["role"] === "string" && isOptionalString(value["content"]) && (value["tool_calls"] === void 0 || Array.isArray(value["tool_calls"])) && isOptionalString(value["tool_call_id"]) && (value["usage"] === void 0 || isTokenUsage(value["usage"]));
22
+ }
23
+ function isMessageList(value) {
24
+ return Array.isArray(value) && value.every((item) => isMessage(item));
25
+ }
26
+ function toApiTool(tool) {
27
+ const raw = isRecord(tool) ? tool : {};
28
+ const definition = raw["function"];
29
+ if (raw["type"] === "function" && isRecord(definition)) {
30
+ const { name: name2, description: description2, parameters: parameters2, strict } = definition;
31
+ return {
32
+ type: "function",
33
+ function: {
34
+ name: typeof name2 === "string" ? name2 : "",
35
+ ...typeof description2 === "string" ? { description: description2 } : {},
36
+ ...isRecord(parameters2) ? { parameters: parameters2 } : {},
37
+ ...typeof strict === "boolean" ? { strict } : {}
38
+ }
39
+ };
40
+ }
41
+ const { name, description, parameters } = raw;
42
+ return {
43
+ type: "function",
44
+ function: {
45
+ name: typeof name === "string" ? name : "",
46
+ description: typeof description === "string" ? description : "",
47
+ parameters: isRecord(parameters) ? parameters : {}
48
+ }
49
+ };
50
+ }
51
+ function parseArguments(raw) {
52
+ if (raw.trim().length === 0) {
53
+ return {};
54
+ }
55
+ try {
56
+ const parsed = JSON.parse(raw);
57
+ return isRecord(parsed) ? parsed : {};
58
+ } catch {
59
+ return {};
60
+ }
61
+ }
62
+ function mapUsage(usage) {
63
+ return {
64
+ completion_tokens: usage.completion_tokens ?? 0,
65
+ prompt_tokens: usage.prompt_tokens ?? 0,
66
+ total_tokens: usage.total_tokens ?? 0,
67
+ ...usage.prompt_cache_hit_tokens === void 0 ? {} : { prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens },
68
+ ...usage.prompt_cache_miss_tokens === void 0 ? {} : { prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens }
69
+ };
70
+ }
71
+ var OpenAICompatible = class extends Component {
72
+ client;
73
+ model;
74
+ temperature;
75
+ maxTokens;
76
+ topP;
77
+ systemPrompt;
78
+ tools;
79
+ verbose;
80
+ name;
5
81
  constructor(config) {
6
82
  super({});
7
83
  this.client = new OpenAI({
8
84
  apiKey: config.apiKey,
9
- baseURL: config.baseURL || "https://api.deepseek.com/v1",
85
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL,
10
86
  dangerouslyAllowBrowser: config.dangerouslyAllowBrowser ?? false
11
87
  });
12
- this.model = config.model || "deepseek-chat";
13
- this.temperature = config.temperature ?? 0.7;
88
+ this.model = config.model;
89
+ this.name = config.name ?? DEFAULT_NAME;
90
+ this.temperature = config.temperature ?? DEFAULT_TEMPERATURE;
14
91
  this.maxTokens = config.maxTokens;
15
92
  this.topP = config.topP;
16
93
  this.systemPrompt = config.systemPrompt;
17
- if (config.rawTools && config.rawTools.length > 0) {
18
- this.tools = config.rawTools.map((tool) => ({
19
- type: "function",
20
- function: {
21
- name: tool.name,
22
- description: tool.description,
23
- parameters: tool.parameters || {}
24
- }
25
- }));
26
- } else {
27
- this.tools = config.tools;
28
- }
94
+ this.verbose = config.verbose ?? false;
95
+ this.tools = config.rawTools !== void 0 && config.rawTools.length > 0 ? config.rawTools.map(toApiTool) : config.tools;
29
96
  Component.Port.I("prompt").attach(this);
30
97
  Component.Port.I("messages").attach(this);
31
98
  Component.Port.O("completion").attach(this);
32
99
  Component.Port.O("message").attach(this);
33
100
  }
34
101
  /**
35
- * 生成文本完成
36
- * @param prompt 提示文本
37
- * @returns 生成的文本
102
+ * Generates a completion for a single prompt.
103
+ *
104
+ * @returns The text answer, or the JSON-encoded assistant message when it contains tool calls.
38
105
  */
39
106
  async generateCompletion(prompt) {
40
- const messages = [];
41
- if (this.systemPrompt) {
42
- messages.push({
43
- role: "system",
44
- content: this.systemPrompt
45
- });
46
- }
47
- messages.push({
48
- role: "user",
49
- content: prompt
107
+ const response = await this.client.chat.completions.create({
108
+ ...this.baseRequest(),
109
+ messages: [...this.systemMessages(), { role: "user", content: prompt }],
110
+ ...this.toolsParam(this.tools)
50
111
  });
51
- const requestParams = {
52
- model: this.model,
53
- messages,
54
- temperature: this.temperature,
55
- max_tokens: this.maxTokens,
56
- top_p: this.topP
57
- };
58
- if (this.tools && this.tools.length > 0) {
59
- requestParams.tools = this.tools;
60
- }
61
- const response = await this.client.chat.completions.create(requestParams);
62
- if (response.choices[0].message.tool_calls && response.choices[0].message.tool_calls.length > 0) {
63
- return JSON.stringify(response.choices[0].message);
112
+ const message = this.firstMessage(response);
113
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
114
+ return JSON.stringify(message);
64
115
  }
65
- return response.choices[0].message.content || "";
116
+ return message.content ?? "";
66
117
  }
67
118
  /**
68
- * 流式处理对话消息
69
- * @param messages 对话消息数组
70
- * @param options 可选的调用选项,包含临时工具列表
71
- * @returns 生成的流式响应消息异步生成器
119
+ * Sends a conversation and returns the assistant reply.
72
120
  */
73
- async *streamChatCompletion(messages, options) {
74
- const formattedMessages = [];
75
- if (this.systemPrompt) {
76
- formattedMessages.push({
77
- role: "system",
78
- content: this.systemPrompt
79
- });
121
+ async chatCompletion(messages, options) {
122
+ const response = await this.client.chat.completions.create({
123
+ ...this.baseRequest(),
124
+ messages: this.formatMessages(messages),
125
+ ...this.toolsParam(options?.temporaryTools ?? this.tools)
126
+ });
127
+ const message = this.firstMessage(response);
128
+ const result = { role: "assistant", content: message.content ?? "" };
129
+ if (response.usage !== void 0) {
130
+ result.usage = mapUsage(response.usage);
80
131
  }
81
- formattedMessages.push(
82
- ...messages.map((msg) => {
83
- const formattedMsg = {
84
- role: msg.role,
85
- content: msg.content
86
- };
87
- if (msg.role === "tool" && msg.tool_call_id) {
88
- formattedMsg.tool_call_id = msg.tool_call_id;
89
- }
90
- return formattedMsg;
91
- })
92
- );
93
- const requestParams = {
94
- model: this.model,
95
- messages: formattedMessages,
96
- temperature: this.temperature,
97
- max_tokens: this.maxTokens,
98
- top_p: this.topP,
99
- stream: true
100
- // 开启流式模式
101
- };
102
- const toolsToUse = options?.temporaryTools || this.tools;
103
- if (toolsToUse && toolsToUse.length > 0) {
104
- const formattedTools = toolsToUse.map((tool) => {
105
- const typedTool = tool;
106
- if (typedTool.type === "function") {
107
- return tool;
132
+ if (message.tool_calls !== void 0 && message.tool_calls.length > 0) {
133
+ result.tool_calls = message.tool_calls.map((toolCall) => {
134
+ if (toolCall.type !== "function") {
135
+ throw new Error(`Unsupported tool call type: ${toolCall.type}`);
108
136
  }
137
+ const rawArguments = toolCall.function.arguments;
109
138
  return {
139
+ id: toolCall.id,
110
140
  type: "function",
111
141
  function: {
112
- name: typedTool.name,
113
- description: typedTool.description,
114
- parameters: typedTool.parameters || {}
115
- }
142
+ name: toolCall.function.name,
143
+ arguments: rawArguments.length > 0 ? rawArguments : "{}"
144
+ },
145
+ tool_name: toolCall.function.name,
146
+ arguments: parseArguments(rawArguments)
116
147
  };
117
148
  });
118
- requestParams.tools = formattedTools;
119
149
  }
120
- const stream = await this.client.chat.completions.create(requestParams);
150
+ return result;
151
+ }
152
+ /**
153
+ * Streams the assistant reply. Each chunk holds a text delta, tool-call deltas, or usage.
154
+ */
155
+ async *streamChatCompletion(messages, options) {
156
+ const stream = await this.client.chat.completions.create({
157
+ ...this.baseRequest(),
158
+ messages: this.formatMessages(messages),
159
+ ...this.toolsParam(options?.temporaryTools ?? this.tools),
160
+ stream: true
161
+ });
121
162
  for await (const chunk of stream) {
122
163
  const delta = chunk.choices[0]?.delta;
123
- if (delta?.content) {
124
- yield {
125
- role: "assistant",
126
- content: delta.content
127
- };
164
+ const text = delta?.content ?? "";
165
+ if (text !== "") {
166
+ yield { role: "assistant", content: text };
128
167
  }
129
168
  if (delta?.tool_calls) {
130
169
  yield {
131
170
  role: "assistant",
132
171
  content: "",
133
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
134
- tool_calls: delta.tool_calls.map((toolCall) => ({
135
- id: toolCall.id || "",
136
- function: {
137
- name: toolCall.function?.name || "",
138
- arguments: toolCall.function?.arguments || ""
139
- },
140
- type: "function",
141
- tool_name: toolCall.function?.name || "",
142
- arguments: toolCall.function?.arguments || ""
143
- }))
172
+ tool_calls: delta.tool_calls.map((toolCall) => {
173
+ const name = toolCall.function?.name ?? "";
174
+ const args = toolCall.function?.arguments ?? "";
175
+ return {
176
+ id: toolCall.id ?? "",
177
+ type: "function",
178
+ function: { name, arguments: args },
179
+ tool_name: name,
180
+ arguments: args
181
+ };
182
+ })
144
183
  };
145
184
  }
146
185
  if (chunk.usage) {
147
- yield {
148
- role: "assistant",
149
- content: "",
150
- usage: {
151
- completion_tokens: chunk.usage.completion_tokens || 0,
152
- prompt_tokens: chunk.usage.prompt_tokens || 0,
153
- prompt_cache_hit_tokens: chunk.usage.prompt_cache_hit_tokens,
154
- prompt_cache_miss_tokens: chunk.usage.prompt_cache_miss_tokens,
155
- total_tokens: chunk.usage.total_tokens || 0
156
- }
157
- };
186
+ yield { role: "assistant", content: "", usage: mapUsage(chunk.usage) };
158
187
  }
159
188
  }
160
189
  }
161
190
  /**
162
- * 处理对话消息
163
- * @param messages 对话消息数组
164
- * @param options 可选的调用选项,包含临时工具列表
165
- * @returns 生成的响应消息
191
+ * Runs the model outside a pipeline.
192
+ *
193
+ * @param input - A prompt string or a list of messages.
166
194
  */
167
- async chatCompletion(messages, options) {
168
- const formattedMessages = [];
169
- if (this.systemPrompt) {
170
- formattedMessages.push({
171
- role: "system",
172
- content: this.systemPrompt
173
- });
174
- }
175
- formattedMessages.push(
176
- ...messages.map((msg) => {
177
- const formattedMsg = {
178
- role: msg.role,
179
- content: msg.content
180
- };
181
- if (msg.role === "tool" && msg.tool_call_id) {
182
- formattedMsg.tool_call_id = msg.tool_call_id;
195
+ run(input) {
196
+ return typeof input === "string" ? this.generateCompletion(input) : this.chatCompletion(input);
197
+ }
198
+ /**
199
+ * Connects the `prompt` and `messages` input ports to the model.
200
+ * API errors produce a fallback value so the pipeline keeps flowing. Input of the wrong
201
+ * type fails the pipeline run with a `TypeError` before the API is called.
202
+ */
203
+ _transform($i, $o) {
204
+ $i("prompt").receive((prompt) => {
205
+ if (typeof prompt !== "string") {
206
+ throw new TypeError(`${this.name}: the prompt port expects a string.`);
207
+ }
208
+ return this.generateCompletion(prompt).then(
209
+ (completion) => {
210
+ $o("completion").send(completion);
211
+ },
212
+ (error) => {
213
+ this.logError(error);
214
+ $o("completion").send(FALLBACK_TEXT);
183
215
  }
184
- return formattedMsg;
185
- })
186
- );
187
- const requestParams = {
216
+ );
217
+ });
218
+ $i("messages").receive((messages) => {
219
+ if (!isMessageList(messages)) {
220
+ throw new TypeError(`${this.name}: the messages port expects an array of messages.`);
221
+ }
222
+ return this.chatCompletion(messages).then(
223
+ (message) => {
224
+ $o("message").send(message);
225
+ },
226
+ (error) => {
227
+ this.logError(error);
228
+ $o("message").send({ role: "assistant", content: FALLBACK_TEXT });
229
+ }
230
+ );
231
+ });
232
+ }
233
+ baseRequest() {
234
+ return {
188
235
  model: this.model,
189
- messages: formattedMessages,
190
236
  temperature: this.temperature,
191
- max_tokens: this.maxTokens,
192
- top_p: this.topP
237
+ ...this.maxTokens === void 0 ? {} : { max_tokens: this.maxTokens },
238
+ ...this.topP === void 0 ? {} : { top_p: this.topP }
193
239
  };
194
- const toolsToUse = options?.temporaryTools || this.tools;
195
- if (toolsToUse && toolsToUse.length > 0) {
196
- const formattedTools = toolsToUse.map((tool) => {
197
- const typedTool = tool;
198
- if (typedTool.type === "function") {
199
- return tool;
200
- }
201
- return {
202
- type: "function",
203
- function: {
204
- name: typedTool.name,
205
- description: typedTool.description,
206
- parameters: typedTool.parameters || {}
207
- }
208
- };
209
- });
210
- requestParams.tools = formattedTools;
211
- }
212
- const response = await this.client.chat.completions.create(requestParams);
213
- const responseMessage = response.choices[0].message;
214
- const result = {
215
- role: "assistant",
216
- content: responseMessage.content || ""
217
- };
218
- if (response.usage) {
219
- const usage = response.usage;
220
- result.usage = {
221
- completion_tokens: usage.completion_tokens || 0,
222
- prompt_tokens: usage.prompt_tokens || 0,
223
- prompt_cache_hit_tokens: usage.prompt_cache_hit_tokens,
224
- prompt_cache_miss_tokens: usage.prompt_cache_miss_tokens,
225
- total_tokens: usage.total_tokens || 0
226
- };
227
- }
228
- if (responseMessage.tool_calls && responseMessage.tool_calls.length > 0) {
229
- result.tool_calls = responseMessage.tool_calls.map((toolCall) => {
230
- if (toolCall.type !== "function" || !("function" in toolCall)) {
231
- throw new Error(`Unsupported tool call type: ${toolCall.type}`);
232
- }
233
- let args;
234
- const rawArgs = toolCall.function.arguments;
235
- if (typeof rawArgs === "string") {
236
- const raw = rawArgs;
237
- try {
238
- args = raw && raw.trim().length > 0 ? JSON.parse(raw) : {};
239
- } catch {
240
- args = {};
241
- }
242
- } else if (rawArgs && typeof rawArgs === "object") {
243
- args = rawArgs;
244
- } else {
245
- args = {};
246
- }
247
- return {
248
- id: toolCall.id,
249
- // 满足 Agent 组件期望的格式
250
- function: {
251
- name: toolCall.function.name,
252
- arguments: toolCall.function.arguments || "{}"
253
- },
254
- type: "function",
255
- // 满足内部 ToolCall 接口
256
- tool_name: toolCall.function.name,
257
- arguments: args
258
- };
259
- });
260
- }
261
- return result;
240
+ }
241
+ toolsParam(tools) {
242
+ return tools !== void 0 && tools.length > 0 ? { tools: tools.map(toApiTool) } : {};
243
+ }
244
+ systemMessages() {
245
+ return this.systemPrompt === void 0 || this.systemPrompt === "" ? [] : [{ role: "system", content: this.systemPrompt }];
262
246
  }
263
247
  /**
264
- * 在独立模式下运行组件
265
- * @param input 输入参数,可以是提示文本或对话消息数组
266
- * @returns 生成的文本或响应消息
248
+ * Converts input messages to API messages. Assistant `tool_calls` are not forwarded:
249
+ * agents send tool results back as user messages, and the API rejects assistant tool
250
+ * calls that are not followed by tool messages.
267
251
  */
268
- async run(input) {
269
- if (typeof input === "string") {
270
- return this.generateCompletion(input);
271
- } else {
272
- return this.chatCompletion(input);
273
- }
252
+ formatMessages(messages) {
253
+ return [...this.systemMessages(), ...messages.map((message) => this.toApiMessage(message))];
274
254
  }
275
255
  /**
276
- * 在流水线中运行组件
277
- * @param $i 输入端口
278
- * @param $o 输出端口
256
+ * Converts one input message to its API shape.
257
+ *
258
+ * @throws Error when the role is not one of `MessageRole`, or a `tool` message has no
259
+ * `tool_call_id`. The API would reject both.
279
260
  */
280
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
281
- _transform($i, $o) {
282
- $i("prompt").receive(async (prompt) => {
283
- try {
284
- const completion = await this.generateCompletion(prompt);
285
- $o("completion").send(completion);
286
- } catch (error) {
287
- console.error("Deepseek API \u8C03\u7528\u9519\u8BEF:", error);
288
- $o("completion").send("API \u8C03\u7528\u9519\u8BEF");
289
- }
290
- });
291
- $i("messages").receive(async (messages) => {
292
- try {
293
- const responseMessage = await this.chatCompletion(messages);
294
- $o("message").send(responseMessage);
295
- } catch (error) {
296
- console.error("Deepseek API \u8C03\u7528\u9519\u8BEF:", error);
297
- $o("message").send({
298
- role: "assistant",
299
- content: "API \u8C03\u7528\u9519\u8BEF"
300
- });
301
- }
261
+ toApiMessage(message) {
262
+ const content = message.content ?? "";
263
+ switch (message.role) {
264
+ case "system":
265
+ return { role: "system", content };
266
+ case "user":
267
+ return { role: "user", content };
268
+ case "assistant":
269
+ return message.content === void 0 ? { role: "assistant" } : { role: "assistant", content: message.content };
270
+ case "tool":
271
+ if (message.tool_call_id === void 0 || message.tool_call_id === "") {
272
+ throw new Error(
273
+ `${this.name}: a tool message needs the tool_call_id of the call it answers.`
274
+ );
275
+ }
276
+ return { role: "tool", content, tool_call_id: message.tool_call_id };
277
+ default:
278
+ throw new Error(
279
+ `${this.name}: unsupported message role '${message.role}'. Use system, user, assistant or tool.`
280
+ );
281
+ }
282
+ }
283
+ firstMessage(response) {
284
+ const choice = response.choices[0];
285
+ if (choice === void 0) {
286
+ throw new Error(`${this.name} returned no choices.`);
287
+ }
288
+ return choice.message;
289
+ }
290
+ logError(error) {
291
+ if (this.verbose) {
292
+ console.error(`[${this.name}] API call failed:`, error);
293
+ }
294
+ }
295
+ };
296
+ var openai_compatible_default = OpenAICompatible;
297
+
298
+ // src/model-provider/deepseek/index.ts
299
+ var DEFAULT_BASE_URL2 = "https://api.deepseek.com/v1";
300
+ var DEFAULT_MODEL = "deepseek-chat";
301
+ var Deepseek = class extends openai_compatible_default {
302
+ constructor(config) {
303
+ super({
304
+ ...config,
305
+ model: config.model ?? DEFAULT_MODEL,
306
+ baseURL: config.baseURL ?? DEFAULT_BASE_URL2,
307
+ name: "DeepSeek"
302
308
  });
303
309
  }
304
310
  };
305
311
  var deepseek_default = Deepseek;
306
312
  export {
307
- deepseek_default as Deepseek
313
+ deepseek_default as Deepseek,
314
+ openai_compatible_default as OpenAICompatible
308
315
  };
309
316
  //# sourceMappingURL=index.js.map