@oh-my-pi/pi-ai 12.0.0 → 12.1.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "12.0.0",
3
+ "version": "12.1.1",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -63,7 +63,7 @@
63
63
  "@connectrpc/connect-node": "^2.1.1",
64
64
  "@google/genai": "^1.39.0",
65
65
  "@mistralai/mistralai": "^1.13.0",
66
- "@oh-my-pi/pi-utils": "12.0.0",
66
+ "@oh-my-pi/pi-utils": "12.1.1",
67
67
  "@sinclair/typebox": "^0.34.48",
68
68
  "@smithy/node-http-handler": "^4.4.9",
69
69
  "ajv": "^8.17.1",
@@ -82,17 +82,22 @@
82
82
  "unified",
83
83
  "api"
84
84
  ],
85
- "author": "Mario Zechner",
85
+ "author": "Can Bölük",
86
+ "contributors": ["Mario Zechner"],
86
87
  "license": "MIT",
87
88
  "repository": {
88
89
  "type": "git",
89
90
  "url": "git+https://github.com/can1357/oh-my-pi.git",
90
91
  "directory": "packages/ai"
91
92
  },
92
- "engines": {
93
- "bun": ">=1.3.7"
94
- },
95
- "devDependencies": {
96
- "@types/bun": "^1.3.9"
97
- }
93
+ "homepage": "https://github.com/can1357/oh-my-pi",
94
+ "bugs": {
95
+ "url": "https://github.com/can1357/oh-my-pi/issues"
96
+ },
97
+ "engines": {
98
+ "bun": ">=1.3.7"
99
+ },
100
+ "devDependencies": {
101
+ "@types/bun": "^1.3.9"
102
+ }
98
103
  }
@@ -460,12 +460,17 @@ function buildParams(
460
460
  }
461
461
 
462
462
  if (model.reasoning) {
463
+ // Always request encrypted reasoning content so reasoning items can be
464
+ // replayed in multi-turn conversations when store is false (items aren't
465
+ // persisted server-side, so we must include the full content).
466
+ // See: https://github.com/can1357/oh-my-pi/issues/41
467
+ params.include = ["reasoning.encrypted_content"];
468
+
463
469
  if (options?.reasoningEffort || options?.reasoningSummary) {
464
470
  params.reasoning = {
465
471
  effort: options?.reasoningEffort || "medium",
466
472
  summary: options?.reasoningSummary || "auto",
467
473
  };
468
- params.include = ["reasoning.encrypted_content"];
469
474
  } else {
470
475
  if (model.name.toLowerCase().startsWith("gpt-5")) {
471
476
  // Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7
@@ -2,6 +2,8 @@
2
2
  * Shared utilities for Google Generative AI and Google Cloud Code Assist providers.
3
3
  */
4
4
  import { type Content, FinishReason, FunctionCallingConfigMode, type Part } from "@google/genai";
5
+ import type { AnySchema } from "ajv";
6
+ import Ajv2020 from "ajv/dist/2020.js";
5
7
  import type { Context, ImageContent, Model, StopReason, TextContent, Tool } from "../types";
6
8
  import { sanitizeSurrogates } from "../utils/sanitize-unicode";
7
9
  import { transformMessages } from "./transform-messages";
@@ -257,7 +259,6 @@ const UNSUPPORTED_SCHEMA_FIELDS = new Set([
257
259
  "$defs",
258
260
  "$dynamicRef",
259
261
  "$dynamicAnchor",
260
- "format",
261
262
  "examples",
262
263
  "prefixItems",
263
264
  "unevaluatedProperties",
@@ -276,65 +277,164 @@ const UNSUPPORTED_SCHEMA_FIELDS = new Set([
276
277
  "format",
277
278
  ]);
278
279
 
279
- function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknown {
280
- if (Array.isArray(value)) {
281
- return value.map(entry => sanitizeSchemaImpl(entry, isInsideProperties));
280
+ interface SanitizeSchemaOptions {
281
+ insideProperties: boolean;
282
+ normalizeTypeArrayToNullable: boolean;
283
+ stripNullableKeyword: boolean;
284
+ }
285
+
286
+ type JsonObject = Record<string, unknown>;
287
+
288
+ function isJsonObject(value: unknown): value is JsonObject {
289
+ return !!value && typeof value === "object" && !Array.isArray(value);
290
+ }
291
+
292
+ function areJsonValuesEqual(left: unknown, right: unknown): boolean {
293
+ if (Object.is(left, right)) {
294
+ return true;
295
+ }
296
+ if (Array.isArray(left) || Array.isArray(right)) {
297
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) {
298
+ return false;
299
+ }
300
+ for (let i = 0; i < left.length; i += 1) {
301
+ if (!areJsonValuesEqual(left[i], right[i])) {
302
+ return false;
303
+ }
304
+ }
305
+ return true;
306
+ }
307
+ if (!isJsonObject(left) || !isJsonObject(right)) {
308
+ return false;
309
+ }
310
+ const leftKeys = Object.keys(left);
311
+ const rightKeys = Object.keys(right);
312
+ if (leftKeys.length !== rightKeys.length) {
313
+ return false;
314
+ }
315
+ for (const key of leftKeys) {
316
+ if (!(key in right) || !areJsonValuesEqual(left[key], right[key])) {
317
+ return false;
318
+ }
319
+ }
320
+ return true;
321
+ }
322
+
323
+ function mergeCompatibleEnumSchemas(existing: unknown, incoming: unknown): JsonObject | null {
324
+ if (!isJsonObject(existing) || !isJsonObject(incoming)) {
325
+ return null;
326
+ }
327
+ const existingEnum = Array.isArray(existing.enum) ? existing.enum : null;
328
+ const incomingEnum = Array.isArray(incoming.enum) ? incoming.enum : null;
329
+ if (!existingEnum || !incomingEnum) {
330
+ return null;
331
+ }
332
+ if (!areJsonValuesEqual(existing.type, incoming.type)) {
333
+ return null;
334
+ }
335
+ const existingKeys = Object.keys(existing).filter(key => key !== "enum");
336
+ const incomingKeys = Object.keys(incoming).filter(key => key !== "enum");
337
+ if (existingKeys.length !== incomingKeys.length) {
338
+ return null;
339
+ }
340
+ for (const key of existingKeys) {
341
+ if (!(key in incoming) || !areJsonValuesEqual(existing[key], incoming[key])) {
342
+ return null;
343
+ }
344
+ }
345
+
346
+ const mergedEnum = [...existingEnum];
347
+ for (const enumValue of incomingEnum) {
348
+ if (!mergedEnum.some(existingValue => Object.is(existingValue, enumValue))) {
349
+ mergedEnum.push(enumValue);
350
+ }
351
+ }
352
+ return {
353
+ ...existing,
354
+ enum: mergedEnum,
355
+ };
356
+ }
357
+
358
+ function getAnyOfVariants(schema: unknown): unknown[] {
359
+ if (isJsonObject(schema) && Array.isArray(schema.anyOf)) {
360
+ return schema.anyOf;
361
+ }
362
+ return [schema];
363
+ }
364
+
365
+ function mergePropertySchemas(existing: unknown, incoming: unknown): unknown {
366
+ if (areJsonValuesEqual(existing, incoming)) {
367
+ return existing;
368
+ }
369
+ const mergedEnumSchema = mergeCompatibleEnumSchemas(existing, incoming);
370
+ if (mergedEnumSchema !== null) {
371
+ return mergedEnumSchema;
372
+ }
373
+
374
+ const mergedAnyOf = [...getAnyOfVariants(existing)];
375
+ for (const variant of getAnyOfVariants(incoming)) {
376
+ if (!mergedAnyOf.some(existingVariant => areJsonValuesEqual(existingVariant, variant))) {
377
+ mergedAnyOf.push(variant);
378
+ }
282
379
  }
380
+ return mergedAnyOf.length === 1 ? mergedAnyOf[0] : { anyOf: mergedAnyOf };
381
+ }
283
382
 
383
+ function sanitizeSchemaImpl(value: unknown, options: SanitizeSchemaOptions): unknown {
384
+ if (Array.isArray(value)) {
385
+ return value.map(entry => sanitizeSchemaImpl(entry, options));
386
+ }
284
387
  if (!value || typeof value !== "object") {
285
388
  return value;
286
389
  }
287
-
288
390
  const obj = value as Record<string, unknown>;
289
391
  const result: Record<string, unknown> = {};
290
-
291
- // Collapse anyOf/oneOf of const values into enum
292
392
  for (const combiner of ["anyOf", "oneOf"] as const) {
293
393
  if (Array.isArray(obj[combiner])) {
294
394
  const variants = obj[combiner] as Record<string, unknown>[];
295
-
296
- // Check if ALL variants have a const field
297
395
  const allHaveConst = variants.every(v => v && typeof v === "object" && "const" in v);
298
-
299
396
  if (allHaveConst && variants.length > 0) {
300
- // Extract all const values into enum
301
397
  result.enum = variants.map(v => v.const);
302
-
303
- // Inherit type from first variant if present
304
398
  const firstType = variants[0]?.type;
305
399
  if (firstType) {
306
400
  result.type = firstType;
307
401
  }
308
-
309
402
  // Copy description and other top-level fields (not the combiner)
310
403
  for (const [key, entry] of Object.entries(obj)) {
311
404
  if (key !== combiner && !(key in result)) {
312
- result[key] = sanitizeSchemaImpl(entry, false);
405
+ result[key] = sanitizeSchemaImpl(entry, {
406
+ insideProperties: false,
407
+ normalizeTypeArrayToNullable: options.normalizeTypeArrayToNullable,
408
+ stripNullableKeyword: options.stripNullableKeyword,
409
+ });
313
410
  }
314
411
  }
315
412
  return result;
316
413
  }
317
414
  }
318
415
  }
319
-
320
416
  // Regular field processing
321
417
  let constValue: unknown;
322
418
  for (const [key, entry] of Object.entries(obj)) {
323
419
  // Only strip unsupported schema keywords when NOT inside "properties" object
324
420
  // Inside "properties", keys are property names (e.g., "pattern") not schema keywords
325
- if (!isInsideProperties && UNSUPPORTED_SCHEMA_FIELDS.has(key)) continue;
421
+ if (!options.insideProperties && UNSUPPORTED_SCHEMA_FIELDS.has(key)) continue;
422
+ if (options.stripNullableKeyword && key === "nullable") continue;
326
423
  if (key === "const") {
327
424
  constValue = entry;
328
425
  continue;
329
426
  }
330
427
  if (key === "additionalProperties" && entry === false) continue;
331
428
  // When key is "properties", child keys are property names, not schema keywords
332
- result[key] = sanitizeSchemaImpl(entry, key === "properties");
429
+ result[key] = sanitizeSchemaImpl(entry, {
430
+ insideProperties: key === "properties",
431
+ normalizeTypeArrayToNullable: options.normalizeTypeArrayToNullable,
432
+ stripNullableKeyword: options.stripNullableKeyword,
433
+ });
333
434
  }
334
-
335
435
  // Normalize array-valued "type" (e.g. ["string", "null"]) to a single type + nullable.
336
436
  // Google's Schema proto expects type to be a single enum string, not an array.
337
- if (Array.isArray(result.type)) {
437
+ if (options.normalizeTypeArrayToNullable && Array.isArray(result.type)) {
338
438
  const types = result.type as string[];
339
439
  const nonNull = types.filter(t => t !== "null");
340
440
  if (types.includes("null")) {
@@ -342,7 +442,6 @@ function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknow
342
442
  }
343
443
  result.type = nonNull[0] ?? types[0];
344
444
  }
345
-
346
445
  if (constValue !== undefined) {
347
446
  // Convert const to enum, merging with existing enum if present
348
447
  const existingEnum = Array.isArray(result.enum) ? result.enum : [];
@@ -364,9 +463,135 @@ function sanitizeSchemaImpl(value: unknown, isInsideProperties: boolean): unknow
364
463
 
365
464
  return result;
366
465
  }
367
-
368
466
  export function sanitizeSchemaForGoogle(value: unknown): unknown {
369
- return sanitizeSchemaImpl(value, false);
467
+ return sanitizeSchemaImpl(value, {
468
+ insideProperties: false,
469
+ normalizeTypeArrayToNullable: true,
470
+ stripNullableKeyword: false,
471
+ });
472
+ }
473
+
474
+ export function sanitizeSchemaForCloudCodeAssistClaude(value: unknown): unknown {
475
+ return sanitizeSchemaImpl(value, {
476
+ insideProperties: false,
477
+ normalizeTypeArrayToNullable: false,
478
+ stripNullableKeyword: true,
479
+ });
480
+ }
481
+
482
+ /**
483
+ * Claude via Cloud Code Assist (`parameters` path) can reject schemas that keep
484
+ * object variant combiners, so flatten object-only unions into one object shape.
485
+ */
486
+ function mergeObjectCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
487
+ const variantsRaw = schema[combiner];
488
+ if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) {
489
+ return schema;
490
+ }
491
+
492
+ const variants: JsonObject[] = [];
493
+ for (const entry of variantsRaw) {
494
+ if (!isJsonObject(entry)) {
495
+ return schema;
496
+ }
497
+ const variantType = entry.type;
498
+ if (variantType !== undefined && variantType !== "object") {
499
+ return schema;
500
+ }
501
+ if (entry.properties !== undefined && !isJsonObject(entry.properties)) {
502
+ return schema;
503
+ }
504
+ variants.push(entry);
505
+ }
506
+
507
+ const mergedProperties: JsonObject = {};
508
+ const ownProperties = isJsonObject(schema.properties) ? schema.properties : {};
509
+ for (const [name, propertySchema] of Object.entries(ownProperties)) {
510
+ mergedProperties[name] = propertySchema;
511
+ }
512
+
513
+ for (const variant of variants) {
514
+ const properties = isJsonObject(variant.properties) ? variant.properties : {};
515
+ for (const [name, propertySchema] of Object.entries(properties)) {
516
+ const existingSchema = mergedProperties[name];
517
+ mergedProperties[name] =
518
+ existingSchema === undefined ? propertySchema : mergePropertySchemas(existingSchema, propertySchema);
519
+ }
520
+ }
521
+
522
+ const nextSchema: JsonObject = {};
523
+ for (const [key, entry] of Object.entries(schema)) {
524
+ if (key === combiner) continue;
525
+ nextSchema[key] = entry;
526
+ }
527
+
528
+ nextSchema.type = "object";
529
+ nextSchema.properties = mergedProperties;
530
+ return nextSchema;
531
+ }
532
+
533
+ function normalizeSchemaForCloudCodeAssistClaude(value: unknown): unknown {
534
+ if (Array.isArray(value)) {
535
+ return value.map(entry => normalizeSchemaForCloudCodeAssistClaude(entry));
536
+ }
537
+ if (!isJsonObject(value)) {
538
+ return value;
539
+ }
540
+
541
+ const normalized: JsonObject = {};
542
+ for (const [key, entry] of Object.entries(value)) {
543
+ normalized[key] = normalizeSchemaForCloudCodeAssistClaude(entry);
544
+ }
545
+
546
+ const mergedAnyOf = mergeObjectCombinerVariants(normalized, "anyOf");
547
+ return mergeObjectCombinerVariants(mergedAnyOf, "oneOf");
548
+ }
549
+
550
+ let cloudCodeAssistSchemaValidator: Ajv2020 | null = null;
551
+ function getCloudCodeAssistSchemaValidator(): Ajv2020 {
552
+ if (cloudCodeAssistSchemaValidator) {
553
+ return cloudCodeAssistSchemaValidator;
554
+ }
555
+
556
+ cloudCodeAssistSchemaValidator = new Ajv2020({
557
+ allErrors: true,
558
+ strict: false,
559
+ validateSchema: true,
560
+ });
561
+ return cloudCodeAssistSchemaValidator;
562
+ }
563
+
564
+ /**
565
+ * Keep validation synchronous in this request path.
566
+ */
567
+ function isValidCloudCodeAssistClaudeSchema(schema: unknown): boolean {
568
+ try {
569
+ const result = getCloudCodeAssistSchemaValidator().validateSchema(schema as AnySchema);
570
+ return typeof result === "boolean" ? result : false;
571
+ } catch {
572
+ return false;
573
+ }
574
+ }
575
+
576
+ const CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA = {
577
+ type: "object",
578
+ properties: {},
579
+ } as const;
580
+
581
+ /**
582
+ * Prepare schema for Claude on Cloud Code Assist:
583
+ * sanitize -> normalize union objects -> validate -> fallback.
584
+ *
585
+ * Fallback is per-tool and fail-open to avoid rejecting the entire request when
586
+ * one tool schema is invalid.
587
+ */
588
+ export function prepareSchemaForCloudCodeAssistClaude(value: unknown): unknown {
589
+ const sanitized = sanitizeSchemaForCloudCodeAssistClaude(value);
590
+ const normalized = normalizeSchemaForCloudCodeAssistClaude(sanitized);
591
+ if (isValidCloudCodeAssistClaudeSchema(normalized)) {
592
+ return normalized;
593
+ }
594
+ return CLOUD_CODE_ASSIST_CLAUDE_FALLBACK_SCHEMA;
370
595
  }
371
596
 
372
597
  /**
@@ -384,8 +609,10 @@ export function convertTools(
384
609
  ): { functionDeclarations: Record<string, unknown>[] }[] | undefined {
385
610
  if (tools.length === 0) return undefined;
386
611
 
387
- // Claude models on Cloud Code Assist need the legacy `parameters` field;
388
- // the API translates it into Anthropic's `input_schema`.
612
+ /**
613
+ * Claude models on Cloud Code Assist need the legacy `parameters` field;
614
+ * the API translates it into Anthropic's `input_schema`.
615
+ */
389
616
  const useParameters = model.id.startsWith("claude-");
390
617
 
391
618
  return [
@@ -394,7 +621,7 @@ export function convertTools(
394
621
  name: tool.name,
395
622
  description: tool.description,
396
623
  ...(useParameters
397
- ? { parameters: sanitizeSchemaForGoogle(tool.parameters) }
624
+ ? { parameters: prepareSchemaForCloudCodeAssistClaude(tool.parameters) }
398
625
  : { parametersJsonSchema: tool.parameters }),
399
626
  })),
400
627
  },
@@ -454,12 +454,17 @@ function buildParams(model: Model<"openai-responses">, context: Context, options
454
454
  }
455
455
 
456
456
  if (model.reasoning) {
457
+ // Always request encrypted reasoning content so reasoning items can be
458
+ // replayed in multi-turn conversations when store is false (items aren't
459
+ // persisted server-side, so we must include the full content).
460
+ // See: https://github.com/can1357/oh-my-pi/issues/41
461
+ params.include = ["reasoning.encrypted_content"];
462
+
457
463
  if (options?.reasoningEffort || options?.reasoningSummary) {
458
464
  params.reasoning = {
459
465
  effort: options?.reasoningEffort || "medium",
460
466
  summary: options?.reasoningSummary || "auto",
461
467
  };
462
- params.include = ["reasoning.encrypted_content"];
463
468
  } else {
464
469
  if (model.name.startsWith("gpt-5")) {
465
470
  // Jesus Christ, see https://community.openai.com/t/need-reasoning-false-option-for-gpt-5/1351588/7
package/src/storage.ts CHANGED
@@ -5,9 +5,8 @@
5
5
 
6
6
  import { Database, type Statement } from "bun:sqlite";
7
7
  import * as fs from "node:fs/promises";
8
- import * as os from "node:os";
9
8
  import * as path from "node:path";
10
- import { $env } from "@oh-my-pi/pi-utils";
9
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
11
10
  import type { OAuthCredentials } from "./utils/oauth/types";
12
11
 
13
12
  type AuthCredential = { type: "api_key"; key: string } | ({ type: "oauth" } & OAuthCredentials);
@@ -21,14 +20,6 @@ type AuthRow = {
21
20
  updated_at: number;
22
21
  };
23
22
 
24
- /**
25
- * Get the agent config directory (e.g., ~/.omp/agent/)
26
- */
27
- function getAgentDir(): string {
28
- const configDir = $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent");
29
- return configDir;
30
- }
31
-
32
23
  /**
33
24
  * Get path to agent.db
34
25
  */
@@ -7,7 +7,7 @@ import type { OAuthController, OAuthCredentials } from "./types";
7
7
 
8
8
  const decode = (s: string) => atob(s);
9
9
  const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
10
- const AUTHORIZE_URL = "https://platform.claude.com/oauth/authorize";
10
+ const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
11
11
  const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
12
12
  const CALLBACK_PORT = 54545;
13
13
  const CALLBACK_PATH = "/callback";
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { $env, abortableSleep, isEnoent } from "@oh-my-pi/pi-utils";
10
+ import { getAgentDir } from "@oh-my-pi/pi-utils/dirs";
10
11
  import packageJson from "../../../package.json" with { type: "json" };
11
12
  import type { OAuthController, OAuthCredentials } from "./types";
12
13
 
@@ -36,11 +37,6 @@ interface TokenResponse {
36
37
  interval?: number;
37
38
  }
38
39
 
39
- function getAgentDir(): string {
40
- const configDir = $env.PI_CODING_AGENT_DIR || path.join(os.homedir(), ".omp", "agent");
41
- return configDir;
42
- }
43
-
44
40
  function resolveOAuthHost(): string {
45
41
  return $env.KIMI_CODE_OAUTH_HOST || $env.KIMI_OAUTH_HOST || DEFAULT_OAUTH_HOST;
46
42
  }