@victor-software-house/pi-openai-proxy 0.0.3 → 0.1.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 +190 -63
- package/extensions/proxy.ts +222 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
# pi-openai-proxy
|
|
2
2
|
|
|
3
|
-
A local OpenAI-compatible HTTP proxy built on [pi](https://github.com/badlogic/pi-mono)'s SDK. Routes requests through pi's multi-provider model registry and credential management, exposing a single `http://localhost
|
|
4
|
-
|
|
5
|
-
## Project docs
|
|
6
|
-
|
|
7
|
-
- `README.md` -- project overview and API surface
|
|
8
|
-
- `ROADMAP.md` -- short phase summary and delivery order
|
|
9
|
-
- `PLAN.md` -- detailed implementation contract (internal)
|
|
3
|
+
A local OpenAI-compatible HTTP proxy built on [pi](https://github.com/badlogic/pi-mono)'s SDK. Routes requests through pi's multi-provider model registry and credential management, exposing a single `http://localhost:4141/v1/...` endpoint that any OpenAI-compatible client can connect to.
|
|
10
4
|
|
|
11
5
|
## Why
|
|
12
6
|
|
|
@@ -14,70 +8,122 @@ A local OpenAI-compatible HTTP proxy built on [pi](https://github.com/badlogic/p
|
|
|
14
8
|
- **No duplicate config** -- reuses pi's `~/.pi/agent/auth.json` and `models.json` for credentials and model definitions
|
|
15
9
|
- **Self-hosted** -- runs locally, no third-party proxy services
|
|
16
10
|
- **Streaming** -- full SSE streaming with token usage and cost tracking
|
|
17
|
-
- **
|
|
11
|
+
- **Strict validation** -- unsupported parameters are rejected clearly, not silently ignored
|
|
18
12
|
|
|
19
|
-
##
|
|
13
|
+
## Prerequisites
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
| `GET /v1/models/{model}` | Implemented | Model details for a canonical model ID (supports URL-encoded IDs with `/`) |
|
|
25
|
-
| `POST /v1/chat/completions` | Implemented | Chat completions (streaming and non-streaming) |
|
|
15
|
+
1. [pi](https://github.com/badlogic/pi-mono) must be installed
|
|
16
|
+
2. At least one provider must be configured via `pi /login`
|
|
17
|
+
3. [Bun](https://bun.sh) (for development) or [Node.js](https://nodejs.org) >= 20 (for production)
|
|
26
18
|
|
|
27
|
-
##
|
|
19
|
+
## Installation
|
|
28
20
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
| `messages` (text) | Implemented | System, developer, user, assistant, tool messages |
|
|
33
|
-
| `messages` (base64 images) | Implemented | Base64 data URI image content parts |
|
|
34
|
-
| `messages` (remote images) | Rejected | Disabled by default; returns clear error |
|
|
35
|
-
| `stream` | Implemented | SSE with `text_delta` / `toolcall_delta` mapping |
|
|
36
|
-
| `temperature` | Implemented | Direct passthrough to `StreamOptions` |
|
|
37
|
-
| `max_tokens` / `max_completion_tokens` | Implemented | Normalized to `StreamOptions.maxTokens` |
|
|
38
|
-
| `stop` sequences | Implemented | Via `onPayload` passthrough |
|
|
39
|
-
| `user` | Implemented | Via `onPayload` passthrough |
|
|
40
|
-
| `stream_options.include_usage` | Implemented | Final usage chunk in SSE stream |
|
|
41
|
-
| `tools` / `tool_choice` | Implemented | JSON Schema -> TypeBox conversion (supported subset) |
|
|
42
|
-
| `tool_calls` in messages | Implemented | Assistant tool call + tool result roundtrip |
|
|
43
|
-
| `reasoning_effort` | Implemented | Maps to pi's `ThinkingLevel` (`low`, `medium`, `high`) |
|
|
44
|
-
| `response_format` | Implemented | `text` and `json_object` via `onPayload` passthrough |
|
|
45
|
-
| `top_p` | Implemented | Via `onPayload` passthrough |
|
|
46
|
-
| `frequency_penalty` | Implemented | Via `onPayload` passthrough |
|
|
47
|
-
| `presence_penalty` | Implemented | Via `onPayload` passthrough |
|
|
48
|
-
| `seed` | Implemented | Via `onPayload` passthrough |
|
|
49
|
-
| `n > 1` | Not planned | Pi streams one completion at a time |
|
|
50
|
-
| `logprobs` | Not planned | Not in pi-ai's abstraction layer |
|
|
21
|
+
```bash
|
|
22
|
+
# Install globally
|
|
23
|
+
npm install -g @victor-software-house/pi-openai-proxy
|
|
51
24
|
|
|
52
|
-
|
|
25
|
+
# Or run directly with npx
|
|
26
|
+
npx @victor-software-house/pi-openai-proxy
|
|
27
|
+
```
|
|
53
28
|
|
|
29
|
+
## Quickstart
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Start the proxy (defaults to http://127.0.0.1:4141)
|
|
33
|
+
pi-openai-proxy
|
|
54
34
|
```
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
+--completions------>| +-- Message converter |
|
|
61
|
-
| | +-- Model resolver |
|
|
62
|
-
| GET /v1/models | +-- Tool converter |
|
|
63
|
-
+------------------>| +-- SSE encoder |
|
|
64
|
-
| | |
|
|
65
|
-
| | Pi SDK |
|
|
66
|
-
| SSE / JSON | +-- ModelRegistry |
|
|
67
|
-
|<------------------+ +-- AuthStorage |
|
|
68
|
-
| +-- streamSimple() |
|
|
69
|
-
| +-- AgentSession (P4) |
|
|
70
|
-
+--------------------------+
|
|
35
|
+
|
|
36
|
+
### List available models
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
curl http://localhost:4141/v1/models | jq '.data[].id'
|
|
71
40
|
```
|
|
72
41
|
|
|
73
|
-
###
|
|
42
|
+
### Chat completion (non-streaming)
|
|
74
43
|
|
|
75
|
-
|
|
76
|
-
|
|
44
|
+
```bash
|
|
45
|
+
curl http://localhost:4141/v1/chat/completions \
|
|
46
|
+
-H "Content-Type: application/json" \
|
|
47
|
+
-d '{
|
|
48
|
+
"model": "anthropic/claude-sonnet-4-20250514",
|
|
49
|
+
"messages": [{"role": "user", "content": "Hello!"}]
|
|
50
|
+
}'
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Chat completion (streaming)
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
curl http://localhost:4141/v1/chat/completions \
|
|
57
|
+
-H "Content-Type: application/json" \
|
|
58
|
+
-d '{
|
|
59
|
+
"model": "openai/gpt-4o",
|
|
60
|
+
"messages": [{"role": "user", "content": "Tell me a joke"}],
|
|
61
|
+
"stream": true
|
|
62
|
+
}'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Use with any OpenAI-compatible client
|
|
66
|
+
|
|
67
|
+
Point any client that supports `OPENAI_API_BASE` (or equivalent) at `http://localhost:4141/v1`:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Example: Aider
|
|
71
|
+
OPENAI_API_BASE=http://localhost:4141/v1 aider --model anthropic/claude-sonnet-4-20250514
|
|
72
|
+
|
|
73
|
+
# Example: Continue (in settings.json)
|
|
74
|
+
# "apiBase": "http://localhost:4141/v1"
|
|
75
|
+
|
|
76
|
+
# Example: Open WebUI
|
|
77
|
+
# Set "OpenAI API Base URL" to http://localhost:4141/v1
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### Shorthand model names
|
|
81
|
+
|
|
82
|
+
If a model ID is unique across providers, you can omit the provider prefix:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
curl http://localhost:4141/v1/chat/completions \
|
|
86
|
+
-H "Content-Type: application/json" \
|
|
87
|
+
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]}'
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Ambiguous shorthand requests fail with a clear error listing the matching canonical IDs.
|
|
91
|
+
|
|
92
|
+
## Supported Endpoints
|
|
93
|
+
|
|
94
|
+
| Endpoint | Description |
|
|
95
|
+
|---|---|
|
|
96
|
+
| `GET /v1/models` | List all available models (only those with configured credentials) |
|
|
97
|
+
| `GET /v1/models/{model}` | Model details by canonical ID (supports URL-encoded IDs with `/`) |
|
|
98
|
+
| `POST /v1/chat/completions` | Chat completions (streaming and non-streaming) |
|
|
99
|
+
|
|
100
|
+
## Supported Chat Completions Features
|
|
101
|
+
|
|
102
|
+
| Feature | Notes |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `model` | Canonical (`provider/model-id`) or unique shorthand |
|
|
105
|
+
| `messages` (text) | `system`, `developer`, `user`, `assistant`, `tool` roles |
|
|
106
|
+
| `messages` (base64 images) | Base64 data URI image content parts (`image/png`, `image/jpeg`, `image/gif`, `image/webp`) |
|
|
107
|
+
| `stream` | SSE with `text_delta` and `toolcall_delta` mapping |
|
|
108
|
+
| `temperature` | Direct passthrough |
|
|
109
|
+
| `max_tokens` / `max_completion_tokens` | Normalized to `maxTokens` |
|
|
110
|
+
| `stop` | Via passthrough |
|
|
111
|
+
| `user` | Via passthrough |
|
|
112
|
+
| `stream_options.include_usage` | Final usage chunk in SSE stream |
|
|
113
|
+
| `tools` / `tool_choice` | JSON Schema -> TypeBox conversion (supported subset) |
|
|
114
|
+
| `tool_calls` in messages | Assistant tool call + tool result roundtrip |
|
|
115
|
+
| `reasoning_effort` | Maps to pi's `ThinkingLevel` (`low`, `medium`, `high`) |
|
|
116
|
+
| `response_format` | `text` and `json_object` via passthrough |
|
|
117
|
+
| `top_p` | Via passthrough |
|
|
118
|
+
| `frequency_penalty` | Via passthrough |
|
|
119
|
+
| `presence_penalty` | Via passthrough |
|
|
120
|
+
| `seed` | Via passthrough |
|
|
121
|
+
|
|
122
|
+
**Not supported:** `n > 1`, `logprobs`, `logit_bias`, remote image URLs (disabled by default).
|
|
77
123
|
|
|
78
124
|
## Model Naming
|
|
79
125
|
|
|
80
|
-
Models
|
|
126
|
+
Models use the `provider/model-id` canonical format, matching pi's registry:
|
|
81
127
|
|
|
82
128
|
```
|
|
83
129
|
anthropic/claude-sonnet-4-20250514
|
|
@@ -87,15 +133,12 @@ xai/grok-3
|
|
|
87
133
|
openrouter/anthropic/claude-sonnet-4-20250514
|
|
88
134
|
```
|
|
89
135
|
|
|
90
|
-
Shorthand (bare model ID) is resolved by scanning all providers for a unique match. Ambiguous shorthand requests fail with a clear error listing the matching canonical IDs.
|
|
91
|
-
|
|
92
136
|
## Configuration
|
|
93
137
|
|
|
94
|
-
|
|
138
|
+
The proxy reuses pi's existing configuration:
|
|
95
139
|
|
|
96
140
|
- **API keys**: `~/.pi/agent/auth.json` (managed by `pi /login`)
|
|
97
141
|
- **Custom models**: `~/.pi/agent/models.json`
|
|
98
|
-
- **Per-request override**: `X-Pi-Upstream-Api-Key` header overrides the registry-resolved API key for a single request, keeping `Authorization` available for proxy authentication
|
|
99
142
|
|
|
100
143
|
### Environment Variables
|
|
101
144
|
|
|
@@ -104,11 +147,95 @@ Uses pi's existing configuration:
|
|
|
104
147
|
| `PI_PROXY_HOST` | `127.0.0.1` | Bind address |
|
|
105
148
|
| `PI_PROXY_PORT` | `4141` | Listen port |
|
|
106
149
|
| `PI_PROXY_AUTH_TOKEN` | (disabled) | Bearer token for proxy authentication |
|
|
107
|
-
| `PI_PROXY_AGENTIC` | `false` | Enable experimental agentic mode |
|
|
108
150
|
| `PI_PROXY_REMOTE_IMAGES` | `false` | Enable remote image URL fetching |
|
|
109
151
|
| `PI_PROXY_MAX_BODY_SIZE` | `52428800` (50 MB) | Maximum request body size in bytes |
|
|
110
152
|
| `PI_PROXY_UPSTREAM_TIMEOUT_MS` | `120000` (120s) | Upstream request timeout in milliseconds |
|
|
111
153
|
|
|
154
|
+
### Per-request API key override
|
|
155
|
+
|
|
156
|
+
The `X-Pi-Upstream-Api-Key` header overrides the registry-resolved API key for a single request. This keeps `Authorization` available for proxy authentication:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
curl http://localhost:4141/v1/chat/completions \
|
|
160
|
+
-H "Content-Type: application/json" \
|
|
161
|
+
-H "X-Pi-Upstream-Api-Key: sk-your-key-here" \
|
|
162
|
+
-d '{"model": "openai/gpt-4o", "messages": [{"role": "user", "content": "Hi"}]}'
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Proxy authentication
|
|
166
|
+
|
|
167
|
+
Set `PI_PROXY_AUTH_TOKEN` to require a bearer token for all requests:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
PI_PROXY_AUTH_TOKEN=my-secret-token pi-openai-proxy
|
|
171
|
+
|
|
172
|
+
# Clients must include the token
|
|
173
|
+
curl http://localhost:4141/v1/models \
|
|
174
|
+
-H "Authorization: Bearer my-secret-token"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Pi Integration
|
|
178
|
+
|
|
179
|
+
Install as a pi package to get the `/proxy` command and `--proxy` flag inside pi sessions:
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
pi install npm:@victor-software-house/pi-openai-proxy
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Start the proxy from inside pi
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
/proxy start Start the proxy server
|
|
189
|
+
/proxy stop Stop the proxy server
|
|
190
|
+
/proxy status Show proxy status (default)
|
|
191
|
+
/proxy Show proxy status
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Auto-start with pi
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
pi --proxy
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The proxy starts automatically on session start and stops when the session ends. A status indicator in the footer shows the proxy URL and model count.
|
|
201
|
+
|
|
202
|
+
The proxy can also run standalone (see [Installation](#installation) above). The extension detects externally running instances and shows their status without trying to manage them.
|
|
203
|
+
|
|
204
|
+
## Architecture
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
HTTP Client pi-openai-proxy
|
|
208
|
+
(curl, Aider, Continue, +--------------------------+
|
|
209
|
+
LiteLLM, Open WebUI, etc.) | |
|
|
210
|
+
| | Hono HTTP Server |
|
|
211
|
+
| POST /v1/chat/ | +-- Request parser |
|
|
212
|
+
+--completions------>| +-- Message converter |
|
|
213
|
+
| | +-- Model resolver |
|
|
214
|
+
| GET /v1/models | +-- Tool converter |
|
|
215
|
+
+------------------>| +-- SSE encoder |
|
|
216
|
+
| | |
|
|
217
|
+
| | Pi SDK |
|
|
218
|
+
| SSE / JSON | +-- ModelRegistry |
|
|
219
|
+
|<------------------+ +-- AuthStorage |
|
|
220
|
+
| +-- streamSimple() |
|
|
221
|
+
| +-- completeSimple() |
|
|
222
|
+
+--------------------------+
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Pi SDK layers used
|
|
226
|
+
|
|
227
|
+
- **`@mariozechner/pi-ai`** -- `streamSimple()`, `completeSimple()`, `Model`, `Usage`, `AssistantMessageEvent`
|
|
228
|
+
- **`@mariozechner/pi-coding-agent`** -- `ModelRegistry`, `AuthStorage`
|
|
229
|
+
|
|
230
|
+
## Security defaults
|
|
231
|
+
|
|
232
|
+
- Binds to `127.0.0.1` (localhost only) by default
|
|
233
|
+
- Remote image URLs disabled by default
|
|
234
|
+
- Request body size limited to 50 MB
|
|
235
|
+
- Upstream timeout of 120 seconds
|
|
236
|
+
- Secrets are never included in error responses
|
|
237
|
+
- Client disconnects abort upstream work immediately
|
|
238
|
+
|
|
112
239
|
## Dev Workflow
|
|
113
240
|
|
|
114
241
|
```bash
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi extension: /proxy command and --proxy flag.
|
|
3
|
+
*
|
|
4
|
+
* Manages the pi-openai-proxy server from inside a pi session.
|
|
5
|
+
*
|
|
6
|
+
* - /proxy Show status
|
|
7
|
+
* - /proxy start Start the proxy server
|
|
8
|
+
* - /proxy stop Stop the proxy server (session-managed only)
|
|
9
|
+
* - /proxy status Show status
|
|
10
|
+
* - --proxy Auto-start on session start
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
14
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
15
|
+
import { resolve, dirname } from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
export default function proxyExtension(pi: ExtensionAPI) {
|
|
19
|
+
let proxyProcess: ChildProcess | undefined;
|
|
20
|
+
|
|
21
|
+
const host = process.env["PI_PROXY_HOST"] ?? "127.0.0.1";
|
|
22
|
+
const port = process.env["PI_PROXY_PORT"] ?? "4141";
|
|
23
|
+
const proxyUrl = `http://${host}:${port}`;
|
|
24
|
+
|
|
25
|
+
const extensionDir = dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
const packageRoot = resolve(extensionDir, "..");
|
|
27
|
+
const proxyEntry = resolve(packageRoot, "dist", "index.mjs");
|
|
28
|
+
|
|
29
|
+
// --- Flag: --proxy ---
|
|
30
|
+
|
|
31
|
+
pi.registerFlag("proxy", {
|
|
32
|
+
description: "Start the OpenAI proxy on session start",
|
|
33
|
+
type: "boolean",
|
|
34
|
+
default: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// --- Lifecycle ---
|
|
38
|
+
|
|
39
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
40
|
+
if (pi.getFlag("--proxy")) {
|
|
41
|
+
await startProxy(ctx);
|
|
42
|
+
} else {
|
|
43
|
+
await refreshStatus(ctx);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("session_shutdown", async () => {
|
|
48
|
+
killProxy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// --- Command: /proxy ---
|
|
52
|
+
|
|
53
|
+
pi.registerCommand("proxy", {
|
|
54
|
+
description: "Manage the OpenAI-compatible proxy (start/stop/status)",
|
|
55
|
+
getArgumentCompletions: (prefix) => {
|
|
56
|
+
const subs = [
|
|
57
|
+
{ value: "start", label: "Start the proxy server" },
|
|
58
|
+
{ value: "stop", label: "Stop the proxy server" },
|
|
59
|
+
{ value: "status", label: "Show proxy status" },
|
|
60
|
+
];
|
|
61
|
+
if (prefix.length === 0) return subs;
|
|
62
|
+
return subs.filter((s) => s.value.startsWith(prefix));
|
|
63
|
+
},
|
|
64
|
+
handler: async (args, ctx) => {
|
|
65
|
+
const sub = args.trim().split(/\s+/)[0] ?? "";
|
|
66
|
+
|
|
67
|
+
switch (sub) {
|
|
68
|
+
case "":
|
|
69
|
+
case "status":
|
|
70
|
+
await showStatus(ctx);
|
|
71
|
+
break;
|
|
72
|
+
case "start":
|
|
73
|
+
await startProxy(ctx);
|
|
74
|
+
break;
|
|
75
|
+
case "stop":
|
|
76
|
+
await stopProxy(ctx);
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
ctx.ui.notify("/proxy [start|stop|status]", "info");
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// --- Proxy management ---
|
|
85
|
+
|
|
86
|
+
async function probe(): Promise<{ reachable: boolean; models: number }> {
|
|
87
|
+
try {
|
|
88
|
+
const res = await fetch(`${proxyUrl}/v1/models`, {
|
|
89
|
+
signal: AbortSignal.timeout(2000),
|
|
90
|
+
});
|
|
91
|
+
if (res.ok) {
|
|
92
|
+
const body = (await res.json()) as { data?: unknown[] };
|
|
93
|
+
return { reachable: true, models: body.data?.length ?? 0 };
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
// not reachable
|
|
97
|
+
}
|
|
98
|
+
return { reachable: false, models: 0 };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function refreshStatus(ctx: ExtensionContext): Promise<void> {
|
|
102
|
+
const status = await probe();
|
|
103
|
+
if (status.reachable) {
|
|
104
|
+
ctx.ui.setStatus("proxy", `proxy: ${proxyUrl} (${String(status.models)} models)`);
|
|
105
|
+
} else if (proxyProcess !== undefined) {
|
|
106
|
+
ctx.ui.setStatus("proxy", "proxy: starting...");
|
|
107
|
+
} else {
|
|
108
|
+
ctx.ui.setStatus("proxy", undefined);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function startProxy(ctx: ExtensionContext): Promise<void> {
|
|
113
|
+
const status = await probe();
|
|
114
|
+
if (status.reachable) {
|
|
115
|
+
ctx.ui.notify(
|
|
116
|
+
`Proxy already running at ${proxyUrl} (${String(status.models)} models)`,
|
|
117
|
+
"info",
|
|
118
|
+
);
|
|
119
|
+
await refreshStatus(ctx);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (proxyProcess !== undefined) {
|
|
124
|
+
ctx.ui.notify("Proxy is already starting...", "info");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
ctx.ui.setStatus("proxy", "proxy: starting...");
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
proxyProcess = spawn("bun", ["run", proxyEntry], {
|
|
132
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
+
detached: false,
|
|
134
|
+
env: { ...process.env },
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
proxyProcess.on("exit", (code) => {
|
|
138
|
+
proxyProcess = undefined;
|
|
139
|
+
if (code !== null && code !== 0) {
|
|
140
|
+
ctx.ui.notify(`Proxy exited with code ${String(code)}`, "warn");
|
|
141
|
+
}
|
|
142
|
+
ctx.ui.setStatus("proxy", undefined);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
proxyProcess.on("error", (err) => {
|
|
146
|
+
proxyProcess = undefined;
|
|
147
|
+
ctx.ui.notify(`Failed to start proxy: ${err.message}`, "error");
|
|
148
|
+
ctx.ui.setStatus("proxy", undefined);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Wait for the server to become reachable
|
|
152
|
+
const ready = await waitForReady(3000);
|
|
153
|
+
if (ready.reachable) {
|
|
154
|
+
ctx.ui.notify(
|
|
155
|
+
`Proxy started at ${proxyUrl} (${String(ready.models)} models)`,
|
|
156
|
+
"info",
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
ctx.ui.notify(`Proxy spawned but not yet reachable at ${proxyUrl}`, "warn");
|
|
160
|
+
}
|
|
161
|
+
await refreshStatus(ctx);
|
|
162
|
+
} catch (err: unknown) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
ctx.ui.notify(`Failed to start proxy: ${message}`, "error");
|
|
165
|
+
ctx.ui.setStatus("proxy", undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function stopProxy(ctx: ExtensionContext): Promise<void> {
|
|
170
|
+
if (proxyProcess !== undefined) {
|
|
171
|
+
killProxy();
|
|
172
|
+
ctx.ui.notify("Proxy stopped", "info");
|
|
173
|
+
ctx.ui.setStatus("proxy", undefined);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const status = await probe();
|
|
178
|
+
if (status.reachable) {
|
|
179
|
+
ctx.ui.notify(
|
|
180
|
+
`Proxy at ${proxyUrl} is running externally (not managed by this session)`,
|
|
181
|
+
"info",
|
|
182
|
+
);
|
|
183
|
+
} else {
|
|
184
|
+
ctx.ui.notify("Proxy is not running", "info");
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function showStatus(ctx: ExtensionContext): Promise<void> {
|
|
189
|
+
const status = await probe();
|
|
190
|
+
const managed = proxyProcess !== undefined ? " (managed)" : " (external)";
|
|
191
|
+
|
|
192
|
+
if (status.reachable) {
|
|
193
|
+
ctx.ui.notify(
|
|
194
|
+
`${proxyUrl}${managed} -- ${String(status.models)} models available`,
|
|
195
|
+
"info",
|
|
196
|
+
);
|
|
197
|
+
} else {
|
|
198
|
+
ctx.ui.notify("Proxy not running. Use /proxy start or pi --proxy", "info");
|
|
199
|
+
}
|
|
200
|
+
await refreshStatus(ctx);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function killProxy(): void {
|
|
204
|
+
if (proxyProcess !== undefined) {
|
|
205
|
+
proxyProcess.kill("SIGTERM");
|
|
206
|
+
proxyProcess = undefined;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function waitForReady(
|
|
211
|
+
timeoutMs: number,
|
|
212
|
+
): Promise<{ reachable: boolean; models: number }> {
|
|
213
|
+
const start = Date.now();
|
|
214
|
+
const interval = 300;
|
|
215
|
+
while (Date.now() - start < timeoutMs) {
|
|
216
|
+
const status = await probe();
|
|
217
|
+
if (status.reachable) return status;
|
|
218
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
219
|
+
}
|
|
220
|
+
return { reachable: false, models: 0 };
|
|
221
|
+
}
|
|
222
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@victor-software-house/pi-openai-proxy",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Local OpenAI-compatible HTTP proxy built on pi's SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Victor Software House",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"llm",
|
|
21
21
|
"gateway"
|
|
22
22
|
],
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extensions"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
23
28
|
"engines": {
|
|
24
29
|
"node": ">=20"
|
|
25
30
|
},
|
|
@@ -29,7 +34,8 @@
|
|
|
29
34
|
"pi-openai-proxy": "dist/index.mjs"
|
|
30
35
|
},
|
|
31
36
|
"files": [
|
|
32
|
-
"dist"
|
|
37
|
+
"dist",
|
|
38
|
+
"extensions"
|
|
33
39
|
],
|
|
34
40
|
"scripts": {
|
|
35
41
|
"dev": "bun src/index.ts",
|