@arki-moe/agent-ts 6.0.0 → 6.0.1

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.
@@ -4,6 +4,7 @@ exports.openaiAdapter = openaiAdapter;
4
4
  const abort_1 = require("../abort");
5
5
  const types_1 = require("../types");
6
6
  const sse_1 = require("./sse");
7
+ const retry_1 = require("./retry");
7
8
  const ROLE_TO_OPENAI = {
8
9
  [types_1.Role.System]: "system",
9
10
  [types_1.Role.User]: "user",
@@ -40,6 +41,13 @@ function toUsage(usage) {
40
41
  return { promptTokens, completionTokens, totalTokens };
41
42
  }
42
43
  async function openaiAdapter(config, context, tools) {
44
+ let hasStreamed = false;
45
+ const retryOptions = {
46
+ maxRetries: 2,
47
+ baseDelayMs: 1000,
48
+ maxDelayMs: 10000,
49
+ isRetryable: (err) => !hasStreamed && (0, retry_1.isRetryableError)(err),
50
+ };
43
51
  const baseUrl = config.baseUrl ?? "https://api.openai.com";
44
52
  const apiKey = config.apiKey || process.env.OPENAI_API_KEY || "";
45
53
  const model = config.model ?? "gpt-5-nano";
@@ -61,122 +69,135 @@ async function openaiAdapter(config, context, tools) {
61
69
  stream: onStream ? true : undefined,
62
70
  stream_options: onStream ? { include_usage: true } : undefined,
63
71
  };
64
- if (await check())
65
- return { messages: [] };
66
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
67
- method: "POST",
68
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
69
- body: JSON.stringify(body),
70
- });
71
- if (!res.ok) {
72
- const text = await res.text();
73
- let errMsg = `OpenAI API HTTP ${res.status}`;
74
- try {
75
- const parsed = JSON.parse(text);
76
- if (parsed?.error?.message)
77
- errMsg = parsed.error.message;
78
- }
79
- catch {
80
- if (text)
81
- errMsg += `: ${text.slice(0, 200)}`;
82
- }
83
- throw new Error(errMsg);
84
- }
85
- if (onStream) {
86
- let content = "";
87
- let usage;
88
- const toolCalls = new Map();
89
- const upsertToolCall = (tc) => {
90
- const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
91
- let entry = toolCalls.get(index);
92
- if (!entry) {
93
- entry = { args: "" };
94
- toolCalls.set(index, entry);
95
- }
96
- if (typeof tc?.id === "string")
97
- entry.id = tc.id;
98
- const fn = tc?.function;
99
- if (fn) {
100
- if (typeof fn.name === "string")
101
- entry.name = fn.name;
102
- if (typeof fn.arguments === "string")
103
- entry.args += fn.arguments;
104
- }
105
- };
106
- await (0, sse_1.readSse)(res, async (dataLine) => {
107
- if (dataLine === "[DONE]")
108
- return;
109
- let parsed;
72
+ return await (0, retry_1.withRetry)(async () => {
73
+ hasStreamed = false;
74
+ if (await check())
75
+ return { messages: [] };
76
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
79
+ body: JSON.stringify(body),
80
+ });
81
+ if (!res.ok) {
82
+ const text = await res.text();
83
+ let errMsg = `OpenAI API HTTP ${res.status}`;
110
84
  try {
111
- parsed = JSON.parse(dataLine);
85
+ const parsed = JSON.parse(text);
86
+ if (parsed?.error?.message)
87
+ errMsg = `OpenAI API error: ${parsed.error.message} (HTTP ${res.status})`;
112
88
  }
113
89
  catch {
114
- throw new Error(`OpenAI API returned invalid JSON: ${dataLine.slice(0, 200)}`);
90
+ if (text)
91
+ errMsg += `: ${text.slice(0, 200)}`;
115
92
  }
116
- if (parsed?.usage) {
117
- const nextUsage = toUsage(parsed.usage);
118
- if (nextUsage)
119
- usage = nextUsage;
120
- }
121
- const delta = parsed?.choices?.[0]?.delta;
122
- if (!delta)
123
- return;
124
- if (Array.isArray(delta.tool_calls)) {
125
- for (const tc of delta.tool_calls) {
126
- upsertToolCall(tc);
93
+ throw new Error(errMsg);
94
+ }
95
+ if (onStream) {
96
+ let content = "";
97
+ let usage;
98
+ const toolCalls = new Map();
99
+ const upsertToolCall = (tc) => {
100
+ const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
101
+ let entry = toolCalls.get(index);
102
+ if (!entry) {
103
+ entry = { args: "" };
104
+ toolCalls.set(index, entry);
127
105
  }
128
- }
129
- if (typeof delta.content === "string") {
130
- content += delta.content;
131
- await Promise.resolve(onStream(delta.content));
132
- }
133
- await check();
134
- }, check);
135
- if (toolCalls.size > 0) {
136
- return {
137
- messages: [...toolCalls.entries()]
138
- .sort((a, b) => a[0] - b[0])
139
- .map(([index, tc]) => {
140
- if (!tc.name)
141
- throw new Error(`OpenAI streaming tool call missing function name at index ${index}`);
106
+ if (typeof tc?.id === "string")
107
+ entry.id = tc.id;
108
+ const fn = tc?.function;
109
+ if (fn) {
110
+ if (typeof fn.name === "string")
111
+ entry.name = fn.name;
112
+ if (typeof fn.arguments === "string")
113
+ entry.args += fn.arguments;
114
+ }
115
+ };
116
+ await (0, sse_1.readSse)(res, async (dataLine) => {
117
+ if (dataLine === "[DONE]")
118
+ return;
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(dataLine);
122
+ }
123
+ catch {
124
+ throw new Error(`OpenAI API returned invalid JSON: ${dataLine.slice(0, 200)}`);
125
+ }
126
+ if (parsed?.usage) {
127
+ const nextUsage = toUsage(parsed.usage);
128
+ if (nextUsage)
129
+ usage = nextUsage;
130
+ }
131
+ const delta = parsed?.choices?.[0]?.delta;
132
+ if (!delta)
133
+ return;
134
+ if (Array.isArray(delta.tool_calls)) {
135
+ for (const tc of delta.tool_calls) {
136
+ upsertToolCall(tc);
137
+ }
138
+ }
139
+ if (typeof delta.content === "string") {
140
+ content += delta.content;
141
+ hasStreamed = true;
142
+ await Promise.resolve(onStream(delta.content));
143
+ }
144
+ await check();
145
+ }, check);
146
+ if (toolCalls.size > 0) {
147
+ const ordered = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]);
148
+ const valid = ordered.filter(([index, tc]) => {
149
+ if (!tc.name) {
150
+ console.warn(`OpenAI adapter: skipping incomplete streaming tool call at index ${index}`);
151
+ return false;
152
+ }
153
+ return true;
154
+ });
155
+ if (valid.length > 0) {
142
156
  return {
143
- role: types_1.Role.ToolCall,
144
- toolName: tc.name,
145
- callId: tc.id ?? `call_${index}`,
146
- argsText: tc.args.length ? tc.args : "{}",
157
+ messages: valid.map(([index, tc]) => ({
158
+ role: types_1.Role.ToolCall,
159
+ toolName: tc.name,
160
+ callId: tc.id ?? `call_${index}`,
161
+ argsText: tc.args.length ? tc.args : "{}",
162
+ })),
163
+ usage,
147
164
  };
148
- }),
165
+ }
166
+ if (!content) {
167
+ if (isAborted())
168
+ return { messages: [], usage };
169
+ throw new Error("OpenAI API returned empty response");
170
+ }
171
+ }
172
+ if (!content)
173
+ return { messages: [], usage };
174
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
175
+ }
176
+ const text = await res.text();
177
+ let data;
178
+ try {
179
+ data = JSON.parse(text);
180
+ }
181
+ catch {
182
+ throw new Error(`OpenAI API returned invalid JSON: ${text.slice(0, 200)}`);
183
+ }
184
+ if (data.error)
185
+ throw new Error(`OpenAI API error: ${data.error.message}`);
186
+ const usage = toUsage(data.usage);
187
+ const msg = data.choices?.[0]?.message;
188
+ if (!msg)
189
+ throw new Error("OpenAI API returned empty response");
190
+ if (msg.tool_calls?.length) {
191
+ return {
192
+ messages: msg.tool_calls.map((tc) => ({
193
+ role: types_1.Role.ToolCall,
194
+ toolName: tc.function.name,
195
+ callId: tc.id,
196
+ argsText: tc.function.arguments ?? "{}",
197
+ })),
149
198
  usage,
150
199
  };
151
200
  }
152
- if (!content)
153
- return { messages: [], usage };
154
- return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
155
- }
156
- const text = await res.text();
157
- let data;
158
- try {
159
- data = JSON.parse(text);
160
- }
161
- catch {
162
- throw new Error(`OpenAI API returned invalid JSON: ${text.slice(0, 200)}`);
163
- }
164
- if (data.error)
165
- throw new Error(`OpenAI API error: ${data.error.message}`);
166
- const usage = toUsage(data.usage);
167
- const msg = data.choices?.[0]?.message;
168
- if (!msg)
169
- throw new Error("OpenAI API returned empty response");
170
- if (msg.tool_calls?.length) {
171
- return {
172
- messages: msg.tool_calls.map((tc) => ({
173
- role: types_1.Role.ToolCall,
174
- toolName: tc.function.name,
175
- callId: tc.id,
176
- argsText: tc.function.arguments ?? "{}",
177
- })),
178
- usage,
179
- };
180
- }
181
- return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
201
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
202
+ }, retryOptions);
182
203
  }
@@ -4,6 +4,7 @@ exports.openrouterAdapter = openrouterAdapter;
4
4
  const abort_1 = require("../abort");
5
5
  const types_1 = require("../types");
6
6
  const sse_1 = require("./sse");
7
+ const retry_1 = require("./retry");
7
8
  const ROLE_TO_OPENROUTER = {
8
9
  [types_1.Role.System]: "system",
9
10
  [types_1.Role.User]: "user",
@@ -40,6 +41,13 @@ function toUsage(usage) {
40
41
  return { promptTokens, completionTokens, totalTokens };
41
42
  }
42
43
  async function openrouterAdapter(config, context, tools) {
44
+ let hasStreamed = false;
45
+ const retryOptions = {
46
+ maxRetries: 2,
47
+ baseDelayMs: 1000,
48
+ maxDelayMs: 10000,
49
+ isRetryable: (err) => !hasStreamed && (0, retry_1.isRetryableError)(err),
50
+ };
43
51
  const baseUrl = config.baseUrl ?? "https://openrouter.ai/api/v1";
44
52
  const apiKey = config.apiKey || process.env.OPENROUTER_API_KEY || "";
45
53
  const model = config.model ?? "gpt-5-nano";
@@ -71,122 +79,135 @@ async function openrouterAdapter(config, context, tools) {
71
79
  headers["HTTP-Referer"] = httpReferer;
72
80
  if (title)
73
81
  headers["X-Title"] = title;
74
- if (await check())
75
- return { messages: [] };
76
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
77
- method: "POST",
78
- headers,
79
- body: JSON.stringify(body),
80
- });
81
- if (!res.ok) {
82
- const text = await res.text();
83
- let errMsg = `OpenRouter API HTTP ${res.status}`;
84
- try {
85
- const parsed = JSON.parse(text);
86
- if (parsed?.error?.message)
87
- errMsg = parsed.error.message;
88
- }
89
- catch {
90
- if (text)
91
- errMsg += `: ${text.slice(0, 200)}`;
92
- }
93
- throw new Error(errMsg);
94
- }
95
- if (onStream) {
96
- let content = "";
97
- let usage;
98
- const toolCalls = new Map();
99
- const upsertToolCall = (tc) => {
100
- const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
101
- let entry = toolCalls.get(index);
102
- if (!entry) {
103
- entry = { args: "" };
104
- toolCalls.set(index, entry);
105
- }
106
- if (typeof tc?.id === "string")
107
- entry.id = tc.id;
108
- const fn = tc?.function;
109
- if (fn) {
110
- if (typeof fn.name === "string")
111
- entry.name = fn.name;
112
- if (typeof fn.arguments === "string")
113
- entry.args += fn.arguments;
114
- }
115
- };
116
- await (0, sse_1.readSse)(res, async (dataLine) => {
117
- if (dataLine === "[DONE]")
118
- return;
119
- let parsed;
82
+ return await (0, retry_1.withRetry)(async () => {
83
+ hasStreamed = false;
84
+ if (await check())
85
+ return { messages: [] };
86
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
87
+ method: "POST",
88
+ headers,
89
+ body: JSON.stringify(body),
90
+ });
91
+ if (!res.ok) {
92
+ const text = await res.text();
93
+ let errMsg = `OpenRouter API HTTP ${res.status}`;
120
94
  try {
121
- parsed = JSON.parse(dataLine);
95
+ const parsed = JSON.parse(text);
96
+ if (parsed?.error?.message)
97
+ errMsg = `OpenRouter API error: ${parsed.error.message} (HTTP ${res.status})`;
122
98
  }
123
99
  catch {
124
- throw new Error(`OpenRouter API returned invalid JSON: ${dataLine.slice(0, 200)}`);
100
+ if (text)
101
+ errMsg += `: ${text.slice(0, 200)}`;
125
102
  }
126
- if (parsed?.usage) {
127
- const nextUsage = toUsage(parsed.usage);
128
- if (nextUsage)
129
- usage = nextUsage;
130
- }
131
- const delta = parsed?.choices?.[0]?.delta;
132
- if (!delta)
133
- return;
134
- if (Array.isArray(delta.tool_calls)) {
135
- for (const tc of delta.tool_calls) {
136
- upsertToolCall(tc);
103
+ throw new Error(errMsg);
104
+ }
105
+ if (onStream) {
106
+ let content = "";
107
+ let usage;
108
+ const toolCalls = new Map();
109
+ const upsertToolCall = (tc) => {
110
+ const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
111
+ let entry = toolCalls.get(index);
112
+ if (!entry) {
113
+ entry = { args: "" };
114
+ toolCalls.set(index, entry);
137
115
  }
138
- }
139
- if (typeof delta.content === "string") {
140
- content += delta.content;
141
- await Promise.resolve(onStream(delta.content));
142
- }
143
- await check();
144
- }, check);
145
- if (toolCalls.size > 0) {
146
- return {
147
- messages: [...toolCalls.entries()]
148
- .sort((a, b) => a[0] - b[0])
149
- .map(([index, tc]) => {
150
- if (!tc.name)
151
- throw new Error(`OpenRouter streaming tool call missing function name at index ${index}`);
116
+ if (typeof tc?.id === "string")
117
+ entry.id = tc.id;
118
+ const fn = tc?.function;
119
+ if (fn) {
120
+ if (typeof fn.name === "string")
121
+ entry.name = fn.name;
122
+ if (typeof fn.arguments === "string")
123
+ entry.args += fn.arguments;
124
+ }
125
+ };
126
+ await (0, sse_1.readSse)(res, async (dataLine) => {
127
+ if (dataLine === "[DONE]")
128
+ return;
129
+ let parsed;
130
+ try {
131
+ parsed = JSON.parse(dataLine);
132
+ }
133
+ catch {
134
+ throw new Error(`OpenRouter API returned invalid JSON: ${dataLine.slice(0, 200)}`);
135
+ }
136
+ if (parsed?.usage) {
137
+ const nextUsage = toUsage(parsed.usage);
138
+ if (nextUsage)
139
+ usage = nextUsage;
140
+ }
141
+ const delta = parsed?.choices?.[0]?.delta;
142
+ if (!delta)
143
+ return;
144
+ if (Array.isArray(delta.tool_calls)) {
145
+ for (const tc of delta.tool_calls) {
146
+ upsertToolCall(tc);
147
+ }
148
+ }
149
+ if (typeof delta.content === "string") {
150
+ content += delta.content;
151
+ hasStreamed = true;
152
+ await Promise.resolve(onStream(delta.content));
153
+ }
154
+ await check();
155
+ }, check);
156
+ if (toolCalls.size > 0) {
157
+ const ordered = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]);
158
+ const valid = ordered.filter(([index, tc]) => {
159
+ if (!tc.name) {
160
+ console.warn(`OpenRouter adapter: skipping incomplete streaming tool call at index ${index}`);
161
+ return false;
162
+ }
163
+ return true;
164
+ });
165
+ if (valid.length > 0) {
152
166
  return {
153
- role: types_1.Role.ToolCall,
154
- toolName: tc.name,
155
- callId: tc.id ?? `call_${index}`,
156
- argsText: tc.args.length ? tc.args : "{}",
167
+ messages: valid.map(([index, tc]) => ({
168
+ role: types_1.Role.ToolCall,
169
+ toolName: tc.name,
170
+ callId: tc.id ?? `call_${index}`,
171
+ argsText: tc.args.length ? tc.args : "{}",
172
+ })),
173
+ usage,
157
174
  };
158
- }),
175
+ }
176
+ if (!content) {
177
+ if (isAborted())
178
+ return { messages: [], usage };
179
+ throw new Error("OpenRouter API returned empty response");
180
+ }
181
+ }
182
+ if (!content)
183
+ return { messages: [], usage };
184
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
185
+ }
186
+ const text = await res.text();
187
+ let data;
188
+ try {
189
+ data = JSON.parse(text);
190
+ }
191
+ catch {
192
+ throw new Error(`OpenRouter API returned invalid JSON: ${text.slice(0, 200)}`);
193
+ }
194
+ if (data.error)
195
+ throw new Error(`OpenRouter API error: ${data.error.message}`);
196
+ const usage = toUsage(data.usage);
197
+ const msg = data.choices?.[0]?.message;
198
+ if (!msg)
199
+ throw new Error("OpenRouter API returned empty response");
200
+ if (msg.tool_calls?.length) {
201
+ return {
202
+ messages: msg.tool_calls.map((tc) => ({
203
+ role: types_1.Role.ToolCall,
204
+ toolName: tc.function.name,
205
+ callId: tc.id,
206
+ argsText: tc.function.arguments ?? "{}",
207
+ })),
159
208
  usage,
160
209
  };
161
210
  }
162
- if (!content)
163
- return { messages: [], usage };
164
- return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
165
- }
166
- const text = await res.text();
167
- let data;
168
- try {
169
- data = JSON.parse(text);
170
- }
171
- catch {
172
- throw new Error(`OpenRouter API returned invalid JSON: ${text.slice(0, 200)}`);
173
- }
174
- if (data.error)
175
- throw new Error(`OpenRouter API error: ${data.error.message}`);
176
- const usage = toUsage(data.usage);
177
- const msg = data.choices?.[0]?.message;
178
- if (!msg)
179
- throw new Error("OpenRouter API returned empty response");
180
- if (msg.tool_calls?.length) {
181
- return {
182
- messages: msg.tool_calls.map((tc) => ({
183
- role: types_1.Role.ToolCall,
184
- toolName: tc.function.name,
185
- callId: tc.id,
186
- argsText: tc.function.arguments ?? "{}",
187
- })),
188
- usage,
189
- };
190
- }
191
- return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
211
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
212
+ }, retryOptions);
192
213
  }
@@ -0,0 +1,9 @@
1
+ type RetryOptions = {
2
+ maxRetries: number;
3
+ baseDelayMs: number;
4
+ maxDelayMs: number;
5
+ isRetryable: (err: unknown) => boolean;
6
+ };
7
+ export declare function isRetryableError(err: unknown): boolean;
8
+ export declare function withRetry<T>(fn: () => Promise<T>, options: RetryOptions): Promise<T>;
9
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isRetryableError = isRetryableError;
4
+ exports.withRetry = withRetry;
5
+ const RETRYABLE_CODES = new Set([
6
+ "ECONNRESET",
7
+ "ECONNREFUSED",
8
+ "ETIMEDOUT",
9
+ "UND_ERR_CONNECT_TIMEOUT",
10
+ "ENOTFOUND",
11
+ "EAI_AGAIN",
12
+ ]);
13
+ function isRetryableError(err) {
14
+ if (!(err instanceof Error))
15
+ return false;
16
+ const msg = err.message ?? "";
17
+ const code = err.cause?.code ?? err.code;
18
+ if (typeof code === "string" && RETRYABLE_CODES.has(code))
19
+ return true;
20
+ if (/ECONNRESET|ECONNREFUSED|ETIMEDOUT|UND_ERR|socket hang up/i.test(msg))
21
+ return true;
22
+ if (/HTTP (429|5\d{2})\b/i.test(msg))
23
+ return true;
24
+ if (/status (429|5\d{2})\b/i.test(msg))
25
+ return true;
26
+ if (/invalid JSON|empty response|Response body is empty/i.test(msg))
27
+ return true;
28
+ if (/missing function name/i.test(msg))
29
+ return true;
30
+ if (/overloaded|rate limit|temporarily unavailable|capacity|try again|timeout/i.test(msg))
31
+ return true;
32
+ return false;
33
+ }
34
+ function sleep(ms) {
35
+ return new Promise((resolve) => setTimeout(resolve, ms));
36
+ }
37
+ async function withRetry(fn, options) {
38
+ for (let attempt = 0;; attempt++) {
39
+ try {
40
+ return await fn();
41
+ }
42
+ catch (err) {
43
+ if (!options.isRetryable(err) || attempt >= options.maxRetries)
44
+ throw err;
45
+ const delay = Math.min(options.baseDelayMs * 2 ** attempt, options.maxDelayMs);
46
+ await sleep(delay);
47
+ }
48
+ }
49
+ }
@@ -4,6 +4,7 @@ exports.selfhostChatCompletionsAdapter = selfhostChatCompletionsAdapter;
4
4
  const abort_1 = require("../abort");
5
5
  const types_1 = require("../types");
6
6
  const sse_1 = require("./sse");
7
+ const retry_1 = require("./retry");
7
8
  const ROLE_TO_OPENAI = {
8
9
  [types_1.Role.System]: "system",
9
10
  [types_1.Role.User]: "user",
@@ -40,6 +41,13 @@ function toUsage(usage) {
40
41
  return { promptTokens, completionTokens, totalTokens };
41
42
  }
42
43
  async function selfhostChatCompletionsAdapter(config, context, tools) {
44
+ let hasStreamed = false;
45
+ const retryOptions = {
46
+ maxRetries: 2,
47
+ baseDelayMs: 1000,
48
+ maxDelayMs: 10000,
49
+ isRetryable: (err) => !hasStreamed && (0, retry_1.isRetryableError)(err),
50
+ };
43
51
  const baseUrl = config.baseUrl ?? "http://localhost:1234";
44
52
  const apiKey = config.apiKey || process.env.SELFHOST_API_KEY || "";
45
53
  const model = config.model ?? "gpt-5-nano";
@@ -61,122 +69,135 @@ async function selfhostChatCompletionsAdapter(config, context, tools) {
61
69
  stream: onStream ? true : undefined,
62
70
  stream_options: onStream ? { include_usage: true } : undefined,
63
71
  };
64
- if (await check())
65
- return { messages: [] };
66
- const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
67
- method: "POST",
68
- headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
69
- body: JSON.stringify(body),
70
- });
71
- if (!res.ok) {
72
- const text = await res.text();
73
- let errMsg = `Self-host chat completions API HTTP ${res.status}`;
74
- try {
75
- const parsed = JSON.parse(text);
76
- if (parsed?.error?.message)
77
- errMsg = parsed.error.message;
78
- }
79
- catch {
80
- if (text)
81
- errMsg += `: ${text.slice(0, 200)}`;
82
- }
83
- throw new Error(errMsg);
84
- }
85
- if (onStream) {
86
- let content = "";
87
- let usage;
88
- const toolCalls = new Map();
89
- const upsertToolCall = (tc) => {
90
- const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
91
- let entry = toolCalls.get(index);
92
- if (!entry) {
93
- entry = { args: "" };
94
- toolCalls.set(index, entry);
95
- }
96
- if (typeof tc?.id === "string")
97
- entry.id = tc.id;
98
- const fn = tc?.function;
99
- if (fn) {
100
- if (typeof fn.name === "string")
101
- entry.name = fn.name;
102
- if (typeof fn.arguments === "string")
103
- entry.args += fn.arguments;
104
- }
105
- };
106
- await (0, sse_1.readSse)(res, async (dataLine) => {
107
- if (dataLine === "[DONE]")
108
- return;
109
- let parsed;
72
+ return await (0, retry_1.withRetry)(async () => {
73
+ hasStreamed = false;
74
+ if (await check())
75
+ return { messages: [] };
76
+ const res = await fetch(`${baseUrl.replace(/\/$/, "")}/v1/chat/completions`, {
77
+ method: "POST",
78
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
79
+ body: JSON.stringify(body),
80
+ });
81
+ if (!res.ok) {
82
+ const text = await res.text();
83
+ let errMsg = `Self-host chat completions API HTTP ${res.status}`;
110
84
  try {
111
- parsed = JSON.parse(dataLine);
85
+ const parsed = JSON.parse(text);
86
+ if (parsed?.error?.message)
87
+ errMsg = `Self-host chat completions API error: ${parsed.error.message} (HTTP ${res.status})`;
112
88
  }
113
89
  catch {
114
- throw new Error(`Self-host chat completions API returned invalid JSON: ${dataLine.slice(0, 200)}`);
90
+ if (text)
91
+ errMsg += `: ${text.slice(0, 200)}`;
115
92
  }
116
- if (parsed?.usage) {
117
- const nextUsage = toUsage(parsed.usage);
118
- if (nextUsage)
119
- usage = nextUsage;
120
- }
121
- const delta = parsed?.choices?.[0]?.delta;
122
- if (!delta)
123
- return;
124
- if (Array.isArray(delta.tool_calls)) {
125
- for (const tc of delta.tool_calls) {
126
- upsertToolCall(tc);
93
+ throw new Error(errMsg);
94
+ }
95
+ if (onStream) {
96
+ let content = "";
97
+ let usage;
98
+ const toolCalls = new Map();
99
+ const upsertToolCall = (tc) => {
100
+ const index = typeof tc?.index === "number" ? tc.index : toolCalls.size;
101
+ let entry = toolCalls.get(index);
102
+ if (!entry) {
103
+ entry = { args: "" };
104
+ toolCalls.set(index, entry);
127
105
  }
128
- }
129
- if (typeof delta.content === "string") {
130
- content += delta.content;
131
- await Promise.resolve(onStream(delta.content));
132
- }
133
- await check();
134
- }, check);
135
- if (toolCalls.size > 0) {
136
- return {
137
- messages: [...toolCalls.entries()]
138
- .sort((a, b) => a[0] - b[0])
139
- .map(([index, tc]) => {
140
- if (!tc.name)
141
- throw new Error(`Self-host streaming tool call missing function name at index ${index}`);
106
+ if (typeof tc?.id === "string")
107
+ entry.id = tc.id;
108
+ const fn = tc?.function;
109
+ if (fn) {
110
+ if (typeof fn.name === "string")
111
+ entry.name = fn.name;
112
+ if (typeof fn.arguments === "string")
113
+ entry.args += fn.arguments;
114
+ }
115
+ };
116
+ await (0, sse_1.readSse)(res, async (dataLine) => {
117
+ if (dataLine === "[DONE]")
118
+ return;
119
+ let parsed;
120
+ try {
121
+ parsed = JSON.parse(dataLine);
122
+ }
123
+ catch {
124
+ throw new Error(`Self-host chat completions API returned invalid JSON: ${dataLine.slice(0, 200)}`);
125
+ }
126
+ if (parsed?.usage) {
127
+ const nextUsage = toUsage(parsed.usage);
128
+ if (nextUsage)
129
+ usage = nextUsage;
130
+ }
131
+ const delta = parsed?.choices?.[0]?.delta;
132
+ if (!delta)
133
+ return;
134
+ if (Array.isArray(delta.tool_calls)) {
135
+ for (const tc of delta.tool_calls) {
136
+ upsertToolCall(tc);
137
+ }
138
+ }
139
+ if (typeof delta.content === "string") {
140
+ content += delta.content;
141
+ hasStreamed = true;
142
+ await Promise.resolve(onStream(delta.content));
143
+ }
144
+ await check();
145
+ }, check);
146
+ if (toolCalls.size > 0) {
147
+ const ordered = [...toolCalls.entries()].sort((a, b) => a[0] - b[0]);
148
+ const valid = ordered.filter(([index, tc]) => {
149
+ if (!tc.name) {
150
+ console.warn(`Self-host chat completions adapter: skipping incomplete streaming tool call at index ${index}`);
151
+ return false;
152
+ }
153
+ return true;
154
+ });
155
+ if (valid.length > 0) {
142
156
  return {
143
- role: types_1.Role.ToolCall,
144
- toolName: tc.name,
145
- callId: tc.id ?? `call_${index}`,
146
- argsText: tc.args.length ? tc.args : "{}",
157
+ messages: valid.map(([index, tc]) => ({
158
+ role: types_1.Role.ToolCall,
159
+ toolName: tc.name,
160
+ callId: tc.id ?? `call_${index}`,
161
+ argsText: tc.args.length ? tc.args : "{}",
162
+ })),
163
+ usage,
147
164
  };
148
- }),
165
+ }
166
+ if (!content) {
167
+ if (isAborted())
168
+ return { messages: [], usage };
169
+ throw new Error("Self-host chat completions API returned empty response");
170
+ }
171
+ }
172
+ if (!content)
173
+ return { messages: [], usage };
174
+ return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
175
+ }
176
+ const text = await res.text();
177
+ let data;
178
+ try {
179
+ data = JSON.parse(text);
180
+ }
181
+ catch {
182
+ throw new Error(`Self-host chat completions API returned invalid JSON: ${text.slice(0, 200)}`);
183
+ }
184
+ if (data.error)
185
+ throw new Error(`Self-host chat completions API error: ${data.error.message}`);
186
+ const usage = toUsage(data.usage);
187
+ const msg = data.choices?.[0]?.message;
188
+ if (!msg)
189
+ throw new Error("Self-host chat completions API returned empty response");
190
+ if (msg.tool_calls?.length) {
191
+ return {
192
+ messages: msg.tool_calls.map((tc) => ({
193
+ role: types_1.Role.ToolCall,
194
+ toolName: tc.function.name,
195
+ callId: tc.id,
196
+ argsText: tc.function.arguments ?? "{}",
197
+ })),
149
198
  usage,
150
199
  };
151
200
  }
152
- if (!content)
153
- return { messages: [], usage };
154
- return { messages: [{ role: types_1.Role.Ai, content, isPartial: isAborted() ? true : undefined }], usage };
155
- }
156
- const text = await res.text();
157
- let data;
158
- try {
159
- data = JSON.parse(text);
160
- }
161
- catch {
162
- throw new Error(`Self-host chat completions API returned invalid JSON: ${text.slice(0, 200)}`);
163
- }
164
- if (data.error)
165
- throw new Error(`Self-host chat completions API error: ${data.error.message}`);
166
- const usage = toUsage(data.usage);
167
- const msg = data.choices?.[0]?.message;
168
- if (!msg)
169
- throw new Error("Self-host chat completions API returned empty response");
170
- if (msg.tool_calls?.length) {
171
- return {
172
- messages: msg.tool_calls.map((tc) => ({
173
- role: types_1.Role.ToolCall,
174
- toolName: tc.function.name,
175
- callId: tc.id,
176
- argsText: tc.function.arguments ?? "{}",
177
- })),
178
- usage,
179
- };
180
- }
181
- return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
201
+ return { messages: [{ role: types_1.Role.Ai, content: msg.content ?? "" }], usage };
202
+ }, retryOptions);
182
203
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arki-moe/agent-ts",
3
- "version": "6.0.0",
3
+ "version": "6.0.1",
4
4
  "description": "Minimal Agent library, zero dependencies",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",