@probeo/anymodel 0.3.0 → 0.3.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/README.md +1 -10
- package/dist/cli.cjs +827 -108
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +2785 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +848 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -22
- package/dist/index.d.ts +632 -14
- package/dist/index.js +2782 -15
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/dist/batch/index.d.ts +0 -4
- package/dist/batch/index.d.ts.map +0 -1
- package/dist/batch/index.js +0 -3
- package/dist/batch/index.js.map +0 -1
- package/dist/batch/manager.d.ts +0 -72
- package/dist/batch/manager.d.ts.map +0 -1
- package/dist/batch/manager.js +0 -328
- package/dist/batch/manager.js.map +0 -1
- package/dist/batch/store.d.ts +0 -54
- package/dist/batch/store.d.ts.map +0 -1
- package/dist/batch/store.js +0 -109
- package/dist/batch/store.js.map +0 -1
- package/dist/cli.d.ts.map +0 -1
- package/dist/client.d.ts +0 -42
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js +0 -181
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -120
- package/dist/config.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/providers/adapter.d.ts +0 -33
- package/dist/providers/adapter.d.ts.map +0 -1
- package/dist/providers/adapter.js +0 -2
- package/dist/providers/adapter.js.map +0 -1
- package/dist/providers/anthropic-batch.d.ts +0 -3
- package/dist/providers/anthropic-batch.d.ts.map +0 -1
- package/dist/providers/anthropic-batch.js +0 -228
- package/dist/providers/anthropic-batch.js.map +0 -1
- package/dist/providers/anthropic.d.ts +0 -3
- package/dist/providers/anthropic.d.ts.map +0 -1
- package/dist/providers/anthropic.js +0 -358
- package/dist/providers/anthropic.js.map +0 -1
- package/dist/providers/custom.d.ts +0 -8
- package/dist/providers/custom.d.ts.map +0 -1
- package/dist/providers/custom.js +0 -61
- package/dist/providers/custom.js.map +0 -1
- package/dist/providers/google.d.ts +0 -3
- package/dist/providers/google.d.ts.map +0 -1
- package/dist/providers/google.js +0 -331
- package/dist/providers/google.js.map +0 -1
- package/dist/providers/index.d.ts +0 -6
- package/dist/providers/index.d.ts.map +0 -1
- package/dist/providers/index.js +0 -5
- package/dist/providers/index.js.map +0 -1
- package/dist/providers/openai-batch.d.ts +0 -3
- package/dist/providers/openai-batch.d.ts.map +0 -1
- package/dist/providers/openai-batch.js +0 -208
- package/dist/providers/openai-batch.js.map +0 -1
- package/dist/providers/openai.d.ts +0 -3
- package/dist/providers/openai.d.ts.map +0 -1
- package/dist/providers/openai.js +0 -221
- package/dist/providers/openai.js.map +0 -1
- package/dist/providers/registry.d.ts +0 -10
- package/dist/providers/registry.d.ts.map +0 -1
- package/dist/providers/registry.js +0 -27
- package/dist/providers/registry.js.map +0 -1
- package/dist/router.d.ts +0 -29
- package/dist/router.d.ts.map +0 -1
- package/dist/router.js +0 -212
- package/dist/router.js.map +0 -1
- package/dist/server.d.ts +0 -10
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -149
- package/dist/server.js.map +0 -1
- package/dist/types.d.ts +0 -283
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -21
- package/dist/types.js.map +0 -1
- package/dist/utils/fs-io.d.ts +0 -40
- package/dist/utils/fs-io.d.ts.map +0 -1
- package/dist/utils/fs-io.js +0 -203
- package/dist/utils/fs-io.js.map +0 -1
- package/dist/utils/generation-stats.d.ts +0 -25
- package/dist/utils/generation-stats.d.ts.map +0 -1
- package/dist/utils/generation-stats.js +0 -46
- package/dist/utils/generation-stats.js.map +0 -1
- package/dist/utils/id.d.ts +0 -2
- package/dist/utils/id.d.ts.map +0 -1
- package/dist/utils/id.js +0 -6
- package/dist/utils/id.js.map +0 -1
- package/dist/utils/model-parser.d.ts +0 -6
- package/dist/utils/model-parser.d.ts.map +0 -1
- package/dist/utils/model-parser.js +0 -21
- package/dist/utils/model-parser.js.map +0 -1
- package/dist/utils/rate-limiter.d.ts +0 -36
- package/dist/utils/rate-limiter.d.ts.map +0 -1
- package/dist/utils/rate-limiter.js +0 -80
- package/dist/utils/rate-limiter.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -7
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js +0 -54
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/transforms.d.ts +0 -7
- package/dist/utils/transforms.d.ts.map +0 -1
- package/dist/utils/transforms.js +0 -66
- package/dist/utils/transforms.js.map +0 -1
- package/dist/utils/validate.d.ts +0 -3
- package/dist/utils/validate.d.ts.map +0 -1
- package/dist/utils/validate.js +0 -31
- package/dist/utils/validate.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,16 +1,2783 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
// src/types.ts
|
|
2
|
+
var AnyModelError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
metadata;
|
|
5
|
+
constructor(code, message, metadata = {}) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "AnyModelError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.metadata = metadata;
|
|
10
|
+
}
|
|
11
|
+
toJSON() {
|
|
12
|
+
return {
|
|
13
|
+
error: {
|
|
14
|
+
code: this.code,
|
|
15
|
+
message: this.message,
|
|
16
|
+
metadata: this.metadata
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/providers/registry.ts
|
|
23
|
+
var ProviderRegistry = class {
|
|
24
|
+
adapters = /* @__PURE__ */ new Map();
|
|
25
|
+
register(slug, adapter) {
|
|
26
|
+
if (this.adapters.has(slug)) {
|
|
27
|
+
throw new AnyModelError(500, `Provider '${slug}' is already registered`);
|
|
28
|
+
}
|
|
29
|
+
this.adapters.set(slug, adapter);
|
|
30
|
+
}
|
|
31
|
+
get(slug) {
|
|
32
|
+
const adapter = this.adapters.get(slug);
|
|
33
|
+
if (!adapter) {
|
|
34
|
+
throw new AnyModelError(400, `Provider '${slug}' not configured`);
|
|
35
|
+
}
|
|
36
|
+
return adapter;
|
|
37
|
+
}
|
|
38
|
+
has(slug) {
|
|
39
|
+
return this.adapters.has(slug);
|
|
40
|
+
}
|
|
41
|
+
list() {
|
|
42
|
+
return Array.from(this.adapters.keys());
|
|
43
|
+
}
|
|
44
|
+
all() {
|
|
45
|
+
return Array.from(this.adapters.values());
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/utils/model-parser.ts
|
|
50
|
+
function parseModelString(model, aliases) {
|
|
51
|
+
if (aliases && model in aliases) {
|
|
52
|
+
model = aliases[model];
|
|
53
|
+
}
|
|
54
|
+
const slashIndex = model.indexOf("/");
|
|
55
|
+
if (slashIndex === -1) {
|
|
56
|
+
throw new AnyModelError(
|
|
57
|
+
400,
|
|
58
|
+
`Model must be in provider/model format or be a valid alias. Got: '${model}'`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const provider = model.substring(0, slashIndex);
|
|
62
|
+
const modelId = model.substring(slashIndex + 1);
|
|
63
|
+
if (!provider) {
|
|
64
|
+
throw new AnyModelError(400, `Invalid model string: missing provider in '${model}'`);
|
|
65
|
+
}
|
|
66
|
+
if (!modelId) {
|
|
67
|
+
throw new AnyModelError(400, `Invalid model string: missing model ID in '${model}'`);
|
|
68
|
+
}
|
|
69
|
+
return { provider, model: modelId };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/utils/validate.ts
|
|
73
|
+
function validateRequest(request) {
|
|
74
|
+
if (!request.model && !request.models?.length) {
|
|
75
|
+
throw new AnyModelError(400, "Missing required field: model");
|
|
76
|
+
}
|
|
77
|
+
if (!request.messages || !Array.isArray(request.messages) || request.messages.length === 0) {
|
|
78
|
+
throw new AnyModelError(400, "Missing or empty required field: messages");
|
|
79
|
+
}
|
|
80
|
+
if (request.temperature !== void 0 && (request.temperature < 0 || request.temperature > 2)) {
|
|
81
|
+
throw new AnyModelError(400, "temperature must be between 0.0 and 2.0");
|
|
82
|
+
}
|
|
83
|
+
if (request.top_p !== void 0 && (request.top_p < 0 || request.top_p > 1)) {
|
|
84
|
+
throw new AnyModelError(400, "top_p must be between 0.0 and 1.0");
|
|
85
|
+
}
|
|
86
|
+
if (request.top_logprobs !== void 0 && !request.logprobs) {
|
|
87
|
+
throw new AnyModelError(400, "top_logprobs requires logprobs: true");
|
|
88
|
+
}
|
|
89
|
+
if (request.top_logprobs !== void 0 && (request.top_logprobs < 0 || request.top_logprobs > 20)) {
|
|
90
|
+
throw new AnyModelError(400, "top_logprobs must be between 0 and 20");
|
|
91
|
+
}
|
|
92
|
+
if (request.stop !== void 0) {
|
|
93
|
+
const stops = Array.isArray(request.stop) ? request.stop : [request.stop];
|
|
94
|
+
if (stops.length > 4) {
|
|
95
|
+
throw new AnyModelError(400, "stop may have at most 4 sequences");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (request.models && request.models.length > 0 && request.route && request.route !== "fallback") {
|
|
99
|
+
throw new AnyModelError(400, `Invalid route: '${request.route}'. Only 'fallback' is supported.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/utils/retry.ts
|
|
104
|
+
var DEFAULT_RETRY = {
|
|
105
|
+
maxRetries: 2,
|
|
106
|
+
baseDelay: 500,
|
|
107
|
+
maxDelay: 1e4
|
|
108
|
+
};
|
|
109
|
+
var RETRYABLE_CODES = /* @__PURE__ */ new Set([429, 502, 503, 529]);
|
|
110
|
+
function isRetryable(error) {
|
|
111
|
+
if (error instanceof AnyModelError) {
|
|
112
|
+
return RETRYABLE_CODES.has(error.code);
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
function getRetryAfter(error) {
|
|
117
|
+
if (error instanceof AnyModelError && error.metadata.raw) {
|
|
118
|
+
const raw = error.metadata.raw;
|
|
119
|
+
if (raw?.retry_after) return Number(raw.retry_after) * 1e3;
|
|
120
|
+
if (raw?.headers?.["retry-after"]) return Number(raw.headers["retry-after"]) * 1e3;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
function computeDelay(attempt, options, error) {
|
|
125
|
+
const retryAfter = getRetryAfter(error);
|
|
126
|
+
if (retryAfter && retryAfter > 0) {
|
|
127
|
+
return Math.min(retryAfter, options.maxDelay);
|
|
128
|
+
}
|
|
129
|
+
const exponential = options.baseDelay * Math.pow(2, attempt);
|
|
130
|
+
const jitter = exponential * 0.2 * Math.random();
|
|
131
|
+
return Math.min(exponential + jitter, options.maxDelay);
|
|
132
|
+
}
|
|
133
|
+
async function withRetry(fn, options = {}) {
|
|
134
|
+
const opts = { ...DEFAULT_RETRY, ...options };
|
|
135
|
+
let lastError;
|
|
136
|
+
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
return await fn();
|
|
139
|
+
} catch (error) {
|
|
140
|
+
lastError = error;
|
|
141
|
+
if (attempt >= opts.maxRetries || !isRetryable(error)) {
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
const delay = computeDelay(attempt, opts, error);
|
|
145
|
+
await new Promise((resolve2) => setTimeout(resolve2, delay));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
throw lastError;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/utils/rate-limiter.ts
|
|
152
|
+
var RateLimitTracker = class {
|
|
153
|
+
state = /* @__PURE__ */ new Map();
|
|
154
|
+
/**
|
|
155
|
+
* Update rate limit state from response headers.
|
|
156
|
+
*/
|
|
157
|
+
updateFromHeaders(provider, headers) {
|
|
158
|
+
const state = this.getOrCreate(provider);
|
|
159
|
+
const remaining = headers["x-ratelimit-remaining"] || headers["x-ratelimit-remaining-requests"];
|
|
160
|
+
if (remaining !== void 0) {
|
|
161
|
+
state.remaining = parseInt(remaining, 10);
|
|
162
|
+
}
|
|
163
|
+
const reset = headers["x-ratelimit-reset"] || headers["x-ratelimit-reset-requests"];
|
|
164
|
+
if (reset !== void 0) {
|
|
165
|
+
const parsed = Number(reset);
|
|
166
|
+
state.resetAt = parsed > 1e12 ? parsed : parsed * 1e3;
|
|
167
|
+
}
|
|
168
|
+
const retryAfter = headers["retry-after"];
|
|
169
|
+
if (retryAfter !== void 0) {
|
|
170
|
+
state.retryAfter = Number(retryAfter) * 1e3;
|
|
171
|
+
}
|
|
172
|
+
state.lastUpdated = Date.now();
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Record a 429 for a provider.
|
|
176
|
+
*/
|
|
177
|
+
recordRateLimit(provider, retryAfterMs) {
|
|
178
|
+
const state = this.getOrCreate(provider);
|
|
179
|
+
state.remaining = 0;
|
|
180
|
+
if (retryAfterMs) {
|
|
181
|
+
state.retryAfter = retryAfterMs;
|
|
182
|
+
state.resetAt = Date.now() + retryAfterMs;
|
|
183
|
+
}
|
|
184
|
+
state.lastUpdated = Date.now();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if a provider is currently rate-limited.
|
|
188
|
+
*/
|
|
189
|
+
isRateLimited(provider) {
|
|
190
|
+
const state = this.state.get(provider);
|
|
191
|
+
if (!state) return false;
|
|
192
|
+
if (state.remaining === 0 && state.resetAt) {
|
|
193
|
+
return Date.now() < state.resetAt;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get ms until rate limit resets for a provider.
|
|
199
|
+
*/
|
|
200
|
+
getWaitTime(provider) {
|
|
201
|
+
const state = this.state.get(provider);
|
|
202
|
+
if (!state?.resetAt) return 0;
|
|
203
|
+
const wait = state.resetAt - Date.now();
|
|
204
|
+
return Math.max(0, wait);
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Get state for a provider.
|
|
208
|
+
*/
|
|
209
|
+
getState(provider) {
|
|
210
|
+
return this.state.get(provider);
|
|
211
|
+
}
|
|
212
|
+
getOrCreate(provider) {
|
|
213
|
+
let state = this.state.get(provider);
|
|
214
|
+
if (!state) {
|
|
215
|
+
state = {
|
|
216
|
+
provider,
|
|
217
|
+
remaining: null,
|
|
218
|
+
resetAt: null,
|
|
219
|
+
retryAfter: null,
|
|
220
|
+
lastUpdated: Date.now()
|
|
221
|
+
};
|
|
222
|
+
this.state.set(provider, state);
|
|
223
|
+
}
|
|
224
|
+
return state;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// src/utils/transforms.ts
|
|
229
|
+
var CHARS_PER_TOKEN = 4;
|
|
230
|
+
function middleOut(messages, maxTokens) {
|
|
231
|
+
if (messages.length <= 2) return messages;
|
|
232
|
+
const maxChars = maxTokens * CHARS_PER_TOKEN;
|
|
233
|
+
function messageLength(msg) {
|
|
234
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
235
|
+
return content.length + 20;
|
|
236
|
+
}
|
|
237
|
+
const totalChars = messages.reduce((sum, m) => sum + messageLength(m), 0);
|
|
238
|
+
if (totalChars <= maxChars) return messages;
|
|
239
|
+
const systemMessages = messages.filter((m) => m.role === "system");
|
|
240
|
+
const conversationMessages = messages.filter((m) => m.role !== "system");
|
|
241
|
+
if (conversationMessages.length <= 2) return messages;
|
|
242
|
+
const systemChars = systemMessages.reduce((sum, m) => sum + messageLength(m), 0);
|
|
243
|
+
const budgetForConversation = maxChars - systemChars;
|
|
244
|
+
if (budgetForConversation <= 0) return [...systemMessages, conversationMessages[conversationMessages.length - 1]];
|
|
245
|
+
const kept = [];
|
|
246
|
+
let usedChars = 0;
|
|
247
|
+
const tail = [];
|
|
248
|
+
let tailChars = 0;
|
|
249
|
+
for (let i = conversationMessages.length - 1; i >= 0; i--) {
|
|
250
|
+
const len = messageLength(conversationMessages[i]);
|
|
251
|
+
if (tailChars + len > budgetForConversation * 0.7) break;
|
|
252
|
+
tail.unshift(conversationMessages[i]);
|
|
253
|
+
tailChars += len;
|
|
254
|
+
}
|
|
255
|
+
const headBudget = budgetForConversation - tailChars;
|
|
256
|
+
const headEnd = conversationMessages.length - tail.length;
|
|
257
|
+
for (let i = 0; i < headEnd; i++) {
|
|
258
|
+
const len = messageLength(conversationMessages[i]);
|
|
259
|
+
if (usedChars + len > headBudget) break;
|
|
260
|
+
kept.push(conversationMessages[i]);
|
|
261
|
+
usedChars += len;
|
|
262
|
+
}
|
|
263
|
+
return [...systemMessages, ...kept, ...tail];
|
|
264
|
+
}
|
|
265
|
+
function applyTransform(name, messages, contextLength) {
|
|
266
|
+
if (name === "middle-out") {
|
|
267
|
+
return middleOut(messages, contextLength);
|
|
268
|
+
}
|
|
269
|
+
return messages;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// src/router.ts
|
|
273
|
+
var STRIPPABLE_PARAMS = [
|
|
274
|
+
"temperature",
|
|
275
|
+
"max_tokens",
|
|
276
|
+
"top_p",
|
|
277
|
+
"top_k",
|
|
278
|
+
"frequency_penalty",
|
|
279
|
+
"presence_penalty",
|
|
280
|
+
"repetition_penalty",
|
|
281
|
+
"seed",
|
|
282
|
+
"stop",
|
|
283
|
+
"logprobs",
|
|
284
|
+
"top_logprobs",
|
|
285
|
+
"response_format",
|
|
286
|
+
"tools",
|
|
287
|
+
"tool_choice",
|
|
288
|
+
"user"
|
|
289
|
+
];
|
|
290
|
+
var Router = class {
|
|
291
|
+
constructor(registry, aliases, config) {
|
|
292
|
+
this.registry = registry;
|
|
293
|
+
this.aliases = aliases;
|
|
294
|
+
this.config = config;
|
|
295
|
+
}
|
|
296
|
+
rateLimiter = new RateLimitTracker();
|
|
297
|
+
/**
|
|
298
|
+
* Strip parameters that the target provider doesn't support.
|
|
299
|
+
*/
|
|
300
|
+
stripUnsupported(request, adapter) {
|
|
301
|
+
const cleaned = { ...request };
|
|
302
|
+
for (const param of STRIPPABLE_PARAMS) {
|
|
303
|
+
if (cleaned[param] !== void 0 && !adapter.supportsParameter(param)) {
|
|
304
|
+
delete cleaned[param];
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return cleaned;
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Apply transforms (e.g., middle-out) to the request messages.
|
|
311
|
+
*/
|
|
312
|
+
applyTransforms(request) {
|
|
313
|
+
const transforms = request.transforms || this.config?.defaults?.transforms;
|
|
314
|
+
if (!transforms || transforms.length === 0) return request;
|
|
315
|
+
let messages = [...request.messages];
|
|
316
|
+
const contextLength = 128e3;
|
|
317
|
+
for (const transform of transforms) {
|
|
318
|
+
messages = applyTransform(transform, messages, contextLength);
|
|
319
|
+
}
|
|
320
|
+
return { ...request, messages };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Order models based on provider preferences.
|
|
324
|
+
*/
|
|
325
|
+
applyProviderPreferences(models, prefs) {
|
|
326
|
+
if (!prefs) return models;
|
|
327
|
+
let filtered = [...models];
|
|
328
|
+
if (prefs.only && prefs.only.length > 0) {
|
|
329
|
+
const onlySet = new Set(prefs.only);
|
|
330
|
+
filtered = filtered.filter((m) => {
|
|
331
|
+
const { provider } = parseModelString(m, this.aliases);
|
|
332
|
+
return onlySet.has(provider);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (prefs.ignore && prefs.ignore.length > 0) {
|
|
336
|
+
const ignoreSet = new Set(prefs.ignore);
|
|
337
|
+
filtered = filtered.filter((m) => {
|
|
338
|
+
const { provider } = parseModelString(m, this.aliases);
|
|
339
|
+
return !ignoreSet.has(provider);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
if (prefs.order && prefs.order.length > 0) {
|
|
343
|
+
const orderMap = new Map(prefs.order.map((p, i) => [p, i]));
|
|
344
|
+
filtered.sort((a, b) => {
|
|
345
|
+
const aProvider = parseModelString(a, this.aliases).provider;
|
|
346
|
+
const bProvider = parseModelString(b, this.aliases).provider;
|
|
347
|
+
const aOrder = orderMap.get(aProvider) ?? Infinity;
|
|
348
|
+
const bOrder = orderMap.get(bProvider) ?? Infinity;
|
|
349
|
+
return aOrder - bOrder;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (prefs.require_parameters) {
|
|
353
|
+
filtered = filtered.filter((m) => {
|
|
354
|
+
try {
|
|
355
|
+
const { provider } = parseModelString(m, this.aliases);
|
|
356
|
+
const adapter = this.registry.get(provider);
|
|
357
|
+
return adapter !== void 0;
|
|
358
|
+
} catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
filtered = filtered.filter((m) => {
|
|
364
|
+
const { provider } = parseModelString(m, this.aliases);
|
|
365
|
+
return !this.rateLimiter.isRateLimited(provider);
|
|
366
|
+
});
|
|
367
|
+
return filtered;
|
|
368
|
+
}
|
|
369
|
+
getRetryOptions() {
|
|
370
|
+
return {
|
|
371
|
+
maxRetries: this.config?.defaults?.retries ?? 2
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
async complete(request) {
|
|
375
|
+
validateRequest(request);
|
|
376
|
+
const transformed = this.applyTransforms(request);
|
|
377
|
+
if (transformed.models && transformed.models.length > 0 && transformed.route === "fallback") {
|
|
378
|
+
return this.completeWithFallback(transformed);
|
|
379
|
+
}
|
|
380
|
+
const { provider, model } = parseModelString(transformed.model, this.aliases);
|
|
381
|
+
const adapter = this.registry.get(provider);
|
|
382
|
+
const resolvedRequest = this.stripUnsupported({ ...transformed, model }, adapter);
|
|
383
|
+
return withRetry(
|
|
384
|
+
() => adapter.sendRequest(resolvedRequest),
|
|
385
|
+
this.getRetryOptions()
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
async stream(request) {
|
|
389
|
+
validateRequest(request);
|
|
390
|
+
const transformed = this.applyTransforms(request);
|
|
391
|
+
if (transformed.models && transformed.models.length > 0 && transformed.route === "fallback") {
|
|
392
|
+
return this.streamWithFallback(transformed);
|
|
393
|
+
}
|
|
394
|
+
const { provider, model } = parseModelString(transformed.model, this.aliases);
|
|
395
|
+
const adapter = this.registry.get(provider);
|
|
396
|
+
const resolvedRequest = this.stripUnsupported({ ...transformed, model, stream: true }, adapter);
|
|
397
|
+
return withRetry(
|
|
398
|
+
() => adapter.sendStreamingRequest(resolvedRequest),
|
|
399
|
+
this.getRetryOptions()
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
async completeWithFallback(request) {
|
|
403
|
+
let models = request.models;
|
|
404
|
+
const errors = [];
|
|
405
|
+
models = this.applyProviderPreferences(models, request.provider);
|
|
406
|
+
for (const modelStr of models) {
|
|
407
|
+
try {
|
|
408
|
+
const { provider, model } = parseModelString(modelStr, this.aliases);
|
|
409
|
+
const adapter = this.registry.get(provider);
|
|
410
|
+
const resolvedRequest = this.stripUnsupported(
|
|
411
|
+
{ ...request, model, models: void 0, route: void 0 },
|
|
412
|
+
adapter
|
|
413
|
+
);
|
|
414
|
+
const response = await withRetry(
|
|
415
|
+
() => adapter.sendRequest(resolvedRequest),
|
|
416
|
+
this.getRetryOptions()
|
|
417
|
+
);
|
|
418
|
+
response.model = modelStr;
|
|
419
|
+
return response;
|
|
420
|
+
} catch (err) {
|
|
421
|
+
const error = err instanceof AnyModelError ? err : new AnyModelError(500, String(err));
|
|
422
|
+
if (error.code === 429) {
|
|
423
|
+
const { provider } = parseModelString(modelStr, this.aliases);
|
|
424
|
+
this.rateLimiter.recordRateLimit(provider);
|
|
425
|
+
}
|
|
426
|
+
errors.push({ model: modelStr, error });
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const lastError = errors[errors.length - 1];
|
|
430
|
+
throw new AnyModelError(lastError.error.code, lastError.error.message, {
|
|
431
|
+
...lastError.error.metadata,
|
|
432
|
+
raw: {
|
|
433
|
+
attempts: errors.map((e) => ({
|
|
434
|
+
model: e.model,
|
|
435
|
+
code: e.error.code,
|
|
436
|
+
message: e.error.message
|
|
437
|
+
}))
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async streamWithFallback(request) {
|
|
442
|
+
let models = request.models;
|
|
443
|
+
const errors = [];
|
|
444
|
+
models = this.applyProviderPreferences(models, request.provider);
|
|
445
|
+
for (const modelStr of models) {
|
|
446
|
+
try {
|
|
447
|
+
const { provider, model } = parseModelString(modelStr, this.aliases);
|
|
448
|
+
const adapter = this.registry.get(provider);
|
|
449
|
+
const resolvedRequest = this.stripUnsupported(
|
|
450
|
+
{ ...request, model, models: void 0, route: void 0, stream: true },
|
|
451
|
+
adapter
|
|
452
|
+
);
|
|
453
|
+
return await withRetry(
|
|
454
|
+
() => adapter.sendStreamingRequest(resolvedRequest),
|
|
455
|
+
this.getRetryOptions()
|
|
456
|
+
);
|
|
457
|
+
} catch (err) {
|
|
458
|
+
const error = err instanceof AnyModelError ? err : new AnyModelError(500, String(err));
|
|
459
|
+
if (error.code === 429) {
|
|
460
|
+
const { provider } = parseModelString(modelStr, this.aliases);
|
|
461
|
+
this.rateLimiter.recordRateLimit(provider);
|
|
462
|
+
}
|
|
463
|
+
errors.push({ model: modelStr, error });
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
const lastError = errors[errors.length - 1];
|
|
467
|
+
throw new AnyModelError(lastError.error.code, lastError.error.message, {
|
|
468
|
+
...lastError.error.metadata,
|
|
469
|
+
raw: {
|
|
470
|
+
attempts: errors.map((e) => ({
|
|
471
|
+
model: e.model,
|
|
472
|
+
code: e.error.code,
|
|
473
|
+
message: e.error.message
|
|
474
|
+
}))
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
}
|
|
478
|
+
getRateLimiter() {
|
|
479
|
+
return this.rateLimiter;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// src/providers/openai.ts
|
|
484
|
+
var OPENAI_API_BASE = "https://api.openai.com/v1";
|
|
485
|
+
var SUPPORTED_PARAMS = /* @__PURE__ */ new Set([
|
|
486
|
+
"temperature",
|
|
487
|
+
"max_tokens",
|
|
488
|
+
"top_p",
|
|
489
|
+
"frequency_penalty",
|
|
490
|
+
"presence_penalty",
|
|
491
|
+
"seed",
|
|
492
|
+
"stop",
|
|
493
|
+
"stream",
|
|
494
|
+
"logprobs",
|
|
495
|
+
"top_logprobs",
|
|
496
|
+
"response_format",
|
|
497
|
+
"tools",
|
|
498
|
+
"tool_choice",
|
|
499
|
+
"user",
|
|
500
|
+
"logit_bias"
|
|
501
|
+
]);
|
|
502
|
+
function createOpenAIAdapter(apiKey, baseURL) {
|
|
503
|
+
const base = baseURL || OPENAI_API_BASE;
|
|
504
|
+
async function makeRequest(path2, body, method = "POST") {
|
|
505
|
+
const res = await fetch(`${base}${path2}`, {
|
|
506
|
+
method,
|
|
507
|
+
headers: {
|
|
508
|
+
"Content-Type": "application/json",
|
|
509
|
+
"Authorization": `Bearer ${apiKey}`
|
|
510
|
+
},
|
|
511
|
+
body: body ? JSON.stringify(body) : void 0
|
|
512
|
+
});
|
|
513
|
+
if (!res.ok) {
|
|
514
|
+
let errorBody;
|
|
515
|
+
try {
|
|
516
|
+
errorBody = await res.json();
|
|
517
|
+
} catch {
|
|
518
|
+
errorBody = { message: res.statusText };
|
|
519
|
+
}
|
|
520
|
+
const msg = errorBody?.error?.message || errorBody?.message || res.statusText;
|
|
521
|
+
throw new AnyModelError(mapErrorCode(res.status), msg, {
|
|
522
|
+
provider_name: "openai",
|
|
523
|
+
raw: errorBody
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
return res;
|
|
527
|
+
}
|
|
528
|
+
function mapErrorCode(status) {
|
|
529
|
+
if (status === 401 || status === 403) return 401;
|
|
530
|
+
if (status === 429) return 429;
|
|
531
|
+
if (status === 400 || status === 422) return 400;
|
|
532
|
+
if (status >= 500) return 502;
|
|
533
|
+
return status;
|
|
534
|
+
}
|
|
535
|
+
function rePrefixId(id) {
|
|
536
|
+
if (id && id.startsWith("chatcmpl-")) {
|
|
537
|
+
return `gen-${id.substring(9)}`;
|
|
538
|
+
}
|
|
539
|
+
return id.startsWith("gen-") ? id : `gen-${id}`;
|
|
540
|
+
}
|
|
541
|
+
function buildRequestBody(request) {
|
|
542
|
+
const body = {
|
|
543
|
+
model: request.model,
|
|
544
|
+
messages: request.messages
|
|
545
|
+
};
|
|
546
|
+
if (request.temperature !== void 0) body.temperature = request.temperature;
|
|
547
|
+
if (request.max_tokens !== void 0) body.max_tokens = request.max_tokens;
|
|
548
|
+
if (request.top_p !== void 0) body.top_p = request.top_p;
|
|
549
|
+
if (request.frequency_penalty !== void 0) body.frequency_penalty = request.frequency_penalty;
|
|
550
|
+
if (request.presence_penalty !== void 0) body.presence_penalty = request.presence_penalty;
|
|
551
|
+
if (request.seed !== void 0) body.seed = request.seed;
|
|
552
|
+
if (request.stop !== void 0) body.stop = request.stop;
|
|
553
|
+
if (request.stream !== void 0) body.stream = request.stream;
|
|
554
|
+
if (request.logprobs !== void 0) body.logprobs = request.logprobs;
|
|
555
|
+
if (request.top_logprobs !== void 0) body.top_logprobs = request.top_logprobs;
|
|
556
|
+
if (request.response_format !== void 0) body.response_format = request.response_format;
|
|
557
|
+
if (request.tools !== void 0) body.tools = request.tools;
|
|
558
|
+
if (request.tool_choice !== void 0) body.tool_choice = request.tool_choice;
|
|
559
|
+
if (request.user !== void 0) body.user = request.user;
|
|
560
|
+
return body;
|
|
561
|
+
}
|
|
562
|
+
const adapter = {
|
|
563
|
+
name: "openai",
|
|
564
|
+
translateRequest(request) {
|
|
565
|
+
return buildRequestBody(request);
|
|
566
|
+
},
|
|
567
|
+
translateResponse(response) {
|
|
568
|
+
const r = response;
|
|
569
|
+
return {
|
|
570
|
+
id: rePrefixId(r.id),
|
|
571
|
+
object: "chat.completion",
|
|
572
|
+
created: r.created,
|
|
573
|
+
model: `openai/${r.model}`,
|
|
574
|
+
choices: r.choices,
|
|
575
|
+
usage: r.usage
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
async *translateStream(stream) {
|
|
579
|
+
const reader = stream.getReader();
|
|
580
|
+
const decoder = new TextDecoder();
|
|
581
|
+
let buffer = "";
|
|
582
|
+
try {
|
|
583
|
+
while (true) {
|
|
584
|
+
const { done, value } = await reader.read();
|
|
585
|
+
if (done) break;
|
|
586
|
+
buffer += decoder.decode(value, { stream: true });
|
|
587
|
+
const lines = buffer.split("\n");
|
|
588
|
+
buffer = lines.pop() || "";
|
|
589
|
+
for (const line of lines) {
|
|
590
|
+
const trimmed = line.trim();
|
|
591
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
592
|
+
if (trimmed === "data: [DONE]") return;
|
|
593
|
+
if (trimmed.startsWith("data: ")) {
|
|
594
|
+
const json = JSON.parse(trimmed.substring(6));
|
|
595
|
+
json.id = rePrefixId(json.id);
|
|
596
|
+
json.model = `openai/${json.model}`;
|
|
597
|
+
yield json;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
} finally {
|
|
602
|
+
reader.releaseLock();
|
|
603
|
+
}
|
|
604
|
+
},
|
|
605
|
+
translateError(error) {
|
|
606
|
+
if (error instanceof AnyModelError) {
|
|
607
|
+
return { code: error.code, message: error.message, metadata: error.metadata };
|
|
608
|
+
}
|
|
609
|
+
const err = error;
|
|
610
|
+
const status = err?.status || err?.code || 500;
|
|
611
|
+
return {
|
|
612
|
+
code: mapErrorCode(status),
|
|
613
|
+
message: err?.message || "Unknown OpenAI error",
|
|
614
|
+
metadata: { provider_name: "openai", raw: error }
|
|
615
|
+
};
|
|
616
|
+
},
|
|
617
|
+
async listModels() {
|
|
618
|
+
const res = await makeRequest("/models", void 0, "GET");
|
|
619
|
+
const data = await res.json();
|
|
620
|
+
return (data.data || []).filter((m) => {
|
|
621
|
+
const id = m.id;
|
|
622
|
+
if (id.includes("embedding")) return false;
|
|
623
|
+
if (id.includes("whisper")) return false;
|
|
624
|
+
if (id.includes("tts")) return false;
|
|
625
|
+
if (id.includes("dall-e")) return false;
|
|
626
|
+
if (id.includes("davinci")) return false;
|
|
627
|
+
if (id.includes("babbage")) return false;
|
|
628
|
+
if (id.includes("moderation")) return false;
|
|
629
|
+
if (id.includes("realtime")) return false;
|
|
630
|
+
if (id.startsWith("ft:")) return false;
|
|
631
|
+
return id.startsWith("gpt-") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt-");
|
|
632
|
+
}).map((m) => ({
|
|
633
|
+
id: `openai/${m.id}`,
|
|
634
|
+
name: m.id,
|
|
635
|
+
created: m.created,
|
|
636
|
+
description: "",
|
|
637
|
+
context_length: 128e3,
|
|
638
|
+
pricing: { prompt: "0", completion: "0" },
|
|
639
|
+
architecture: {
|
|
640
|
+
modality: "text+image->text",
|
|
641
|
+
input_modalities: ["text", "image"],
|
|
642
|
+
output_modalities: ["text"],
|
|
643
|
+
tokenizer: "o200k_base"
|
|
644
|
+
},
|
|
645
|
+
top_provider: {
|
|
646
|
+
context_length: 128e3,
|
|
647
|
+
max_completion_tokens: 16384,
|
|
648
|
+
is_moderated: true
|
|
649
|
+
},
|
|
650
|
+
supported_parameters: Array.from(SUPPORTED_PARAMS)
|
|
651
|
+
}));
|
|
652
|
+
},
|
|
653
|
+
supportsParameter(param) {
|
|
654
|
+
return SUPPORTED_PARAMS.has(param);
|
|
655
|
+
},
|
|
656
|
+
supportsBatch() {
|
|
657
|
+
return true;
|
|
658
|
+
},
|
|
659
|
+
async sendRequest(request) {
|
|
660
|
+
const body = buildRequestBody(request);
|
|
661
|
+
const res = await makeRequest("/chat/completions", body);
|
|
662
|
+
const json = await res.json();
|
|
663
|
+
return adapter.translateResponse(json);
|
|
664
|
+
},
|
|
665
|
+
async sendStreamingRequest(request) {
|
|
666
|
+
const body = buildRequestBody({ ...request, stream: true });
|
|
667
|
+
const res = await makeRequest("/chat/completions", body);
|
|
668
|
+
if (!res.body) {
|
|
669
|
+
throw new AnyModelError(502, "No response body for streaming request", {
|
|
670
|
+
provider_name: "openai"
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
return adapter.translateStream(res.body);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
return adapter;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/utils/id.ts
|
|
680
|
+
import { randomBytes } from "crypto";
|
|
681
|
+
function generateId(prefix = "gen") {
|
|
682
|
+
const random = randomBytes(12).toString("base64url");
|
|
683
|
+
return `${prefix}-${random}`;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// src/providers/anthropic.ts
|
|
687
|
+
var ANTHROPIC_API_BASE = "https://api.anthropic.com/v1";
|
|
688
|
+
var ANTHROPIC_VERSION = "2023-06-01";
|
|
689
|
+
var DEFAULT_MAX_TOKENS = 4096;
|
|
690
|
+
var SUPPORTED_PARAMS2 = /* @__PURE__ */ new Set([
|
|
691
|
+
"temperature",
|
|
692
|
+
"max_tokens",
|
|
693
|
+
"top_p",
|
|
694
|
+
"top_k",
|
|
695
|
+
"stop",
|
|
696
|
+
"stream",
|
|
697
|
+
"tools",
|
|
698
|
+
"tool_choice",
|
|
699
|
+
"response_format"
|
|
700
|
+
]);
|
|
701
|
+
var FALLBACK_MODELS = [
|
|
702
|
+
// Claude 4.6
|
|
703
|
+
{ id: "anthropic/claude-opus-4-6", name: "Claude Opus 4.6", created: 0, description: "Most capable model", context_length: 2e5, pricing: { prompt: "0.000015", completion: "0.000075" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 32768, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) },
|
|
704
|
+
{ id: "anthropic/claude-sonnet-4-6", name: "Claude Sonnet 4.6", created: 0, description: "Best balance of speed and capability", context_length: 2e5, pricing: { prompt: "0.000003", completion: "0.000015" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 16384, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) },
|
|
705
|
+
// Claude 4.5
|
|
706
|
+
{ id: "anthropic/claude-sonnet-4-5-20251022", name: "Claude Sonnet 4.5", created: 0, description: "Previous generation balanced model", context_length: 2e5, pricing: { prompt: "0.000003", completion: "0.000015" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 16384, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) },
|
|
707
|
+
{ id: "anthropic/claude-haiku-4-5", name: "Claude Haiku 4.5", created: 0, description: "Fast and compact", context_length: 2e5, pricing: { prompt: "0.000001", completion: "0.000005" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 8192, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) },
|
|
708
|
+
// Claude 3.5
|
|
709
|
+
{ id: "anthropic/claude-3-5-sonnet-20241022", name: "Claude 3.5 Sonnet", created: 0, description: "Legacy balanced model", context_length: 2e5, pricing: { prompt: "0.000003", completion: "0.000015" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 8192, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) },
|
|
710
|
+
{ id: "anthropic/claude-3-5-haiku-20241022", name: "Claude 3.5 Haiku", created: 0, description: "Legacy fast model", context_length: 2e5, pricing: { prompt: "0.0000008", completion: "0.000004" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image"], output_modalities: ["text"], tokenizer: "claude" }, top_provider: { context_length: 2e5, max_completion_tokens: 8192, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS2) }
|
|
711
|
+
];
|
|
712
|
+
function createAnthropicAdapter(apiKey) {
|
|
713
|
+
async function makeRequest(path2, body, stream = false) {
|
|
714
|
+
const res = await fetch(`${ANTHROPIC_API_BASE}${path2}`, {
|
|
715
|
+
method: "POST",
|
|
716
|
+
headers: {
|
|
717
|
+
"Content-Type": "application/json",
|
|
718
|
+
"x-api-key": apiKey,
|
|
719
|
+
"anthropic-version": ANTHROPIC_VERSION
|
|
720
|
+
},
|
|
721
|
+
body: JSON.stringify(body)
|
|
722
|
+
});
|
|
723
|
+
if (!res.ok) {
|
|
724
|
+
let errorBody;
|
|
725
|
+
try {
|
|
726
|
+
errorBody = await res.json();
|
|
727
|
+
} catch {
|
|
728
|
+
errorBody = { message: res.statusText };
|
|
729
|
+
}
|
|
730
|
+
const msg = errorBody?.error?.message || errorBody?.message || res.statusText;
|
|
731
|
+
throw new AnyModelError(mapErrorCode(res.status), msg, {
|
|
732
|
+
provider_name: "anthropic",
|
|
733
|
+
raw: errorBody
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
return res;
|
|
737
|
+
}
|
|
738
|
+
function mapErrorCode(status) {
|
|
739
|
+
if (status === 401 || status === 403) return 401;
|
|
740
|
+
if (status === 429) return 429;
|
|
741
|
+
if (status === 400 || status === 422) return 400;
|
|
742
|
+
if (status === 529) return 502;
|
|
743
|
+
if (status >= 500) return 502;
|
|
744
|
+
return status;
|
|
745
|
+
}
|
|
746
|
+
function translateRequest(request) {
|
|
747
|
+
const body = {
|
|
748
|
+
model: request.model,
|
|
749
|
+
max_tokens: request.max_tokens || DEFAULT_MAX_TOKENS
|
|
750
|
+
};
|
|
751
|
+
const systemMessages = request.messages.filter((m) => m.role === "system");
|
|
752
|
+
const nonSystemMessages = request.messages.filter((m) => m.role !== "system");
|
|
753
|
+
if (systemMessages.length > 0) {
|
|
754
|
+
body.system = systemMessages.map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
755
|
+
}
|
|
756
|
+
body.messages = nonSystemMessages.map((m) => ({
|
|
757
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
758
|
+
content: m.tool_call_id ? [{ type: "tool_result", tool_use_id: m.tool_call_id, content: typeof m.content === "string" ? m.content : "" }] : m.content
|
|
759
|
+
}));
|
|
760
|
+
if (request.temperature !== void 0) body.temperature = request.temperature;
|
|
761
|
+
if (request.top_p !== void 0) body.top_p = request.top_p;
|
|
762
|
+
if (request.top_k !== void 0) body.top_k = request.top_k;
|
|
763
|
+
if (request.stop !== void 0) body.stop_sequences = Array.isArray(request.stop) ? request.stop : [request.stop];
|
|
764
|
+
if (request.stream) body.stream = true;
|
|
765
|
+
if (request.tools && request.tools.length > 0) {
|
|
766
|
+
body.tools = request.tools.map((t) => ({
|
|
767
|
+
name: t.function.name,
|
|
768
|
+
description: t.function.description || "",
|
|
769
|
+
input_schema: t.function.parameters || { type: "object", properties: {} }
|
|
770
|
+
}));
|
|
771
|
+
if (request.tool_choice) {
|
|
772
|
+
if (request.tool_choice === "auto") {
|
|
773
|
+
body.tool_choice = { type: "auto" };
|
|
774
|
+
} else if (request.tool_choice === "required") {
|
|
775
|
+
body.tool_choice = { type: "any" };
|
|
776
|
+
} else if (request.tool_choice === "none") {
|
|
777
|
+
delete body.tools;
|
|
778
|
+
} else if (typeof request.tool_choice === "object") {
|
|
779
|
+
body.tool_choice = { type: "tool", name: request.tool_choice.function.name };
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
if (request.response_format) {
|
|
784
|
+
if (request.response_format.type === "json_object" || request.response_format.type === "json_schema") {
|
|
785
|
+
const jsonInstruction = "Respond with valid JSON only. Do not include any text outside the JSON object.";
|
|
786
|
+
body.system = body.system ? `${jsonInstruction}
|
|
787
|
+
|
|
788
|
+
${body.system}` : jsonInstruction;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return body;
|
|
792
|
+
}
|
|
793
|
+
function mapStopReason(reason) {
|
|
794
|
+
switch (reason) {
|
|
795
|
+
case "end_turn":
|
|
796
|
+
return "stop";
|
|
797
|
+
case "max_tokens":
|
|
798
|
+
return "length";
|
|
799
|
+
case "tool_use":
|
|
800
|
+
return "tool_calls";
|
|
801
|
+
case "stop_sequence":
|
|
802
|
+
return "stop";
|
|
803
|
+
default:
|
|
804
|
+
return "stop";
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
function translateResponse(response) {
|
|
808
|
+
const r = response;
|
|
809
|
+
let content = "";
|
|
810
|
+
const toolCalls = [];
|
|
811
|
+
for (const block of r.content || []) {
|
|
812
|
+
if (block.type === "text") {
|
|
813
|
+
content += block.text;
|
|
814
|
+
} else if (block.type === "tool_use") {
|
|
815
|
+
toolCalls.push({
|
|
816
|
+
id: block.id,
|
|
817
|
+
type: "function",
|
|
818
|
+
function: {
|
|
819
|
+
name: block.name,
|
|
820
|
+
arguments: JSON.stringify(block.input)
|
|
821
|
+
}
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const message = {
|
|
826
|
+
role: "assistant",
|
|
827
|
+
content
|
|
828
|
+
};
|
|
829
|
+
if (toolCalls.length > 0) {
|
|
830
|
+
message.tool_calls = toolCalls;
|
|
831
|
+
}
|
|
832
|
+
return {
|
|
833
|
+
id: generateId(),
|
|
834
|
+
object: "chat.completion",
|
|
835
|
+
created: Math.floor(Date.now() / 1e3),
|
|
836
|
+
model: `anthropic/${r.model}`,
|
|
837
|
+
choices: [{
|
|
838
|
+
index: 0,
|
|
839
|
+
message,
|
|
840
|
+
finish_reason: mapStopReason(r.stop_reason)
|
|
841
|
+
}],
|
|
842
|
+
usage: {
|
|
843
|
+
prompt_tokens: r.usage?.input_tokens || 0,
|
|
844
|
+
completion_tokens: r.usage?.output_tokens || 0,
|
|
845
|
+
total_tokens: (r.usage?.input_tokens || 0) + (r.usage?.output_tokens || 0)
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
const adapter = {
|
|
850
|
+
name: "anthropic",
|
|
851
|
+
translateRequest,
|
|
852
|
+
translateResponse,
|
|
853
|
+
async *translateStream(stream) {
|
|
854
|
+
const reader = stream.getReader();
|
|
855
|
+
const decoder = new TextDecoder();
|
|
856
|
+
let buffer = "";
|
|
857
|
+
const id = generateId();
|
|
858
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
859
|
+
let model = "";
|
|
860
|
+
let usage = null;
|
|
861
|
+
try {
|
|
862
|
+
while (true) {
|
|
863
|
+
const { done, value } = await reader.read();
|
|
864
|
+
if (done) break;
|
|
865
|
+
buffer += decoder.decode(value, { stream: true });
|
|
866
|
+
const lines = buffer.split("\n");
|
|
867
|
+
buffer = lines.pop() || "";
|
|
868
|
+
for (const line of lines) {
|
|
869
|
+
const trimmed = line.trim();
|
|
870
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
871
|
+
if (trimmed.startsWith("data: ")) {
|
|
872
|
+
const data = JSON.parse(trimmed.substring(6));
|
|
873
|
+
if (data.type === "message_start") {
|
|
874
|
+
model = `anthropic/${data.message.model}`;
|
|
875
|
+
usage = data.message.usage;
|
|
876
|
+
yield {
|
|
877
|
+
id,
|
|
878
|
+
object: "chat.completion.chunk",
|
|
879
|
+
created,
|
|
880
|
+
model,
|
|
881
|
+
choices: [{ index: 0, delta: { role: "assistant" }, finish_reason: null }]
|
|
882
|
+
};
|
|
883
|
+
} else if (data.type === "content_block_delta") {
|
|
884
|
+
if (data.delta?.type === "text_delta") {
|
|
885
|
+
yield {
|
|
886
|
+
id,
|
|
887
|
+
object: "chat.completion.chunk",
|
|
888
|
+
created,
|
|
889
|
+
model,
|
|
890
|
+
choices: [{ index: 0, delta: { content: data.delta.text }, finish_reason: null }]
|
|
891
|
+
};
|
|
892
|
+
} else if (data.delta?.type === "input_json_delta") {
|
|
893
|
+
yield {
|
|
894
|
+
id,
|
|
895
|
+
object: "chat.completion.chunk",
|
|
896
|
+
created,
|
|
897
|
+
model,
|
|
898
|
+
choices: [{
|
|
899
|
+
index: 0,
|
|
900
|
+
delta: {
|
|
901
|
+
tool_calls: [{
|
|
902
|
+
id: data.content_block?.id || "",
|
|
903
|
+
type: "function",
|
|
904
|
+
function: { name: "", arguments: data.delta.partial_json }
|
|
905
|
+
}]
|
|
906
|
+
},
|
|
907
|
+
finish_reason: null
|
|
908
|
+
}]
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
} else if (data.type === "content_block_start") {
|
|
912
|
+
if (data.content_block?.type === "tool_use") {
|
|
913
|
+
yield {
|
|
914
|
+
id,
|
|
915
|
+
object: "chat.completion.chunk",
|
|
916
|
+
created,
|
|
917
|
+
model,
|
|
918
|
+
choices: [{
|
|
919
|
+
index: 0,
|
|
920
|
+
delta: {
|
|
921
|
+
tool_calls: [{
|
|
922
|
+
id: data.content_block.id,
|
|
923
|
+
type: "function",
|
|
924
|
+
function: { name: data.content_block.name, arguments: "" }
|
|
925
|
+
}]
|
|
926
|
+
},
|
|
927
|
+
finish_reason: null
|
|
928
|
+
}]
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
} else if (data.type === "message_delta") {
|
|
932
|
+
const finalUsage = usage ? {
|
|
933
|
+
prompt_tokens: usage.input_tokens || 0,
|
|
934
|
+
completion_tokens: data.usage?.output_tokens || 0,
|
|
935
|
+
total_tokens: (usage.input_tokens || 0) + (data.usage?.output_tokens || 0)
|
|
936
|
+
} : void 0;
|
|
937
|
+
yield {
|
|
938
|
+
id,
|
|
939
|
+
object: "chat.completion.chunk",
|
|
940
|
+
created,
|
|
941
|
+
model,
|
|
942
|
+
choices: [{
|
|
943
|
+
index: 0,
|
|
944
|
+
delta: {},
|
|
945
|
+
finish_reason: mapStopReason(data.delta?.stop_reason || "end_turn")
|
|
946
|
+
}],
|
|
947
|
+
usage: finalUsage
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
} finally {
|
|
954
|
+
reader.releaseLock();
|
|
955
|
+
}
|
|
956
|
+
},
|
|
957
|
+
translateError(error) {
|
|
958
|
+
if (error instanceof AnyModelError) {
|
|
959
|
+
return { code: error.code, message: error.message, metadata: error.metadata };
|
|
960
|
+
}
|
|
961
|
+
const err = error;
|
|
962
|
+
const status = err?.status || err?.code || 500;
|
|
963
|
+
return {
|
|
964
|
+
code: mapErrorCode(status),
|
|
965
|
+
message: err?.message || "Unknown Anthropic error",
|
|
966
|
+
metadata: { provider_name: "anthropic", raw: error }
|
|
967
|
+
};
|
|
968
|
+
},
|
|
969
|
+
async listModels() {
|
|
970
|
+
try {
|
|
971
|
+
const res = await fetch(`${ANTHROPIC_API_BASE}/models`, {
|
|
972
|
+
method: "GET",
|
|
973
|
+
headers: {
|
|
974
|
+
"x-api-key": apiKey,
|
|
975
|
+
"anthropic-version": ANTHROPIC_VERSION
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
if (!res.ok) return FALLBACK_MODELS;
|
|
979
|
+
const data = await res.json();
|
|
980
|
+
const models = data.data || [];
|
|
981
|
+
return models.filter((m) => m.type === "model").map((m) => ({
|
|
982
|
+
id: `anthropic/${m.id}`,
|
|
983
|
+
name: m.display_name || m.id,
|
|
984
|
+
created: m.created_at ? new Date(m.created_at).getTime() / 1e3 : 0,
|
|
985
|
+
description: m.display_name || "",
|
|
986
|
+
context_length: 2e5,
|
|
987
|
+
pricing: { prompt: "0", completion: "0" },
|
|
988
|
+
architecture: {
|
|
989
|
+
modality: "text+image->text",
|
|
990
|
+
input_modalities: ["text", "image"],
|
|
991
|
+
output_modalities: ["text"],
|
|
992
|
+
tokenizer: "claude"
|
|
993
|
+
},
|
|
994
|
+
top_provider: {
|
|
995
|
+
context_length: 2e5,
|
|
996
|
+
max_completion_tokens: 16384,
|
|
997
|
+
is_moderated: false
|
|
998
|
+
},
|
|
999
|
+
supported_parameters: Array.from(SUPPORTED_PARAMS2)
|
|
1000
|
+
}));
|
|
1001
|
+
} catch {
|
|
1002
|
+
return FALLBACK_MODELS;
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
supportsParameter(param) {
|
|
1006
|
+
return SUPPORTED_PARAMS2.has(param);
|
|
1007
|
+
},
|
|
1008
|
+
supportsBatch() {
|
|
1009
|
+
return true;
|
|
1010
|
+
},
|
|
1011
|
+
async sendRequest(request) {
|
|
1012
|
+
const body = translateRequest(request);
|
|
1013
|
+
const res = await makeRequest("/messages", body);
|
|
1014
|
+
const json = await res.json();
|
|
1015
|
+
return translateResponse(json);
|
|
1016
|
+
},
|
|
1017
|
+
async sendStreamingRequest(request) {
|
|
1018
|
+
const body = translateRequest({ ...request, stream: true });
|
|
1019
|
+
const res = await makeRequest("/messages", body, true);
|
|
1020
|
+
if (!res.body) {
|
|
1021
|
+
throw new AnyModelError(502, "No response body for streaming request", {
|
|
1022
|
+
provider_name: "anthropic"
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
return adapter.translateStream(res.body);
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
return adapter;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// src/providers/google.ts
|
|
1032
|
+
var GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
|
|
1033
|
+
var SUPPORTED_PARAMS3 = /* @__PURE__ */ new Set([
|
|
1034
|
+
"temperature",
|
|
1035
|
+
"max_tokens",
|
|
1036
|
+
"top_p",
|
|
1037
|
+
"top_k",
|
|
1038
|
+
"stop",
|
|
1039
|
+
"stream",
|
|
1040
|
+
"tools",
|
|
1041
|
+
"tool_choice",
|
|
1042
|
+
"response_format"
|
|
1043
|
+
]);
|
|
1044
|
+
var FALLBACK_MODELS2 = [
|
|
1045
|
+
// Gemini 2.5
|
|
1046
|
+
{ id: "google/gemini-2.5-pro", name: "Gemini 2.5 Pro", created: 0, description: "Most capable Gemini model", context_length: 1048576, pricing: { prompt: "0.00000125", completion: "0.000005" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 1048576, max_completion_tokens: 65536, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) },
|
|
1047
|
+
{ id: "google/gemini-2.5-flash", name: "Gemini 2.5 Flash", created: 0, description: "Fast and efficient", context_length: 1048576, pricing: { prompt: "0.00000015", completion: "0.0000006" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 1048576, max_completion_tokens: 65536, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) },
|
|
1048
|
+
// Gemini 2.0
|
|
1049
|
+
{ id: "google/gemini-2.0-flash", name: "Gemini 2.0 Flash", created: 0, description: "Fast multimodal model", context_length: 1048576, pricing: { prompt: "0.0000001", completion: "0.0000004" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 1048576, max_completion_tokens: 65536, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) },
|
|
1050
|
+
{ id: "google/gemini-2.0-flash-lite", name: "Gemini 2.0 Flash Lite", created: 0, description: "Lightweight and fast", context_length: 1048576, pricing: { prompt: "0.00000005", completion: "0.0000002" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 1048576, max_completion_tokens: 65536, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) },
|
|
1051
|
+
// Gemini 1.5
|
|
1052
|
+
{ id: "google/gemini-1.5-pro", name: "Gemini 1.5 Pro", created: 0, description: "Previous generation pro model", context_length: 2097152, pricing: { prompt: "0.00000125", completion: "0.000005" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 2097152, max_completion_tokens: 8192, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) },
|
|
1053
|
+
{ id: "google/gemini-1.5-flash", name: "Gemini 1.5 Flash", created: 0, description: "Previous generation flash model", context_length: 1048576, pricing: { prompt: "0.000000075", completion: "0.0000003" }, architecture: { modality: "text+image->text", input_modalities: ["text", "image", "video", "audio"], output_modalities: ["text"], tokenizer: "gemini" }, top_provider: { context_length: 1048576, max_completion_tokens: 8192, is_moderated: false }, supported_parameters: Array.from(SUPPORTED_PARAMS3) }
|
|
1054
|
+
];
|
|
1055
|
+
function createGoogleAdapter(apiKey) {
|
|
1056
|
+
function getModelEndpoint(model, stream) {
|
|
1057
|
+
const action = stream ? "streamGenerateContent" : "generateContent";
|
|
1058
|
+
return `${GEMINI_API_BASE}/models/${model}:${action}?key=${apiKey}${stream ? "&alt=sse" : ""}`;
|
|
1059
|
+
}
|
|
1060
|
+
function mapErrorCode(status) {
|
|
1061
|
+
if (status === 401 || status === 403) return 401;
|
|
1062
|
+
if (status === 429) return 429;
|
|
1063
|
+
if (status === 400) return 400;
|
|
1064
|
+
if (status >= 500) return 502;
|
|
1065
|
+
return status;
|
|
1066
|
+
}
|
|
1067
|
+
function translateRequest(request) {
|
|
1068
|
+
const body = {};
|
|
1069
|
+
const systemMessages = request.messages.filter((m) => m.role === "system");
|
|
1070
|
+
const nonSystemMessages = request.messages.filter((m) => m.role !== "system");
|
|
1071
|
+
if (systemMessages.length > 0) {
|
|
1072
|
+
body.systemInstruction = {
|
|
1073
|
+
parts: [{ text: systemMessages.map((m) => typeof m.content === "string" ? m.content : "").join("\n") }]
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
body.contents = nonSystemMessages.map((m) => ({
|
|
1077
|
+
role: m.role === "assistant" ? "model" : "user",
|
|
1078
|
+
parts: typeof m.content === "string" ? [{ text: m.content }] : Array.isArray(m.content) ? m.content.map((p) => p.type === "text" ? { text: p.text } : { text: "" }) : [{ text: "" }]
|
|
1079
|
+
}));
|
|
1080
|
+
const generationConfig = {};
|
|
1081
|
+
if (request.temperature !== void 0) generationConfig.temperature = request.temperature;
|
|
1082
|
+
if (request.max_tokens !== void 0) generationConfig.maxOutputTokens = request.max_tokens;
|
|
1083
|
+
if (request.top_p !== void 0) generationConfig.topP = request.top_p;
|
|
1084
|
+
if (request.top_k !== void 0) generationConfig.topK = request.top_k;
|
|
1085
|
+
if (request.stop !== void 0) {
|
|
1086
|
+
generationConfig.stopSequences = Array.isArray(request.stop) ? request.stop : [request.stop];
|
|
1087
|
+
}
|
|
1088
|
+
if (request.response_format) {
|
|
1089
|
+
if (request.response_format.type === "json_object") {
|
|
1090
|
+
generationConfig.responseMimeType = "application/json";
|
|
1091
|
+
} else if (request.response_format.type === "json_schema") {
|
|
1092
|
+
generationConfig.responseMimeType = "application/json";
|
|
1093
|
+
generationConfig.responseSchema = request.response_format.json_schema.schema;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (Object.keys(generationConfig).length > 0) {
|
|
1097
|
+
body.generationConfig = generationConfig;
|
|
1098
|
+
}
|
|
1099
|
+
if (request.tools && request.tools.length > 0) {
|
|
1100
|
+
body.tools = [{
|
|
1101
|
+
functionDeclarations: request.tools.map((t) => ({
|
|
1102
|
+
name: t.function.name,
|
|
1103
|
+
description: t.function.description || "",
|
|
1104
|
+
parameters: t.function.parameters || {}
|
|
1105
|
+
}))
|
|
1106
|
+
}];
|
|
1107
|
+
if (request.tool_choice) {
|
|
1108
|
+
if (request.tool_choice === "auto") {
|
|
1109
|
+
body.toolConfig = { functionCallingConfig: { mode: "AUTO" } };
|
|
1110
|
+
} else if (request.tool_choice === "required") {
|
|
1111
|
+
body.toolConfig = { functionCallingConfig: { mode: "ANY" } };
|
|
1112
|
+
} else if (request.tool_choice === "none") {
|
|
1113
|
+
body.toolConfig = { functionCallingConfig: { mode: "NONE" } };
|
|
1114
|
+
} else if (typeof request.tool_choice === "object") {
|
|
1115
|
+
body.toolConfig = {
|
|
1116
|
+
functionCallingConfig: {
|
|
1117
|
+
mode: "ANY",
|
|
1118
|
+
allowedFunctionNames: [request.tool_choice.function.name]
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return body;
|
|
1125
|
+
}
|
|
1126
|
+
function mapFinishReason(reason) {
|
|
1127
|
+
switch (reason) {
|
|
1128
|
+
case "STOP":
|
|
1129
|
+
return "stop";
|
|
1130
|
+
case "MAX_TOKENS":
|
|
1131
|
+
return "length";
|
|
1132
|
+
case "SAFETY":
|
|
1133
|
+
return "content_filter";
|
|
1134
|
+
case "RECITATION":
|
|
1135
|
+
return "content_filter";
|
|
1136
|
+
default:
|
|
1137
|
+
return "stop";
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
function translateResponse(response) {
|
|
1141
|
+
const r = response;
|
|
1142
|
+
const candidate = r.candidates?.[0];
|
|
1143
|
+
let content = "";
|
|
1144
|
+
const toolCalls = [];
|
|
1145
|
+
for (const part of candidate?.content?.parts || []) {
|
|
1146
|
+
if (part.text) {
|
|
1147
|
+
content += part.text;
|
|
1148
|
+
} else if (part.functionCall) {
|
|
1149
|
+
toolCalls.push({
|
|
1150
|
+
id: generateId("call"),
|
|
1151
|
+
type: "function",
|
|
1152
|
+
function: {
|
|
1153
|
+
name: part.functionCall.name,
|
|
1154
|
+
arguments: JSON.stringify(part.functionCall.args || {})
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const message = { role: "assistant", content };
|
|
1160
|
+
if (toolCalls.length > 0) {
|
|
1161
|
+
message.tool_calls = toolCalls;
|
|
1162
|
+
}
|
|
1163
|
+
const finishReason = toolCalls.length > 0 ? "tool_calls" : mapFinishReason(candidate?.finishReason || "STOP");
|
|
1164
|
+
return {
|
|
1165
|
+
id: generateId(),
|
|
1166
|
+
object: "chat.completion",
|
|
1167
|
+
created: Math.floor(Date.now() / 1e3),
|
|
1168
|
+
model: `google/${r.modelVersion || "unknown"}`,
|
|
1169
|
+
choices: [{ index: 0, message, finish_reason: finishReason }],
|
|
1170
|
+
usage: {
|
|
1171
|
+
prompt_tokens: r.usageMetadata?.promptTokenCount || 0,
|
|
1172
|
+
completion_tokens: r.usageMetadata?.candidatesTokenCount || 0,
|
|
1173
|
+
total_tokens: r.usageMetadata?.totalTokenCount || 0
|
|
1174
|
+
}
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
const adapter = {
|
|
1178
|
+
name: "google",
|
|
1179
|
+
translateRequest,
|
|
1180
|
+
translateResponse,
|
|
1181
|
+
async *translateStream(stream) {
|
|
1182
|
+
const reader = stream.getReader();
|
|
1183
|
+
const decoder = new TextDecoder();
|
|
1184
|
+
let buffer = "";
|
|
1185
|
+
const id = generateId();
|
|
1186
|
+
const created = Math.floor(Date.now() / 1e3);
|
|
1187
|
+
let emittedRole = false;
|
|
1188
|
+
try {
|
|
1189
|
+
while (true) {
|
|
1190
|
+
const { done, value } = await reader.read();
|
|
1191
|
+
if (done) break;
|
|
1192
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1193
|
+
const lines = buffer.split("\n");
|
|
1194
|
+
buffer = lines.pop() || "";
|
|
1195
|
+
for (const line of lines) {
|
|
1196
|
+
const trimmed = line.trim();
|
|
1197
|
+
if (!trimmed || trimmed.startsWith(":")) continue;
|
|
1198
|
+
if (trimmed === "data: [DONE]") return;
|
|
1199
|
+
if (trimmed.startsWith("data: ")) {
|
|
1200
|
+
const data = JSON.parse(trimmed.substring(6));
|
|
1201
|
+
const candidate = data.candidates?.[0];
|
|
1202
|
+
if (!candidate) continue;
|
|
1203
|
+
const parts = candidate.content?.parts || [];
|
|
1204
|
+
for (const part of parts) {
|
|
1205
|
+
if (part.text !== void 0) {
|
|
1206
|
+
const chunk = {
|
|
1207
|
+
id,
|
|
1208
|
+
object: "chat.completion.chunk",
|
|
1209
|
+
created,
|
|
1210
|
+
model: `google/${data.modelVersion || "unknown"}`,
|
|
1211
|
+
choices: [{
|
|
1212
|
+
index: 0,
|
|
1213
|
+
delta: emittedRole ? { content: part.text } : { role: "assistant", content: part.text },
|
|
1214
|
+
finish_reason: null
|
|
1215
|
+
}]
|
|
1216
|
+
};
|
|
1217
|
+
emittedRole = true;
|
|
1218
|
+
yield chunk;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (candidate.finishReason) {
|
|
1222
|
+
yield {
|
|
1223
|
+
id,
|
|
1224
|
+
object: "chat.completion.chunk",
|
|
1225
|
+
created,
|
|
1226
|
+
model: `google/${data.modelVersion || "unknown"}`,
|
|
1227
|
+
choices: [{ index: 0, delta: {}, finish_reason: mapFinishReason(candidate.finishReason) }],
|
|
1228
|
+
usage: data.usageMetadata ? {
|
|
1229
|
+
prompt_tokens: data.usageMetadata.promptTokenCount || 0,
|
|
1230
|
+
completion_tokens: data.usageMetadata.candidatesTokenCount || 0,
|
|
1231
|
+
total_tokens: data.usageMetadata.totalTokenCount || 0
|
|
1232
|
+
} : void 0
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
} finally {
|
|
1239
|
+
reader.releaseLock();
|
|
1240
|
+
}
|
|
1241
|
+
},
|
|
1242
|
+
translateError(error) {
|
|
1243
|
+
if (error instanceof AnyModelError) {
|
|
1244
|
+
return { code: error.code, message: error.message, metadata: error.metadata };
|
|
1245
|
+
}
|
|
1246
|
+
const err = error;
|
|
1247
|
+
const status = err?.status || err?.code || 500;
|
|
1248
|
+
return {
|
|
1249
|
+
code: mapErrorCode(status),
|
|
1250
|
+
message: err?.message || "Unknown Google error",
|
|
1251
|
+
metadata: { provider_name: "google", raw: error }
|
|
1252
|
+
};
|
|
1253
|
+
},
|
|
1254
|
+
async listModels() {
|
|
1255
|
+
try {
|
|
1256
|
+
const res = await fetch(`${GEMINI_API_BASE}/models?key=${apiKey}`);
|
|
1257
|
+
if (!res.ok) return FALLBACK_MODELS2;
|
|
1258
|
+
const data = await res.json();
|
|
1259
|
+
const models = data.models || [];
|
|
1260
|
+
return models.filter((m) => m.name?.startsWith("models/gemini-") && m.supportedGenerationMethods?.includes("generateContent")).map((m) => {
|
|
1261
|
+
const modelId = m.name.replace("models/", "");
|
|
1262
|
+
return {
|
|
1263
|
+
id: `google/${modelId}`,
|
|
1264
|
+
name: m.displayName || modelId,
|
|
1265
|
+
created: 0,
|
|
1266
|
+
description: m.description || "",
|
|
1267
|
+
context_length: m.inputTokenLimit || 1048576,
|
|
1268
|
+
pricing: { prompt: "0", completion: "0" },
|
|
1269
|
+
architecture: {
|
|
1270
|
+
modality: "text+image->text",
|
|
1271
|
+
input_modalities: ["text", "image", "video", "audio"],
|
|
1272
|
+
output_modalities: ["text"],
|
|
1273
|
+
tokenizer: "gemini"
|
|
1274
|
+
},
|
|
1275
|
+
top_provider: {
|
|
1276
|
+
context_length: m.inputTokenLimit || 1048576,
|
|
1277
|
+
max_completion_tokens: m.outputTokenLimit || 65536,
|
|
1278
|
+
is_moderated: false
|
|
1279
|
+
},
|
|
1280
|
+
supported_parameters: Array.from(SUPPORTED_PARAMS3)
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
} catch {
|
|
1284
|
+
return FALLBACK_MODELS2;
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
supportsParameter(param) {
|
|
1288
|
+
return SUPPORTED_PARAMS3.has(param);
|
|
1289
|
+
},
|
|
1290
|
+
supportsBatch() {
|
|
1291
|
+
return false;
|
|
1292
|
+
},
|
|
1293
|
+
async sendRequest(request) {
|
|
1294
|
+
const body = translateRequest(request);
|
|
1295
|
+
const url = getModelEndpoint(request.model, false);
|
|
1296
|
+
const res = await fetch(url, {
|
|
1297
|
+
method: "POST",
|
|
1298
|
+
headers: { "Content-Type": "application/json" },
|
|
1299
|
+
body: JSON.stringify(body)
|
|
1300
|
+
});
|
|
1301
|
+
if (!res.ok) {
|
|
1302
|
+
let errorBody;
|
|
1303
|
+
try {
|
|
1304
|
+
errorBody = await res.json();
|
|
1305
|
+
} catch {
|
|
1306
|
+
errorBody = { message: res.statusText };
|
|
1307
|
+
}
|
|
1308
|
+
throw new AnyModelError(mapErrorCode(res.status), errorBody?.error?.message || res.statusText, {
|
|
1309
|
+
provider_name: "google",
|
|
1310
|
+
raw: errorBody
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
const json = await res.json();
|
|
1314
|
+
return translateResponse(json);
|
|
1315
|
+
},
|
|
1316
|
+
async sendStreamingRequest(request) {
|
|
1317
|
+
const body = translateRequest(request);
|
|
1318
|
+
const url = getModelEndpoint(request.model, true);
|
|
1319
|
+
const res = await fetch(url, {
|
|
1320
|
+
method: "POST",
|
|
1321
|
+
headers: { "Content-Type": "application/json" },
|
|
1322
|
+
body: JSON.stringify(body)
|
|
1323
|
+
});
|
|
1324
|
+
if (!res.ok) {
|
|
1325
|
+
let errorBody;
|
|
1326
|
+
try {
|
|
1327
|
+
errorBody = await res.json();
|
|
1328
|
+
} catch {
|
|
1329
|
+
errorBody = { message: res.statusText };
|
|
1330
|
+
}
|
|
1331
|
+
throw new AnyModelError(mapErrorCode(res.status), errorBody?.error?.message || res.statusText, {
|
|
1332
|
+
provider_name: "google",
|
|
1333
|
+
raw: errorBody
|
|
1334
|
+
});
|
|
1335
|
+
}
|
|
1336
|
+
if (!res.body) {
|
|
1337
|
+
throw new AnyModelError(502, "No response body for streaming request", { provider_name: "google" });
|
|
1338
|
+
}
|
|
1339
|
+
return adapter.translateStream(res.body);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
return adapter;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// src/providers/custom.ts
|
|
1346
|
+
function createCustomAdapter(name, config) {
|
|
1347
|
+
const openaiAdapter = createOpenAIAdapter(config.apiKey || "", config.baseURL);
|
|
1348
|
+
return {
|
|
1349
|
+
...openaiAdapter,
|
|
1350
|
+
name,
|
|
1351
|
+
supportsBatch() {
|
|
1352
|
+
return false;
|
|
1353
|
+
},
|
|
1354
|
+
async listModels() {
|
|
1355
|
+
if (config.models && config.models.length > 0) {
|
|
1356
|
+
return config.models.map((modelId) => ({
|
|
1357
|
+
id: `${name}/${modelId}`,
|
|
1358
|
+
name: modelId,
|
|
1359
|
+
created: 0,
|
|
1360
|
+
description: `Custom model via ${name}`,
|
|
1361
|
+
context_length: 128e3,
|
|
1362
|
+
pricing: { prompt: "0", completion: "0" },
|
|
1363
|
+
architecture: {
|
|
1364
|
+
modality: "text->text",
|
|
1365
|
+
input_modalities: ["text"],
|
|
1366
|
+
output_modalities: ["text"],
|
|
1367
|
+
tokenizer: "unknown"
|
|
1368
|
+
},
|
|
1369
|
+
top_provider: {
|
|
1370
|
+
context_length: 128e3,
|
|
1371
|
+
max_completion_tokens: 16384,
|
|
1372
|
+
is_moderated: false
|
|
1373
|
+
},
|
|
1374
|
+
supported_parameters: ["temperature", "max_tokens", "top_p", "stop", "stream", "tools", "tool_choice"]
|
|
1375
|
+
}));
|
|
1376
|
+
}
|
|
1377
|
+
try {
|
|
1378
|
+
const models = await openaiAdapter.listModels();
|
|
1379
|
+
return models.map((m) => ({
|
|
1380
|
+
...m,
|
|
1381
|
+
id: `${name}/${m.name}`
|
|
1382
|
+
}));
|
|
1383
|
+
} catch {
|
|
1384
|
+
return [];
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
translateResponse(response) {
|
|
1388
|
+
const translated = openaiAdapter.translateResponse(response);
|
|
1389
|
+
if (translated.model.startsWith("openai/")) {
|
|
1390
|
+
translated.model = `${name}/${translated.model.substring(7)}`;
|
|
1391
|
+
}
|
|
1392
|
+
return translated;
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
// src/config.ts
|
|
1398
|
+
import { readFileSync, existsSync } from "fs";
|
|
1399
|
+
import { resolve, join } from "path";
|
|
1400
|
+
import { homedir } from "os";
|
|
1401
|
+
var LOCAL_CONFIG_NAMES = ["anymodel.config.json"];
|
|
1402
|
+
var GLOBAL_CONFIG_DIR = join(homedir(), ".anymodel");
|
|
1403
|
+
var GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, "config.json");
|
|
1404
|
+
function interpolateEnvVars(value) {
|
|
1405
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => process.env[name] || "");
|
|
1406
|
+
}
|
|
1407
|
+
function interpolateDeep(obj) {
|
|
1408
|
+
if (typeof obj === "string") return interpolateEnvVars(obj);
|
|
1409
|
+
if (Array.isArray(obj)) return obj.map(interpolateDeep);
|
|
1410
|
+
if (obj !== null && typeof obj === "object") {
|
|
1411
|
+
const result = {};
|
|
1412
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
1413
|
+
result[key] = interpolateDeep(val);
|
|
1414
|
+
}
|
|
1415
|
+
return result;
|
|
1416
|
+
}
|
|
1417
|
+
return obj;
|
|
1418
|
+
}
|
|
1419
|
+
function loadJsonFile(path2) {
|
|
1420
|
+
if (!existsSync(path2)) return null;
|
|
1421
|
+
try {
|
|
1422
|
+
const raw = readFileSync(path2, "utf-8");
|
|
1423
|
+
const parsed = JSON.parse(raw);
|
|
1424
|
+
return interpolateDeep(parsed);
|
|
1425
|
+
} catch {
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function findLocalConfig(cwd) {
|
|
1430
|
+
const dir = cwd || process.cwd();
|
|
1431
|
+
for (const name of LOCAL_CONFIG_NAMES) {
|
|
1432
|
+
const config = loadJsonFile(resolve(dir, name));
|
|
1433
|
+
if (config) return config;
|
|
1434
|
+
}
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
function findGlobalConfig() {
|
|
1438
|
+
return loadJsonFile(GLOBAL_CONFIG_FILE);
|
|
1439
|
+
}
|
|
1440
|
+
function deepMerge(target, source) {
|
|
1441
|
+
const result = { ...target };
|
|
1442
|
+
for (const [key, value] of Object.entries(source)) {
|
|
1443
|
+
if (value === void 0) continue;
|
|
1444
|
+
const existing = target[key];
|
|
1445
|
+
if (existing !== null && typeof existing === "object" && !Array.isArray(existing) && value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1446
|
+
result[key] = deepMerge(existing, value);
|
|
1447
|
+
} else {
|
|
1448
|
+
result[key] = value;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
return result;
|
|
1452
|
+
}
|
|
1453
|
+
function envConfig() {
|
|
1454
|
+
const config = {};
|
|
1455
|
+
const envMap = [
|
|
1456
|
+
["openai", "OPENAI_API_KEY"],
|
|
1457
|
+
["anthropic", "ANTHROPIC_API_KEY"],
|
|
1458
|
+
["google", "GOOGLE_API_KEY"],
|
|
1459
|
+
["mistral", "MISTRAL_API_KEY"],
|
|
1460
|
+
["groq", "GROQ_API_KEY"],
|
|
1461
|
+
["deepseek", "DEEPSEEK_API_KEY"],
|
|
1462
|
+
["xai", "XAI_API_KEY"],
|
|
1463
|
+
["together", "TOGETHER_API_KEY"],
|
|
1464
|
+
["fireworks", "FIREWORKS_API_KEY"],
|
|
1465
|
+
["perplexity", "PERPLEXITY_API_KEY"]
|
|
1466
|
+
];
|
|
1467
|
+
for (const [key, envVar] of envMap) {
|
|
1468
|
+
if (process.env[envVar]) {
|
|
1469
|
+
config[key] = { apiKey: process.env[envVar] };
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
return config;
|
|
1473
|
+
}
|
|
1474
|
+
function resolveConfig(programmatic = {}, cwd) {
|
|
1475
|
+
const env = envConfig();
|
|
1476
|
+
const global = findGlobalConfig() || {};
|
|
1477
|
+
const local = findLocalConfig(cwd) || {};
|
|
1478
|
+
let config = deepMerge(env, global);
|
|
1479
|
+
config = deepMerge(config, local);
|
|
1480
|
+
config = deepMerge(config, programmatic);
|
|
1481
|
+
return config;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/utils/generation-stats.ts
|
|
1485
|
+
var GenerationStatsStore = class {
|
|
1486
|
+
records = /* @__PURE__ */ new Map();
|
|
1487
|
+
maxRecords;
|
|
1488
|
+
constructor(maxRecords = 1e3) {
|
|
1489
|
+
this.maxRecords = maxRecords;
|
|
1490
|
+
}
|
|
1491
|
+
record(entry) {
|
|
1492
|
+
if (this.records.size >= this.maxRecords) {
|
|
1493
|
+
const oldest = this.records.keys().next().value;
|
|
1494
|
+
if (oldest) this.records.delete(oldest);
|
|
1495
|
+
}
|
|
1496
|
+
this.records.set(entry.id, entry);
|
|
1497
|
+
}
|
|
1498
|
+
get(id) {
|
|
1499
|
+
const rec = this.records.get(id);
|
|
1500
|
+
if (!rec) return void 0;
|
|
1501
|
+
const latency = rec.endTime - rec.startTime;
|
|
1502
|
+
return {
|
|
1503
|
+
id: rec.id,
|
|
1504
|
+
model: rec.model,
|
|
1505
|
+
provider_name: rec.providerName,
|
|
1506
|
+
total_cost: 0,
|
|
1507
|
+
// Cost calculation requires pricing data
|
|
1508
|
+
tokens_prompt: rec.promptTokens,
|
|
1509
|
+
tokens_completion: rec.completionTokens,
|
|
1510
|
+
latency,
|
|
1511
|
+
generation_time: latency,
|
|
1512
|
+
created_at: new Date(rec.startTime).toISOString(),
|
|
1513
|
+
finish_reason: rec.finishReason,
|
|
1514
|
+
streamed: rec.streamed
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
list(limit = 50) {
|
|
1518
|
+
const entries = Array.from(this.records.values()).slice(-limit).reverse();
|
|
1519
|
+
return entries.map((rec) => this.get(rec.id));
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
// src/utils/fs-io.ts
|
|
1524
|
+
import { mkdir, open, readFile as fsReadFile, rename, writeFile as fsWriteFile, readdir as fsReaddir, stat as fsStat } from "fs/promises";
|
|
1525
|
+
import { createWriteStream } from "fs";
|
|
1526
|
+
import path from "path";
|
|
1527
|
+
import PQueue from "p-queue";
|
|
1528
|
+
var writeQueue = new PQueue({ concurrency: 10 });
|
|
1529
|
+
var readQueue = new PQueue({ concurrency: 20 });
|
|
1530
|
+
function configureFsIO(options) {
|
|
1531
|
+
if (options.readConcurrency !== void 0) {
|
|
1532
|
+
readQueue.concurrency = options.readConcurrency;
|
|
1533
|
+
}
|
|
1534
|
+
if (options.writeConcurrency !== void 0) {
|
|
1535
|
+
writeQueue.concurrency = options.writeConcurrency;
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
var ensuredDirs = /* @__PURE__ */ new Set();
|
|
1539
|
+
var joinPathCache = /* @__PURE__ */ new Map();
|
|
1540
|
+
var dirnameCache = /* @__PURE__ */ new Map();
|
|
1541
|
+
var resolvePathCache = /* @__PURE__ */ new Map();
|
|
1542
|
+
async function ensureDir(dir) {
|
|
1543
|
+
if (!dir) return;
|
|
1544
|
+
if (ensuredDirs.has(dir)) return;
|
|
1545
|
+
await mkdir(dir, { recursive: true });
|
|
1546
|
+
ensuredDirs.add(dir);
|
|
1547
|
+
}
|
|
1548
|
+
async function readFileQueued(filePath, encoding = "utf8") {
|
|
1549
|
+
return readQueue.add(async () => {
|
|
1550
|
+
return fsReadFile(filePath, encoding);
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
async function readJsonQueued(filePath) {
|
|
1554
|
+
const raw = await readFileQueued(filePath, "utf8");
|
|
1555
|
+
return JSON.parse(raw);
|
|
1556
|
+
}
|
|
1557
|
+
async function readDirQueued(dirPath) {
|
|
1558
|
+
return readQueue.add(async () => {
|
|
1559
|
+
return fsReaddir(dirPath, { withFileTypes: true });
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
async function pathExistsQueued(p) {
|
|
1563
|
+
return readQueue.add(async () => {
|
|
1564
|
+
try {
|
|
1565
|
+
await fsStat(p);
|
|
1566
|
+
return true;
|
|
1567
|
+
} catch {
|
|
1568
|
+
return false;
|
|
1569
|
+
}
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
async function fileExistsQueued(filePath) {
|
|
1573
|
+
return readQueue.add(async () => {
|
|
1574
|
+
try {
|
|
1575
|
+
const s = await fsStat(filePath);
|
|
1576
|
+
return s.isFile();
|
|
1577
|
+
} catch {
|
|
1578
|
+
return false;
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
async function writeFileQueued(filePath, data) {
|
|
1583
|
+
await writeQueue.add(async () => {
|
|
1584
|
+
const dir = dirnameOf(filePath);
|
|
1585
|
+
await ensureDir(dir);
|
|
1586
|
+
await fsWriteFile(filePath, data);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
async function appendFileQueued(filePath, data) {
|
|
1590
|
+
await writeQueue.add(async () => {
|
|
1591
|
+
const dir = dirnameOf(filePath);
|
|
1592
|
+
await ensureDir(dir);
|
|
1593
|
+
await fsWriteFile(filePath, data, { flag: "a" });
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
async function writeFileFlushedQueued(filePath, data) {
|
|
1597
|
+
await writeQueue.add(async () => {
|
|
1598
|
+
const dir = dirnameOf(filePath);
|
|
1599
|
+
await ensureDir(dir);
|
|
1600
|
+
const tmpPath = joinPath(
|
|
1601
|
+
dir,
|
|
1602
|
+
`.${path.basename(filePath)}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`
|
|
1603
|
+
);
|
|
1604
|
+
const fh = await open(tmpPath, "w");
|
|
1605
|
+
try {
|
|
1606
|
+
await fh.writeFile(data);
|
|
1607
|
+
await fh.sync();
|
|
1608
|
+
} finally {
|
|
1609
|
+
await fh.close();
|
|
1610
|
+
}
|
|
1611
|
+
await rename(tmpPath, filePath);
|
|
1612
|
+
try {
|
|
1613
|
+
const dh = await open(dir, "r");
|
|
1614
|
+
try {
|
|
1615
|
+
await dh.sync();
|
|
1616
|
+
} finally {
|
|
1617
|
+
await dh.close();
|
|
1618
|
+
}
|
|
1619
|
+
} catch {
|
|
1620
|
+
}
|
|
1621
|
+
});
|
|
1622
|
+
}
|
|
1623
|
+
function joinPath(...segments) {
|
|
1624
|
+
const key = segments.join("\0");
|
|
1625
|
+
const cached = joinPathCache.get(key);
|
|
1626
|
+
if (cached !== void 0) return cached;
|
|
1627
|
+
const out = path.join(...segments);
|
|
1628
|
+
joinPathCache.set(key, out);
|
|
1629
|
+
return out;
|
|
1630
|
+
}
|
|
1631
|
+
function dirnameOf(p) {
|
|
1632
|
+
const cached = dirnameCache.get(p);
|
|
1633
|
+
if (cached !== void 0) return cached;
|
|
1634
|
+
const out = path.dirname(p);
|
|
1635
|
+
dirnameCache.set(p, out);
|
|
1636
|
+
return out;
|
|
1637
|
+
}
|
|
1638
|
+
function resolvePath(...segments) {
|
|
1639
|
+
const key = segments.join("\0");
|
|
1640
|
+
const cached = resolvePathCache.get(key);
|
|
1641
|
+
if (cached !== void 0) return cached;
|
|
1642
|
+
const out = path.resolve(...segments);
|
|
1643
|
+
resolvePathCache.set(key, out);
|
|
1644
|
+
return out;
|
|
1645
|
+
}
|
|
1646
|
+
function getFsQueueStatus() {
|
|
1647
|
+
return {
|
|
1648
|
+
read: { size: readQueue.size, pending: readQueue.pending },
|
|
1649
|
+
write: { size: writeQueue.size, pending: writeQueue.pending }
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
async function waitForFsQueuesIdle() {
|
|
1653
|
+
await Promise.all([writeQueue.onIdle(), readQueue.onIdle()]);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// src/batch/store.ts
|
|
1657
|
+
var DEFAULT_BATCH_DIR = joinPath(process.cwd(), ".anymodel", "batches");
|
|
1658
|
+
var BatchStore = class {
|
|
1659
|
+
dir;
|
|
1660
|
+
initialized = false;
|
|
1661
|
+
constructor(dir) {
|
|
1662
|
+
this.dir = resolvePath(dir || DEFAULT_BATCH_DIR);
|
|
1663
|
+
}
|
|
1664
|
+
async init() {
|
|
1665
|
+
if (this.initialized) return;
|
|
1666
|
+
await ensureDir(this.dir);
|
|
1667
|
+
this.initialized = true;
|
|
1668
|
+
}
|
|
1669
|
+
batchDir(id) {
|
|
1670
|
+
return joinPath(this.dir, id);
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Create a new batch directory and save initial metadata.
|
|
1674
|
+
*/
|
|
1675
|
+
async create(batch) {
|
|
1676
|
+
await this.init();
|
|
1677
|
+
const dir = this.batchDir(batch.id);
|
|
1678
|
+
await ensureDir(dir);
|
|
1679
|
+
await writeFileFlushedQueued(joinPath(dir, "meta.json"), JSON.stringify(batch, null, 2));
|
|
1680
|
+
}
|
|
1681
|
+
/**
|
|
1682
|
+
* Update batch metadata (atomic write).
|
|
1683
|
+
*/
|
|
1684
|
+
async updateMeta(batch) {
|
|
1685
|
+
await writeFileFlushedQueued(
|
|
1686
|
+
joinPath(this.batchDir(batch.id), "meta.json"),
|
|
1687
|
+
JSON.stringify(batch, null, 2)
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Save requests as JSONL.
|
|
1692
|
+
*/
|
|
1693
|
+
async saveRequests(id, requests) {
|
|
1694
|
+
const lines = requests.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
1695
|
+
await writeFileQueued(joinPath(this.batchDir(id), "requests.jsonl"), lines);
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Append a result to results.jsonl.
|
|
1699
|
+
*/
|
|
1700
|
+
async appendResult(id, result) {
|
|
1701
|
+
await appendFileQueued(
|
|
1702
|
+
joinPath(this.batchDir(id), "results.jsonl"),
|
|
1703
|
+
JSON.stringify(result) + "\n"
|
|
1704
|
+
);
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Save provider-specific state (e.g., provider batch ID).
|
|
1708
|
+
*/
|
|
1709
|
+
async saveProviderState(id, state) {
|
|
1710
|
+
await writeFileFlushedQueued(
|
|
1711
|
+
joinPath(this.batchDir(id), "provider.json"),
|
|
1712
|
+
JSON.stringify(state, null, 2)
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
/**
|
|
1716
|
+
* Load provider state.
|
|
1717
|
+
*/
|
|
1718
|
+
async loadProviderState(id) {
|
|
1719
|
+
const p = joinPath(this.batchDir(id), "provider.json");
|
|
1720
|
+
if (!await fileExistsQueued(p)) return null;
|
|
1721
|
+
return readJsonQueued(p);
|
|
1722
|
+
}
|
|
1723
|
+
/**
|
|
1724
|
+
* Get batch metadata.
|
|
1725
|
+
*/
|
|
1726
|
+
async getMeta(id) {
|
|
1727
|
+
const p = joinPath(this.batchDir(id), "meta.json");
|
|
1728
|
+
if (!await fileExistsQueued(p)) return null;
|
|
1729
|
+
return readJsonQueued(p);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Get all results for a batch.
|
|
1733
|
+
*/
|
|
1734
|
+
async getResults(id) {
|
|
1735
|
+
const p = joinPath(this.batchDir(id), "results.jsonl");
|
|
1736
|
+
if (!await fileExistsQueued(p)) return [];
|
|
1737
|
+
const raw = await readFileQueued(p, "utf8");
|
|
1738
|
+
return raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* List all batch IDs.
|
|
1742
|
+
*/
|
|
1743
|
+
async listBatches() {
|
|
1744
|
+
await this.init();
|
|
1745
|
+
if (!await pathExistsQueued(this.dir)) return [];
|
|
1746
|
+
const entries = await readDirQueued(this.dir);
|
|
1747
|
+
return entries.filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
1748
|
+
}
|
|
1749
|
+
/**
|
|
1750
|
+
* Check if a batch exists.
|
|
1751
|
+
*/
|
|
1752
|
+
async exists(id) {
|
|
1753
|
+
return fileExistsQueued(joinPath(this.batchDir(id), "meta.json"));
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
// src/batch/manager.ts
|
|
1758
|
+
var BatchManager = class {
|
|
1759
|
+
store;
|
|
1760
|
+
router;
|
|
1761
|
+
concurrencyLimit;
|
|
1762
|
+
defaultPollInterval;
|
|
1763
|
+
batchAdapters = /* @__PURE__ */ new Map();
|
|
1764
|
+
constructor(router, options) {
|
|
1765
|
+
this.store = new BatchStore(options?.dir);
|
|
1766
|
+
this.router = router;
|
|
1767
|
+
this.concurrencyLimit = options?.concurrency ?? 5;
|
|
1768
|
+
this.defaultPollInterval = options?.pollInterval ?? 5e3;
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* Register a native batch adapter for a provider.
|
|
1772
|
+
*/
|
|
1773
|
+
registerBatchAdapter(providerName, adapter) {
|
|
1774
|
+
this.batchAdapters.set(providerName, adapter);
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* Check if a provider has native batch support.
|
|
1778
|
+
*/
|
|
1779
|
+
getNativeBatchAdapter(model) {
|
|
1780
|
+
const providerName = model.split("/")[0];
|
|
1781
|
+
const adapter = this.batchAdapters.get(providerName);
|
|
1782
|
+
return adapter ? { adapter, providerName } : null;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* Create a batch and return immediately (no polling).
|
|
1786
|
+
*/
|
|
1787
|
+
async create(request) {
|
|
1788
|
+
const id = generateId("batch");
|
|
1789
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1790
|
+
const providerName = request.model.split("/")[0] || "unknown";
|
|
1791
|
+
const native = this.getNativeBatchAdapter(request.model);
|
|
1792
|
+
const batchMode = native ? "native" : "concurrent";
|
|
1793
|
+
const batch = {
|
|
1794
|
+
id,
|
|
1795
|
+
object: "batch",
|
|
1796
|
+
status: "pending",
|
|
1797
|
+
model: request.model,
|
|
1798
|
+
provider_name: providerName,
|
|
1799
|
+
batch_mode: batchMode,
|
|
1800
|
+
total: request.requests.length,
|
|
1801
|
+
completed: 0,
|
|
1802
|
+
failed: 0,
|
|
1803
|
+
created_at: now,
|
|
1804
|
+
completed_at: null,
|
|
1805
|
+
expires_at: null
|
|
1806
|
+
};
|
|
1807
|
+
await this.store.create(batch);
|
|
1808
|
+
await this.store.saveRequests(id, request.requests);
|
|
1809
|
+
if (native) {
|
|
1810
|
+
this.processNativeBatch(id, request, native.adapter).catch(() => {
|
|
1811
|
+
});
|
|
1812
|
+
} else {
|
|
1813
|
+
this.processConcurrentBatch(id, request).catch(() => {
|
|
1814
|
+
});
|
|
1815
|
+
}
|
|
1816
|
+
return batch;
|
|
1817
|
+
}
|
|
1818
|
+
/**
|
|
1819
|
+
* Create a batch and poll until completion.
|
|
1820
|
+
*/
|
|
1821
|
+
async createAndPoll(request, options = {}) {
|
|
1822
|
+
const batch = await this.create(request);
|
|
1823
|
+
return this.poll(batch.id, options);
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Poll an existing batch until completion.
|
|
1827
|
+
*/
|
|
1828
|
+
async poll(id, options = {}) {
|
|
1829
|
+
const interval = options.interval ?? this.defaultPollInterval;
|
|
1830
|
+
const timeout = options.timeout ?? 0;
|
|
1831
|
+
const startTime = Date.now();
|
|
1832
|
+
while (true) {
|
|
1833
|
+
let batch = await this.store.getMeta(id);
|
|
1834
|
+
if (!batch) {
|
|
1835
|
+
throw new AnyModelError(404, `Batch ${id} not found`);
|
|
1836
|
+
}
|
|
1837
|
+
if (batch.batch_mode === "native" && batch.status === "processing") {
|
|
1838
|
+
await this.syncNativeBatchStatus(id);
|
|
1839
|
+
batch = await this.store.getMeta(id);
|
|
1840
|
+
if (!batch) throw new AnyModelError(404, `Batch ${id} not found`);
|
|
1841
|
+
}
|
|
1842
|
+
if (options.onProgress) {
|
|
1843
|
+
options.onProgress(batch);
|
|
1844
|
+
}
|
|
1845
|
+
if (batch.status === "completed" || batch.status === "failed" || batch.status === "cancelled") {
|
|
1846
|
+
return this.getResults(id);
|
|
1847
|
+
}
|
|
1848
|
+
if (timeout > 0 && Date.now() - startTime > timeout) {
|
|
1849
|
+
throw new AnyModelError(408, `Batch ${id} timed out after ${timeout}ms`);
|
|
1850
|
+
}
|
|
1851
|
+
await new Promise((resolve2) => setTimeout(resolve2, interval));
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Get the current status of a batch.
|
|
1856
|
+
*/
|
|
1857
|
+
async get(id) {
|
|
1858
|
+
return this.store.getMeta(id);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* Get results for a completed batch.
|
|
1862
|
+
*/
|
|
1863
|
+
async getResults(id) {
|
|
1864
|
+
const batch = await this.store.getMeta(id);
|
|
1865
|
+
if (!batch) {
|
|
1866
|
+
throw new AnyModelError(404, `Batch ${id} not found`);
|
|
1867
|
+
}
|
|
1868
|
+
const results = await this.store.getResults(id);
|
|
1869
|
+
const usage = {
|
|
1870
|
+
total_prompt_tokens: 0,
|
|
1871
|
+
total_completion_tokens: 0,
|
|
1872
|
+
estimated_cost: 0
|
|
1873
|
+
};
|
|
1874
|
+
for (const result of results) {
|
|
1875
|
+
if (result.response) {
|
|
1876
|
+
usage.total_prompt_tokens += result.response.usage.prompt_tokens;
|
|
1877
|
+
usage.total_completion_tokens += result.response.usage.completion_tokens;
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
id: batch.id,
|
|
1882
|
+
status: batch.status,
|
|
1883
|
+
results,
|
|
1884
|
+
usage_summary: usage
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* List all batches.
|
|
1889
|
+
*/
|
|
1890
|
+
async list() {
|
|
1891
|
+
const ids = await this.store.listBatches();
|
|
1892
|
+
const batches = [];
|
|
1893
|
+
for (const id of ids) {
|
|
1894
|
+
const meta = await this.store.getMeta(id);
|
|
1895
|
+
if (meta) batches.push(meta);
|
|
1896
|
+
}
|
|
1897
|
+
return batches;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Cancel a batch.
|
|
1901
|
+
*/
|
|
1902
|
+
async cancel(id) {
|
|
1903
|
+
const batch = await this.store.getMeta(id);
|
|
1904
|
+
if (!batch) {
|
|
1905
|
+
throw new AnyModelError(404, `Batch ${id} not found`);
|
|
1906
|
+
}
|
|
1907
|
+
if (batch.status === "completed" || batch.status === "cancelled") {
|
|
1908
|
+
return batch;
|
|
1909
|
+
}
|
|
1910
|
+
if (batch.batch_mode === "native") {
|
|
1911
|
+
const providerState = await this.store.loadProviderState(id);
|
|
1912
|
+
const adapter = this.batchAdapters.get(batch.provider_name);
|
|
1913
|
+
if (adapter && providerState?.providerBatchId) {
|
|
1914
|
+
try {
|
|
1915
|
+
await adapter.cancelBatch(providerState.providerBatchId);
|
|
1916
|
+
} catch {
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
batch.status = "cancelled";
|
|
1921
|
+
batch.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1922
|
+
await this.store.updateMeta(batch);
|
|
1923
|
+
return batch;
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Process batch via native provider batch API.
|
|
1927
|
+
*/
|
|
1928
|
+
async processNativeBatch(batchId, request, adapter) {
|
|
1929
|
+
const batch = await this.store.getMeta(batchId);
|
|
1930
|
+
if (!batch) return;
|
|
1931
|
+
try {
|
|
1932
|
+
const model = request.model.includes("/") ? request.model.split("/").slice(1).join("/") : request.model;
|
|
1933
|
+
const { providerBatchId, metadata } = await adapter.createBatch(
|
|
1934
|
+
model,
|
|
1935
|
+
request.requests,
|
|
1936
|
+
request.options
|
|
1937
|
+
);
|
|
1938
|
+
await this.store.saveProviderState(batchId, {
|
|
1939
|
+
providerBatchId,
|
|
1940
|
+
providerName: batch.provider_name,
|
|
1941
|
+
...metadata
|
|
1942
|
+
});
|
|
1943
|
+
batch.status = "processing";
|
|
1944
|
+
await this.store.updateMeta(batch);
|
|
1945
|
+
} catch (err) {
|
|
1946
|
+
batch.status = "failed";
|
|
1947
|
+
batch.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1948
|
+
await this.store.updateMeta(batch);
|
|
1949
|
+
throw err;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* Sync native batch status from provider.
|
|
1954
|
+
*/
|
|
1955
|
+
async syncNativeBatchStatus(batchId) {
|
|
1956
|
+
const batch = await this.store.getMeta(batchId);
|
|
1957
|
+
if (!batch) return;
|
|
1958
|
+
const providerState = await this.store.loadProviderState(batchId);
|
|
1959
|
+
if (!providerState?.providerBatchId) return;
|
|
1960
|
+
const adapter = this.batchAdapters.get(batch.provider_name);
|
|
1961
|
+
if (!adapter) return;
|
|
1962
|
+
try {
|
|
1963
|
+
const status = await adapter.pollBatch(providerState.providerBatchId);
|
|
1964
|
+
batch.total = status.total || batch.total;
|
|
1965
|
+
batch.completed = status.completed;
|
|
1966
|
+
batch.failed = status.failed;
|
|
1967
|
+
if (status.status === "completed" || status.status === "failed" || status.status === "cancelled") {
|
|
1968
|
+
batch.status = status.status;
|
|
1969
|
+
batch.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
1970
|
+
if (status.status === "completed" || status.status === "failed") {
|
|
1971
|
+
try {
|
|
1972
|
+
const results = await adapter.getBatchResults(providerState.providerBatchId);
|
|
1973
|
+
for (const result of results) {
|
|
1974
|
+
await this.store.appendResult(batchId, result);
|
|
1975
|
+
}
|
|
1976
|
+
batch.completed = results.filter((r) => r.status === "success").length;
|
|
1977
|
+
batch.failed = results.filter((r) => r.status === "error").length;
|
|
1978
|
+
} catch {
|
|
1979
|
+
if (batch.status !== "failed") {
|
|
1980
|
+
batch.status = "failed";
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
} else {
|
|
1985
|
+
batch.status = "processing";
|
|
1986
|
+
}
|
|
1987
|
+
await this.store.updateMeta(batch);
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Process batch requests concurrently (fallback path).
|
|
1993
|
+
*/
|
|
1994
|
+
async processConcurrentBatch(batchId, request) {
|
|
1995
|
+
const batch = await this.store.getMeta(batchId);
|
|
1996
|
+
if (!batch) return;
|
|
1997
|
+
batch.status = "processing";
|
|
1998
|
+
await this.store.updateMeta(batch);
|
|
1999
|
+
const items = request.requests;
|
|
2000
|
+
const active = /* @__PURE__ */ new Set();
|
|
2001
|
+
const processItem = async (item) => {
|
|
2002
|
+
const current = await this.store.getMeta(batchId);
|
|
2003
|
+
if (current?.status === "cancelled") return;
|
|
2004
|
+
const chatRequest = {
|
|
2005
|
+
model: request.model,
|
|
2006
|
+
messages: item.messages,
|
|
2007
|
+
max_tokens: item.max_tokens ?? request.options?.max_tokens,
|
|
2008
|
+
temperature: item.temperature ?? request.options?.temperature,
|
|
2009
|
+
top_p: item.top_p ?? request.options?.top_p,
|
|
2010
|
+
top_k: item.top_k ?? request.options?.top_k,
|
|
2011
|
+
stop: item.stop ?? request.options?.stop,
|
|
2012
|
+
response_format: item.response_format ?? request.options?.response_format,
|
|
2013
|
+
tools: item.tools ?? request.options?.tools,
|
|
2014
|
+
tool_choice: item.tool_choice ?? request.options?.tool_choice
|
|
2015
|
+
};
|
|
2016
|
+
let result;
|
|
2017
|
+
try {
|
|
2018
|
+
const response = await this.router.complete(chatRequest);
|
|
2019
|
+
result = {
|
|
2020
|
+
custom_id: item.custom_id,
|
|
2021
|
+
status: "success",
|
|
2022
|
+
response,
|
|
2023
|
+
error: null
|
|
2024
|
+
};
|
|
2025
|
+
} catch (err) {
|
|
2026
|
+
const error = err instanceof AnyModelError ? err : new AnyModelError(500, String(err));
|
|
2027
|
+
result = {
|
|
2028
|
+
custom_id: item.custom_id,
|
|
2029
|
+
status: "error",
|
|
2030
|
+
response: null,
|
|
2031
|
+
error: { code: error.code, message: error.message }
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
await this.store.appendResult(batchId, result);
|
|
2035
|
+
const meta = await this.store.getMeta(batchId);
|
|
2036
|
+
if (meta) {
|
|
2037
|
+
if (result.status === "success") {
|
|
2038
|
+
meta.completed++;
|
|
2039
|
+
} else {
|
|
2040
|
+
meta.failed++;
|
|
2041
|
+
}
|
|
2042
|
+
await this.store.updateMeta(meta);
|
|
2043
|
+
}
|
|
2044
|
+
};
|
|
2045
|
+
for (const item of items) {
|
|
2046
|
+
const current = await this.store.getMeta(batchId);
|
|
2047
|
+
if (current?.status === "cancelled") break;
|
|
2048
|
+
if (active.size >= this.concurrencyLimit) {
|
|
2049
|
+
await Promise.race(active);
|
|
2050
|
+
}
|
|
2051
|
+
const promise = processItem(item).then(() => {
|
|
2052
|
+
active.delete(promise);
|
|
2053
|
+
});
|
|
2054
|
+
active.add(promise);
|
|
2055
|
+
}
|
|
2056
|
+
await Promise.all(active);
|
|
2057
|
+
const finalMeta = await this.store.getMeta(batchId);
|
|
2058
|
+
if (finalMeta && finalMeta.status !== "cancelled") {
|
|
2059
|
+
finalMeta.status = finalMeta.failed === finalMeta.total ? "failed" : "completed";
|
|
2060
|
+
finalMeta.completed_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
2061
|
+
await this.store.updateMeta(finalMeta);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
|
|
2066
|
+
// src/providers/openai-batch.ts
|
|
2067
|
+
var OPENAI_API_BASE2 = "https://api.openai.com/v1";
|
|
2068
|
+
function createOpenAIBatchAdapter(apiKey) {
|
|
2069
|
+
async function apiRequest(path2, options = {}) {
|
|
2070
|
+
const headers = {
|
|
2071
|
+
"Authorization": `Bearer ${apiKey}`
|
|
2072
|
+
};
|
|
2073
|
+
let fetchBody;
|
|
2074
|
+
if (options.formData) {
|
|
2075
|
+
fetchBody = options.formData;
|
|
2076
|
+
} else if (options.body) {
|
|
2077
|
+
headers["Content-Type"] = "application/json";
|
|
2078
|
+
fetchBody = JSON.stringify(options.body);
|
|
2079
|
+
}
|
|
2080
|
+
const res = await fetch(`${OPENAI_API_BASE2}${path2}`, {
|
|
2081
|
+
method: options.method || "GET",
|
|
2082
|
+
headers,
|
|
2083
|
+
body: fetchBody
|
|
2084
|
+
});
|
|
2085
|
+
if (!res.ok) {
|
|
2086
|
+
let errorBody;
|
|
2087
|
+
try {
|
|
2088
|
+
errorBody = await res.json();
|
|
2089
|
+
} catch {
|
|
2090
|
+
errorBody = { message: res.statusText };
|
|
2091
|
+
}
|
|
2092
|
+
const msg = errorBody?.error?.message || errorBody?.message || res.statusText;
|
|
2093
|
+
throw new AnyModelError(res.status >= 500 ? 502 : res.status, msg, {
|
|
2094
|
+
provider_name: "openai",
|
|
2095
|
+
raw: errorBody
|
|
2096
|
+
});
|
|
2097
|
+
}
|
|
2098
|
+
return res;
|
|
2099
|
+
}
|
|
2100
|
+
function buildJSONL(model, requests) {
|
|
2101
|
+
return requests.map((req) => {
|
|
2102
|
+
const body = {
|
|
2103
|
+
model,
|
|
2104
|
+
messages: req.messages
|
|
2105
|
+
};
|
|
2106
|
+
if (req.max_tokens !== void 0) body.max_tokens = req.max_tokens;
|
|
2107
|
+
if (req.temperature !== void 0) body.temperature = req.temperature;
|
|
2108
|
+
if (req.top_p !== void 0) body.top_p = req.top_p;
|
|
2109
|
+
if (req.stop !== void 0) body.stop = req.stop;
|
|
2110
|
+
if (req.response_format !== void 0) body.response_format = req.response_format;
|
|
2111
|
+
if (req.tools !== void 0) body.tools = req.tools;
|
|
2112
|
+
if (req.tool_choice !== void 0) body.tool_choice = req.tool_choice;
|
|
2113
|
+
return JSON.stringify({
|
|
2114
|
+
custom_id: req.custom_id,
|
|
2115
|
+
method: "POST",
|
|
2116
|
+
url: "/v1/chat/completions",
|
|
2117
|
+
body
|
|
2118
|
+
});
|
|
2119
|
+
}).join("\n");
|
|
2120
|
+
}
|
|
2121
|
+
function rePrefixId(id) {
|
|
2122
|
+
if (id && id.startsWith("chatcmpl-")) {
|
|
2123
|
+
return `gen-${id.substring(9)}`;
|
|
2124
|
+
}
|
|
2125
|
+
return id.startsWith("gen-") ? id : `gen-${id}`;
|
|
2126
|
+
}
|
|
2127
|
+
function translateOpenAIResponse(body) {
|
|
2128
|
+
return {
|
|
2129
|
+
id: rePrefixId(body.id || generateId()),
|
|
2130
|
+
object: "chat.completion",
|
|
2131
|
+
created: body.created || Math.floor(Date.now() / 1e3),
|
|
2132
|
+
model: `openai/${body.model}`,
|
|
2133
|
+
choices: body.choices,
|
|
2134
|
+
usage: body.usage
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
function mapStatus(openaiStatus) {
|
|
2138
|
+
switch (openaiStatus) {
|
|
2139
|
+
case "validating":
|
|
2140
|
+
case "finalizing":
|
|
2141
|
+
return "processing";
|
|
2142
|
+
case "in_progress":
|
|
2143
|
+
return "processing";
|
|
2144
|
+
case "completed":
|
|
2145
|
+
return "completed";
|
|
2146
|
+
case "failed":
|
|
2147
|
+
return "failed";
|
|
2148
|
+
case "expired":
|
|
2149
|
+
return "failed";
|
|
2150
|
+
case "cancelled":
|
|
2151
|
+
case "cancelling":
|
|
2152
|
+
return "cancelled";
|
|
2153
|
+
default:
|
|
2154
|
+
return "pending";
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
return {
|
|
2158
|
+
async createBatch(model, requests, options) {
|
|
2159
|
+
const jsonlContent = buildJSONL(model, requests);
|
|
2160
|
+
const blob = new Blob([jsonlContent], { type: "application/jsonl" });
|
|
2161
|
+
const formData = new FormData();
|
|
2162
|
+
formData.append("purpose", "batch");
|
|
2163
|
+
formData.append("file", blob, "batch_input.jsonl");
|
|
2164
|
+
const uploadRes = await apiRequest("/files", { method: "POST", formData });
|
|
2165
|
+
const fileData = await uploadRes.json();
|
|
2166
|
+
const inputFileId = fileData.id;
|
|
2167
|
+
const batchRes = await apiRequest("/batches", {
|
|
2168
|
+
method: "POST",
|
|
2169
|
+
body: {
|
|
2170
|
+
input_file_id: inputFileId,
|
|
2171
|
+
endpoint: "/v1/chat/completions",
|
|
2172
|
+
completion_window: "24h",
|
|
2173
|
+
metadata: options?.metadata
|
|
2174
|
+
}
|
|
2175
|
+
});
|
|
2176
|
+
const batchData = await batchRes.json();
|
|
2177
|
+
return {
|
|
2178
|
+
providerBatchId: batchData.id,
|
|
2179
|
+
metadata: {
|
|
2180
|
+
input_file_id: inputFileId,
|
|
2181
|
+
openai_status: batchData.status
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
},
|
|
2185
|
+
async pollBatch(providerBatchId) {
|
|
2186
|
+
const res = await apiRequest(`/batches/${providerBatchId}`);
|
|
2187
|
+
const data = await res.json();
|
|
2188
|
+
const requestCounts = data.request_counts || {};
|
|
2189
|
+
return {
|
|
2190
|
+
status: mapStatus(data.status),
|
|
2191
|
+
total: requestCounts.total || 0,
|
|
2192
|
+
completed: requestCounts.completed || 0,
|
|
2193
|
+
failed: requestCounts.failed || 0
|
|
2194
|
+
};
|
|
2195
|
+
},
|
|
2196
|
+
async getBatchResults(providerBatchId) {
|
|
2197
|
+
const batchRes = await apiRequest(`/batches/${providerBatchId}`);
|
|
2198
|
+
const batchData = await batchRes.json();
|
|
2199
|
+
const results = [];
|
|
2200
|
+
if (batchData.output_file_id) {
|
|
2201
|
+
const outputRes = await apiRequest(`/files/${batchData.output_file_id}/content`);
|
|
2202
|
+
const outputText = await outputRes.text();
|
|
2203
|
+
for (const line of outputText.trim().split("\n")) {
|
|
2204
|
+
if (!line) continue;
|
|
2205
|
+
const item = JSON.parse(line);
|
|
2206
|
+
if (item.response?.status_code === 200) {
|
|
2207
|
+
results.push({
|
|
2208
|
+
custom_id: item.custom_id,
|
|
2209
|
+
status: "success",
|
|
2210
|
+
response: translateOpenAIResponse(item.response.body),
|
|
2211
|
+
error: null
|
|
2212
|
+
});
|
|
2213
|
+
} else {
|
|
2214
|
+
results.push({
|
|
2215
|
+
custom_id: item.custom_id,
|
|
2216
|
+
status: "error",
|
|
2217
|
+
response: null,
|
|
2218
|
+
error: {
|
|
2219
|
+
code: item.response?.status_code || 500,
|
|
2220
|
+
message: item.error?.message || item.response?.body?.error?.message || "Unknown error"
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (batchData.error_file_id) {
|
|
2227
|
+
const errorRes = await apiRequest(`/files/${batchData.error_file_id}/content`);
|
|
2228
|
+
const errorText = await errorRes.text();
|
|
2229
|
+
for (const line of errorText.trim().split("\n")) {
|
|
2230
|
+
if (!line) continue;
|
|
2231
|
+
const item = JSON.parse(line);
|
|
2232
|
+
const existing = results.find((r) => r.custom_id === item.custom_id);
|
|
2233
|
+
if (!existing) {
|
|
2234
|
+
results.push({
|
|
2235
|
+
custom_id: item.custom_id,
|
|
2236
|
+
status: "error",
|
|
2237
|
+
response: null,
|
|
2238
|
+
error: {
|
|
2239
|
+
code: item.response?.status_code || 500,
|
|
2240
|
+
message: item.error?.message || "Batch item error"
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
return results;
|
|
2247
|
+
},
|
|
2248
|
+
async cancelBatch(providerBatchId) {
|
|
2249
|
+
await apiRequest(`/batches/${providerBatchId}/cancel`, { method: "POST" });
|
|
2250
|
+
}
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
// src/providers/anthropic-batch.ts
|
|
2255
|
+
var ANTHROPIC_API_BASE2 = "https://api.anthropic.com/v1";
|
|
2256
|
+
var ANTHROPIC_VERSION2 = "2023-06-01";
|
|
2257
|
+
var DEFAULT_MAX_TOKENS2 = 4096;
|
|
2258
|
+
function createAnthropicBatchAdapter(apiKey) {
|
|
2259
|
+
async function apiRequest(path2, options = {}) {
|
|
2260
|
+
const headers = {
|
|
2261
|
+
"x-api-key": apiKey,
|
|
2262
|
+
"anthropic-version": ANTHROPIC_VERSION2,
|
|
2263
|
+
"Content-Type": "application/json"
|
|
2264
|
+
};
|
|
2265
|
+
const res = await fetch(`${ANTHROPIC_API_BASE2}${path2}`, {
|
|
2266
|
+
method: options.method || "GET",
|
|
2267
|
+
headers,
|
|
2268
|
+
body: options.body ? JSON.stringify(options.body) : void 0
|
|
2269
|
+
});
|
|
2270
|
+
if (!res.ok) {
|
|
2271
|
+
let errorBody;
|
|
2272
|
+
try {
|
|
2273
|
+
errorBody = await res.json();
|
|
2274
|
+
} catch {
|
|
2275
|
+
errorBody = { message: res.statusText };
|
|
2276
|
+
}
|
|
2277
|
+
const msg = errorBody?.error?.message || errorBody?.message || res.statusText;
|
|
2278
|
+
throw new AnyModelError(res.status >= 500 ? 502 : res.status, msg, {
|
|
2279
|
+
provider_name: "anthropic",
|
|
2280
|
+
raw: errorBody
|
|
2281
|
+
});
|
|
2282
|
+
}
|
|
2283
|
+
return res;
|
|
2284
|
+
}
|
|
2285
|
+
function translateToAnthropicParams(model, req) {
|
|
2286
|
+
const params = {
|
|
2287
|
+
model,
|
|
2288
|
+
max_tokens: req.max_tokens || DEFAULT_MAX_TOKENS2
|
|
2289
|
+
};
|
|
2290
|
+
const systemMessages = req.messages.filter((m) => m.role === "system");
|
|
2291
|
+
const nonSystemMessages = req.messages.filter((m) => m.role !== "system");
|
|
2292
|
+
if (systemMessages.length > 0) {
|
|
2293
|
+
params.system = systemMessages.map((m) => typeof m.content === "string" ? m.content : "").join("\n");
|
|
2294
|
+
}
|
|
2295
|
+
params.messages = nonSystemMessages.map((m) => ({
|
|
2296
|
+
role: m.role === "tool" ? "user" : m.role,
|
|
2297
|
+
content: m.tool_call_id ? [{ type: "tool_result", tool_use_id: m.tool_call_id, content: typeof m.content === "string" ? m.content : "" }] : m.content
|
|
2298
|
+
}));
|
|
2299
|
+
if (req.temperature !== void 0) params.temperature = req.temperature;
|
|
2300
|
+
if (req.top_p !== void 0) params.top_p = req.top_p;
|
|
2301
|
+
if (req.top_k !== void 0) params.top_k = req.top_k;
|
|
2302
|
+
if (req.stop !== void 0) params.stop_sequences = Array.isArray(req.stop) ? req.stop : [req.stop];
|
|
2303
|
+
if (req.tools && req.tools.length > 0) {
|
|
2304
|
+
params.tools = req.tools.map((t) => ({
|
|
2305
|
+
name: t.function.name,
|
|
2306
|
+
description: t.function.description || "",
|
|
2307
|
+
input_schema: t.function.parameters || { type: "object", properties: {} }
|
|
2308
|
+
}));
|
|
2309
|
+
if (req.tool_choice) {
|
|
2310
|
+
if (req.tool_choice === "auto") {
|
|
2311
|
+
params.tool_choice = { type: "auto" };
|
|
2312
|
+
} else if (req.tool_choice === "required") {
|
|
2313
|
+
params.tool_choice = { type: "any" };
|
|
2314
|
+
} else if (req.tool_choice === "none") {
|
|
2315
|
+
delete params.tools;
|
|
2316
|
+
} else if (typeof req.tool_choice === "object") {
|
|
2317
|
+
params.tool_choice = { type: "tool", name: req.tool_choice.function.name };
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
if (req.response_format) {
|
|
2322
|
+
if (req.response_format.type === "json_object" || req.response_format.type === "json_schema") {
|
|
2323
|
+
const jsonInstruction = "Respond with valid JSON only. Do not include any text outside the JSON object.";
|
|
2324
|
+
params.system = params.system ? `${jsonInstruction}
|
|
2325
|
+
|
|
2326
|
+
${params.system}` : jsonInstruction;
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
return params;
|
|
2330
|
+
}
|
|
2331
|
+
function mapStopReason(reason) {
|
|
2332
|
+
switch (reason) {
|
|
2333
|
+
case "end_turn":
|
|
2334
|
+
return "stop";
|
|
2335
|
+
case "max_tokens":
|
|
2336
|
+
return "length";
|
|
2337
|
+
case "tool_use":
|
|
2338
|
+
return "tool_calls";
|
|
2339
|
+
case "stop_sequence":
|
|
2340
|
+
return "stop";
|
|
2341
|
+
default:
|
|
2342
|
+
return "stop";
|
|
2343
|
+
}
|
|
2344
|
+
}
|
|
2345
|
+
function translateAnthropicMessage(msg) {
|
|
2346
|
+
let content = "";
|
|
2347
|
+
const toolCalls = [];
|
|
2348
|
+
for (const block of msg.content || []) {
|
|
2349
|
+
if (block.type === "text") {
|
|
2350
|
+
content += block.text;
|
|
2351
|
+
} else if (block.type === "tool_use") {
|
|
2352
|
+
toolCalls.push({
|
|
2353
|
+
id: block.id,
|
|
2354
|
+
type: "function",
|
|
2355
|
+
function: {
|
|
2356
|
+
name: block.name,
|
|
2357
|
+
arguments: JSON.stringify(block.input)
|
|
2358
|
+
}
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const message = { role: "assistant", content };
|
|
2363
|
+
if (toolCalls.length > 0) {
|
|
2364
|
+
message.tool_calls = toolCalls;
|
|
2365
|
+
}
|
|
2366
|
+
return {
|
|
2367
|
+
id: generateId(),
|
|
2368
|
+
object: "chat.completion",
|
|
2369
|
+
created: Math.floor(Date.now() / 1e3),
|
|
2370
|
+
model: `anthropic/${msg.model}`,
|
|
2371
|
+
choices: [{
|
|
2372
|
+
index: 0,
|
|
2373
|
+
message,
|
|
2374
|
+
finish_reason: mapStopReason(msg.stop_reason)
|
|
2375
|
+
}],
|
|
2376
|
+
usage: {
|
|
2377
|
+
prompt_tokens: msg.usage?.input_tokens || 0,
|
|
2378
|
+
completion_tokens: msg.usage?.output_tokens || 0,
|
|
2379
|
+
total_tokens: (msg.usage?.input_tokens || 0) + (msg.usage?.output_tokens || 0)
|
|
2380
|
+
}
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
return {
|
|
2384
|
+
async createBatch(model, requests, _options) {
|
|
2385
|
+
const batchRequests = requests.map((req) => ({
|
|
2386
|
+
custom_id: req.custom_id,
|
|
2387
|
+
params: translateToAnthropicParams(model, req)
|
|
2388
|
+
}));
|
|
2389
|
+
const res = await apiRequest("/messages/batches", {
|
|
2390
|
+
method: "POST",
|
|
2391
|
+
body: { requests: batchRequests }
|
|
2392
|
+
});
|
|
2393
|
+
const data = await res.json();
|
|
2394
|
+
return {
|
|
2395
|
+
providerBatchId: data.id,
|
|
2396
|
+
metadata: {
|
|
2397
|
+
anthropic_type: data.type,
|
|
2398
|
+
created_at: data.created_at
|
|
2399
|
+
}
|
|
2400
|
+
};
|
|
2401
|
+
},
|
|
2402
|
+
async pollBatch(providerBatchId) {
|
|
2403
|
+
const res = await apiRequest(`/messages/batches/${providerBatchId}`);
|
|
2404
|
+
const data = await res.json();
|
|
2405
|
+
const counts = data.request_counts || {};
|
|
2406
|
+
const total = (counts.processing || 0) + (counts.succeeded || 0) + (counts.errored || 0) + (counts.canceled || 0) + (counts.expired || 0);
|
|
2407
|
+
let status;
|
|
2408
|
+
if (data.processing_status === "ended") {
|
|
2409
|
+
if (counts.succeeded === 0 && (counts.errored > 0 || counts.expired > 0 || counts.canceled > 0)) {
|
|
2410
|
+
status = "failed";
|
|
2411
|
+
} else if (data.cancel_initiated_at) {
|
|
2412
|
+
status = "cancelled";
|
|
2413
|
+
} else {
|
|
2414
|
+
status = "completed";
|
|
2415
|
+
}
|
|
2416
|
+
} else {
|
|
2417
|
+
status = "processing";
|
|
2418
|
+
}
|
|
2419
|
+
return {
|
|
2420
|
+
status,
|
|
2421
|
+
total,
|
|
2422
|
+
completed: counts.succeeded || 0,
|
|
2423
|
+
failed: (counts.errored || 0) + (counts.expired || 0) + (counts.canceled || 0)
|
|
2424
|
+
};
|
|
2425
|
+
},
|
|
2426
|
+
async getBatchResults(providerBatchId) {
|
|
2427
|
+
const res = await apiRequest(`/messages/batches/${providerBatchId}/results`);
|
|
2428
|
+
const text = await res.text();
|
|
2429
|
+
const results = [];
|
|
2430
|
+
for (const line of text.trim().split("\n")) {
|
|
2431
|
+
if (!line) continue;
|
|
2432
|
+
const item = JSON.parse(line);
|
|
2433
|
+
if (item.result?.type === "succeeded") {
|
|
2434
|
+
results.push({
|
|
2435
|
+
custom_id: item.custom_id,
|
|
2436
|
+
status: "success",
|
|
2437
|
+
response: translateAnthropicMessage(item.result.message),
|
|
2438
|
+
error: null
|
|
2439
|
+
});
|
|
2440
|
+
} else {
|
|
2441
|
+
const errorType = item.result?.type || "unknown";
|
|
2442
|
+
const errorMsg = item.result?.error?.message || `Batch item ${errorType}`;
|
|
2443
|
+
results.push({
|
|
2444
|
+
custom_id: item.custom_id,
|
|
2445
|
+
status: "error",
|
|
2446
|
+
response: null,
|
|
2447
|
+
error: {
|
|
2448
|
+
code: errorType === "expired" ? 408 : 500,
|
|
2449
|
+
message: errorMsg
|
|
2450
|
+
}
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
return results;
|
|
2455
|
+
},
|
|
2456
|
+
async cancelBatch(providerBatchId) {
|
|
2457
|
+
await apiRequest(`/messages/batches/${providerBatchId}/cancel`, { method: "POST" });
|
|
2458
|
+
}
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// src/client.ts
|
|
2463
|
+
var AnyModel = class {
|
|
2464
|
+
registry;
|
|
2465
|
+
router;
|
|
2466
|
+
config;
|
|
2467
|
+
modelCache = null;
|
|
2468
|
+
statsStore = new GenerationStatsStore();
|
|
2469
|
+
batchManager;
|
|
2470
|
+
chat;
|
|
2471
|
+
models;
|
|
2472
|
+
generation;
|
|
2473
|
+
batches;
|
|
2474
|
+
constructor(config = {}) {
|
|
2475
|
+
this.config = resolveConfig(config);
|
|
2476
|
+
this.registry = new ProviderRegistry();
|
|
2477
|
+
if (this.config.io) {
|
|
2478
|
+
configureFsIO(this.config.io);
|
|
2479
|
+
}
|
|
2480
|
+
this.registerProviders();
|
|
2481
|
+
this.router = new Router(this.registry, this.config.aliases, this.config);
|
|
2482
|
+
this.chat = {
|
|
2483
|
+
completions: {
|
|
2484
|
+
create: async (request) => {
|
|
2485
|
+
const merged = this.applyDefaults(request);
|
|
2486
|
+
if (merged.stream) {
|
|
2487
|
+
return this.router.stream(merged);
|
|
2488
|
+
}
|
|
2489
|
+
const startTime = Date.now();
|
|
2490
|
+
const response = await this.router.complete(merged);
|
|
2491
|
+
const endTime = Date.now();
|
|
2492
|
+
const providerName = response.model.includes("/") ? response.model.split("/")[0] : "unknown";
|
|
2493
|
+
this.statsStore.record({
|
|
2494
|
+
id: response.id,
|
|
2495
|
+
model: response.model,
|
|
2496
|
+
providerName,
|
|
2497
|
+
promptTokens: response.usage.prompt_tokens,
|
|
2498
|
+
completionTokens: response.usage.completion_tokens,
|
|
2499
|
+
startTime,
|
|
2500
|
+
endTime,
|
|
2501
|
+
finishReason: response.choices[0]?.finish_reason || "stop",
|
|
2502
|
+
streamed: false
|
|
2503
|
+
});
|
|
2504
|
+
return response;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
this.models = {
|
|
2509
|
+
list: async (opts) => {
|
|
2510
|
+
if (!this.modelCache) {
|
|
2511
|
+
this.modelCache = await this.fetchModels();
|
|
2512
|
+
}
|
|
2513
|
+
if (opts?.provider) {
|
|
2514
|
+
return this.modelCache.filter((m) => m.id.startsWith(`${opts.provider}/`));
|
|
2515
|
+
}
|
|
2516
|
+
return this.modelCache;
|
|
2517
|
+
},
|
|
2518
|
+
refresh: async () => {
|
|
2519
|
+
this.modelCache = null;
|
|
2520
|
+
return this.models.list();
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
this.generation = {
|
|
2524
|
+
get: (id) => this.statsStore.get(id),
|
|
2525
|
+
list: (limit) => this.statsStore.list(limit)
|
|
2526
|
+
};
|
|
2527
|
+
this.batchManager = new BatchManager(this.router, {
|
|
2528
|
+
dir: this.config.batch?.dir,
|
|
2529
|
+
concurrency: this.config.batch?.concurrencyFallback,
|
|
2530
|
+
pollInterval: this.config.batch?.pollInterval
|
|
2531
|
+
});
|
|
2532
|
+
this.registerBatchAdapters();
|
|
2533
|
+
this.batches = {
|
|
2534
|
+
create: (request) => this.batchManager.create(request),
|
|
2535
|
+
createAndPoll: (request, options) => this.batchManager.createAndPoll(request, options),
|
|
2536
|
+
poll: (id, options) => this.batchManager.poll(id, options),
|
|
2537
|
+
get: (id) => this.batchManager.get(id),
|
|
2538
|
+
list: () => this.batchManager.list(),
|
|
2539
|
+
cancel: (id) => this.batchManager.cancel(id),
|
|
2540
|
+
results: (id) => this.batchManager.getResults(id)
|
|
2541
|
+
};
|
|
2542
|
+
}
|
|
2543
|
+
registerProviders() {
|
|
2544
|
+
const config = this.config;
|
|
2545
|
+
const openaiKey = config.openai?.apiKey || process.env.OPENAI_API_KEY;
|
|
2546
|
+
if (openaiKey) {
|
|
2547
|
+
this.registry.register("openai", createOpenAIAdapter(openaiKey));
|
|
2548
|
+
}
|
|
2549
|
+
const anthropicKey = config.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
2550
|
+
if (anthropicKey) {
|
|
2551
|
+
this.registry.register("anthropic", createAnthropicAdapter(anthropicKey));
|
|
2552
|
+
}
|
|
2553
|
+
const googleKey = config.google?.apiKey || process.env.GOOGLE_API_KEY;
|
|
2554
|
+
if (googleKey) {
|
|
2555
|
+
this.registry.register("google", createGoogleAdapter(googleKey));
|
|
2556
|
+
}
|
|
2557
|
+
const builtinProviders = [
|
|
2558
|
+
{ name: "mistral", baseURL: "https://api.mistral.ai/v1", configKey: "mistral", envVar: "MISTRAL_API_KEY" },
|
|
2559
|
+
{ name: "groq", baseURL: "https://api.groq.com/openai/v1", configKey: "groq", envVar: "GROQ_API_KEY" },
|
|
2560
|
+
{ name: "deepseek", baseURL: "https://api.deepseek.com", configKey: "deepseek", envVar: "DEEPSEEK_API_KEY" },
|
|
2561
|
+
{ name: "xai", baseURL: "https://api.x.ai/v1", configKey: "xai", envVar: "XAI_API_KEY" },
|
|
2562
|
+
{ name: "together", baseURL: "https://api.together.xyz/v1", configKey: "together", envVar: "TOGETHER_API_KEY" },
|
|
2563
|
+
{ name: "fireworks", baseURL: "https://api.fireworks.ai/inference/v1", configKey: "fireworks", envVar: "FIREWORKS_API_KEY" },
|
|
2564
|
+
{ name: "perplexity", baseURL: "https://api.perplexity.ai", configKey: "perplexity", envVar: "PERPLEXITY_API_KEY" }
|
|
2565
|
+
];
|
|
2566
|
+
for (const { name, baseURL, configKey, envVar } of builtinProviders) {
|
|
2567
|
+
const providerConfig = config[configKey];
|
|
2568
|
+
const key = providerConfig?.apiKey || process.env[envVar];
|
|
2569
|
+
if (key) {
|
|
2570
|
+
this.registry.register(name, createCustomAdapter(name, { baseURL, apiKey: key }));
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
const ollamaConfig = config.ollama;
|
|
2574
|
+
const ollamaURL = ollamaConfig?.baseURL || process.env.OLLAMA_BASE_URL || "http://localhost:11434/v1";
|
|
2575
|
+
if (ollamaConfig || process.env.OLLAMA_BASE_URL) {
|
|
2576
|
+
this.registry.register("ollama", createCustomAdapter("ollama", { baseURL: ollamaURL }));
|
|
2577
|
+
}
|
|
2578
|
+
if (config.custom) {
|
|
2579
|
+
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
2580
|
+
this.registry.register(name, createCustomAdapter(name, customConfig));
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
registerBatchAdapters() {
|
|
2585
|
+
const config = this.config;
|
|
2586
|
+
const openaiKey = config.openai?.apiKey || process.env.OPENAI_API_KEY;
|
|
2587
|
+
if (openaiKey) {
|
|
2588
|
+
this.batchManager.registerBatchAdapter("openai", createOpenAIBatchAdapter(openaiKey));
|
|
2589
|
+
}
|
|
2590
|
+
const anthropicKey = config.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
2591
|
+
if (anthropicKey) {
|
|
2592
|
+
this.batchManager.registerBatchAdapter("anthropic", createAnthropicBatchAdapter(anthropicKey));
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
applyDefaults(request) {
|
|
2596
|
+
const defaults = this.config.defaults;
|
|
2597
|
+
if (!defaults) return request;
|
|
2598
|
+
return {
|
|
2599
|
+
...request,
|
|
2600
|
+
temperature: request.temperature ?? defaults.temperature,
|
|
2601
|
+
max_tokens: request.max_tokens ?? defaults.max_tokens
|
|
2602
|
+
};
|
|
2603
|
+
}
|
|
2604
|
+
async fetchModels() {
|
|
2605
|
+
const all = [];
|
|
2606
|
+
for (const adapter of this.registry.all()) {
|
|
2607
|
+
try {
|
|
2608
|
+
const models = await adapter.listModels();
|
|
2609
|
+
all.push(...models);
|
|
2610
|
+
} catch {
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
return all.sort((a, b) => a.id.localeCompare(b.id));
|
|
2614
|
+
}
|
|
2615
|
+
getRegistry() {
|
|
2616
|
+
return this.registry;
|
|
2617
|
+
}
|
|
2618
|
+
};
|
|
2619
|
+
|
|
2620
|
+
// src/server.ts
|
|
2621
|
+
import { createServer } from "http";
|
|
2622
|
+
function parseBody(req) {
|
|
2623
|
+
return new Promise((resolve2, reject) => {
|
|
2624
|
+
const chunks = [];
|
|
2625
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
2626
|
+
req.on("end", () => resolve2(Buffer.concat(chunks).toString()));
|
|
2627
|
+
req.on("error", reject);
|
|
2628
|
+
});
|
|
2629
|
+
}
|
|
2630
|
+
function sendJSON(res, status, body) {
|
|
2631
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
2632
|
+
res.end(JSON.stringify(body));
|
|
2633
|
+
}
|
|
2634
|
+
function sendError(res, status, message) {
|
|
2635
|
+
sendJSON(res, status, { error: { code: status, message, metadata: {} } });
|
|
2636
|
+
}
|
|
2637
|
+
async function sendSSE(res, stream) {
|
|
2638
|
+
res.writeHead(200, {
|
|
2639
|
+
"Content-Type": "text/event-stream",
|
|
2640
|
+
"Cache-Control": "no-cache",
|
|
2641
|
+
"Connection": "keep-alive"
|
|
2642
|
+
});
|
|
2643
|
+
for await (const chunk of stream) {
|
|
2644
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
2645
|
+
|
|
2646
|
+
`);
|
|
2647
|
+
}
|
|
2648
|
+
res.write("data: [DONE]\n\n");
|
|
2649
|
+
res.end();
|
|
2650
|
+
}
|
|
2651
|
+
function createAnyModelServer(options = {}) {
|
|
2652
|
+
const client = new AnyModel(options.config);
|
|
2653
|
+
const basePath = "/api/v1";
|
|
2654
|
+
const server = createServer(async (req, res) => {
|
|
2655
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
2656
|
+
const path2 = url.pathname;
|
|
2657
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
2658
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
2659
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
2660
|
+
if (req.method === "OPTIONS") {
|
|
2661
|
+
res.writeHead(204);
|
|
2662
|
+
res.end();
|
|
2663
|
+
return;
|
|
2664
|
+
}
|
|
2665
|
+
try {
|
|
2666
|
+
if (path2 === "/health" && req.method === "GET") {
|
|
2667
|
+
sendJSON(res, 200, { status: "ok" });
|
|
2668
|
+
return;
|
|
2669
|
+
}
|
|
2670
|
+
if (path2 === `${basePath}/chat/completions` && req.method === "POST") {
|
|
2671
|
+
const body = JSON.parse(await parseBody(req));
|
|
2672
|
+
if (body.stream) {
|
|
2673
|
+
const stream = await client.chat.completions.create(body);
|
|
2674
|
+
await sendSSE(res, stream);
|
|
2675
|
+
} else {
|
|
2676
|
+
const response = await client.chat.completions.create(body);
|
|
2677
|
+
sendJSON(res, 200, response);
|
|
2678
|
+
}
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
if (path2 === `${basePath}/models` && req.method === "GET") {
|
|
2682
|
+
const provider = url.searchParams.get("provider") || void 0;
|
|
2683
|
+
const models = await client.models.list({ provider });
|
|
2684
|
+
sendJSON(res, 200, { object: "list", data: models });
|
|
2685
|
+
return;
|
|
2686
|
+
}
|
|
2687
|
+
if (path2.startsWith(`${basePath}/generation/`) && req.method === "GET") {
|
|
2688
|
+
const id = path2.substring(`${basePath}/generation/`.length);
|
|
2689
|
+
const stats = client.generation.get(id);
|
|
2690
|
+
if (!stats) {
|
|
2691
|
+
sendError(res, 404, `Generation ${id} not found`);
|
|
2692
|
+
return;
|
|
2693
|
+
}
|
|
2694
|
+
sendJSON(res, 200, stats);
|
|
2695
|
+
return;
|
|
2696
|
+
}
|
|
2697
|
+
if (path2 === `${basePath}/batches` && req.method === "POST") {
|
|
2698
|
+
const body = JSON.parse(await parseBody(req));
|
|
2699
|
+
const batch = await client.batches.create(body);
|
|
2700
|
+
sendJSON(res, 201, batch);
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
if (path2 === `${basePath}/batches` && req.method === "GET") {
|
|
2704
|
+
const batches = await client.batches.list();
|
|
2705
|
+
sendJSON(res, 200, { object: "list", data: batches });
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
if (path2.startsWith(`${basePath}/batches/`) && req.method === "GET") {
|
|
2709
|
+
const parts = path2.substring(`${basePath}/batches/`.length).split("/");
|
|
2710
|
+
const id = parts[0];
|
|
2711
|
+
if (parts[1] === "results") {
|
|
2712
|
+
const results = await client.batches.results(id);
|
|
2713
|
+
sendJSON(res, 200, results);
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
const batch = await client.batches.get(id);
|
|
2717
|
+
if (!batch) {
|
|
2718
|
+
sendError(res, 404, `Batch ${id} not found`);
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
sendJSON(res, 200, batch);
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
if (path2.startsWith(`${basePath}/batches/`) && req.method === "POST") {
|
|
2725
|
+
const parts = path2.substring(`${basePath}/batches/`.length).split("/");
|
|
2726
|
+
const id = parts[0];
|
|
2727
|
+
if (parts[1] === "cancel") {
|
|
2728
|
+
const batch = await client.batches.cancel(id);
|
|
2729
|
+
sendJSON(res, 200, batch);
|
|
2730
|
+
return;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
sendError(res, 404, `Not found: ${path2}`);
|
|
2734
|
+
} catch (err) {
|
|
2735
|
+
const code = err?.code || 500;
|
|
2736
|
+
const message = err?.message || "Internal server error";
|
|
2737
|
+
sendError(res, code, message);
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
return server;
|
|
2741
|
+
}
|
|
2742
|
+
function startServer(options = {}) {
|
|
2743
|
+
const port = options.port ?? 4141;
|
|
2744
|
+
const host = options.host ?? "0.0.0.0";
|
|
2745
|
+
const server = createAnyModelServer(options);
|
|
2746
|
+
server.listen(port, host, () => {
|
|
2747
|
+
console.log(`@probeo/anymodel server running at http://${host}:${port}`);
|
|
2748
|
+
console.log(`API base: http://${host}:${port}/api/v1`);
|
|
2749
|
+
console.log("");
|
|
2750
|
+
console.log("Endpoints:");
|
|
2751
|
+
console.log(" POST /api/v1/chat/completions");
|
|
2752
|
+
console.log(" GET /api/v1/models");
|
|
2753
|
+
console.log(" GET /api/v1/generation/:id");
|
|
2754
|
+
console.log(" POST /api/v1/batches");
|
|
2755
|
+
console.log(" GET /api/v1/batches");
|
|
2756
|
+
console.log(" GET /api/v1/batches/:id");
|
|
2757
|
+
console.log(" GET /api/v1/batches/:id/results");
|
|
2758
|
+
console.log(" POST /api/v1/batches/:id/cancel");
|
|
2759
|
+
console.log(" GET /health");
|
|
2760
|
+
});
|
|
2761
|
+
}
|
|
2762
|
+
export {
|
|
2763
|
+
AnyModel,
|
|
2764
|
+
AnyModelError,
|
|
2765
|
+
BatchManager,
|
|
2766
|
+
BatchStore,
|
|
2767
|
+
GenerationStatsStore,
|
|
2768
|
+
appendFileQueued,
|
|
2769
|
+
configureFsIO,
|
|
2770
|
+
createAnthropicBatchAdapter,
|
|
2771
|
+
createAnyModelServer,
|
|
2772
|
+
createOpenAIBatchAdapter,
|
|
2773
|
+
ensureDir,
|
|
2774
|
+
getFsQueueStatus,
|
|
2775
|
+
joinPath,
|
|
2776
|
+
readFileQueued,
|
|
2777
|
+
resolveConfig,
|
|
2778
|
+
startServer,
|
|
2779
|
+
waitForFsQueuesIdle,
|
|
2780
|
+
writeFileFlushedQueued,
|
|
2781
|
+
writeFileQueued
|
|
2782
|
+
};
|
|
16
2783
|
//# sourceMappingURL=index.js.map
|