@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.
- package/dist/adapter/openai.js +129 -108
- package/dist/adapter/openrouter.js +129 -108
- package/dist/adapter/retry.d.ts +9 -0
- package/dist/adapter/retry.js +49 -0
- package/dist/adapter/selfhost_chat_completions.js +129 -108
- package/package.json +1 -1
package/dist/adapter/openai.js
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
+
if (text)
|
|
91
|
+
errMsg += `: ${text.slice(0, 200)}`;
|
|
115
92
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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(
|
|
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
|
-
|
|
100
|
+
if (text)
|
|
101
|
+
errMsg += `: ${text.slice(0, 200)}`;
|
|
125
102
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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(
|
|
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
|
-
|
|
90
|
+
if (text)
|
|
91
|
+
errMsg += `: ${text.slice(0, 200)}`;
|
|
115
92
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
}
|