@plantnet/llm-helpers
v0.4.0
Published
Shared AI helpers for Pl@ntNet - plant descriptions and species identification using LLMs
Readme
@plantnet/llm-helpers
Shared AI helpers for Pl@ntNet mobile and website — LLM-powered plant descriptions, species identification, and visual question generation via OpenRouter.
Installation
pnpm add @plantnet/llm-helpersAPI Key
Fetch an OpenRouter API key for an authenticated user. The baseUrl must match your environment.
import { fetchOpenRouterApiKey } from '@plantnet/llm-helpers'
const { apiKey } = await fetchOpenRouterApiKey(userId, userToken, {
baseUrl: 'https://identify.plantnet.org',
headers: { 'X-Custom': 'value' }, // optional
})Species Descriptions
Fetch pre-generated AI descriptions for a species, or check if an audio description exists.
import {
fetchAISpeciesDescriptions,
checkAISpeciesAudioDescriptionExists,
} from '@plantnet/llm-helpers'
// Fetch descriptions (returns models + descriptions array)
const data = await fetchAISpeciesDescriptions('Quercus robur L.', 'en')
// data.models → [{ value: 0, name: 'informal (gpt-4o)' }, ...]
// data.descriptions → [{ model: 'gpt-4o', tone: 'informal', content: '...' }, ...]
// Check if an audio description exists
const audio = await checkAISpeciesAudioDescriptionExists('Quercus robur L.')
// audio.url → 'https://plantnet.github.io/plantnet-llm-test/audio/quercus-robur.mp3'Species Identification (Streaming)
Stream a response from the LLM explaining how to distinguish between two or more similar species.
import { askAIHowToDistinguishSpecies } from '@plantnet/llm-helpers'
await askAIHowToDistinguishSpecies(
apiKey,
{
prompt: 'How can we distinguish Quercus robur from Quercus petraea?',
promptEnd: 'Focus on leaf morphology.',
imageOrgans: ['leaf', 'fruit'], // optional
},
(chunk) => {
// Called for each streamed token
process.stdout.write(chunk)
},
{
temperature: 0.2,
model: '~anthropic/claude-sonnet-latest',
improvement: 'none', // 'none' | 'advisor' | 'fusion'
},
)Improvement modes
| Mode | Description |
| --------- | -------------------------------------------------------------- |
| none | Standard single-model completion |
| advisor | Model consults a higher-intelligence advisor with web search |
| fusion | Panel of best-in-class models deliberates, a judge synthesizes |
import { getAIImprovementDescription } from '@plantnet/llm-helpers'
getAIImprovementDescription('fusion')
// → "A panel of best-in-class models (...) deliberate and a judge synthesizes the answer..."Visual Questions
Generate visual questions to help users tell apart look-alike species.
import { fetchAIQuestions } from '@plantnet/llm-helpers'
const result = await fetchAIQuestions({
apiKey,
results: [{ species: { name: 'Allium ursinum' } }, { species: { name: 'Allium vineale' } }],
closeResultsIndices: [0, 1],
lang: 'en',
imageOrgans: ['flower'], // optional
})
if (result.status === 'questions') {
// result.questions → [{ text: 'Flower color?', options: [...] }, ...]
}
if (result.status === 'empty') {
// Model didn't generate questions
}Throws APIError on network or API failures.
Models
Fetch available OpenRouter models filtered to text/vision modalities.
import { fetchOpenRouterModels } from '@plantnet/llm-helpers'
const models = await fetchOpenRouterModels()
// [{ id: '~anthropic/claude-opus-latest', name: 'Claude Opus', brand: 'Anthropic', ... }]Usage / Cost
Check the cost of a completed OpenRouter generation.
import { fetchOpenRouterGenerationUsage } from '@plantnet/llm-helpers'
const usage = await fetchOpenRouterGenerationUsage(generationId, apiKey)
// usage.total_cost → 0.0042Note: This function waits 1 second before calling the API to allow the generation to finish.
Default LLM Settings
Get default settings (model, temperature, prompts) for a given locale.
import { getDefaultLLMSettings } from '@plantnet/llm-helpers'
const settings = getDefaultLLMSettings('fr')
// settings.model → '~anthropic/claude-opus-latest'
// settings.temperature → 0.2
// settings.prompt → "N'hésite pas à dire que tu ne sais pas..."
// settings.promptSpeciesDetails → "..."Prompts
Access prompt strings directly for a given locale.
import { getPrompts } from '@plantnet/llm-helpers'
const prompts = getPrompts('en')
// prompts.prompt → "Don't hesitate to say you don't know..."
// prompts.promptIdentifyVariety → "..."
// prompts.promptSpeciesDetails → "..."Constants
import {
DEFAULT_LLM_MODEL, // '~anthropic/claude-opus-latest'
FUSION_ROUTER_MODEL, // 'openrouter/fusion'
FUSION_PANEL_MODELS, // ['~anthropic/claude-opus-latest', '~openai/gpt-latest', '~google/gemini-pro-latest']
TOP_LATEST_MODELS, // [{ id, brand, name }, ...]
LLM_IMPROVEMENT_NONE, // 'none'
LLM_IMPROVEMENT_ADVISOR, // 'advisor'
LLM_IMPROVEMENT_FUSION, // 'fusion'
OPEN_ROUTER_DOC_URLS, // { none: '...', advisor: '...', fusion: '...' }
} from '@plantnet/llm-helpers'Error Handling
All API functions throw typed APIError subclasses. Use instanceof to handle specific errors.
import {
APIError,
APIConnectionError,
AuthenticationError,
RateLimitError,
NotFoundError,
} from '@plantnet/llm-helpers'
try {
await fetchOpenRouterApiKey(userId, token, { baseUrl })
} catch (err) {
if (err instanceof AuthenticationError) {
// 401 — invalid credentials
} else if (err instanceof RateLimitError) {
// 429 — too many requests
} else if (err instanceof NotFoundError) {
// 404 — resource not found
} else if (err instanceof APIConnectionError) {
// Network error — no status code
} else if (err instanceof APIError) {
// Other HTTP errors — err.status, err.error, err.headers
}
}Error classes
| Class | Status |
| -------------------------- | --------------------------- |
| APIConnectionError | No status (network failure) |
| BadRequestError | 400 |
| AuthenticationError | 401 |
| PermissionDeniedError | 403 |
| NotFoundError | 404 |
| ConflictError | 409 |
| UnprocessableEntityError | 422 |
| RateLimitError | 429 |
| InternalServerError | 5xx |
Development
pnpm install
pnpm build
pnpm test
pnpm lint
pnpm formatLicense
MIT
