@earendil-works/pi-ai 1.0.4 → 1.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.
Files changed (121) hide show
  1. package/README.md +14 -3
  2. package/dist/api/anthropic-messages.d.ts.map +1 -1
  3. package/dist/api/anthropic-messages.js +63 -22
  4. package/dist/api/anthropic-messages.js.map +1 -1
  5. package/dist/api/bedrock-converse-stream.js +33 -2
  6. package/dist/api/bedrock-converse-stream.js.map +1 -1
  7. package/dist/api/classifier-shared.d.ts +21 -0
  8. package/dist/api/classifier-shared.d.ts.map +1 -0
  9. package/dist/api/classifier-shared.js +98 -0
  10. package/dist/api/classifier-shared.js.map +1 -0
  11. package/dist/api/cloudflare-workers-ai-system-one.d.ts.map +1 -1
  12. package/dist/api/cloudflare-workers-ai-system-one.js +2 -1
  13. package/dist/api/cloudflare-workers-ai-system-one.js.map +1 -1
  14. package/dist/api/lazy.d.ts.map +1 -1
  15. package/dist/api/lazy.js +4 -3
  16. package/dist/api/lazy.js.map +1 -1
  17. package/dist/api/llama-cpp-classify.d.ts.map +1 -1
  18. package/dist/api/llama-cpp-classify.js +2 -0
  19. package/dist/api/llama-cpp-classify.js.map +1 -1
  20. package/dist/api/mistral-conversations.d.ts.map +1 -1
  21. package/dist/api/mistral-conversations.js +31 -14
  22. package/dist/api/mistral-conversations.js.map +1 -1
  23. package/dist/api/openai-codex-responses.d.ts.map +1 -1
  24. package/dist/api/openai-codex-responses.js +7 -4
  25. package/dist/api/openai-codex-responses.js.map +1 -1
  26. package/dist/api/openai-decisions.d.ts +4 -0
  27. package/dist/api/openai-decisions.d.ts.map +1 -0
  28. package/dist/api/openai-decisions.js +162 -0
  29. package/dist/api/openai-decisions.js.map +1 -0
  30. package/dist/api/openai-decisions.lazy.d.ts +3 -0
  31. package/dist/api/openai-decisions.lazy.d.ts.map +1 -0
  32. package/dist/api/openai-decisions.lazy.js +4 -0
  33. package/dist/api/openai-decisions.lazy.js.map +1 -0
  34. package/dist/api/system-one-shared.d.ts +0 -1
  35. package/dist/api/system-one-shared.d.ts.map +1 -1
  36. package/dist/api/system-one-shared.js +5 -89
  37. package/dist/api/system-one-shared.js.map +1 -1
  38. package/dist/api/typesafe-system-one.d.ts.map +1 -1
  39. package/dist/api/typesafe-system-one.js +2 -1
  40. package/dist/api/typesafe-system-one.js.map +1 -1
  41. package/dist/auth/oauth/anthropic.d.ts.map +1 -1
  42. package/dist/auth/oauth/anthropic.js +14 -6
  43. package/dist/auth/oauth/anthropic.js.map +1 -1
  44. package/dist/auth/oauth/openai-chatgpt.js +1 -1
  45. package/dist/auth/oauth/openai-chatgpt.js.map +1 -1
  46. package/dist/auth/oauth/openai-codex.d.ts.map +1 -1
  47. package/dist/auth/oauth/openai-codex.js +4 -4
  48. package/dist/auth/oauth/openai-codex.js.map +1 -1
  49. package/dist/auth/types.d.ts +5 -0
  50. package/dist/auth/types.d.ts.map +1 -1
  51. package/dist/auth/types.js.map +1 -1
  52. package/dist/models.d.ts +5 -0
  53. package/dist/models.d.ts.map +1 -1
  54. package/dist/models.js +3 -1
  55. package/dist/models.js.map +1 -1
  56. package/dist/providers/compat-schema.d.ts +338 -0
  57. package/dist/providers/compat-schema.d.ts.map +1 -0
  58. package/dist/providers/compat-schema.js +375 -0
  59. package/dist/providers/compat-schema.js.map +1 -0
  60. package/dist/providers/data/.manifest.json +1 -1
  61. package/dist/providers/data/amazon-bedrock.json +1 -1
  62. package/dist/providers/data/anthropic.json +1 -1
  63. package/dist/providers/data/baseten.json +1 -1
  64. package/dist/providers/data/cloudflare-ai-gateway.json +1 -1
  65. package/dist/providers/data/cloudflare-workers-ai.json +1 -1
  66. package/dist/providers/data/github-copilot.json +1 -1
  67. package/dist/providers/data/google-vertex.json +1 -1
  68. package/dist/providers/data/google.json +1 -1
  69. package/dist/providers/data/huggingface.json +1 -1
  70. package/dist/providers/data/meta.json +1 -1
  71. package/dist/providers/data/minimax-cn.json +1 -1
  72. package/dist/providers/data/minimax.json +1 -1
  73. package/dist/providers/data/mistral.json +1 -1
  74. package/dist/providers/data/openai.json +1 -1
  75. package/dist/providers/data/opencode-go.json +1 -1
  76. package/dist/providers/data/opencode.json +1 -1
  77. package/dist/providers/data/openrouter.json +1 -1
  78. package/dist/providers/data/radius.json +1 -1
  79. package/dist/providers/data/vercel-ai-gateway.json +1 -1
  80. package/dist/providers/faux.d.ts.map +1 -1
  81. package/dist/providers/faux.js +25 -8
  82. package/dist/providers/faux.js.map +1 -1
  83. package/dist/providers/model-schema.d.ts +91 -0
  84. package/dist/providers/model-schema.d.ts.map +1 -0
  85. package/dist/providers/model-schema.js +55 -0
  86. package/dist/providers/model-schema.js.map +1 -0
  87. package/dist/providers/openai.d.ts.map +1 -1
  88. package/dist/providers/openai.js +12 -3
  89. package/dist/providers/openai.js.map +1 -1
  90. package/dist/providers/openrouter-api.d.ts +40 -0
  91. package/dist/providers/openrouter-api.d.ts.map +1 -0
  92. package/dist/providers/openrouter-api.js +41 -0
  93. package/dist/providers/openrouter-api.js.map +1 -0
  94. package/dist/providers/openrouter.d.ts +11 -1
  95. package/dist/providers/openrouter.d.ts.map +1 -1
  96. package/dist/providers/openrouter.js +52 -1
  97. package/dist/providers/openrouter.js.map +1 -1
  98. package/dist/providers/radius.d.ts.map +1 -1
  99. package/dist/providers/radius.js +4 -12
  100. package/dist/providers/radius.js.map +1 -1
  101. package/dist/types.d.ts +21 -304
  102. package/dist/types.d.ts.map +1 -1
  103. package/dist/types.js.map +1 -1
  104. package/dist/utils/estimate.js +1 -1
  105. package/dist/utils/estimate.js.map +1 -1
  106. package/dist/utils/event-stream.d.ts +10 -1
  107. package/dist/utils/event-stream.d.ts.map +1 -1
  108. package/dist/utils/event-stream.js +25 -0
  109. package/dist/utils/event-stream.js.map +1 -1
  110. package/dist/utils/model-operations.d.ts +3 -1
  111. package/dist/utils/model-operations.d.ts.map +1 -1
  112. package/dist/utils/model-operations.js +6 -0
  113. package/dist/utils/model-operations.js.map +1 -1
  114. package/dist/utils/provider-retry.d.ts +2 -0
  115. package/dist/utils/provider-retry.d.ts.map +1 -1
  116. package/dist/utils/provider-retry.js +2 -0
  117. package/dist/utils/provider-retry.js.map +1 -1
  118. package/dist/utils/retry.d.ts.map +1 -1
  119. package/dist/utils/retry.js +5 -2
  120. package/dist/utils/retry.js.map +1 -1
  121. package/package.json +2 -2
package/README.md CHANGED
@@ -60,7 +60,7 @@ Unified LLM API with provider collections, automatic auth resolution, token and
60
60
 
61
61
  ## Supported Providers
62
62
 
63
- - **OpenAI**
63
+ - **OpenAI** (including the Decisions classifier API)
64
64
  - **Ant Ling**
65
65
  - **Azure OpenAI (Responses)**
66
66
  - **OpenAI Codex (legacy)** (ChatGPT Plus/Pro subscription, requires OAuth, see below)
@@ -890,7 +890,7 @@ console.log(model.output); // ['image'] or ['image', 'text']
890
890
 
891
891
  ## Classification
892
892
 
893
- Classifier models consume structured JSON state and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model is available from these built-in providers:
893
+ Classifier models consume structured JSON state, and optionally images, and answer one or more typed questions. They do not use chat or image-generation APIs. TypeSafe's Jev model, Cloudflare's Clef models, and OpenAI's GPT-6 Luna are available from these built-in providers:
894
894
 
895
895
  | Provider | Model IDs | Auth |
896
896
  | --- | --- | --- |
@@ -899,6 +899,7 @@ Classifier models consume structured JSON state and answer one or more typed que
899
899
  | `cloudflare-workers-ai` | `typesafe/jev`, `@cf/cloudflare/clef`, `@cf/cloudflare/clef-flash` | `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ACCOUNT_ID` |
900
900
  | `vercel-ai-gateway` | `typesafe-ai/jev` | `AI_GATEWAY_API_KEY` |
901
901
  | `opencode` | `jev-1.13`, `jev-1.13-free` | `OPENCODE_API_KEY` |
902
+ | `openai` | `gpt-6-luna` | `OPENAI_API_KEY` |
902
903
 
903
904
  ```typescript
904
905
  import { builtinModels } from '@earendil-works/pi-ai/providers/all';
@@ -936,10 +937,20 @@ The public contract uses `bool` questions and `{ type: "bool", probability }` an
936
937
 
937
938
  When the service reports token counts, `result.usage` carries them with their cost at the model's catalog price, the same `Usage` shape as chat messages. All System One services report token counts; a request that was answered with malformed answers keeps its usage. Local classifiers such as `llama-cpp-classify` report no usage.
938
939
 
939
- `ClassifierOptions.temperature` divides the answer logits by the given value before they are normalized; values above 1 soften the distribution. APIs that cannot apply it, such as System One, ignore it.
940
+ `ClassifierOptions.temperature` divides the answer logits by the given value before they are normalized; values above 1 soften the distribution. APIs that cannot apply it, such as System One and OpenAI Decisions, ignore it.
941
+
942
+ `ClassifierContext.images` holds image blocks (`{ type: "image", data, mimeType }`) judged together with the state. Only models whose `input` includes `"image"` accept them; `classify()` returns an error result for other models.
943
+
944
+ ### OpenAI Decisions
945
+
946
+ The `openai-decisions` API calls OpenAI's [Decisions API](https://developers.openai.com/api/docs/guides/decisions) (`POST /v1/decisions`). GPT-6 Luna is its only model. The state is sent as JSON text; with images, the input is one user message with the state followed by the images as data URLs (at most 128). `choice` and `score` questions map to the Decisions types of the same name. `bool` questions become `predicate` questions; predicates have no criteria field, so the meanings of true and false are appended to the instructions. If OpenAI refuses to answer a question, the whole result is an error. Usage reports input tokens only, priced at the catalog input rate with OpenAI's long-context multiplier.
947
+
948
+ The endpoint needs an OpenAI API key; Sign in with ChatGPT credentials are rejected, so the `openai` provider hides `gpt-6-luna` from `getAvailableOfType('classifier')` while it uses OAuth. The API rejects inputs above 922K tokens, but requests that run longer than about five seconds, currently above roughly 600K input tokens, fail with a gateway timeout (504).
940
949
 
941
950
  ### Chat models on llama.cpp
942
951
 
952
+ llama.cpp also serves decision models such as Julia-1 or Kev natively through `/v1/systemone`. Since llama.cpp 0.6.0, `GET /models` lists `decisions` in a model's `architecture.output_modalities` for these models. They use the `typesafe-system-one` API with the server's `/v1` URL as `baseUrl` (for example `http://127.0.0.1:8080/v1`); it needs an `apiKey`, which llama.cpp ignores unless it was started with `--api-key`. The `llama-cpp-classify` API is the fallback for chat models.
953
+
943
954
  The `llama-cpp-classify` API turns a chat model served by llama.cpp's `llama-server` into a classifier. Each question becomes one chat prompt: the state, every question of the request, the state again, and the question with its answers under single-token labels (letters for a choice, `Yes`/`No` for a bool, digits for a score). The prompt up to the final question is shared by all questions of a request, so the server's prompt cache evaluates the state once per request. The server returns the log-probabilities of the next token, and the answer is the softmax over the label tokens. Choices support up to 62 options and scores up to 10 levels. The model's `baseUrl` is the server URL; a trailing `/v1` is ignored. In router mode, the model ID selects the model.
944
955
 
945
956
  ```typescript
@@ -1 +1 @@
1
- {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAmJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAwRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA0UzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
1
+ {"version":3,"file":"anthropic-messages.d.ts","sourceRoot":"","sources":["../../src/api/anthropic-messages.ts"],"names":[],"mappings":"AAAA,OAAO,SAAiC,MAAM,mBAAmB,CAAC;AAoBlE,OAAO,KAAK,EASX,mBAAmB,EAEnB,cAAc,EACd,aAAa,EAMb,MAAM,aAAa,CAAC;AAmJrB,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,CAAC;AAE1E,MAAM,MAAM,wBAAwB,GAAG,YAAY,GAAG,SAAS,CAAC;AA2ChE,MAAM,WAAW,gBAAiB,SAAQ,aAAa;IACtD;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;;;;;OAWG;IACH,MAAM,CAAC,EAAE,eAAe,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,eAAe,CAAC,EAAE,wBAAwB,CAAC;IAC3C;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;OAIG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;CACnB;AAwRD,eAAO,MAAM,MAAM,EAAE,cAAc,CAAC,oBAAoB,EAAE,gBAAgB,CA2WzE,CAAC;AA2BF,eAAO,MAAM,YAAY,EAAE,cAAc,CAAC,oBAAoB,EAAE,mBAAmB,CAgDlF,CAAC"}
@@ -425,21 +425,53 @@ export const stream = (model, context, options) => {
425
425
  client = created.client;
426
426
  isOAuth = created.isOAuthToken;
427
427
  }
428
- let params = buildParams(model, normalizedContext, isOAuth, options);
429
- const nextParams = await options?.onPayload?.(params, model);
430
- if (nextParams !== undefined) {
431
- params = { ...nextParams, stream: true };
432
- }
433
428
  const requestOptions = {
434
429
  ...(options?.signal ? { signal: options.signal } : {}),
435
430
  ...(options?.timeoutMs !== undefined ? { timeout: options.timeoutMs } : {}),
436
431
  maxRetries: 0,
437
432
  };
438
- const response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
439
- maxRetries: options?.maxRetries,
440
- maxRetryDelayMs: options?.maxRetryDelayMs,
441
- signal: options?.signal,
442
- });
433
+ let disablePreferredStrictTools = false;
434
+ let response;
435
+ for (;;) {
436
+ options?.signal?.throwIfAborted();
437
+ const built = buildParams(model, normalizedContext, isOAuth, options, disablePreferredStrictTools);
438
+ let params = built.params;
439
+ const nextParams = await options?.onPayload?.(params, model);
440
+ if (nextParams !== undefined) {
441
+ params = { ...nextParams, stream: true };
442
+ }
443
+ try {
444
+ response = await retryProviderRequest(() => client.beta.messages.create(params, requestOptions).asResponse(), {
445
+ maxRetries: options?.maxRetries,
446
+ maxRetryDelayMs: options?.maxRetryDelayMs,
447
+ signal: options?.signal,
448
+ });
449
+ break;
450
+ }
451
+ catch (error) {
452
+ // Only downgrade a rejected request, never an already-started stream.
453
+ if (options?.signal?.aborted ||
454
+ disablePreferredStrictTools ||
455
+ !built.hasPreferredStrictTools ||
456
+ !(error instanceof Anthropic.APIError) ||
457
+ error.status !== 400 ||
458
+ error.type !== "invalid_request_error" ||
459
+ ![
460
+ "The compiled grammar is too large",
461
+ "Too many strict tools (",
462
+ "Schemas contains too many parameters with union types (",
463
+ ].some((message) => error.message.includes(message))) {
464
+ throw error;
465
+ }
466
+ disablePreferredStrictTools = true;
467
+ appendAssistantMessageDiagnostic(output, {
468
+ type: "anthropic_strict_tools_fallback",
469
+ timestamp: Date.now(),
470
+ error: { message: error.message },
471
+ details: { reason: "strict_tool_limits_exceeded" },
472
+ });
473
+ }
474
+ }
443
475
  await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model);
444
476
  stream.push({ type: "start", partial: output });
445
477
  const blocks = output.content;
@@ -855,7 +887,7 @@ function getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, option
855
887
  features.push(INLINE_TOOLS_BETA);
856
888
  return [...new Set(features)];
857
889
  }
858
- function buildParams(model, context, isOAuthToken, options) {
890
+ function buildParams(model, context, isOAuthToken, options, disablePreferredStrictTools = false) {
859
891
  const { cacheControl } = getCacheControl(model, options?.cacheRetention, options?.env);
860
892
  const compat = getAnthropicCompat(model);
861
893
  const initialSystemMessage = getInitialSystemMessage(context.messages);
@@ -866,11 +898,20 @@ function buildParams(model, context, isOAuthToken, options) {
866
898
  // by value in a `tool_addition` block, which also expresses same-name redefinitions.
867
899
  // Anthropic rejects a tool list where every tool is deferred, so there must be an initial
868
900
  // active tool to anchor the placeholder. Otherwise the current tool list is sent.
901
+ let hasPreferredStrictTools = false;
902
+ const convertToolDefinitions = (tools, toolCacheControl) => {
903
+ const definitions = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl, disablePreferredStrictTools);
904
+ if (definitions.some((definition, index) => {
905
+ const config = tools[index].constrainedSampling;
906
+ return definition.strict === true && config && config.type === "json_schema" && config.strict === "prefer";
907
+ })) {
908
+ hasPreferredStrictTools = true;
909
+ }
910
+ return definitions;
911
+ };
869
912
  const initialTools = initialSystemMessage?.toolsAdded ?? [];
870
913
  const nativeToolChanges = compat.supportsMidConvoSystemMessages && compat.supportsMidConvoToolChanges && initialTools.length > 0;
871
- const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined, nativeToolChanges
872
- ? (tools) => convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools)
873
- : undefined);
914
+ const converted = convertMessages(conversationMessages, isOAuthToken, cacheControl, compat.allowEmptySignature, model.compat?.supportsMidConvoEffort === true ? model.provider : undefined, nativeToolChanges ? convertToolDefinitions : undefined);
874
915
  const activeEffort = options?.effort ?? "high";
875
916
  const betaFeatures = getBetaFeatures(model, context, isOAuthToken, nativeToolChanges, options);
876
917
  const params = {
@@ -922,15 +963,12 @@ function buildParams(model, context, isOAuthToken, options) {
922
963
  // placeholder. The list never changes afterwards: later tools are defined by value in
923
964
  // `tool_addition` blocks and withdrawn by `tool_removal`, so the cached prefix survives
924
965
  // every tool change.
925
- params.tools = [
926
- ...convertTools(initialTools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl),
927
- DEFERRED_TOOL_PLACEHOLDER,
928
- ];
966
+ params.tools = [...convertToolDefinitions(initialTools, toolCacheControl), DEFERRED_TOOL_PLACEHOLDER];
929
967
  }
930
968
  else {
931
969
  const tools = getCurrentTools(context.messages);
932
970
  if (tools.length > 0) {
933
- params.tools = convertTools(tools, isOAuthToken, compat.supportsEagerToolInputStreaming, compat.supportsStrictTools, toolCacheControl);
971
+ params.tools = convertToolDefinitions(tools, toolCacheControl);
934
972
  }
935
973
  }
936
974
  // Managed effort models always use adaptive thinking so prefix mismatches can
@@ -986,7 +1024,7 @@ function buildParams(model, context, isOAuthToken, options) {
986
1024
  if (allowedFallbackModels && allowedFallbackModels.length > 0) {
987
1025
  params.fallbacks = allowedFallbackModels.map((fallback) => ({ model: fallback.model }));
988
1026
  }
989
- return params;
1027
+ return { params, hasPreferredStrictTools };
990
1028
  }
991
1029
  // Normalize tool call IDs to match Anthropic's required pattern and length
992
1030
  function normalizeToolCallId(id) {
@@ -1255,11 +1293,14 @@ const isAnthropicStrictUnsupportedKeyword = (key, value) => {
1255
1293
  return typeof value !== "string" || !ANTHROPIC_STRICT_STRING_FORMATS.has(value);
1256
1294
  return false;
1257
1295
  };
1258
- function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl) {
1296
+ function convertTools(tools, isOAuthToken, supportsEagerToolInputStreaming, supportsStrictTools, cacheControl, disablePreferredStrictTools = false) {
1259
1297
  if (!tools)
1260
1298
  return [];
1261
1299
  return tools.map((tool, index) => {
1262
- const strict = resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1300
+ const config = tool.constrainedSampling;
1301
+ const strict = disablePreferredStrictTools && config && config.type === "json_schema" && config.strict === "prefer"
1302
+ ? undefined
1303
+ : resolveJsonSchemaStrictSampling(tool, supportsStrictTools, isAnthropicStrictUnsupportedKeyword);
1263
1304
  const parameters = getJsonSchemaToolParameters(tool, strict);
1264
1305
  const schema = parameters;
1265
1306
  const legacyInputSchema = {