@telepat/ideon 0.1.30 → 0.1.32
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 -0
- package/README.zh-CN.md +1 -0
- package/dist/ideon.js +353 -33
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ Built for marketers, founders, and lean teams who need to ship high-quality cont
|
|
|
24
24
|
- **Write once, publish everywhere** — One idea turns into article, blog, newsletter, X, LinkedIn, and Reddit posts in a single run. Your article anchors the campaign. Everything else promotes it.
|
|
25
25
|
- **Style and intent control** — 13 styles × 13 intents. Every output shares one consistent voice across every channel.
|
|
26
26
|
- **Research-backed links** — Ideon browses the web and inserts contextual external links like a human writer would. No manual research.
|
|
27
|
+
- **SEO-optimized output** — On-page SEO, E-E-A-T credibility signals, and fact density baked into the writing pipeline. Content built to rank in both traditional search and AI-generated summaries.
|
|
27
28
|
- **Any model via OpenRouter** — Plug in Claude, GPT-4, or any supported model. Switch without changing your workflow.
|
|
28
29
|
- **Writing guide-driven** — Prompt composition grounded in proven writing principles compiled from real advice. No generic AI filler.
|
|
29
30
|
- **Code-driven efficiency** — Deterministic pipeline code handles orchestration. You pay for tokens only when drafting prose.
|
package/README.zh-CN.md
CHANGED
|
@@ -24,6 +24,7 @@ Ideon 是一款 AI 内容写作工具,可将一个想法转化为多格式、
|
|
|
24
24
|
- **写一次,处处发布** — 一次运行将一个创意转化为文章、博客、新闻通讯、X、LinkedIn 和 Reddit 帖子。文章是核心,其余均为推广内容。
|
|
25
25
|
- **风格与意图控制** — 13 种风格 × 13 种意图。所有输出共享同一种一致的声音。
|
|
26
26
|
- **研究支撑的链接** — Ideon 浏览网络并像人类作者一样插入与上下文相关的外部链接。无需手动研究。
|
|
27
|
+
- **SEO 优化输出** — 页面 SEO、E-E-A-T 可信度信号和事实密度已融入写作流水线。生成的内容针对传统搜索和 AI 生成摘要进行了排名优化。
|
|
27
28
|
- **通过 OpenRouter 接入任何模型** — 接入 Claude、GPT-4 或任何支持的模型。无需更改工作流程即可切换。
|
|
28
29
|
- **写作指南驱动** — 提示词组合基于经过实践检验的写作原则汇编而成。没有通用 AI 套话。
|
|
29
30
|
- **代码驱动的高效率** — 确定性流水线代码处理编排。您只需在起草正文时支付 token 费用。
|
package/dist/ideon.js
CHANGED
|
@@ -552,7 +552,8 @@ var baseT2ISettingsSchema = z2.preprocess(
|
|
|
552
552
|
z2.object({
|
|
553
553
|
modelId: z2.string().default(DEFAULT_LIMN_MODEL_ID),
|
|
554
554
|
replicateModelId: z2.string().optional(),
|
|
555
|
-
inputOverrides: z2.record(z2.string(), z2.unknown()).default({})
|
|
555
|
+
inputOverrides: z2.record(z2.string(), z2.unknown()).default({}),
|
|
556
|
+
maxAttempts: z2.number().int().min(1).max(10).default(4)
|
|
556
557
|
})
|
|
557
558
|
);
|
|
558
559
|
var notificationsSettingsSchema = z2.object({
|
|
@@ -562,6 +563,7 @@ var appSettingsSchema = z2.object({
|
|
|
562
563
|
model: z2.string().default("deepseek/deepseek-v4-pro"),
|
|
563
564
|
modelSettings: modelSettingsSchema.default(modelSettingsSchema.parse({})),
|
|
564
565
|
modelRequestTimeoutMs: z2.number().int().positive().default(9e4),
|
|
566
|
+
modelRequestMaxAttempts: z2.number().int().min(1).max(10).default(4),
|
|
565
567
|
t2i: baseT2ISettingsSchema.default(baseT2ISettingsSchema.parse({})),
|
|
566
568
|
notifications: notificationsSettingsSchema.default(notificationsSettingsSchema.parse({})),
|
|
567
569
|
contentTargets: z2.array(contentTargetSchema).min(1).refine((targets) => targets.filter((target) => target.role === "primary").length === 1, {
|
|
@@ -580,6 +582,7 @@ var envSettingsSchema = z2.object({
|
|
|
580
582
|
maxTokens: z2.number().int().positive().optional(),
|
|
581
583
|
topP: z2.number().min(0).max(1).optional(),
|
|
582
584
|
modelRequestTimeoutMs: z2.number().int().positive().optional(),
|
|
585
|
+
modelRequestMaxAttempts: z2.number().int().min(1).max(10).optional(),
|
|
583
586
|
notificationsEnabled: z2.boolean().optional(),
|
|
584
587
|
style: z2.enum(writingStyleValues).optional(),
|
|
585
588
|
intent: z2.enum(contentIntentValues).optional(),
|
|
@@ -624,6 +627,7 @@ function readEnvSettings(env = process.env) {
|
|
|
624
627
|
maxTokens: parseNumber(env.IDEON_MAX_TOKENS),
|
|
625
628
|
topP: parseNumber(env.IDEON_TOP_P),
|
|
626
629
|
modelRequestTimeoutMs: parseNumber(env.IDEON_MODEL_REQUEST_TIMEOUT_MS),
|
|
630
|
+
modelRequestMaxAttempts: parseNumber(env.IDEON_MODEL_REQUEST_MAX_ATTEMPTS),
|
|
627
631
|
notificationsEnabled: parseBoolean(env.IDEON_NOTIFICATIONS_ENABLED),
|
|
628
632
|
style: env.IDEON_STYLE,
|
|
629
633
|
intent: env.IDEON_INTENT,
|
|
@@ -1424,7 +1428,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
1424
1428
|
// package.json
|
|
1425
1429
|
var package_default = {
|
|
1426
1430
|
name: "@telepat/ideon",
|
|
1427
|
-
version: "0.1.
|
|
1431
|
+
version: "0.1.32",
|
|
1428
1432
|
description: "CLI for generating rich articles and images from ideas.",
|
|
1429
1433
|
type: "module",
|
|
1430
1434
|
repository: {
|
|
@@ -1547,6 +1551,7 @@ async function resolveRunInput(input) {
|
|
|
1547
1551
|
...job?.settings ?? {},
|
|
1548
1552
|
...envSettings.model ? { model: envSettings.model } : {},
|
|
1549
1553
|
...envSettings.modelRequestTimeoutMs !== void 0 ? { modelRequestTimeoutMs: envSettings.modelRequestTimeoutMs } : {},
|
|
1554
|
+
...envSettings.modelRequestMaxAttempts !== void 0 ? { modelRequestMaxAttempts: envSettings.modelRequestMaxAttempts } : {},
|
|
1550
1555
|
...envSettings.notificationsEnabled !== void 0 ? {
|
|
1551
1556
|
notifications: {
|
|
1552
1557
|
...savedSettings.notifications,
|
|
@@ -2059,6 +2064,9 @@ function intentToGuidePath(intent) {
|
|
|
2059
2064
|
function styleToGuidePath(style) {
|
|
2060
2065
|
return `writing-guide/styles/${style}.md`;
|
|
2061
2066
|
}
|
|
2067
|
+
function seoGuidePath(name) {
|
|
2068
|
+
return `writing-guide/seo/${name}.md`;
|
|
2069
|
+
}
|
|
2062
2070
|
function dedupe(items) {
|
|
2063
2071
|
return Array.from(new Set(items));
|
|
2064
2072
|
}
|
|
@@ -2074,6 +2082,7 @@ function buildPrimaryPlanGuideInstruction(intent, contentType) {
|
|
|
2074
2082
|
"writing-guide/references/headline-writing-systems.md",
|
|
2075
2083
|
"writing-guide/references/ideation-and-credibility-systems.md",
|
|
2076
2084
|
"writing-guide/references/content-frameworks.md",
|
|
2085
|
+
seoGuidePath("on-page-essentials"),
|
|
2077
2086
|
intentToGuidePath(intent),
|
|
2078
2087
|
formatToGuidePath(contentType)
|
|
2079
2088
|
];
|
|
@@ -2093,6 +2102,9 @@ function buildArticleSectionGuideInstruction(style, intent, contentType) {
|
|
|
2093
2102
|
"writing-guide/references/prose-quality-checks.md",
|
|
2094
2103
|
"writing-guide/references/readability-and-pace.md",
|
|
2095
2104
|
"writing-guide/references/skimmability-patterns.md",
|
|
2105
|
+
seoGuidePath("on-page-essentials"),
|
|
2106
|
+
seoGuidePath("eeat-signals"),
|
|
2107
|
+
seoGuidePath("fact-density"),
|
|
2096
2108
|
styleToGuidePath(style),
|
|
2097
2109
|
intentToGuidePath(intent),
|
|
2098
2110
|
formatToGuidePath(contentType)
|
|
@@ -2440,9 +2452,9 @@ function buildLongFormPlanMessages(idea, options) {
|
|
|
2440
2452
|
"",
|
|
2441
2453
|
"Requirements:",
|
|
2442
2454
|
"- The content should feel authoritative, practical, and clearly structured for scanning and deep reading.",
|
|
2443
|
-
"- Generate a memorable title
|
|
2455
|
+
"- Generate a memorable title (under 60 characters) that leads with the primary entity. Include a sharp subtitle that promises a concrete benefit, mechanism, or outcome.",
|
|
2444
2456
|
"- The slug must be lowercase kebab-case and publication-ready.",
|
|
2445
|
-
"- The description should work as a concise meta description and align with the shared content plan.",
|
|
2457
|
+
"- The description should work as a concise meta description (120-160 characters), include the primary entity, and align with the shared content plan.",
|
|
2446
2458
|
`- Plan ${sectionCounts.label} strong sections with distinct focus areas and logical progression (no repetitive section intent).`,
|
|
2447
2459
|
"- Frame section titles to reflect likely search intent or practical reader questions when appropriate.",
|
|
2448
2460
|
"- Each section description should name the mechanism, evidence type, or practical action that makes the section useful.",
|
|
@@ -2463,7 +2475,7 @@ function buildLongFormPlanMessages(idea, options) {
|
|
|
2463
2475
|
`- contentType: set to "${options.contentType}" exactly`,
|
|
2464
2476
|
"- title: string",
|
|
2465
2477
|
"- subtitle: string",
|
|
2466
|
-
"- keywords: array of 3 to 8 strings",
|
|
2478
|
+
"- keywords: array of 3 to 8 specific, non-generic strings representing primary entities and search topics (not exact-match duplicates of heading text)",
|
|
2467
2479
|
"- slug: string in lowercase kebab-case",
|
|
2468
2480
|
"- description: string",
|
|
2469
2481
|
"- introBrief: string",
|
|
@@ -2610,6 +2622,30 @@ async function planPrimaryContent({
|
|
|
2610
2622
|
return shortFormPlanSchema.parse(data);
|
|
2611
2623
|
}
|
|
2612
2624
|
});
|
|
2625
|
+
if (!dryRun) {
|
|
2626
|
+
const seoWarnings = [];
|
|
2627
|
+
if (basePlan.title && basePlan.title.length > 60) {
|
|
2628
|
+
seoWarnings.push(`Title is ${basePlan.title.length} chars (recommended: under 60 for search display safety)`);
|
|
2629
|
+
}
|
|
2630
|
+
if (basePlan.description) {
|
|
2631
|
+
if (basePlan.description.length < 120) {
|
|
2632
|
+
seoWarnings.push(`Description is ${basePlan.description.length} chars (recommended: 120-160 for meta description effectiveness)`);
|
|
2633
|
+
} else if (basePlan.description.length > 160) {
|
|
2634
|
+
seoWarnings.push(`Description is ${basePlan.description.length} chars (recommended: 120-160 for meta description effectiveness)`);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
if (isLongForm) {
|
|
2638
|
+
const longPlan = basePlan;
|
|
2639
|
+
const headings = longPlan.sections.map((s) => s.title.toLowerCase());
|
|
2640
|
+
const duplicateKeywords = longPlan.keywords.filter((kw) => headings.includes(kw.toLowerCase()));
|
|
2641
|
+
if (duplicateKeywords.length > 0) {
|
|
2642
|
+
seoWarnings.push(`Keywords duplicate heading text: ${duplicateKeywords.join(", ")} (consider using semantic variants)`);
|
|
2643
|
+
}
|
|
2644
|
+
}
|
|
2645
|
+
for (const warning of seoWarnings) {
|
|
2646
|
+
console.warn(`[seo] ${warning}`);
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2613
2649
|
const normalizedSlug = slugify(basePlan.slug || basePlan.title);
|
|
2614
2650
|
const uniqueSlug = await resolveUniqueSlug(markdownOutputDir, normalizedSlug);
|
|
2615
2651
|
if (isLongForm) {
|
|
@@ -2944,7 +2980,8 @@ function buildSectionMessages(plan, section, articleSoFar, style, intent, conten
|
|
|
2944
2980
|
"Requirements:",
|
|
2945
2981
|
`- ${paragraphCount} paragraphs.`,
|
|
2946
2982
|
`- Target length: about ${sectionTargetWords} words.`,
|
|
2947
|
-
"- Be concrete and specific.",
|
|
2983
|
+
"- Be concrete and specific. Support key claims with statistics, data points, or authoritative citations.",
|
|
2984
|
+
"- Include at least one practical insight that sounds like first-hand practitioner experience.",
|
|
2948
2985
|
"- Continue naturally from the article draft so far without rehashing prior sections.",
|
|
2949
2986
|
"- Use short Markdown lists only if they materially improve clarity."
|
|
2950
2987
|
].join("\n")
|
|
@@ -3181,6 +3218,212 @@ function buildImagePromptMessages(plan, image, section) {
|
|
|
3181
3218
|
];
|
|
3182
3219
|
}
|
|
3183
3220
|
|
|
3221
|
+
// src/llm/retry.ts
|
|
3222
|
+
var DEFAULT_BASE_BACKOFF_MS = 1500;
|
|
3223
|
+
var DEFAULT_MAX_BACKOFF_MS = 6e4;
|
|
3224
|
+
var DEFAULT_JITTER_MS = 250;
|
|
3225
|
+
var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 409, 425, 429]);
|
|
3226
|
+
var TRANSIENT_ERROR_PATTERN = /timeout|network|fetch|temporarily|aborted|ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|ETIMEDOUT|socket hang up/i;
|
|
3227
|
+
async function withRetry(op, opts) {
|
|
3228
|
+
const maxAttempts = Math.max(1, Math.floor(opts.maxAttempts));
|
|
3229
|
+
const baseBackoffMs = opts.baseBackoffMs ?? DEFAULT_BASE_BACKOFF_MS;
|
|
3230
|
+
const maxBackoffMs = opts.maxBackoffMs ?? DEFAULT_MAX_BACKOFF_MS;
|
|
3231
|
+
const jitterMs = opts.jitterMs ?? DEFAULT_JITTER_MS;
|
|
3232
|
+
const sleep = opts.sleep ?? defaultSleep;
|
|
3233
|
+
const randomFraction = opts.randomFraction ?? Math.random;
|
|
3234
|
+
let lastError;
|
|
3235
|
+
let lastClassification = null;
|
|
3236
|
+
let attemptsMade = 0;
|
|
3237
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
3238
|
+
attemptsMade = attempt;
|
|
3239
|
+
try {
|
|
3240
|
+
return await op(attempt);
|
|
3241
|
+
} catch (error) {
|
|
3242
|
+
lastError = error;
|
|
3243
|
+
const classification = classifyHttpError(error);
|
|
3244
|
+
lastClassification = classification;
|
|
3245
|
+
if (!classification.retryable || attempt >= maxAttempts) {
|
|
3246
|
+
break;
|
|
3247
|
+
}
|
|
3248
|
+
const delayMs = computeDelayMs({
|
|
3249
|
+
retryAfterMs: classification.retryAfterMs,
|
|
3250
|
+
attempt,
|
|
3251
|
+
baseBackoffMs,
|
|
3252
|
+
maxBackoffMs,
|
|
3253
|
+
jitterMs,
|
|
3254
|
+
randomFraction
|
|
3255
|
+
});
|
|
3256
|
+
opts.onRetry?.({
|
|
3257
|
+
attempt,
|
|
3258
|
+
delayMs,
|
|
3259
|
+
reason: classification.reason,
|
|
3260
|
+
statusCode: classification.statusCode
|
|
3261
|
+
});
|
|
3262
|
+
await sleep(delayMs);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
throw buildFinalError(opts.operationLabel, attemptsMade, lastError, lastClassification);
|
|
3266
|
+
}
|
|
3267
|
+
function computeDelayMs(input) {
|
|
3268
|
+
if (typeof input.retryAfterMs === "number" && input.retryAfterMs > 0) {
|
|
3269
|
+
return Math.min(input.maxBackoffMs, input.retryAfterMs);
|
|
3270
|
+
}
|
|
3271
|
+
const exponential = input.baseBackoffMs * 2 ** (input.attempt - 1);
|
|
3272
|
+
const capped = Math.min(input.maxBackoffMs, exponential);
|
|
3273
|
+
const jitter = input.jitterMs > 0 ? input.randomFraction() * input.jitterMs : 0;
|
|
3274
|
+
return Math.floor(capped + jitter);
|
|
3275
|
+
}
|
|
3276
|
+
function classifyHttpError(error) {
|
|
3277
|
+
if (!error) {
|
|
3278
|
+
return { retryable: true, reason: "Empty error value treated as transient." };
|
|
3279
|
+
}
|
|
3280
|
+
const message = errorMessage(error);
|
|
3281
|
+
const statusFromObject = extractStatusFromObject(error);
|
|
3282
|
+
const statusFromMessage = statusFromObject ?? extractStatusFromMessage(message);
|
|
3283
|
+
const retryAfterMs = extractRetryAfterMs(error, message);
|
|
3284
|
+
if (statusFromMessage !== void 0) {
|
|
3285
|
+
if (RETRYABLE_STATUS_CODES.has(statusFromMessage) || statusFromMessage >= 500) {
|
|
3286
|
+
return {
|
|
3287
|
+
retryable: true,
|
|
3288
|
+
statusCode: statusFromMessage,
|
|
3289
|
+
retryAfterMs,
|
|
3290
|
+
reason: `HTTP ${statusFromMessage}`
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
return {
|
|
3294
|
+
retryable: false,
|
|
3295
|
+
statusCode: statusFromMessage,
|
|
3296
|
+
reason: `HTTP ${statusFromMessage}`
|
|
3297
|
+
};
|
|
3298
|
+
}
|
|
3299
|
+
if (TRANSIENT_ERROR_PATTERN.test(message)) {
|
|
3300
|
+
return {
|
|
3301
|
+
retryable: true,
|
|
3302
|
+
retryAfterMs,
|
|
3303
|
+
reason: "Transient network or timeout error."
|
|
3304
|
+
};
|
|
3305
|
+
}
|
|
3306
|
+
return {
|
|
3307
|
+
retryable: true,
|
|
3308
|
+
retryAfterMs,
|
|
3309
|
+
reason: "Unknown error treated as transient."
|
|
3310
|
+
};
|
|
3311
|
+
}
|
|
3312
|
+
function extractStatusFromObject(error) {
|
|
3313
|
+
if (typeof error !== "object" || error === null) {
|
|
3314
|
+
return void 0;
|
|
3315
|
+
}
|
|
3316
|
+
const record = error;
|
|
3317
|
+
const direct = numberFrom(record.status);
|
|
3318
|
+
if (direct !== void 0) {
|
|
3319
|
+
return direct;
|
|
3320
|
+
}
|
|
3321
|
+
const response = record.response;
|
|
3322
|
+
if (typeof response === "object" && response !== null) {
|
|
3323
|
+
const responseStatus = numberFrom(response.status);
|
|
3324
|
+
if (responseStatus !== void 0) {
|
|
3325
|
+
return responseStatus;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
return void 0;
|
|
3329
|
+
}
|
|
3330
|
+
function extractStatusFromMessage(message) {
|
|
3331
|
+
const match = message.match(/status\s+(\d{3})/i);
|
|
3332
|
+
if (!match) {
|
|
3333
|
+
return void 0;
|
|
3334
|
+
}
|
|
3335
|
+
const parsed = Number.parseInt(match[1], 10);
|
|
3336
|
+
return Number.isInteger(parsed) ? parsed : void 0;
|
|
3337
|
+
}
|
|
3338
|
+
function extractRetryAfterMs(error, message) {
|
|
3339
|
+
const headerSeconds = extractRetryAfterHeader(error);
|
|
3340
|
+
if (headerSeconds !== void 0) {
|
|
3341
|
+
return Math.round(headerSeconds * 1e3);
|
|
3342
|
+
}
|
|
3343
|
+
const bodyMatch = message.match(/"?retry_after"?\s*[:=]\s*(\d+(?:\.\d+)?)/i);
|
|
3344
|
+
if (bodyMatch) {
|
|
3345
|
+
const seconds = Number.parseFloat(bodyMatch[1]);
|
|
3346
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
3347
|
+
return Math.round(seconds * 1e3);
|
|
3348
|
+
}
|
|
3349
|
+
}
|
|
3350
|
+
return void 0;
|
|
3351
|
+
}
|
|
3352
|
+
function extractRetryAfterHeader(error) {
|
|
3353
|
+
if (typeof error !== "object" || error === null) {
|
|
3354
|
+
return void 0;
|
|
3355
|
+
}
|
|
3356
|
+
const response = error.response;
|
|
3357
|
+
if (typeof response !== "object" || response === null) {
|
|
3358
|
+
return void 0;
|
|
3359
|
+
}
|
|
3360
|
+
const headers = response.headers;
|
|
3361
|
+
if (!headers) {
|
|
3362
|
+
return void 0;
|
|
3363
|
+
}
|
|
3364
|
+
let rawValue;
|
|
3365
|
+
if (typeof headers === "object" && typeof headers.get === "function") {
|
|
3366
|
+
rawValue = headers.get("retry-after");
|
|
3367
|
+
} else if (typeof headers === "object") {
|
|
3368
|
+
const entries = headers;
|
|
3369
|
+
rawValue = entries["retry-after"] ?? entries["Retry-After"];
|
|
3370
|
+
}
|
|
3371
|
+
if (typeof rawValue !== "string" && typeof rawValue !== "number") {
|
|
3372
|
+
return void 0;
|
|
3373
|
+
}
|
|
3374
|
+
const stringValue = String(rawValue).trim();
|
|
3375
|
+
if (!stringValue) {
|
|
3376
|
+
return void 0;
|
|
3377
|
+
}
|
|
3378
|
+
const numeric = Number.parseFloat(stringValue);
|
|
3379
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
3380
|
+
return numeric;
|
|
3381
|
+
}
|
|
3382
|
+
const dateMs = Date.parse(stringValue);
|
|
3383
|
+
if (Number.isFinite(dateMs)) {
|
|
3384
|
+
const diffSeconds = (dateMs - Date.now()) / 1e3;
|
|
3385
|
+
return diffSeconds > 0 ? diffSeconds : void 0;
|
|
3386
|
+
}
|
|
3387
|
+
return void 0;
|
|
3388
|
+
}
|
|
3389
|
+
function numberFrom(value2) {
|
|
3390
|
+
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
3391
|
+
return value2;
|
|
3392
|
+
}
|
|
3393
|
+
if (typeof value2 === "string") {
|
|
3394
|
+
const parsed = Number.parseFloat(value2);
|
|
3395
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
3396
|
+
}
|
|
3397
|
+
return void 0;
|
|
3398
|
+
}
|
|
3399
|
+
function errorMessage(error) {
|
|
3400
|
+
if (error instanceof Error) {
|
|
3401
|
+
return error.message;
|
|
3402
|
+
}
|
|
3403
|
+
if (typeof error === "string") {
|
|
3404
|
+
return error;
|
|
3405
|
+
}
|
|
3406
|
+
try {
|
|
3407
|
+
return JSON.stringify(error);
|
|
3408
|
+
} catch {
|
|
3409
|
+
return String(error);
|
|
3410
|
+
}
|
|
3411
|
+
}
|
|
3412
|
+
function buildFinalError(operationLabel, attempts, cause, classification) {
|
|
3413
|
+
const detail = errorMessage(cause) || classification?.reason || "unknown error";
|
|
3414
|
+
const message = `${operationLabel} failed after ${attempts} attempt${attempts === 1 ? "" : "s"}: ${detail}`;
|
|
3415
|
+
const wrapped = new Error(message, cause instanceof Error ? { cause } : void 0);
|
|
3416
|
+
if (!(cause instanceof Error) && cause !== void 0) {
|
|
3417
|
+
wrapped.cause = cause;
|
|
3418
|
+
}
|
|
3419
|
+
return wrapped;
|
|
3420
|
+
}
|
|
3421
|
+
function defaultSleep(ms) {
|
|
3422
|
+
return new Promise((resolve) => {
|
|
3423
|
+
setTimeout(resolve, ms);
|
|
3424
|
+
});
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3184
3427
|
// src/pipeline/analytics.ts
|
|
3185
3428
|
var LLM_USD_PER_1K_TOKENS = {
|
|
3186
3429
|
// AUTO-GENERATED:OPENROUTER_PRICING_START
|
|
@@ -3433,8 +3676,26 @@ async function renderExpandedImages({
|
|
|
3433
3676
|
...replicateModelOverride && isReplicateModelIdForFamily(family, replicateModelOverride) ? { replicateModel: replicateModelOverride } : {}
|
|
3434
3677
|
};
|
|
3435
3678
|
const renderStartedAtMs = Date.now();
|
|
3679
|
+
let attemptsMade = 0;
|
|
3680
|
+
let retryCount = 0;
|
|
3681
|
+
let retryBackoffMs = 0;
|
|
3436
3682
|
try {
|
|
3437
|
-
const result = await
|
|
3683
|
+
const result = await withRetry(
|
|
3684
|
+
() => {
|
|
3685
|
+
attemptsMade += 1;
|
|
3686
|
+
return limn.generate(prompt.prompt, family, limnOptions);
|
|
3687
|
+
},
|
|
3688
|
+
{
|
|
3689
|
+
operationLabel: `Replicate ${prompt.kind} image (${prompt.id})`,
|
|
3690
|
+
maxAttempts: settings.t2i.maxAttempts,
|
|
3691
|
+
baseBackoffMs: 1500,
|
|
3692
|
+
maxBackoffMs: 6e4,
|
|
3693
|
+
onRetry({ delayMs }) {
|
|
3694
|
+
retryCount += 1;
|
|
3695
|
+
retryBackoffMs += delayMs;
|
|
3696
|
+
}
|
|
3697
|
+
}
|
|
3698
|
+
);
|
|
3438
3699
|
const ext = mimeTypeToExtension(result.mimeType);
|
|
3439
3700
|
const liveFileName = `${prompt.kind === "cover" ? "cover" : `inline-${prompt.anchorAfterSection}`}-${index + 1}.${ext}`;
|
|
3440
3701
|
const liveOutputPath = path6.join(assetDir, liveFileName);
|
|
@@ -3456,9 +3717,9 @@ async function renderExpandedImages({
|
|
|
3456
3717
|
kind: prompt.kind,
|
|
3457
3718
|
modelId: result.modelSlug,
|
|
3458
3719
|
durationMs: result.analytics.totalDurationMs,
|
|
3459
|
-
attempts:
|
|
3460
|
-
retries:
|
|
3461
|
-
retryBackoffMs
|
|
3720
|
+
attempts: attemptsMade,
|
|
3721
|
+
retries: retryCount,
|
|
3722
|
+
retryBackoffMs,
|
|
3462
3723
|
outputBytes: result.image.byteLength,
|
|
3463
3724
|
costUsd: result.analytics.totalEstimatedCostUsd,
|
|
3464
3725
|
costSource
|
|
@@ -3472,9 +3733,9 @@ async function renderExpandedImages({
|
|
|
3472
3733
|
startedAt: new Date(renderStartedAtMs).toISOString(),
|
|
3473
3734
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3474
3735
|
durationMs: result.analytics.totalDurationMs,
|
|
3475
|
-
attempts:
|
|
3476
|
-
retries:
|
|
3477
|
-
retryBackoffMs
|
|
3736
|
+
attempts: attemptsMade,
|
|
3737
|
+
retries: retryCount,
|
|
3738
|
+
retryBackoffMs,
|
|
3478
3739
|
status: "succeeded",
|
|
3479
3740
|
prompt: prompt.prompt,
|
|
3480
3741
|
input: {},
|
|
@@ -3491,9 +3752,9 @@ async function renderExpandedImages({
|
|
|
3491
3752
|
startedAt: new Date(renderStartedAtMs).toISOString(),
|
|
3492
3753
|
endedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3493
3754
|
durationMs,
|
|
3494
|
-
attempts: 1,
|
|
3495
|
-
retries:
|
|
3496
|
-
retryBackoffMs
|
|
3755
|
+
attempts: Math.max(attemptsMade, 1),
|
|
3756
|
+
retries: retryCount,
|
|
3757
|
+
retryBackoffMs,
|
|
3497
3758
|
status: "failed",
|
|
3498
3759
|
prompt: prompt.prompt,
|
|
3499
3760
|
input: {},
|
|
@@ -3553,8 +3814,9 @@ var OpenRouterClient = class {
|
|
|
3553
3814
|
}
|
|
3554
3815
|
async requestStructured(request) {
|
|
3555
3816
|
let aggregatedMetrics = null;
|
|
3817
|
+
const maxParseAttempts = Math.max(1, request.settings.modelRequestMaxAttempts);
|
|
3556
3818
|
try {
|
|
3557
|
-
for (let attempt = 0; attempt <
|
|
3819
|
+
for (let attempt = 0; attempt < maxParseAttempts; attempt += 1) {
|
|
3558
3820
|
const response = await this.sendCompletion({
|
|
3559
3821
|
messages: request.messages,
|
|
3560
3822
|
settings: request.settings,
|
|
@@ -3579,7 +3841,7 @@ var OpenRouterClient = class {
|
|
|
3579
3841
|
request.onMetrics?.(aggregatedMetrics);
|
|
3580
3842
|
return structured;
|
|
3581
3843
|
} catch (parseError) {
|
|
3582
|
-
if (attempt <
|
|
3844
|
+
if (attempt < maxParseAttempts - 1 && shouldRetryStructuredParseError(parseError)) {
|
|
3583
3845
|
const backoff = backoffMs(attempt);
|
|
3584
3846
|
aggregatedMetrics = recordParseRetryMetrics(aggregatedMetrics, backoff);
|
|
3585
3847
|
await wait(backoff);
|
|
@@ -3645,7 +3907,8 @@ var OpenRouterClient = class {
|
|
|
3645
3907
|
let attempts = 0;
|
|
3646
3908
|
let retries = 0;
|
|
3647
3909
|
let retryBackoffMs = 0;
|
|
3648
|
-
|
|
3910
|
+
const maxAttempts = Math.max(1, settings.modelRequestMaxAttempts);
|
|
3911
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
3649
3912
|
attempts = attempt + 1;
|
|
3650
3913
|
const attemptStartedAtMs = Date.now();
|
|
3651
3914
|
const controller = new AbortController();
|
|
@@ -3695,8 +3958,9 @@ var OpenRouterClient = class {
|
|
|
3695
3958
|
const providerMessage = json?.error?.message ?? `OpenRouter request failed with status ${response.status}`;
|
|
3696
3959
|
const raw = json?.error?.metadata?.raw;
|
|
3697
3960
|
const message = raw ? `${raw} (OpenRouter: ${providerMessage})` : providerMessage;
|
|
3698
|
-
if (shouldRetryStatus(response.status) && attempt <
|
|
3699
|
-
const
|
|
3961
|
+
if (shouldRetryStatus(response.status) && attempt < maxAttempts - 1) {
|
|
3962
|
+
const advisedMs = extractRetryAfterFromResponse(response, json, rawBody);
|
|
3963
|
+
const backoff = advisedMs !== null ? Math.min(MAX_RETRY_BACKOFF_MS, advisedMs) : backoffMs(attempt);
|
|
3700
3964
|
retries += 1;
|
|
3701
3965
|
retryBackoffMs += backoff;
|
|
3702
3966
|
onInteraction?.({
|
|
@@ -3722,7 +3986,7 @@ var OpenRouterClient = class {
|
|
|
3722
3986
|
throw new Error(message);
|
|
3723
3987
|
}
|
|
3724
3988
|
const content = json.choices?.[0]?.message?.content;
|
|
3725
|
-
if (!content && attempt <
|
|
3989
|
+
if (!content && attempt < maxAttempts - 1) {
|
|
3726
3990
|
const backoff = backoffMs(attempt);
|
|
3727
3991
|
retries += 1;
|
|
3728
3992
|
retryBackoffMs += backoff;
|
|
@@ -3798,7 +4062,7 @@ var OpenRouterClient = class {
|
|
|
3798
4062
|
responseBody: responseBodyRaw,
|
|
3799
4063
|
errorMessage: lastError.message
|
|
3800
4064
|
});
|
|
3801
|
-
if (attempt <
|
|
4065
|
+
if (attempt < maxAttempts - 1 && shouldRetryError(lastError)) {
|
|
3802
4066
|
const backoff = backoffMs(attempt);
|
|
3803
4067
|
retries += 1;
|
|
3804
4068
|
retryBackoffMs += backoff;
|
|
@@ -3925,6 +4189,61 @@ function normalizeClientError(error, timeoutMs) {
|
|
|
3925
4189
|
function backoffMs(attempt) {
|
|
3926
4190
|
return 500 * (attempt + 1);
|
|
3927
4191
|
}
|
|
4192
|
+
var MAX_RETRY_BACKOFF_MS = 6e4;
|
|
4193
|
+
function extractRetryAfterFromResponse(response, json, rawBody) {
|
|
4194
|
+
const headerValue = typeof response.headers?.get === "function" ? response.headers.get("retry-after") : null;
|
|
4195
|
+
if (headerValue) {
|
|
4196
|
+
const numeric = Number.parseFloat(headerValue);
|
|
4197
|
+
if (Number.isFinite(numeric) && numeric > 0) {
|
|
4198
|
+
return Math.round(numeric * 1e3);
|
|
4199
|
+
}
|
|
4200
|
+
const dateMs = Date.parse(headerValue);
|
|
4201
|
+
if (Number.isFinite(dateMs)) {
|
|
4202
|
+
const diff = dateMs - Date.now();
|
|
4203
|
+
if (diff > 0) {
|
|
4204
|
+
return diff;
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
}
|
|
4208
|
+
const metadata = json?.error?.metadata;
|
|
4209
|
+
if (metadata) {
|
|
4210
|
+
const fromMetadata = metadata["retry_after"] ?? metadata["retryAfter"];
|
|
4211
|
+
const numeric = toFiniteNumber(fromMetadata);
|
|
4212
|
+
if (numeric !== null && numeric > 0) {
|
|
4213
|
+
return Math.round(numeric * 1e3);
|
|
4214
|
+
}
|
|
4215
|
+
const rawCandidate = metadata["raw"];
|
|
4216
|
+
if (typeof rawCandidate === "string") {
|
|
4217
|
+
const fromRaw = extractRetryAfterFromString(rawCandidate);
|
|
4218
|
+
if (fromRaw !== null) {
|
|
4219
|
+
return fromRaw;
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
}
|
|
4223
|
+
const fromBody = extractRetryAfterFromString(rawBody);
|
|
4224
|
+
if (fromBody !== null) {
|
|
4225
|
+
return fromBody;
|
|
4226
|
+
}
|
|
4227
|
+
return null;
|
|
4228
|
+
}
|
|
4229
|
+
function extractRetryAfterFromString(value2) {
|
|
4230
|
+
const bodyMatch = value2.match(/\\?"retry_after\\?"\s*:\s*(\d+(?:\.\d+)?)/i);
|
|
4231
|
+
if (!bodyMatch) {
|
|
4232
|
+
return null;
|
|
4233
|
+
}
|
|
4234
|
+
const numeric = Number.parseFloat(bodyMatch[1]);
|
|
4235
|
+
return Number.isFinite(numeric) && numeric > 0 ? Math.round(numeric * 1e3) : null;
|
|
4236
|
+
}
|
|
4237
|
+
function toFiniteNumber(value2) {
|
|
4238
|
+
if (typeof value2 === "number" && Number.isFinite(value2)) {
|
|
4239
|
+
return value2;
|
|
4240
|
+
}
|
|
4241
|
+
if (typeof value2 === "string") {
|
|
4242
|
+
const parsed = Number.parseFloat(value2);
|
|
4243
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
4244
|
+
}
|
|
4245
|
+
return null;
|
|
4246
|
+
}
|
|
3928
4247
|
function aggregateLlmMetrics(total, next) {
|
|
3929
4248
|
if (!total) {
|
|
3930
4249
|
return { ...next };
|
|
@@ -4333,7 +4652,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4333
4652
|
const llmInteractions = [];
|
|
4334
4653
|
const t2iInteractions = [];
|
|
4335
4654
|
let writeSession;
|
|
4336
|
-
const applyRetryUpdate = (stageId, retryIncrement,
|
|
4655
|
+
const applyRetryUpdate = (stageId, retryIncrement, errorMessage2) => {
|
|
4337
4656
|
if (retryIncrement <= 0) {
|
|
4338
4657
|
return;
|
|
4339
4658
|
}
|
|
@@ -4344,7 +4663,7 @@ async function runPipelineShell(input, options = {}) {
|
|
|
4344
4663
|
const existing = stageRetryState.get(stageId) ?? { retries: 0, lastError: null };
|
|
4345
4664
|
const next = {
|
|
4346
4665
|
retries: existing.retries + retryIncrement,
|
|
4347
|
-
lastError:
|
|
4666
|
+
lastError: errorMessage2 && errorMessage2.trim().length > 0 ? errorMessage2 : existing.lastError
|
|
4348
4667
|
};
|
|
4349
4668
|
stageRetryState.set(stageId, next);
|
|
4350
4669
|
stages[stageIndex] = {
|
|
@@ -7384,7 +7703,8 @@ function SettingsFlow({ initialSettings, initialSecrets, onDone }) {
|
|
|
7384
7703
|
t2i: {
|
|
7385
7704
|
modelId: item.value,
|
|
7386
7705
|
replicateModelId: current.t2i.replicateModelId && isReplicateModelIdForFamily(item.value, current.t2i.replicateModelId) ? current.t2i.replicateModelId : void 0,
|
|
7387
|
-
inputOverrides: {}
|
|
7706
|
+
inputOverrides: {},
|
|
7707
|
+
maxAttempts: current.t2i.maxAttempts
|
|
7388
7708
|
}
|
|
7389
7709
|
}));
|
|
7390
7710
|
setShowModelSelect(false);
|
|
@@ -9710,13 +10030,13 @@ function PipelinePresenter({
|
|
|
9710
10030
|
prompt,
|
|
9711
10031
|
stages,
|
|
9712
10032
|
result,
|
|
9713
|
-
errorMessage
|
|
10033
|
+
errorMessage: errorMessage2
|
|
9714
10034
|
}) {
|
|
9715
10035
|
const visibleStages = getVisibleStages(stages);
|
|
9716
10036
|
const maxVisibleItems = computeMaxVisibleItemsPerStage({
|
|
9717
10037
|
terminalRows: process.stdout.rows,
|
|
9718
10038
|
visibleStageCount: visibleStages.length,
|
|
9719
|
-
hasError: Boolean(
|
|
10039
|
+
hasError: Boolean(errorMessage2),
|
|
9720
10040
|
hasResult: Boolean(result)
|
|
9721
10041
|
});
|
|
9722
10042
|
return /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", paddingX: 1, children: [
|
|
@@ -9732,7 +10052,7 @@ function PipelinePresenter({
|
|
|
9732
10052
|
},
|
|
9733
10053
|
stage.id
|
|
9734
10054
|
)) }),
|
|
9735
|
-
|
|
10055
|
+
errorMessage2 ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, borderStyle: "round", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsx5(Text4, { color: "red", children: errorMessage2 }) }) : null,
|
|
9736
10056
|
result ? /* @__PURE__ */ jsx5(Box4, { marginTop: 1, children: /* @__PURE__ */ jsx5(FinalSummary, { artifact: result.artifact, analytics: result.analytics }) }) : null
|
|
9737
10057
|
] });
|
|
9738
10058
|
}
|
|
@@ -10267,7 +10587,7 @@ function WriteApp({
|
|
|
10267
10587
|
() => createInitialStages()
|
|
10268
10588
|
);
|
|
10269
10589
|
const [result, setResult] = useState4(null);
|
|
10270
|
-
const [
|
|
10590
|
+
const [errorMessage2, setErrorMessage] = useState4(null);
|
|
10271
10591
|
useEffect3(() => {
|
|
10272
10592
|
let mounted = true;
|
|
10273
10593
|
void (async () => {
|
|
@@ -10320,7 +10640,7 @@ function WriteApp({
|
|
|
10320
10640
|
};
|
|
10321
10641
|
}, [dryRun, enrichLinks2, input, links, unlinks, maxLinks, maxImages, onError, runMode]);
|
|
10322
10642
|
useEffect3(() => {
|
|
10323
|
-
if (!result && !
|
|
10643
|
+
if (!result && !errorMessage2) {
|
|
10324
10644
|
return;
|
|
10325
10645
|
}
|
|
10326
10646
|
const exitTimer = setTimeout(() => {
|
|
@@ -10329,8 +10649,8 @@ function WriteApp({
|
|
|
10329
10649
|
return () => {
|
|
10330
10650
|
clearTimeout(exitTimer);
|
|
10331
10651
|
};
|
|
10332
|
-
}, [
|
|
10333
|
-
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input.idea, stages, result, errorMessage });
|
|
10652
|
+
}, [errorMessage2, exit, result]);
|
|
10653
|
+
return /* @__PURE__ */ jsx7(PipelinePresenter, { prompt: input.idea, stages, result, errorMessage: errorMessage2 });
|
|
10334
10654
|
}
|
|
10335
10655
|
async function runWriteCommand(options) {
|
|
10336
10656
|
const input = await resolveInputWithInteractiveIdeaFallback(options);
|