metafetch

Metafetch calls Claude, GPT, or any model API you describe in a recipe file

One recipe format for any model API. Claude and OpenAI are built in, keys stay in Obsidian's keychain, and every plugin can share them.

Why Care?

A Tooling note's links come from its homepage, but some fields need a reader: a one-line zinger, a pricing model, the right category from your own list. Those need a model, and which model is your call. Claude this month, GPT next, a local model for a private vault, whatever a router offers next year.

No Lossless plugin had a good answer to "use any model". Each one hard-wired its providers with a plaintext key field per provider. Metafetch now has the answer, built to be copied into the rest.

What's New?

  • from: [model] fields in folder profiles, with a describe: instruction and optional type: enum / type: list with values. One call per note, after the free fields, reading the page's text.

  • Four providers built in: Claude (claude-opus-5-5, reply held to a JSON schema, server-side fallback on a decline), OpenAI (gpt-6-astra, strict JSON schema), TrustedRouter, and any OpenAI-compatible endpoint.

  • Any other API as a recipe file in zz-cf-lib/recipes/. Create example model recipe writes one for Google Gemini's native API.

  • Keys in Obsidian's keychain. Settings store the secret's name, never the key, and the keychain is vault-wide.

How a recipe stays safe

A recipe is a vault file, and vaults get shared. So:

  • A recipe is data. The only computation is filling {{model}}, {{system}}, {{prompt}}, {{schema}}, and {{baseUrl}} into the body and reading one path out of the reply.

  • A bundled recipe's key goes only to its own host. A recipe file's key goes nowhere until you list its host under Approved hosts in settings, so a file someone drops into a shared vault can't send your keys elsewhere.

  • Plain http is refused, except to localhost.

  • An error quotes the provider's message, never the request.

  • The model's answer is checked against the profile before anything is written. A value outside an enum is dropped, and URLs are never asked of a model.

Proof

One live call each, on TinaCMS's homepage text, through the same engine:

  • claude-opus-5-5, 4.4 s: "Open-source, Git-backed headless CMS with visual editing for developers' Markdown sites."

  • gpt-6-astra, 2.7 s: "A Git-backed CMS that lets developers edit website content visually."

Under the hood

  • src/services/modelRecipes.ts: the recipe type, the bundled recipes, parseRecipe() for vault files, and callModel(). Its helpers: substitute() (a whole-string {{schema}} keeps its object type), readPath() ([0], [-1], [?type=text]), and parseJsonObject() (bare, fenced, or wrapped in prose).

  • src/services/modelProviderSettings.ts: the settings rows (a SecretComponent per provider, model, base URL, approved hosts), plus mergeProviderSettings() so a provider added later gets its defaults.

  • frontmatterProfiles.ts: modelSchema() makes every field nullable and required, as strict schema modes need; modelPrompt(); acceptModelValue().

  • Both service files depend only on Obsidian's API, and the fence is cf-recipe rather than a Metafetch name, so other plugins can copy the files and share the recipe folder.

  • 21 new tests (158 total).

What's Next

  • The other plugins: see the blueprint's migration table. Perplexed's Perplexity and Anthropic calls fit as-is; it also needs streaming, which recipes don't do yet.

  • Moving each plugin's plaintext key fields into the keychain is worth doing now, ahead of the full move to recipes.