@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 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({ pdfPath: "./sample.pdf" })
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(PROVIDER_ALIASES.map((providerAlias) => [providerAlias, { apiKey: "" }]))
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
- if (PROVIDER_ALIASES.includes(raw)) return raw
27
- const fromType = PROVIDER_ALIAS_BY_TYPE.get(raw)
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: ${PROVIDER_SET_NAMES.join(", ")}`)
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 PROVIDER_ALIASES[0] || "openai"
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 PROVIDER_ALIASES) {
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 (!next || next.startsWith("--")) {
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 PROVIDER_ENTRIES) {
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
- if (!configuredApiKey && !readEnvApiKey(provider)) {
175
- const apiKeyEnv = PROJECT_CONFIG.providers?.[provider]?.apiKeyEnv || "PROVIDER_API_KEY"
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 <${PROVIDER_SET_NAMES.join("|")}> --api-key <KEY> [--profile name]\n`)
316
- process.stdout.write(` provider use --provider <${PROVIDER_ALIASES.join("|")}> [--profile name]\n`)
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 () => {
@@ -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: string;
4
+ readonly apiKeyEnv?: string;
5
5
  readonly baseUrl?: string;
6
6
  readonly headers?: Record<string, string>;
7
7
  readonly timeoutMs?: number;
@@ -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();
@@ -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, input.provider.apiKeyEnv);
29
+ return readRequiredEnv(input.env, envKey);
27
30
  };
package/dist/types.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type ProviderType = "openai" | "openrouter" | "vercel-ai-gateway";
1
+ export type ProviderType = string;
2
2
  export interface Env {
3
3
  readonly ECHO_PDF_CONFIG_JSON?: string;
4
4
  readonly [key: string]: string | undefined;
@@ -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.6.0",
4
+ "version": "0.7.0",
5
5
  "type": "module",
6
6
  "homepage": "https://pdf.echofile.ai/",
7
7
  "repository": {