pi-cliproxyapi 0.1.2 → 0.3.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 +48 -16
- package/index.ts +3 -3
- package/package.json +1 -1
- package/src/apply.ts +85 -10
- package/src/commands.ts +12 -170
- package/src/fetch-models.ts +2 -5
- package/src/log.ts +17 -4
- package/src/ui-frame.ts +118 -0
- package/src/ui-hub/hub.ts +264 -0
- package/src/ui-hub/index.ts +50 -0
- package/src/ui-hub/shell.ts +119 -0
- package/src/ui-hub/types.ts +16 -0
- package/src/ui-hub/view-diagnostics.ts +108 -0
- package/src/ui-hub/view-models.ts +515 -0
- package/src/ui-hub/view-usage.ts +131 -0
- package/src/ui-picker/catalog.ts +52 -0
- package/src/ui-picker/mutate.ts +167 -0
- package/src/ui-picker/prompt-confirm.ts +71 -0
- package/src/ui-picker/prompt-name.ts +90 -0
- package/src/ui-picker/providers.ts +68 -0
- package/src/ui-picker/render-text.ts +39 -0
- package/src/ui-picker/rows.ts +151 -0
- package/src/ui-picker/types.ts +56 -0
- package/src/ui-setup.ts +21 -48
- package/src/ui-usage.ts +1 -1
- package/src/ui-overlay.ts +0 -292
- package/src/ui-picker.ts +0 -842
package/README.md
CHANGED
|
@@ -9,28 +9,44 @@ One `(endpoint, apiKey)` pair — every provider and model inherits it automatic
|
|
|
9
9
|
|
|
10
10
|
## Features
|
|
11
11
|
|
|
12
|
+
- **Unified hub** — one `/cliproxy` overlay with **Models / Usage / Diagnostics** tabs (number hotkeys `1` `2` `3`) plus global actions: `r` refresh, `e` setup, `s` save
|
|
12
13
|
- **Built-in provider routing** — whitelist which Anthropic / OpenAI / etc. models are available through the proxy
|
|
13
14
|
- **Custom provider groups** — create named groups (e.g. `corp-glm`, `corp-gemini`) for proxy-only models with automatic metadata from [models.dev](https://models.dev)
|
|
14
|
-
- **Exclusive model pool** — a model assigned to one group automatically disappears from others
|
|
15
|
-
- **
|
|
15
|
+
- **Exclusive model pool** — a model assigned to one group automatically disappears from others, grouped by `owned_by` with type-to-filter (`/`)
|
|
16
|
+
- **Live save state** — the header shows `● unsaved` while you edit and `✓ settings saved` after `s`, no console noise
|
|
17
|
+
- **Per-account usage tab** — colored quota bars, toggle disabled accounts, verbose errors — no LLM call
|
|
16
18
|
- **Setup wizard** — `/cliproxy-setup` configures endpoint, API key, provider prefix, and usage key interactively
|
|
17
19
|
|
|
18
20
|
## Commands
|
|
19
21
|
|
|
22
|
+
Two commands; everything else lives inside the hub as tabs and actions.
|
|
23
|
+
|
|
20
24
|
| Command | Description |
|
|
21
25
|
| --- | --- |
|
|
22
|
-
| `/cliproxy` |
|
|
26
|
+
| `/cliproxy` | Hub overlay — **Models** / **Usage** / **Diagnostics** tabs plus global actions |
|
|
23
27
|
| `/cliproxy-setup` | Configure endpoint, API key, provider prefix, usage key |
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
### The `/cliproxy` hub
|
|
30
|
+
|
|
31
|
+
Global keys: `[` / `]` or `1` `2` `3` switch tabs · `r` refresh discovery + reapply · `e` setup · `s` save · `q` / `Esc` close.
|
|
32
|
+
|
|
33
|
+
**Models tab** — three panels cycled with `Tab` / arrows:
|
|
34
|
+
|
|
35
|
+
- **left** — every provider (built-in + custom). `+ new custom group…` is the last row.
|
|
36
|
+
- **right top** — models assigned to the focused provider. `Enter` / `Space` removes one.
|
|
37
|
+
- **right bottom** — available pool, grouped by upstream `owned_by`. `Enter` / `Space` attaches. Press `/` to filter the pool by id/name. A `⚠` marks an API mismatch (attach still allowed).
|
|
38
|
+
|
|
39
|
+
Extra Models keys: `d` removes a custom group (with confirmation).
|
|
40
|
+
|
|
41
|
+
**Usage tab** — per-account quota bars; `d` shows disabled accounts, `v` shows verbose errors.
|
|
42
|
+
|
|
43
|
+
**Diagnostics tab** — connectivity, key resolution, and discovery shape.
|
|
28
44
|
|
|
29
45
|
## Prerequisites
|
|
30
46
|
|
|
31
47
|
You need a running [CliProxyAPI](https://github.com/router-for-me/CLIProxyAPI) instance — this is the corporate LLM proxy that aggregates multiple providers behind a single OpenAI-compatible endpoint.
|
|
32
48
|
|
|
33
|
-
For full functionality (
|
|
49
|
+
For full functionality (Usage tab, enriched model metadata from [models.dev](https://models.dev)), also deploy the companion sidecar: **[pi-cliproxyapi-wellknown](https://github.com/abix5/pi-cliproxyapi-wellknown)**. See [Deploying the sidecar](#deploying-the-sidecar-service) below.
|
|
34
50
|
|
|
35
51
|
## Install
|
|
36
52
|
|
|
@@ -76,7 +92,7 @@ The plugin tries `GET <endpoint-origin>/.well-known/pi` first (requires the side
|
|
|
76
92
|
The **[pi-cliproxyapi-wellknown](https://github.com/abix5/pi-cliproxyapi-wellknown)** sidecar runs alongside CliProxyAPI and provides:
|
|
77
93
|
|
|
78
94
|
- `/.well-known/pi` — model discovery with metadata from [models.dev](https://models.dev) (context windows, costs, reasoning flags)
|
|
79
|
-
- `/api/usage` — per-account quota windows used by
|
|
95
|
+
- `/api/usage` — per-account quota windows used by the hub Usage tab
|
|
80
96
|
|
|
81
97
|
```
|
|
82
98
|
┌──────────────┐ ┌───────────────────────────┐
|
|
@@ -134,16 +150,16 @@ Run `/cliproxy-setup` in Pi and enter:
|
|
|
134
150
|
- **endpoint** — your public proxy URL ending with `/v1`
|
|
135
151
|
- **apiKey** — CliProxyAPI bearer key
|
|
136
152
|
- **providerPrefix** — short slug for custom provider names (e.g. `corp`, `myproxy`)
|
|
137
|
-
- **usageKey** — same value as `PI_PLUGIN_USAGE_KEY` above (enables
|
|
153
|
+
- **usageKey** — same value as `PI_PLUGIN_USAGE_KEY` above (enables the Usage tab)
|
|
138
154
|
|
|
139
155
|
The sidecar is **optional for basic usage** — without it the plugin falls back to raw `/v1/models` with local heuristics. What changes:
|
|
140
156
|
|
|
141
157
|
| | With sidecar | Without sidecar |
|
|
142
158
|
| --- | --- | --- |
|
|
143
159
|
| Model discovery | Enriched from [models.dev](https://models.dev) (real context windows, costs, reasoning) | Defaults: `contextWindow=128k`, `maxTokens=16k`, `cost=0`, `reasoning=false` |
|
|
144
|
-
|
|
|
160
|
+
| Usage tab | Works — per-account quota bars | **Does not work** (no `/api/usage` endpoint) |
|
|
145
161
|
| Classification | Server-side, accurate | Local heuristics by `owned_by` |
|
|
146
|
-
| `/cliproxy
|
|
162
|
+
| `/cliproxy` hub | Works | Works (Usage tab shows an error) |
|
|
147
163
|
|
|
148
164
|
## Layout
|
|
149
165
|
|
|
@@ -151,15 +167,31 @@ The sidecar is **optional for basic usage** — without it the plugin falls back
|
|
|
151
167
|
index.ts ExtensionFactory entry point
|
|
152
168
|
src/
|
|
153
169
|
config.ts ~/.config/pi-cliproxyapi/config.json
|
|
154
|
-
commands.ts
|
|
170
|
+
commands.ts 2 slash commands (hub + setup)
|
|
155
171
|
apply.ts pi.registerProvider calls
|
|
156
172
|
fetch-models.ts well-known + /v1/models fallback
|
|
157
173
|
fetch-usage.ts /api/usage client with TTL cache
|
|
158
174
|
compat.ts baseUrl derivation, model classification
|
|
159
175
|
conflicts.ts read-only ~/.pi/{models,auth}.json scan
|
|
160
|
-
ui-
|
|
161
|
-
ui-usage.ts ANSI-colored usage renderer
|
|
162
|
-
ui-overlay.ts scrollable overlay shell with toggles
|
|
176
|
+
ui-frame.ts single source of truth for overlay frames
|
|
163
177
|
ui-setup.ts setup wizard
|
|
178
|
+
ui-usage.ts ANSI-coloured usage renderer
|
|
179
|
+
ui-hub/ the /cliproxy hub overlay
|
|
180
|
+
index.ts public runHub entry
|
|
181
|
+
hub.ts tabs, status header, global actions
|
|
182
|
+
types.ts HubView contract
|
|
183
|
+
shell.ts tab bar, status header, scroll/slice helpers
|
|
184
|
+
view-models.ts three-panel picker (single pool ordering + filter)
|
|
185
|
+
view-usage.ts usage tab (lazy fetch + d/v toggles)
|
|
186
|
+
view-diagnostics.ts diagnostics tab
|
|
187
|
+
ui-picker/ picker building blocks reused by the Models view
|
|
188
|
+
types.ts shared TS types
|
|
189
|
+
catalog.ts build a model lookup from discovery
|
|
190
|
+
providers.ts resolve the providers shown in the left panel
|
|
191
|
+
mutate.ts attach / detach / claim + pool grouping + display order
|
|
192
|
+
render-text.ts ANSI-aware pad / truncate
|
|
193
|
+
rows.ts per-row renderers for left / right panels
|
|
194
|
+
prompt-confirm.ts remove-group confirmation
|
|
195
|
+
prompt-name.ts new-group name prompt
|
|
164
196
|
log.ts tagged logger
|
|
165
197
|
```
|
package/index.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* 1. load ~/.config/pi-cliproxyapi/config.json (defaults if missing)
|
|
7
7
|
* 2. fetch discovery (well-known → fall back to /v1/models)
|
|
8
8
|
* 3. call pi.registerProvider for each enabled built-in + custom provider
|
|
9
|
-
* 4. register slash commands /cliproxy /cliproxy-setup
|
|
10
|
-
*
|
|
9
|
+
* 4. register slash commands /cliproxy and /cliproxy-setup
|
|
10
|
+
* (refresh, usage, and diagnostics are tabs/actions inside the hub)
|
|
11
11
|
*
|
|
12
12
|
* All discovery + apply errors are logged but never abort extension load —
|
|
13
13
|
* a missing/broken proxy must not prevent Pi from starting.
|
|
@@ -43,7 +43,7 @@ export default async function cliproxyapi(pi: ExtensionAPI): Promise<void> {
|
|
|
43
43
|
await applyAll(pi, cfg, discovery);
|
|
44
44
|
} catch (err) {
|
|
45
45
|
log.error("initial apply failed:", (err as Error).message);
|
|
46
|
-
// Commands stay registered; user can /cliproxy
|
|
46
|
+
// Commands stay registered; user can open /cliproxy and press r to refresh.
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
if (cfg.refreshIntervalMinutes > 0) {
|
package/package.json
CHANGED
package/src/apply.ts
CHANGED
|
@@ -37,6 +37,36 @@ export async function applyAll(
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// -------- builtin providers (anthropic, openai, etc.)
|
|
40
|
+
const builtinByName = new Map<
|
|
41
|
+
string,
|
|
42
|
+
Array<{
|
|
43
|
+
id: string;
|
|
44
|
+
name: string;
|
|
45
|
+
reasoning: boolean;
|
|
46
|
+
contextWindow: number;
|
|
47
|
+
maxTokens: number;
|
|
48
|
+
cost: {
|
|
49
|
+
input: number;
|
|
50
|
+
output: number;
|
|
51
|
+
cacheRead: number;
|
|
52
|
+
cacheWrite: number;
|
|
53
|
+
};
|
|
54
|
+
}>
|
|
55
|
+
>();
|
|
56
|
+
for (const p of discovery.builtinProviders) {
|
|
57
|
+
builtinByName.set(
|
|
58
|
+
p.name,
|
|
59
|
+
p.models.map((m) => ({
|
|
60
|
+
id: m.id,
|
|
61
|
+
name: m.name,
|
|
62
|
+
reasoning: m.reasoning,
|
|
63
|
+
contextWindow: m.contextWindow,
|
|
64
|
+
maxTokens: m.maxTokens,
|
|
65
|
+
cost: m.cost,
|
|
66
|
+
})),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
40
70
|
for (const [name, p] of Object.entries(cfg.builtinProviders)) {
|
|
41
71
|
if (!p?.enabled) {
|
|
42
72
|
report.skipped.push({ provider: name, reason: "disabled" });
|
|
@@ -46,7 +76,7 @@ export async function applyAll(
|
|
|
46
76
|
report.skipped.push({ provider: name, reason: "empty whitelist" });
|
|
47
77
|
continue;
|
|
48
78
|
}
|
|
49
|
-
let
|
|
79
|
+
let catalog: ReadonlyArray<{
|
|
50
80
|
id: string;
|
|
51
81
|
name: string;
|
|
52
82
|
api: Api;
|
|
@@ -56,19 +86,64 @@ export async function applyAll(
|
|
|
56
86
|
contextWindow: number;
|
|
57
87
|
maxTokens: number;
|
|
58
88
|
thinkingLevelMap?: any;
|
|
59
|
-
}
|
|
89
|
+
}> = [];
|
|
60
90
|
try {
|
|
61
|
-
|
|
91
|
+
catalog = getModels(name as any) as any;
|
|
62
92
|
} catch {
|
|
63
|
-
|
|
64
|
-
provider: name,
|
|
65
|
-
reason: `pi-ai has no provider "${name}"`,
|
|
66
|
-
});
|
|
67
|
-
continue;
|
|
93
|
+
/* unknown provider in pi-ai catalog — keep going with proxy data */
|
|
68
94
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
95
|
+
const catalogById = new Map(catalog.map((m) => [m.id, m]));
|
|
96
|
+
const proxyById = new Map(
|
|
97
|
+
(builtinByName.get(name) ?? []).map((m) => [m.id, m]),
|
|
71
98
|
);
|
|
99
|
+
|
|
100
|
+
interface MergedModel {
|
|
101
|
+
id: string;
|
|
102
|
+
name: string;
|
|
103
|
+
reasoning: boolean;
|
|
104
|
+
contextWindow: number;
|
|
105
|
+
maxTokens: number;
|
|
106
|
+
cost: any;
|
|
107
|
+
input: ("text" | "image")[];
|
|
108
|
+
api: Api;
|
|
109
|
+
thinkingLevelMap?: any;
|
|
110
|
+
}
|
|
111
|
+
const selected: MergedModel[] = [];
|
|
112
|
+
for (const id of p.models) {
|
|
113
|
+
if (!proxyIds.has(id)) continue;
|
|
114
|
+
const c = catalogById.get(id);
|
|
115
|
+
const px = proxyById.get(id);
|
|
116
|
+
if (c) {
|
|
117
|
+
selected.push({
|
|
118
|
+
id: c.id,
|
|
119
|
+
name: c.name,
|
|
120
|
+
reasoning: c.reasoning,
|
|
121
|
+
contextWindow: c.contextWindow,
|
|
122
|
+
maxTokens: c.maxTokens,
|
|
123
|
+
cost: c.cost,
|
|
124
|
+
input: c.input,
|
|
125
|
+
api: c.api,
|
|
126
|
+
thinkingLevelMap: c.thinkingLevelMap,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (!px) continue;
|
|
131
|
+
// Catalog miss — fall back to proxy metadata. Pick API by provider name.
|
|
132
|
+
const api: Api =
|
|
133
|
+
name === "anthropic"
|
|
134
|
+
? "anthropic-messages"
|
|
135
|
+
: ((p.apiOverride as Api | undefined) ?? "openai-responses");
|
|
136
|
+
selected.push({
|
|
137
|
+
id: px.id,
|
|
138
|
+
name: px.name,
|
|
139
|
+
reasoning: px.reasoning,
|
|
140
|
+
contextWindow: px.contextWindow,
|
|
141
|
+
maxTokens: px.maxTokens,
|
|
142
|
+
cost: px.cost,
|
|
143
|
+
input: ["text"],
|
|
144
|
+
api,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
72
147
|
if (selected.length === 0) {
|
|
73
148
|
report.skipped.push({
|
|
74
149
|
provider: name,
|
package/src/commands.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
// Slash commands
|
|
2
|
-
// /cliproxy
|
|
3
|
-
// /cliproxy-setup
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// /cliproxy-usage — fetch /api/usage and render in overlay
|
|
7
|
-
// /cliproxy-doctor — connectivity + key-resolution diagnostics
|
|
1
|
+
// Slash commands (2):
|
|
2
|
+
// /cliproxy — open the hub (Models / Usage / Diagnostics + actions)
|
|
3
|
+
// /cliproxy-setup — first-run / re-run wizard for endpoint+keys
|
|
4
|
+
//
|
|
5
|
+
// Refresh, usage, and diagnostics are now actions/tabs inside the hub.
|
|
8
6
|
|
|
9
7
|
import type {
|
|
10
8
|
ExtensionAPI,
|
|
@@ -12,20 +10,16 @@ import type {
|
|
|
12
10
|
} from "@earendil-works/pi-coding-agent";
|
|
13
11
|
|
|
14
12
|
import { applyAll } from "./apply.ts";
|
|
15
|
-
import { loadConfig, resolveConfigValue
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import { log } from "./log.ts";
|
|
20
|
-
import { showOverlay } from "./ui-overlay.ts";
|
|
21
|
-
import { runPicker } from "./ui-picker.ts";
|
|
13
|
+
import { loadConfig, resolveConfigValue } from "./config.ts";
|
|
14
|
+
import { fetchDiscovery } from "./fetch-models.ts";
|
|
15
|
+
import { clearUsageCache } from "./fetch-usage.ts";
|
|
16
|
+
import { runHub } from "./ui-hub/index.ts";
|
|
22
17
|
import { runSetup } from "./ui-setup.ts";
|
|
23
|
-
import { renderUsage } from "./ui-usage.ts";
|
|
24
18
|
|
|
25
19
|
export function registerCommands(pi: ExtensionAPI): void {
|
|
26
20
|
pi.registerCommand("cliproxy", {
|
|
27
21
|
description:
|
|
28
|
-
"
|
|
22
|
+
"Manage proxy models, usage, and diagnostics in one hub overlay",
|
|
29
23
|
handler: handleCliproxy.bind(null, pi),
|
|
30
24
|
});
|
|
31
25
|
|
|
@@ -34,27 +28,6 @@ export function registerCommands(pi: ExtensionAPI): void {
|
|
|
34
28
|
"Set endpoint, API key, and (optional) usage key for the proxy",
|
|
35
29
|
handler: handleSetup.bind(null, pi),
|
|
36
30
|
});
|
|
37
|
-
|
|
38
|
-
pi.registerCommand("cliproxy-refresh", {
|
|
39
|
-
description:
|
|
40
|
-
"Re-fetch upstream model list and re-apply provider registrations",
|
|
41
|
-
handler: handleRefresh.bind(null, pi),
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
pi.registerCommand("cliproxy-list", {
|
|
45
|
-
description: "Show every upstream model in a scrollable overlay",
|
|
46
|
-
handler: handleList,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
pi.registerCommand("cliproxy-usage", {
|
|
50
|
-
description: "Show per-account quota windows from the upstream",
|
|
51
|
-
handler: handleUsage,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
pi.registerCommand("cliproxy-doctor", {
|
|
55
|
-
description: "Check connectivity, key resolution, and discovery shape",
|
|
56
|
-
handler: handleDoctor,
|
|
57
|
-
});
|
|
58
31
|
}
|
|
59
32
|
|
|
60
33
|
// --------------------------------------------------------------------------- /cliproxy
|
|
@@ -67,7 +40,7 @@ async function handleCliproxy(
|
|
|
67
40
|
const cfg = loadConfig();
|
|
68
41
|
if (!cfg.proxy.endpoint || !resolveConfigValue(cfg.proxy.apiKey)) {
|
|
69
42
|
ctx.ui.notify(
|
|
70
|
-
"endpoint or API key not set \u2014 launching
|
|
43
|
+
"endpoint or API key not set \u2014 launching setup first",
|
|
71
44
|
"info",
|
|
72
45
|
);
|
|
73
46
|
const ok = await runSetup(ctx, true);
|
|
@@ -82,17 +55,7 @@ async function handleCliproxy(
|
|
|
82
55
|
ctx.ui.notify(`discovery failed: ${(err as Error).message}`, "error");
|
|
83
56
|
return;
|
|
84
57
|
}
|
|
85
|
-
|
|
86
|
-
if (!updated) {
|
|
87
|
-
ctx.ui.notify("changes discarded", "info");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
saveConfig(updated);
|
|
91
|
-
const rep = await applyAll(pi, updated, discovery);
|
|
92
|
-
ctx.ui.notify(
|
|
93
|
-
`saved \u00b7 ${rep.registered.length} providers registered, ${rep.skipped.length} skipped`,
|
|
94
|
-
"info",
|
|
95
|
-
);
|
|
58
|
+
await runHub(pi, ctx, current, discovery);
|
|
96
59
|
}
|
|
97
60
|
|
|
98
61
|
// --------------------------------------------------------------------------- /cliproxy-setup
|
|
@@ -124,124 +87,3 @@ async function handleSetup(
|
|
|
124
87
|
);
|
|
125
88
|
}
|
|
126
89
|
}
|
|
127
|
-
|
|
128
|
-
// --------------------------------------------------------------------------- /cliproxy-refresh
|
|
129
|
-
|
|
130
|
-
async function handleRefresh(
|
|
131
|
-
pi: ExtensionAPI,
|
|
132
|
-
_args: string,
|
|
133
|
-
ctx: ExtensionCommandContext,
|
|
134
|
-
): Promise<void> {
|
|
135
|
-
const cfg = loadConfig();
|
|
136
|
-
const resolvedKey = resolveConfigValue(cfg.proxy.apiKey);
|
|
137
|
-
try {
|
|
138
|
-
const discovery = await fetchDiscovery(cfg, resolvedKey);
|
|
139
|
-
const rep = await applyAll(pi, cfg, discovery);
|
|
140
|
-
clearUsageCache();
|
|
141
|
-
ctx.ui.notify(
|
|
142
|
-
`cliproxy: ${rep.registered.length} providers registered, ${rep.skipped.length} skipped (source=${discovery.source})`,
|
|
143
|
-
"info",
|
|
144
|
-
);
|
|
145
|
-
} catch (err) {
|
|
146
|
-
ctx.ui.notify(`refresh failed: ${(err as Error).message}`, "error");
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// --------------------------------------------------------------------------- /cliproxy-list
|
|
151
|
-
|
|
152
|
-
async function handleList(
|
|
153
|
-
_args: string,
|
|
154
|
-
ctx: ExtensionCommandContext,
|
|
155
|
-
): Promise<void> {
|
|
156
|
-
const cfg = loadConfig();
|
|
157
|
-
const resolvedKey = resolveConfigValue(cfg.proxy.apiKey);
|
|
158
|
-
let discovery;
|
|
159
|
-
try {
|
|
160
|
-
discovery = await fetchDiscovery(cfg, resolvedKey);
|
|
161
|
-
} catch (err) {
|
|
162
|
-
ctx.ui.notify(`list failed: ${(err as Error).message}`, "error");
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
await runPicker(ctx, cfg, discovery, {
|
|
166
|
-
readOnly: true,
|
|
167
|
-
title: " /cliproxy-list \u00b7 read-only ",
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// --------------------------------------------------------------------------- /cliproxy-usage
|
|
172
|
-
|
|
173
|
-
async function handleUsage(
|
|
174
|
-
args: string,
|
|
175
|
-
ctx: ExtensionCommandContext,
|
|
176
|
-
): Promise<void> {
|
|
177
|
-
const force = /(^|\s)--refresh(\s|$)/.test(args);
|
|
178
|
-
const cfg = loadConfig();
|
|
179
|
-
const usageKey = resolveConfigValue(cfg.proxy.usageKey);
|
|
180
|
-
let doc;
|
|
181
|
-
try {
|
|
182
|
-
doc = await fetchUsage(cfg, usageKey, { force });
|
|
183
|
-
} catch (err) {
|
|
184
|
-
ctx.ui.notify(`usage failed: ${(err as Error).message}`, "error");
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
await showOverlay(ctx, "cliproxy-usage", {
|
|
188
|
-
render: (state) =>
|
|
189
|
-
renderUsage(doc, {
|
|
190
|
-
showDisabled: state["d"] === true,
|
|
191
|
-
verbose: state["v"] === true,
|
|
192
|
-
}).join("\n"),
|
|
193
|
-
toggles: [
|
|
194
|
-
{ key: "d", hint: "d disabled" },
|
|
195
|
-
{ key: "v", hint: "v verbose" },
|
|
196
|
-
],
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// --------------------------------------------------------------------------- /cliproxy-doctor
|
|
201
|
-
|
|
202
|
-
async function handleDoctor(
|
|
203
|
-
_args: string,
|
|
204
|
-
ctx: ExtensionCommandContext,
|
|
205
|
-
): Promise<void> {
|
|
206
|
-
const cfg = loadConfig();
|
|
207
|
-
const lines: string[] = [];
|
|
208
|
-
lines.push(`endpoint: ${cfg.proxy.endpoint}`);
|
|
209
|
-
lines.push(
|
|
210
|
-
`apiKey resolves: ${resolveConfigValue(cfg.proxy.apiKey) ? "yes" : "NO (empty after resolution)"}`,
|
|
211
|
-
);
|
|
212
|
-
lines.push(
|
|
213
|
-
`usageKey resolves: ${cfg.proxy.usageKey ? (resolveConfigValue(cfg.proxy.usageKey) ? "yes" : "NO") : "not configured"}`,
|
|
214
|
-
);
|
|
215
|
-
lines.push(`user-agent: ${PLUGIN_USER_AGENT}`);
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
const discovery = await fetchDiscovery(
|
|
219
|
-
cfg,
|
|
220
|
-
resolveConfigValue(cfg.proxy.apiKey),
|
|
221
|
-
);
|
|
222
|
-
lines.push("");
|
|
223
|
-
lines.push(`discovery source: ${discovery.source}`);
|
|
224
|
-
lines.push(`upstream version: ${discovery.upstreamVersion ?? "(unknown)"}`);
|
|
225
|
-
lines.push(`upstream total ids: ${discovery.upstreamTotal}`);
|
|
226
|
-
lines.push(
|
|
227
|
-
`built-in providers seen: ${discovery.builtinProviders.map((p) => `${p.name}=${p.models.length}`).join(", ") || "(none)"}`,
|
|
228
|
-
);
|
|
229
|
-
lines.push(`custom pool size: ${discovery.customPool.length}`);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
lines.push("");
|
|
232
|
-
lines.push(`discovery FAILED: ${(err as Error).message}`);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
const conflicts = detectConflicts(cfg);
|
|
236
|
-
if (conflicts.length > 0) {
|
|
237
|
-
lines.push("");
|
|
238
|
-
lines.push("conflicts:");
|
|
239
|
-
for (const c of conflicts) lines.push(` [${c.kind}] ${c.detail}`);
|
|
240
|
-
} else {
|
|
241
|
-
lines.push("");
|
|
242
|
-
lines.push("conflicts: none");
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
log.info("doctor:", lines.join(" | "));
|
|
246
|
-
await showOverlay(ctx, "cliproxy-doctor", lines.join("\n"));
|
|
247
|
-
}
|
package/src/fetch-models.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import type { ProxyConfig } from "./config.ts";
|
|
21
21
|
import { log } from "./log.ts";
|
|
22
22
|
|
|
23
|
-
export const PLUGIN_USER_AGENT = "pi-cliproxyapi/0.
|
|
23
|
+
export const PLUGIN_USER_AGENT = "pi-cliproxyapi/0.3.0";
|
|
24
24
|
const REQUEST_TIMEOUT_MS = 5_000;
|
|
25
25
|
|
|
26
26
|
export interface DiscoveryModelEntry {
|
|
@@ -215,10 +215,7 @@ async function fetchRawModels(
|
|
|
215
215
|
.filter((m) => m.id);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function classifyLocally(
|
|
219
|
-
raw: RawUpstreamModel[],
|
|
220
|
-
cfg: ProxyConfig,
|
|
221
|
-
): Discovery {
|
|
218
|
+
function classifyLocally(raw: RawUpstreamModel[], cfg: ProxyConfig): Discovery {
|
|
222
219
|
const excludes = cfg.discoveryExcludes;
|
|
223
220
|
const builtinByName = new Map<string, DiscoveryBuiltinProvider>();
|
|
224
221
|
const customPool: DiscoveryCustomEntry[] = [];
|
package/src/log.ts
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
// Tiny logger that prefixes all messages with the extension tag.
|
|
2
2
|
// Uses console.* directly — pi pipes stdout/stderr into its log sink.
|
|
3
|
+
//
|
|
4
|
+
// While an interactive overlay is open, console output corrupts the TUI (it
|
|
5
|
+
// prints over the box and forces a redraw below it — stacking headers). The
|
|
6
|
+
// overlay code wraps its lifetime in setLogQuiet(true) to mute us; user-facing
|
|
7
|
+
// results are surfaced inside the overlay instead.
|
|
3
8
|
|
|
4
9
|
const TAG = "[pi-cliproxyapi]";
|
|
5
10
|
|
|
11
|
+
let quiet = false;
|
|
12
|
+
|
|
13
|
+
/** Mute/unmute all console output (used around interactive overlays). */
|
|
14
|
+
export function setLogQuiet(v: boolean): void {
|
|
15
|
+
quiet = v;
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export const log = {
|
|
7
19
|
info(...args: unknown[]): void {
|
|
8
|
-
console.log(TAG, ...args);
|
|
20
|
+
if (!quiet) console.log(TAG, ...args);
|
|
9
21
|
},
|
|
10
22
|
warn(...args: unknown[]): void {
|
|
11
|
-
console.warn(TAG, ...args);
|
|
23
|
+
if (!quiet) console.warn(TAG, ...args);
|
|
12
24
|
},
|
|
13
25
|
error(...args: unknown[]): void {
|
|
14
|
-
console.error(TAG, ...args);
|
|
26
|
+
if (!quiet) console.error(TAG, ...args);
|
|
15
27
|
},
|
|
16
28
|
debug(...args: unknown[]): void {
|
|
17
|
-
if (process.env.PI_CLIPROXYAPI_DEBUG)
|
|
29
|
+
if (!quiet && process.env.PI_CLIPROXYAPI_DEBUG)
|
|
30
|
+
console.log(TAG, "[debug]", ...args);
|
|
18
31
|
},
|
|
19
32
|
};
|
package/src/ui-frame.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Shared frame renderer for every /cliproxy overlay.
|
|
2
|
+
//
|
|
3
|
+
// Geometry contract:
|
|
4
|
+
// - frame() always returns lines that are EXACTLY `width` visible cells.
|
|
5
|
+
// - top = \u256d\u2500 <title> <fill> \u256e
|
|
6
|
+
// - body i = \u2502 <line padded/truncated to width-2> \u2502
|
|
7
|
+
// - footer = \u2570\u2500 <hint> <fill> [badge] \u256f (if footer provided)
|
|
8
|
+
// or \u2570\u2500\u2500\u2500\u2026\u2500\u2500\u2500\u256f (otherwise)
|
|
9
|
+
//
|
|
10
|
+
// Callers stay simple:
|
|
11
|
+
//
|
|
12
|
+
// return frame(theme, {
|
|
13
|
+
// width,
|
|
14
|
+
// title: " setup ",
|
|
15
|
+
// lines: [
|
|
16
|
+
// " hint goes here ",
|
|
17
|
+
// "",
|
|
18
|
+
// theme.fg("accent", "> input"),
|
|
19
|
+
// ],
|
|
20
|
+
// footer: { hint: "enter = save \u00b7 esc = cancel" },
|
|
21
|
+
// });
|
|
22
|
+
//
|
|
23
|
+
// Anything inside `lines` may already contain ANSI escapes; pad() is ANSI-aware
|
|
24
|
+
// (see render-text.ts). No caller should ever draw \u256d/\u256e/\u2502/\u2570/\u256f by hand again.
|
|
25
|
+
|
|
26
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
27
|
+
|
|
28
|
+
import { pad } from "./ui-picker/render-text.ts";
|
|
29
|
+
import type { Theme } from "./ui-picker/types.ts";
|
|
30
|
+
|
|
31
|
+
export interface FrameFooter {
|
|
32
|
+
/** Left-aligned hint text (dimmed). */
|
|
33
|
+
hint?: string;
|
|
34
|
+
/** Right-aligned focus/state badge (muted). */
|
|
35
|
+
badge?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface FrameOpts {
|
|
39
|
+
width: number;
|
|
40
|
+
title?: string;
|
|
41
|
+
titleColor?: "accent" | "error" | "success" | "warning";
|
|
42
|
+
lines: string[];
|
|
43
|
+
footer?: FrameFooter;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const SIDE = "\u2502";
|
|
47
|
+
const TL = "\u256d";
|
|
48
|
+
const TR = "\u256e";
|
|
49
|
+
const BL = "\u2570";
|
|
50
|
+
const BR = "\u256f";
|
|
51
|
+
const HR = "\u2500";
|
|
52
|
+
|
|
53
|
+
export function frame(theme: Theme, opts: FrameOpts): string[] {
|
|
54
|
+
const w = Math.max(4, opts.width);
|
|
55
|
+
return [
|
|
56
|
+
drawTop(theme, opts.title, opts.titleColor ?? "accent", w),
|
|
57
|
+
...opts.lines.map((line) => drawBody(theme, line, w)),
|
|
58
|
+
drawBottom(theme, opts.footer, w),
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function drawTop(
|
|
63
|
+
theme: Theme,
|
|
64
|
+
title: string | undefined,
|
|
65
|
+
color: "accent" | "error" | "success" | "warning",
|
|
66
|
+
width: number,
|
|
67
|
+
): string {
|
|
68
|
+
// Layout: \u256d \u2500 <title> <fill> \u256e \u2014 exactly `width` cells.
|
|
69
|
+
// Fixed cells = 2 (\u256d\u2500) + 1 (\u256e) = 3. The rest is title + fill.
|
|
70
|
+
const t = title ?? "";
|
|
71
|
+
const titleVis = visibleWidth(t);
|
|
72
|
+
const rest = Math.max(0, width - 3 - titleVis);
|
|
73
|
+
const titleColoured = t ? theme.bold(theme.fg(color, t)) : "";
|
|
74
|
+
const body = `${TL}${HR}${titleColoured}${HR.repeat(rest)}${TR}`;
|
|
75
|
+
return theme.fg("borderAccent", body);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function drawBody(theme: Theme, content: string, width: number): string {
|
|
79
|
+
// Layout: \u2502 <content padded to width-2> \u2502
|
|
80
|
+
const inner = Math.max(0, width - 2);
|
|
81
|
+
const side = theme.fg("borderAccent", SIDE);
|
|
82
|
+
return `${side}${pad(content, inner)}${side}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function drawBottom(
|
|
86
|
+
theme: Theme,
|
|
87
|
+
footer: FrameFooter | undefined,
|
|
88
|
+
width: number,
|
|
89
|
+
): string {
|
|
90
|
+
const start = `${BL}${HR}`;
|
|
91
|
+
const end = `${BR}`;
|
|
92
|
+
const fixedCells = 3; // \u2570 + \u2500 + \u256f
|
|
93
|
+
|
|
94
|
+
if (!footer || (!footer.hint && !footer.badge)) {
|
|
95
|
+
const rest = Math.max(0, width - fixedCells);
|
|
96
|
+
return theme.fg("borderAccent", `${start}${HR.repeat(rest)}${end}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const hint = footer.hint ?? "";
|
|
100
|
+
const badge = footer.badge ?? "";
|
|
101
|
+
const hintVis = visibleWidth(hint);
|
|
102
|
+
const badgeVis = visibleWidth(badge);
|
|
103
|
+
const rest = Math.max(0, width - fixedCells - hintVis - badgeVis);
|
|
104
|
+
|
|
105
|
+
const left = theme.fg("dim", hint);
|
|
106
|
+
const right = badge ? theme.fg("muted", badge) : "";
|
|
107
|
+
const fill = HR.repeat(rest);
|
|
108
|
+
|
|
109
|
+
return `${theme.fg("borderAccent", start)}${left}${theme.fg("borderAccent", fill)}${right}${theme.fg("borderAccent", end)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Convenience: number of usable content cells per body line for a given frame
|
|
114
|
+
* width. Use this when laying out children that need to know their width.
|
|
115
|
+
*/
|
|
116
|
+
export function frameInner(width: number): number {
|
|
117
|
+
return Math.max(0, width - 2);
|
|
118
|
+
}
|