// npm package
@danmademe/pi-provider-litellm
Pi agent extension for LiteLLM proxy auto-discovery and model configuration
weekly
240
monthly
240
versions
3
maintainers
1
license
MIT
first publish
2026-05-29
publisher
danmademe
tarball
39,407 B
AUTO-PUBLISHED·1 version indexed·latest published 2026-06-05
// exfil path
what is read → where it shipssteals
- ● GCP creds
- ○ home dir
sends to
(no destination string extracted — payload may be dynamic / obfuscated)
evidence in excerpt
> const res = await fetch(url, {
> const res = await fetch(url, {// publisher campaignby danmademe
2 caught packages from this accountThis is not an isolated catch. The same publisher has shipped 1 other package that our pipeline flagged — the shape of a coordinated campaign, not a one-off. Each link below opens that sibling's analysis.
// offending code· @0.3.0· 3 files flagged
- @0.3.0··AUTO-PUBLISHED·publisher: danmademeheuristic 89/100static flags 3llm skippednew-publisher:15dknown-actor-target:teampcp:litellmpublisher-multi-name-burst:5publisher-version-pump:11reads-gcp-credsreads-env-varsreads-homedir
// offending code· 3 files flaggedpatterns: 3
--- package/src/gcloud-token.ts (excerpt) --- import { existsSync, readFileSync } from 'fs' import { join } from 'path' let cachedToken: string | null = null let cachedAt: number = 0 let inflight: Promise<string | null> | null = null export const CACHE_TTL = 50 * 60 * 1000 // 50 minutes in ms interface AuthorizedUserCredentials { type: 'authorized_user' client_id: string client_secret: string refresh_token: string account?: string universe_domain?: string } interface ServiceAccountCredentials { type: 'service_account' } type GoogleCredentials = AuthorizedUserCredentials | ServiceAccountCredentials const ADC_FILENAME = 'application_default_credentials.json' function getAdcPath(): string | null { // 1. GOOGLE_APPLICATION_CREDENTIALS env var (all platforms) const envPath = typeof process !== 'undefined' ? process.env.GOOGLE_APPLICATION_CREDENTIALS : undefined if (envPath) { return envPath } // 2. Default ADC locations (Google's official search order) const candidates: string[] = [] // Linux / macOS: ~/.config/gcloud/ const home = typeof process !== 'undefined' ? process.env.HOME : undefined if (home) { candidates.push(join(home, '.config', 'gcloud', ADC_FILENAME)) } // Windows: %APPDATA%/gcloud/ const appData = typeof process !== 'undefined' ? process.env.APPDATA : undefined if (appData) { candidates.push(join(appData, 'gcloud', ADC_FILENAME)) } for (const path of candidates) { if (existsSync(path)) { return path } } return null } function --- package/src/index.ts (excerpt) --- import type { ExtensionAPI, BeforeAgentStartEvent, BeforeAgentStartEventResult } from '@earendil-works/pi-coding-agent' import { resolvePluginConfig, discoverModels, discoverMcpTools, listSkills, buildProviderConfig } from './litellm-api.js' import { createMcpToolDefinitions, createSkillToolDefinitions, createSkillsInjector } from './tools.js' import { getGcloudToken } from './gcloud-token.js' import { loadModelCache, saveModelCache } from './model-cache.js' import type { LiteLLMModelInfo, McpTool, PluginConfig } from './types.js' const LOG = '[pi-provider-litellm]' export default async function (pi: ExtensionAPI): Promise<void> { const config = resolvePluginConfig() if (!config) { console.warn(`${LOG} No config found — set LITELLM_URL and LITELLM_KEY (or LITELLM_GCLOUD_TOKEN_AUTH=1)`) return } const isGcloudAuth = !!(process.env.LITELLM_GCLOUD_TOKEN_AUTH && process.env.LITELLM_GCLOUD_TOKEN_AUTH !== '' && process.env.LITELLM_GCLOUD_TOKEN_AUTH !== '0') // When gcloud token auth is enabled, fetch a live token instead of using the static apiKey const getToken = async (): Promise<string> => { if (isGcloudAuth) { return (await getGcloudToken()) ?? '' } return config.apiKey } // Await discovery so PI blocks until models are registered before resolving // model patterns. Cache is loaded at the top of discoverAndRegister so the // first call returns quickly on subsequent startups. await discoverAndRegister(pi, config, getTo --- package/src/litellm-api.ts (excerpt) --- import os from 'node:os' import path from 'node:path' import fs from 'node:fs' import type { LiteLLMHealthModel, LiteLLMHealthResponse, LiteLLMModelInfo, McpTool, Skill, SkillPluginsResponse, PluginConfig, ProviderModelConfig, ProviderConfig, } from './types.js' const DISCOVERY_TIMEOUT = 10_000 const TOOL_EXEC_TIMEOUT = 30_000 const SKILL_FETCH_TIMEOUT = 5_000 async function fetchJson<T>(url: string, timeout: number, options?: RequestInit): Promise<T | null> { try { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeout) const res = await fetch(url, { ...options, signal: controller.signal, }) clearTimeout(timer) if (!res.ok) return null return (await res.json()) as T } catch { return null } } async function fetchJsonWithStatus<T>(url: string, timeout: number, options?: RequestInit): Promise<{ data: T | null, status: number }> { try { const controller = new AbortController() const timer = setTimeout(() => controller.abort(), timeout) const res = await fetch(url, { ...options, signal: controller.signal, }) clearTimeout(timer) if (!res.ok) return { data: null, status: res.status } return { data: (await res.json()) as T, status: res.status } } catch { return { data: null, status: 0 } } } export async function discoverModels(config: PluginConfig, token: string): Promise<Record<string, LiteLLMModelInfo>> { con
