@oh-my-pi/pi-coding-agent 16.1.2 → 16.1.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/CHANGELOG.md +30 -1
- package/dist/cli.js +3046 -3047
- package/dist/types/config/model-resolver.d.ts +3 -3
- package/dist/types/mnemopi/embed-client.d.ts +70 -0
- package/dist/types/mnemopi/embed-protocol.d.ts +52 -0
- package/dist/types/mnemopi/embed-worker.d.ts +12 -0
- package/dist/types/mnemopi/state.d.ts +9 -1
- package/dist/types/session/agent-storage.d.ts +2 -0
- package/dist/types/session/auth-broker-config.d.ts +3 -2
- package/dist/types/session/history-storage.d.ts +1 -1
- package/dist/types/tools/image-gen.d.ts +2 -2
- package/dist/types/utils/image-loading.d.ts +1 -1
- package/dist/types/utils/ipc.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity-auth.d.ts +37 -0
- package/package.json +12 -12
- package/src/cli.ts +8 -0
- package/src/commands/token.ts +52 -33
- package/src/config/append-only-context-mode.ts +45 -0
- package/src/config/model-discovery.ts +3 -0
- package/src/config/model-registry.ts +21 -3
- package/src/config/model-resolver.ts +31 -8
- package/src/discovery/builtin-rules/ts-no-return-type.md +0 -1
- package/src/lsp/client.ts +24 -0
- package/src/mnemopi/backend.ts +49 -3
- package/src/mnemopi/embed-client.ts +401 -0
- package/src/mnemopi/embed-protocol.ts +35 -0
- package/src/mnemopi/embed-worker.ts +113 -0
- package/src/mnemopi/state.ts +29 -1
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-selector.ts +2 -2
- package/src/modes/components/welcome.ts +1 -1
- package/src/modes/controllers/event-controller.ts +8 -0
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/theme/theme.ts +69 -0
- package/src/sdk.ts +4 -0
- package/src/session/agent-session.ts +8 -0
- package/src/session/agent-storage.ts +14 -0
- package/src/session/auth-broker-config.ts +2 -1
- package/src/session/history-storage.ts +13 -1
- package/src/stt/asr-client.ts +2 -7
- package/src/tiny/title-client.ts +2 -7
- package/src/tools/image-gen.ts +4 -8
- package/src/tools/render-utils.ts +4 -1
- package/src/tts/tts-client.ts +2 -7
- package/src/utils/image-loading.ts +12 -2
- package/src/utils/ipc.ts +38 -0
- package/src/web/search/providers/perplexity-auth.ts +133 -0
- package/src/web/search/providers/perplexity.ts +2 -125
|
@@ -201,9 +201,9 @@ export declare function resolveModelScope(patterns: string[], modelRegistry: Pic
|
|
|
201
201
|
* the result to models matching those patterns.
|
|
202
202
|
*
|
|
203
203
|
* Returns the unfiltered available list when `enabledModels` is empty.
|
|
204
|
-
* Returns an empty list when `enabledModels` is configured but no
|
|
205
|
-
*
|
|
206
|
-
*
|
|
204
|
+
* Returns an empty list when `enabledModels` is configured but no model matches
|
|
205
|
+
* any pattern — callers MUST treat this as "no usable model" rather than
|
|
206
|
+
* falling back to the global default (see issue #1022).
|
|
207
207
|
*/
|
|
208
208
|
export declare function resolveAllowedModels(modelRegistry: Pick<ModelRegistry, "getAvailable" | "getCanonicalVariants">, settings: Settings | undefined, preferences?: ModelMatchPreferences): Promise<Model<Api>[]>;
|
|
209
209
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { Subprocess } from "bun";
|
|
2
|
+
import type { MnemopiEmbedModelId, MnemopiEmbedWorkerInbound, MnemopiEmbedWorkerOutbound } from "./embed-protocol";
|
|
3
|
+
/**
|
|
4
|
+
* Abstraction over the mnemopi embeddings subprocess. The runtime
|
|
5
|
+
* implementation is a Bun child process so `onnxruntime-node`'s NAPI
|
|
6
|
+
* constructor + finalizer never run inside the main agent address space —
|
|
7
|
+
* those destructors segfault Bun on Windows when mnemopi's local embedding
|
|
8
|
+
* provider loads fastembed in the main process (issue #3031; the mnemopi
|
|
9
|
+
* sibling of the tiny-model fix from #1606 / #1607).
|
|
10
|
+
*/
|
|
11
|
+
export interface MnemopiEmbedWorkerHandle {
|
|
12
|
+
send(message: MnemopiEmbedWorkerInbound): void;
|
|
13
|
+
onMessage(handler: (message: MnemopiEmbedWorkerOutbound) => void): () => void;
|
|
14
|
+
onError(handler: (error: Error) => void): () => void;
|
|
15
|
+
terminate(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Hidden subcommand on the main CLI that boots the mnemopi embeddings worker
|
|
19
|
+
* in the spawned subprocess. Kept in sync with the dispatch in `cli.ts`.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
|
|
22
|
+
interface SpawnedSubprocess {
|
|
23
|
+
proc: Subprocess<"ignore", "ignore", "ignore">;
|
|
24
|
+
inbound: Set<(message: MnemopiEmbedWorkerOutbound) => void>;
|
|
25
|
+
errors: Set<(error: Error) => void>;
|
|
26
|
+
/**
|
|
27
|
+
* Flipped to `true` right before the deliberate SIGKILL so `onExit` can
|
|
28
|
+
* distinguish the expected hard-kill from a crash (SIGSEGV from a native
|
|
29
|
+
* fault, OOM SIGKILL, operator `kill -9`). Only the latter surfaces as a
|
|
30
|
+
* worker error so callers don't await forever.
|
|
31
|
+
*/
|
|
32
|
+
intentionalExit: {
|
|
33
|
+
value: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Spawn the mnemopi embeddings worker as a subprocess. Exported for tests and
|
|
38
|
+
* the smoke probe; production callers go through {@link spawnMnemopiEmbedWorker}.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createMnemopiEmbedSubprocess(): SpawnedSubprocess;
|
|
41
|
+
/**
|
|
42
|
+
* Per-model wrapper produced by {@link MnemopiEmbedClient.initialize}.
|
|
43
|
+
* `embed()` round-trips one batch of texts through the worker subprocess and
|
|
44
|
+
* yields the resulting vectors in a single asynchronous batch — fastembed's
|
|
45
|
+
* own iterator was emitting batches that we collect on the child side anyway,
|
|
46
|
+
* and serializing per-batch over IPC would not improve throughput.
|
|
47
|
+
*/
|
|
48
|
+
export interface MnemopiSubprocessEmbeddingModel {
|
|
49
|
+
embed(texts: string[], batchSize?: number): AsyncIterable<number[][]>;
|
|
50
|
+
}
|
|
51
|
+
export declare class MnemopiEmbedClient {
|
|
52
|
+
#private;
|
|
53
|
+
constructor(spawnWorker?: () => MnemopiEmbedWorkerHandle);
|
|
54
|
+
/**
|
|
55
|
+
* Load the named fastembed model inside the subprocess. Resolves to a
|
|
56
|
+
* thin wrapper whose `embed()` round-trips through the same worker, or
|
|
57
|
+
* `null` when the worker cannot init the model (missing peer, native
|
|
58
|
+
* load failure, etc.). Multiple calls with the same model reuse the
|
|
59
|
+
* single in-flight worker; calling with a different model loads it on
|
|
60
|
+
* the child without restarting the process.
|
|
61
|
+
*/
|
|
62
|
+
initialize(model: MnemopiEmbedModelId, cacheDir: string | undefined): Promise<MnemopiSubprocessEmbeddingModel | null>;
|
|
63
|
+
terminate(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
export declare const mnemopiEmbedClient: MnemopiEmbedClient;
|
|
66
|
+
export declare function shutdownMnemopiEmbedClient(): Promise<void>;
|
|
67
|
+
export declare function smokeTestMnemopiEmbedWorker({ timeoutMs, }?: {
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
}): Promise<void>;
|
|
70
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire types between the parent (`MnemopiEmbedClient`) and the local
|
|
3
|
+
* embeddings subprocess. The parent owns the subprocess lifecycle (graceful
|
|
4
|
+
* work, hard `SIGKILL` on shutdown); the protocol carries no explicit close
|
|
5
|
+
* handshake — once the parent decides to terminate, it signals the OS to reap
|
|
6
|
+
* the child so `onnxruntime-node`'s NAPI finalizer never runs in the main
|
|
7
|
+
* agent address space (it crashes Bun on Windows shutdown — issue #3031, the
|
|
8
|
+
* mnemopi sibling of the tiny-model fix from #1606/#1607). See
|
|
9
|
+
* `embed-client.ts` for the spawn/kill glue.
|
|
10
|
+
*/
|
|
11
|
+
/** Identifier of the fastembed model the worker should load (e.g. `fast-bge-base-en-v1.5`). */
|
|
12
|
+
export type MnemopiEmbedModelId = string;
|
|
13
|
+
export type MnemopiEmbedWorkerInbound = {
|
|
14
|
+
type: "ping";
|
|
15
|
+
id: string;
|
|
16
|
+
} | {
|
|
17
|
+
type: "init";
|
|
18
|
+
id: string;
|
|
19
|
+
model: MnemopiEmbedModelId;
|
|
20
|
+
cacheDir?: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "embed";
|
|
23
|
+
id: string;
|
|
24
|
+
model: MnemopiEmbedModelId;
|
|
25
|
+
cacheDir?: string;
|
|
26
|
+
texts: string[];
|
|
27
|
+
batchSize?: number;
|
|
28
|
+
};
|
|
29
|
+
export type MnemopiEmbedWorkerOutbound = {
|
|
30
|
+
type: "pong";
|
|
31
|
+
id: string;
|
|
32
|
+
} | {
|
|
33
|
+
type: "ready";
|
|
34
|
+
id: string;
|
|
35
|
+
} | {
|
|
36
|
+
type: "vectors";
|
|
37
|
+
id: string;
|
|
38
|
+
vectors: number[][];
|
|
39
|
+
} | {
|
|
40
|
+
type: "error";
|
|
41
|
+
id: string;
|
|
42
|
+
error: string;
|
|
43
|
+
} | {
|
|
44
|
+
type: "log";
|
|
45
|
+
level: "debug" | "warn" | "error";
|
|
46
|
+
msg: string;
|
|
47
|
+
meta?: Record<string, unknown>;
|
|
48
|
+
};
|
|
49
|
+
export interface MnemopiEmbedTransport {
|
|
50
|
+
send(message: MnemopiEmbedWorkerOutbound): void;
|
|
51
|
+
onMessage(handler: (message: MnemopiEmbedWorkerInbound) => void): () => void;
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mnemopi local-embeddings worker. Loaded inside the dedicated subprocess
|
|
3
|
+
* spawned by `embed-client.ts` (re-entered through the agent CLI's hidden
|
|
4
|
+
* `__omp_worker_mnemopi_embed` selector). The whole point of this module is
|
|
5
|
+
* that `loadFastembed()` — and therefore `onnxruntime-node`'s NAPI
|
|
6
|
+
* constructor + finalizer — only ever runs in this child address space. The
|
|
7
|
+
* parent `SIGKILL`s us on shutdown so the destructor that crashes Bun on
|
|
8
|
+
* Windows shutdown (issue #3031, mnemopi sibling of #1606/#1607) never runs
|
|
9
|
+
* in either process.
|
|
10
|
+
*/
|
|
11
|
+
import type { MnemopiEmbedTransport } from "./embed-protocol";
|
|
12
|
+
export declare function startMnemopiEmbedWorker(transport: MnemopiEmbedTransport): void;
|
|
@@ -4,7 +4,15 @@ import type { Mnemopi, RecallResult } from "@oh-my-pi/pi-mnemopi";
|
|
|
4
4
|
import type * as MnemopiCoreNs from "@oh-my-pi/pi-mnemopi/core";
|
|
5
5
|
import type { AgentSession } from "../session/agent-session";
|
|
6
6
|
import type { MnemopiBackendConfig } from "./config";
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Lazily load `@oh-my-pi/pi-mnemopi` (memoized) and route fastembed loads
|
|
9
|
+
* through the dedicated embeddings subprocess. The override is installed once
|
|
10
|
+
* — before any consumer gets the chance to call `embed()` — so
|
|
11
|
+
* `onnxruntime-node`'s NAPI constructor + finalizer never run inside the
|
|
12
|
+
* agent's address space (issue #3031). Test seams that swap the initializer
|
|
13
|
+
* with `setLocalModelInitializerForTests` still win because both go through
|
|
14
|
+
* the same module-level slot.
|
|
15
|
+
*/
|
|
8
16
|
export declare function loadMnemopi(): Promise<typeof MnemopiNs>;
|
|
9
17
|
/** Lazily load `@oh-my-pi/pi-mnemopi/core` (memoized). */
|
|
10
18
|
export declare function loadMnemopiCore(): Promise<typeof MnemopiCoreNs>;
|
|
@@ -16,6 +16,8 @@ export declare class AgentStorage {
|
|
|
16
16
|
* @returns AgentStorage instance for the given path
|
|
17
17
|
*/
|
|
18
18
|
static open(dbPath?: string): Promise<AgentStorage>;
|
|
19
|
+
/** @internal Reset all singletons and close their databases — test-only. */
|
|
20
|
+
static resetInstance(): void;
|
|
19
21
|
/**
|
|
20
22
|
* Reads legacy settings persisted in the agent.db `settings` table.
|
|
21
23
|
* The canonical settings store is `config.yml`; this accessor only
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
* `runRootCommand`, and we want hand-edited config entries to be honoured at
|
|
21
21
|
* boot without forcing a startup reorder.
|
|
22
22
|
*/
|
|
23
|
-
import { type AuthBrokerClientConfig, type DiscoverAuthStorageOptions,
|
|
23
|
+
import { type AuthBrokerClientConfig, type DiscoverAuthStorageOptions, getAuthBrokerTokenFilePath } from "@oh-my-pi/pi-ai/auth-broker/discover";
|
|
24
|
+
import type { AuthStorage } from "./auth-storage";
|
|
24
25
|
export { type AuthBrokerClientConfig, getAuthBrokerTokenFilePath };
|
|
25
26
|
/**
|
|
26
27
|
* Read broker configuration. Returns null when the URL is missing
|
|
@@ -41,4 +42,4 @@ export declare function resolveAuthBrokerConfig(): Promise<AuthBrokerClientConfi
|
|
|
41
42
|
*
|
|
42
43
|
* Default `agentDir` is the current configured agent directory.
|
|
43
44
|
*/
|
|
44
|
-
export declare function discoverAuthStorage(agentDir?: string, options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">):
|
|
45
|
+
export declare function discoverAuthStorage(agentDir?: string, options?: Omit<DiscoverAuthStorageOptions, "agentDir" | "configValueResolver">): Promise<AuthStorage>;
|
|
@@ -10,7 +10,7 @@ export declare class HistoryStorage {
|
|
|
10
10
|
#private;
|
|
11
11
|
private constructor();
|
|
12
12
|
static open(dbPath?: string): HistoryStorage;
|
|
13
|
-
/** @internal Reset the singleton — test-only. */
|
|
13
|
+
/** @internal Reset the singleton and close its database — test-only. */
|
|
14
14
|
static resetInstance(): void;
|
|
15
15
|
/**
|
|
16
16
|
* Register a resolver that supplies the current session ID for prompts added
|
|
@@ -61,6 +61,6 @@ export declare function isImageProviderPreference(value: unknown): value is Imag
|
|
|
61
61
|
/** Set the preferred image provider from settings */
|
|
62
62
|
export declare function setPreferredImageProvider(provider: ImageProviderPreference): void;
|
|
63
63
|
export declare const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails>;
|
|
64
|
-
export declare function getImageGenTools(
|
|
65
|
-
export declare function getImageGenToolsWithRegistry(
|
|
64
|
+
export declare function getImageGenTools(_modelRegistry?: ModelRegistry, _activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
|
|
65
|
+
export declare function getImageGenToolsWithRegistry(_modelRegistry: ModelRegistry, _activeModel?: Model): Promise<Array<CustomTool<typeof imageGenSchema, ImageGenToolDetails>>>;
|
|
66
66
|
export {};
|
|
@@ -8,7 +8,7 @@ export declare const SUPPORTED_INPUT_IMAGE_MIME_TYPES: Set<string>;
|
|
|
8
8
|
* with an opaque HTTP 400. Detect those models so the resize pipeline encodes
|
|
9
9
|
* to PNG/JPEG instead — the automatic equivalent of `OMP_NO_WEBP=1`.
|
|
10
10
|
*/
|
|
11
|
-
export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api"> | undefined): boolean;
|
|
11
|
+
export declare function modelLacksWebpSupport(model: Pick<Model, "provider" | "api" | "imageInputDecoder"> | undefined): boolean;
|
|
12
12
|
/**
|
|
13
13
|
* `true` when `model` cannot decode WebP, otherwise `undefined` so the
|
|
14
14
|
* `OMP_NO_WEBP` env fallback in {@link resizeImage} still applies. Feed straight
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow a value to a thenable so a rejection handler can be attached.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the local helper in `mcp/transports/stdio.ts` (kept separate because
|
|
5
|
+
* that copy serves the FileSink stdin-write path and is battle-tested there).
|
|
6
|
+
* This shared copy is the home for the IPC `send()` sites.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isThenable(value: unknown): value is PromiseLike<unknown>;
|
|
9
|
+
/**
|
|
10
|
+
* Send a message to a Bun subprocess over IPC, neutralizing both the
|
|
11
|
+
* synchronous throw ("cannot be used after the process has exited") and any
|
|
12
|
+
* asynchronous rejection (EPIPE from a pipe that broke between exit being
|
|
13
|
+
* observed and the next `send()`). The dead worker is detected separately via
|
|
14
|
+
* `onExit`/`onError` and respawned or disabled by the owning client; an
|
|
15
|
+
* un-awaited EPIPE rejection must not escape as a fatal unhandled rejection
|
|
16
|
+
* that takes down the whole session. See issue #2997.
|
|
17
|
+
*
|
|
18
|
+
* `label` prefixes the debug log on synchronous failure (e.g. "tts").
|
|
19
|
+
*/
|
|
20
|
+
export declare function safeSend(proc: {
|
|
21
|
+
send(message: unknown): unknown;
|
|
22
|
+
}, message: unknown, label: string): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { AuthStorage, OAuthAccess } from "@oh-my-pi/pi-ai";
|
|
2
|
+
export declare const PERPLEXITY_CHAT_BASE_URL = "https://api.perplexity.ai";
|
|
3
|
+
export declare const PERPLEXITY_RESPONSES_BASE_URL = "https://api.perplexity.ai/v1";
|
|
4
|
+
export declare const OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
|
|
5
|
+
export declare const OAUTH_EXPIRY_BUFFER_MS: number;
|
|
6
|
+
export interface ApiConfig {
|
|
7
|
+
type: "api_key";
|
|
8
|
+
apiKey: string;
|
|
9
|
+
provider: "perplexity" | "openrouter";
|
|
10
|
+
chatBaseUrl: string;
|
|
11
|
+
responsesBaseUrl: string;
|
|
12
|
+
modelPrefix: string;
|
|
13
|
+
useResponses: boolean;
|
|
14
|
+
}
|
|
15
|
+
export type PerplexityAuth = ApiConfig | {
|
|
16
|
+
type: "oauth";
|
|
17
|
+
access: OAuthAccess;
|
|
18
|
+
} | {
|
|
19
|
+
type: "cookies";
|
|
20
|
+
cookies: string;
|
|
21
|
+
} | {
|
|
22
|
+
type: "anonymous";
|
|
23
|
+
};
|
|
24
|
+
export interface PerplexityAuthOptions {
|
|
25
|
+
signal?: AbortSignal;
|
|
26
|
+
forceRefresh?: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** Detect API-key endpoints to try in priority order (Perplexity direct, then OpenRouter). */
|
|
29
|
+
export declare function getApiConfigs(authStorage: AuthStorage, sessionId: string | undefined, options?: PerplexityAuthOptions): Promise<ApiConfig[]>;
|
|
30
|
+
/**
|
|
31
|
+
* Decode a Perplexity JWT's `exp` claim, in ms. Returns `undefined` when the
|
|
32
|
+
* token has no `exp` (which is the common case — Perplexity sessions are
|
|
33
|
+
* server-side and effectively non-expiring from the client's POV).
|
|
34
|
+
*/
|
|
35
|
+
export declare function jwtExpiryMs(token: string): number | undefined;
|
|
36
|
+
/** Collect all available auth methods to try in priority order */
|
|
37
|
+
export declare function getAvailableAuthMethods(authStorage: AuthStorage, sessionId: string | undefined, options?: PerplexityAuthOptions): Promise<PerplexityAuth[]>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.3",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -48,17 +48,17 @@
|
|
|
48
48
|
"@agentclientprotocol/sdk": "0.25.0",
|
|
49
49
|
"@babel/parser": "^7.29.7",
|
|
50
50
|
"@mozilla/readability": "^0.6.0",
|
|
51
|
-
"@oh-my-pi/hashline": "16.1.
|
|
52
|
-
"@oh-my-pi/omp-stats": "16.1.
|
|
53
|
-
"@oh-my-pi/pi-agent-core": "16.1.
|
|
54
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
55
|
-
"@oh-my-pi/pi-catalog": "16.1.
|
|
56
|
-
"@oh-my-pi/pi-mnemopi": "16.1.
|
|
57
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
58
|
-
"@oh-my-pi/pi-tui": "16.1.
|
|
59
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
60
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
61
|
-
"@oh-my-pi/snapcompact": "16.1.
|
|
51
|
+
"@oh-my-pi/hashline": "16.1.3",
|
|
52
|
+
"@oh-my-pi/omp-stats": "16.1.3",
|
|
53
|
+
"@oh-my-pi/pi-agent-core": "16.1.3",
|
|
54
|
+
"@oh-my-pi/pi-ai": "16.1.3",
|
|
55
|
+
"@oh-my-pi/pi-catalog": "16.1.3",
|
|
56
|
+
"@oh-my-pi/pi-mnemopi": "16.1.3",
|
|
57
|
+
"@oh-my-pi/pi-natives": "16.1.3",
|
|
58
|
+
"@oh-my-pi/pi-tui": "16.1.3",
|
|
59
|
+
"@oh-my-pi/pi-utils": "16.1.3",
|
|
60
|
+
"@oh-my-pi/pi-wire": "16.1.3",
|
|
61
|
+
"@oh-my-pi/snapcompact": "16.1.3",
|
|
62
62
|
"@opentelemetry/api": "^1.9.1",
|
|
63
63
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
64
64
|
"@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
|
package/src/cli.ts
CHANGED
|
@@ -68,6 +68,7 @@ async function runSmokeTest(): Promise<void> {
|
|
|
68
68
|
const { smokeTestTinyTitleWorker } = await import("./tiny/title-client");
|
|
69
69
|
const { smokeTestSttWorker } = await import("./stt/asr-client");
|
|
70
70
|
const { smokeTestTtsWorker } = await import("./tts/tts-client");
|
|
71
|
+
const { smokeTestMnemopiEmbedWorker } = await import("./mnemopi/embed-client");
|
|
71
72
|
const { smokeTestJsEvalWorker } = await import("./eval/js/context-manager");
|
|
72
73
|
await smokeTestSyncWorker();
|
|
73
74
|
|
|
@@ -87,6 +88,7 @@ async function runSmokeTest(): Promise<void> {
|
|
|
87
88
|
await smokeTestSttWorker();
|
|
88
89
|
await smokeTestJsEvalWorker();
|
|
89
90
|
await smokeTestTtsWorker();
|
|
91
|
+
await smokeTestMnemopiEmbedWorker();
|
|
90
92
|
process.stdout.write("smoke-test: ok\n");
|
|
91
93
|
}
|
|
92
94
|
|
|
@@ -96,6 +98,7 @@ const TAB_WORKER_ARG = "__omp_worker_tab";
|
|
|
96
98
|
const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
|
|
97
99
|
const STT_WORKER_ARG = "__omp_worker_stt";
|
|
98
100
|
const TTS_WORKER_ARG = "__omp_worker_tts";
|
|
101
|
+
const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
|
|
99
102
|
|
|
100
103
|
async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
101
104
|
if (arg === TINY_WORKER_ARG) {
|
|
@@ -151,6 +154,11 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
|
|
|
151
154
|
await runIpcSubprocessWorker(startTtsWorker);
|
|
152
155
|
return true;
|
|
153
156
|
}
|
|
157
|
+
if (arg === MNEMOPI_EMBED_WORKER_ARG) {
|
|
158
|
+
const { startMnemopiEmbedWorker } = await import("./mnemopi/embed-worker");
|
|
159
|
+
await runIpcSubprocessWorker(startMnemopiEmbedWorker);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
154
162
|
return false;
|
|
155
163
|
}
|
|
156
164
|
|
package/src/commands/token.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
|
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
import { isAuthenticated, ModelRegistry } from "../config/model-registry";
|
|
9
9
|
import { discoverAuthStorage } from "../sdk";
|
|
10
|
+
import { getAvailableAuthMethods } from "../web/search/providers/perplexity-auth";
|
|
10
11
|
|
|
11
12
|
export default class Token extends Command {
|
|
12
13
|
static description = "Get the API key or OAuth token for a provider";
|
|
@@ -41,49 +42,67 @@ export default class Token extends Command {
|
|
|
41
42
|
const provider = providerName.toLowerCase();
|
|
42
43
|
|
|
43
44
|
const authStorage = await discoverAuthStorage();
|
|
44
|
-
|
|
45
|
+
try {
|
|
46
|
+
const modelRegistry = new ModelRegistry(authStorage);
|
|
45
47
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
forceRefresh: flags["force-refresh"],
|
|
49
|
-
});
|
|
48
|
+
// Resolve the API key / token
|
|
49
|
+
let apiKey: string | undefined;
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
if (provider === "perplexity") {
|
|
52
|
+
const methods = await getAvailableAuthMethods(authStorage, undefined, {
|
|
53
|
+
forceRefresh: flags["force-refresh"],
|
|
54
|
+
});
|
|
55
|
+
const printable = methods.find(m => m.type === "oauth" || m.type === "api_key");
|
|
56
|
+
if (printable) {
|
|
57
|
+
apiKey = printable.type === "oauth" ? printable.access.accessToken : printable.apiKey;
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
}
|
|
60
|
+
|
|
61
|
+
if (!apiKey) {
|
|
62
|
+
apiKey = await modelRegistry.getApiKeyForProvider(provider, undefined, {
|
|
63
|
+
forceRefresh: flags["force-refresh"],
|
|
64
|
+
});
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
if (!isAuthenticated(apiKey)) {
|
|
68
|
+
// Find all active/configured providers
|
|
69
|
+
const activeProviders = new Set<string>();
|
|
70
|
+
for (const p of PROVIDER_REGISTRY) {
|
|
71
|
+
if (authStorage.hasAuth(p.id)) {
|
|
72
|
+
activeProviders.add(p.id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const all = authStorage.getAll();
|
|
76
|
+
for (const p in all) {
|
|
77
|
+
if (authStorage.hasAuth(p)) {
|
|
78
|
+
activeProviders.add(p);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const msg = `No active credential found for provider "${providerName}".`;
|
|
83
|
+
process.stderr.write(`${chalk.red(msg)}\n`);
|
|
84
|
+
if (activeProviders.size > 0) {
|
|
85
|
+
process.stderr.write(`Configured providers: ${Array.from(activeProviders).sort().join(", ")}\n`);
|
|
86
|
+
}
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
return;
|
|
70
89
|
}
|
|
71
|
-
process.exitCode = 1;
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
90
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
91
|
+
if (!flags.raw) {
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(apiKey);
|
|
94
|
+
if (parsed && typeof parsed === "object" && typeof parsed.token === "string") {
|
|
95
|
+
process.stdout.write(`${parsed.token}\n`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// Not a JSON string, print as-is
|
|
81
100
|
}
|
|
82
|
-
} catch {
|
|
83
|
-
// Not a JSON string, print as-is
|
|
84
101
|
}
|
|
85
|
-
}
|
|
86
102
|
|
|
87
|
-
|
|
103
|
+
process.stdout.write(`${apiKey}\n`);
|
|
104
|
+
} finally {
|
|
105
|
+
authStorage.close();
|
|
106
|
+
}
|
|
88
107
|
}
|
|
89
108
|
}
|
|
@@ -8,10 +8,55 @@ export interface AppendOnlyContextModel {
|
|
|
8
8
|
compatConfig?: object;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Local model servers (Ollama, LM Studio, llama.cpp, vLLM, sglang, …) all
|
|
13
|
+
* rely on llama.cpp-style prefix KV-cache reuse: identical leading tokens
|
|
14
|
+
* skip re-prefill on the next request. Append-only mode is the only way to
|
|
15
|
+
* guarantee byte-stable bytes across turns, since the live system prompt,
|
|
16
|
+
* tool catalogue, and message log all flow through fresh allocations every
|
|
17
|
+
* step (see `agent-loop.ts` `streamAssistantResponse` fallback path).
|
|
18
|
+
*/
|
|
19
|
+
const LOCAL_INFERENCE_PROVIDERS = new Set(["ollama", "ollama-cloud", "lm-studio", "llama.cpp"]);
|
|
20
|
+
|
|
21
|
+
/** True when `baseUrl` resolves to a loopback or RFC1918 host — covers
|
|
22
|
+
* llama.cpp/vLLM/sglang servers registered under a user-defined provider id
|
|
23
|
+
* via `models.yaml`. Built-in local provider ids (`ollama`, `lm-studio`,
|
|
24
|
+
* `llama.cpp`) are already handled by `LOCAL_INFERENCE_PROVIDERS`.
|
|
25
|
+
* Substring match on the parsed hostname only; ports, paths, and unparseable
|
|
26
|
+
* URLs return false.
|
|
27
|
+
*/
|
|
28
|
+
function hasLocalLoopbackBaseUrl(baseUrl: string | undefined): boolean {
|
|
29
|
+
if (!baseUrl) return false;
|
|
30
|
+
let hostname: string;
|
|
31
|
+
try {
|
|
32
|
+
hostname = new URL(baseUrl).hostname.toLowerCase();
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
hostname === "localhost" ||
|
|
38
|
+
hostname === "127.0.0.1" ||
|
|
39
|
+
hostname === "0.0.0.0" ||
|
|
40
|
+
hostname === "::1" ||
|
|
41
|
+
hostname === "[::1]"
|
|
42
|
+
) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
// RFC1918 private IPv4 ranges.
|
|
46
|
+
if (/^10\./.test(hostname)) return true;
|
|
47
|
+
if (/^192\.168\./.test(hostname)) return true;
|
|
48
|
+
if (/^172\.(1[6-9]|2[0-9]|3[01])\./.test(hostname)) return true;
|
|
49
|
+
// Common ".local" mDNS hostnames used for home-LAN llama.cpp boxes.
|
|
50
|
+
if (hostname.endsWith(".local")) return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
11
54
|
function shouldAutoEnableAppendOnlyContext(model: AppendOnlyContextModel | null | undefined): boolean {
|
|
12
55
|
if (!model) return false;
|
|
13
56
|
if (model.provider === "deepseek") return true;
|
|
57
|
+
if (LOCAL_INFERENCE_PROVIDERS.has(model.provider)) return true;
|
|
14
58
|
if (hostMatchesUrl(model.baseUrl, "xiaomi")) return true;
|
|
59
|
+
if (hasLocalLoopbackBaseUrl(model.baseUrl)) return true;
|
|
15
60
|
return !!model.compatConfig && "supportsStore" in model.compatConfig && model.compatConfig.supportsStore === true;
|
|
16
61
|
}
|
|
17
62
|
|
|
@@ -275,6 +275,7 @@ export async function discoverOllamaModels(
|
|
|
275
275
|
baseUrl: `${endpoint}/v1`,
|
|
276
276
|
reasoning: metadata?.reasoning ?? false,
|
|
277
277
|
input: metadata?.input ?? ["text"],
|
|
278
|
+
imageInputDecoder: "stb",
|
|
278
279
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
279
280
|
contextWindow: metadata?.contextWindow ?? 128000,
|
|
280
281
|
maxTokens: Math.min(metadata?.contextWindow ?? Number.POSITIVE_INFINITY, DISCOVERY_DEFAULT_MAX_TOKENS),
|
|
@@ -352,6 +353,7 @@ export async function discoverLlamaCppModels(
|
|
|
352
353
|
baseUrl,
|
|
353
354
|
reasoning: false,
|
|
354
355
|
input: serverMetadata?.input ?? ["text"],
|
|
356
|
+
imageInputDecoder: "stb",
|
|
355
357
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
356
358
|
contextWindow: serverMetadata?.contextWindow ?? 128000,
|
|
357
359
|
maxTokens: Math.min(
|
|
@@ -424,6 +426,7 @@ export async function discoverOpenAIModelsList(
|
|
|
424
426
|
baseUrl,
|
|
425
427
|
reasoning: false,
|
|
426
428
|
input: nativeMetadataForModel?.input ?? ["text"],
|
|
429
|
+
...(providerConfig.discovery.type === "lm-studio" ? { imageInputDecoder: "stb" as const } : {}),
|
|
427
430
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
428
431
|
contextWindow,
|
|
429
432
|
maxTokens: Math.min(contextWindow, discoveryDefaultMaxTokens(providerConfig.api)),
|
|
@@ -900,6 +900,7 @@ export class ModelRegistry {
|
|
|
900
900
|
...replacementModel,
|
|
901
901
|
contextWindow: replacementModel.contextWindow ?? existing.contextWindow,
|
|
902
902
|
maxTokens: replacementModel.maxTokens ?? existing.maxTokens,
|
|
903
|
+
omitMaxOutputTokens: replacementModel.omitMaxOutputTokens ?? existing.omitMaxOutputTokens,
|
|
903
904
|
...(supportsTools !== undefined ? { supportsTools } : {}),
|
|
904
905
|
};
|
|
905
906
|
});
|
|
@@ -1023,12 +1024,21 @@ export class ModelRegistry {
|
|
|
1023
1024
|
}
|
|
1024
1025
|
|
|
1025
1026
|
#normalizeDiscoverableModels(providerConfig: DiscoveryProviderConfig, models: Model<Api>[]): Model<Api>[] {
|
|
1027
|
+
const withDecoderMetadata =
|
|
1028
|
+
providerConfig.discovery.type === "ollama" ||
|
|
1029
|
+
providerConfig.discovery.type === "llama.cpp" ||
|
|
1030
|
+
providerConfig.discovery.type === "lm-studio"
|
|
1031
|
+
? models.map(model =>
|
|
1032
|
+
buildModel({ ...model, imageInputDecoder: "stb", compat: model.compatConfig } as ModelSpec<Api>),
|
|
1033
|
+
)
|
|
1034
|
+
: models;
|
|
1035
|
+
|
|
1026
1036
|
if (providerConfig.provider !== "ollama" || providerConfig.api !== "openai-responses") {
|
|
1027
|
-
return
|
|
1037
|
+
return withDecoderMetadata;
|
|
1028
1038
|
}
|
|
1029
1039
|
|
|
1030
1040
|
const contextLengthOverride = getOllamaContextLengthOverride();
|
|
1031
|
-
return
|
|
1041
|
+
return withDecoderMetadata.map(model => {
|
|
1032
1042
|
const normalized =
|
|
1033
1043
|
model.api === "openai-completions"
|
|
1034
1044
|
? buildModel({
|
|
@@ -1269,7 +1279,12 @@ export class ModelRegistry {
|
|
|
1269
1279
|
models: cached?.models.map(model => model.id) ?? [],
|
|
1270
1280
|
});
|
|
1271
1281
|
this.#lastDiscoveryWarnings.delete(providerConfig.provider);
|
|
1272
|
-
return cached
|
|
1282
|
+
return cached
|
|
1283
|
+
? this.#normalizeDiscoverableModels(
|
|
1284
|
+
providerConfig,
|
|
1285
|
+
cached.models.map(model => buildModel(model)),
|
|
1286
|
+
)
|
|
1287
|
+
: [];
|
|
1273
1288
|
}
|
|
1274
1289
|
}
|
|
1275
1290
|
|
|
@@ -1569,6 +1584,9 @@ export class ModelRegistry {
|
|
|
1569
1584
|
}
|
|
1570
1585
|
#applyHardcodedModelPolicies(models: Model<Api>[]): Model<Api>[] {
|
|
1571
1586
|
return models.map(model => {
|
|
1587
|
+
if (model.provider === "ollama-cloud" && model.omitMaxOutputTokens !== true) {
|
|
1588
|
+
model = applyModelOverride(model, { omitMaxOutputTokens: true });
|
|
1589
|
+
}
|
|
1572
1590
|
if (model.id !== "gpt-5.4" || model.provider === "github-copilot") {
|
|
1573
1591
|
return model;
|
|
1574
1592
|
}
|