patina-cli 3.11.0 → 4.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/.patina.default.yaml +29 -29
- package/CHANGELOG.md +53 -0
- package/NOTICE +21 -0
- package/README.md +117 -224
- package/README_JA.md +134 -77
- package/README_KR.md +132 -74
- package/README_ZH.md +137 -80
- package/SKILL.md +11 -20
- package/artifacts/rebaseline-2025/README.md +147 -0
- package/artifacts/rebaseline-2025/human-controls.public.jsonl +250 -0
- package/artifacts/rebaseline-2025/intake.example.jsonl +2 -0
- package/artifacts/rebaseline-2025/intake.local.example.jsonl +25 -0
- package/artifacts/rebaseline-2025/prompts.template.jsonl +7 -0
- package/artifacts/rebaseline-2025/sources.ko-public.jsonl +39 -0
- package/assets/brand/patina-badge.svg +18 -0
- package/assets/brand/patina-mark.svg +8 -0
- package/assets/demo/README.md +79 -0
- package/core/scoring.md +12 -12
- package/core/standalone-prompt.md +3 -1
- package/core/stylometry.md +93 -22
- package/docs/API.md +1554 -0
- package/docs/AUTHENTICATION.md +50 -26
- package/docs/AUTHENTICATION_KR.md +54 -29
- package/docs/BRANDING.md +9 -8
- package/docs/CLI.md +55 -14
- package/docs/COOKBOOK.md +8 -21
- package/docs/DEMO.md +32 -5
- package/docs/EXIT-CODES.md +2 -3
- package/docs/FALSE-POSITIVES.md +63 -0
- package/docs/FAQ.md +9 -1
- package/docs/FAQ_KR.md +3 -1
- package/docs/FLAG-PARITY.md +33 -47
- package/docs/ISSUE-WAVES.md +57 -0
- package/docs/PATTERNS-EN.md +67 -3
- package/docs/PATTERNS-JA.md +68 -2
- package/docs/PATTERNS-KO.md +70 -7
- package/docs/PATTERNS-ZH.md +67 -3
- package/docs/PATTERNS.md +5 -5
- package/docs/RESEARCH-DOCS-PLATFORM.md +54 -0
- package/docs/ROADMAP.md +46 -66
- package/docs/TRANSLATIONESE-KO.md +51 -0
- package/docs/audits/2026-05-deep-research.md +3 -1
- package/docs/benchmarks/README.md +51 -0
- package/docs/benchmarks/detector-comparison.json +69 -9
- package/docs/benchmarks/detector-comparison.md +10 -5
- package/docs/benchmarks/katfish-ko-latest.json +657 -0
- package/docs/benchmarks/katfish-ko-latest.md +77 -0
- package/docs/benchmarks/latest.json +1183 -108
- package/docs/benchmarks/latest.md +84 -60
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.json +1121 -0
- package/docs/benchmarks/lexicon-freshness-en-2026-05-22.md +136 -0
- package/docs/benchmarks/rebaseline-latest.json +381 -0
- package/docs/benchmarks/rebaseline-latest.md +121 -0
- package/docs/benchmarks/register-stratified-latest.json +164 -0
- package/docs/benchmarks/register-stratified-latest.md +99 -0
- package/docs/benchmarks/register-stratified.md +43 -0
- package/docs/integrations/github-action.md +44 -11
- package/docs/integrations/playground.md +58 -0
- package/docs/integrations/pre-commit.md +5 -5
- package/docs/integrations/release.md +5 -3
- package/docs/integrations/static-sites.md +83 -0
- package/docs/research/2025-rebaseline-plan.md +71 -2
- package/docs/research/2026-rebaseline.md +102 -0
- package/docs/research/adversarial-mps.md +41 -0
- package/docs/research/ai-human-metrics.md +35 -23
- package/docs/research/human-eval-panel.md +42 -0
- package/docs/research/judge-agreement.md +24 -0
- package/docs/research/ko-2025-corpus-sources.md +135 -0
- package/docs/research/lexicon-freshness-audit.md +64 -0
- package/docs/research/zh-ja-lexicon-calibration.md +60 -0
- package/docs/social/patina-launch-copy.md +173 -100
- package/docs/social/patina-launch-execution.md +94 -0
- package/docs/social/patina-launch-korean-first.md +83 -0
- package/docs/social/signs-of-ai-writing.md +26 -0
- package/docs/social/signs-of-ai-writing_KR.md +26 -0
- package/lexicon/ai-en.md +21 -24
- package/lexicon/ai-ja.md +158 -0
- package/lexicon/ai-ko.md +9 -9
- package/lexicon/ai-zh.md +158 -0
- package/lexicon/provenance/ai-en.json +970 -0
- package/lexicon/provenance/ai-ja.json +542 -0
- package/lexicon/provenance/ai-ko.json +866 -0
- package/lexicon/provenance/ai-zh.json +542 -0
- package/package.json +49 -8
- package/patterns/en-communication.md +5 -0
- package/patterns/en-content.md +5 -0
- package/patterns/en-filler.md +5 -0
- package/patterns/en-language.md +29 -1
- package/patterns/en-structure.md +5 -0
- package/patterns/en-style.md +5 -0
- package/patterns/en-viral-hook.md +42 -2
- package/patterns/ja-communication.md +5 -0
- package/patterns/ja-content.md +5 -0
- package/patterns/ja-filler.md +5 -0
- package/patterns/ja-language.md +33 -1
- package/patterns/ja-structure.md +12 -0
- package/patterns/ja-style.md +5 -0
- package/patterns/ja-viral-hook.md +41 -2
- package/patterns/ko-communication.md +5 -0
- package/patterns/ko-content.md +5 -0
- package/patterns/ko-filler.md +5 -0
- package/patterns/ko-language.md +33 -1
- package/patterns/ko-structure.md +25 -6
- package/patterns/ko-style.md +5 -0
- package/patterns/ko-viral-hook.md +38 -2
- package/patterns/zh-communication.md +5 -0
- package/patterns/zh-content.md +5 -0
- package/patterns/zh-filler.md +5 -0
- package/patterns/zh-language.md +37 -1
- package/patterns/zh-structure.md +12 -0
- package/patterns/zh-style.md +5 -0
- package/patterns/zh-viral-hook.md +38 -2
- package/playground/README.md +55 -0
- package/playground/analytics.js +4 -0
- package/playground/analyzer.js +883 -0
- package/playground/app.js +157 -0
- package/playground/data/lexicons.js +343 -0
- package/playground/index.html +138 -0
- package/playground/styles.css +267 -0
- package/profiles/namuwiki.md +111 -0
- package/scripts/adversarial-mps-report.mjs +201 -0
- package/scripts/badge-json.mjs +79 -0
- package/scripts/benchmark-report.mjs +56 -9
- package/scripts/check-release-metadata.mjs +0 -2
- package/scripts/detector-comparison.mjs +7 -7
- package/scripts/generate-playground-data.mjs +77 -0
- package/scripts/katfish-calibration.mjs +464 -0
- package/scripts/lexicon-freshness.mjs +485 -0
- package/scripts/lint.mjs +1 -1
- package/scripts/precommit-score.mjs +4 -3
- package/scripts/prose-score.mjs +81 -5
- package/scripts/rebaseline-intake.mjs +242 -0
- package/scripts/rebaseline-score.mjs +268 -0
- package/scripts/rebaseline-summary.mjs +773 -0
- package/scripts/rebaseline-web-collect.mjs +410 -0
- package/scripts/update-benchmark-ranges.mjs +1 -0
- package/src/api.js +69 -105
- package/src/auth.js +50 -2
- package/src/backends/claude-cli.js +19 -4
- package/src/backends/codex-cli.js +19 -3
- package/src/backends/contract.js +230 -1
- package/src/backends/gemini-cli.js +18 -5
- package/src/backends/index.js +87 -12
- package/src/backends/kimi-cli.js +161 -0
- package/src/cli.js +577 -567
- package/src/commands/doctor.js +2 -2
- package/src/config.js +29 -0
- package/src/errors.js +53 -1
- package/src/features/discourse-tells.js +68 -0
- package/src/features/index.js +82 -8
- package/src/features/lexicon.js +40 -6
- package/src/features/markup-leakage.js +69 -0
- package/src/features/segment.js +41 -0
- package/src/features/signal-strength.js +81 -0
- package/src/features/stylometry.js +231 -1
- package/src/features/translationese.js +127 -0
- package/src/loader.js +76 -0
- package/src/logger.js +22 -23
- package/src/model-defaults.js +55 -0
- package/src/ouroboros.js +31 -0
- package/src/output.js +102 -90
- package/src/prompt-builder.js +103 -68
- package/src/providers.js +51 -4
- package/src/scoring.js +210 -2
- package/src/security.js +75 -0
- package/tests/fixtures/live-quality/en/public-docs-01.md +26 -0
- package/tests/fixtures/live-quality/ko/public-docs-01.md +26 -0
- package/tests/fixtures/suspect-zones/expected-ranges.json +207 -16
- package/tests/fixtures/suspect-zones/ja/ai/ja-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/ja/natural/ja-nat-04-lexicon-cold.md +11 -0
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-02.md +4 -5
- package/tests/fixtures/suspect-zones/ko/ai/ko-ai-07-ko-diagnostic.md +11 -0
- package/tests/fixtures/suspect-zones/zh/ai/zh-ai-04-lexicon.md +11 -0
- package/tests/fixtures/suspect-zones/zh/natural/zh-nat-04-lexicon-cold.md +11 -0
- package/tests/quality/README.md +188 -11
- package/tests/quality/adversarial-mps/fixtures.jsonl +10 -0
- package/tests/quality/benchmark.mjs +39 -1
- package/tests/quality/dogfood.mjs +5 -3
- package/tests/quality/live-fixtures.jsonl +2 -0
- package/tests/quality/live-quality.mjs +596 -0
- package/tests/quality/ranking-metrics.mjs +136 -0
- package/tests/quality/rebaseline-manifest.example.jsonl +5 -0
- package/vercel.json +53 -0
- package/SKILL-MAX.md +0 -455
- package/docs/internal/HARNESS.md +0 -14
- package/docs/internal/README.md +0 -14
- package/docs/internal/WARP.md +0 -23
- package/patina-max/SKILL.md +0 -523
- package/patina-max/composite.py +0 -457
- package/src/cache.js +0 -106
- package/src/commands/init.js +0 -208
- package/src/manifest.js +0 -162
- package/src/max-mode.js +0 -207
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fetch public Korean web pages into the private 2025+ rebaseline workspace.
|
|
3
|
+
//
|
|
4
|
+
// The output intentionally keeps full text in an ignored private JSONL file.
|
|
5
|
+
// Run scripts/rebaseline-score.mjs afterward to publish only hash/metadata and
|
|
6
|
+
// deterministic outcome fields.
|
|
7
|
+
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
import { MATRIX } from './rebaseline-summary.mjs';
|
|
14
|
+
|
|
15
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const REPO_ROOT = resolve(__dirname, '..');
|
|
17
|
+
|
|
18
|
+
export const DEFAULT_SOURCE_INPUT = 'artifacts/rebaseline-2025/sources.ko-public.jsonl';
|
|
19
|
+
export const DEFAULT_PRIVATE_OUTPUT = 'artifacts/rebaseline-2025/private/web-human-controls.generated.private.jsonl';
|
|
20
|
+
export const DEFAULT_MIN_CHARS = 90;
|
|
21
|
+
export const DEFAULT_MAX_CHARS = 700;
|
|
22
|
+
export const DEFAULT_MAX_PER_SOURCE = 8;
|
|
23
|
+
export const DEFAULT_TARGET_PER_REGISTER = 50;
|
|
24
|
+
export const DEFAULT_DELAY_MS = 250;
|
|
25
|
+
|
|
26
|
+
const HANGUL_RE = /[\u3131-\u318e\uac00-\ud7a3]/gu;
|
|
27
|
+
const BAD_BOILERPLATE_RE = /(본문듣기|말하기 속도|글자크기|인쇄하기|공유하기|목록|검색|닫기|저작권자|무단 전재|재배포 금지|자료출처|문의:|페이스북|트위터|카카오|Copyright|All rights reserved|View all|Apply now)/iu;
|
|
28
|
+
|
|
29
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
30
|
+
const args = {
|
|
31
|
+
input: DEFAULT_SOURCE_INPUT,
|
|
32
|
+
output: DEFAULT_PRIVATE_OUTPUT,
|
|
33
|
+
minChars: DEFAULT_MIN_CHARS,
|
|
34
|
+
maxChars: DEFAULT_MAX_CHARS,
|
|
35
|
+
maxPerSource: DEFAULT_MAX_PER_SOURCE,
|
|
36
|
+
targetPerRegister: DEFAULT_TARGET_PER_REGISTER,
|
|
37
|
+
delayMs: DEFAULT_DELAY_MS,
|
|
38
|
+
collectedAt: new Date().toISOString().slice(0, 10),
|
|
39
|
+
dryRun: false,
|
|
40
|
+
json: false,
|
|
41
|
+
help: false,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < argv.length; i++) {
|
|
45
|
+
const arg = argv[i];
|
|
46
|
+
if (arg === '--input') args.input = argv[++i];
|
|
47
|
+
else if (arg === '--output') args.output = argv[++i];
|
|
48
|
+
else if (arg === '--min-chars') args.minChars = Number(argv[++i]);
|
|
49
|
+
else if (arg === '--max-chars') args.maxChars = Number(argv[++i]);
|
|
50
|
+
else if (arg === '--max-per-source') args.maxPerSource = Number(argv[++i]);
|
|
51
|
+
else if (arg === '--target-per-register') args.targetPerRegister = Number(argv[++i]);
|
|
52
|
+
else if (arg === '--delay-ms') args.delayMs = Number(argv[++i]);
|
|
53
|
+
else if (arg === '--collected-at') args.collectedAt = argv[++i];
|
|
54
|
+
else if (arg === '--dry-run') args.dryRun = true;
|
|
55
|
+
else if (arg === '--json') args.json = true;
|
|
56
|
+
else if (arg === '--help' || arg === '-h') args.help = true;
|
|
57
|
+
else throw new Error(`Unknown argument: ${arg}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
for (const [name, value] of Object.entries({
|
|
61
|
+
minChars: args.minChars,
|
|
62
|
+
maxChars: args.maxChars,
|
|
63
|
+
maxPerSource: args.maxPerSource,
|
|
64
|
+
targetPerRegister: args.targetPerRegister,
|
|
65
|
+
delayMs: args.delayMs,
|
|
66
|
+
})) {
|
|
67
|
+
if (!Number.isFinite(value) || value < 0) throw new Error(`${name} must be a non-negative number`);
|
|
68
|
+
}
|
|
69
|
+
if (args.minChars > args.maxChars) throw new Error('minChars cannot exceed maxChars');
|
|
70
|
+
if (Number.isNaN(Date.parse(args.collectedAt))) throw new Error('collected-at must be an ISO-like date');
|
|
71
|
+
|
|
72
|
+
return args;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function loadSourceRows(inputPath = DEFAULT_SOURCE_INPUT) {
|
|
76
|
+
const abs = resolveRepoPath(inputPath);
|
|
77
|
+
const result = {
|
|
78
|
+
path: abs,
|
|
79
|
+
relativePath: toRepoRelative(abs),
|
|
80
|
+
rows: [],
|
|
81
|
+
errors: [],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (!existsSync(abs)) {
|
|
85
|
+
result.errors.push(`source input not found: ${result.relativePath}`);
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const lines = readFileSync(abs, 'utf8').split(/\r?\n/u);
|
|
90
|
+
for (let index = 0; index < lines.length; index++) {
|
|
91
|
+
const lineNumber = index + 1;
|
|
92
|
+
const line = lines[index].trim();
|
|
93
|
+
if (!line || line.startsWith('#')) continue;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
result.rows.push({ lineNumber, value: normalizeSource(JSON.parse(line), lineNumber) });
|
|
97
|
+
} catch (error) {
|
|
98
|
+
result.errors.push(`line ${lineNumber}: ${error.message}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeSource(input, lineNumber) {
|
|
106
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
107
|
+
throw new Error('source row must be a JSON object');
|
|
108
|
+
}
|
|
109
|
+
const source = { ...input };
|
|
110
|
+
for (const field of ['source_id', 'url', 'register', 'source_title', 'source_license']) {
|
|
111
|
+
if (typeof source[field] !== 'string' || source[field].trim() === '') {
|
|
112
|
+
throw new Error(`${field} is required`);
|
|
113
|
+
}
|
|
114
|
+
source[field] = source[field].trim();
|
|
115
|
+
}
|
|
116
|
+
if (!MATRIX.registers.includes(source.register)) {
|
|
117
|
+
throw new Error(`register must be one of ${MATRIX.registers.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const parsed = new URL(source.url);
|
|
121
|
+
if (parsed.protocol !== 'https:') throw new Error('source url must use https');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`invalid url on line ${lineNumber}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
if (source.source_published_at && Number.isNaN(Date.parse(source.source_published_at))) {
|
|
126
|
+
throw new Error('source_published_at must be an ISO-like date when present');
|
|
127
|
+
}
|
|
128
|
+
if (source.max_rows !== undefined && (!Number.isFinite(Number(source.max_rows)) || Number(source.max_rows) < 0)) {
|
|
129
|
+
throw new Error('max_rows must be a non-negative number when present');
|
|
130
|
+
}
|
|
131
|
+
source.max_rows = source.max_rows === undefined ? null : Number(source.max_rows);
|
|
132
|
+
source.sample_prefix = typeof source.sample_prefix === 'string' && source.sample_prefix.trim()
|
|
133
|
+
? source.sample_prefix.trim()
|
|
134
|
+
: `ko-human-web-${slugify(source.source_id)}`;
|
|
135
|
+
source.source_kind = typeof source.source_kind === 'string' && source.source_kind.trim()
|
|
136
|
+
? source.source_kind.trim()
|
|
137
|
+
: 'public-web';
|
|
138
|
+
return source;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export async function collectSources(sources, options = {}) {
|
|
142
|
+
const opts = {
|
|
143
|
+
minChars: options.minChars ?? DEFAULT_MIN_CHARS,
|
|
144
|
+
maxChars: options.maxChars ?? DEFAULT_MAX_CHARS,
|
|
145
|
+
maxPerSource: options.maxPerSource ?? DEFAULT_MAX_PER_SOURCE,
|
|
146
|
+
targetPerRegister: options.targetPerRegister ?? DEFAULT_TARGET_PER_REGISTER,
|
|
147
|
+
delayMs: options.delayMs ?? DEFAULT_DELAY_MS,
|
|
148
|
+
collectedAt: options.collectedAt || new Date().toISOString().slice(0, 10),
|
|
149
|
+
fetchImpl: options.fetchImpl || globalThis.fetch,
|
|
150
|
+
};
|
|
151
|
+
if (typeof opts.fetchImpl !== 'function') throw new Error('fetch is not available in this runtime');
|
|
152
|
+
|
|
153
|
+
const records = [];
|
|
154
|
+
const errors = [];
|
|
155
|
+
const warnings = [];
|
|
156
|
+
const seenHashes = new Set();
|
|
157
|
+
const registerCounts = Object.fromEntries(MATRIX.registers.map((register) => [register, 0]));
|
|
158
|
+
|
|
159
|
+
for (const source of sources) {
|
|
160
|
+
if (registerCounts[source.register] >= opts.targetPerRegister) continue;
|
|
161
|
+
let html;
|
|
162
|
+
try {
|
|
163
|
+
html = await fetchHtml(source.url, opts.fetchImpl);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
warnings.push(`${source.source_id}: ${error.message}`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const candidates = extractTextCandidates(html, opts);
|
|
170
|
+
const sourceLimit = Math.min(
|
|
171
|
+
source.max_rows ?? opts.maxPerSource,
|
|
172
|
+
opts.maxPerSource,
|
|
173
|
+
opts.targetPerRegister - registerCounts[source.register]
|
|
174
|
+
);
|
|
175
|
+
let acceptedFromSource = 0;
|
|
176
|
+
for (const text of candidates) {
|
|
177
|
+
if (acceptedFromSource >= sourceLimit) break;
|
|
178
|
+
const textHash = hashText(text);
|
|
179
|
+
if (seenHashes.has(textHash)) continue;
|
|
180
|
+
seenHashes.add(textHash);
|
|
181
|
+
acceptedFromSource++;
|
|
182
|
+
registerCounts[source.register]++;
|
|
183
|
+
records.push(buildPrivateRecord({
|
|
184
|
+
source,
|
|
185
|
+
text,
|
|
186
|
+
textHash,
|
|
187
|
+
ordinal: acceptedFromSource,
|
|
188
|
+
collectedAt: opts.collectedAt,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (acceptedFromSource === 0) {
|
|
193
|
+
warnings.push(`${source.source_id}: no paragraphs accepted from ${source.url}`);
|
|
194
|
+
}
|
|
195
|
+
if (opts.delayMs > 0) await sleep(opts.delayMs);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
records,
|
|
200
|
+
errors,
|
|
201
|
+
warnings,
|
|
202
|
+
registerCounts,
|
|
203
|
+
sources: sources.length,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fetchHtml(url, fetchImpl) {
|
|
208
|
+
const response = await fetchImpl(url, {
|
|
209
|
+
headers: {
|
|
210
|
+
'user-agent': 'patina-rebaseline-corpus-builder/1.0 (+https://github.com/devswha/patina)',
|
|
211
|
+
accept: 'text/html,application/xhtml+xml',
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
if (!response || !response.ok) {
|
|
215
|
+
throw new Error(`fetch failed: HTTP ${response?.status ?? 'unknown'}`);
|
|
216
|
+
}
|
|
217
|
+
const contentType = response.headers?.get?.('content-type') || '';
|
|
218
|
+
if (contentType && !/text\/html|application\/xhtml\+xml/iu.test(contentType)) {
|
|
219
|
+
throw new Error(`expected HTML but got ${contentType}`);
|
|
220
|
+
}
|
|
221
|
+
return response.text();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function extractTextCandidates(html, options = {}) {
|
|
225
|
+
const minChars = options.minChars ?? DEFAULT_MIN_CHARS;
|
|
226
|
+
const maxChars = options.maxChars ?? DEFAULT_MAX_CHARS;
|
|
227
|
+
const plain = decodeHtmlEntities(String(html || ''))
|
|
228
|
+
.replace(/<!--[\s\S]*?-->/gu, ' ')
|
|
229
|
+
.replace(/<script\b[\s\S]*?<\/script>/giu, ' ')
|
|
230
|
+
.replace(/<style\b[\s\S]*?<\/style>/giu, ' ')
|
|
231
|
+
.replace(/<noscript\b[\s\S]*?<\/noscript>/giu, ' ')
|
|
232
|
+
.replace(/<(?:p|div|section|article|main|br|li|h[1-6]|tr|td|blockquote)\b[^>]*>/giu, '\n')
|
|
233
|
+
.replace(/<[^>]+>/gu, ' ')
|
|
234
|
+
.replace(/\u00a0/gu, ' ');
|
|
235
|
+
|
|
236
|
+
const seen = new Set();
|
|
237
|
+
const candidates = [];
|
|
238
|
+
for (const raw of plain.split(/\n+/u)) {
|
|
239
|
+
const text = normalizeParagraph(raw);
|
|
240
|
+
if (!isUsefulKoreanParagraph(text, { minChars, maxChars })) continue;
|
|
241
|
+
const key = text.toLowerCase();
|
|
242
|
+
if (seen.has(key)) continue;
|
|
243
|
+
seen.add(key);
|
|
244
|
+
candidates.push(text);
|
|
245
|
+
}
|
|
246
|
+
return candidates;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isUsefulKoreanParagraph(text, { minChars, maxChars }) {
|
|
250
|
+
const chars = Array.from(text);
|
|
251
|
+
if (chars.length < minChars || chars.length > maxChars) return false;
|
|
252
|
+
if (BAD_BOILERPLATE_RE.test(text)) return false;
|
|
253
|
+
const hangulCount = (text.match(HANGUL_RE) || []).length;
|
|
254
|
+
if (hangulCount < 25) return false;
|
|
255
|
+
const letterish = chars.filter((char) => /[\p{L}\p{N}]/u.test(char)).length || 1;
|
|
256
|
+
if (hangulCount / letterish < 0.35) return false;
|
|
257
|
+
if ((text.match(/https?:\/\//giu) || []).length > 0) return false;
|
|
258
|
+
if ((text.match(/[|{}[\]<>]/gu) || []).length > 5) return false;
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildPrivateRecord({ source, text, textHash, ordinal, collectedAt }) {
|
|
263
|
+
const suffix = String(ordinal).padStart(2, '0');
|
|
264
|
+
return {
|
|
265
|
+
language: 'ko',
|
|
266
|
+
class: 'natural-human',
|
|
267
|
+
model_family: 'human-reference',
|
|
268
|
+
provider: 'web-human-control',
|
|
269
|
+
model: 'human-authored-web-candidate',
|
|
270
|
+
generated_at: source.source_published_at || collectedAt,
|
|
271
|
+
decoding: 'not-applicable',
|
|
272
|
+
postprocess: {
|
|
273
|
+
editing_pass: 'none',
|
|
274
|
+
extraction: 'scripted web paragraph candidate',
|
|
275
|
+
source_kind: source.source_kind,
|
|
276
|
+
},
|
|
277
|
+
redistribution: 'hash-only',
|
|
278
|
+
source_review: source.source_review || {
|
|
279
|
+
status: 'hash-only-web-candidate',
|
|
280
|
+
rationale: 'Raw text stays in gitignored private intake. Commit only URL, license note, metadata, score, and sha256 digest until redistribution review is complete.',
|
|
281
|
+
license_basis: source.source_license,
|
|
282
|
+
},
|
|
283
|
+
reviewer_notes: source.reviewer_notes || 'Human-control candidate from public Korean web source; not a public benchmark claim.',
|
|
284
|
+
sample_id: `${source.sample_prefix}-${suffix}`,
|
|
285
|
+
register: source.register,
|
|
286
|
+
source_url: source.url,
|
|
287
|
+
source_title: source.source_title,
|
|
288
|
+
source_license: source.source_license,
|
|
289
|
+
...(source.source_published_at ? { source_published_at: source.source_published_at } : {}),
|
|
290
|
+
prompt_id: `${source.sample_prefix}-${suffix}`,
|
|
291
|
+
text_hash: textHash,
|
|
292
|
+
text,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function writePrivateOutput(result, outputPath = DEFAULT_PRIVATE_OUTPUT) {
|
|
297
|
+
if (result.errors.length) throw new Error('refusing to write web corpus output with collection errors');
|
|
298
|
+
const abs = resolveRepoPath(outputPath);
|
|
299
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
300
|
+
writeFileSync(abs, result.records.map((record) => JSON.stringify(record)).join('\n') + (result.records.length ? '\n' : ''));
|
|
301
|
+
return { output: toRepoRelative(abs) };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function renderSummary(result, written = null) {
|
|
305
|
+
const lines = [
|
|
306
|
+
'# Rebaseline Web Collect Summary',
|
|
307
|
+
'',
|
|
308
|
+
`- Sources: ${result.sources}`,
|
|
309
|
+
`- Private rows: ${result.records.length}`,
|
|
310
|
+
`- Validation: **${result.errors.length ? 'FAIL' : 'PASS'}**`,
|
|
311
|
+
];
|
|
312
|
+
if (written) lines.push(`- Private output: \`${written.output}\``);
|
|
313
|
+
lines.push('', '## Register counts');
|
|
314
|
+
for (const register of MATRIX.registers) {
|
|
315
|
+
lines.push(`- ${register}: ${result.registerCounts[register] || 0}`);
|
|
316
|
+
}
|
|
317
|
+
if (result.errors.length) lines.push('', '## Errors', ...result.errors.map((error) => `- ${escapeMarkdown(error)}`));
|
|
318
|
+
if (result.warnings.length) lines.push('', '## Warnings', ...result.warnings.map((warning) => `- ${escapeMarkdown(warning)}`));
|
|
319
|
+
return `${lines.join('\n')}\n`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeParagraph(text) {
|
|
323
|
+
return String(text || '')
|
|
324
|
+
.replace(/\s+/gu, ' ')
|
|
325
|
+
.replace(/^[·•*\\-–—\s]+/u, '')
|
|
326
|
+
.trim();
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function decodeHtmlEntities(text) {
|
|
330
|
+
return text
|
|
331
|
+
.replace(/ /giu, ' ')
|
|
332
|
+
.replace(/&/giu, '&')
|
|
333
|
+
.replace(/</giu, '<')
|
|
334
|
+
.replace(/>/giu, '>')
|
|
335
|
+
.replace(/"/giu, '"')
|
|
336
|
+
.replace(/'/giu, "'")
|
|
337
|
+
.replace(/&#x([0-9a-f]+);/giu, (_, hex) => String.fromCodePoint(Number.parseInt(hex, 16)))
|
|
338
|
+
.replace(/&#([0-9]+);/gu, (_, num) => String.fromCodePoint(Number.parseInt(num, 10)));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function hashText(text) {
|
|
342
|
+
return `sha256:${createHash('sha256').update(String(text)).digest('hex')}`;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function slugify(value) {
|
|
346
|
+
return String(value || '')
|
|
347
|
+
.normalize('NFKD')
|
|
348
|
+
.toLowerCase()
|
|
349
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
350
|
+
.replace(/^-+|-+$/gu, '')
|
|
351
|
+
.slice(0, 64) || 'source';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sleep(ms) {
|
|
355
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function resolveRepoPath(path) {
|
|
359
|
+
return resolve(REPO_ROOT, path);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function toRepoRelative(path) {
|
|
363
|
+
return relative(REPO_ROOT, path) || path;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function escapeMarkdown(value) {
|
|
367
|
+
return String(value ?? '').replace(/\|/gu, '\\|').replace(/\n/gu, ' ');
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function printHelp() {
|
|
371
|
+
console.log(`Usage: node scripts/rebaseline-web-collect.mjs [--input <sources.jsonl>] [--output <private.jsonl>] [--target-per-register <n>] [--max-per-source <n>] [--dry-run] [--json]
|
|
372
|
+
|
|
373
|
+
Fetches public Korean web pages listed in a JSONL source inventory and writes
|
|
374
|
+
private raw-text rows for the 2025+ rebaseline workflow. The output path should
|
|
375
|
+
stay under artifacts/rebaseline-2025/private/ and must not be committed.
|
|
376
|
+
|
|
377
|
+
Default input: ${DEFAULT_SOURCE_INPUT}
|
|
378
|
+
Default output: ${DEFAULT_PRIVATE_OUTPUT}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function main() {
|
|
382
|
+
const args = parseArgs();
|
|
383
|
+
if (args.help) {
|
|
384
|
+
printHelp();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const loaded = loadSourceRows(args.input);
|
|
389
|
+
if (loaded.errors.length) {
|
|
390
|
+
const result = { sources: 0, records: [], registerCounts: {}, errors: loaded.errors, warnings: [] };
|
|
391
|
+
console.log(args.json ? JSON.stringify(result, null, 2) : renderSummary(result));
|
|
392
|
+
process.exitCode = 1;
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const result = await collectSources(loaded.rows.map((row) => row.value), args);
|
|
397
|
+
const written = !args.dryRun && result.errors.length === 0
|
|
398
|
+
? writePrivateOutput(result, args.output)
|
|
399
|
+
: null;
|
|
400
|
+
if (args.json) console.log(JSON.stringify({ ...result, written }, null, 2));
|
|
401
|
+
else console.log(renderSummary(result, written));
|
|
402
|
+
if (result.errors.length) process.exitCode = 1;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
406
|
+
main().catch((error) => {
|
|
407
|
+
console.error(`rebaseline-web-collect: ${error.message}`);
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
});
|
|
410
|
+
}
|
|
@@ -60,6 +60,7 @@ function range(value, tolerance, floor = 0) {
|
|
|
60
60
|
function detectorHot(result) {
|
|
61
61
|
return {
|
|
62
62
|
burstiness: result.paragraphs.some((p) => p.burstiness?.band === 'low'),
|
|
63
|
+
koDiagnostics: result.paragraphs.some((p) => p.koDiagnostics?.hot),
|
|
63
64
|
mattr: result.paragraphs.some((p) => p.mattr?.band === 'low'),
|
|
64
65
|
lexicon: result.paragraphs.some((p) => p.lexicon?.hot),
|
|
65
66
|
};
|
package/src/api.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
|
+
// @ts-check
|
|
1
2
|
import { validateBaseURL } from './security.js';
|
|
3
|
+
import { DEFAULT_BEST_MODELS } from './model-defaults.js';
|
|
2
4
|
|
|
3
5
|
const DEFAULT_TIMEOUT = 120000;
|
|
4
6
|
const DEFAULT_MAX_RETRIES = 2;
|
|
5
7
|
const DEFAULT_BASE_BACKOFF_MS = 1000;
|
|
6
8
|
const DEFAULT_MAX_BACKOFF_MS = 30000;
|
|
9
|
+
/**
|
|
10
|
+
* Default sampling temperature for OpenAI-compatible chat completion calls.
|
|
11
|
+
*
|
|
12
|
+
* @type {number}
|
|
13
|
+
* @example
|
|
14
|
+
* const temperature = DEFAULT_TEMPERATURE; // 0.7
|
|
15
|
+
*/
|
|
7
16
|
export const DEFAULT_TEMPERATURE = 0.7;
|
|
8
17
|
|
|
9
18
|
// Status codes that warrant a retry. Network errors (no status, AbortError)
|
|
@@ -12,6 +21,15 @@ const RETRYABLE_STATUS = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
|
12
21
|
|
|
13
22
|
// Subclassed error so the retry loop can read `.status` + `.retryAfter`
|
|
14
23
|
// without re-parsing strings.
|
|
24
|
+
/**
|
|
25
|
+
* Error raised for non-2xx HTTP responses from an LLM provider.
|
|
26
|
+
*
|
|
27
|
+
* @param {number} status HTTP status code returned by the provider.
|
|
28
|
+
* @param {string} body Response body text, truncated in the message.
|
|
29
|
+
* @param {string|null} retryAfter Raw Retry-After response header, if present.
|
|
30
|
+
* @example
|
|
31
|
+
* throw new HttpError(429, 'rate limit', '2');
|
|
32
|
+
*/
|
|
15
33
|
export class HttpError extends Error {
|
|
16
34
|
constructor(status, body, retryAfter) {
|
|
17
35
|
super(`HTTP ${status}: ${truncate(body)}`);
|
|
@@ -67,6 +85,15 @@ function sleepWithSignal(sleep, ms, signal) {
|
|
|
67
85
|
});
|
|
68
86
|
}
|
|
69
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Decide whether an LLM call failure should be retried.
|
|
90
|
+
*
|
|
91
|
+
* @param {Error|Object} err Error thrown by fetch or {@link HttpError}.
|
|
92
|
+
* @returns {boolean} True for retryable HTTP statuses, aborts, and common network failures.
|
|
93
|
+
* @throws {Error} Does not intentionally throw; unexpected Error-like inputs may still propagate JavaScript runtime failures.
|
|
94
|
+
* @example
|
|
95
|
+
* const retry = isRetryable(new HttpError(429, 'rate limit', '1'));
|
|
96
|
+
*/
|
|
70
97
|
export function isRetryable(err) {
|
|
71
98
|
if (!err) return false;
|
|
72
99
|
if (err.name === 'AbortError') return true;
|
|
@@ -77,6 +104,21 @@ export function isRetryable(err) {
|
|
|
77
104
|
|
|
78
105
|
// Honors Retry-After (seconds or HTTP-date). Falls back to exponential
|
|
79
106
|
// backoff with up to 50% jitter, capped at maxDelay.
|
|
107
|
+
/**
|
|
108
|
+
* Compute retry delay from Retry-After or exponential backoff with jitter.
|
|
109
|
+
*
|
|
110
|
+
* @param {number} attempt Zero-based retry attempt.
|
|
111
|
+
* @param {string|null|undefined} retryAfter Retry-After seconds or HTTP-date header.
|
|
112
|
+
* @param {object} [opts] Backoff tuning and deterministic test hooks.
|
|
113
|
+
* @param {number} [opts.base=1000] Initial exponential backoff in milliseconds.
|
|
114
|
+
* @param {number} [opts.max=30000] Maximum returned delay in milliseconds.
|
|
115
|
+
* @param {Function} [opts.now] Clock returning epoch milliseconds.
|
|
116
|
+
* @param {Function} [opts.random] Random number provider used for jitter.
|
|
117
|
+
* @returns {number} Delay in milliseconds, capped at opts.max.
|
|
118
|
+
* @throws {Error} Propagates validation, filesystem, network, or dependency failures when the underlying operation cannot complete.
|
|
119
|
+
* @example
|
|
120
|
+
* const delay = computeBackoffMs(1, '2'); // 2000
|
|
121
|
+
*/
|
|
80
122
|
export function computeBackoffMs(attempt, retryAfter, opts = {}) {
|
|
81
123
|
const {
|
|
82
124
|
base = DEFAULT_BASE_BACKOFF_MS,
|
|
@@ -101,39 +143,36 @@ export function computeBackoffMs(attempt, retryAfter, opts = {}) {
|
|
|
101
143
|
return Math.min(exp + jitter, max);
|
|
102
144
|
}
|
|
103
145
|
|
|
104
|
-
// Bounded-concurrency semaphore. `max <= 0` yields a no-op gate for callers
|
|
105
|
-
// that explicitly opt into unlimited fanout.
|
|
106
|
-
export function createSemaphore(max) {
|
|
107
|
-
if (!max || max <= 0) {
|
|
108
|
-
return { acquire: () => Promise.resolve(() => {}) };
|
|
109
|
-
}
|
|
110
|
-
let active = 0;
|
|
111
|
-
const queue = [];
|
|
112
|
-
const drain = () => {
|
|
113
|
-
if (active < max && queue.length) {
|
|
114
|
-
active++;
|
|
115
|
-
const resolve = queue.shift();
|
|
116
|
-
resolve(() => {
|
|
117
|
-
active--;
|
|
118
|
-
drain();
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
return {
|
|
123
|
-
acquire() {
|
|
124
|
-
return new Promise((resolve) => {
|
|
125
|
-
queue.push(resolve);
|
|
126
|
-
if (active < max) drain();
|
|
127
|
-
});
|
|
128
|
-
},
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Call an OpenAI-compatible chat completions endpoint with retries, timeout, and abort support.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} options LLM request options.
|
|
151
|
+
* @param {string} options.prompt User prompt sent as the single chat message.
|
|
152
|
+
* @param {string} [options.apiKey] Bearer token for the provider.
|
|
153
|
+
* @param {string} [options.baseURL] OpenAI-compatible API base URL. Defaults to https://api.openai.com/v1.
|
|
154
|
+
* @param {string} [options.model] Model id to request. Defaults to gpt-5.5.
|
|
155
|
+
* @param {number} [options.temperature=DEFAULT_TEMPERATURE] Sampling temperature.
|
|
156
|
+
* @param {number|string} [options.seed] Optional deterministic seed forwarded to the provider.
|
|
157
|
+
* @param {number} [options.timeout=120000] Per-attempt timeout in milliseconds.
|
|
158
|
+
* @param {number} [options.maxRetries=2] Retry count after the first attempt.
|
|
159
|
+
* @param {number} [options.deadline] Absolute epoch-millisecond deadline for all attempts.
|
|
160
|
+
* @param {AbortSignal} [options.signal] External cancellation signal.
|
|
161
|
+
* @param {boolean} [options.allowInsecureBaseURL=false] Allow non-loopback HTTP base URLs.
|
|
162
|
+
* @param {Function} [options.onResponse] Callback receiving provider metadata.
|
|
163
|
+
* @param {Function} [options.sleep] Injectable sleep function for tests.
|
|
164
|
+
* @param {Function} [options.now] Clock returning epoch milliseconds.
|
|
165
|
+
* @returns {Promise<string>} Assistant message content.
|
|
166
|
+
* @throws {HttpError} When the provider returns a non-2xx response after retries.
|
|
167
|
+
* @throws {Error} On abort, timeout, malformed provider payload, or base URL validation failure.
|
|
168
|
+
* @example
|
|
169
|
+
* const text = await callLLM({ prompt: 'Rewrite this', apiKey: process.env.OPENAI_API_KEY });
|
|
170
|
+
*/
|
|
132
171
|
export async function callLLM({
|
|
133
172
|
prompt,
|
|
134
173
|
apiKey,
|
|
135
174
|
baseURL = 'https://api.openai.com/v1',
|
|
136
|
-
model =
|
|
175
|
+
model = DEFAULT_BEST_MODELS.openai,
|
|
137
176
|
temperature = DEFAULT_TEMPERATURE,
|
|
138
177
|
seed,
|
|
139
178
|
timeout = DEFAULT_TIMEOUT,
|
|
@@ -142,7 +181,6 @@ export async function callLLM({
|
|
|
142
181
|
signal,
|
|
143
182
|
allowInsecureBaseURL = false,
|
|
144
183
|
onResponse,
|
|
145
|
-
cache,
|
|
146
184
|
// Allows tests to inject a deterministic delay function.
|
|
147
185
|
sleep = (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
148
186
|
now = () => Date.now(),
|
|
@@ -156,21 +194,6 @@ export async function callLLM({
|
|
|
156
194
|
};
|
|
157
195
|
if (seed !== undefined && seed !== null) body.seed = seed;
|
|
158
196
|
|
|
159
|
-
const cached = cache?.get?.({ prompt, model, temperature, baseURL });
|
|
160
|
-
if (cached) {
|
|
161
|
-
onResponse?.({
|
|
162
|
-
provider: 'cache',
|
|
163
|
-
model: cached.responseModel ?? cached.model ?? model,
|
|
164
|
-
requestedModel: model,
|
|
165
|
-
temperature,
|
|
166
|
-
seed: seed ?? null,
|
|
167
|
-
usage: cached.usage ?? null,
|
|
168
|
-
rawResponse: null,
|
|
169
|
-
content: cached.content,
|
|
170
|
-
cache: { hit: true, key: cached.key, path: cached.path },
|
|
171
|
-
});
|
|
172
|
-
return cached.content;
|
|
173
|
-
}
|
|
174
197
|
|
|
175
198
|
let lastError;
|
|
176
199
|
let attemptsMade = 0;
|
|
@@ -234,9 +257,7 @@ export async function callLLM({
|
|
|
234
257
|
usage: data.usage ?? null,
|
|
235
258
|
rawResponse: data,
|
|
236
259
|
content,
|
|
237
|
-
cache: cache ? { hit: false } : null,
|
|
238
260
|
};
|
|
239
|
-
cache?.set?.({ prompt, model, temperature, baseURL }, content, metadata);
|
|
240
261
|
onResponse?.(metadata);
|
|
241
262
|
|
|
242
263
|
return content;
|
|
@@ -266,65 +287,8 @@ export async function callLLM({
|
|
|
266
287
|
|
|
267
288
|
const err = new Error(`LLM API failed after ${attemptsMade || 1} attempts: ${lastError?.message ?? 'unknown'}`);
|
|
268
289
|
if (lastError?.name === 'AbortError') err.name = 'AbortError';
|
|
269
|
-
|
|
290
|
+
const lastStatus = lastError ? /** @type {any} */ (lastError).status : undefined;
|
|
291
|
+
if (typeof lastStatus === 'number') /** @type {any} */ (err).status = lastStatus;
|
|
270
292
|
throw err;
|
|
271
293
|
}
|
|
272
294
|
|
|
273
|
-
export async function callLLMMultiple({
|
|
274
|
-
prompt,
|
|
275
|
-
models,
|
|
276
|
-
apiKey,
|
|
277
|
-
baseURL = 'https://api.openai.com/v1',
|
|
278
|
-
temperature = DEFAULT_TEMPERATURE,
|
|
279
|
-
seed,
|
|
280
|
-
timeout = DEFAULT_TIMEOUT,
|
|
281
|
-
allowInsecureBaseURL = false,
|
|
282
|
-
deadline,
|
|
283
|
-
signal,
|
|
284
|
-
maxConcurrency,
|
|
285
|
-
onStart,
|
|
286
|
-
onComplete,
|
|
287
|
-
onResponse,
|
|
288
|
-
cache,
|
|
289
|
-
callLLM: callLLMImpl = callLLM,
|
|
290
|
-
sleep,
|
|
291
|
-
now = () => Date.now(),
|
|
292
|
-
}) {
|
|
293
|
-
validateBaseURL(baseURL, { allowInsecure: allowInsecureBaseURL });
|
|
294
|
-
const effectiveMaxConcurrency =
|
|
295
|
-
maxConcurrency === undefined || maxConcurrency === null
|
|
296
|
-
? Math.min(models.length, 3)
|
|
297
|
-
: maxConcurrency;
|
|
298
|
-
const sem = createSemaphore(effectiveMaxConcurrency);
|
|
299
|
-
const promises = models.map(async (model) => {
|
|
300
|
-
const release = await sem.acquire();
|
|
301
|
-
if (onStart) onStart(model);
|
|
302
|
-
try {
|
|
303
|
-
const result = await callLLMImpl({
|
|
304
|
-
prompt,
|
|
305
|
-
apiKey,
|
|
306
|
-
baseURL,
|
|
307
|
-
model,
|
|
308
|
-
temperature,
|
|
309
|
-
seed,
|
|
310
|
-
timeout,
|
|
311
|
-
deadline,
|
|
312
|
-
signal,
|
|
313
|
-
allowInsecureBaseURL,
|
|
314
|
-
onResponse,
|
|
315
|
-
cache,
|
|
316
|
-
sleep,
|
|
317
|
-
now,
|
|
318
|
-
});
|
|
319
|
-
if (onComplete) onComplete(model, true);
|
|
320
|
-
return { model, result, ok: true };
|
|
321
|
-
} catch (err) {
|
|
322
|
-
if (onComplete) onComplete(model, false);
|
|
323
|
-
return { model, error: err.message, ok: false };
|
|
324
|
-
} finally {
|
|
325
|
-
release();
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
return Promise.all(promises);
|
|
330
|
-
}
|