@oh-my-pi/pi-ai 17.2.13 → 17.2.14

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.14] - 2026-08-11
6
+
7
+ ### Added
8
+
9
+ - Added `forceReasoningOff` and `disableReasoning` options to disable reasoning in OpenAI and Azure OpenAI models
10
+
5
11
  ## [17.2.13] - 2026-08-11
6
12
 
7
13
  ### Changed
@@ -9,6 +9,7 @@ export interface AzureOpenAIResponsesOptions extends StreamOptions {
9
9
  azureDeploymentName?: string;
10
10
  toolChoice?: ToolChoice;
11
11
  serviceTier?: ServiceTier;
12
+ disableReasoning?: boolean;
12
13
  }
13
14
  /**
14
15
  * Generate function for Azure OpenAI Responses API
@@ -13,6 +13,8 @@ export interface ReasoningConfig {
13
13
  export interface CodexRequestOptions {
14
14
  /** User-facing effort; maps 1:1 onto the wire tier of the same name. */
15
15
  reasoningEffort?: CodexCallerEffort | "none";
16
+ /** Suppress native reasoning by sending `reasoning.effort: "none"`. */
17
+ reasoningOff?: boolean;
16
18
  reasoningSummary?: ReasoningConfig["summary"] | null;
17
19
  /** Explicit `reasoning.context` override. Omitted by default; Responses Lite forces `all_turns` as required by that transport. */
18
20
  reasoningContext?: CodexReasoningContext;
@@ -114,6 +114,7 @@ type OpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
114
114
  export declare const streamOpenAIResponses: StreamFunction<"openai-responses">;
115
115
  export declare function buildParams(model: Model<"openai-responses">, context: Context, options: OpenAIResponsesOptions | undefined, providerSessionState: OpenAIResponsesProviderSessionState | undefined, strictToolsScope?: OpenAIStrictToolsScope, disableStrictToolsOverride?: boolean, statefulCacheBaseline?: ResponseInput): {
116
116
  params: OpenAIResponsesSamplingParams;
117
+ trailingScaffoldingItems: number;
117
118
  strictToolsApplied: boolean;
118
119
  };
119
120
  /**
@@ -505,6 +505,8 @@ export declare function appendMessageContentPart(item: ResponseOutputMessage, pa
505
505
  export declare function appendMessageTextDelta(item: ResponseOutputMessage, block: TextContent, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number, partType: "output_text" | "refusal"): void;
506
506
  /** Chooses final message text while treating non-empty terminal content as authoritative. */
507
507
  export declare function finalizeMessageText(item: ResponseOutputMessage, streamedText: string): string;
508
+ export declare const JUICE_EFFORT_MAP: Record<string, number>;
509
+ export declare function getJuiceValue(effort?: string): number;
508
510
  export declare function accumulateToolCallArgumentsDelta(block: ResponsesToolCallBlock, delta: string, stream: AssistantMessageEventStream, output: AssistantMessage, contentIndex: number): void;
509
511
  /**
510
512
  * Finalize streamed function-call arguments from the authoritative `.done`
@@ -591,6 +593,14 @@ type ReasoningOptions = {
591
593
  export interface ApplyResponsesCompatPolicyOptions {
592
594
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
593
595
  mapEffort?: (effort: string) => string;
596
+ /**
597
+ * Suppress native reasoning by sending `reasoning.effort: "none"` — the only
598
+ * disable level the Responses API defines (`"off"` is not a wire value and
599
+ * 400s everywhere). Gateways that reject `none` for a given model are
600
+ * handled by the reasoning-effort fallback retry, which clamps to the
601
+ * lowest level the error reports as allowed.
602
+ */
603
+ forceReasoningOff?: boolean;
594
604
  }
595
605
  export declare function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(params: P, policy: OpenAICompatPolicy, options: ApplyResponsesCompatPolicyOptions | undefined): void;
596
606
  /**
@@ -294,6 +294,11 @@ export interface StreamOptions {
294
294
  * `false` so `previous_response_id` cannot explain a result.
295
295
  */
296
296
  statefulResponses?: boolean;
297
+ /**
298
+ * Emit `reasoning: { effort: "none" }` for OpenAI Responses and Codex requests.
299
+ * Used when a caller supplies an external reasoning scratchpad; other transports ignore it.
300
+ */
301
+ forceReasoningOff?: boolean;
297
302
  /**
298
303
  * Provider-scoped mutable state store for this agent session.
299
304
  * Providers can use this to persist transport/session state between turns.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.2.13",
4
+ "version": "17.2.14",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/omptype": "17.2.13",
42
- "@oh-my-pi/pi-catalog": "17.2.13",
43
- "@oh-my-pi/pi-utils": "17.2.13",
44
- "@oh-my-pi/pi-wire": "17.2.13"
41
+ "@oh-my-pi/omptype": "17.2.14",
42
+ "@oh-my-pi/pi-catalog": "17.2.14",
43
+ "@oh-my-pi/pi-utils": "17.2.14",
44
+ "@oh-my-pi/pi-wire": "17.2.14"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@bufbuild/protoc-gen-es": "^2.12.1",
@@ -65,6 +65,7 @@ export interface AzureOpenAIResponsesOptions extends StreamOptions {
65
65
  azureDeploymentName?: string;
66
66
  toolChoice?: ToolChoice;
67
67
  serviceTier?: ServiceTier;
68
+ disableReasoning?: boolean;
68
69
  }
69
70
 
70
71
  type AzureOpenAIResponsesSamplingParams = ResponseCreateParamsStreaming & {
@@ -32,6 +32,8 @@ export interface ReasoningConfig {
32
32
  export interface CodexRequestOptions {
33
33
  /** User-facing effort; maps 1:1 onto the wire tier of the same name. */
34
34
  reasoningEffort?: CodexCallerEffort | "none";
35
+ /** Suppress native reasoning by sending `reasoning.effort: "none"`. */
36
+ reasoningOff?: boolean;
35
37
  reasoningSummary?: ReasoningConfig["summary"] | null;
36
38
  /** Explicit `reasoning.context` override. Omitted by default; Responses Lite forces `all_turns` as required by that transport. */
37
39
  reasoningContext?: CodexReasoningContext;
@@ -454,9 +456,12 @@ export async function transformRequestBody(
454
456
  applyCodexResponsesLiteShape(body);
455
457
  }
456
458
 
457
- if (options.reasoningEffort !== undefined || responsesLite) {
458
- const reasoningConfig =
459
- options.reasoningEffort !== undefined ? getReasoningConfig(model, options.reasoningEffort, options) : {};
459
+ if (options.reasoningOff || options.reasoningEffort !== undefined || responsesLite) {
460
+ const reasoningConfig: Partial<ReasoningConfig> = options.reasoningOff
461
+ ? { effort: "none" }
462
+ : options.reasoningEffort !== undefined
463
+ ? getReasoningConfig(model, options.reasoningEffort, options)
464
+ : {};
460
465
  body.reasoning = {
461
466
  ...body.reasoning,
462
467
  ...reasoningConfig,
@@ -478,7 +483,7 @@ export async function transformRequestBody(
478
483
  // Catalog pro aliases (`gpt-5.6-*-pro`): applied after the effort branch so
479
484
  // the mode is sent even when no effort is set (the branch above deletes
480
485
  // `body.reasoning` in that case) — mode and effort are independent fields.
481
- if (model.reasoningMode) {
486
+ if (model.reasoningMode && !options.reasoningOff) {
482
487
  body.reasoning = { ...body.reasoning, mode: model.reasoningMode };
483
488
  }
484
489
 
@@ -1529,6 +1529,7 @@ export async function buildTransformedCodexRequestBody(
1529
1529
  }
1530
1530
  const codexOptions: CodexRequestOptions = {
1531
1531
  reasoningEffort: options?.reasoning,
1532
+ reasoningOff: options?.forceReasoningOff,
1532
1533
  reasoningSummary: options?.reasoningSummary,
1533
1534
  reasoningContext: options?.reasoningContext,
1534
1535
  textVerbosity: options?.textVerbosity,
@@ -132,7 +132,13 @@ function collectMessageParts(error: unknown, captured: CapturedHttpErrorResponse
132
132
  return parts.join("\n");
133
133
  }
134
134
 
135
- const REASONING_EFFORT_FIELD_PATTERN = /reasoning[_. ]effort|reasoning value/i;
135
+ /**
136
+ * Text that identifies a 400 as being about the reasoning-effort field.
137
+ * OpenAI-compatible gateways (cliproxy, …) never name the field — they reject
138
+ * the value alone with `level "none" not supported, valid levels: low, …` — so
139
+ * the allowed-level phrasing counts as a mention too.
140
+ */
141
+ const REASONING_EFFORT_FIELD_PATTERN = /reasoning[_. ]effort|reasoning value|(?:valid|supported|allowed) levels?/i;
136
142
 
137
143
  function mentionsReasoningEffort(error: unknown, captured: CapturedHttpErrorResponse | undefined): boolean {
138
144
  const param = capturedStringField(captured, "param");
@@ -168,10 +174,13 @@ function isInvalidReasoningEffortError(
168
174
  if (/(?:unsupported|not supported)[^\n]*(?:reasoning[_. ]effort|reasoning value)/i.test(message)) {
169
175
  return true;
170
176
  }
171
- return new RegExp(
172
- `(?:invalid|unsupported|not supported)[^\\n]*["'\`]${escapeRegExp(currentEffort)}["'\`]`,
173
- "i",
174
- ).test(message);
177
+ // Gateways put the rejected value first (`level "none" not supported`), the
178
+ // official API puts the verdict first (`Unsupported value: 'none'`).
179
+ const quoted = `["'\`]${escapeRegExp(currentEffort)}["'\`]`;
180
+ return (
181
+ new RegExp(`(?:invalid|unsupported|not supported)[^\\n]*${quoted}`, "i").test(message) ||
182
+ new RegExp(`${quoted}[^\\n]*(?:invalid|unsupported|not supported)`, "i").test(message)
183
+ );
175
184
  }
176
185
 
177
186
  function escapeRegExp(value: string): string {
@@ -186,9 +195,12 @@ function parseKnownReasoningValues(text: string): Set<string> {
186
195
  values.add(quotedMatch[1]!.toLowerCase());
187
196
  quotedMatch = quotedPattern.exec(text);
188
197
  }
189
- const allowedMatch = /(?:must be|one of|allowed values?|supported values?(?: are)?|expected)([^.\n]+)/i.exec(text);
198
+ const allowedMatch =
199
+ /(?:must be|one of|allowed values?|supported values?(?: are)?|expected|(?:valid|supported|allowed) levels?(?: are)?)[^.\n]+/i.exec(
200
+ text,
201
+ );
190
202
  if (allowedMatch) {
191
- const allowedText = allowedMatch[1]!;
203
+ const allowedText = allowedMatch[0]!;
192
204
  const barePattern = /\b(none|minimal|low|medium|high|xhigh|max)\b/gi;
193
205
  let bareMatch = barePattern.exec(allowedText);
194
206
  while (bareMatch !== null) {
@@ -201,7 +213,8 @@ function parseKnownReasoningValues(text: string): Set<string> {
201
213
 
202
214
  function parseAllowedReasoningValues(message: string, currentEffort: string): Set<string> | undefined {
203
215
  const values = parseKnownReasoningValues(message);
204
- const hasAllowedCue = /must be|one of|allowed values?|supported values?|expected/i.test(message);
216
+ const hasAllowedCue =
217
+ /must be|one of|allowed values?|supported values?|expected|(?:valid|supported|allowed) levels?/i.test(message);
205
218
  values.delete(currentEffort.toLowerCase());
206
219
  if (!hasAllowedCue && values.size === 0) return undefined;
207
220
  return values;
@@ -1,5 +1,6 @@
1
1
  import { scheduler } from "node:timers/promises";
2
2
  import { hostMatchesUrl } from "@oh-my-pi/pi-catalog/hosts";
3
+ import { bareModelId, parseOpenAIModel, semverGte } from "@oh-my-pi/pi-catalog/identity";
3
4
  import { $flag, logger, structuredCloneJSON } from "@oh-my-pi/pi-utils";
4
5
  import * as AIError from "../error";
5
6
  import { getEnvApiKey } from "../stream";
@@ -80,6 +81,7 @@ import {
80
81
  createInitialResponsesAssistantMessage,
81
82
  createOpenAIStrictToolsState,
82
83
  disableStrictToolsForScope,
84
+ getJuiceValue,
83
85
  getOpenAIPromptCacheKey,
84
86
  getOpenAIResponsesRoutingSessionId,
85
87
  getOpenAIStrictToolsScope,
@@ -298,14 +300,23 @@ interface OpenAIResponsesChainedParams {
298
300
  */
299
301
  function buildOpenAIResponsesChainedParams(
300
302
  params: OpenAIResponsesSamplingParams,
303
+ trailingScaffoldingItems: number,
301
304
  chain: OpenAIResponsesChainState,
302
305
  ): OpenAIResponsesChainedParams {
306
+ const historyParams =
307
+ trailingScaffoldingItems > 0 && Array.isArray(params.input)
308
+ ? { ...params, input: params.input.slice(0, params.input.length - trailingScaffoldingItems) }
309
+ : params;
303
310
  const deltaInput = chain.canAppend
304
- ? buildResponsesDeltaInput(chain.lastParams, chain.lastResponseItems, params)
311
+ ? buildResponsesDeltaInput(chain.lastParams, chain.lastResponseItems, historyParams)
305
312
  : null;
306
313
  if (deltaInput && deltaInput.length > 0 && chain.lastResponseId) {
314
+ const scaffolding =
315
+ historyParams !== params && Array.isArray(params.input)
316
+ ? params.input.slice(params.input.length - trailingScaffoldingItems)
317
+ : [];
307
318
  return {
308
- params: { ...params, previous_response_id: chain.lastResponseId, input: deltaInput },
319
+ params: { ...params, previous_response_id: chain.lastResponseId, input: [...deltaInput, ...scaffolding] },
309
320
  previousResponseId: chain.lastResponseId,
310
321
  };
311
322
  }
@@ -462,8 +473,9 @@ const streamOpenAIResponsesOnce = (
462
473
  false,
463
474
  chainState?.canAppend ? chainState.lastParams?.input : undefined,
464
475
  );
465
- const params = builtParams.params;
476
+ const { params, trailingScaffoldingItems } = builtParams;
466
477
  let activeParams = params;
478
+ let activeTrailingScaffoldingItems = trailingScaffoldingItems;
467
479
  const resolvedBaseUrl = (baseUrl ?? "https://api.openai.com/v1").replace(/\/+$/, "");
468
480
  const requestReasoningEffortFallbacks = new Map<string, OpenAIReasoningEffortFallback>();
469
481
  const attemptedReasoningEffortFallbacks = new Set<string>();
@@ -490,7 +502,9 @@ const streamOpenAIResponsesOnce = (
490
502
  }
491
503
  applyReasoningEffortFallbackForRequest(params);
492
504
  let chained: OpenAIResponsesChainedParams =
493
- chainState && !chainState.disabled ? buildOpenAIResponsesChainedParams(params, chainState) : { params };
505
+ chainState && !chainState.disabled
506
+ ? buildOpenAIResponsesChainedParams(params, trailingScaffoldingItems, chainState)
507
+ : { params };
494
508
  sentPreviousResponseId = chained.previousResponseId;
495
509
  const idleTimeoutMs =
496
510
  options?.streamIdleTimeoutMs ?? getOpenAIStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
@@ -586,7 +600,9 @@ const streamOpenAIResponsesOnce = (
586
600
  const reasoningEffortFallback =
587
601
  activeReasoningEffortFallbackKey && activeRequestParams && !requestSignal.aborted
588
602
  ? resolveOpenAIReasoningEffortFallback(error, capturedErrorResponse, activeRequestParams, {
589
- explicitDisable: options?.disableReasoning === true && options.reasoning === undefined,
603
+ explicitDisable:
604
+ options?.forceReasoningOff === true ||
605
+ (options?.disableReasoning === true && options.reasoning === undefined),
590
606
  })
591
607
  : undefined;
592
608
  if (reasoningEffortFallback !== undefined && activeReasoningEffortFallbackKey) {
@@ -632,7 +648,11 @@ const streamOpenAIResponsesOnce = (
632
648
  if (chainState && !chainState.disabled) fallbackParams.store = true;
633
649
  let fallbackChained: OpenAIResponsesChainedParams =
634
650
  chainState && !chainState.disabled
635
- ? buildOpenAIResponsesChainedParams(fallbackParams, chainState)
651
+ ? buildOpenAIResponsesChainedParams(
652
+ fallbackParams,
653
+ fallbackBuilt.trailingScaffoldingItems,
654
+ chainState,
655
+ )
636
656
  : { params: fallbackParams };
637
657
  sentPreviousResponseId = fallbackChained.previousResponseId;
638
658
  fallbackChained = {
@@ -642,7 +662,7 @@ const streamOpenAIResponsesOnce = (
642
662
  chained = fallbackChained;
643
663
  activeRawRequestDump.body = chained.params;
644
664
  activeParams = fallbackParams;
645
- activeStrictToolsApplied = fallbackBuilt.strictToolsApplied;
665
+ activeTrailingScaffoldingItems = fallbackBuilt.trailingScaffoldingItems;
646
666
  continue;
647
667
  }
648
668
  if (!chainState || !sentPreviousResponseId || requestSignal.aborted) {
@@ -688,6 +708,7 @@ const streamOpenAIResponsesOnce = (
688
708
  chained = { params: retryParams };
689
709
  activeRawRequestDump.body = retryParams;
690
710
  activeParams = currentParams;
711
+ activeTrailingScaffoldingItems = currentBuilt.trailingScaffoldingItems;
691
712
  activeStrictToolsApplied = currentBuilt.strictToolsApplied;
692
713
  }
693
714
  }
@@ -824,7 +845,17 @@ const streamOpenAIResponsesOnce = (
824
845
  if (replayableResponseItems) {
825
846
  if (providerSessionState) providerSessionState.nativeHistoryReplayWarmed = true;
826
847
  if (chainState) {
827
- chainState.lastParams = structuredCloneJSON(activeParams);
848
+ chainState.lastParams = structuredCloneJSON(
849
+ activeTrailingScaffoldingItems > 0 && Array.isArray(activeParams.input)
850
+ ? {
851
+ ...activeParams,
852
+ input: activeParams.input.slice(
853
+ 0,
854
+ activeParams.input.length - activeTrailingScaffoldingItems,
855
+ ),
856
+ }
857
+ : activeParams,
858
+ );
828
859
  chainState.lastPromptCacheBreakpointPolicy = promptCacheBreakpointPolicy;
829
860
  if (output.responseId) {
830
861
  chainState.lastResponseId = output.responseId;
@@ -843,7 +874,14 @@ const streamOpenAIResponsesOnce = (
843
874
  // baseline, but `lastParams` still records the successful wire controls
844
875
  // without re-enabling `previous_response_id` chaining.
845
876
  chainState.canAppend = false;
846
- chainState.lastParams = structuredCloneJSON(activeParams);
877
+ chainState.lastParams = structuredCloneJSON(
878
+ activeTrailingScaffoldingItems > 0 && Array.isArray(activeParams.input)
879
+ ? {
880
+ ...activeParams,
881
+ input: activeParams.input.slice(0, activeParams.input.length - activeTrailingScaffoldingItems),
882
+ }
883
+ : activeParams,
884
+ );
847
885
  chainState.lastPromptCacheBreakpointPolicy = promptCacheBreakpointPolicy;
848
886
  chainState.lastResponseId = undefined;
849
887
  chainState.lastResponseItems = undefined;
@@ -899,6 +937,17 @@ function isOfficialOpenAIResponsesEndpoint(model: Model<"openai-responses">): bo
899
937
  }
900
938
  }
901
939
 
940
+ /**
941
+ * GPT-5.6+ family check for Responses routes. The model id classifies the
942
+ * reasoning family regardless of the provider/host serving it — a cliproxy or
943
+ * other OpenAI-compatible gateway carrying `gpt-5.6-sol` gets the same
944
+ * scaffolding as the official endpoint.
945
+ */
946
+ function isGpt56PlusResponsesModel(model: Model<"openai-responses">): boolean {
947
+ const parsed = parseOpenAIModel(bareModelId(model.requestModelId ?? model.id));
948
+ return parsed !== null && semverGte(parsed.version, "5.6");
949
+ }
950
+
902
951
  function isResponsesPromptCacheableContentBlock(block: unknown): block is ResponseInputContent {
903
952
  if (typeof block !== "object" || block === null || !("type" in block)) return false;
904
953
  return block.type === "input_text" || block.type === "input_image" || block.type === "input_file";
@@ -1090,7 +1139,7 @@ export function buildParams(
1090
1139
  strictToolsScope?: OpenAIStrictToolsScope,
1091
1140
  disableStrictToolsOverride = false,
1092
1141
  statefulCacheBaseline?: ResponseInput,
1093
- ): { params: OpenAIResponsesSamplingParams; strictToolsApplied: boolean } {
1142
+ ): { params: OpenAIResponsesSamplingParams; trailingScaffoldingItems: number; strictToolsApplied: boolean } {
1094
1143
  const policy = resolveOpenAICompatPolicy(model, {
1095
1144
  endpoint: "responses",
1096
1145
  reasoning: options?.reasoning,
@@ -1244,6 +1293,7 @@ export function buildParams(
1244
1293
  : options?.reasoningSummary;
1245
1294
  applyResponsesCompatPolicy(params, reasoningPolicy, {
1246
1295
  reasoningSummary,
1296
+ forceReasoningOff: options?.forceReasoningOff,
1247
1297
  mapEffort: effort =>
1248
1298
  model.compat.reasoningEffortMap?.[effort as NonNullable<OpenAIResponsesOptions["reasoning"]>] ??
1249
1299
  model.thinking?.effortMap?.[effort as NonNullable<OpenAIResponsesOptions["reasoning"]>] ??
@@ -1253,7 +1303,7 @@ export function buildParams(
1253
1303
  // mode survives every policy branch (disabled/omitted effort included) while
1254
1304
  // keeping whatever effort/summary the policy produced — mode and effort are
1255
1305
  // independent wire fields.
1256
- if (model.reasoningMode) {
1306
+ if (model.reasoningMode && !options?.forceReasoningOff) {
1257
1307
  params.reasoning = { ...params.reasoning, mode: model.reasoningMode };
1258
1308
  }
1259
1309
 
@@ -1266,7 +1316,18 @@ export function buildParams(
1266
1316
  applyOpenAIExtraBody(params, options?.extraBody);
1267
1317
  applyOpenAIResponsesPromptCachePolicy(params, model, options, statefulCacheBaseline);
1268
1318
 
1269
- return { params, strictToolsApplied };
1319
+ let trailingScaffoldingItems = 0;
1320
+ if (options?.forceReasoningOff && isGpt56PlusResponsesModel(model)) {
1321
+ const effort = options.reasoning ?? "medium";
1322
+ const juice = getJuiceValue(effort);
1323
+ messages.push({
1324
+ role: "developer",
1325
+ content: [{ type: "input_text", text: `# Juice: ${juice} !important` }],
1326
+ });
1327
+ trailingScaffoldingItems = 1;
1328
+ }
1329
+
1330
+ return { params, trailingScaffoldingItems, strictToolsApplied };
1270
1331
  }
1271
1332
 
1272
1333
  /**
@@ -2428,6 +2428,20 @@ export function finalizeMessageText(item: ResponseOutputMessage, streamedText: s
2428
2428
  if (!item.content?.length) return streamedText || "";
2429
2429
  return item.content.map(part => (part.type === "output_text" ? (part.text ?? "") : (part.refusal ?? ""))).join("");
2430
2430
  }
2431
+ export const JUICE_EFFORT_MAP: Record<string, number> = {
2432
+ none: 0,
2433
+ minimal: 2,
2434
+ low: 4,
2435
+ medium: 8,
2436
+ high: 48,
2437
+ xhigh: 112,
2438
+ max: 960,
2439
+ };
2440
+
2441
+ export function getJuiceValue(effort?: string): number {
2442
+ if (!effort) return 8;
2443
+ return JUICE_EFFORT_MAP[effort] ?? 8;
2444
+ }
2431
2445
 
2432
2446
  export function accumulateToolCallArgumentsDelta(
2433
2447
  block: ResponsesToolCallBlock,
@@ -3308,6 +3322,14 @@ type ReasoningOptions = {
3308
3322
  export interface ApplyResponsesCompatPolicyOptions {
3309
3323
  reasoningSummary?: "auto" | "detailed" | "concise" | null;
3310
3324
  mapEffort?: (effort: string) => string;
3325
+ /**
3326
+ * Suppress native reasoning by sending `reasoning.effort: "none"` — the only
3327
+ * disable level the Responses API defines (`"off"` is not a wire value and
3328
+ * 400s everywhere). Gateways that reject `none` for a given model are
3329
+ * handled by the reasoning-effort fallback retry, which clamps to the
3330
+ * lowest level the error reports as allowed.
3331
+ */
3332
+ forceReasoningOff?: boolean;
3311
3333
  }
3312
3334
 
3313
3335
  export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreaming>(
@@ -3316,6 +3338,10 @@ export function applyResponsesCompatPolicy<P extends ResponseCreateParamsStreami
3316
3338
  options: ApplyResponsesCompatPolicyOptions | undefined,
3317
3339
  ): void {
3318
3340
  const reasoning = policy.reasoning;
3341
+ if (options?.forceReasoningOff) {
3342
+ params.reasoning = { effort: "none" } as P["reasoning"];
3343
+ return;
3344
+ }
3319
3345
  if (!reasoning.modelSupported) return;
3320
3346
  if (reasoning.includeEncryptedReasoning) {
3321
3347
  const include = params.include ?? [];
package/src/stream.ts CHANGED
@@ -1692,6 +1692,7 @@ function mapOptionsForApi<TApi extends Api>(
1692
1692
  openrouterVariant: options?.openrouterVariant,
1693
1693
  maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1694
1694
  disableReasoning: options?.disableReasoning,
1695
+ forceReasoningOff: options?.forceReasoningOff,
1695
1696
  textVerbosity: options?.textVerbosity,
1696
1697
  promptCache: options?.promptCache,
1697
1698
  statefulResponses: options?.statefulResponses,
@@ -1706,6 +1707,8 @@ function mapOptionsForApi<TApi extends Api>(
1706
1707
  reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1707
1708
  promptCache: options?.promptCache,
1708
1709
  statefulResponses: options?.statefulResponses,
1710
+ disableReasoning: options?.disableReasoning || options?.forceReasoningOff,
1711
+ forceReasoningOff: options?.forceReasoningOff,
1709
1712
  });
1710
1713
 
1711
1714
  case "openai-codex-responses":
@@ -1718,6 +1721,7 @@ function mapOptionsForApi<TApi extends Api>(
1718
1721
  codexCompaction: options?.codexCompaction,
1719
1722
  reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1720
1723
  textVerbosity: options?.textVerbosity,
1724
+ forceReasoningOff: options?.forceReasoningOff,
1721
1725
  });
1722
1726
 
1723
1727
  case "google-generative-ai": {
package/src/types.ts CHANGED
@@ -478,6 +478,11 @@ export interface StreamOptions {
478
478
  * `false` so `previous_response_id` cannot explain a result.
479
479
  */
480
480
  statefulResponses?: boolean;
481
+ /**
482
+ * Emit `reasoning: { effort: "none" }` for OpenAI Responses and Codex requests.
483
+ * Used when a caller supplies an external reasoning scratchpad; other transports ignore it.
484
+ */
485
+ forceReasoningOff?: boolean;
481
486
  /**
482
487
  * Provider-scoped mutable state store for this agent session.
483
488
  * Providers can use this to persist transport/session state between turns.