@echofiles/echo-pdf 0.11.0 → 0.11.4
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 +6 -6
- package/bin/echo-pdf.js +2 -2
- package/dist/local/semantic.js +70 -3
- package/dist/local/shared.d.ts +4 -0
- package/dist/local/shared.js +90 -0
- package/dist/provider-client.js +224 -30
- package/package.json +9 -8
- package/scripts/check-runtime.sh +2 -2
- package/scripts/smoke.sh +1 -1
package/README.md
CHANGED
|
@@ -187,12 +187,12 @@ Published docs site:
|
|
|
187
187
|
## Development
|
|
188
188
|
|
|
189
189
|
```bash
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
190
|
+
bun install --frozen-lockfile
|
|
191
|
+
bun run build
|
|
192
|
+
bun run typecheck
|
|
193
|
+
bun run test:unit
|
|
194
|
+
bun run test:acceptance
|
|
195
|
+
bun run test:integration
|
|
196
196
|
```
|
|
197
197
|
|
|
198
198
|
For source-checkout CLI development and repo-local workflows, see [docs/DEVELOPMENT.md](./docs/DEVELOPMENT.md).
|
package/bin/echo-pdf.js
CHANGED
|
@@ -203,13 +203,13 @@ const loadLocalDocumentApi = async () => {
|
|
|
203
203
|
}
|
|
204
204
|
throw new Error(
|
|
205
205
|
"Internal source-checkout CLI dev mode requires Bun and src/local/index.ts. " +
|
|
206
|
-
"Use `
|
|
206
|
+
"Use `bun run cli:dev -- <primitive> ...` only from a source checkout."
|
|
207
207
|
)
|
|
208
208
|
}
|
|
209
209
|
if (!fs.existsSync(LOCAL_DOCUMENT_DIST_PATH)) {
|
|
210
210
|
throw new Error(
|
|
211
211
|
"Local primitive commands require built artifacts in a source checkout. " +
|
|
212
|
-
"Run `
|
|
212
|
+
"Run `bun run build` first, use the internal `bun run cli:dev -- <primitive> ...` path in this repo, or install the published package."
|
|
213
213
|
)
|
|
214
214
|
}
|
|
215
215
|
return import(LOCAL_DOCUMENT_DIST_ENTRY.href)
|
package/dist/local/semantic.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveModelForProvider, resolveProviderAlias } from "../provider-defau
|
|
|
5
5
|
import { toDataUrl } from "../file-utils.js";
|
|
6
6
|
import { generateText, visionRecognize } from "../provider-client.js";
|
|
7
7
|
import { ensureRenderArtifact, indexDocumentInternal } from "./document.js";
|
|
8
|
-
import { fileExists, matchesSourceSnapshot, matchesStrategyKey, pageLabel, parseJsonObject, readJson, resolveConfig, resolveEnv, writeJson, } from "./shared.js";
|
|
8
|
+
import { fileExists, matchesSourceSnapshot, matchesStrategyKey, pageLabel, parseJsonObject, parseJsonObjectWithRepair, readJson, resolveConfig, resolveEnv, writeJson, } from "./shared.js";
|
|
9
9
|
import { normalizeFigureItems, normalizeUnderstandingFormulas, normalizeUnderstandingTables } from "./understanding.js";
|
|
10
10
|
const resolveSemanticExtractionBudget = (input) => ({
|
|
11
11
|
pageSelection: "all",
|
|
@@ -134,6 +134,15 @@ const resolveSemanticAgentContext = (config, request) => {
|
|
|
134
134
|
}
|
|
135
135
|
return { provider, model };
|
|
136
136
|
};
|
|
137
|
+
class SemanticAggregationModelOutputError extends Error {
|
|
138
|
+
detail;
|
|
139
|
+
code = "SEMANTIC_AGGREGATION_INVALID_JSON";
|
|
140
|
+
constructor(message, detail) {
|
|
141
|
+
super(message);
|
|
142
|
+
this.detail = detail;
|
|
143
|
+
this.name = "SemanticAggregationModelOutputError";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
137
146
|
const extractCombinedPageData = async (input) => {
|
|
138
147
|
const renderArtifact = await ensureRenderArtifact({
|
|
139
148
|
pdfPath: input.request.pdfPath,
|
|
@@ -170,6 +179,55 @@ const extractCombinedPageData = async (input) => {
|
|
|
170
179
|
},
|
|
171
180
|
};
|
|
172
181
|
};
|
|
182
|
+
const buildSemanticAggregationRetryPrompt = (record, candidates) => {
|
|
183
|
+
return [
|
|
184
|
+
buildSemanticAggregationPrompt(record, candidates),
|
|
185
|
+
"",
|
|
186
|
+
"Your previous response was not strict JSON.",
|
|
187
|
+
"Return the same semantic structure again, but this time produce strict RFC 8259 JSON only.",
|
|
188
|
+
"Do not wrap in markdown fences.",
|
|
189
|
+
"Do not use invalid backslash escapes such as \\(, \\), \\_, or \\- inside JSON strings.",
|
|
190
|
+
].join("\n");
|
|
191
|
+
};
|
|
192
|
+
const parseSemanticAggregationResponse = async (input) => {
|
|
193
|
+
try {
|
|
194
|
+
const parsed = parseJsonObjectWithRepair(input.aggregated);
|
|
195
|
+
return {
|
|
196
|
+
sections: parsed.parsed?.sections,
|
|
197
|
+
repaired: parsed.repaired,
|
|
198
|
+
retried: false,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
catch (firstError) {
|
|
202
|
+
const causeMessage = firstError instanceof Error ? firstError.message : String(firstError);
|
|
203
|
+
const retried = await generateText({
|
|
204
|
+
config: input.config,
|
|
205
|
+
env: input.env,
|
|
206
|
+
providerAlias: input.provider,
|
|
207
|
+
model: input.model,
|
|
208
|
+
prompt: buildSemanticAggregationRetryPrompt(input.record, input.candidates),
|
|
209
|
+
runtimeApiKeys: input.runtimeApiKeys,
|
|
210
|
+
});
|
|
211
|
+
try {
|
|
212
|
+
const parsed = parseJsonObjectWithRepair(retried);
|
|
213
|
+
return {
|
|
214
|
+
sections: parsed.parsed?.sections,
|
|
215
|
+
repaired: parsed.repaired,
|
|
216
|
+
retried: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
catch (retryError) {
|
|
220
|
+
const retryCauseMessage = retryError instanceof Error ? retryError.message : String(retryError);
|
|
221
|
+
throw new SemanticAggregationModelOutputError("semantic aggregation returned invalid JSON after repair and retry", {
|
|
222
|
+
provider: input.provider,
|
|
223
|
+
model: input.model,
|
|
224
|
+
repaired: false,
|
|
225
|
+
retried: true,
|
|
226
|
+
causeMessage: `${causeMessage}; retry=${retryCauseMessage}`,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
173
231
|
const mergeCrossPageTables = (understandings) => {
|
|
174
232
|
const merged = [];
|
|
175
233
|
let nextId = 1;
|
|
@@ -304,8 +362,17 @@ const ensureSemanticStructureArtifact = async (request) => {
|
|
|
304
362
|
prompt: buildSemanticAggregationPrompt(record, [...candidateMap.values()]),
|
|
305
363
|
runtimeApiKeys: request.providerApiKeys,
|
|
306
364
|
});
|
|
307
|
-
const parsed =
|
|
308
|
-
|
|
365
|
+
const parsed = await parseSemanticAggregationResponse({
|
|
366
|
+
aggregated,
|
|
367
|
+
record,
|
|
368
|
+
candidates: [...candidateMap.values()],
|
|
369
|
+
config,
|
|
370
|
+
env,
|
|
371
|
+
provider,
|
|
372
|
+
model,
|
|
373
|
+
runtimeApiKeys: request.providerApiKeys,
|
|
374
|
+
});
|
|
375
|
+
const sections = toSemanticTree(parsed.sections, pageArtifactPaths);
|
|
309
376
|
const mergedTables = mergeCrossPageTables(pageElements);
|
|
310
377
|
const mergedFormulas = mergeCrossPageFormulas(pageElements);
|
|
311
378
|
const mergedFigures = mergeCrossPageFigures(pageElements);
|
package/dist/local/shared.d.ts
CHANGED
|
@@ -19,6 +19,10 @@ export declare const createPreview: (text: string) => string;
|
|
|
19
19
|
export declare const createPageTitle: (pageNumber: number, text: string) => string;
|
|
20
20
|
export declare const stripCodeFences: (value: string) => string;
|
|
21
21
|
export declare const parseJsonObject: (value: string) => unknown;
|
|
22
|
+
export declare const parseJsonObjectWithRepair: (value: string) => {
|
|
23
|
+
parsed: unknown;
|
|
24
|
+
repaired: boolean;
|
|
25
|
+
};
|
|
22
26
|
export declare const normalizeTableItems: (value: unknown) => LocalTableArtifactItem[];
|
|
23
27
|
export declare const normalizeFormulaItems: (value: unknown) => LocalFormulaArtifactItem[];
|
|
24
28
|
export declare const resolveEnv: (env?: Env) => Env;
|
package/dist/local/shared.js
CHANGED
|
@@ -77,6 +77,96 @@ export const parseJsonObject = (value) => {
|
|
|
77
77
|
throw new Error("model output was not valid JSON");
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
const validJsonEscape = (value) => /["\\/bfnrt]/.test(value);
|
|
81
|
+
const repairInvalidJsonEscapes = (value) => {
|
|
82
|
+
let repaired = false;
|
|
83
|
+
let inString = false;
|
|
84
|
+
let escaping = false;
|
|
85
|
+
let unicodeDigitsRemaining = 0;
|
|
86
|
+
let output = "";
|
|
87
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
88
|
+
const char = value[index] ?? "";
|
|
89
|
+
if (!inString) {
|
|
90
|
+
output += char;
|
|
91
|
+
if (char === "\"")
|
|
92
|
+
inString = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (unicodeDigitsRemaining > 0) {
|
|
96
|
+
output += char;
|
|
97
|
+
if (/^[0-9a-fA-F]$/.test(char)) {
|
|
98
|
+
unicodeDigitsRemaining -= 1;
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
repaired = true;
|
|
102
|
+
unicodeDigitsRemaining = 0;
|
|
103
|
+
}
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (escaping) {
|
|
107
|
+
if (validJsonEscape(char)) {
|
|
108
|
+
output += char;
|
|
109
|
+
}
|
|
110
|
+
else if (char === "u") {
|
|
111
|
+
output += char;
|
|
112
|
+
unicodeDigitsRemaining = 4;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
output += `\\${char}`;
|
|
116
|
+
repaired = true;
|
|
117
|
+
}
|
|
118
|
+
escaping = false;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (char === "\\") {
|
|
122
|
+
output += char;
|
|
123
|
+
escaping = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
output += char;
|
|
127
|
+
if (char === "\"")
|
|
128
|
+
inString = false;
|
|
129
|
+
}
|
|
130
|
+
if (escaping) {
|
|
131
|
+
output += "\\";
|
|
132
|
+
repaired = true;
|
|
133
|
+
}
|
|
134
|
+
return { repairedText: output, repaired };
|
|
135
|
+
};
|
|
136
|
+
export const parseJsonObjectWithRepair = (value) => {
|
|
137
|
+
const trimmed = stripCodeFences(value).trim();
|
|
138
|
+
if (!trimmed)
|
|
139
|
+
return { parsed: null, repaired: false };
|
|
140
|
+
const candidates = [trimmed];
|
|
141
|
+
const start = trimmed.indexOf("{");
|
|
142
|
+
const end = trimmed.lastIndexOf("}");
|
|
143
|
+
if (start >= 0 && end > start) {
|
|
144
|
+
const sliced = trimmed.slice(start, end + 1);
|
|
145
|
+
if (sliced !== trimmed)
|
|
146
|
+
candidates.push(sliced);
|
|
147
|
+
}
|
|
148
|
+
let lastError = null;
|
|
149
|
+
for (const candidate of candidates) {
|
|
150
|
+
try {
|
|
151
|
+
return { parsed: JSON.parse(candidate), repaired: false };
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
155
|
+
}
|
|
156
|
+
const repairedCandidate = repairInvalidJsonEscapes(candidate);
|
|
157
|
+
if (!repairedCandidate.repaired)
|
|
158
|
+
continue;
|
|
159
|
+
try {
|
|
160
|
+
return { parsed: JSON.parse(repairedCandidate.repairedText), repaired: true };
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (lastError)
|
|
167
|
+
throw lastError;
|
|
168
|
+
throw new Error("model output was not valid JSON");
|
|
169
|
+
};
|
|
80
170
|
export const normalizeTableItems = (value) => {
|
|
81
171
|
if (!Array.isArray(value))
|
|
82
172
|
return [];
|
package/dist/provider-client.js
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
1
|
import { resolveProviderApiKey } from "./provider-keys.js";
|
|
2
|
+
class ProviderRequestError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
detail;
|
|
5
|
+
constructor(code, detail) {
|
|
6
|
+
const parts = [
|
|
7
|
+
`${detail.operation} failed`,
|
|
8
|
+
`code=${code}`,
|
|
9
|
+
`url=${detail.url}`,
|
|
10
|
+
`attempt=${detail.attempt}/${detail.maxAttempts}`,
|
|
11
|
+
];
|
|
12
|
+
if (typeof detail.status === "number")
|
|
13
|
+
parts.push(`http=${detail.status}`);
|
|
14
|
+
if (detail.contentType)
|
|
15
|
+
parts.push(`contentType=${detail.contentType}`);
|
|
16
|
+
if (detail.causeMessage)
|
|
17
|
+
parts.push(`cause=${detail.causeMessage}`);
|
|
18
|
+
if (detail.responsePreview)
|
|
19
|
+
parts.push(`preview=${JSON.stringify(detail.responsePreview)}`);
|
|
20
|
+
super(parts.join(" "));
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.detail = detail;
|
|
23
|
+
this.name = "ProviderRequestError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
2
26
|
const defaultBaseUrl = (provider) => {
|
|
3
27
|
if (provider.baseUrl)
|
|
4
28
|
return provider.baseUrl;
|
|
@@ -32,33 +56,197 @@ const toAuthHeader = (config, providerAlias, provider, env, runtimeApiKeys) => {
|
|
|
32
56
|
});
|
|
33
57
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
34
58
|
};
|
|
35
|
-
const
|
|
59
|
+
const isPrivateIpv4 = (hostname) => /^10\./.test(hostname) ||
|
|
60
|
+
/^127\./.test(hostname) ||
|
|
61
|
+
/^192\.168\./.test(hostname) ||
|
|
62
|
+
/^172\.(1[6-9]|2\d|3[0-1])\./.test(hostname);
|
|
63
|
+
const isLocalOpenAiCompatible = (provider, url) => {
|
|
64
|
+
try {
|
|
65
|
+
const parsed = new URL(url);
|
|
66
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
67
|
+
return (provider.type === "openai-compatible" &&
|
|
68
|
+
(hostname === "localhost" || hostname === "::1" || isPrivateIpv4(hostname)));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
const maxAttemptsFor = (provider, url, method, operation) => {
|
|
75
|
+
if (method !== "POST")
|
|
76
|
+
return 1;
|
|
77
|
+
if (operation === "Model list request")
|
|
78
|
+
return 1;
|
|
79
|
+
return isLocalOpenAiCompatible(provider, url) ? 2 : 1;
|
|
80
|
+
};
|
|
81
|
+
const truncate = (value, length = 800) => value.slice(0, length);
|
|
82
|
+
const withTimeout = async (input) => {
|
|
36
83
|
const ctrl = new AbortController();
|
|
37
|
-
const timer = setTimeout(() => ctrl.abort("timeout"), timeoutMs);
|
|
84
|
+
const timer = setTimeout(() => ctrl.abort("timeout"), input.timeoutMs);
|
|
38
85
|
try {
|
|
39
|
-
return await fetch(url, { ...init, signal: ctrl.signal });
|
|
86
|
+
return await fetch(input.url, { ...input.init, signal: ctrl.signal });
|
|
40
87
|
}
|
|
41
88
|
catch (error) {
|
|
42
89
|
if (error instanceof Error && error.name === "AbortError") {
|
|
43
|
-
throw new
|
|
90
|
+
throw new ProviderRequestError("PROVIDER_REQUEST_TIMEOUT", {
|
|
91
|
+
operation: input.operation,
|
|
92
|
+
providerAlias: input.providerAlias,
|
|
93
|
+
url: input.url,
|
|
94
|
+
attempt: input.attempt,
|
|
95
|
+
maxAttempts: input.maxAttempts,
|
|
96
|
+
causeMessage: `timeout after ${input.timeoutMs}ms`,
|
|
97
|
+
});
|
|
44
98
|
}
|
|
45
|
-
throw
|
|
99
|
+
throw new ProviderRequestError("PROVIDER_REQUEST_SEND_FAILED", {
|
|
100
|
+
operation: input.operation,
|
|
101
|
+
providerAlias: input.providerAlias,
|
|
102
|
+
url: input.url,
|
|
103
|
+
attempt: input.attempt,
|
|
104
|
+
maxAttempts: input.maxAttempts,
|
|
105
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
106
|
+
});
|
|
46
107
|
}
|
|
47
108
|
finally {
|
|
48
109
|
clearTimeout(timer);
|
|
49
110
|
}
|
|
50
111
|
};
|
|
51
|
-
const
|
|
52
|
-
const
|
|
112
|
+
const readResponseText = async (input) => {
|
|
113
|
+
const reader = input.response.body?.getReader();
|
|
114
|
+
if (!reader) {
|
|
115
|
+
try {
|
|
116
|
+
return await input.response.text();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new ProviderRequestError("PROVIDER_RESPONSE_BODY_READ_FAILED", {
|
|
120
|
+
operation: input.operation,
|
|
121
|
+
providerAlias: input.providerAlias,
|
|
122
|
+
url: input.url,
|
|
123
|
+
status: input.response.status,
|
|
124
|
+
contentType: input.response.headers.get("content-type") ?? "",
|
|
125
|
+
attempt: input.attempt,
|
|
126
|
+
maxAttempts: input.maxAttempts,
|
|
127
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const chunks = [];
|
|
132
|
+
let total = 0;
|
|
53
133
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
134
|
+
while (true) {
|
|
135
|
+
const { done, value } = await reader.read();
|
|
136
|
+
if (done)
|
|
137
|
+
break;
|
|
138
|
+
if (!value || value.length === 0)
|
|
139
|
+
continue;
|
|
140
|
+
chunks.push(value);
|
|
141
|
+
total += value.length;
|
|
56
142
|
}
|
|
57
|
-
return (await response.text()).slice(0, 800);
|
|
58
143
|
}
|
|
59
|
-
catch {
|
|
60
|
-
|
|
144
|
+
catch (error) {
|
|
145
|
+
const combined = new Uint8Array(total);
|
|
146
|
+
let offset = 0;
|
|
147
|
+
for (const chunk of chunks) {
|
|
148
|
+
combined.set(chunk, offset);
|
|
149
|
+
offset += chunk.length;
|
|
150
|
+
}
|
|
151
|
+
throw new ProviderRequestError("PROVIDER_RESPONSE_BODY_READ_FAILED", {
|
|
152
|
+
operation: input.operation,
|
|
153
|
+
providerAlias: input.providerAlias,
|
|
154
|
+
url: input.url,
|
|
155
|
+
status: input.response.status,
|
|
156
|
+
contentType: input.response.headers.get("content-type") ?? "",
|
|
157
|
+
attempt: input.attempt,
|
|
158
|
+
maxAttempts: input.maxAttempts,
|
|
159
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
160
|
+
responsePreview: truncate(new TextDecoder().decode(combined)),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const combined = new Uint8Array(total);
|
|
164
|
+
let offset = 0;
|
|
165
|
+
for (const chunk of chunks) {
|
|
166
|
+
combined.set(chunk, offset);
|
|
167
|
+
offset += chunk.length;
|
|
61
168
|
}
|
|
169
|
+
return new TextDecoder().decode(combined);
|
|
170
|
+
};
|
|
171
|
+
const parseJsonResponse = (input) => {
|
|
172
|
+
try {
|
|
173
|
+
return JSON.parse(input.rawText);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
throw new ProviderRequestError("PROVIDER_RESPONSE_JSON_PARSE_FAILED", {
|
|
177
|
+
operation: input.operation,
|
|
178
|
+
providerAlias: input.providerAlias,
|
|
179
|
+
url: input.url,
|
|
180
|
+
status: input.response.status,
|
|
181
|
+
contentType: input.response.headers.get("content-type") ?? "",
|
|
182
|
+
attempt: input.attempt,
|
|
183
|
+
maxAttempts: input.maxAttempts,
|
|
184
|
+
causeMessage: error instanceof Error ? error.message : String(error),
|
|
185
|
+
responsePreview: truncate(input.rawText),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
const requestJson = async (input) => {
|
|
190
|
+
const maxAttempts = maxAttemptsFor(input.provider, input.url, input.method, input.operation);
|
|
191
|
+
let lastError = null;
|
|
192
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
193
|
+
try {
|
|
194
|
+
const response = await withTimeout({
|
|
195
|
+
operation: input.operation,
|
|
196
|
+
providerAlias: input.providerAlias,
|
|
197
|
+
url: input.url,
|
|
198
|
+
init: {
|
|
199
|
+
method: input.method,
|
|
200
|
+
headers: input.headers,
|
|
201
|
+
...(typeof input.body === "string" ? { body: input.body } : {}),
|
|
202
|
+
},
|
|
203
|
+
timeoutMs: input.provider.timeoutMs ?? 30000,
|
|
204
|
+
attempt,
|
|
205
|
+
maxAttempts,
|
|
206
|
+
});
|
|
207
|
+
const rawText = await readResponseText({
|
|
208
|
+
response,
|
|
209
|
+
operation: input.operation,
|
|
210
|
+
providerAlias: input.providerAlias,
|
|
211
|
+
url: input.url,
|
|
212
|
+
attempt,
|
|
213
|
+
maxAttempts,
|
|
214
|
+
});
|
|
215
|
+
if (!response.ok) {
|
|
216
|
+
throw new ProviderRequestError("PROVIDER_HTTP_ERROR", {
|
|
217
|
+
operation: input.operation,
|
|
218
|
+
providerAlias: input.providerAlias,
|
|
219
|
+
url: input.url,
|
|
220
|
+
status: response.status,
|
|
221
|
+
contentType: response.headers.get("content-type") ?? "",
|
|
222
|
+
attempt,
|
|
223
|
+
maxAttempts,
|
|
224
|
+
responsePreview: truncate(rawText),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return parseJsonResponse({
|
|
228
|
+
rawText,
|
|
229
|
+
response,
|
|
230
|
+
operation: input.operation,
|
|
231
|
+
providerAlias: input.providerAlias,
|
|
232
|
+
url: input.url,
|
|
233
|
+
attempt,
|
|
234
|
+
maxAttempts,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
239
|
+
if (attempt < maxAttempts &&
|
|
240
|
+
lastError instanceof ProviderRequestError &&
|
|
241
|
+
(lastError.code === "PROVIDER_REQUEST_SEND_FAILED" ||
|
|
242
|
+
lastError.code === "PROVIDER_RESPONSE_BODY_READ_FAILED" ||
|
|
243
|
+
lastError.code === "PROVIDER_RESPONSE_JSON_PARSE_FAILED")) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
throw lastError;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
throw lastError ?? new Error(`${input.operation} failed`);
|
|
62
250
|
};
|
|
63
251
|
const getProvider = (config, alias) => {
|
|
64
252
|
const provider = config.providers[alias];
|
|
@@ -70,18 +258,20 @@ const getProvider = (config, alias) => {
|
|
|
70
258
|
export const listProviderModels = async (config, env, alias, runtimeApiKeys) => {
|
|
71
259
|
const provider = getProvider(config, alias);
|
|
72
260
|
const url = resolveEndpoint(provider, "modelsPath");
|
|
73
|
-
const
|
|
261
|
+
const payload = await requestJson({
|
|
262
|
+
operation: "Model list request",
|
|
263
|
+
config,
|
|
264
|
+
env,
|
|
265
|
+
providerAlias: alias,
|
|
266
|
+
provider,
|
|
267
|
+
url,
|
|
74
268
|
method: "GET",
|
|
75
269
|
headers: {
|
|
76
270
|
Accept: "application/json",
|
|
77
271
|
...toAuthHeader(config, alias, provider, env, runtimeApiKeys),
|
|
78
272
|
...(provider.headers ?? {}),
|
|
79
273
|
},
|
|
80
|
-
}
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
throw new Error(`Model list request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
|
|
83
|
-
}
|
|
84
|
-
const payload = await response.json();
|
|
274
|
+
});
|
|
85
275
|
const data = payload.data;
|
|
86
276
|
if (!Array.isArray(data))
|
|
87
277
|
return [];
|
|
@@ -93,7 +283,13 @@ export const listProviderModels = async (config, env, alias, runtimeApiKeys) =>
|
|
|
93
283
|
export const visionRecognize = async (input) => {
|
|
94
284
|
const provider = getProvider(input.config, input.providerAlias);
|
|
95
285
|
const url = resolveEndpoint(provider, "chatCompletionsPath");
|
|
96
|
-
const
|
|
286
|
+
const payload = await requestJson({
|
|
287
|
+
operation: "Vision request",
|
|
288
|
+
config: input.config,
|
|
289
|
+
env: input.env,
|
|
290
|
+
providerAlias: input.providerAlias,
|
|
291
|
+
provider,
|
|
292
|
+
url,
|
|
97
293
|
method: "POST",
|
|
98
294
|
headers: {
|
|
99
295
|
"Content-Type": "application/json",
|
|
@@ -112,11 +308,7 @@ export const visionRecognize = async (input) => {
|
|
|
112
308
|
},
|
|
113
309
|
],
|
|
114
310
|
}),
|
|
115
|
-
}
|
|
116
|
-
if (!response.ok) {
|
|
117
|
-
throw new Error(`Vision request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
|
|
118
|
-
}
|
|
119
|
-
const payload = await response.json();
|
|
311
|
+
});
|
|
120
312
|
const message = payload.choices?.[0]?.message;
|
|
121
313
|
if (!message)
|
|
122
314
|
return "";
|
|
@@ -135,7 +327,13 @@ export const visionRecognize = async (input) => {
|
|
|
135
327
|
export const generateText = async (input) => {
|
|
136
328
|
const provider = getProvider(input.config, input.providerAlias);
|
|
137
329
|
const url = resolveEndpoint(provider, "chatCompletionsPath");
|
|
138
|
-
const
|
|
330
|
+
const payload = await requestJson({
|
|
331
|
+
operation: "Text generation request",
|
|
332
|
+
config: input.config,
|
|
333
|
+
env: input.env,
|
|
334
|
+
providerAlias: input.providerAlias,
|
|
335
|
+
provider,
|
|
336
|
+
url,
|
|
139
337
|
method: "POST",
|
|
140
338
|
headers: {
|
|
141
339
|
"Content-Type": "application/json",
|
|
@@ -151,11 +349,7 @@ export const generateText = async (input) => {
|
|
|
151
349
|
},
|
|
152
350
|
],
|
|
153
351
|
}),
|
|
154
|
-
}
|
|
155
|
-
if (!response.ok) {
|
|
156
|
-
throw new Error(`Text generation request failed: HTTP ${response.status} url=${url} detail=${await responseDetail(response)}`);
|
|
157
|
-
}
|
|
158
|
-
const payload = await response.json();
|
|
352
|
+
});
|
|
159
353
|
const message = payload.choices?.[0]?.message;
|
|
160
354
|
if (!message)
|
|
161
355
|
return "";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@echofiles/echo-pdf",
|
|
3
3
|
"description": "Local-first PDF document component core with CLI, workspace artifacts, and reusable page primitives.",
|
|
4
|
-
"version": "0.11.
|
|
4
|
+
"version": "0.11.4",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://pdf.echofile.ai/",
|
|
7
7
|
"repository": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"cli",
|
|
19
19
|
"vision-language"
|
|
20
20
|
],
|
|
21
|
+
"packageManager": "bun@1.3.5",
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|
|
23
24
|
},
|
|
@@ -53,14 +54,14 @@
|
|
|
53
54
|
"eval:stress": "node ./eval/run-local.mjs --suite stress",
|
|
54
55
|
"eval:known-bad": "node ./eval/run-local.mjs --suite known-bad",
|
|
55
56
|
"eval:fetch-public-samples": "node ./eval/fetch-public-samples.mjs",
|
|
56
|
-
"typecheck": "
|
|
57
|
-
"test:unit": "
|
|
58
|
-
"test:acceptance": "
|
|
59
|
-
"test:import-smoke": "
|
|
60
|
-
"test:integration": "
|
|
61
|
-
"test": "
|
|
57
|
+
"typecheck": "bun run check:runtime && tsc --noEmit",
|
|
58
|
+
"test:unit": "bun run check:runtime && vitest run tests/unit",
|
|
59
|
+
"test:acceptance": "bun run check:runtime && bun run build && vitest run tests/acceptance",
|
|
60
|
+
"test:import-smoke": "bun run check:runtime && bun run build && vitest run tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
|
|
61
|
+
"test:integration": "bun run check:runtime && bun run build && vitest run tests/integration/local-document-cli.integration.test.ts tests/integration/local-document.integration.test.ts tests/integration/local-provider-stability.integration.test.ts tests/integration/local-semantic-structure.integration.test.ts tests/integration/npm-pack-import.integration.test.ts tests/integration/ts-nodenext-consumer.integration.test.ts",
|
|
62
|
+
"test": "bun run test:unit && bun run test:acceptance && bun run test:integration",
|
|
62
63
|
"smoke": "bash ./scripts/smoke.sh",
|
|
63
|
-
"prepublishOnly": "
|
|
64
|
+
"prepublishOnly": "bun run build && bun run typecheck && bun run test"
|
|
64
65
|
},
|
|
65
66
|
"engines": {
|
|
66
67
|
"node": ">=20.0.0"
|
package/scripts/check-runtime.sh
CHANGED
|
@@ -9,7 +9,7 @@ if [[ -z "${current_node_major}" ]] || (( current_node_major < required_node_maj
|
|
|
9
9
|
exit 1
|
|
10
10
|
fi
|
|
11
11
|
|
|
12
|
-
for cmd in
|
|
12
|
+
for cmd in bun curl grep sed; do
|
|
13
13
|
if ! command -v "${cmd}" >/dev/null 2>&1; then
|
|
14
14
|
echo "Missing required command: ${cmd}"
|
|
15
15
|
exit 1
|
|
@@ -23,4 +23,4 @@ if [[ "${CHECK_LLM_KEYS:-0}" == "1" ]]; then
|
|
|
23
23
|
fi
|
|
24
24
|
fi
|
|
25
25
|
|
|
26
|
-
echo "runtime check passed: node=$(node -v),
|
|
26
|
+
echo "runtime check passed: node=$(node -v), bun=$(bun -v)"
|
package/scripts/smoke.sh
CHANGED