@oh-my-pi/pi-ai 16.3.4 → 16.3.6
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 +22 -0
- package/dist/types/auth-broker/client.d.ts +3 -1
- package/dist/types/auth-broker/remote-store.d.ts +6 -1
- package/dist/types/auth-broker/types.d.ts +13 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +46 -0
- package/dist/types/auth-storage.d.ts +40 -5
- package/dist/types/registry/oauth/types.d.ts +14 -0
- package/dist/types/types.d.ts +16 -0
- package/dist/types/utils/schema/normalize.d.ts +5 -0
- package/package.json +4 -4
- package/src/auth-broker/client.ts +41 -12
- package/src/auth-broker/remote-store.ts +189 -9
- package/src/auth-broker/server.ts +109 -7
- package/src/auth-broker/types.ts +22 -1
- package/src/auth-broker/wire-schemas.ts +22 -0
- package/src/auth-storage.ts +300 -24
- package/src/providers/ollama.ts +2 -2
- package/src/providers/openai-codex-responses.ts +6 -1
- package/src/providers/openai-completions.ts +12 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/registry/oauth/callback-server.ts +97 -9
- package/src/registry/oauth/types.ts +14 -0
- package/src/types.ts +18 -0
- package/src/usage/claude.ts +28 -8
- package/src/utils/schema/CONSTRAINTS.md +1 -0
- package/src/utils/schema/normalize.ts +131 -0
- package/src/utils/validation.ts +295 -0
package/src/utils/validation.ts
CHANGED
|
@@ -835,6 +835,263 @@ function normalizeOptionalNullsForSchema(
|
|
|
835
835
|
return { value: changed ? nextValue : value, changed };
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
+
function decodeJsonPointerToken(token: string): string {
|
|
839
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function resolveLocalJsonSchemaRef(root: unknown, ref: string): unknown | undefined {
|
|
843
|
+
if (ref === "#") return root;
|
|
844
|
+
if (!ref.startsWith("#/")) return undefined;
|
|
845
|
+
let current: unknown = root;
|
|
846
|
+
for (const rawToken of ref.slice(2).split("/")) {
|
|
847
|
+
const token = decodeJsonPointerToken(rawToken);
|
|
848
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
849
|
+
current = (current as Record<string, unknown>)[token];
|
|
850
|
+
}
|
|
851
|
+
return current;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function normalizeEnumStringWhitespace(
|
|
855
|
+
schema: unknown,
|
|
856
|
+
value: unknown,
|
|
857
|
+
root: unknown = schema,
|
|
858
|
+
refs: ReadonlySet<string> = new Set(),
|
|
859
|
+
): { value: unknown; changed: boolean } {
|
|
860
|
+
if (value === null || value === undefined) return { value, changed: false };
|
|
861
|
+
if (schema === null || typeof schema !== "object") return { value, changed: false };
|
|
862
|
+
|
|
863
|
+
const schemaObject = schema as Record<string, unknown>;
|
|
864
|
+
const ref = schemaObject.$ref;
|
|
865
|
+
if (typeof ref === "string") {
|
|
866
|
+
if (refs.has(ref)) return { value, changed: false };
|
|
867
|
+
const resolved = resolveLocalJsonSchemaRef(root, ref);
|
|
868
|
+
if (resolved === undefined) return { value, changed: false };
|
|
869
|
+
return normalizeEnumStringWhitespace(resolved, value, root, new Set([...refs, ref]));
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const branchMatches = (branch: unknown, candidate: unknown): boolean => {
|
|
873
|
+
if (branch !== null && typeof branch === "object") {
|
|
874
|
+
const branchRef = (branch as Record<string, unknown>).$ref;
|
|
875
|
+
if (typeof branchRef === "string" && !refs.has(branchRef)) {
|
|
876
|
+
const resolved = resolveLocalJsonSchemaRef(root, branchRef);
|
|
877
|
+
if (resolved !== undefined) return branchMatchesSchema(resolved, candidate);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return branchMatchesSchema(branch, candidate);
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const normalizeAnyOfLike = (keyword: "anyOf" | "oneOf"): { value: unknown; changed: boolean } => {
|
|
884
|
+
const branches = schemaObject[keyword];
|
|
885
|
+
if (!Array.isArray(branches)) return { value, changed: false };
|
|
886
|
+
if (branches.some(branch => branchMatches(branch, value))) return { value, changed: false };
|
|
887
|
+
|
|
888
|
+
for (const branch of branches) {
|
|
889
|
+
const normalized = normalizeEnumStringWhitespace(branch, value, root, refs);
|
|
890
|
+
if (!normalized.changed) continue;
|
|
891
|
+
if (branchMatches(branch, normalized.value)) return normalized;
|
|
892
|
+
}
|
|
893
|
+
return { value, changed: false };
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
const anyOfNormalization = normalizeAnyOfLike("anyOf");
|
|
897
|
+
if (anyOfNormalization.changed) return anyOfNormalization;
|
|
898
|
+
|
|
899
|
+
const oneOfNormalization = normalizeAnyOfLike("oneOf");
|
|
900
|
+
if (oneOfNormalization.changed) return oneOfNormalization;
|
|
901
|
+
|
|
902
|
+
if (Array.isArray(schemaObject.allOf)) {
|
|
903
|
+
let changed = false;
|
|
904
|
+
let nextValue: unknown = value;
|
|
905
|
+
for (const branch of schemaObject.allOf) {
|
|
906
|
+
const normalized = normalizeEnumStringWhitespace(branch, nextValue, root, refs);
|
|
907
|
+
if (!normalized.changed) continue;
|
|
908
|
+
nextValue = normalized.value;
|
|
909
|
+
changed = true;
|
|
910
|
+
}
|
|
911
|
+
if (changed) return { value: nextValue, changed: true };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
if (typeof value === "string") {
|
|
915
|
+
const trimmed = value.trim();
|
|
916
|
+
if (trimmed !== value) {
|
|
917
|
+
const enumValues = schemaObject.enum;
|
|
918
|
+
if (Array.isArray(enumValues) && !enumValues.includes(value) && enumValues.includes(trimmed)) {
|
|
919
|
+
return { value: trimmed, changed: true };
|
|
920
|
+
}
|
|
921
|
+
const constValue = schemaObject.const;
|
|
922
|
+
if (typeof constValue === "string" && trimmed === constValue) {
|
|
923
|
+
return { value: trimmed, changed: true };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return { value, changed: false };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (Array.isArray(value)) {
|
|
930
|
+
let changed = false;
|
|
931
|
+
let nextValue = value;
|
|
932
|
+
const prefixItems = schemaObject.prefixItems;
|
|
933
|
+
if (Array.isArray(prefixItems)) {
|
|
934
|
+
for (let i = 0; i < value.length && i < prefixItems.length; i += 1) {
|
|
935
|
+
const itemSchema = prefixItems[i];
|
|
936
|
+
const normalized = normalizeEnumStringWhitespace(itemSchema, value[i], root, refs);
|
|
937
|
+
if (!normalized.changed) continue;
|
|
938
|
+
if (!changed) {
|
|
939
|
+
nextValue = [...value];
|
|
940
|
+
changed = true;
|
|
941
|
+
}
|
|
942
|
+
nextValue[i] = normalized.value;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const itemSchema = schemaObject.items;
|
|
947
|
+
if (itemSchema !== null && typeof itemSchema === "object" && !Array.isArray(itemSchema)) {
|
|
948
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
949
|
+
if (Array.isArray(prefixItems) && i < prefixItems.length) continue;
|
|
950
|
+
const normalized = normalizeEnumStringWhitespace(itemSchema, nextValue[i], root, refs);
|
|
951
|
+
if (!normalized.changed) continue;
|
|
952
|
+
if (!changed) {
|
|
953
|
+
nextValue = [...value];
|
|
954
|
+
changed = true;
|
|
955
|
+
}
|
|
956
|
+
nextValue[i] = normalized.value;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
return { value: changed ? nextValue : value, changed };
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
if (typeof value !== "object") return { value, changed: false };
|
|
963
|
+
const properties = schemaObject.properties;
|
|
964
|
+
if (!properties || typeof properties !== "object") return { value, changed: false };
|
|
965
|
+
|
|
966
|
+
const propsObject = properties as Record<string, unknown>;
|
|
967
|
+
const valueObject = value as Record<string, unknown>;
|
|
968
|
+
let changed = false;
|
|
969
|
+
let nextValue = valueObject;
|
|
970
|
+
for (const [key, propertySchema] of Object.entries(propsObject)) {
|
|
971
|
+
if (!(key in nextValue)) continue;
|
|
972
|
+
const normalized = normalizeEnumStringWhitespace(propertySchema, nextValue[key], root, refs);
|
|
973
|
+
if (!normalized.changed) continue;
|
|
974
|
+
if (!changed) {
|
|
975
|
+
nextValue = { ...nextValue };
|
|
976
|
+
changed = true;
|
|
977
|
+
}
|
|
978
|
+
nextValue[key] = normalized.value;
|
|
979
|
+
}
|
|
980
|
+
return { value: changed ? nextValue : valueObject, changed };
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// ============================================================================
|
|
984
|
+
// Identifier-string trailing-whitespace normalization (LLM quirk).
|
|
985
|
+
// ============================================================================
|
|
986
|
+
//
|
|
987
|
+
// LLMs sometimes emit tool arguments with a trailing newline dangling off a
|
|
988
|
+
// short identifier — a path, URL, or a display label like `title`. These
|
|
989
|
+
// values are never legitimately terminated by line breaks, so we strip trailing
|
|
990
|
+
// line terminators from string values on the well-known keys below before the
|
|
991
|
+
// tool ever sees them. Content-carrying properties (`content`, `input`, `body`,
|
|
992
|
+
// `text`, `command`, `code`) are intentionally not traversed or trimmed so
|
|
993
|
+
// genuine trailing whitespace survives on writes, patches, shell commands, and
|
|
994
|
+
// eval snippets.
|
|
995
|
+
// ============================================================================
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Property names whose values are treated as short identifiers — filesystem
|
|
999
|
+
* paths, URLs, URIs, or display labels. The trim only fires on strings sitting
|
|
1000
|
+
* under one of these keys, so `path: "docs/report "` still targets the file
|
|
1001
|
+
* whose name ends in a space.
|
|
1002
|
+
*/
|
|
1003
|
+
const IDENTIFIER_STRING_KEYS: ReadonlySet<string> = new Set([
|
|
1004
|
+
"path",
|
|
1005
|
+
"paths",
|
|
1006
|
+
"file",
|
|
1007
|
+
"file_path",
|
|
1008
|
+
"filePath",
|
|
1009
|
+
"filepath",
|
|
1010
|
+
"url",
|
|
1011
|
+
"uri",
|
|
1012
|
+
"title",
|
|
1013
|
+
"label",
|
|
1014
|
+
]);
|
|
1015
|
+
|
|
1016
|
+
const CONTENT_CARRYING_KEYS: ReadonlySet<string> = new Set(["content", "input", "body", "text", "command", "code"]);
|
|
1017
|
+
|
|
1018
|
+
const TRAILING_LINE_TERMINATOR_RE = /[\r\n]+$/;
|
|
1019
|
+
|
|
1020
|
+
function trimTrailingLineTerminators(input: string): string {
|
|
1021
|
+
if (!TRAILING_LINE_TERMINATOR_RE.test(input)) return input;
|
|
1022
|
+
return input.replace(TRAILING_LINE_TERMINATOR_RE, "");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function trimIdentifierStringLeaf(input: unknown): unknown {
|
|
1026
|
+
if (typeof input === "string") {
|
|
1027
|
+
const trimmed = trimTrailingLineTerminators(input);
|
|
1028
|
+
return trimmed === input ? input : trimmed;
|
|
1029
|
+
}
|
|
1030
|
+
if (Array.isArray(input)) {
|
|
1031
|
+
let changed = false;
|
|
1032
|
+
let next = input;
|
|
1033
|
+
for (let i = 0; i < input.length; i += 1) {
|
|
1034
|
+
const item = input[i];
|
|
1035
|
+
if (typeof item !== "string") continue;
|
|
1036
|
+
const trimmed = trimTrailingLineTerminators(item);
|
|
1037
|
+
if (trimmed === item) continue;
|
|
1038
|
+
if (!changed) {
|
|
1039
|
+
next = input.slice();
|
|
1040
|
+
changed = true;
|
|
1041
|
+
}
|
|
1042
|
+
next[i] = trimmed;
|
|
1043
|
+
}
|
|
1044
|
+
return changed ? next : input;
|
|
1045
|
+
}
|
|
1046
|
+
return input;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Recursively strip trailing line terminators from string values whose property
|
|
1051
|
+
* key matches {@link IDENTIFIER_STRING_KEYS}. Runs by property name only
|
|
1052
|
+
* (schema-agnostic) so it fires uniformly across Zod, ArkType, and plain JSON
|
|
1053
|
+
* Schema tools while preserving nested payloads under content-carrying keys.
|
|
1054
|
+
*/
|
|
1055
|
+
function normalizeIdentifierStringWhitespace(value: unknown): { value: unknown; changed: boolean } {
|
|
1056
|
+
if (Array.isArray(value)) {
|
|
1057
|
+
let changed = false;
|
|
1058
|
+
let next = value;
|
|
1059
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
1060
|
+
const normalized = normalizeIdentifierStringWhitespace(value[i]);
|
|
1061
|
+
if (!normalized.changed) continue;
|
|
1062
|
+
if (!changed) {
|
|
1063
|
+
next = [...value];
|
|
1064
|
+
changed = true;
|
|
1065
|
+
}
|
|
1066
|
+
next[i] = normalized.value;
|
|
1067
|
+
}
|
|
1068
|
+
return { value: changed ? next : value, changed };
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (value === null || typeof value !== "object") return { value, changed: false };
|
|
1072
|
+
|
|
1073
|
+
const source = value as Record<string, unknown>;
|
|
1074
|
+
let changed = false;
|
|
1075
|
+
let out: Record<string, unknown> = source;
|
|
1076
|
+
for (const [key, entry] of Object.entries(source)) {
|
|
1077
|
+
let nextEntry = entry;
|
|
1078
|
+
if (CONTENT_CARRYING_KEYS.has(key)) continue;
|
|
1079
|
+
if (IDENTIFIER_STRING_KEYS.has(key)) {
|
|
1080
|
+
const trimmed = trimIdentifierStringLeaf(entry);
|
|
1081
|
+
if (trimmed !== entry) nextEntry = trimmed;
|
|
1082
|
+
}
|
|
1083
|
+
const nested = normalizeIdentifierStringWhitespace(nextEntry);
|
|
1084
|
+
if (nested.changed) nextEntry = nested.value;
|
|
1085
|
+
if (nextEntry === entry) continue;
|
|
1086
|
+
if (!changed) {
|
|
1087
|
+
out = { ...source };
|
|
1088
|
+
changed = true;
|
|
1089
|
+
}
|
|
1090
|
+
out[key] = nextEntry;
|
|
1091
|
+
}
|
|
1092
|
+
return { value: changed ? out : value, changed };
|
|
1093
|
+
}
|
|
1094
|
+
|
|
838
1095
|
// ============================================================================
|
|
839
1096
|
// Double-encoded object-key normalization (LLM quirk).
|
|
840
1097
|
// ============================================================================
|
|
@@ -1485,6 +1742,23 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1485
1742
|
changed = true;
|
|
1486
1743
|
}
|
|
1487
1744
|
|
|
1745
|
+
const enumStringNormalization = normalizeEnumStringWhitespace(json, normalizedArgs);
|
|
1746
|
+
if (enumStringNormalization.changed) {
|
|
1747
|
+
normalizedArgs = enumStringNormalization.value;
|
|
1748
|
+
changed = true;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
// Strip trailing whitespace from string values on well-known
|
|
1752
|
+
// identifier-like property names (paths, URLs, titles). Some models tack
|
|
1753
|
+
// a newline onto a short-identifier arg from stream artifacts; downstream
|
|
1754
|
+
// tools then either fail to stat the target or annotate a "corrected
|
|
1755
|
+
// from" hint the model misreads as tool corruption.
|
|
1756
|
+
const identifierStringNormalization = normalizeIdentifierStringWhitespace(normalizedArgs);
|
|
1757
|
+
if (identifierStringNormalization.changed) {
|
|
1758
|
+
normalizedArgs = identifierStringNormalization.value;
|
|
1759
|
+
changed = true;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1488
1762
|
// Then re-shape JSON-stringified arrays whose schema accepts both string
|
|
1489
1763
|
// and array (e.g. `paths: string | string[]`). Without this, zod accepts
|
|
1490
1764
|
// the literal `'["a","b"]'` as a string and downstream tools treat it as
|
|
@@ -1495,6 +1769,12 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1495
1769
|
changed = true;
|
|
1496
1770
|
}
|
|
1497
1771
|
|
|
1772
|
+
const identifierStringNormalizationAfterArray = normalizeIdentifierStringWhitespace(normalizedArgs);
|
|
1773
|
+
if (identifierStringNormalizationAfterArray.changed) {
|
|
1774
|
+
normalizedArgs = identifierStringNormalizationAfterArray.value;
|
|
1775
|
+
changed = true;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1498
1778
|
// Single-argument tools (e.g. `edit`): if the model put the lone required
|
|
1499
1779
|
// string under a different key, adopt the first string field as that key.
|
|
1500
1780
|
const singleStringNorm = normalizeSingleStringField(json, normalizedArgs);
|
|
@@ -1527,6 +1807,16 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1527
1807
|
normalizedArgs = nullNormalization.value;
|
|
1528
1808
|
}
|
|
1529
1809
|
|
|
1810
|
+
const enumStringNormalizationPass = normalizeEnumStringWhitespace(json, normalizedArgs);
|
|
1811
|
+
if (enumStringNormalizationPass.changed) {
|
|
1812
|
+
normalizedArgs = enumStringNormalizationPass.value;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
const identifierStringNormalizationPass = normalizeIdentifierStringWhitespace(normalizedArgs);
|
|
1816
|
+
if (identifierStringNormalizationPass.changed) {
|
|
1817
|
+
normalizedArgs = identifierStringNormalizationPass.value;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1530
1820
|
// Re-run the union-string coercion because `coerceArgsFromIssues` may
|
|
1531
1821
|
// have just unwrapped a JSON-stringified object at the root or inside a
|
|
1532
1822
|
// nested field — exposing `string | string[]` descendants the initial
|
|
@@ -1536,6 +1826,11 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
|
|
|
1536
1826
|
normalizedArgs = stringEncodedArrayNormPass.value;
|
|
1537
1827
|
}
|
|
1538
1828
|
|
|
1829
|
+
const identifierStringNormalizationAfterArrayPass = normalizeIdentifierStringWhitespace(normalizedArgs);
|
|
1830
|
+
if (identifierStringNormalizationAfterArrayPass.changed) {
|
|
1831
|
+
normalizedArgs = identifierStringNormalizationAfterArrayPass.value;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1539
1834
|
// Re-run single-string remap: `coerceArgsFromIssues` may have just
|
|
1540
1835
|
// unwrapped a JSON-stringified root object, exposing a mislabelled lone
|
|
1541
1836
|
// string field the initial pre-pass could not see.
|