docs-combiner 0.2.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentApi/agentControlClaim.js +26 -0
- package/dist/agentApi/approachMatrixView.js +19 -0
- package/dist/agentApi/capabilitiesMeta.js +78 -0
- package/dist/agentApi/catalogUrlPresets.js +26 -0
- package/dist/agentApi/constants.js +11 -0
- package/dist/agentApi/creativeSelections.js +25 -0
- package/dist/agentApi/escalation.js +221 -0
- package/dist/agentApi/generatedPairsGuard.js +85 -0
- package/dist/agentApi/httpHelpers.js +77 -0
- package/dist/agentApi/imagesReadiness.js +60 -0
- package/dist/agentApi/jobStatus.js +113 -0
- package/dist/agentApi/productImageDrive.js +39 -0
- package/dist/agentApi/regenerateImages.js +125 -0
- package/dist/agentApi/rendererTypes.js +3 -0
- package/dist/agentApi/routes.js +440 -0
- package/dist/agentApi/sessionSnapshot.js +82 -0
- package/dist/agentApi/sessionTypes.js +2 -0
- package/dist/agentApi/spec.js +1045 -0
- package/dist/agentApi/types.js +2 -0
- package/dist/agentApi/validation.js +32 -0
- package/dist/campaignsCsv.js +420 -0
- package/dist/contentPairs.js +62 -0
- package/dist/flexcardBalance.js +88 -0
- package/dist/integrations/driveApi.js +420 -0
- package/dist/integrations/httpTimeout.js +116 -0
- package/dist/integrations/openrouter.js +532 -0
- package/dist/main.js +830 -165
- package/dist/models.js +17 -0
- package/dist/offerSettings.js +19 -0
- package/dist/preload.js +28 -0
- package/dist/promptOverrides.js +437 -0
- package/dist/prompts.js +1243 -0
- package/dist/renderer.js +19 -19
- package/dist/renderer.js.LICENSE.txt +4 -0
- package/dist/renderer.js.map +1 -1
- package/dist/server/app.js +378 -0
- package/dist/server/auth.js +74 -0
- package/dist/server/contentJobs.js +367 -0
- package/dist/server/driveCatalog.js +122 -0
- package/dist/server/driveProxy.js +220 -0
- package/dist/server/flexcardMeta.js +29 -0
- package/dist/server/googleAuth.js +84 -0
- package/dist/server/imageAssets.js +87 -0
- package/dist/server/imageJobs.js +776 -0
- package/dist/server/index.js +38 -0
- package/dist/server/jobEvents.js +116 -0
- package/dist/server/jobs.js +358 -0
- package/dist/server/openRouterMeta.js +58 -0
- package/dist/server/spec.js +544 -0
- package/dist/server/storage.js +133 -0
- package/dist/server/telegram.js +53 -0
- package/dist/server/workspaceSnapshot.js +273 -0
- package/package.json +18 -4
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* OpenRouter HTTP-клиент: chat completions (включая стриминг), модели, баланс.
|
|
4
|
+
* Без зависимостей от React и состояния приложения — все входы через параметры.
|
|
5
|
+
* (Фаза 0 миграции на VPS: см. VPS_MIGRATION_DRAFT.md.)
|
|
6
|
+
*/
|
|
7
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
8
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
9
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
10
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
11
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
12
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
13
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.OPENROUTER_BASE_URL = void 0;
|
|
18
|
+
exports.buildOpenRouterHeaders = buildOpenRouterHeaders;
|
|
19
|
+
exports.extractChatCompletionText = extractChatCompletionText;
|
|
20
|
+
exports.buildValidatorStoredResultText = buildValidatorStoredResultText;
|
|
21
|
+
exports.isCompletionTruncatedByTokenLimit = isCompletionTruncatedByTokenLimit;
|
|
22
|
+
exports.postOpenRouterChatCompletion = postOpenRouterChatCompletion;
|
|
23
|
+
exports.requestOpenRouterChatCompletion = requestOpenRouterChatCompletion;
|
|
24
|
+
exports.fetchOpenRouterModels = fetchOpenRouterModels;
|
|
25
|
+
exports.filterImageGenerationModels = filterImageGenerationModels;
|
|
26
|
+
exports.filterChatCompletionModels = filterChatCompletionModels;
|
|
27
|
+
exports.filterVisionValidationModels = filterVisionValidationModels;
|
|
28
|
+
exports.fetchOpenRouterBalances = fetchOpenRouterBalances;
|
|
29
|
+
const httpTimeout_1 = require("./httpTimeout");
|
|
30
|
+
exports.OPENROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
|
|
31
|
+
/** Жёсткий страховочный таймаут для chat/completions даже когда вызывающий передал свой signal. */
|
|
32
|
+
const OPENROUTER_HARD_TIMEOUT_MS = 15 * 60 * 1000;
|
|
33
|
+
function openRouterReferer() {
|
|
34
|
+
var _a;
|
|
35
|
+
if (typeof window !== 'undefined' && ((_a = window.location) === null || _a === void 0 ? void 0 : _a.origin)) {
|
|
36
|
+
return window.location.origin;
|
|
37
|
+
}
|
|
38
|
+
return 'https://docs-combiner.app';
|
|
39
|
+
}
|
|
40
|
+
function buildOpenRouterHeaders(apiKey, extra) {
|
|
41
|
+
return Object.assign({ 'Authorization': `Bearer ${apiKey}`, 'HTTP-Referer': openRouterReferer(), 'X-Title': 'Docs Combiner' }, extra);
|
|
42
|
+
}
|
|
43
|
+
/** Текст ответа из OpenRouter/OpenAI chat completion (string или массив частей). */
|
|
44
|
+
function extractChatCompletionText(choice) {
|
|
45
|
+
const msg = choice === null || choice === void 0 ? void 0 : choice.message;
|
|
46
|
+
if (!msg)
|
|
47
|
+
return '';
|
|
48
|
+
const c = msg.content;
|
|
49
|
+
let fromContent = '';
|
|
50
|
+
if (typeof c === 'string') {
|
|
51
|
+
fromContent = c;
|
|
52
|
+
}
|
|
53
|
+
else if (Array.isArray(c)) {
|
|
54
|
+
fromContent = c
|
|
55
|
+
.map((part) => { var _a, _b; return (typeof part === 'string' ? part : (_b = (_a = part === null || part === void 0 ? void 0 : part.text) !== null && _a !== void 0 ? _a : part === null || part === void 0 ? void 0 : part.content) !== null && _b !== void 0 ? _b : ''); })
|
|
56
|
+
.filter(Boolean)
|
|
57
|
+
.join('');
|
|
58
|
+
}
|
|
59
|
+
else if (c != null && typeof c === 'object' && typeof c.text === 'string') {
|
|
60
|
+
fromContent = c.text;
|
|
61
|
+
}
|
|
62
|
+
if (fromContent.trim())
|
|
63
|
+
return fromContent;
|
|
64
|
+
// Часть моделей (reasoning) кладёт текст в reasoning/refusal — иначе «Полный ответ» пустой.
|
|
65
|
+
const reasoning = msg.reasoning;
|
|
66
|
+
if (typeof reasoning === 'string' && reasoning.trim())
|
|
67
|
+
return reasoning;
|
|
68
|
+
if (Array.isArray(reasoning)) {
|
|
69
|
+
const r = reasoning
|
|
70
|
+
.map((p) => { var _a; return (typeof p === 'string' ? p : (_a = p === null || p === void 0 ? void 0 : p.text) !== null && _a !== void 0 ? _a : ''); })
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.join('\n');
|
|
73
|
+
if (r.trim())
|
|
74
|
+
return r;
|
|
75
|
+
}
|
|
76
|
+
const refusal = msg.refusal;
|
|
77
|
+
if (typeof refusal === 'string' && refusal.trim())
|
|
78
|
+
return refusal;
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
/** Сырой JSON, если в message не извлекли ни одной буквы — иначе «Полный ответ» = пусто и UI показывает лишь подсказку из checkErrors. */
|
|
82
|
+
const VALIDATOR_RESULT_RAW_JSON_MAX = 48000;
|
|
83
|
+
function buildValidatorStoredResultText(content, data) {
|
|
84
|
+
if (content.trim().length > 0) {
|
|
85
|
+
return content;
|
|
86
|
+
}
|
|
87
|
+
const raw = JSON.stringify(data !== null && data !== void 0 ? data : {}, null, 2);
|
|
88
|
+
const pre = 'Текст ответа модели не извлечён из message.content (проверьте reasoning/формат в JSON ниже). Ответ API:\n\n';
|
|
89
|
+
if (raw.length <= VALIDATOR_RESULT_RAW_JSON_MAX) {
|
|
90
|
+
return pre + raw;
|
|
91
|
+
}
|
|
92
|
+
return `${pre}${raw.slice(0, VALIDATOR_RESULT_RAW_JSON_MAX)}…\n[обрезано по ${VALIDATOR_RESULT_RAW_JSON_MAX} симв.]`;
|
|
93
|
+
}
|
|
94
|
+
/** Обрыв ответа по лимиту токенов (OpenRouter / OpenAI-совместимый completion). */
|
|
95
|
+
function isCompletionTruncatedByTokenLimit(choice) {
|
|
96
|
+
if (!choice)
|
|
97
|
+
return false;
|
|
98
|
+
const fr = choice.finish_reason;
|
|
99
|
+
const nfr = choice.native_finish_reason;
|
|
100
|
+
if (fr === 'length')
|
|
101
|
+
return true;
|
|
102
|
+
if (nfr === 'max_output_tokens')
|
|
103
|
+
return true;
|
|
104
|
+
const ns = typeof nfr === 'string' ? nfr.toLowerCase() : '';
|
|
105
|
+
return ns === 'length' || ns.includes('max_output');
|
|
106
|
+
}
|
|
107
|
+
function buildStreamedCompletionData(content, finishReason, id) {
|
|
108
|
+
return {
|
|
109
|
+
id: id !== null && id !== void 0 ? id : undefined,
|
|
110
|
+
choices: [
|
|
111
|
+
{
|
|
112
|
+
finish_reason: finishReason || 'stop',
|
|
113
|
+
message: {
|
|
114
|
+
role: 'assistant',
|
|
115
|
+
content
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
]
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function appendOpenRouterDeltaContent(target, delta) {
|
|
122
|
+
const content = delta === null || delta === void 0 ? void 0 : delta.content;
|
|
123
|
+
if (typeof content === 'string') {
|
|
124
|
+
target.push(content);
|
|
125
|
+
}
|
|
126
|
+
else if (Array.isArray(content)) {
|
|
127
|
+
content.forEach((part) => {
|
|
128
|
+
if (typeof part === 'string')
|
|
129
|
+
target.push(part);
|
|
130
|
+
else if (typeof (part === null || part === void 0 ? void 0 : part.text) === 'string')
|
|
131
|
+
target.push(part.text);
|
|
132
|
+
else if (typeof (part === null || part === void 0 ? void 0 : part.content) === 'string')
|
|
133
|
+
target.push(part.content);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const reasoning = delta === null || delta === void 0 ? void 0 : delta.reasoning;
|
|
137
|
+
if (!content && typeof reasoning === 'string') {
|
|
138
|
+
target.push(reasoning);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function readOpenRouterStream(response_1) {
|
|
142
|
+
return __awaiter(this, arguments, void 0, function* (response, opts = {}) {
|
|
143
|
+
var _a, _b, _c;
|
|
144
|
+
const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader();
|
|
145
|
+
if (!reader)
|
|
146
|
+
throw new Error('OpenRouter stream response body is not readable');
|
|
147
|
+
const decoder = new TextDecoder();
|
|
148
|
+
const chunks = [];
|
|
149
|
+
let buffer = '';
|
|
150
|
+
let rawEventCount = 0;
|
|
151
|
+
let finishReason;
|
|
152
|
+
let completionId = null;
|
|
153
|
+
const readStart = Date.now();
|
|
154
|
+
const readTimeoutMs = (_b = opts.readTimeoutMs) !== null && _b !== void 0 ? _b : 180000;
|
|
155
|
+
const timeoutId = setTimeout(() => {
|
|
156
|
+
var _a, _b;
|
|
157
|
+
(_a = opts.logMsg) === null || _a === void 0 ? void 0 : _a.call(opts, 'error', `❌ Stream read timeout after ${Math.round((Date.now() - readStart) / 1000)}s`);
|
|
158
|
+
(_b = opts.controller) === null || _b === void 0 ? void 0 : _b.abort();
|
|
159
|
+
}, readTimeoutMs);
|
|
160
|
+
const processLine = (line) => {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
const trimmed = line.trim();
|
|
163
|
+
if (!trimmed || trimmed.startsWith(':'))
|
|
164
|
+
return false;
|
|
165
|
+
if (!trimmed.startsWith('data: '))
|
|
166
|
+
return false;
|
|
167
|
+
const payload = trimmed.slice(6).trim();
|
|
168
|
+
if (payload === '[DONE]')
|
|
169
|
+
return true;
|
|
170
|
+
rawEventCount += 1;
|
|
171
|
+
let parsed;
|
|
172
|
+
try {
|
|
173
|
+
parsed = JSON.parse(payload);
|
|
174
|
+
}
|
|
175
|
+
catch (_c) {
|
|
176
|
+
(_a = opts.logMsg) === null || _a === void 0 ? void 0 : _a.call(opts, 'warn', `⚠️ Skipping non-JSON stream event: ${payload.slice(0, 160)}`);
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
if ((parsed === null || parsed === void 0 ? void 0 : parsed.id) && !completionId)
|
|
180
|
+
completionId = parsed.id;
|
|
181
|
+
if (parsed === null || parsed === void 0 ? void 0 : parsed.error) {
|
|
182
|
+
const msg = parsed.error.message || parsed.error.code || 'OpenRouter stream error';
|
|
183
|
+
throw new Error(msg);
|
|
184
|
+
}
|
|
185
|
+
const choice = (_b = parsed === null || parsed === void 0 ? void 0 : parsed.choices) === null || _b === void 0 ? void 0 : _b[0];
|
|
186
|
+
appendOpenRouterDeltaContent(chunks, choice === null || choice === void 0 ? void 0 : choice.delta);
|
|
187
|
+
if (choice === null || choice === void 0 ? void 0 : choice.finish_reason)
|
|
188
|
+
finishReason = choice.finish_reason;
|
|
189
|
+
return false;
|
|
190
|
+
};
|
|
191
|
+
try {
|
|
192
|
+
while (true) {
|
|
193
|
+
const { done, value } = yield reader.read();
|
|
194
|
+
if (done)
|
|
195
|
+
break;
|
|
196
|
+
buffer += decoder.decode(value, { stream: true });
|
|
197
|
+
while (true) {
|
|
198
|
+
const lineEnd = buffer.indexOf('\n');
|
|
199
|
+
if (lineEnd === -1)
|
|
200
|
+
break;
|
|
201
|
+
const line = buffer.slice(0, lineEnd);
|
|
202
|
+
buffer = buffer.slice(lineEnd + 1);
|
|
203
|
+
if (processLine(line)) {
|
|
204
|
+
const content = chunks.join('');
|
|
205
|
+
const data = buildStreamedCompletionData(content, finishReason, completionId);
|
|
206
|
+
return {
|
|
207
|
+
data,
|
|
208
|
+
responseText: JSON.stringify(data),
|
|
209
|
+
generationId: response.headers.get('X-Generation-Id'),
|
|
210
|
+
streamed: true
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (buffer.trim())
|
|
216
|
+
processLine(buffer);
|
|
217
|
+
const content = chunks.join('');
|
|
218
|
+
const data = buildStreamedCompletionData(content, finishReason, completionId);
|
|
219
|
+
(_c = opts.logMsg) === null || _c === void 0 ? void 0 : _c.call(opts, 'log', `✅ Stream completed: ${rawEventCount} events, ${content.length} chars`);
|
|
220
|
+
return {
|
|
221
|
+
data,
|
|
222
|
+
responseText: JSON.stringify(data),
|
|
223
|
+
generationId: response.headers.get('X-Generation-Id'),
|
|
224
|
+
streamed: true
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
finally {
|
|
228
|
+
clearTimeout(timeoutId);
|
|
229
|
+
try {
|
|
230
|
+
reader.releaseLock();
|
|
231
|
+
}
|
|
232
|
+
catch (_d) {
|
|
233
|
+
/* ignore */
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/** Низкоуровневый POST chat/completions: отдаёт сырой Response (для кейсов с ручным чтением тела, как генерация картинок). */
|
|
239
|
+
function postOpenRouterChatCompletion(requestBody, opts) {
|
|
240
|
+
var _a;
|
|
241
|
+
// Если вызывающий уже управляет отменой (свой signal с таймаутом) — не режем его дедлайном,
|
|
242
|
+
// но держим жёсткий страховочный лимит, чтобы «мёртвое» соединение не висело вечно.
|
|
243
|
+
const timeoutMs = (_a = opts.timeoutMs) !== null && _a !== void 0 ? _a : (opts.signal ? OPENROUTER_HARD_TIMEOUT_MS : httpTimeout_1.DEFAULT_NETWORK_TIMEOUT_MS);
|
|
244
|
+
return (0, httpTimeout_1.fetchWithTimeout)(`${exports.OPENROUTER_BASE_URL}/chat/completions`, {
|
|
245
|
+
method: 'POST',
|
|
246
|
+
headers: buildOpenRouterHeaders(opts.apiKey, { 'Content-Type': 'application/json' }),
|
|
247
|
+
body: JSON.stringify(requestBody),
|
|
248
|
+
}, { signal: opts.signal, timeoutMs });
|
|
249
|
+
}
|
|
250
|
+
function requestOpenRouterChatCompletion(requestBody, opts) {
|
|
251
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
252
|
+
var _a, _b, _c, _d, _e;
|
|
253
|
+
// Гарантируем возможность прервать «зависший» стрим: если вызывающий не передал контроллер,
|
|
254
|
+
// создаём свой, чтобы таймаут чтения (readTimeoutMs) реально обрывал соединение при пропаже сети.
|
|
255
|
+
const callerControlled = Boolean(opts.controller || opts.signal);
|
|
256
|
+
const effectiveController = (_a = opts.controller) !== null && _a !== void 0 ? _a : new AbortController();
|
|
257
|
+
if (opts.signal && !opts.controller) {
|
|
258
|
+
if (opts.signal.aborted)
|
|
259
|
+
effectiveController.abort();
|
|
260
|
+
else
|
|
261
|
+
opts.signal.addEventListener('abort', () => effectiveController.abort(), { once: true });
|
|
262
|
+
}
|
|
263
|
+
const connectTimeoutMs = callerControlled ? undefined : httpTimeout_1.DEFAULT_NETWORK_TIMEOUT_MS;
|
|
264
|
+
const makeRequest = (stream) => postOpenRouterChatCompletion(stream ? Object.assign(Object.assign({}, requestBody), { stream: true }) : requestBody, {
|
|
265
|
+
apiKey: opts.apiKey,
|
|
266
|
+
signal: effectiveController.signal,
|
|
267
|
+
timeoutMs: connectTimeoutMs,
|
|
268
|
+
});
|
|
269
|
+
const response = yield makeRequest(Boolean(opts.preferStream));
|
|
270
|
+
const generationId = response.headers.get('X-Generation-Id');
|
|
271
|
+
if (generationId)
|
|
272
|
+
(_b = opts.logMsg) === null || _b === void 0 ? void 0 : _b.call(opts, 'log', `🧾 OpenRouter generation id: ${generationId}`);
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
const responseText = yield response.text();
|
|
275
|
+
let message = `HTTP ${response.status}: ${response.statusText}`;
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(responseText);
|
|
278
|
+
message = ((_c = parsed.error) === null || _c === void 0 ? void 0 : _c.message) || parsed.message || message;
|
|
279
|
+
}
|
|
280
|
+
catch (_f) {
|
|
281
|
+
if (responseText.trim())
|
|
282
|
+
message = `${message}. Response: ${responseText.slice(0, 200)}`;
|
|
283
|
+
}
|
|
284
|
+
if (opts.preferStream && /stream|streaming/i.test(message)) {
|
|
285
|
+
(_d = opts.logMsg) === null || _d === void 0 ? void 0 : _d.call(opts, 'warn', `⚠️ Streaming is not available for this request, retrying without stream: ${message}`);
|
|
286
|
+
const fallback = yield makeRequest(false);
|
|
287
|
+
const fallbackText = yield fallback.text();
|
|
288
|
+
if (!fallback.ok) {
|
|
289
|
+
let fallbackMessage = `HTTP ${fallback.status}: ${fallback.statusText}`;
|
|
290
|
+
try {
|
|
291
|
+
const parsed = JSON.parse(fallbackText);
|
|
292
|
+
fallbackMessage = ((_e = parsed.error) === null || _e === void 0 ? void 0 : _e.message) || parsed.message || fallbackMessage;
|
|
293
|
+
}
|
|
294
|
+
catch (_g) {
|
|
295
|
+
if (fallbackText.trim())
|
|
296
|
+
fallbackMessage = `${fallbackMessage}. Response: ${fallbackText.slice(0, 200)}`;
|
|
297
|
+
}
|
|
298
|
+
throw new Error(fallbackMessage);
|
|
299
|
+
}
|
|
300
|
+
return {
|
|
301
|
+
data: JSON.parse(fallbackText),
|
|
302
|
+
responseText: fallbackText,
|
|
303
|
+
generationId: fallback.headers.get('X-Generation-Id'),
|
|
304
|
+
streamed: false
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
throw new Error(message);
|
|
308
|
+
}
|
|
309
|
+
if (opts.preferStream) {
|
|
310
|
+
return readOpenRouterStream(response, {
|
|
311
|
+
logMsg: opts.logMsg,
|
|
312
|
+
readTimeoutMs: opts.readTimeoutMs,
|
|
313
|
+
controller: effectiveController
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const responseText = yield response.text();
|
|
317
|
+
return {
|
|
318
|
+
data: JSON.parse(responseText),
|
|
319
|
+
responseText,
|
|
320
|
+
generationId,
|
|
321
|
+
streamed: false
|
|
322
|
+
};
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// Модели
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
/** Сырой список моделей `GET /models`; при HTTP-ошибке возвращает null (как прежнее поведение «warn + пустой список»). */
|
|
329
|
+
function fetchOpenRouterModels(apiKey, logMsg) {
|
|
330
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
331
|
+
var _a;
|
|
332
|
+
const response = yield (0, httpTimeout_1.fetchWithTimeout)(`${exports.OPENROUTER_BASE_URL}/models`, {
|
|
333
|
+
method: 'GET',
|
|
334
|
+
headers: buildOpenRouterHeaders(apiKey, { 'Accept': 'application/json' })
|
|
335
|
+
});
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', `⚠️ Failed to fetch models. Status: ${response.status}`);
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const data = yield response.json();
|
|
341
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', `✅ Fetched ${((_a = data.data) === null || _a === void 0 ? void 0 : _a.length) || 0} models from OpenRouter`);
|
|
342
|
+
return data.data || [];
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Модели с генерацией картинок.
|
|
347
|
+
* OpenRouter отдаёт capability двумя способами:
|
|
348
|
+
* 1. model.architecture.modality — строка вида "text->image" или "text->text+image"
|
|
349
|
+
* 2. model.modalities (legacy) — может содержать 'image'
|
|
350
|
+
* Плюс fallback по известным паттернам ID.
|
|
351
|
+
*/
|
|
352
|
+
function filterImageGenerationModels(models) {
|
|
353
|
+
return models
|
|
354
|
+
.filter((model) => {
|
|
355
|
+
var _a;
|
|
356
|
+
const archModality = ((_a = model.architecture) === null || _a === void 0 ? void 0 : _a.modality) || '';
|
|
357
|
+
if (archModality.includes('->image') || archModality.match(/->.*image/))
|
|
358
|
+
return true;
|
|
359
|
+
const modalities = model.modalities || [];
|
|
360
|
+
if (modalities.includes('image') || modalities.includes('image_url'))
|
|
361
|
+
return true;
|
|
362
|
+
const id = model.id || '';
|
|
363
|
+
if (id.includes('image') ||
|
|
364
|
+
id.includes('dall-e') ||
|
|
365
|
+
id.includes('flux') ||
|
|
366
|
+
id.includes('riverflow') ||
|
|
367
|
+
id.includes('imagen') ||
|
|
368
|
+
id.includes('stable-diffusion') ||
|
|
369
|
+
id.includes('sdxl'))
|
|
370
|
+
return true;
|
|
371
|
+
return false;
|
|
372
|
+
})
|
|
373
|
+
.map((model) => ({
|
|
374
|
+
id: model.id,
|
|
375
|
+
name: model.name || model.id
|
|
376
|
+
}))
|
|
377
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
378
|
+
}
|
|
379
|
+
/** Chat completions (text): исключаем чистые text→image эндпоинты. */
|
|
380
|
+
function filterChatCompletionModels(models) {
|
|
381
|
+
return models
|
|
382
|
+
.filter((model) => {
|
|
383
|
+
var _a;
|
|
384
|
+
const modality = String(((_a = model.architecture) === null || _a === void 0 ? void 0 : _a.modality) || '');
|
|
385
|
+
if (modality === 'text->image')
|
|
386
|
+
return false;
|
|
387
|
+
return true;
|
|
388
|
+
})
|
|
389
|
+
.map((model) => ({
|
|
390
|
+
id: model.id,
|
|
391
|
+
name: model.name || model.id
|
|
392
|
+
}))
|
|
393
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
394
|
+
}
|
|
395
|
+
/** Модели с анализом изображений (vision) для валидации крео. */
|
|
396
|
+
function filterVisionValidationModels(models) {
|
|
397
|
+
return models
|
|
398
|
+
.filter((model) => {
|
|
399
|
+
const modalities = model.modalities || [];
|
|
400
|
+
const supportsVision = modalities.includes('image') || modalities.includes('image_url') || modalities.includes('vision');
|
|
401
|
+
const supportsChatWithImages = model.id && (model.id.includes('gpt') ||
|
|
402
|
+
model.id.includes('claude') ||
|
|
403
|
+
model.id.includes('gemini'));
|
|
404
|
+
return supportsVision || supportsChatWithImages;
|
|
405
|
+
})
|
|
406
|
+
.map((model) => ({
|
|
407
|
+
id: model.id,
|
|
408
|
+
name: model.name || model.id
|
|
409
|
+
}))
|
|
410
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
411
|
+
}
|
|
412
|
+
/** `GET /credits` + `GET /key` с устойчивым парсингом (включая ответы с неверным content-type). */
|
|
413
|
+
function fetchOpenRouterBalances(apiKey, logMsg) {
|
|
414
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
415
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0;
|
|
416
|
+
const headers = buildOpenRouterHeaders(apiKey, { 'Accept': 'application/json' });
|
|
417
|
+
const result = { accountBalance: null, keyLimit: undefined };
|
|
418
|
+
const [creditsResponse, keyResponse] = yield Promise.all([
|
|
419
|
+
(0, httpTimeout_1.fetchWithTimeout)(`${exports.OPENROUTER_BASE_URL}/credits`, { method: 'GET', headers }),
|
|
420
|
+
(0, httpTimeout_1.fetchWithTimeout)(`${exports.OPENROUTER_BASE_URL}/key`, { method: 'GET', headers })
|
|
421
|
+
]);
|
|
422
|
+
// Остаток кредитов аккаунта
|
|
423
|
+
if (creditsResponse.ok) {
|
|
424
|
+
const contentType = creditsResponse.headers.get('content-type') || '';
|
|
425
|
+
if (!contentType.includes('application/json')) {
|
|
426
|
+
const text = yield creditsResponse.text();
|
|
427
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', `⚠️ Credits endpoint returned non-JSON. Content-Type: ${contentType}, Preview: ${text.substring(0, 200)}`);
|
|
428
|
+
if (contentType.includes('text/html') || text.trim().startsWith('<!DOCTYPE') || text.trim().startsWith('<html')) {
|
|
429
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Credits endpoint returned HTML instead of JSON. This may indicate the endpoint is unavailable or requires different authentication.');
|
|
430
|
+
}
|
|
431
|
+
else {
|
|
432
|
+
// Некоторые API не выставляют content-type корректно — пробуем распарсить
|
|
433
|
+
try {
|
|
434
|
+
const creditsData = JSON.parse(text);
|
|
435
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', '💰 Parsed credits data despite wrong content-type:', JSON.stringify(creditsData, null, 2));
|
|
436
|
+
const totalCredits = (_c = (_b = (_a = creditsData.data) === null || _a === void 0 ? void 0 : _a.total_credits) !== null && _b !== void 0 ? _b : creditsData.total_credits) !== null && _c !== void 0 ? _c : null;
|
|
437
|
+
const totalUsage = (_f = (_e = (_d = creditsData.data) === null || _d === void 0 ? void 0 : _d.total_usage) !== null && _e !== void 0 ? _e : creditsData.total_usage) !== null && _f !== void 0 ? _f : null;
|
|
438
|
+
if (totalCredits !== null && totalUsage !== null) {
|
|
439
|
+
const remainingCredits = totalCredits - totalUsage;
|
|
440
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', `✅ Credits found: total=${totalCredits}, used=${totalUsage}, remaining=${remainingCredits.toFixed(2)}`);
|
|
441
|
+
result.accountBalance = remainingCredits;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (_1) {
|
|
445
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Could not parse credits response as JSON');
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
const creditsData = yield creditsResponse.json();
|
|
451
|
+
const totalCredits = (_h = (_g = creditsData.data) === null || _g === void 0 ? void 0 : _g.total_credits) !== null && _h !== void 0 ? _h : null;
|
|
452
|
+
const totalUsage = (_k = (_j = creditsData.data) === null || _j === void 0 ? void 0 : _j.total_usage) !== null && _k !== void 0 ? _k : null;
|
|
453
|
+
if (totalCredits !== null && totalUsage !== null) {
|
|
454
|
+
result.accountBalance = totalCredits - totalUsage;
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Credits data not found. Top-level fields:', Object.keys(creditsData));
|
|
458
|
+
if (creditsData.data) {
|
|
459
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Credits data.data fields:', Object.keys(creditsData.data));
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const creditsErrorText = yield creditsResponse.text().catch(() => 'Failed to read error text');
|
|
466
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', `⚠️ Failed to fetch credits. Status: ${creditsResponse.status}, Response preview: ${creditsErrorText.substring(0, 200)}`);
|
|
467
|
+
if (creditsResponse.status === 401) {
|
|
468
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('error', '❌ Unauthorized - invalid API key for credits endpoint');
|
|
469
|
+
}
|
|
470
|
+
else if (creditsResponse.status === 404) {
|
|
471
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Credits endpoint not found');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// Остаток лимита ключа
|
|
475
|
+
if (keyResponse.ok) {
|
|
476
|
+
const contentType = keyResponse.headers.get('content-type') || '';
|
|
477
|
+
if (!contentType.includes('application/json')) {
|
|
478
|
+
const text = yield keyResponse.text();
|
|
479
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', `⚠️ Key endpoint returned non-JSON. Content-Type: ${contentType}, Preview: ${text.substring(0, 200)}`);
|
|
480
|
+
try {
|
|
481
|
+
const keyData = JSON.parse(text);
|
|
482
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', '💰 Parsed key data despite wrong content-type:', JSON.stringify(keyData, null, 2));
|
|
483
|
+
const limitRemaining = (_o = (_m = (_l = keyData.data) === null || _l === void 0 ? void 0 : _l.limit_remaining) !== null && _m !== void 0 ? _m : keyData.limit_remaining) !== null && _o !== void 0 ? _o : null;
|
|
484
|
+
const limit = (_r = (_q = (_p = keyData.data) === null || _p === void 0 ? void 0 : _p.limit) !== null && _q !== void 0 ? _q : keyData.limit) !== null && _r !== void 0 ? _r : null;
|
|
485
|
+
if (limitRemaining !== null && limitRemaining !== undefined) {
|
|
486
|
+
const limitValue = typeof limitRemaining === 'number' ? limitRemaining : parseFloat(limitRemaining);
|
|
487
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', `✅ Key limit found: ${limitValue.toFixed(4)}`);
|
|
488
|
+
result.keyLimit = limitValue;
|
|
489
|
+
}
|
|
490
|
+
else if (limit === null || limit === undefined) {
|
|
491
|
+
result.keyLimit = -1;
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
result.keyLimit = null;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch (_2) {
|
|
498
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Could not parse key response as JSON');
|
|
499
|
+
result.keyLimit = null;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
const keyData = yield keyResponse.json();
|
|
504
|
+
const limitRemaining = (_x = (_u = (_t = (_s = keyData.data) === null || _s === void 0 ? void 0 : _s.limit_remaining) !== null && _t !== void 0 ? _t : keyData.limit_remaining) !== null && _u !== void 0 ? _u : (_w = (_v = keyData.data) === null || _v === void 0 ? void 0 : _v.limit) === null || _w === void 0 ? void 0 : _w.remaining) !== null && _x !== void 0 ? _x : null;
|
|
505
|
+
const limit = (_0 = (_z = (_y = keyData.data) === null || _y === void 0 ? void 0 : _y.limit) !== null && _z !== void 0 ? _z : keyData.limit) !== null && _0 !== void 0 ? _0 : null;
|
|
506
|
+
if (limitRemaining !== null && limitRemaining !== undefined) {
|
|
507
|
+
const limitValue = typeof limitRemaining === 'number' ? limitRemaining : parseFloat(limitRemaining);
|
|
508
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('log', `✅ Key limit found: ${limitValue.toFixed(4)}`);
|
|
509
|
+
result.keyLimit = limitValue;
|
|
510
|
+
}
|
|
511
|
+
else if (limit === null || limit === undefined) {
|
|
512
|
+
result.keyLimit = -1; // лимита нет (unlimited)
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Key limit not found. Available fields:', Object.keys(keyData));
|
|
516
|
+
if (keyData.data) {
|
|
517
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', '⚠️ Key data.data fields:', Object.keys(keyData.data));
|
|
518
|
+
}
|
|
519
|
+
result.keyLimit = null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const errorText = yield keyResponse.text().catch(() => 'Failed to read error text');
|
|
525
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('warn', `⚠️ Failed to fetch key limit. Status: ${keyResponse.status}, Response preview: ${errorText.substring(0, 200)}`);
|
|
526
|
+
if (keyResponse.status === 401) {
|
|
527
|
+
logMsg === null || logMsg === void 0 ? void 0 : logMsg('error', '❌ Unauthorized - invalid API key');
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
});
|
|
532
|
+
}
|