@victor-software-house/pi-openai-proxy 4.7.0 → 4.8.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 +188 -8
  2. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1247,18 +1247,28 @@ const REASONING_EFFORT_MAP = {
1247
1247
  xhigh: "xhigh"
1248
1248
  };
1249
1249
  /**
1250
- * APIs where onPayload passthrough fields are not supported.
1251
- * These APIs use non-standard request formats that reject standard OpenAI fields.
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
1252
  */
1253
- const SKIP_PAYLOAD_PASSTHROUGH_APIS = new Set(["openai-codex-responses"]);
1253
+ const OPENAI_COMPATIBLE_APIS = new Set([
1254
+ "openai-completions",
1255
+ "openai-responses",
1256
+ "azure-openai-responses",
1257
+ "mistral-conversations"
1258
+ ]);
1259
+ const ANTHROPIC_APIS = new Set(["anthropic-messages"]);
1260
+ const GOOGLE_APIS = new Set([
1261
+ "google-generative-ai",
1262
+ "google-gemini-cli",
1263
+ "google-vertex"
1264
+ ]);
1254
1265
  /**
1255
- * Collect fields that need to be injected via onPayload.
1256
- * Skips passthrough for APIs that use non-standard request formats.
1266
+ * Collect OpenAI-format fields for OpenAI-compatible APIs.
1267
+ * Fields are injected as flat top-level properties on the payload.
1257
1268
  *
1258
1269
  * @internal Exported for unit testing only.
1259
1270
  */
1260
- function collectPayloadFields(request, api) {
1261
- if (SKIP_PAYLOAD_PASSTHROUGH_APIS.has(api)) return;
1271
+ function collectOpenAIPayloadFields(request) {
1262
1272
  const fields = {};
1263
1273
  let hasFields = false;
1264
1274
  if (request.stop !== void 0) {
@@ -1308,6 +1318,174 @@ function collectPayloadFields(request, api) {
1308
1318
  return hasFields ? fields : void 0;
1309
1319
  }
1310
1320
  /**
1321
+ * Translate OpenAI tool_choice to Anthropic tool_choice format.
1322
+ *
1323
+ * OpenAI "auto" -> { type: "auto" }
1324
+ * OpenAI "none" -> { type: "none" } (Anthropic skips tool calling)
1325
+ * OpenAI "required" -> { type: "any" } (force tool use)
1326
+ * OpenAI { type: "function", function: { name } } -> { type: "tool", name }
1327
+ *
1328
+ * @internal Exported for unit testing only.
1329
+ */
1330
+ function translateToolChoiceForAnthropic(toolChoice) {
1331
+ if (toolChoice === void 0) return;
1332
+ if (toolChoice === "auto") return { type: "auto" };
1333
+ if (toolChoice === "none") return { type: "none" };
1334
+ if (toolChoice === "required") return { type: "any" };
1335
+ return {
1336
+ type: "tool",
1337
+ name: toolChoice.function.name
1338
+ };
1339
+ }
1340
+ /**
1341
+ * Collect Anthropic-format fields translated from the OpenAI request.
1342
+ *
1343
+ * Supported translations:
1344
+ * - top_p -> top_p (same name, natively supported)
1345
+ * - stop -> stop_sequences (different field name)
1346
+ * - tool_choice -> Anthropic tool_choice format (object with type)
1347
+ * - parallel_tool_calls: false -> disable_parallel_tool_use on tool_choice
1348
+ * - user -> metadata.user_id
1349
+ *
1350
+ * Not supported (silently skipped — these concepts don't exist in Anthropic):
1351
+ * - seed, frequency_penalty, presence_penalty, response_format, prediction, metadata (arbitrary keys)
1352
+ *
1353
+ * @internal Exported for unit testing only.
1354
+ */
1355
+ function collectAnthropicPayloadFields(request) {
1356
+ const fields = {};
1357
+ let hasFields = false;
1358
+ if (request.top_p !== void 0) {
1359
+ fields["top_p"] = request.top_p;
1360
+ hasFields = true;
1361
+ }
1362
+ if (request.stop !== void 0) {
1363
+ fields["stop_sequences"] = Array.isArray(request.stop) ? request.stop : [request.stop];
1364
+ hasFields = true;
1365
+ }
1366
+ const toolChoice = translateToolChoiceForAnthropic(request.tool_choice);
1367
+ const disableParallel = request.parallel_tool_calls === false;
1368
+ if (toolChoice !== void 0) {
1369
+ if (disableParallel) toolChoice["disable_parallel_tool_use"] = true;
1370
+ fields["tool_choice"] = toolChoice;
1371
+ hasFields = true;
1372
+ } else if (disableParallel) {
1373
+ fields["tool_choice"] = {
1374
+ type: "auto",
1375
+ disable_parallel_tool_use: true
1376
+ };
1377
+ hasFields = true;
1378
+ }
1379
+ if (request.user !== void 0) {
1380
+ fields["metadata"] = { user_id: request.user };
1381
+ hasFields = true;
1382
+ }
1383
+ return hasFields ? fields : void 0;
1384
+ }
1385
+ /**
1386
+ * Translate OpenAI tool_choice to Google FunctionCallingConfigMode string.
1387
+ *
1388
+ * OpenAI "auto" -> "AUTO"
1389
+ * OpenAI "none" -> "NONE"
1390
+ * OpenAI "required" -> "ANY"
1391
+ * Named function choice -> "ANY" (Google doesn't support per-function forcing
1392
+ * in the same way, but ANY forces tool use)
1393
+ */
1394
+ function translateToolChoiceForGoogle(toolChoice) {
1395
+ if (toolChoice === void 0) return;
1396
+ if (toolChoice === "auto") return "AUTO";
1397
+ if (toolChoice === "none") return "NONE";
1398
+ return "ANY";
1399
+ }
1400
+ /**
1401
+ * Patch Google's nested payload structure with translated fields.
1402
+ *
1403
+ * Google's payload shape: { model, contents, config: { generationConfig, toolConfig, ... } }
1404
+ * Fields go into config.generationConfig (camelCase) or config.toolConfig.
1405
+ *
1406
+ * Supported translations:
1407
+ * - top_p -> config.generationConfig.topP (camelCase, nested)
1408
+ * - stop -> config.generationConfig.stopSequences (array, nested)
1409
+ * - seed -> config.generationConfig.seed (nested)
1410
+ * - frequency_penalty -> config.generationConfig.frequencyPenalty (nested)
1411
+ * - presence_penalty -> config.generationConfig.presencePenalty (nested)
1412
+ * - tool_choice -> config.toolConfig.functionCallingConfig.mode (nested)
1413
+ *
1414
+ * Not supported (silently skipped):
1415
+ * - response_format, metadata, prediction, parallel_tool_calls, user
1416
+ *
1417
+ * @internal Exported for unit testing only.
1418
+ */
1419
+ function patchGooglePayload(payload, request) {
1420
+ let patched = false;
1421
+ const config = isRecord(payload["config"]) ? payload["config"] : void 0;
1422
+ if (config === void 0) return false;
1423
+ let genConfig = isRecord(config["generationConfig"]) ? config["generationConfig"] : void 0;
1424
+ if (request.top_p !== void 0) {
1425
+ genConfig ??= {};
1426
+ genConfig["topP"] = request.top_p;
1427
+ patched = true;
1428
+ }
1429
+ if (request.stop !== void 0) {
1430
+ genConfig ??= {};
1431
+ const sequences = Array.isArray(request.stop) ? request.stop : [request.stop];
1432
+ genConfig["stopSequences"] = sequences;
1433
+ patched = true;
1434
+ }
1435
+ if (request.seed !== void 0) {
1436
+ genConfig ??= {};
1437
+ genConfig["seed"] = request.seed;
1438
+ patched = true;
1439
+ }
1440
+ if (request.frequency_penalty !== void 0) {
1441
+ genConfig ??= {};
1442
+ genConfig["frequencyPenalty"] = request.frequency_penalty;
1443
+ patched = true;
1444
+ }
1445
+ if (request.presence_penalty !== void 0) {
1446
+ genConfig ??= {};
1447
+ genConfig["presencePenalty"] = request.presence_penalty;
1448
+ patched = true;
1449
+ }
1450
+ if (genConfig !== void 0 && patched) config["generationConfig"] = genConfig;
1451
+ const mode = translateToolChoiceForGoogle(request.tool_choice);
1452
+ if (mode !== void 0) {
1453
+ let toolConfig = isRecord(config["toolConfig"]) ? config["toolConfig"] : void 0;
1454
+ toolConfig ??= {};
1455
+ let funcConfig = isRecord(toolConfig["functionCallingConfig"]) ? toolConfig["functionCallingConfig"] : void 0;
1456
+ funcConfig ??= {};
1457
+ funcConfig["mode"] = mode;
1458
+ toolConfig["functionCallingConfig"] = funcConfig;
1459
+ config["toolConfig"] = toolConfig;
1460
+ patched = true;
1461
+ }
1462
+ return patched;
1463
+ }
1464
+ /**
1465
+ * Collect API-specific payload fields from an OpenAI request.
1466
+ *
1467
+ * Dispatches to the appropriate translator based on the target API:
1468
+ * - OpenAI-compatible: flat field injection (same names)
1469
+ * - Anthropic: translated field names and formats
1470
+ * - Google: nested generationConfig patching (handled separately in onPayload)
1471
+ * - Others (Bedrock, Codex): no passthrough
1472
+ *
1473
+ * For Google APIs, returns undefined (patching is done directly in onPayload
1474
+ * via patchGooglePayload because the payload structure is nested).
1475
+ *
1476
+ * @internal Exported for unit testing only.
1477
+ */
1478
+ function collectPayloadFields(request, api) {
1479
+ if (OPENAI_COMPATIBLE_APIS.has(api)) return collectOpenAIPayloadFields(request);
1480
+ if (ANTHROPIC_APIS.has(api)) return collectAnthropicPayloadFields(request);
1481
+ }
1482
+ /**
1483
+ * Whether the given API requires Google-style nested payload patching.
1484
+ */
1485
+ function isGoogleApi(api) {
1486
+ return GOOGLE_APIS.has(api);
1487
+ }
1488
+ /**
1311
1489
  * Collect tool strict flags from the original OpenAI request.
1312
1490
  *
1313
1491
  * The pi SDK's `Tool` interface has no `strict` field, so the SDK always sets
@@ -1381,9 +1559,11 @@ async function buildStreamOptions(model, request, options) {
1381
1559
  }
1382
1560
  const payloadFields = collectPayloadFields(request, model.api);
1383
1561
  const strictFlags = collectToolStrictFlags(request.tools);
1384
- if (payloadFields !== void 0 || strictFlags !== void 0) opts.onPayload = (payload) => {
1562
+ const needsGooglePatch = isGoogleApi(model.api);
1563
+ if (payloadFields !== void 0 || strictFlags !== void 0 || needsGooglePatch) opts.onPayload = (payload) => {
1385
1564
  if (isRecord(payload)) {
1386
1565
  if (payloadFields !== void 0) for (const [key, value] of Object.entries(payloadFields)) payload[key] = value;
1566
+ if (needsGooglePatch) patchGooglePayload(payload, request);
1387
1567
  if (strictFlags !== void 0) applyToolStrictFlags(payload, strictFlags);
1388
1568
  }
1389
1569
  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.0",
3
+ "version": "4.8.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",