pi-omlx-picker 0.2.0 → 0.2.1

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.
@@ -0,0 +1,52 @@
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
+ ```
package/README.md CHANGED
@@ -10,32 +10,10 @@ pi install npm:pi-omlx-picker
10
10
 
11
11
  ## Configure
12
12
 
13
- Set these env vars (or copy `.env-example` to `.env`):
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.
14
14
 
15
- ```sh
16
- export OMLX_BASE_URL="http://127.0.0.1:8008/v1"
17
- export OMLX_API_KEY="omlx-..."
18
- ```
19
-
20
- If either is missing, the provider is skipped and Pi logs a message on startup.
21
-
22
- Optionally override the model metadata path (default: `~/.omlx/model_settings.json`):
23
-
24
- ```sh
25
- export OMLX_MODEL_SETTINGS_PATH="/path/to/model_settings.json"
26
- ```
15
+ Env-var overrides, model metadata overlay, and stream timeout knobs are documented in [docs/CONFIGURATION.md](./docs/CONFIGURATION.md).
27
16
 
28
17
  ## How it works
29
18
 
30
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`.
31
-
32
- ## Debugging
33
-
34
- ```sh
35
- mise run debug:omlx # OMLX config, model path, template, cache
36
- mise run debug:pi # Pi install, config, session, log, cache
37
- mise run verify # Biome + typecheck + unit tests
38
- mise run smoke:omlx # Live smoke test against a running OMLX server
39
- ```
40
-
41
- See [docs/DEBUG.md](docs/DEBUG.md) for triage order.
package/index.ts CHANGED
@@ -1,10 +1,11 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
+ import { PROVIDER_KEY, saveOmlxCredential } from "./src/auth-storage.ts";
2
3
  import { fetchModels, resolveLocalModelSettingsPath, type OmlxModel } from "./src/catalog.ts";
3
- import { loadConfig, MissingEnvError, type OmlxConfig } from "./src/config.ts";
4
+ import { applyStoredCredentialToEnv, loadConfig, MissingEnvError, normalizeBaseUrl, type OmlxConfig } from "./src/config.ts";
4
5
  import { toProviderConfig } from "./src/provider.ts";
5
6
  import { applyOmlxThinkingControls } from "./src/thinking.ts";
6
7
 
7
- const PROVIDER = "omlx";
8
+ const PROVIDER = PROVIDER_KEY;
8
9
  const EXTENSION_SINGLETON_KEY = Symbol.for("pi-omlx-picker/loaded");
9
10
 
10
11
  interface State {
@@ -30,6 +31,7 @@ export default async function (pi: ExtensionAPI): Promise<void> {
30
31
  modelSettingsPath: undefined,
31
32
  };
32
33
 
34
+ applyStoredCredentialToEnv();
33
35
  await refreshProvider(pi, state);
34
36
 
35
37
  pi.on("before_provider_request", (event, ctx) => {
@@ -37,8 +39,45 @@ export default async function (pi: ExtensionAPI): Promise<void> {
37
39
  const activeModel = findCatalogModel(state, ctx.model.id);
38
40
  return applyOmlxThinkingControls(event.payload, pi.getThinkingLevel(), activeModel?.thinkingDefault);
39
41
  });
42
+
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
+ },
76
+ });
40
77
  }
41
78
 
79
+ const VALIDATE_TIMEOUT_MS = 10_000;
80
+
42
81
  function clearRegisteredProvider(pi: ExtensionAPI, state: State): void {
43
82
  if (state.registered) {
44
83
  pi.unregisterProvider(PROVIDER);
package/package.json CHANGED
@@ -1,25 +1,25 @@
1
1
  {
2
- "name": "pi-omlx-picker",
3
- "version": "0.2.0",
4
- "type": "module",
5
- "description": "Pi extension that maps OMLX model settings into native Pi model metadata.",
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.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
+ }
25
25
  }
@@ -0,0 +1,41 @@
1
+ import {
2
+ type ApiKeyCredential,
3
+ AuthStorage,
4
+ } from "@mariozechner/pi-coding-agent";
5
+
6
+ export const PROVIDER_KEY = "omlx";
7
+
8
+ export interface OmlxStoredCredential {
9
+ baseUrl: string;
10
+ apiKey: string;
11
+ }
12
+
13
+ type OmlxApiKeyCredential = ApiKeyCredential & { baseUrl?: string };
14
+
15
+ let storage: AuthStorage | undefined;
16
+ function getStorage(): AuthStorage {
17
+ if (!storage) storage = AuthStorage.create();
18
+ return storage;
19
+ }
20
+
21
+ export function _setStorageForTesting(s: AuthStorage | undefined): void {
22
+ storage = s;
23
+ }
24
+
25
+ export function loadOmlxCredential(): OmlxStoredCredential | undefined {
26
+ const cred = getStorage().get(PROVIDER_KEY) as
27
+ | OmlxApiKeyCredential
28
+ | 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
+ }
33
+
34
+ export function saveOmlxCredential(baseUrl: string, apiKey: string): void {
35
+ const cred: OmlxApiKeyCredential = { type: "api_key", key: apiKey, baseUrl };
36
+ getStorage().set(PROVIDER_KEY, cred);
37
+ }
38
+
39
+ export function deleteOmlxCredential(): void {
40
+ getStorage().remove(PROVIDER_KEY);
41
+ }
package/src/catalog.ts CHANGED
@@ -545,4 +545,3 @@ function compactObject(
545
545
  });
546
546
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
547
547
  }
548
-
package/src/config.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { loadOmlxCredential } from "./auth-storage.ts";
2
+
1
3
  export interface OmlxConfig {
2
4
  apiRoot: string;
3
5
  apiKeyEnvVar: string;
@@ -25,3 +27,22 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): OmlxConfig {
25
27
  apiKeyEnvVar: "OMLX_API_KEY",
26
28
  };
27
29
  }
30
+
31
+ // Env vars win over stored creds so CI and per-shell overrides work.
32
+ export function applyStoredCredentialToEnv(
33
+ env: NodeJS.ProcessEnv = process.env,
34
+ ): boolean {
35
+ if (env.OMLX_BASE_URL && env.OMLX_API_KEY) return false;
36
+ 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;
48
+ }