pi-omlx-picker 0.2.1 → 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 CHANGED
@@ -1,19 +1,40 @@
1
1
  # Pi OMLX Picker
2
2
 
3
- Pi extension that discovers models from a local [OMLX](https://github.com/Open-Model-Lookup-Exchange) server and registers them as a native Pi provider. Switch models with Pi's built-in `/model` command.
3
+ > Seamlessly integrate your local [oMLX](https://github.com/jundot/omlx) models into Pi.
4
4
 
5
- ## Install
5
+ This extension discovers models from a local OMLX server and registers them as native Pi providers. Switch between your local and remote models effortlessly using Pi's built-in `/model` command.
6
+
7
+ ![Pi OMLX Picker Demo](./.assets/demo.gif)
8
+
9
+ ## ✨ Features
10
+
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
+
15
+ ## 📦 Installation
6
16
 
7
17
  ```sh
8
18
  pi install npm:pi-omlx-picker
9
19
  ```
10
20
 
11
- ## Configure
21
+ ## 🛠️ Development
12
22
 
13
- Run `/omlx-login` in Pi and paste your OMLX base URL and API key. That's it. Re-run `/omlx-login` to change credentials.
23
+ - `npm install`
24
+ - `npm run check`
25
+ - `npm run format`
26
+ - `npm test`
27
+ - `npm run test:watch`
14
28
 
15
- Env-var overrides, model metadata overlay, and stream timeout knobs are documented in [docs/CONFIGURATION.md](./docs/CONFIGURATION.md).
29
+ ## 🚀 Quick Start
30
+
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.
16
35
 
17
- ## How it works
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.
18
37
 
19
- On startup, the extension fetches available models from OMLX, merges local `model_settings.json` metadata, and registers an `omlx` provider in Pi. Thinking controls are applied per-request based on each model's `thinkingDefault`.
38
+ ## ⚙️ Configuration
39
+
40
+ Env-var overrides, model metadata overlay, and stream timeout knobs are documented in [docs/CONFIGURATION.md](./docs/CONFIGURATION.md).
package/index.ts CHANGED
@@ -1,23 +1,52 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
- import { PROVIDER_KEY, saveOmlxCredential } from "./src/auth-storage.ts";
3
- import { fetchModels, resolveLocalModelSettingsPath, type OmlxModel } from "./src/catalog.ts";
4
- import { applyStoredCredentialToEnv, loadConfig, MissingEnvError, normalizeBaseUrl, type OmlxConfig } from "./src/config.ts";
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 async function (pi: ExtensionAPI): Promise<void> {
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
- applyStoredCredentialToEnv();
35
- await refreshProvider(pi, state);
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(event.payload, pi.getThinkingLevel(), activeModel?.thinkingDefault);
72
+ return applyOmlxThinkingControls(
73
+ event.payload,
74
+ pi.getThinkingLevel(),
75
+ activeModel?.thinkingDefault,
76
+ );
41
77
  });
42
78
 
43
- pi.registerCommand("omlx-login", {
44
- description: "Sign in to OMLX (set base URL and API key)",
45
- handler: async (_args, ctx) => {
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
- const VALIDATE_TIMEOUT_MS = 10_000;
80
-
81
- function clearRegisteredProvider(pi: ExtensionAPI, state: State): void {
82
- if (state.registered) {
83
- pi.unregisterProvider(PROVIDER);
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
- async function refreshProvider(pi: ExtensionAPI, state: State): Promise<void> {
92
- let config: OmlxConfig;
104
+ function tryLoadConfig(): OmlxConfig | undefined {
93
105
  try {
94
- config = loadConfig();
95
- } catch (err) {
96
- state.lastError =
97
- err instanceof MissingEnvError
98
- ? `${err.varName} is not set`
99
- : err instanceof Error
100
- ? err.message
101
- : String(err);
102
- clearRegisteredProvider(pi, state);
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
- const modelSettingsPath = resolveLocalModelSettingsPath(process.env.OMLX_MODEL_SETTINGS_PATH);
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, process.env.OMLX_API_KEY!, { modelSettingsPath });
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
- clearRegisteredProvider(pi, state);
119
- return;
226
+ return "failed";
120
227
  }
121
228
 
122
- pi.registerProvider(PROVIDER, toProviderConfig(config.apiRoot, config.apiKeyEnvVar, models));
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;
229
+ writeCatalogCache(config.apiRoot, models);
230
+ registerModels(pi, state, config, models, modelSettingsPath);
231
+ return "registered";
129
232
  }
130
233
 
131
- function findCatalogModel(state: State, id: string | undefined): OmlxModel | undefined {
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
- "name": "pi-omlx-picker",
3
- "version": "0.2.1",
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
- "scripts": {
8
- "test": "node --import tsx --test 'test/*.test.ts'",
9
- "debug:omlx": "tsx scripts/debug-omlx.ts",
10
- "debug:pi": "tsx scripts/debug-pi.ts",
11
- "smoke:omlx": "tsx scripts/smoke-live-omlx.ts",
12
- "typecheck": "tsc --noEmit"
13
- },
14
- "pi": {
15
- "extensions": [
16
- "./index.ts"
17
- ]
18
- },
19
- "devDependencies": {
20
- "@mariozechner/pi-ai": "^0.73.1",
21
- "@mariozechner/pi-coding-agent": "*",
22
- "tsx": "^4.7.0",
23
- "typescript": "^6.0.3"
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
  }
@@ -1,16 +1,18 @@
1
1
  import {
2
2
  type ApiKeyCredential,
3
3
  AuthStorage,
4
- } from "@mariozechner/pi-coding-agent";
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: string;
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 || cred.type !== "api_key") return undefined;
30
- if (!cred.baseUrl || !cred.key) return undefined;
31
- return { baseUrl: cred.baseUrl, apiKey: cred.key };
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 baseUrl = env.OMLX_BASE_URL;
23
- if (!baseUrl) throw new MissingEnvError("OMLX_BASE_URL");
24
- if (!env.OMLX_API_KEY) throw new MissingEnvError("OMLX_API_KEY");
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
- // Env vars win over stored creds so CI and per-shell overrides work.
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 && env.OMLX_API_KEY) return false;
42
+ if (env.OMLX_BASE_URL || env.OMLX_API_KEY) return false;
36
43
  const stored = loadOmlxCredential();
37
- if (!stored) return false;
38
- let applied = false;
39
- if (!env.OMLX_BASE_URL) {
40
- env.OMLX_BASE_URL = stored.baseUrl;
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 "@mariozechner/pi-ai";
11
+ } from "@earendil-works/pi-ai";
12
12
  import type {
13
13
  ProviderConfig,
14
14
  ProviderModelConfig,
15
- } from "@mariozechner/pi-coding-agent";
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/.biomeignore DELETED
@@ -1 +0,0 @@
1
- references/
@@ -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
- ```