@oh-my-pi/pi-ai 12.1.0 → 12.2.0

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.1.0",
3
+ "version": "12.2.0",
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.1.0",
66
+ "@oh-my-pi/pi-utils": "12.2.0",
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
  }
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type { Static, TSchema } from "@sinclair/typebox";
2
2
  export { Type } from "@sinclair/typebox";
3
3
  export * from "./models";
4
+ export * from "./provider-details";
4
5
  export * from "./providers/anthropic";
5
6
  export * from "./providers/azure-openai-responses";
6
7
  export * from "./providers/cursor";
package/src/models.json CHANGED
@@ -12965,6 +12965,7 @@
12965
12965
  "provider": "openai-codex",
12966
12966
  "baseUrl": "https://chatgpt.com/backend-api",
12967
12967
  "reasoning": true,
12968
+ "preferWebsockets": true,
12968
12969
  "input": [
12969
12970
  "text"
12970
12971
  ],
@@ -0,0 +1,81 @@
1
+ import { getOpenAICodexTransportDetails, type OpenAICodexTransportDetails } from "./providers/openai-codex-responses";
2
+ import type { Api, Model, Provider, ProviderSessionState } from "./types";
3
+
4
+ export interface ProviderDetailField {
5
+ label: string;
6
+ value: string;
7
+ }
8
+
9
+ export interface ProviderDetails {
10
+ provider: Provider;
11
+ api: Api;
12
+ fields: ProviderDetailField[];
13
+ }
14
+
15
+ export interface ProviderDetailsContext {
16
+ model: Model<Api>;
17
+ sessionId?: string;
18
+ authMode?: string;
19
+ preferWebsockets?: boolean;
20
+ providerSessionState?: Map<string, ProviderSessionState>;
21
+ }
22
+
23
+ export function getProviderDetails(context: ProviderDetailsContext): ProviderDetails {
24
+ const endpoint = formatEndpoint(context.model.baseUrl);
25
+ const fields: ProviderDetailField[] = [
26
+ { label: "Model", value: context.model.id },
27
+ { label: "API", value: context.model.api },
28
+ { label: "Auth", value: context.authMode ?? "auto" },
29
+ { label: "Endpoint", value: endpoint },
30
+ ];
31
+
32
+ if (context.model.api === "openai-codex-responses") {
33
+ const codexDetails = getOpenAICodexTransportDetails(context.model as Model<"openai-codex-responses">, {
34
+ sessionId: context.sessionId,
35
+ baseUrl: context.model.baseUrl,
36
+ preferWebsockets: context.preferWebsockets,
37
+ providerSessionState: context.providerSessionState,
38
+ });
39
+ fields.push({ label: "Transport", value: formatCodexTransport(codexDetails) });
40
+ fields.push({ label: "WebSocket", value: formatCodexWebSocket(codexDetails) });
41
+ fields.push({ label: "Reuse", value: formatCodexReuse(codexDetails, context.sessionId) });
42
+ }
43
+
44
+ return {
45
+ provider: context.model.provider,
46
+ api: context.model.api,
47
+ fields,
48
+ };
49
+ }
50
+
51
+ function formatEndpoint(baseUrl: string): string {
52
+ try {
53
+ const parsed = new URL(baseUrl);
54
+ const path = parsed.pathname.replace(/\/$/, "");
55
+ return `${parsed.origin}${path || "/"}`;
56
+ } catch {
57
+ return baseUrl;
58
+ }
59
+ }
60
+
61
+ function formatCodexTransport(details: OpenAICodexTransportDetails): string {
62
+ if (details.lastTransport === "websocket") return "websocket";
63
+ if (details.lastTransport === "sse" && (details.websocketDisabled || details.fallbackCount > 0)) {
64
+ return "sse (fallback)";
65
+ }
66
+ if (details.lastTransport === "sse") return "sse";
67
+ return details.websocketPreferred ? "websocket preferred" : "sse";
68
+ }
69
+
70
+ function formatCodexWebSocket(details: OpenAICodexTransportDetails): string {
71
+ if (!details.websocketPreferred) return "off";
72
+ if (details.websocketDisabled) return "disabled after fallback";
73
+ if (details.websocketConnected) return "connected";
74
+ if (details.prewarmed) return "prewarmed";
75
+ return details.hasSessionState ? "enabled" : "waiting for first request";
76
+ }
77
+
78
+ function formatCodexReuse(details: OpenAICodexTransportDetails, sessionId: string | undefined): string {
79
+ if (!sessionId) return "no session key";
80
+ return details.canAppend ? "append enabled" : "full request";
81
+ }
@@ -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",
@@ -282,6 +283,103 @@ interface SanitizeSchemaOptions {
282
283
  stripNullableKeyword: boolean;
283
284
  }
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
+ }
379
+ }
380
+ return mergedAnyOf.length === 1 ? mergedAnyOf[0] : { anyOf: mergedAnyOf };
381
+ }
382
+
285
383
  function sanitizeSchemaImpl(value: unknown, options: SanitizeSchemaOptions): unknown {
286
384
  if (Array.isArray(value)) {
287
385
  return value.map(entry => sanitizeSchemaImpl(entry, options));
@@ -381,6 +479,121 @@ export function sanitizeSchemaForCloudCodeAssistClaude(value: unknown): unknown
381
479
  });
382
480
  }
383
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;
595
+ }
596
+
384
597
  /**
385
598
  * Convert tools to Gemini function declarations format.
386
599
  *
@@ -396,8 +609,10 @@ export function convertTools(
396
609
  ): { functionDeclarations: Record<string, unknown>[] }[] | undefined {
397
610
  if (tools.length === 0) return undefined;
398
611
 
399
- // Claude models on Cloud Code Assist need the legacy `parameters` field;
400
- // 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
+ */
401
616
  const useParameters = model.id.startsWith("claude-");
402
617
 
403
618
  return [
@@ -406,7 +621,7 @@ export function convertTools(
406
621
  name: tool.name,
407
622
  description: tool.description,
408
623
  ...(useParameters
409
- ? { parameters: sanitizeSchemaForCloudCodeAssistClaude(tool.parameters) }
624
+ ? { parameters: prepareSchemaForCloudCodeAssistClaude(tool.parameters) }
410
625
  : { parametersJsonSchema: tool.parameters }),
411
626
  })),
412
627
  },
@@ -14,6 +14,8 @@ export const OPENAI_HEADERS = {
14
14
 
15
15
  export const OPENAI_HEADER_VALUES = {
16
16
  BETA_RESPONSES: "responses=experimental",
17
+ BETA_RESPONSES_WEBSOCKETS: "responses_websockets=2026-02-04",
18
+ BETA_RESPONSES_WEBSOCKETS_V2: "responses_websockets=2026-02-06",
17
19
  ORIGINATOR_CODEX: "pi",
18
20
  } as const;
19
21