@oh-my-pi/pi-ai 17.0.3 → 17.0.4

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 CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.0.4] - 2026-07-18
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Kimi Code usage reports dropping the 5h window reset time (`omp usage` showed no "resets in …" for the 5h limit): the API returns `resetTime` on the limit `detail`, not on `window`, so the parsed row-level reset is now carried onto the window when the window itself has none.
10
+ - Made Kimi device-id persistence best-effort: a missing or unwritable `~/.omp/agent` directory no longer throws during Kimi header construction, which silently nulled every `kimi-code` usage probe on fresh installs.
11
+ - Coerced boolean tool-schema subschemas to MFJS object forms for native Moonshot/Kimi endpoints, preventing the task tool's `outputSchema` field from causing HTTP 400 responses ([#5952](https://github.com/can1357/oh-my-pi/issues/5952)).
12
+
5
13
  ## [17.0.3] - 2026-07-17
6
14
 
7
15
  ### Fixed
@@ -2,8 +2,11 @@ import { type DescriptionSpillFormat } from "./spill.js";
2
2
  import { type JsonObject } from "./types.js";
3
3
  export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners" | "not";
4
4
  export interface NormalizeSchemaOptions {
5
- /** Coerce JSON Schema boolean subschemas for providers whose wire cannot encode them. */
6
- coerceBooleanSubschemas?: boolean;
5
+ /**
6
+ * Coerce boolean subschemas to object forms. `standard` preserves `false`
7
+ * with `not`; `permissive` uses `{}` when the provider cannot express it.
8
+ */
9
+ coerceBooleanSubschemas?: "standard" | "permissive";
7
10
  unsupportedFields: (key: string) => boolean;
8
11
  normalizeFieldNames: boolean;
9
12
  collapseNullFields: boolean;
@@ -62,6 +65,9 @@ export declare function normalizeSchemaForMCP(value: unknown): unknown;
62
65
  * `default` and `description` are MFJS Meta Data fields and are preserved.
63
66
  * - `additionalProperties` (boolean or schema) and `type: "null"` (incl.
64
67
  * inside `anyOf`) are kept.
68
+ * - Boolean subschemas are object-coerced; MFJS has no exact `false` schema,
69
+ * so both values become the permissive empty schema while local tool
70
+ * validation remains authoritative.
65
71
  *
66
72
  * Out of scope (absent from the built-in tool surface, spec-ambiguous to
67
73
  * rewrite blindly): `allOf` intersection merging, external/recursive `$ref`,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.0.3",
4
+ "version": "17.0.4",
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,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "17.0.3",
42
- "@oh-my-pi/pi-utils": "17.0.3",
43
- "@oh-my-pi/pi-wire": "17.0.3",
41
+ "@oh-my-pi/pi-catalog": "17.0.4",
42
+ "@oh-my-pi/pi-utils": "17.0.4",
43
+ "@oh-my-pi/pi-wire": "17.0.4",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -38,6 +38,7 @@ import {
38
38
  adaptSchemaForStrict,
39
39
  findStrictToolSchemaViolation,
40
40
  NO_STRICT,
41
+ normalizeSchemaForMoonshot,
41
42
  sanitizeSchemaForOpenAIResponses,
42
43
  toolWireSchema,
43
44
  } from "../utils/schema";
@@ -999,7 +1000,15 @@ export function convertTools(
999
1000
  }
1000
1001
  const strict = !NO_STRICT && strictMode && tool.strict !== false;
1001
1002
  const baseParameters = toolWireSchema(tool);
1002
- const responseParameters = sanitizeSchemaForOpenAIResponses(baseParameters);
1003
+ // MFJS must run AFTER the Responses sanitizer: the sanitizer normalizes
1004
+ // `{}` → `true` (issue #1179), and Moonshot's validator rejects boolean
1005
+ // subschemas ("property schema … must be an object"), so the Moonshot
1006
+ // pass re-coerces them last.
1007
+ const sanitized = sanitizeSchemaForOpenAIResponses(baseParameters);
1008
+ const responseParameters =
1009
+ model.compat.toolSchemaFlavor === "moonshot-mfjs"
1010
+ ? (normalizeSchemaForMoonshot(sanitized) as Record<string, unknown>)
1011
+ : sanitized;
1003
1012
  const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
1004
1013
  // Quarantine a tool whose emitted schema carries a provider-rejecting
1005
1014
  // enum/const-vs-type contradiction: dropping just that tool keeps the rest
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { scheduler } from "node:timers/promises";
10
- import { $env, getAgentDir, isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { $env, getAgentDir } from "@oh-my-pi/pi-utils";
11
11
  import packageJson from "../../../package.json" with { type: "json" };
12
12
  import * as AIError from "../../error";
13
13
  import type { OAuthController, OAuthCredentials } from "./types";
@@ -57,21 +57,29 @@ function getDeviceModel(): string {
57
57
  return formatDeviceModel(label, release, arch);
58
58
  }
59
59
 
60
+ // Device id identifies this install to Kimi. Persistence is best-effort: a
61
+ // missing/unwritable agent dir must never break header construction (and with
62
+ // it every usage probe / request that spreads getKimiCommonHeaders()) — fall
63
+ // back to a per-process ephemeral id instead.
60
64
  let getDeviceId = (): string => {
61
65
  const deviceIdPath = path.join(getAgentDir(), DEVICE_ID_FILENAME);
62
66
  try {
63
- const existing = fs.readFileSync(deviceIdPath, "utf-8");
64
- const trimmed = existing.trim();
65
- if (trimmed) {
66
- getDeviceId = () => trimmed;
67
- return trimmed;
67
+ const existing = fs.readFileSync(deviceIdPath, "utf-8").trim();
68
+ if (existing) {
69
+ getDeviceId = () => existing;
70
+ return existing;
68
71
  }
69
- } catch (error) {
70
- if (!isEnoent(error)) throw error;
72
+ } catch {
73
+ // Unreadable device-id file: regenerate below.
71
74
  }
72
75
 
73
76
  const deviceId = crypto.randomUUID().replace(/-/g, "");
74
- fs.writeFileSync(deviceIdPath, `${deviceId}\n`, { mode: 0o600 });
77
+ try {
78
+ fs.mkdirSync(path.dirname(deviceIdPath), { recursive: true });
79
+ fs.writeFileSync(deviceIdPath, `${deviceId}\n`, { mode: 0o600 });
80
+ } catch {
81
+ // Persist failure → ephemeral id for this process.
82
+ }
75
83
  getDeviceId = () => deviceId;
76
84
  return deviceId;
77
85
  };
package/src/usage/kimi.ts CHANGED
@@ -144,15 +144,21 @@ function buildUsageStatus(amount: UsageAmount): UsageStatus {
144
144
  }
145
145
 
146
146
  function toUsageLimit(row: KimiUsageRow, provider: string, index: number, accountId?: string): UsageLimit {
147
- const window: UsageWindow | undefined =
148
- row.window ??
149
- (row.resetsAt
147
+ // Kimi puts `resetTime` on the limit `detail`, not on `window`, so a
148
+ // window built from `duration`/`timeUnit` alone carries no resetsAt.
149
+ // Fall back to the row-level reset so `omp usage` can render
150
+ // "resets in …" for the 5h window too.
151
+ const window: UsageWindow | undefined = row.window
152
+ ? row.window.resetsAt !== undefined || row.resetsAt === undefined
153
+ ? row.window
154
+ : { ...row.window, resetsAt: row.resetsAt }
155
+ : row.resetsAt
150
156
  ? {
151
157
  id: "default",
152
158
  label: "Usage window",
153
159
  resetsAt: row.resetsAt,
154
160
  }
155
- : undefined);
161
+ : undefined;
156
162
 
157
163
  const amount = buildUsageAmount(row);
158
164
  return {
@@ -29,8 +29,11 @@ import { decontaminateZodInstance } from "./zod-decontaminate";
29
29
  export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners" | "not";
30
30
 
31
31
  export interface NormalizeSchemaOptions {
32
- /** Coerce JSON Schema boolean subschemas for providers whose wire cannot encode them. */
33
- coerceBooleanSubschemas?: boolean;
32
+ /**
33
+ * Coerce boolean subschemas to object forms. `standard` preserves `false`
34
+ * with `not`; `permissive` uses `{}` when the provider cannot express it.
35
+ */
36
+ coerceBooleanSubschemas?: "standard" | "permissive";
34
37
  unsupportedFields: (key: string) => boolean;
35
38
  normalizeFieldNames: boolean;
36
39
  collapseNullFields: boolean;
@@ -284,12 +287,12 @@ function normalizeSchemaNode(value: unknown, options: NormalizeSchemaWalkOptions
284
287
  }
285
288
  if (typeof value === "boolean") {
286
289
  // A bare boolean is a JSON Schema subschema only in a subschema slot.
287
- // The Google/CCA protobuf Schema wire has no representation for it
288
- // (issue #5604): `true` accepts anything -> `{}`, `false` accepts nothing
289
- // -> `{ not: {} }`. In a keyword slot (`nullable`, `enum` entry, …) a
290
- // boolean is a plain value and is left untouched.
291
- if (!options.coerceBooleanSubschemas || !options.booleanIsSubschema) return value;
292
- return value ? {} : { not: {} };
290
+ // Some provider wires have no boolean-schema representation: `true`
291
+ // becomes `{}`; `false` uses `not` when supported, or the permissive
292
+ // `{}` fallback when the provider cannot express an impossible schema.
293
+ const mode = options.coerceBooleanSubschemas;
294
+ if (!mode || !options.booleanIsSubschema) return value;
295
+ return value || mode === "permissive" ? {} : { not: {} };
293
296
  }
294
297
  if (!isJsonObject(value)) {
295
298
  return value;
@@ -993,7 +996,7 @@ export function normalizeSchema(value: unknown, options: NormalizeSchemaOptions)
993
996
 
994
997
  export function normalizeSchemaForGoogle(value: unknown): unknown {
995
998
  return normalizeSchema(value, {
996
- coerceBooleanSubschemas: true,
999
+ coerceBooleanSubschemas: "standard",
997
1000
  unsupportedFields: isGoogleUnsupportedSchemaField,
998
1001
  normalizeFieldNames: true,
999
1002
  collapseNullFields: true,
@@ -1015,7 +1018,7 @@ export function normalizeSchemaForGoogle(value: unknown): unknown {
1015
1018
 
1016
1019
  export function normalizeSchemaForCCA(value: unknown): unknown {
1017
1020
  return normalizeSchema(value, {
1018
- coerceBooleanSubschemas: true,
1021
+ coerceBooleanSubschemas: "standard",
1019
1022
  unsupportedFields: isGoogleUnsupportedSchemaField,
1020
1023
  normalizeFieldNames: true,
1021
1024
  collapseNullFields: false,
@@ -1080,6 +1083,9 @@ export function normalizeSchemaForMCP(value: unknown): unknown {
1080
1083
  * `default` and `description` are MFJS Meta Data fields and are preserved.
1081
1084
  * - `additionalProperties` (boolean or schema) and `type: "null"` (incl.
1082
1085
  * inside `anyOf`) are kept.
1086
+ * - Boolean subschemas are object-coerced; MFJS has no exact `false` schema,
1087
+ * so both values become the permissive empty schema while local tool
1088
+ * validation remains authoritative.
1083
1089
  *
1084
1090
  * Out of scope (absent from the built-in tool surface, spec-ambiguous to
1085
1091
  * rewrite blindly): `allOf` intersection merging, external/recursive `$ref`,
@@ -1087,6 +1093,7 @@ export function normalizeSchemaForMCP(value: unknown): unknown {
1087
1093
  */
1088
1094
  export function normalizeSchemaForMoonshot(value: unknown): unknown {
1089
1095
  return normalizeSchema(value, {
1096
+ coerceBooleanSubschemas: "permissive",
1090
1097
  unsupportedFields: isMoonshotUnsupportedSchemaField,
1091
1098
  normalizeFieldNames: false,
1092
1099
  collapseNullFields: false,