@victor-software-house/pi-openai-proxy 0.3.1 → 2.0.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 +53 -19
- package/dist/config.d.mts +59 -0
- package/dist/config.mjs +118 -0
- package/dist/index.d.mts +1 -10
- package/dist/index.mjs +490 -3029
- package/extensions/proxy.ts +64 -152
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -77,9 +77,14 @@ OPENAI_API_BASE=http://localhost:4141/v1 aider --model anthropic/claude-sonnet-4
|
|
|
77
77
|
# Set "OpenAI API Base URL" to http://localhost:4141/v1
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
###
|
|
80
|
+
### Model resolution
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
The proxy resolves model references in this order:
|
|
83
|
+
|
|
84
|
+
1. **Exact public ID match** -- the ID from `GET /v1/models`
|
|
85
|
+
2. **Canonical ID fallback** -- `provider/model-id` format (only for exposed models)
|
|
86
|
+
|
|
87
|
+
With the default `collision-prefixed` mode and no collisions, model IDs are exposed without prefixes:
|
|
83
88
|
|
|
84
89
|
```bash
|
|
85
90
|
curl http://localhost:4141/v1/chat/completions \
|
|
@@ -87,21 +92,19 @@ curl http://localhost:4141/v1/chat/completions \
|
|
|
87
92
|
-d '{"model": "gpt-4o", "messages": [{"role": "user", "content": "Hi"}]}'
|
|
88
93
|
```
|
|
89
94
|
|
|
90
|
-
Ambiguous shorthand requests fail with a clear error listing the matching canonical IDs.
|
|
91
|
-
|
|
92
95
|
## Supported Endpoints
|
|
93
96
|
|
|
94
97
|
| Endpoint | Description |
|
|
95
98
|
|---|---|
|
|
96
|
-
| `GET /v1/models` | List
|
|
97
|
-
| `GET /v1/models/{model}` | Model details by canonical ID (supports URL-encoded IDs with `/`) |
|
|
99
|
+
| `GET /v1/models` | List exposed models (filtered by exposure mode, only those with configured credentials) |
|
|
100
|
+
| `GET /v1/models/{model}` | Model details by public ID or canonical ID (supports URL-encoded IDs with `/`) |
|
|
98
101
|
| `POST /v1/chat/completions` | Chat completions (streaming and non-streaming) |
|
|
99
102
|
|
|
100
103
|
## Supported Chat Completions Features
|
|
101
104
|
|
|
102
105
|
| Feature | Notes |
|
|
103
106
|
|---|---|
|
|
104
|
-
| `model` |
|
|
107
|
+
| `model` | Public ID, canonical (`provider/model-id`), or collision-prefixed shorthand |
|
|
105
108
|
| `messages` (text) | `system`, `developer`, `user`, `assistant`, `tool` roles |
|
|
106
109
|
| `messages` (base64 images) | Base64 data URI image content parts (`image/png`, `image/jpeg`, `image/gif`, `image/webp`) |
|
|
107
110
|
| `stream` | SSE with `text_delta` and `toolcall_delta` mapping |
|
|
@@ -112,8 +115,8 @@ Ambiguous shorthand requests fail with a clear error listing the matching canoni
|
|
|
112
115
|
| `stream_options.include_usage` | Final usage chunk in SSE stream |
|
|
113
116
|
| `tools` / `tool_choice` | JSON Schema -> TypeBox conversion (supported subset) |
|
|
114
117
|
| `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 `
|
|
118
|
+
| `reasoning_effort` | Maps to pi's `ThinkingLevel` (`none`, `minimal`, `low`, `medium`, `high`, `xhigh`) |
|
|
119
|
+
| `response_format` | `text`, `json_object`, and `json_schema` via passthrough |
|
|
117
120
|
| `top_p` | Via passthrough |
|
|
118
121
|
| `frequency_penalty` | Via passthrough |
|
|
119
122
|
| `presence_penalty` | Via passthrough |
|
|
@@ -121,9 +124,33 @@ Ambiguous shorthand requests fail with a clear error listing the matching canoni
|
|
|
121
124
|
|
|
122
125
|
**Not supported:** `n > 1`, `logprobs`, `logit_bias`, remote image URLs (disabled by default).
|
|
123
126
|
|
|
124
|
-
## Model Naming
|
|
127
|
+
## Model Naming and Exposure
|
|
128
|
+
|
|
129
|
+
### Public model IDs
|
|
130
|
+
|
|
131
|
+
The proxy generates public model IDs based on a configurable ID mode:
|
|
132
|
+
|
|
133
|
+
| Mode | Behavior | Example |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `collision-prefixed` (default) | Raw model IDs; prefix only providers that share a model name | `gpt-4o` or `openai/gpt-4o` if `codex` also has `gpt-4o` |
|
|
136
|
+
| `universal` | Raw model IDs only; rejects config if duplicates exist | `gpt-4o`, `claude-sonnet-4-20250514` |
|
|
137
|
+
| `always-prefixed` | Always `<prefix>/<model-id>` | `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514` |
|
|
138
|
+
|
|
139
|
+
The `collision-prefixed` mode prefixes **all** models from providers that form a connected conflict group (not just the colliding model names).
|
|
140
|
+
|
|
141
|
+
### Exposure modes
|
|
142
|
+
|
|
143
|
+
Control which models appear in the API:
|
|
125
144
|
|
|
126
|
-
|
|
145
|
+
| Mode | Behavior |
|
|
146
|
+
|---|---|
|
|
147
|
+
| `all` (default) | Expose every model with configured credentials |
|
|
148
|
+
| `scoped` | Expose models from selected providers only (`scopedProviders`) |
|
|
149
|
+
| `custom` | Expose an explicit allowlist of canonical IDs (`customModels`) |
|
|
150
|
+
|
|
151
|
+
### Canonical model IDs
|
|
152
|
+
|
|
153
|
+
Internal canonical IDs use the `provider/model-id` format matching pi's registry:
|
|
127
154
|
|
|
128
155
|
```
|
|
129
156
|
anthropic/claude-sonnet-4-20250514
|
|
@@ -133,6 +160,8 @@ xai/grok-3
|
|
|
133
160
|
openrouter/anthropic/claude-sonnet-4-20250514
|
|
134
161
|
```
|
|
135
162
|
|
|
163
|
+
Canonical IDs are accepted as backward-compatible fallback in requests, but only for models that are currently exposed. Hidden models cannot be reached by canonical ID.
|
|
164
|
+
|
|
136
165
|
## Configuration
|
|
137
166
|
|
|
138
167
|
### What comes from pi
|
|
@@ -159,22 +188,27 @@ Proxy-specific settings are configured via environment variables or the `/proxy
|
|
|
159
188
|
| Max body size | `PI_PROXY_MAX_BODY_SIZE` | `52428800` (50 MB) | Maximum request body size in bytes |
|
|
160
189
|
| Upstream timeout | `PI_PROXY_UPSTREAM_TIMEOUT_MS` | `120000` (120s) | Upstream request timeout in milliseconds |
|
|
161
190
|
|
|
162
|
-
|
|
191
|
+
Model exposure settings are configured via the JSON config file (`~/.pi/agent/proxy-config.json`):
|
|
192
|
+
|
|
193
|
+
| Setting | Default | Description |
|
|
194
|
+
|---|---|---|
|
|
195
|
+
| `publicModelIdMode` | `collision-prefixed` | Public ID format: `collision-prefixed`, `universal`, `always-prefixed` |
|
|
196
|
+
| `modelExposureMode` | `all` | Which models to expose: `all`, `scoped`, `custom` |
|
|
197
|
+
| `scopedProviders` | `[]` | Provider keys for `scoped` mode |
|
|
198
|
+
| `customModels` | `[]` | Canonical model IDs for `custom` mode |
|
|
199
|
+
| `providerPrefixes` | `{}` | Provider key -> custom prefix label overrides |
|
|
200
|
+
|
|
201
|
+
When used as a pi package, all settings are persisted in `~/.pi/agent/proxy-config.json` and applied when the extension spawns the proxy.
|
|
163
202
|
|
|
164
203
|
### Discovering available models
|
|
165
204
|
|
|
166
|
-
List all models the proxy
|
|
205
|
+
List all models the proxy exposes (filtered by the active exposure mode):
|
|
167
206
|
|
|
168
207
|
```bash
|
|
169
208
|
curl http://localhost:4141/v1/models | jq '.data[].id'
|
|
170
209
|
```
|
|
171
210
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
curl http://localhost:4141/v1/models/anthropic%2Fclaude-sonnet-4-20250514 | jq '.x_pi'
|
|
176
|
-
# { "api": "anthropic", "reasoning": true, "input": ["text", "image"], ... }
|
|
177
|
-
```
|
|
211
|
+
Model objects follow the standard OpenAI shape (`id`, `object`, `created`, `owned_by`) with no proprietary extensions.
|
|
178
212
|
|
|
179
213
|
### Per-request API key override
|
|
180
214
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/config/schema.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Proxy configuration schema -- single source of truth.
|
|
5
|
+
*
|
|
6
|
+
* Used by both the proxy server and the pi extension.
|
|
7
|
+
* The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
|
|
8
|
+
* The pi extension reads and writes the JSON config file via the /proxy config panel.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* How public model IDs are generated from canonical provider/model-id pairs.
|
|
12
|
+
*
|
|
13
|
+
* - "collision-prefixed": prefix only providers in conflict groups that share a raw model ID
|
|
14
|
+
* - "universal": expose raw model IDs only; duplicates are a config error
|
|
15
|
+
* - "always-prefixed": always expose <prefix>/<model-id> for every model
|
|
16
|
+
*/
|
|
17
|
+
type PublicModelIdMode = "collision-prefixed" | "universal" | "always-prefixed";
|
|
18
|
+
/**
|
|
19
|
+
* Which models are exposed on the public HTTP API.
|
|
20
|
+
*
|
|
21
|
+
* - "all": every available model
|
|
22
|
+
* - "scoped": all available models from selected providers only
|
|
23
|
+
* - "custom": explicit allowlist of canonical model IDs
|
|
24
|
+
*/
|
|
25
|
+
type ModelExposureMode = "all" | "scoped" | "custom";
|
|
26
|
+
interface ProxyConfig {
|
|
27
|
+
/** Bind address. Default: "127.0.0.1" */
|
|
28
|
+
readonly host: string;
|
|
29
|
+
/** Listen port. Default: 4141 */
|
|
30
|
+
readonly port: number;
|
|
31
|
+
/** Bearer token for proxy auth. Empty string = disabled. */
|
|
32
|
+
readonly authToken: string;
|
|
33
|
+
/** Allow remote image URL fetching. Default: false */
|
|
34
|
+
readonly remoteImages: boolean;
|
|
35
|
+
/** Max request body in MB. Default: 50 */
|
|
36
|
+
readonly maxBodySizeMb: number;
|
|
37
|
+
/** Upstream timeout in seconds. Default: 120 */
|
|
38
|
+
readonly upstreamTimeoutSec: number;
|
|
39
|
+
/** "detached" = background daemon, "session" = dies with pi session. Default: "detached" */
|
|
40
|
+
readonly lifetime: "detached" | "session";
|
|
41
|
+
/** How public model IDs are generated. Default: "collision-prefixed" */
|
|
42
|
+
readonly publicModelIdMode: PublicModelIdMode;
|
|
43
|
+
/** Which models are exposed. Default: "all" */
|
|
44
|
+
readonly modelExposureMode: ModelExposureMode;
|
|
45
|
+
/** Provider keys to expose when modelExposureMode is "scoped". */
|
|
46
|
+
readonly scopedProviders: readonly string[];
|
|
47
|
+
/** Canonical model IDs to expose when modelExposureMode is "custom". */
|
|
48
|
+
readonly customModels: readonly string[];
|
|
49
|
+
/** Provider key -> custom public prefix label. Default prefix = provider key. */
|
|
50
|
+
readonly providerPrefixes: Readonly<Record<string, string>>;
|
|
51
|
+
}
|
|
52
|
+
declare const DEFAULT_CONFIG: Readonly<ProxyConfig>;
|
|
53
|
+
declare function normalizeConfig(raw: unknown): ProxyConfig;
|
|
54
|
+
declare function getConfigPath(): string;
|
|
55
|
+
declare function loadConfigFromFile(): ProxyConfig;
|
|
56
|
+
declare function saveConfigToFile(config: ProxyConfig): void;
|
|
57
|
+
declare function configToEnv(config: ProxyConfig): Record<string, string>;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { DEFAULT_CONFIG, ModelExposureMode, ProxyConfig, PublicModelIdMode, configToEnv, getConfigPath, loadConfigFromFile, normalizeConfig, saveConfigToFile };
|
package/dist/config.mjs
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
//#region src/config/schema.ts
|
|
5
|
+
/**
|
|
6
|
+
* Proxy configuration schema -- single source of truth.
|
|
7
|
+
*
|
|
8
|
+
* Used by both the proxy server and the pi extension.
|
|
9
|
+
* The server reads the JSON config file as defaults, with env vars and CLI args as overrides.
|
|
10
|
+
* The pi extension reads and writes the JSON config file via the /proxy config panel.
|
|
11
|
+
*/
|
|
12
|
+
const DEFAULT_CONFIG = {
|
|
13
|
+
host: "127.0.0.1",
|
|
14
|
+
port: 4141,
|
|
15
|
+
authToken: "",
|
|
16
|
+
remoteImages: false,
|
|
17
|
+
maxBodySizeMb: 50,
|
|
18
|
+
upstreamTimeoutSec: 120,
|
|
19
|
+
lifetime: "detached",
|
|
20
|
+
publicModelIdMode: "collision-prefixed",
|
|
21
|
+
modelExposureMode: "all",
|
|
22
|
+
scopedProviders: [],
|
|
23
|
+
customModels: [],
|
|
24
|
+
providerPrefixes: {}
|
|
25
|
+
};
|
|
26
|
+
function isRecord(value) {
|
|
27
|
+
return value !== null && value !== void 0 && typeof value === "object" && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function clampInt(raw, min, max, fallback) {
|
|
30
|
+
if (typeof raw !== "number" || !Number.isFinite(raw)) return fallback;
|
|
31
|
+
return Math.max(min, Math.min(max, Math.round(raw)));
|
|
32
|
+
}
|
|
33
|
+
const VALID_PUBLIC_ID_MODES = new Set([
|
|
34
|
+
"collision-prefixed",
|
|
35
|
+
"universal",
|
|
36
|
+
"always-prefixed"
|
|
37
|
+
]);
|
|
38
|
+
function isPublicModelIdMode(value) {
|
|
39
|
+
return VALID_PUBLIC_ID_MODES.has(value);
|
|
40
|
+
}
|
|
41
|
+
const VALID_EXPOSURE_MODES = new Set([
|
|
42
|
+
"all",
|
|
43
|
+
"scoped",
|
|
44
|
+
"custom"
|
|
45
|
+
]);
|
|
46
|
+
function isModelExposureMode(value) {
|
|
47
|
+
return VALID_EXPOSURE_MODES.has(value);
|
|
48
|
+
}
|
|
49
|
+
function normalizeStringArray(raw) {
|
|
50
|
+
if (!Array.isArray(raw)) return [];
|
|
51
|
+
const result = [];
|
|
52
|
+
for (const item of raw) if (typeof item === "string" && item.length > 0) result.push(item);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function normalizeStringRecord(raw) {
|
|
56
|
+
if (!isRecord(raw)) return {};
|
|
57
|
+
const result = {};
|
|
58
|
+
for (const [key, val] of Object.entries(raw)) if (typeof val === "string" && val.length > 0) result[key] = val;
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function normalizeConfig(raw) {
|
|
62
|
+
const v = isRecord(raw) ? raw : {};
|
|
63
|
+
const rawHost = v["host"];
|
|
64
|
+
const rawAuthToken = v["authToken"];
|
|
65
|
+
const rawRemoteImages = v["remoteImages"];
|
|
66
|
+
const rawPublicIdMode = v["publicModelIdMode"];
|
|
67
|
+
const rawExposureMode = v["modelExposureMode"];
|
|
68
|
+
return {
|
|
69
|
+
host: typeof rawHost === "string" && rawHost.length > 0 ? rawHost : DEFAULT_CONFIG.host,
|
|
70
|
+
port: clampInt(v["port"], 1, 65535, DEFAULT_CONFIG.port),
|
|
71
|
+
authToken: typeof rawAuthToken === "string" ? rawAuthToken : DEFAULT_CONFIG.authToken,
|
|
72
|
+
remoteImages: typeof rawRemoteImages === "boolean" ? rawRemoteImages : DEFAULT_CONFIG.remoteImages,
|
|
73
|
+
maxBodySizeMb: clampInt(v["maxBodySizeMb"], 1, 500, DEFAULT_CONFIG.maxBodySizeMb),
|
|
74
|
+
upstreamTimeoutSec: clampInt(v["upstreamTimeoutSec"], 5, 600, DEFAULT_CONFIG.upstreamTimeoutSec),
|
|
75
|
+
lifetime: v["lifetime"] === "session" ? "session" : "detached",
|
|
76
|
+
publicModelIdMode: typeof rawPublicIdMode === "string" && isPublicModelIdMode(rawPublicIdMode) ? rawPublicIdMode : DEFAULT_CONFIG.publicModelIdMode,
|
|
77
|
+
modelExposureMode: typeof rawExposureMode === "string" && isModelExposureMode(rawExposureMode) ? rawExposureMode : DEFAULT_CONFIG.modelExposureMode,
|
|
78
|
+
scopedProviders: normalizeStringArray(v["scopedProviders"]),
|
|
79
|
+
customModels: normalizeStringArray(v["customModels"]),
|
|
80
|
+
providerPrefixes: normalizeStringRecord(v["providerPrefixes"])
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function getConfigPath() {
|
|
84
|
+
return resolve(process.env["PI_CODING_AGENT_DIR"] ?? resolve(process.env["HOME"] ?? "~", ".pi", "agent"), "proxy-config.json");
|
|
85
|
+
}
|
|
86
|
+
function loadConfigFromFile() {
|
|
87
|
+
const p = getConfigPath();
|
|
88
|
+
if (!existsSync(p)) return { ...DEFAULT_CONFIG };
|
|
89
|
+
try {
|
|
90
|
+
return normalizeConfig(JSON.parse(readFileSync(p, "utf-8")));
|
|
91
|
+
} catch {
|
|
92
|
+
return { ...DEFAULT_CONFIG };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function saveConfigToFile(config) {
|
|
96
|
+
const p = getConfigPath();
|
|
97
|
+
const normalized = normalizeConfig(config);
|
|
98
|
+
const tmp = `${p}.tmp`;
|
|
99
|
+
try {
|
|
100
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
101
|
+
writeFileSync(tmp, `${JSON.stringify(normalized, null, " ")}\n`, "utf-8");
|
|
102
|
+
renameSync(tmp, p);
|
|
103
|
+
} catch {
|
|
104
|
+
if (existsSync(tmp)) unlinkSync(tmp);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function configToEnv(config) {
|
|
108
|
+
const env = {};
|
|
109
|
+
env["PI_PROXY_HOST"] = config.host;
|
|
110
|
+
env["PI_PROXY_PORT"] = String(config.port);
|
|
111
|
+
if (config.authToken.length > 0) env["PI_PROXY_AUTH_TOKEN"] = config.authToken;
|
|
112
|
+
env["PI_PROXY_REMOTE_IMAGES"] = String(config.remoteImages);
|
|
113
|
+
env["PI_PROXY_MAX_BODY_SIZE"] = String(config.maxBodySizeMb * 1024 * 1024);
|
|
114
|
+
env["PI_PROXY_UPSTREAM_TIMEOUT_MS"] = String(config.upstreamTimeoutSec * 1e3);
|
|
115
|
+
return env;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { DEFAULT_CONFIG, configToEnv, getConfigPath, loadConfigFromFile, normalizeConfig, saveConfigToFile };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,2 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* pi-openai-proxy entry point.
|
|
5
|
-
*
|
|
6
|
-
* Initializes the pi model registry, creates the Hono app, and starts serving.
|
|
7
|
-
* Implements graceful shutdown on SIGTERM/SIGINT.
|
|
8
|
-
*/
|
|
9
|
-
declare const server: Bun.Server<undefined>;
|
|
10
|
-
//#endregion
|
|
11
|
-
export { server as default };
|
|
2
|
+
export { };
|