@victor-software-house/pi-openai-proxy 4.7.1 → 4.9.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.
Files changed (2) hide show
  1. package/dist/index.mjs +215 -13
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1247,28 +1247,36 @@ const REASONING_EFFORT_MAP = {
1247
1247
  xhigh: "xhigh"
1248
1248
  };
1249
1249
  /**
1250
- * APIs that use the OpenAI chat completions wire format and accept standard
1251
- * passthrough fields (stop, seed, top_p, tool_choice, etc.) in the payload.
1252
- *
1253
- * Only these APIs receive injected fields via onPayload. All other APIs
1254
- * (anthropic-messages, google-*, bedrock-*, openai-codex-responses) use
1255
- * different payload schemas that reject unknown fields.
1250
+ * APIs that accept the full set of OpenAI passthrough fields (stop, seed, top_p,
1251
+ * tool_choice, frequency_penalty, etc.) as top-level payload properties.
1256
1252
  */
1257
- const OPENAI_COMPLETIONS_COMPATIBLE_APIS = new Set([
1253
+ const OPENAI_FULL_PASSTHROUGH_APIS = new Set([
1258
1254
  "openai-completions",
1259
1255
  "openai-responses",
1260
1256
  "azure-openai-responses",
1261
1257
  "mistral-conversations"
1262
1258
  ]);
1263
1259
  /**
1264
- * Collect fields that need to be injected via onPayload.
1265
- * Only injects for APIs that use the OpenAI chat completions wire format.
1266
- * Non-compatible APIs (Anthropic, Google, Bedrock, Codex) reject unknown fields.
1260
+ * The Codex Responses API accepts only tool_choice and parallel_tool_calls.
1261
+ * Other standard fields (top_p, seed, stop, user, frequency_penalty, etc.)
1262
+ * are rejected with "Unsupported parameter". The SDK hardcodes
1263
+ * tool_choice: "auto" and parallel_tool_calls: true; onPayload overrides
1264
+ * those when the client sends explicit values.
1265
+ */
1266
+ const CODEX_APIS = new Set(["openai-codex-responses"]);
1267
+ const ANTHROPIC_APIS = new Set(["anthropic-messages"]);
1268
+ const GOOGLE_APIS = new Set([
1269
+ "google-generative-ai",
1270
+ "google-gemini-cli",
1271
+ "google-vertex"
1272
+ ]);
1273
+ /**
1274
+ * Collect OpenAI-format fields for OpenAI-compatible APIs.
1275
+ * Fields are injected as flat top-level properties on the payload.
1267
1276
  *
1268
1277
  * @internal Exported for unit testing only.
1269
1278
  */
1270
- function collectPayloadFields(request, api) {
1271
- if (!OPENAI_COMPLETIONS_COMPATIBLE_APIS.has(api)) return;
1279
+ function collectOpenAIPayloadFields(request) {
1272
1280
  const fields = {};
1273
1281
  let hasFields = false;
1274
1282
  if (request.stop !== void 0) {
@@ -1318,6 +1326,198 @@ function collectPayloadFields(request, api) {
1318
1326
  return hasFields ? fields : void 0;
1319
1327
  }
1320
1328
  /**
1329
+ * Translate OpenAI tool_choice to Anthropic tool_choice format.
1330
+ *
1331
+ * OpenAI "auto" -> { type: "auto" }
1332
+ * OpenAI "none" -> { type: "none" } (Anthropic skips tool calling)
1333
+ * OpenAI "required" -> { type: "any" } (force tool use)
1334
+ * OpenAI { type: "function", function: { name } } -> { type: "tool", name }
1335
+ *
1336
+ * @internal Exported for unit testing only.
1337
+ */
1338
+ function translateToolChoiceForAnthropic(toolChoice) {
1339
+ if (toolChoice === void 0) return;
1340
+ if (toolChoice === "auto") return { type: "auto" };
1341
+ if (toolChoice === "none") return { type: "none" };
1342
+ if (toolChoice === "required") return { type: "any" };
1343
+ return {
1344
+ type: "tool",
1345
+ name: toolChoice.function.name
1346
+ };
1347
+ }
1348
+ /**
1349
+ * Collect Anthropic-format fields translated from the OpenAI request.
1350
+ *
1351
+ * Supported translations:
1352
+ * - top_p -> top_p (same name, natively supported)
1353
+ * - stop -> stop_sequences (different field name)
1354
+ * - tool_choice -> Anthropic tool_choice format (object with type)
1355
+ * - parallel_tool_calls: false -> disable_parallel_tool_use on tool_choice
1356
+ * - user -> metadata.user_id
1357
+ *
1358
+ * Not supported (silently skipped — these concepts don't exist in Anthropic):
1359
+ * - seed, frequency_penalty, presence_penalty, response_format, prediction, metadata (arbitrary keys)
1360
+ *
1361
+ * @internal Exported for unit testing only.
1362
+ */
1363
+ function collectAnthropicPayloadFields(request) {
1364
+ const fields = {};
1365
+ let hasFields = false;
1366
+ if (request.top_p !== void 0) {
1367
+ fields["top_p"] = request.top_p;
1368
+ hasFields = true;
1369
+ }
1370
+ if (request.stop !== void 0) {
1371
+ fields["stop_sequences"] = Array.isArray(request.stop) ? request.stop : [request.stop];
1372
+ hasFields = true;
1373
+ }
1374
+ const toolChoice = translateToolChoiceForAnthropic(request.tool_choice);
1375
+ const disableParallel = request.parallel_tool_calls === false;
1376
+ if (toolChoice !== void 0) {
1377
+ if (disableParallel) toolChoice["disable_parallel_tool_use"] = true;
1378
+ fields["tool_choice"] = toolChoice;
1379
+ hasFields = true;
1380
+ } else if (disableParallel) {
1381
+ fields["tool_choice"] = {
1382
+ type: "auto",
1383
+ disable_parallel_tool_use: true
1384
+ };
1385
+ hasFields = true;
1386
+ }
1387
+ if (request.user !== void 0) {
1388
+ fields["metadata"] = { user_id: request.user };
1389
+ hasFields = true;
1390
+ }
1391
+ return hasFields ? fields : void 0;
1392
+ }
1393
+ /**
1394
+ * Translate OpenAI tool_choice to Google FunctionCallingConfigMode string.
1395
+ *
1396
+ * OpenAI "auto" -> "AUTO"
1397
+ * OpenAI "none" -> "NONE"
1398
+ * OpenAI "required" -> "ANY"
1399
+ * Named function choice -> "ANY" (Google doesn't support per-function forcing
1400
+ * in the same way, but ANY forces tool use)
1401
+ */
1402
+ function translateToolChoiceForGoogle(toolChoice) {
1403
+ if (toolChoice === void 0) return;
1404
+ if (toolChoice === "auto") return "AUTO";
1405
+ if (toolChoice === "none") return "NONE";
1406
+ return "ANY";
1407
+ }
1408
+ /**
1409
+ * Patch Google's nested payload structure with translated fields.
1410
+ *
1411
+ * Google's payload shape: { model, contents, config: { generationConfig, toolConfig, ... } }
1412
+ * Fields go into config.generationConfig (camelCase) or config.toolConfig.
1413
+ *
1414
+ * Supported translations:
1415
+ * - top_p -> config.generationConfig.topP (camelCase, nested)
1416
+ * - stop -> config.generationConfig.stopSequences (array, nested)
1417
+ * - seed -> config.generationConfig.seed (nested)
1418
+ * - frequency_penalty -> config.generationConfig.frequencyPenalty (nested)
1419
+ * - presence_penalty -> config.generationConfig.presencePenalty (nested)
1420
+ * - tool_choice -> config.toolConfig.functionCallingConfig.mode (nested)
1421
+ *
1422
+ * Not supported (silently skipped):
1423
+ * - response_format, metadata, prediction, parallel_tool_calls, user
1424
+ *
1425
+ * @internal Exported for unit testing only.
1426
+ */
1427
+ function patchGooglePayload(payload, request) {
1428
+ let patched = false;
1429
+ const config = isRecord(payload["config"]) ? payload["config"] : void 0;
1430
+ if (config === void 0) return false;
1431
+ let genConfig = isRecord(config["generationConfig"]) ? config["generationConfig"] : void 0;
1432
+ if (request.top_p !== void 0) {
1433
+ genConfig ??= {};
1434
+ genConfig["topP"] = request.top_p;
1435
+ patched = true;
1436
+ }
1437
+ if (request.stop !== void 0) {
1438
+ genConfig ??= {};
1439
+ const sequences = Array.isArray(request.stop) ? request.stop : [request.stop];
1440
+ genConfig["stopSequences"] = sequences;
1441
+ patched = true;
1442
+ }
1443
+ if (request.seed !== void 0) {
1444
+ genConfig ??= {};
1445
+ genConfig["seed"] = request.seed;
1446
+ patched = true;
1447
+ }
1448
+ if (request.frequency_penalty !== void 0) {
1449
+ genConfig ??= {};
1450
+ genConfig["frequencyPenalty"] = request.frequency_penalty;
1451
+ patched = true;
1452
+ }
1453
+ if (request.presence_penalty !== void 0) {
1454
+ genConfig ??= {};
1455
+ genConfig["presencePenalty"] = request.presence_penalty;
1456
+ patched = true;
1457
+ }
1458
+ if (genConfig !== void 0 && patched) config["generationConfig"] = genConfig;
1459
+ const mode = translateToolChoiceForGoogle(request.tool_choice);
1460
+ if (mode !== void 0) {
1461
+ let toolConfig = isRecord(config["toolConfig"]) ? config["toolConfig"] : void 0;
1462
+ toolConfig ??= {};
1463
+ let funcConfig = isRecord(toolConfig["functionCallingConfig"]) ? toolConfig["functionCallingConfig"] : void 0;
1464
+ funcConfig ??= {};
1465
+ funcConfig["mode"] = mode;
1466
+ toolConfig["functionCallingConfig"] = funcConfig;
1467
+ config["toolConfig"] = toolConfig;
1468
+ patched = true;
1469
+ }
1470
+ return patched;
1471
+ }
1472
+ /**
1473
+ * Collect the restricted set of fields the Codex Responses API accepts.
1474
+ *
1475
+ * Only tool_choice and parallel_tool_calls are supported. The SDK hardcodes
1476
+ * tool_choice: "auto" and parallel_tool_calls: true; these overrides let clients
1477
+ * control tool behavior explicitly.
1478
+ *
1479
+ * @internal Exported for unit testing only.
1480
+ */
1481
+ function collectCodexPayloadFields(request) {
1482
+ const fields = {};
1483
+ let hasFields = false;
1484
+ if (request.tool_choice !== void 0) {
1485
+ fields["tool_choice"] = request.tool_choice;
1486
+ hasFields = true;
1487
+ }
1488
+ if (request.parallel_tool_calls !== void 0) {
1489
+ fields["parallel_tool_calls"] = request.parallel_tool_calls;
1490
+ hasFields = true;
1491
+ }
1492
+ return hasFields ? fields : void 0;
1493
+ }
1494
+ /**
1495
+ * Collect API-specific payload fields from an OpenAI request.
1496
+ *
1497
+ * Dispatches to the appropriate translator based on the target API:
1498
+ * - OpenAI full passthrough: all standard fields (same names)
1499
+ * - Codex: restricted to tool_choice + parallel_tool_calls only
1500
+ * - Anthropic: translated field names and formats
1501
+ * - Google: nested generationConfig patching (handled separately in onPayload)
1502
+ * - Others (Bedrock): no passthrough
1503
+ *
1504
+ * For Google APIs, returns undefined (patching is done directly in onPayload
1505
+ * via patchGooglePayload because the payload structure is nested).
1506
+ *
1507
+ * @internal Exported for unit testing only.
1508
+ */
1509
+ function collectPayloadFields(request, api) {
1510
+ if (OPENAI_FULL_PASSTHROUGH_APIS.has(api)) return collectOpenAIPayloadFields(request);
1511
+ if (CODEX_APIS.has(api)) return collectCodexPayloadFields(request);
1512
+ if (ANTHROPIC_APIS.has(api)) return collectAnthropicPayloadFields(request);
1513
+ }
1514
+ /**
1515
+ * Whether the given API requires Google-style nested payload patching.
1516
+ */
1517
+ function isGoogleApi(api) {
1518
+ return GOOGLE_APIS.has(api);
1519
+ }
1520
+ /**
1321
1521
  * Collect tool strict flags from the original OpenAI request.
1322
1522
  *
1323
1523
  * The pi SDK's `Tool` interface has no `strict` field, so the SDK always sets
@@ -1391,9 +1591,11 @@ async function buildStreamOptions(model, request, options) {
1391
1591
  }
1392
1592
  const payloadFields = collectPayloadFields(request, model.api);
1393
1593
  const strictFlags = collectToolStrictFlags(request.tools);
1394
- if (payloadFields !== void 0 || strictFlags !== void 0) opts.onPayload = (payload) => {
1594
+ const needsGooglePatch = isGoogleApi(model.api);
1595
+ if (payloadFields !== void 0 || strictFlags !== void 0 || needsGooglePatch) opts.onPayload = (payload) => {
1395
1596
  if (isRecord(payload)) {
1396
1597
  if (payloadFields !== void 0) for (const [key, value] of Object.entries(payloadFields)) payload[key] = value;
1598
+ if (needsGooglePatch) patchGooglePayload(payload, request);
1397
1599
  if (strictFlags !== void 0) applyToolStrictFlags(payload, strictFlags);
1398
1600
  }
1399
1601
  return payload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@victor-software-house/pi-openai-proxy",
3
- "version": "4.7.1",
3
+ "version": "4.9.0",
4
4
  "description": "OpenAI-compatible HTTP proxy for pi's multi-provider model registry",
5
5
  "license": "MIT",
6
6
  "author": "Victor Software House",