@echofiles/echo-pdf 0.6.0 → 0.7.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/README.md +15 -1
- package/bin/echo-pdf.js +22 -16
- package/dist/pdf-types.d.ts +1 -1
- package/dist/provider-client.js +1 -1
- package/dist/provider-keys.js +4 -1
- package/dist/types.d.ts +1 -1
- package/echo-pdf.config.json +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -68,6 +68,16 @@ echo-pdf semantic ./sample.pdf
|
|
|
68
68
|
|
|
69
69
|
`echo-pdf semantic` now uses the CLI profile's provider/model/api-key settings. If the selected provider or model is missing, it fails early with a clear setup error instead of quietly dropping back to a weaker path.
|
|
70
70
|
|
|
71
|
+
For a local OpenAI-compatible LLM server, point a provider at `http://localhost:...` and leave `apiKeyEnv` empty in `echo-pdf.config.json`. Then configure the CLI profile without a dummy key:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
echo-pdf provider set --provider ollama --api-key ""
|
|
75
|
+
echo-pdf model set --provider ollama --model llava:13b
|
|
76
|
+
echo-pdf semantic ./sample.pdf --provider ollama
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
This works for local OpenAI-compatible servers such as Ollama, llama.cpp, vLLM, LM Studio, or LocalAI, as long as the selected model supports vision input.
|
|
80
|
+
|
|
71
81
|
What these commands map to:
|
|
72
82
|
|
|
73
83
|
- `document` -> `get_document`
|
|
@@ -109,7 +119,11 @@ import {
|
|
|
109
119
|
|
|
110
120
|
const document = await get_document({ pdfPath: "./sample.pdf" })
|
|
111
121
|
const structure = await get_document_structure({ pdfPath: "./sample.pdf" })
|
|
112
|
-
const semantic = await get_semantic_document_structure({
|
|
122
|
+
const semantic = await get_semantic_document_structure({
|
|
123
|
+
pdfPath: "./sample.pdf",
|
|
124
|
+
provider: "openai",
|
|
125
|
+
model: "gpt-4.1-mini",
|
|
126
|
+
})
|
|
113
127
|
const page1 = await get_page_content({ pdfPath: "./sample.pdf", pageNumber: 1 })
|
|
114
128
|
const render1 = await get_page_render({ pdfPath: "./sample.pdf", pageNumber: 1, scale: 2 })
|
|
115
129
|
```
|
package/bin/echo-pdf.js
CHANGED
|
@@ -9,24 +9,26 @@ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json")
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const PROJECT_CONFIG_FILE = path.resolve(__dirname, "../echo-pdf.config.json")
|
|
11
11
|
const PROJECT_CONFIG = JSON.parse(fs.readFileSync(PROJECT_CONFIG_FILE, "utf-8"))
|
|
12
|
-
const PROVIDER_ENTRIES = Object.entries(PROJECT_CONFIG.providers || {})
|
|
13
|
-
const PROVIDER_ALIASES = PROVIDER_ENTRIES.map(([alias]) => alias)
|
|
14
|
-
const PROVIDER_ALIAS_BY_TYPE = new Map(PROVIDER_ENTRIES.map(([alias, provider]) => [provider.type, alias]))
|
|
15
|
-
const PROVIDER_SET_NAMES = Array.from(new Set(PROVIDER_ENTRIES.flatMap(([alias, provider]) => [alias, provider.type])))
|
|
16
12
|
const PROJECT_DEFAULT_MODEL = String(PROJECT_CONFIG.agent?.defaultModel || "").trim()
|
|
17
13
|
|
|
14
|
+
const getProviderEntries = () => Object.entries(PROJECT_CONFIG.providers || {})
|
|
15
|
+
const getProviderAliases = () => getProviderEntries().map(([alias]) => alias)
|
|
16
|
+
const getProviderAliasByType = () => new Map(getProviderEntries().map(([alias, provider]) => [provider.type, alias]))
|
|
17
|
+
const getProviderSetNames = () => Array.from(new Set(getProviderEntries().flatMap(([alias, provider]) => [alias, provider.type])))
|
|
18
|
+
|
|
18
19
|
const emptyProviders = () =>
|
|
19
|
-
Object.fromEntries(
|
|
20
|
+
Object.fromEntries(getProviderAliases().map((providerAlias) => [providerAlias, { apiKey: "" }]))
|
|
20
21
|
|
|
21
22
|
const resolveProviderAliasInput = (input) => {
|
|
22
23
|
if (typeof input !== "string" || input.trim().length === 0) {
|
|
23
24
|
throw new Error("provider is required")
|
|
24
25
|
}
|
|
25
26
|
const raw = input.trim()
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const providerAliases = getProviderAliases()
|
|
28
|
+
if (providerAliases.includes(raw)) return raw
|
|
29
|
+
const fromType = getProviderAliasByType().get(raw)
|
|
28
30
|
if (fromType) return fromType
|
|
29
|
-
throw new Error(`provider must be one of: ${
|
|
31
|
+
throw new Error(`provider must be one of: ${getProviderSetNames().join(", ")}`)
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
function resolveDefaultProviderAlias() {
|
|
@@ -34,7 +36,7 @@ function resolveDefaultProviderAlias() {
|
|
|
34
36
|
if (typeof configured === "string" && configured.trim().length > 0) {
|
|
35
37
|
return resolveProviderAliasInput(configured.trim())
|
|
36
38
|
}
|
|
37
|
-
return
|
|
39
|
+
return getProviderAliases()[0] || "openai"
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const DEFAULT_PROVIDER_ALIAS = resolveDefaultProviderAlias()
|
|
@@ -73,7 +75,7 @@ const getProfile = (config, name) => {
|
|
|
73
75
|
}
|
|
74
76
|
const profile = config.profiles[profileName]
|
|
75
77
|
if (!profile.providers || typeof profile.providers !== "object") profile.providers = {}
|
|
76
|
-
for (const providerAlias of
|
|
78
|
+
for (const providerAlias of getProviderAliases()) {
|
|
77
79
|
if (!profile.providers[providerAlias] || typeof profile.providers[providerAlias] !== "object") {
|
|
78
80
|
profile.providers[providerAlias] = { apiKey: "" }
|
|
79
81
|
}
|
|
@@ -102,7 +104,7 @@ const parseFlags = (args) => {
|
|
|
102
104
|
if (!token?.startsWith("--")) continue
|
|
103
105
|
const key = token.slice(2)
|
|
104
106
|
const next = args[i + 1]
|
|
105
|
-
if (
|
|
107
|
+
if (typeof next !== "string" || next.startsWith("--")) {
|
|
106
108
|
flags[key] = true
|
|
107
109
|
} else {
|
|
108
110
|
flags[key] = next
|
|
@@ -147,7 +149,7 @@ const readEnvApiKey = (providerAlias) => {
|
|
|
147
149
|
const buildProviderApiKeys = (config, profileName) => {
|
|
148
150
|
const profile = getProfile(config, profileName)
|
|
149
151
|
const providerApiKeys = {}
|
|
150
|
-
for (const [providerAlias, providerConfig] of
|
|
152
|
+
for (const [providerAlias, providerConfig] of getProviderEntries()) {
|
|
151
153
|
const apiKey = profile.providers?.[providerAlias]?.apiKey || profile.providers?.[providerConfig.type]?.apiKey || ""
|
|
152
154
|
providerApiKeys[providerAlias] = apiKey
|
|
153
155
|
providerApiKeys[providerConfig.type] = apiKey
|
|
@@ -171,8 +173,8 @@ const resolveLocalSemanticContext = (flags) => {
|
|
|
171
173
|
}
|
|
172
174
|
const providerApiKeys = buildProviderApiKeys(config, profileName)
|
|
173
175
|
const configuredApiKey = typeof providerApiKeys[provider] === "string" ? providerApiKeys[provider].trim() : ""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
+
const apiKeyEnv = PROJECT_CONFIG.providers?.[provider]?.apiKeyEnv || ""
|
|
177
|
+
if (apiKeyEnv && !configuredApiKey && !readEnvApiKey(provider)) {
|
|
176
178
|
throw new Error(
|
|
177
179
|
[
|
|
178
180
|
`semantic requires an API key for provider "${provider}".`,
|
|
@@ -312,12 +314,16 @@ const usage = () => {
|
|
|
312
314
|
process.stdout.write(` page <file.pdf> --page <N> [--workspace DIR] [--force-refresh]\n`)
|
|
313
315
|
process.stdout.write(` render <file.pdf> --page <N> [--scale N] [--workspace DIR] [--force-refresh]\n`)
|
|
314
316
|
process.stdout.write(`\nLocal config commands:\n`)
|
|
315
|
-
process.stdout.write(` provider set --provider <${
|
|
316
|
-
process.stdout.write(` provider use --provider <${
|
|
317
|
+
process.stdout.write(` provider set --provider <${getProviderSetNames().join("|")}> --api-key <KEY> [--profile name]\n`)
|
|
318
|
+
process.stdout.write(` provider use --provider <${getProviderAliases().join("|")}> [--profile name]\n`)
|
|
317
319
|
process.stdout.write(` provider list [--profile name]\n`)
|
|
318
320
|
process.stdout.write(` model set --model <model-id> [--provider alias] [--profile name]\n`)
|
|
319
321
|
process.stdout.write(` model get [--provider alias] [--profile name]\n`)
|
|
320
322
|
process.stdout.write(` model list [--profile name]\n`)
|
|
323
|
+
process.stdout.write(`\nLocal LLM example (no auth):\n`)
|
|
324
|
+
process.stdout.write(` echo-pdf provider set --provider ollama --api-key \"\"\n`)
|
|
325
|
+
process.stdout.write(` echo-pdf model set --provider ollama --model llava:13b\n`)
|
|
326
|
+
process.stdout.write(` echo-pdf semantic ./sample.pdf --provider ollama\n`)
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
const main = async () => {
|
package/dist/pdf-types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ProviderType } from "./types.js";
|
|
2
2
|
export interface EchoPdfProviderConfig {
|
|
3
3
|
readonly type: ProviderType;
|
|
4
|
-
readonly apiKeyEnv
|
|
4
|
+
readonly apiKeyEnv?: string;
|
|
5
5
|
readonly baseUrl?: string;
|
|
6
6
|
readonly headers?: Record<string, string>;
|
|
7
7
|
readonly timeoutMs?: number;
|
package/dist/provider-client.js
CHANGED
|
@@ -30,7 +30,7 @@ const toAuthHeader = (config, providerAlias, provider, env, runtimeApiKeys) => {
|
|
|
30
30
|
provider,
|
|
31
31
|
runtimeApiKeys,
|
|
32
32
|
});
|
|
33
|
-
return { Authorization: `Bearer ${token}` };
|
|
33
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
34
34
|
};
|
|
35
35
|
const withTimeout = async (url, init, timeoutMs) => {
|
|
36
36
|
const ctrl = new AbortController();
|
package/dist/provider-keys.js
CHANGED
|
@@ -16,6 +16,9 @@ export const runtimeProviderKeyCandidates = (_config, providerAlias, provider) =
|
|
|
16
16
|
return Array.from(new Set([...aliases, ...types]));
|
|
17
17
|
};
|
|
18
18
|
export const resolveProviderApiKey = (input) => {
|
|
19
|
+
const envKey = typeof input.provider.apiKeyEnv === "string" ? input.provider.apiKeyEnv.trim() : "";
|
|
20
|
+
if (!envKey)
|
|
21
|
+
return "";
|
|
19
22
|
const candidates = runtimeProviderKeyCandidates(input.config, input.providerAlias, input.provider);
|
|
20
23
|
for (const candidate of candidates) {
|
|
21
24
|
const value = input.runtimeApiKeys?.[candidate];
|
|
@@ -23,5 +26,5 @@ export const resolveProviderApiKey = (input) => {
|
|
|
23
26
|
return value.trim();
|
|
24
27
|
}
|
|
25
28
|
}
|
|
26
|
-
return readRequiredEnv(input.env,
|
|
29
|
+
return readRequiredEnv(input.env, envKey);
|
|
27
30
|
};
|
package/dist/types.d.ts
CHANGED
package/echo-pdf.config.json
CHANGED
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
"chatCompletionsPath": "/chat/completions",
|
|
38
38
|
"modelsPath": "/models"
|
|
39
39
|
}
|
|
40
|
+
},
|
|
41
|
+
"ollama": {
|
|
42
|
+
"type": "openai-compatible",
|
|
43
|
+
"apiKeyEnv": "",
|
|
44
|
+
"baseUrl": "http://127.0.0.1:11434/v1",
|
|
45
|
+
"endpoints": {
|
|
46
|
+
"chatCompletionsPath": "/chat/completions",
|
|
47
|
+
"modelsPath": "/models"
|
|
48
|
+
}
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
}
|
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.
|
|
4
|
+
"version": "0.7.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"homepage": "https://pdf.echofile.ai/",
|
|
7
7
|
"repository": {
|