@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 +14 -9
- package/src/index.ts +1 -0
- package/src/models.json +1 -0
- package/src/provider-details.ts +81 -0
- package/src/providers/google-shared.ts +219 -4
- package/src/providers/openai-codex/constants.ts +2 -0
- package/src/providers/openai-codex-responses.ts +1083 -237
- package/src/stream.ts +2 -0
- package/src/types.ts +13 -0
- package/src/utils/oauth/index.ts +32 -4
- package/src/utils/oauth/perplexity.ts +0 -217
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-ai",
|
|
3
|
-
"version": "12.
|
|
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.
|
|
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": "
|
|
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
|
-
"
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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:
|
|
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
|
|