pi-media-models 0.1.4 → 0.1.6

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
@@ -3,109 +3,97 @@
3
3
  [![npm version](https://img.shields.io/npm/v/pi-media-models.svg)](https://www.npmjs.com/package/pi-media-models)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- Provider-neutral multimodal generation extension for [Pi Coding Agent](https://pi.dev). Generate images, video, audio, and speech across 9 providers with a single unified interface.
6
+ **Provider-neutral multimodal generation extension for Pi Coding Agent.**
7
7
 
8
- ---
8
+ This extension seamlessly bridges Pi's reasoning capabilities with top-tier AI media generation platforms. It abstracts away complex multi-part uploads, background task polling, CDN hosting, and API differences, exposing exactly **6 unified tools** for the agent.
9
9
 
10
- ## Installation
10
+ ## ✨ Features
11
11
 
12
- Run this once in any terminal:
12
+ - **Unified Interface**: One request format (`provider`, `model`, `prompt`, `referenceImages`, etc.) maps automatically to the correct capability across providers.
13
+ - **Config-First Authentication**: Manage all API keys and credentials directly in a single `media-models.json` configuration file — no cluttered environment variables needed.
14
+ - **Auto-Download**: Output media (images, videos, audio) is automatically downloaded and saved to a local directory (`~/.pi/agent/media/outputs/`) using atomic `.part` renames. LLM context remains pristine and only receives local file paths.
15
+ - **Smart Input Resolution**: Pass local paths (`C:/...` or `/path/...`), file URIs (`file://`), standard URLs (`http(s)://`), or base64 (`data:...`). The router transparently handles multipart uploads, base64 encoding, or CDN pre-uploading (e.g., for `fal.ai`).
16
+ - **Resilient Polling**: Advanced `MediaJob` processing handles asynchronous Long-Running Operations (LROs), 429 rate limits (respecting `Retry-After`), and timeouts. Supports remote job cancellation where supported by the provider.
13
17
 
14
- ```bash
15
- pi install npm:pi-media-models
16
- ```
18
+ ## 📦 Installation
17
19
 
18
- That's it. Pi downloads the package, registers the extension, and loads the skill guide automatically. No manual configuration needed to get started.
20
+ This extension is built for the **Pi Coding Agent**. It bundles both the executable extension logic and the LLM `SKILL.md` prompt context.
19
21
 
20
- If Pi is already running, type `/reload` in the chat to activate it in the current session.
22
+ Install natively inside your Pi environment:
21
23
 
22
- > **Alternative:** Install directly from source
23
- > ```bash
24
- > pi install git:github.com/luffysolution-svg/pi-media-models
25
- > ```
24
+ ```bash
25
+ # Install via NPM (Recommended)
26
+ pi install npm:pi-media-models
26
27
 
27
- ---
28
+ # Or install directly from GitHub
29
+ pi install git:github.com/luffysolution-svg/pi-media-models
30
+ ```
28
31
 
29
- ## Quick Start
32
+ Once installed, restart Pi or type `/reload` in your active session.
30
33
 
31
- After installation, open Pi and start chatting. No extra steps required if you have environment variables set for your chosen provider.
34
+ ## 🚀 Quick Usage
32
35
 
33
- **Example prompts:**
36
+ After installation, the extension and its companion skill are active immediately. You can talk to Pi naturally — Pi knows how to query available models and route tasks to the appropriate tool.
34
37
 
35
- ```
36
- Generate an image of a misty mountain at sunrise using fal.ai
37
- ```
38
+ **Example Prompts:**
38
39
 
39
40
  ```
40
- Use xAI to create a 5-second video of ocean waves with audio
41
+ 帮我用 fal.ai 画一张赛博朋克风格的雨夜街景,比例 16:9
41
42
  ```
42
43
 
43
44
  ```
44
- Convert this text to speech using OpenAI: "Hello, world"
45
+ Use xAI to generate a 5-second video of ocean waves with audio
45
46
  ```
46
47
 
47
48
  ```
48
- What image/video providers do I have configured?
49
+ 使用 Atlas 编辑这张图片,把背景换成雪山:C:/assets/photo.jpg
49
50
  ```
50
51
 
51
- Pi will automatically call the right tool, handle uploads and polling, and save the result to your local disk. It reports the saved file path when done.
52
-
53
- ---
54
-
55
- ## API Key Setup
56
-
57
- The extension reads keys from environment variables. Set whichever providers you want to use:
58
-
59
- | Provider | Environment Variable |
60
- |---|---|
61
- | fal.ai | `FAL_KEY` |
62
- | OpenAI | `OPENAI_API_KEY` |
63
- | xAI (Grok) | `XAI_API_KEY` |
64
- | Google Gemini | `GEMINI_API_KEY` |
65
- | DashScope / QwenCloud | `DASHSCOPE_API_KEY` |
66
- | OpenRouter | `OPENROUTER_API_KEY` |
67
- | Atlas | `ATLAS_API_KEY` |
68
- | Vertex AI | `GOOGLE_APPLICATION_CREDENTIALS` (ADC) |
69
-
70
- **macOS / Linux** — add to `~/.zshrc` or `~/.bashrc`:
71
-
72
- ```bash
73
- export FAL_KEY="your-key"
74
- export XAI_API_KEY="your-key"
75
52
  ```
76
-
77
- **Windows** — run in PowerShell (persists across reboots):
78
-
79
- ```powershell
80
- [System.Environment]::SetEnvironmentVariable("FAL_KEY", "your-key", "User")
81
- [System.Environment]::SetEnvironmentVariable("XAI_API_KEY", "your-key", "User")
53
+ 帮我查一下当前已配置好可用的多模态模型有哪些?
82
54
  ```
83
55
 
84
- Then restart Pi (or your terminal) for the variables to take effect.
56
+ Pi 会自动调取对应工具、完成排队轮询与文件下载,并直接返回本地媒体文件的保存路径。
85
57
 
86
- ### Alternative: Config File
58
+ ## ⚙️ Configuration
87
59
 
88
- If you prefer not to use environment variables, create the file `~/.pi/agent/media-models.json`:
60
+ All API keys, custom endpoints, and output paths are configured directly in a single JSON file.
89
61
 
90
- ```json
91
- {
92
- "providerOptions": {
93
- "fal": { "apiKey": "your-fal-key" },
94
- "xai": { "apiKey": "xai-your-key" },
95
- "openai": { "apiKey": "sk-your-key" }
96
- }
97
- }
98
- ```
62
+ **Configuration File Location:**
63
+ - **Global (Recommended):** `~/.pi/agent/media-models.json`
64
+ - **Project-Specific:** `<project_root>/.pi/media-models.json`
99
65
 
100
- Keys in the config file take precedence over environment variables for that provider.
66
+ ### Example `media-models.json`
101
67
 
102
- **Vertex AI** requires a service account JSON file path instead of an API key:
68
+ Create or edit `~/.pi/agent/media-models.json`:
103
69
 
104
70
  ```json
105
71
  {
72
+ "outputDir": "~/.pi/agent/media/outputs",
106
73
  "providerOptions": {
74
+ "fal": {
75
+ "apiKey": "fal-xxxxxxxxxxxxxxxxxxxx"
76
+ },
77
+ "xai": {
78
+ "apiKey": "xai-xxxxxxxxxxxxxxxxxxxx"
79
+ },
80
+ "atlas": {
81
+ "apiKey": "sk-xxxxxxxxxxxxxxxxxxxx"
82
+ },
83
+ "dashscope": {
84
+ "apiKey": "sk-xxxxxxxxxxxxxxxxxxxx"
85
+ },
86
+ "openai": {
87
+ "apiKey": "sk-xxxxxxxxxxxxxxxxxxxx"
88
+ },
89
+ "gemini": {
90
+ "apiKey": "AIzaxxxxxxxxxxxxxxxxxxxx"
91
+ },
92
+ "openrouter": {
93
+ "apiKey": "sk-or-xxxxxxxxxxxxxxxxxxxx"
94
+ },
107
95
  "vertex": {
108
- "credentialsFile": "/path/to/service-account.json",
96
+ "credentialsFile": "/path/to/vertex-service-account.json",
109
97
  "project": "my-gcp-project",
110
98
  "location": "us-central1"
111
99
  }
@@ -113,83 +101,27 @@ Keys in the config file take precedence over environment variables for that prov
113
101
  }
114
102
  ```
115
103
 
116
- ---
117
-
118
- ## Output Files
119
-
120
- All generated media is automatically downloaded and saved to:
121
-
122
- ```
123
- ~/.pi/agent/media/outputs/
124
- ```
125
-
126
- To use a different directory, add `outputDir` to the config file:
127
-
128
- ```json
129
- {
130
- "outputDir": "/Users/alice/Pictures/ai-output"
131
- }
132
- ```
133
-
134
- ---
135
-
136
- ## Supported Providers
137
-
138
- | Provider | Capabilities | Env Var |
139
- |---|---|---|
140
- | **fal.ai** | Images, Video, Audio, TTS, STT | `FAL_KEY` |
141
- | **xAI (Grok Imagine)** | Image gen/edit, T2V, I2V, video extend | `XAI_API_KEY` |
142
- | **OpenAI** | Image gen/edit (DALL-E), TTS, STT (Whisper) | `OPENAI_API_KEY` |
143
- | **Google Gemini** | Imagen, Gemini image, Veo video, Lyria audio, TTS | `GEMINI_API_KEY` |
144
- | **Google Vertex AI** | Same as Gemini, enterprise ADC auth | ADC JSON |
145
- | **DashScope / 百炼** | Qwen image, Wan video, music, TTS/STT | `DASHSCOPE_API_KEY` |
146
- | **QwenCloud** | Same as DashScope (international endpoints) | `DASHSCOPE_API_KEY` |
147
- | **OpenRouter** | Image gen, async video, TTS | `OPENROUTER_API_KEY` |
148
- | **Atlas** | Image gen/edit, video (sync + async) | `ATLAS_API_KEY` |
149
-
150
- To see which providers are active in your current setup, ask Pi:
151
-
152
- ```
153
- What media providers do I have configured?
154
- ```
155
-
156
- ---
157
-
158
- ## Available Tools
159
-
160
- The extension registers exactly 6 tools. Pi selects the correct one automatically based on your request.
161
-
162
- | Tool | Purpose |
163
- |---|---|
164
- | `media_models` | List providers, models, and capabilities |
165
- | `image_generate` | Generate images from text or reference images |
166
- | `image_edit` | Edit an existing image (inpainting, style, background) |
167
- | `video_generate` | Text-to-video, image-to-video, video extend/edit |
168
- | `audio_generate` | Generate music or raw audio |
169
- | `speech_generate` | TTS (text → audio file) or STT (audio → transcript) |
170
-
171
- ---
104
+ > **Note**: You only need to fill in the providers you plan to use. Unused providers can simply be omitted.
172
105
 
173
- ## Advanced: Custom OpenAI-Compatible Providers
106
+ ### Custom OpenAI-Compatible Providers
174
107
 
175
- You can add any OpenAI-compatible API as a custom provider in the config file. Capabilities and endpoints must be declared explicitly — the extension does not probe `/models` to infer them.
108
+ You can connect any third-party or internal OpenAI-compatible media gateway by declaring it in `customProviders`:
176
109
 
177
110
  ```json
178
111
  {
179
112
  "customProviders": [
180
113
  {
181
- "id": "my-gateway",
114
+ "id": "my-custom-ai",
182
115
  "name": "Internal AI Gateway",
183
- "baseUrl": "https://api.internal.example.com/v1",
184
- "apiKeyEnv": "MY_INTERNAL_KEY",
116
+ "baseUrl": "https://api.internal.com/v1",
185
117
  "auth": "bearer",
186
118
  "models": [
187
119
  {
188
- "id": "internal-image-v2",
120
+ "id": "internal-video-pro",
189
121
  "vendor": "internal",
190
- "capabilities": ["image.text_to_image"],
122
+ "capabilities": ["video.text_to_video"],
191
123
  "endpoints": {
192
- "image.text_to_image": "/images/generations"
124
+ "video.text_to_video": "/videos/generations"
193
125
  }
194
126
  }
195
127
  ]
@@ -198,24 +130,45 @@ You can add any OpenAI-compatible API as a custom provider in the config file. C
198
130
  }
199
131
  ```
200
132
 
201
- ---
133
+ ## 🔌 Supported Providers
134
+
135
+ | Provider | Provider ID | Supported Capabilities | Config Option |
136
+ |---|---|---|---|
137
+ | **fal.ai** | `fal` | Images, Video, Audio, TTS, STT (Queue/CDN integration) | `providerOptions.fal.apiKey` |
138
+ | **xAI (Grok Imagine)** | `xai` | Image Gen/Edit, T2V/I2V, Reference-to-Video, Video Extend | `providerOptions.xai.apiKey` |
139
+ | **Atlas API** | `atlas` | Sync/Async Image Gen & Edit, Video LROs | `providerOptions.atlas.apiKey` |
140
+ | **DashScope / 百炼** | `dashscope` | Qwen/Wan Images, Wan Video, Fun-Music, TTS/STT | `providerOptions.dashscope.apiKey` |
141
+ | **QwenCloud** | `qwencloud` | Same as DashScope (International endpoints) | `providerOptions.qwencloud.apiKey` |
142
+ | **OpenAI API** | `openai` | Image Gen/Edit (DALL-E), TTS, STT (Whisper) | `providerOptions.openai.apiKey` |
143
+ | **Google Gemini API** | `gemini` | Gemini/Imagen, Veo, Lyria, TTS, STT | `providerOptions.gemini.apiKey` |
144
+ | **Google Vertex AI** | `vertex` | ADC, Imagen/Gemini, Veo, Lyria, TTS, STT | `providerOptions.vertex.credentialsFile` |
145
+ | **OpenRouter** | `openrouter` | Image Generation, Async Video, TTS | `providerOptions.openrouter.apiKey` |
146
+
147
+ ## 🛠️ Exposed Tools
148
+
149
+ The extension registers exactly 6 unified tools for the reasoning agent:
202
150
 
203
- ## Security
151
+ 1. `media_models`: Lists providers, configured models, and supported capabilities.
152
+ 2. `image_generate`: Generate images from text, image, or multiple reference inputs.
153
+ 3. `image_edit`: Edit existing images (supports masks and multiple references).
154
+ 4. `video_generate`: Generates, edits, or extends videos. Automatically maps inputs (`referenceImages`, `inputVideo`, `duration`, `generateAudio`, etc.) to the provider's exact capability.
155
+ 5. `audio_generate`: Generate music or raw audio (separate from TTS).
156
+ 6. `speech_generate`: Handle TTS (Text-to-Speech) and STT (Speech-to-Text).
204
157
 
205
- - API keys and Bearer tokens are automatically redacted (`[REDACTED]`) from all error messages and logs before being returned to the LLM context.
206
- - The config file (`media-models.json`) is listed in `.gitignore` and is never included in the npm package.
207
- - No telemetry. All requests go directly from your machine to the provider API.
158
+ ## 🔒 Security & Privacy
159
+
160
+ - **No Key Logging**: API keys and Bearer tokens are redacted (`[REDACTED]`) from all error logs and HTTP outputs before being returned to the LLM.
161
+ - **Local Downloads**: Media assets are fetched directly by your local client into your configured `outputDir` without third-party proxies.
162
+ - **Git Ignored**: `media-models.json` is automatically ignored from git repositories to prevent accidental credential commits.
208
163
 
209
164
  ---
210
165
 
211
- ## Updating
166
+ ## 🔄 Updating
212
167
 
213
168
  ```bash
214
169
  pi update npm:pi-media-models
215
170
  ```
216
171
 
217
- ---
218
-
219
- ## License
172
+ ## 📄 License
220
173
 
221
174
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-media-models",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "description": "Provider-neutral multimodal generation tools for Pi Coding Agent",
6
6
  "keywords": [
@@ -82,34 +82,28 @@ All generated media is automatically downloaded to `~/.pi/agent/media/outputs/`.
82
82
 
83
83
  ## Key Configuration
84
84
 
85
- If the user asks how to set up API keys, there are two methods:
86
-
87
- **Environment variables** (any shell or system env):
88
-
89
- | Provider | Variable |
90
- |---|---|
91
- | OpenAI | `OPENAI_API_KEY` |
92
- | fal.ai | `FAL_KEY` |
93
- | Gemini | `GEMINI_API_KEY` |
94
- | Vertex AI | `GOOGLE_APPLICATION_CREDENTIALS` (ADC) |
95
- | xAI | `XAI_API_KEY` |
96
- | DashScope / QwenCloud | `DASHSCOPE_API_KEY` |
97
- | OpenRouter | `OPENROUTER_API_KEY` |
98
- | Atlas | `ATLAS_API_KEY` |
99
-
100
- **Config file** (`~/.pi/agent/media-models.json` or `.pi/media-models.json` in the project root):
85
+ All API keys and provider options are configured in `~/.pi/agent/media-models.json` (or `.pi/media-models.json` in the workspace root):
101
86
 
102
87
  ```json
103
88
  {
104
- "outputDir": "/path/to/output",
89
+ "outputDir": "~/.pi/agent/media/outputs",
105
90
  "providerOptions": {
91
+ "fal": { "apiKey": "fal-..." },
106
92
  "xai": { "apiKey": "xai-..." },
93
+ "atlas": { "apiKey": "sk-..." },
94
+ "dashscope": { "apiKey": "sk-..." },
95
+ "openai": { "apiKey": "sk-..." },
96
+ "gemini": { "apiKey": "AIza..." },
97
+ "openrouter": { "apiKey": "sk-or-..." },
107
98
  "vertex": {
108
99
  "credentialsFile": "/path/to/service-account.json",
109
- "project": "my-gcp-project"
100
+ "project": "my-gcp-project",
101
+ "location": "us-central1"
110
102
  }
111
103
  }
112
104
  }
113
105
  ```
114
106
 
115
- Keys in `providerOptions` take precedence over environment variables for that provider.
107
+ Tell the user to edit `~/.pi/agent/media-models.json` directly to add or update API keys.
108
+
109
+ *(Environment variables such as `FAL_KEY`, `OPENAI_API_KEY`, `XAI_API_KEY` are also checked as a fallback).*
@@ -30,7 +30,7 @@ export abstract class BaseAdapter implements ProviderAdapter {
30
30
 
31
31
  protected key(request: MediaRequest): string {
32
32
  const configKey = request.providerOptions?.apiKey;
33
- const value = (typeof configKey === "string" ? configKey : undefined) ?? (this.envKey ? this.env[this.envKey] : undefined)
33
+ const value = (typeof configKey === "string" && configKey.trim() ? configKey.trim() : undefined) ?? (this.envKey ? this.env[this.envKey] : undefined)
34
34
  if (!value) throw new MediaError('AUTH', `${this.envKey ?? `${this.id} API key`} is not set in environment or config`, { provider: this.id })
35
35
  return value
36
36
  }
@@ -3,6 +3,7 @@ import { fileURLToPath } from 'node:url'
3
3
  import { MediaError } from '../errors.js'
4
4
  import { MediaJob } from '../media-job.js'
5
5
  import { BaseAdapter, artifactsOrThrow, makeModel } from './base.js'
6
+ import { expandHomePath } from '../config.js'
6
7
  import type { AdapterContext, AdapterResult, Capability, JobStatus, JsonObject, MediaRequest, ModelDescriptor } from '../types.js'
7
8
  import type { AdapterDependencies } from './base.js'
8
9
 
@@ -164,7 +165,8 @@ export class GoogleMediaAdapter extends BaseAdapter {
164
165
  const options = request.providerOptions;
165
166
  const configuredFile = typeof options?.credentialsFile === "string" ? options.credentialsFile : undefined
166
167
  const rawKeyFilename = configuredFile ?? this.env.VERTEX_CREDENTIALS_FILE ?? this.env.GOOGLE_APPLICATION_CREDENTIALS
167
- const keyFilename = rawKeyFilename?.startsWith('file://') ? fileURLToPath(rawKeyFilename) : rawKeyFilename
168
+ const expandedKeyFile = expandHomePath(rawKeyFilename)
169
+ const keyFilename = expandedKeyFile?.startsWith('file://') ? fileURLToPath(expandedKeyFile) : expandedKeyFile
168
170
  const auth = new GoogleAuth({
169
171
  scopes: ['https://www.googleapis.com/auth/cloud-platform'],
170
172
  ...(keyFilename ? { keyFilename } : {}),
package/src/config.ts CHANGED
@@ -1,9 +1,18 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { homedir } from 'node:os'
3
- import { join } from 'node:path'
3
+ import { join, resolve } from 'node:path'
4
4
  import { CAPABILITIES, type Capability, type JsonObject } from './types.js'
5
5
  import { MediaError } from './errors.js'
6
6
 
7
+ export function expandHomePath(rawPath: string | undefined): string | undefined {
8
+ if (!rawPath) return undefined
9
+ if (rawPath === '~') return homedir()
10
+ if (rawPath.startsWith('~/') || rawPath.startsWith('~\\')) {
11
+ return join(homedir(), rawPath.slice(2))
12
+ }
13
+ return rawPath
14
+ }
15
+
7
16
  export interface CustomAsyncConfig {
8
17
  idPath: string
9
18
  statusPath: string
@@ -61,8 +70,11 @@ export async function loadMediaConfig(cwd: string, allowProjectConfig: boolean):
61
70
  const globalPath = join(homedir(), '.pi', 'agent', 'media-models.json')
62
71
  const global = await parseFile(globalPath) ?? EMPTY_CONFIG
63
72
  const project = allowProjectConfig ? await parseFile(join(cwd, '.pi', 'media-models.json')) : undefined
73
+ const rawOutputDir = project?.outputDir ?? global.outputDir
74
+ const expandedOutputDir = expandHomePath(rawOutputDir)
75
+ const outputDir = expandedOutputDir ? resolve(cwd, expandedOutputDir) : undefined
64
76
  const merged: MediaConfig = {
65
- outputDir: project?.outputDir ?? global.outputDir,
77
+ outputDir,
66
78
  customProviders: project?.customProviders ?? global.customProviders ?? [],
67
79
  providerOptions: { ...(global.providerOptions ?? {}), ...(project?.providerOptions ?? {}) },
68
80
  }
package/src/router.ts CHANGED
@@ -49,9 +49,15 @@ export class CapabilityRouter {
49
49
  }
50
50
 
51
51
  private isConfigured(adapter: ProviderAdapter): boolean {
52
- if (!adapter.envKey) return true
53
- if (this.env[adapter.envKey]) return true
54
- if (this.providerDefaults[adapter.id]?.apiKey) return true
52
+ if (adapter.id === 'vertex') {
53
+ const vertexOpts = this.providerDefaults['vertex']
54
+ if (typeof vertexOpts?.credentialsFile === 'string' && vertexOpts.credentialsFile.trim()) return true
55
+ if (this.env.GOOGLE_APPLICATION_CREDENTIALS || this.env.VERTEX_CREDENTIALS_FILE) return true
56
+ return false
57
+ }
58
+ const configKey = this.providerDefaults[adapter.id]?.apiKey
59
+ if (typeof configKey === 'string' && configKey.trim()) return true
60
+ if (adapter.envKey && this.env[adapter.envKey]) return true
55
61
  return false
56
62
  }
57
63