@oh-my-pi/pi-ai 16.0.7 → 16.0.8
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 +9 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/utils/validation.d.ts +2 -1
- package/package.json +3 -3
- package/src/auth-broker/snapshot-cache.ts +21 -5
- package/src/auth-storage.ts +13 -4
- package/src/index.ts +2 -2
- package/src/providers/cursor.ts +37 -29
- package/src/utils/validation.ts +12 -7
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [16.0.8] - 2026-06-18
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Improved reliability of auth-broker snapshot loading by implementing a robust manual schema check
|
|
10
|
+
- Fixed MCP tool argument validation to drop optional empty-string parameters before schema validation, matching the existing optional null handling and avoiding pattern/type failures for omitted model-filled fields. ([#2981](https://github.com/can1357/oh-my-pi/issues/2981))
|
|
11
|
+
- Fixed API-key credential replacement to hard-delete superseded disabled `api_key` rows so `auth_credentials` does not grow indefinitely after key rotation. ([#2941](https://github.com/can1357/oh-my-pi/issues/2941))
|
|
12
|
+
- Fixed Cursor provider streaming to close text blocks before tool calls so post-tool text opens a new content block and TUI transcript cards render inline instead of grouped near the bottom. ([#2924](https://github.com/can1357/oh-my-pi/issues/2924))
|
|
13
|
+
|
|
5
14
|
## [16.0.7] - 2026-06-18
|
|
6
15
|
|
|
7
16
|
### Changed
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type Type, type } from "arktype";
|
|
2
2
|
export { type ZodType, z } from "zod/v4";
|
|
3
3
|
export * from "./api-registry";
|
|
4
|
-
export * from "./auth-broker";
|
|
5
|
-
export {
|
|
4
|
+
export type * from "./auth-broker";
|
|
5
|
+
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server";
|
|
6
6
|
export * from "./auth-gateway/types";
|
|
7
7
|
export * from "./auth-retry";
|
|
8
8
|
export * from "./auth-storage";
|
|
@@ -10,7 +10,8 @@ export declare function validateToolCall(tools: Tool[], toolCall: ToolCall): Too
|
|
|
10
10
|
/**
|
|
11
11
|
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
12
12
|
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
13
|
-
* containers, null-for-optional, null-for-default) before
|
|
13
|
+
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
14
|
+
* declaring failure.
|
|
14
15
|
*
|
|
15
16
|
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
16
17
|
*/
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-ai",
|
|
4
|
-
"version": "16.0.
|
|
4
|
+
"version": "16.0.8",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@bufbuild/protobuf": "^2.12.0",
|
|
41
|
-
"@oh-my-pi/pi-catalog": "16.0.
|
|
42
|
-
"@oh-my-pi/pi-utils": "16.0.
|
|
41
|
+
"@oh-my-pi/pi-catalog": "16.0.8",
|
|
42
|
+
"@oh-my-pi/pi-utils": "16.0.8",
|
|
43
43
|
"arktype": "^2.2.0",
|
|
44
44
|
"partial-json": "^0.1.7",
|
|
45
45
|
"zod": "^4"
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
import * as fs from "node:fs/promises";
|
|
10
10
|
import * as path from "node:path";
|
|
11
11
|
import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
12
|
-
import { type } from "arktype";
|
|
13
12
|
import type { SnapshotResponse } from "./types";
|
|
14
|
-
import { snapshotResponseSchema } from "./wire-schemas";
|
|
15
13
|
|
|
16
14
|
const MAGIC = new Uint8Array([0x4f, 0x4d, 0x50, 0x53]); // "OMPS"
|
|
17
15
|
const VERSION = 1;
|
|
@@ -40,6 +38,25 @@ export interface WriteAuthBrokerSnapshotCacheOptions {
|
|
|
40
38
|
snapshot: SnapshotResponse;
|
|
41
39
|
}
|
|
42
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Cheap structural guard for a decrypted cache payload. The bytes are already
|
|
43
|
+
* AES-256-GCM authenticated, so this only rejects shape/version drift (a cache
|
|
44
|
+
* written by a different omp build, or a buggy write) — not tampering. A
|
|
45
|
+
* mismatch returns null so the caller refetches a fresh snapshot.
|
|
46
|
+
*/
|
|
47
|
+
function isSnapshotResponseShape(v: unknown): v is SnapshotResponse {
|
|
48
|
+
if (typeof v !== "object" || v === null) return false;
|
|
49
|
+
const o = v as Record<string, unknown>;
|
|
50
|
+
return (
|
|
51
|
+
typeof o.generation === "number" &&
|
|
52
|
+
typeof o.generatedAt === "number" &&
|
|
53
|
+
typeof o.serverNowMs === "number" &&
|
|
54
|
+
typeof o.refresher === "object" &&
|
|
55
|
+
o.refresher !== null &&
|
|
56
|
+
Array.isArray(o.credentials)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
export async function readAuthBrokerSnapshotCache(
|
|
44
61
|
opts: ReadAuthBrokerSnapshotCacheOptions,
|
|
45
62
|
): Promise<SnapshotResponse | null> {
|
|
@@ -56,12 +73,11 @@ export async function readAuthBrokerSnapshotCache(
|
|
|
56
73
|
const plaintext = await decryptCachePayload(data, opts.token, opts.url);
|
|
57
74
|
if (!plaintext) return null;
|
|
58
75
|
const parsed: unknown = JSON.parse(TEXT_DECODER.decode(plaintext));
|
|
59
|
-
|
|
60
|
-
if (result instanceof type.errors) {
|
|
76
|
+
if (!isSnapshotResponseShape(parsed)) {
|
|
61
77
|
logger.debug("auth-broker snapshot cache schema invalid", { path: opts.path });
|
|
62
78
|
return null;
|
|
63
79
|
}
|
|
64
|
-
const snapshot =
|
|
80
|
+
const snapshot = parsed;
|
|
65
81
|
const now = opts.now?.() ?? Date.now();
|
|
66
82
|
if (now - snapshot.generatedAt > opts.ttlMs) return null;
|
|
67
83
|
return snapshot;
|
package/src/auth-storage.ts
CHANGED
|
@@ -4953,21 +4953,30 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
|
|
|
4953
4953
|
}
|
|
4954
4954
|
|
|
4955
4955
|
/**
|
|
4956
|
-
* Hard-deletes disabled rows for a provider when an active
|
|
4957
|
-
*
|
|
4958
|
-
*
|
|
4956
|
+
* Hard-deletes disabled rows for a provider when an active replacement exists.
|
|
4957
|
+
* OAuth credentials match by identity key; API keys match by provider and type.
|
|
4958
|
+
* Disabled rows without an active same-type replacement remain recoverable.
|
|
4959
4959
|
*/
|
|
4960
4960
|
#purgeSupersededDisabledRows(provider: string, activeRows: StoredAuthCredential[]): void {
|
|
4961
4961
|
try {
|
|
4962
|
+
let hasActiveApiKey = false;
|
|
4962
4963
|
const activeIdentityKeys = new Set<string>();
|
|
4963
4964
|
for (const row of activeRows) {
|
|
4965
|
+
if (row.credential.type === "api_key") {
|
|
4966
|
+
hasActiveApiKey = true;
|
|
4967
|
+
continue;
|
|
4968
|
+
}
|
|
4964
4969
|
const identityKey = resolveCredentialIdentityKey(provider, row.credential);
|
|
4965
4970
|
if (identityKey) activeIdentityKeys.add(identityKey);
|
|
4966
4971
|
}
|
|
4967
|
-
if (activeIdentityKeys.size === 0) return;
|
|
4972
|
+
if (!hasActiveApiKey && activeIdentityKeys.size === 0) return;
|
|
4968
4973
|
|
|
4969
4974
|
const disabledRows = this.#listDisabledByProviderStmt.all(provider) as AuthRow[];
|
|
4970
4975
|
for (const row of disabledRows) {
|
|
4976
|
+
if (hasActiveApiKey && row.credential_type === "api_key") {
|
|
4977
|
+
this.#hardDeleteStmt.run(row.id);
|
|
4978
|
+
continue;
|
|
4979
|
+
}
|
|
4971
4980
|
const identityKey = resolveRowCredentialIdentityKey(provider, row);
|
|
4972
4981
|
if (identityKey && activeIdentityKeys.has(identityKey)) {
|
|
4973
4982
|
this.#hardDeleteStmt.run(row.id);
|
package/src/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { type Type, type } from "arktype";
|
|
2
2
|
export { type ZodType, z } from "zod/v4";
|
|
3
3
|
export * from "./api-registry";
|
|
4
|
-
export * from "./auth-broker";
|
|
5
|
-
export {
|
|
4
|
+
export type * from "./auth-broker";
|
|
5
|
+
export type { AuthGatewayBootOptions, ModelResolver } from "./auth-gateway/server";
|
|
6
6
|
export * from "./auth-gateway/types";
|
|
7
7
|
export * from "./auth-retry";
|
|
8
8
|
export * from "./auth-storage";
|
package/src/providers/cursor.ts
CHANGED
|
@@ -548,24 +548,8 @@ export const streamCursor: StreamFunction<"cursor-agent"> = (
|
|
|
548
548
|
}
|
|
549
549
|
});
|
|
550
550
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
stream.push({
|
|
554
|
-
type: "text_end",
|
|
555
|
-
contentIndex: idx,
|
|
556
|
-
content: state.currentTextBlock.text,
|
|
557
|
-
partial: output,
|
|
558
|
-
});
|
|
559
|
-
}
|
|
560
|
-
if (state.currentThinkingBlock) {
|
|
561
|
-
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
562
|
-
stream.push({
|
|
563
|
-
type: "thinking_end",
|
|
564
|
-
contentIndex: idx,
|
|
565
|
-
content: state.currentThinkingBlock.thinking,
|
|
566
|
-
partial: output,
|
|
567
|
-
});
|
|
568
|
-
}
|
|
551
|
+
endCurrentTextBlock(output, stream, state);
|
|
552
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
569
553
|
if (state.currentToolCall) {
|
|
570
554
|
const idx = output.content.indexOf(state.currentToolCall);
|
|
571
555
|
state.currentToolCall.arguments = parseStreamingJson(state.currentToolCall.partialJson);
|
|
@@ -1972,6 +1956,38 @@ export function mergeCursorMcpToolCallArgs(
|
|
|
1972
1956
|
return merged;
|
|
1973
1957
|
}
|
|
1974
1958
|
|
|
1959
|
+
function endCurrentTextBlock(output: AssistantMessage, stream: AssistantMessageEventStream, state: BlockState): void {
|
|
1960
|
+
const block = state.currentTextBlock;
|
|
1961
|
+
if (!block) return;
|
|
1962
|
+
const idx = output.content.indexOf(block);
|
|
1963
|
+
delete (block as { index?: number }).index;
|
|
1964
|
+
stream.push({
|
|
1965
|
+
type: "text_end",
|
|
1966
|
+
contentIndex: idx,
|
|
1967
|
+
content: block.text,
|
|
1968
|
+
partial: output,
|
|
1969
|
+
});
|
|
1970
|
+
state.setTextBlock(null);
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function endCurrentThinkingBlock(
|
|
1974
|
+
output: AssistantMessage,
|
|
1975
|
+
stream: AssistantMessageEventStream,
|
|
1976
|
+
state: BlockState,
|
|
1977
|
+
): void {
|
|
1978
|
+
const block = state.currentThinkingBlock;
|
|
1979
|
+
if (!block) return;
|
|
1980
|
+
const idx = output.content.indexOf(block);
|
|
1981
|
+
delete (block as { index?: number }).index;
|
|
1982
|
+
stream.push({
|
|
1983
|
+
type: "thinking_end",
|
|
1984
|
+
contentIndex: idx,
|
|
1985
|
+
content: block.thinking,
|
|
1986
|
+
partial: output,
|
|
1987
|
+
});
|
|
1988
|
+
state.setThinkingBlock(null);
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1975
1991
|
/** Exported for tests: drives one Cursor interaction update through the streaming state machine. */
|
|
1976
1992
|
export function processInteractionUpdate(
|
|
1977
1993
|
update: any,
|
|
@@ -2017,18 +2033,10 @@ export function processInteractionUpdate(
|
|
|
2017
2033
|
const idx = output.content.indexOf(state.currentThinkingBlock!);
|
|
2018
2034
|
stream.push({ type: "thinking_delta", contentIndex: idx, delta, partial: output });
|
|
2019
2035
|
} else if (updateCase === "thinkingCompleted") {
|
|
2020
|
-
|
|
2021
|
-
const idx = output.content.indexOf(state.currentThinkingBlock);
|
|
2022
|
-
delete (state.currentThinkingBlock as any).index;
|
|
2023
|
-
stream.push({
|
|
2024
|
-
type: "thinking_end",
|
|
2025
|
-
contentIndex: idx,
|
|
2026
|
-
content: state.currentThinkingBlock.thinking,
|
|
2027
|
-
partial: output,
|
|
2028
|
-
});
|
|
2029
|
-
state.setThinkingBlock(null);
|
|
2030
|
-
}
|
|
2036
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
2031
2037
|
} else if (updateCase === "toolCallStarted") {
|
|
2038
|
+
endCurrentTextBlock(output, stream, state);
|
|
2039
|
+
endCurrentThinkingBlock(output, stream, state);
|
|
2032
2040
|
const toolCall = update.message.value.toolCall;
|
|
2033
2041
|
if (toolCall) {
|
|
2034
2042
|
const mcpCall = toolCall.mcpToolCall;
|
package/src/utils/validation.ts
CHANGED
|
@@ -764,10 +764,13 @@ function normalizeOptionalNullsForSchema(
|
|
|
764
764
|
if (!(key in nextValue)) continue;
|
|
765
765
|
const currentValue = nextValue[key];
|
|
766
766
|
const isNullish = currentValue === null || currentValue === "null";
|
|
767
|
+
const isInvalidEmptyString =
|
|
768
|
+
currentValue === "" && !required.has(key) && !branchMatchesSchema(propertySchema, currentValue);
|
|
767
769
|
|
|
768
|
-
// Strip null
|
|
769
|
-
//
|
|
770
|
-
|
|
770
|
+
// Strip null/string "null" from optional fields, and strip empty
|
|
771
|
+
// strings only when the property schema would reject the explicit value.
|
|
772
|
+
// LLMs sometimes output these placeholders to mean "no value".
|
|
773
|
+
if ((isNullish || isInvalidEmptyString) && !required.has(key)) {
|
|
771
774
|
if (!changed) {
|
|
772
775
|
nextValue = { ...nextValue };
|
|
773
776
|
changed = true;
|
|
@@ -1281,7 +1284,8 @@ function truncateArgsForError(value: unknown): unknown {
|
|
|
1281
1284
|
/**
|
|
1282
1285
|
* Validates tool call arguments against the tool's schema (Zod or plain JSON
|
|
1283
1286
|
* Schema). Applies LLM-quirk coercions (numeric strings, JSON-string
|
|
1284
|
-
* containers, null-for-optional, null-for-default) before
|
|
1287
|
+
* containers, null/invalid-empty-string-for-optional, null-for-default) before
|
|
1288
|
+
* declaring failure.
|
|
1285
1289
|
*
|
|
1286
1290
|
* @throws Error with a formatted message when validation cannot be reconciled.
|
|
1287
1291
|
*/
|
|
@@ -1290,9 +1294,10 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1290
1294
|
const ctx = getValidationContext(tool);
|
|
1291
1295
|
const { json } = ctx;
|
|
1292
1296
|
|
|
1293
|
-
// Always normalize first — strip null
|
|
1294
|
-
//
|
|
1295
|
-
//
|
|
1297
|
+
// Always normalize first — strip null/string "null" from optional fields,
|
|
1298
|
+
// strip optional empty strings only when their property schema rejects the
|
|
1299
|
+
// explicit value, and substitute defaults. Handles LLM outputting
|
|
1300
|
+
// placeholders for "no value" even when validation would otherwise pass.
|
|
1296
1301
|
let normalizedArgs: unknown = originalArgs;
|
|
1297
1302
|
let changed = false;
|
|
1298
1303
|
const initialNormalization = normalizeOptionalNullsForSchema(json, normalizedArgs);
|