pi-omlx-picker 0.2.2 → 0.2.3
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 +16 -7
- package/index.ts +187 -74
- package/package.json +37 -23
- package/src/auth-storage.ts +15 -5
- package/src/catalog.ts +41 -1
- package/src/config.ts +23 -23
- package/src/provider.ts +2 -2
- package/.assets/demo.gif +0 -0
- package/.assets/demo.tape +0 -31
- package/.biomeignore +0 -1
- package/.github/dependabot.yml +0 -11
- package/CONTRIBUTING.md +0 -52
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ This extension discovers models from a local OMLX server and registers them as n
|
|
|
8
8
|
|
|
9
9
|
## ✨ Features
|
|
10
10
|
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* Auto discovery: fetches and registers available OMLX models without blocking Pi startup.
|
|
12
|
+
* Native integration: login uses Pi's standard `/login`, and models use Pi's standard `/model` menu.
|
|
13
|
+
* Smart overrides: applies per-request thinking controls based on each model's `thinkingDefault` metadata.
|
|
14
14
|
|
|
15
15
|
## 📦 Installation
|
|
16
16
|
|
|
@@ -18,13 +18,22 @@ This extension discovers models from a local OMLX server and registers them as n
|
|
|
18
18
|
pi install npm:pi-omlx-picker
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## 🛠️ Development
|
|
22
|
+
|
|
23
|
+
- `npm install`
|
|
24
|
+
- `npm run check`
|
|
25
|
+
- `npm run format`
|
|
26
|
+
- `npm test`
|
|
27
|
+
- `npm run test:watch`
|
|
28
|
+
|
|
21
29
|
## 🚀 Quick Start
|
|
22
30
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3.
|
|
31
|
+
1. Start your local OMLX server.
|
|
32
|
+
2. Run `/login` in Pi, choose **API key**, then choose **OMLX**.
|
|
33
|
+
3. Enter your OMLX API key.
|
|
34
|
+
4. Type `/model` to see and select your OMLX models.
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
The default base URL is `http://127.0.0.1:8000/v1`. Set `OMLX_BASE_URL` before starting Pi if your OMLX server uses a different URL.
|
|
28
37
|
|
|
29
38
|
## ⚙️ Configuration
|
|
30
39
|
|
package/index.ts
CHANGED
|
@@ -1,23 +1,52 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
type Api,
|
|
3
|
+
type AssistantMessage,
|
|
4
|
+
createAssistantMessageEventStream,
|
|
5
|
+
type Model,
|
|
6
|
+
} from "@earendil-works/pi-ai";
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import { PROVIDER_KEY } from "./src/auth-storage.ts";
|
|
9
|
+
import {
|
|
10
|
+
fetchModels,
|
|
11
|
+
type OmlxModel,
|
|
12
|
+
readCatalogCache,
|
|
13
|
+
readLastCatalogCache,
|
|
14
|
+
resolveLocalModelSettingsPath,
|
|
15
|
+
writeCatalogCache,
|
|
16
|
+
} from "./src/catalog.ts";
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_OMLX_BASE_URL,
|
|
19
|
+
loadConfig,
|
|
20
|
+
type OmlxConfig,
|
|
21
|
+
resolveConfiguredApiKey,
|
|
22
|
+
} from "./src/config.ts";
|
|
5
23
|
import { toProviderConfig } from "./src/provider.ts";
|
|
6
24
|
import { applyOmlxThinkingControls } from "./src/thinking.ts";
|
|
7
25
|
|
|
8
26
|
const PROVIDER = PROVIDER_KEY;
|
|
9
27
|
const EXTENSION_SINGLETON_KEY = Symbol.for("pi-omlx-picker/loaded");
|
|
10
28
|
|
|
29
|
+
const STARTUP_TIMEOUT_MS = 2_000;
|
|
30
|
+
const POLL_INTERVAL_MS = 10 * 60 * 1000;
|
|
31
|
+
const BACKOFF_BASE_MS = 2_000;
|
|
32
|
+
const BACKOFF_MAX_MS = 60_000;
|
|
33
|
+
|
|
34
|
+
const SETUP_MODEL: OmlxModel = {
|
|
35
|
+
id: "setup",
|
|
36
|
+
displayName: "OMLX (run /login)",
|
|
37
|
+
};
|
|
38
|
+
|
|
11
39
|
interface State {
|
|
12
40
|
config: OmlxConfig | undefined;
|
|
13
41
|
catalog: OmlxModel[];
|
|
14
42
|
registered: boolean;
|
|
43
|
+
stopped: boolean;
|
|
15
44
|
lastError: string | undefined;
|
|
16
45
|
lastRefreshAt: string | undefined;
|
|
17
46
|
modelSettingsPath: string | undefined;
|
|
18
47
|
}
|
|
19
48
|
|
|
20
|
-
export default
|
|
49
|
+
export default function (pi: ExtensionAPI): void {
|
|
21
50
|
const globalState = globalThis as Record<PropertyKey, unknown>;
|
|
22
51
|
if (globalState[EXTENSION_SINGLETON_KEY]) return;
|
|
23
52
|
globalState[EXTENSION_SINGLETON_KEY] = true;
|
|
@@ -26,108 +55,192 @@ export default async function (pi: ExtensionAPI): Promise<void> {
|
|
|
26
55
|
config: undefined,
|
|
27
56
|
catalog: [],
|
|
28
57
|
registered: false,
|
|
58
|
+
stopped: false,
|
|
29
59
|
lastError: undefined,
|
|
30
60
|
lastRefreshAt: undefined,
|
|
31
61
|
modelSettingsPath: undefined,
|
|
32
62
|
};
|
|
33
63
|
|
|
34
|
-
|
|
35
|
-
|
|
64
|
+
registerCachedOrSetupModels(pi, state);
|
|
65
|
+
void startPolling(pi, state).catch((err) => {
|
|
66
|
+
state.lastError = err instanceof Error ? err.message : String(err);
|
|
67
|
+
});
|
|
36
68
|
|
|
37
69
|
pi.on("before_provider_request", (event, ctx) => {
|
|
38
70
|
if (ctx.model?.provider !== PROVIDER) return;
|
|
39
71
|
const activeModel = findCatalogModel(state, ctx.model.id);
|
|
40
|
-
return applyOmlxThinkingControls(
|
|
72
|
+
return applyOmlxThinkingControls(
|
|
73
|
+
event.payload,
|
|
74
|
+
pi.getThinkingLevel(),
|
|
75
|
+
activeModel?.thinkingDefault,
|
|
76
|
+
);
|
|
41
77
|
});
|
|
42
78
|
|
|
43
|
-
pi.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const baseUrlInput = await ctx.ui.input("OMLX base URL", "http://127.0.0.1:8000/v1");
|
|
47
|
-
if (!baseUrlInput) return;
|
|
48
|
-
const apiKey = await ctx.ui.input("OMLX API key", "omlx-...");
|
|
49
|
-
if (!apiKey) return;
|
|
50
|
-
|
|
51
|
-
let baseUrl: string;
|
|
52
|
-
try {
|
|
53
|
-
baseUrl = normalizeBaseUrl(baseUrlInput);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
ctx.ui.notify(`Invalid base URL: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
ctx.ui.notify("Validating OMLX credentials…", "info");
|
|
60
|
-
try {
|
|
61
|
-
await fetchModels(baseUrl, apiKey, { timeoutMs: VALIDATE_TIMEOUT_MS });
|
|
62
|
-
} catch (err) {
|
|
63
|
-
ctx.ui.notify(`OMLX login failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
saveOmlxCredential(baseUrl, apiKey);
|
|
68
|
-
process.env.OMLX_BASE_URL = baseUrl;
|
|
69
|
-
process.env.OMLX_API_KEY = apiKey;
|
|
70
|
-
await refreshProvider(pi, state);
|
|
71
|
-
const message = state.registered
|
|
72
|
-
? `OMLX connected — ${state.catalog.length} models available`
|
|
73
|
-
: `OMLX login saved but provider failed: ${state.lastError ?? "unknown error"}`;
|
|
74
|
-
ctx.ui.notify(message, state.registered ? "info" : "warning");
|
|
75
|
-
},
|
|
79
|
+
pi.on("session_shutdown", () => {
|
|
80
|
+
state.stopped = true;
|
|
81
|
+
delete globalState[EXTENSION_SINGLETON_KEY];
|
|
76
82
|
});
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
pi
|
|
85
|
+
async function startPolling(pi: ExtensionAPI, state: State): Promise<void> {
|
|
86
|
+
let backoffMs = BACKOFF_BASE_MS;
|
|
87
|
+
|
|
88
|
+
while (!state.stopped) {
|
|
89
|
+
const result = await refreshProvider(pi, state, {
|
|
90
|
+
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (result === "registered") {
|
|
94
|
+
backoffMs = BACKOFF_BASE_MS;
|
|
95
|
+
await sleep(POLL_INTERVAL_MS);
|
|
96
|
+
} else {
|
|
97
|
+
registerCachedOrSetupModels(pi, state);
|
|
98
|
+
await sleep(backoffMs);
|
|
99
|
+
backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS);
|
|
100
|
+
}
|
|
84
101
|
}
|
|
85
|
-
state.catalog = [];
|
|
86
|
-
state.registered = false;
|
|
87
|
-
state.config = undefined;
|
|
88
|
-
state.modelSettingsPath = undefined;
|
|
89
102
|
}
|
|
90
103
|
|
|
91
|
-
|
|
92
|
-
let config: OmlxConfig;
|
|
104
|
+
function tryLoadConfig(): OmlxConfig | undefined {
|
|
93
105
|
try {
|
|
94
|
-
|
|
95
|
-
} catch
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
return loadConfig();
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function registerModels(
|
|
113
|
+
pi: ExtensionAPI,
|
|
114
|
+
state: State,
|
|
115
|
+
config: OmlxConfig,
|
|
116
|
+
models: OmlxModel[],
|
|
117
|
+
modelSettingsPath?: string,
|
|
118
|
+
): void {
|
|
119
|
+
pi.registerProvider(PROVIDER, {
|
|
120
|
+
name: "OMLX",
|
|
121
|
+
...toProviderConfig(config.apiRoot, config.apiKeyEnvVar, models),
|
|
122
|
+
});
|
|
123
|
+
state.config = config;
|
|
124
|
+
state.catalog = models;
|
|
125
|
+
state.registered = true;
|
|
126
|
+
state.lastError = undefined;
|
|
127
|
+
state.lastRefreshAt = new Date().toISOString();
|
|
128
|
+
state.modelSettingsPath = modelSettingsPath;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function registerCachedOrSetupModels(pi: ExtensionAPI, state: State): void {
|
|
132
|
+
const config = tryLoadConfig() ?? {
|
|
133
|
+
apiRoot: DEFAULT_OMLX_BASE_URL,
|
|
134
|
+
apiKeyEnvVar: "OMLX_API_KEY",
|
|
135
|
+
};
|
|
136
|
+
const cached = readCatalogCache(config.apiRoot);
|
|
137
|
+
const fallbackCached = resolveConfiguredApiKey()
|
|
138
|
+
? undefined
|
|
139
|
+
: readLastCatalogCache();
|
|
140
|
+
const models =
|
|
141
|
+
cached && cached.length > 0
|
|
142
|
+
? cached
|
|
143
|
+
: fallbackCached && fallbackCached.length > 0
|
|
144
|
+
? fallbackCached
|
|
145
|
+
: [SETUP_MODEL];
|
|
146
|
+
|
|
147
|
+
if (resolveConfiguredApiKey()) {
|
|
148
|
+
registerModels(pi, state, config, models);
|
|
103
149
|
return;
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
|
|
152
|
+
pi.registerProvider(PROVIDER, {
|
|
153
|
+
...toProviderConfig(config.apiRoot, config.apiKeyEnvVar, models),
|
|
154
|
+
name: "OMLX",
|
|
155
|
+
authHeader: false,
|
|
156
|
+
streamSimple: streamMissingCredentials,
|
|
157
|
+
});
|
|
158
|
+
state.config = config;
|
|
159
|
+
state.catalog = models;
|
|
160
|
+
state.registered = true;
|
|
161
|
+
state.lastError = "OMLX credentials are not set. Run /login and choose OMLX.";
|
|
162
|
+
state.lastRefreshAt = new Date().toISOString();
|
|
163
|
+
state.modelSettingsPath = undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function streamMissingCredentials(model: Model<Api>) {
|
|
167
|
+
const stream = createAssistantMessageEventStream();
|
|
168
|
+
const message: AssistantMessage = {
|
|
169
|
+
role: "assistant",
|
|
170
|
+
content: [],
|
|
171
|
+
api: model.api,
|
|
172
|
+
provider: model.provider,
|
|
173
|
+
model: model.id,
|
|
174
|
+
usage: {
|
|
175
|
+
input: 0,
|
|
176
|
+
output: 0,
|
|
177
|
+
cacheRead: 0,
|
|
178
|
+
cacheWrite: 0,
|
|
179
|
+
totalTokens: 0,
|
|
180
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
181
|
+
},
|
|
182
|
+
stopReason: "error",
|
|
183
|
+
errorMessage:
|
|
184
|
+
"OMLX credentials are not configured. Run /login, choose API key, select OMLX, then try the model again.",
|
|
185
|
+
timestamp: Date.now(),
|
|
186
|
+
};
|
|
187
|
+
queueMicrotask(() => {
|
|
188
|
+
stream.push({ type: "start", partial: message });
|
|
189
|
+
stream.push({ type: "error", reason: "error", error: message });
|
|
190
|
+
stream.end();
|
|
191
|
+
});
|
|
192
|
+
return stream;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type RefreshResult = "registered" | "not_configured" | "failed";
|
|
196
|
+
|
|
197
|
+
async function refreshProvider(
|
|
198
|
+
pi: ExtensionAPI,
|
|
199
|
+
state: State,
|
|
200
|
+
opts: { timeoutMs?: number } = {},
|
|
201
|
+
): Promise<RefreshResult> {
|
|
202
|
+
const config = loadConfig();
|
|
203
|
+
const apiKey = resolveConfiguredApiKey();
|
|
204
|
+
if (!apiKey) {
|
|
205
|
+
state.lastError = "OMLX credentials are not set";
|
|
206
|
+
return "not_configured";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const modelSettingsPath = resolveLocalModelSettingsPath(
|
|
210
|
+
process.env.OMLX_MODEL_SETTINGS_PATH,
|
|
211
|
+
);
|
|
107
212
|
|
|
108
213
|
let models: OmlxModel[];
|
|
109
214
|
try {
|
|
110
|
-
models = await fetchModels(config.apiRoot,
|
|
215
|
+
models = await fetchModels(config.apiRoot, apiKey, {
|
|
216
|
+
modelSettingsPath,
|
|
217
|
+
timeoutMs: opts.timeoutMs,
|
|
218
|
+
});
|
|
111
219
|
} catch (err) {
|
|
112
220
|
state.lastError = err instanceof Error ? err.message : String(err);
|
|
113
|
-
return;
|
|
221
|
+
return "failed";
|
|
114
222
|
}
|
|
115
223
|
|
|
116
224
|
if (models.length === 0) {
|
|
117
225
|
state.lastError = "OMLX returned 0 models";
|
|
118
|
-
|
|
119
|
-
return;
|
|
226
|
+
return "failed";
|
|
120
227
|
}
|
|
121
228
|
|
|
122
|
-
|
|
123
|
-
state
|
|
124
|
-
|
|
125
|
-
state.registered = true;
|
|
126
|
-
state.lastError = undefined;
|
|
127
|
-
state.lastRefreshAt = new Date().toISOString();
|
|
128
|
-
state.modelSettingsPath = modelSettingsPath;
|
|
229
|
+
writeCatalogCache(config.apiRoot, models);
|
|
230
|
+
registerModels(pi, state, config, models, modelSettingsPath);
|
|
231
|
+
return "registered";
|
|
129
232
|
}
|
|
130
233
|
|
|
131
|
-
function findCatalogModel(
|
|
234
|
+
function findCatalogModel(
|
|
235
|
+
state: State,
|
|
236
|
+
id: string | undefined,
|
|
237
|
+
): OmlxModel | undefined {
|
|
132
238
|
return id ? state.catalog.find((model) => model.id === id) : undefined;
|
|
133
239
|
}
|
|
240
|
+
|
|
241
|
+
function sleep(ms: number): Promise<void> {
|
|
242
|
+
return new Promise((resolve) => {
|
|
243
|
+
const timer = setTimeout(resolve, ms);
|
|
244
|
+
timer.unref?.();
|
|
245
|
+
});
|
|
246
|
+
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,39 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
2
|
+
"name": "pi-omlx-picker",
|
|
3
|
+
"version": "0.2.3",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Pi extension that discovers models from a local OMLX server and registers them as a native Pi provider.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package"
|
|
9
|
+
],
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=22"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"check": "biome check *.ts *.json src test scripts && tsc --noEmit && vitest run",
|
|
15
|
+
"format": "biome check --write *.ts *.json src test scripts",
|
|
16
|
+
"typecheck": "tsc --noEmit",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:watch": "vitest",
|
|
19
|
+
"publish": "npm publish --access public"
|
|
20
|
+
},
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"@earendil-works/pi-ai": "*",
|
|
28
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@biomejs/biome": "^2.4.15",
|
|
32
|
+
"@earendil-works/pi-ai": "0.75.5",
|
|
33
|
+
"@earendil-works/pi-coding-agent": "0.75.5",
|
|
34
|
+
"@types/node": "^22.19.19",
|
|
35
|
+
"tsx": "^4.22.3",
|
|
36
|
+
"typescript": "^6.0.3",
|
|
37
|
+
"vitest": "^4.1.7"
|
|
38
|
+
}
|
|
25
39
|
}
|
package/src/auth-storage.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type ApiKeyCredential,
|
|
3
3
|
AuthStorage,
|
|
4
|
-
|
|
4
|
+
type OAuthCredential,
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
6
|
|
|
6
7
|
export const PROVIDER_KEY = "omlx";
|
|
7
8
|
|
|
8
9
|
export interface OmlxStoredCredential {
|
|
9
|
-
baseUrl
|
|
10
|
+
baseUrl?: string;
|
|
10
11
|
apiKey: string;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
type OmlxApiKeyCredential = ApiKeyCredential & { baseUrl?: string };
|
|
15
|
+
type OmlxOAuthCredential = OAuthCredential & { baseUrl?: string };
|
|
14
16
|
|
|
15
17
|
let storage: AuthStorage | undefined;
|
|
16
18
|
function getStorage(): AuthStorage {
|
|
@@ -25,10 +27,18 @@ export function _setStorageForTesting(s: AuthStorage | undefined): void {
|
|
|
25
27
|
export function loadOmlxCredential(): OmlxStoredCredential | undefined {
|
|
26
28
|
const cred = getStorage().get(PROVIDER_KEY) as
|
|
27
29
|
| OmlxApiKeyCredential
|
|
30
|
+
| OmlxOAuthCredential
|
|
28
31
|
| undefined;
|
|
29
|
-
if (!cred
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
+
if (!cred) return undefined;
|
|
33
|
+
if (cred.type === "api_key") {
|
|
34
|
+
if (!cred.key) return undefined;
|
|
35
|
+
return { baseUrl: cred.baseUrl, apiKey: cred.key };
|
|
36
|
+
}
|
|
37
|
+
if (cred.type === "oauth") {
|
|
38
|
+
if (!cred.access) return undefined;
|
|
39
|
+
return { baseUrl: cred.baseUrl, apiKey: cred.access };
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
44
|
export function saveOmlxCredential(baseUrl: string, apiKey: string): void {
|
package/src/catalog.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
|
|
5
6
|
export interface OmlxModel {
|
|
6
7
|
id: string;
|
|
@@ -441,6 +442,45 @@ function applyModelSettingsEntry(
|
|
|
441
442
|
return next;
|
|
442
443
|
}
|
|
443
444
|
|
|
445
|
+
const CACHE_FILE = join(getAgentDir(), "cache", "omlx-models.json");
|
|
446
|
+
|
|
447
|
+
interface CachedCatalog {
|
|
448
|
+
apiRoot: string;
|
|
449
|
+
models: OmlxModel[];
|
|
450
|
+
savedAt: number;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
export function readCatalogCache(apiRoot: string): OmlxModel[] | undefined {
|
|
454
|
+
const data = readCatalogCacheFile();
|
|
455
|
+
if (!data || data.apiRoot !== apiRoot) return undefined;
|
|
456
|
+
return data.models;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function readLastCatalogCache(): OmlxModel[] | undefined {
|
|
460
|
+
return readCatalogCacheFile()?.models;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function readCatalogCacheFile(): CachedCatalog | undefined {
|
|
464
|
+
try {
|
|
465
|
+
const raw = readFileSync(CACHE_FILE, "utf-8");
|
|
466
|
+
const data = JSON.parse(raw) as CachedCatalog;
|
|
467
|
+
if (!Array.isArray(data.models)) return undefined;
|
|
468
|
+
return data;
|
|
469
|
+
} catch {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function writeCatalogCache(apiRoot: string, models: OmlxModel[]): void {
|
|
475
|
+
try {
|
|
476
|
+
mkdirSync(join(getAgentDir(), "cache"), { recursive: true });
|
|
477
|
+
const data: CachedCatalog = { apiRoot, models, savedAt: Date.now() };
|
|
478
|
+
writeFileSync(CACHE_FILE, JSON.stringify(data, null, 2));
|
|
479
|
+
} catch {
|
|
480
|
+
// Ignore write errors — cache is best-effort
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
444
484
|
export function resolveLocalModelSettingsPath(
|
|
445
485
|
modelSettingsPath: string | undefined,
|
|
446
486
|
): string | undefined {
|
package/src/config.ts
CHANGED
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
import { loadOmlxCredential } from "./auth-storage.ts";
|
|
2
2
|
|
|
3
|
+
export const DEFAULT_OMLX_BASE_URL = "http://127.0.0.1:8000/v1";
|
|
4
|
+
|
|
3
5
|
export interface OmlxConfig {
|
|
4
6
|
apiRoot: string;
|
|
5
7
|
apiKeyEnvVar: string;
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
export class MissingEnvError extends Error {
|
|
9
|
-
constructor(public readonly varName: string) {
|
|
10
|
-
super(`${varName} is not set`);
|
|
11
|
-
this.name = "MissingEnvError";
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
10
|
export function normalizeBaseUrl(raw: string): string {
|
|
16
11
|
const trimmed = raw.trim().replace(/\/+$/, "");
|
|
17
12
|
if (!trimmed) throw new Error("OMLX_BASE_URL is empty");
|
|
@@ -19,30 +14,35 @@ export function normalizeBaseUrl(raw: string): string {
|
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
export function loadConfig(env: NodeJS.ProcessEnv = process.env): OmlxConfig {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
const stored = loadOmlxCredential();
|
|
18
|
+
const baseUrl = env.OMLX_BASE_URL
|
|
19
|
+
? env.OMLX_BASE_URL
|
|
20
|
+
: env.OMLX_API_KEY
|
|
21
|
+
? DEFAULT_OMLX_BASE_URL
|
|
22
|
+
: (stored?.baseUrl ?? DEFAULT_OMLX_BASE_URL);
|
|
25
23
|
return {
|
|
26
24
|
apiRoot: normalizeBaseUrl(baseUrl),
|
|
27
25
|
apiKeyEnvVar: "OMLX_API_KEY",
|
|
28
26
|
};
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
export function resolveConfiguredApiKey(
|
|
30
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
31
|
+
): string | undefined {
|
|
32
|
+
if (env.OMLX_API_KEY) return env.OMLX_API_KEY;
|
|
33
|
+
if (env.OMLX_BASE_URL) return undefined;
|
|
34
|
+
return loadOmlxCredential()?.apiKey;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Legacy helper for older stored api_key credentials. Never fills only one side
|
|
38
|
+
// of the env pair; partial shell overrides remain explicit shell state.
|
|
32
39
|
export function applyStoredCredentialToEnv(
|
|
33
40
|
env: NodeJS.ProcessEnv = process.env,
|
|
34
41
|
): boolean {
|
|
35
|
-
if (env.OMLX_BASE_URL
|
|
42
|
+
if (env.OMLX_BASE_URL || env.OMLX_API_KEY) return false;
|
|
36
43
|
const stored = loadOmlxCredential();
|
|
37
|
-
if (!stored) return false;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
applied = true;
|
|
42
|
-
}
|
|
43
|
-
if (!env.OMLX_API_KEY) {
|
|
44
|
-
env.OMLX_API_KEY = stored.apiKey;
|
|
45
|
-
applied = true;
|
|
46
|
-
}
|
|
47
|
-
return applied;
|
|
44
|
+
if (!stored?.apiKey) return false;
|
|
45
|
+
env.OMLX_BASE_URL = stored.baseUrl ?? DEFAULT_OMLX_BASE_URL;
|
|
46
|
+
env.OMLX_API_KEY = stored.apiKey;
|
|
47
|
+
return true;
|
|
48
48
|
}
|
package/src/provider.ts
CHANGED
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
type Model,
|
|
9
9
|
type SimpleStreamOptions,
|
|
10
10
|
streamSimpleOpenAICompletions,
|
|
11
|
-
} from "@
|
|
11
|
+
} from "@earendil-works/pi-ai";
|
|
12
12
|
import type {
|
|
13
13
|
ProviderConfig,
|
|
14
14
|
ProviderModelConfig,
|
|
15
|
-
} from "@
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
16
|
import type { OmlxModel } from "./catalog.ts";
|
|
17
17
|
|
|
18
18
|
// Pi's documented defaults when the server doesn't report per-model values.
|
package/.assets/demo.gif
DELETED
|
Binary file
|
package/.assets/demo.tape
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
Output demo.gif
|
|
2
|
-
|
|
3
|
-
# Visual Settings
|
|
4
|
-
Set FontSize 15
|
|
5
|
-
Set Width 900
|
|
6
|
-
Set Height 500
|
|
7
|
-
Set Padding 20
|
|
8
|
-
Set Theme "Catppuccin Mocha"
|
|
9
|
-
Set WindowBar Colorful
|
|
10
|
-
|
|
11
|
-
Sleep 1s
|
|
12
|
-
|
|
13
|
-
Type "pi"
|
|
14
|
-
Enter
|
|
15
|
-
Sleep 1.5s
|
|
16
|
-
|
|
17
|
-
Type "/mo"
|
|
18
|
-
Sleep 1.5s
|
|
19
|
-
|
|
20
|
-
Enter
|
|
21
|
-
Sleep 1.5s
|
|
22
|
-
|
|
23
|
-
Down
|
|
24
|
-
Sleep 500ms
|
|
25
|
-
Down
|
|
26
|
-
Sleep 500ms
|
|
27
|
-
Up
|
|
28
|
-
Sleep 500ms
|
|
29
|
-
|
|
30
|
-
Enter
|
|
31
|
-
Sleep 3s
|
package/.biomeignore
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
references/
|
package/.github/dependabot.yml
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
-
# package ecosystems to update and where the package manifests are located.
|
|
3
|
-
# Please see the documentation for all configuration options:
|
|
4
|
-
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
-
|
|
6
|
-
version: 2
|
|
7
|
-
updates:
|
|
8
|
-
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
-
directory: "/" # Location of package manifests
|
|
10
|
-
schedule:
|
|
11
|
-
interval: "weekly"
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Local development for `pi-omlx-picker`. User config: [docs/CONFIGURATION.md](./docs/CONFIGURATION.md).
|
|
4
|
-
|
|
5
|
-
## Setup
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
npm install
|
|
9
|
-
mise run verify
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
A live OMLX server is needed for `mise run smoke:omlx` and `mise run verify:live`.
|
|
13
|
-
|
|
14
|
-
## Tasks
|
|
15
|
-
|
|
16
|
-
Run via [`mise`](https://mise.jdx.dev/):
|
|
17
|
-
|
|
18
|
-
- `mise run verify` — Biome, TypeScript type checking, unit tests.
|
|
19
|
-
- `mise run smoke:omlx` — live OMLX probe: reasoning model, non-thinking model, tool-flow request.
|
|
20
|
-
- `mise run verify:live` — typecheck, unit tests, and live smoke (skips Biome; run `mise run verify` first if you want lint coverage).
|
|
21
|
-
- `mise run debug:omlx` — OMLX config, model path, template, log, and cache diagnostics. Pass model names after `--` to narrow, e.g. `mise run debug:omlx -- opus sonnet`.
|
|
22
|
-
- `mise run debug:pi` — Pi install, config, session, log, and cache diagnostics. Pass `install`, `config`, `sessions`, `logs`, or `cache` after `--`.
|
|
23
|
-
- `mise run debug:pi -- timeline <session-id|session-file|iso-time>` — Pi provider + OMLX server + changed-files window around a stuck turn. Use `--minutes=N` to set the window (default 3).
|
|
24
|
-
|
|
25
|
-
## Triage order
|
|
26
|
-
|
|
27
|
-
1. Read the latest provider events in `log/provider-debug.log` (or `~/.pi/packages/pi-omlx-picker/log/provider-debug.log` when installed via Pi). Look for `stream_first_delta_timeout`, `assistant_stop_diagnosis`, and the most recent `before_provider_request` / `after_provider_response` pair. The latest `mise run smoke:omlx` proof lives in `log/smoke-test/<iso-timestamp>.json`.
|
|
28
|
-
2. Inspect Pi host state: `mise run debug:pi`.
|
|
29
|
-
3. Inspect OMLX config and model files: `mise run debug:omlx`.
|
|
30
|
-
4. Run live smoke: `mise run smoke:omlx`.
|
|
31
|
-
5. Check upstream OMLX repo releases and issues before writing local workarounds.
|
|
32
|
-
6. Only then change code.
|
|
33
|
-
|
|
34
|
-
Most fixes are upstream (OMLX `model_settings.json`, `chat_template.jinja`), not in Pi-side code. New-session-OK plus takeover-broken means session state, not model capacity.
|
|
35
|
-
|
|
36
|
-
## Failure families
|
|
37
|
-
|
|
38
|
-
```text
|
|
39
|
-
symptom
|
|
40
|
-
├─ package didn't load, wrong install, stale session, compaction, takeover
|
|
41
|
-
│ └─ mise run debug:pi
|
|
42
|
-
├─ model alias, model settings, template, rope config, OMLX/HF cache
|
|
43
|
-
│ └─ mise run debug:omlx
|
|
44
|
-
├─ live model request behavior
|
|
45
|
-
│ └─ mise run smoke:omlx
|
|
46
|
-
├─ new session good, takeover bad
|
|
47
|
-
│ └─ mise run debug:pi -- sessions, then compare model_settings.json and chat_template.jinja
|
|
48
|
-
├─ local checks pass but live smoke fails
|
|
49
|
-
│ └─ check upstream OMLX repo, releases, and issues
|
|
50
|
-
└─ model not appearing in Pi /model list
|
|
51
|
-
└─ check OMLX_BASE_URL is reachable, then mise run debug:omlx
|
|
52
|
-
```
|