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

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,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.15] - 2026-08-12
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where AWS_BEDROCK_SKIP_AUTH failed to expose Amazon Bedrock models when AWS credential files were unavailable.
10
+ - Fixed an issue where forceReasoningOff was ignored by Anthropic and Google transports, which allowed native thinking alongside a caller-supplied external scratchpad.
11
+
12
+ ## [17.2.14] - 2026-08-11
13
+
14
+ ### Added
15
+
16
+ - Added `forceReasoningOff` and `disableReasoning` options to disable reasoning in OpenAI and Azure OpenAI models
17
+
5
18
  ## [17.2.13] - 2026-08-11
6
19
 
7
20
  ### 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
  /**
@@ -1,8 +1,7 @@
1
- import { resolveAwsRegistryApiKey } from "./aws.js";
2
1
  export declare const amazonBedrockProvider: {
3
2
  readonly id: "amazon-bedrock";
4
3
  readonly name: "Amazon Bedrock";
5
- readonly envKeys: typeof resolveAwsRegistryApiKey;
4
+ readonly envKeys: () => string | undefined;
6
5
  readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
7
6
  region: string | undefined;
8
7
  profile: string | undefined;
@@ -8,6 +8,8 @@ export interface AwsBedrockProviderOptions extends Readonly<Record<string, unkno
8
8
  }
9
9
  export declare function hasAwsCredentialSource(): boolean;
10
10
  /** Registry key marker for AWS transports that resolve their own bearer/IAM credentials. */
11
- export declare function resolveAwsRegistryApiKey(): string | undefined;
11
+ export declare function resolveAwsRegistryApiKey(options?: {
12
+ allowSkipAuth?: boolean;
13
+ }): string | undefined;
12
14
  /** Resolve a real AWS bearer token while filtering the registry's auth marker. */
13
15
  export declare function resolveAwsBearerToken(apiKey?: string, bearerToken?: string): string | undefined;
@@ -25,7 +25,7 @@ declare const ALL: ({
25
25
  } | {
26
26
  readonly id: "amazon-bedrock";
27
27
  readonly name: "Amazon Bedrock";
28
- readonly envKeys: typeof import("./aws.js").resolveAwsRegistryApiKey;
28
+ readonly envKeys: () => string | undefined;
29
29
  readonly mapSimpleOptions: (options: import("../index.js").SimpleStreamOptions) => {
30
30
  region: string | undefined;
31
31
  profile: string | undefined;
@@ -294,6 +294,12 @@ export interface StreamOptions {
294
294
  * `false` so `previous_response_id` cannot explain a result.
295
295
  */
296
296
  statefulResponses?: boolean;
297
+ /**
298
+ * Disable native reasoning when the caller supplies an external scratchpad.
299
+ * OpenAI Responses emits `reasoning: { effort: "none" }`; Anthropic and
300
+ * Google transports use their native thinking-off controls.
301
+ */
302
+ forceReasoningOff?: boolean;
297
303
  /**
298
304
  * Provider-scoped mutable state store for this agent session.
299
305
  * 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.15",
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.15",
42
+ "@oh-my-pi/pi-catalog": "17.2.15",
43
+ "@oh-my-pi/pi-utils": "17.2.15",
44
+ "@oh-my-pi/pi-wire": "17.2.15"
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 & {
@@ -0,0 +1 @@
1
+ TOOL-ONLY TURN. This turn accepts a tool call and nothing else; a text reply here is discarded unread and you will be re-prompted. Emit the tool call now.
@@ -36,6 +36,7 @@ import { armPreResponseTimeout, getStreamFirstEventTimeoutMs, iterateWithIdleTim
36
36
  // the stream provider trusts the access token threaded through `options.apiKey`.
37
37
  import { normalizeSchemaForCCA } from "../utils/schema";
38
38
  import { StreamMarkupHealing, type StreamMarkupHealingEvent } from "../utils/stream-markup-healing";
39
+ import forcedToolDirective from "./google-antigravity-forced-tool.md" with { type: "text" };
39
40
  import type { Content, FunctionCallingConfigMode, ThinkingConfig } from "./google-shared";
40
41
  import {
41
42
  convertMessages,
@@ -1347,6 +1348,13 @@ export function buildRequest(
1347
1348
  },
1348
1349
  };
1349
1350
  }
1351
+ // Cloud Code Assist drops `toolConfig` on Antigravity's Gemini routes:
1352
+ // the backend answers in text under `mode: "ANY"` and still emits calls
1353
+ // under `"NONE"`. Claude routes implement it, so only Gemini needs the
1354
+ // forced choice restated in the transcript.
1355
+ if (isAntigravity && !isClaudeModel(model.id) && request.toolConfig?.functionCallingConfig.mode === "ANY") {
1356
+ contents.push({ role: "user", parts: [{ text: forcedToolDirective }] });
1357
+ }
1350
1358
  }
1351
1359
  // Antigravity's default tool mode is VALIDATED (verified for Gemini and
1352
1360
  // Claude); an explicit non-auto tool choice above wins.
@@ -858,13 +858,18 @@ export function buildGoogleGenerateContentParams<T extends "google-generative-ai
858
858
  config.toolConfig = undefined;
859
859
  }
860
860
 
861
- if (options.thinking?.enabled && model.reasoning) {
862
- const cfg: ThinkingConfig = { includeThoughts: !options.hideThinkingSummary };
863
- if (options.thinking.level !== undefined) {
864
- // GoogleThinkingLevel mirrors the SDK's `ThinkingLevel` string enum values 1:1.
865
- cfg.thinkingLevel = options.thinking.level as ThinkingLevel;
866
- } else if (options.thinking.budgetTokens !== undefined) {
867
- cfg.thinkingBudget = options.thinking.budgetTokens;
861
+ const thinking = options.thinking;
862
+ if (
863
+ thinking &&
864
+ model.reasoning &&
865
+ (thinking.enabled || thinking.level !== undefined || thinking.budgetTokens !== undefined)
866
+ ) {
867
+ const cfg: ThinkingConfig = { includeThoughts: thinking.enabled && !options.hideThinkingSummary };
868
+ if (thinking.level !== undefined) {
869
+ // GoogleThinkingLevel mirrors the SDK's ThinkingLevel string enum values 1:1.
870
+ cfg.thinkingLevel = thinking.level as ThinkingLevel;
871
+ } else if (thinking.budgetTokens !== undefined) {
872
+ cfg.thinkingBudget = thinking.budgetTokens;
868
873
  }
869
874
  config.thinkingConfig = cfg;
870
875
  }
@@ -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 ?? [];
@@ -5,7 +5,7 @@ export const amazonBedrockProvider = {
5
5
  id: "amazon-bedrock",
6
6
  name: "Amazon Bedrock",
7
7
  // Amazon Bedrock accepts bearer tokens, IAM keys, profiles, ECS/IRSA credential chains.
8
- envKeys: resolveAwsRegistryApiKey,
8
+ envKeys: () => resolveAwsRegistryApiKey({ allowSkipAuth: true }),
9
9
  mapSimpleOptions: options => {
10
10
  const awsOptions = options.providerOptions as AwsBedrockProviderOptions | undefined;
11
11
  return {
@@ -1,5 +1,5 @@
1
1
  import * as fs from "node:fs";
2
- import { $env } from "@oh-my-pi/pi-utils";
2
+ import { $env, $flag } from "@oh-my-pi/pi-utils";
3
3
  import { hasConfiguredAwsProfile } from "../utils/aws-profile";
4
4
  import { AUTHENTICATED_SENTINEL } from "./types";
5
5
 
@@ -53,7 +53,8 @@ export function hasAwsCredentialSource(): boolean {
53
53
  }
54
54
 
55
55
  /** Registry key marker for AWS transports that resolve their own bearer/IAM credentials. */
56
- export function resolveAwsRegistryApiKey(): string | undefined {
56
+ export function resolveAwsRegistryApiKey(options?: { allowSkipAuth?: boolean }): string | undefined {
57
+ if (options?.allowSkipAuth && $flag("AWS_BEDROCK_SKIP_AUTH")) return AUTHENTICATED_SENTINEL;
57
58
  return hasAwsCredentialSource() ? AUTHENTICATED_SENTINEL : undefined;
58
59
  }
59
60
 
package/src/stream.ts CHANGED
@@ -1408,6 +1408,17 @@ function resolveOpenAiReasoningEffort<TApi extends Api>(
1408
1408
  return requireSupportedEffort(model, reasoning);
1409
1409
  }
1410
1410
 
1411
+ function resolveGoogleThinkingOff<TApi extends Api>(model: Model<TApi>): NonNullable<GoogleOptions["thinking"]> {
1412
+ const thinking: NonNullable<GoogleOptions["thinking"]> = { enabled: false };
1413
+ if (!model.reasoning || !model.thinking) return thinking;
1414
+ if (model.thinking.mode === "budget" && (!model.thinking.requiresEffort || model.thinking.suppressWhenOff)) {
1415
+ thinking.budgetTokens = 0;
1416
+ } else if (model.thinking.mode === "google-level" && model.thinking.suppressWhenOff) {
1417
+ thinking.level = "MINIMAL";
1418
+ }
1419
+ return thinking;
1420
+ }
1421
+
1411
1422
  const castApi = <TApi extends Api>(api: OptionsForApi<TApi>): OptionsForApi<Api> => api as OptionsForApi<Api>;
1412
1423
 
1413
1424
  /**
@@ -1428,13 +1439,13 @@ function normalizeMandatoryReasoningOptions<TApi extends Api>(
1428
1439
  !model.reasoning ||
1429
1440
  !model.thinking?.requiresEffort ||
1430
1441
  model.thinking.suppressWhenOff ||
1431
- (options?.reasoning !== undefined && !options.disableReasoning)
1442
+ (options?.reasoning !== undefined && !options.disableReasoning && !options.forceReasoningOff)
1432
1443
  ) {
1433
1444
  return options;
1434
1445
  }
1435
1446
  const floor = minimumSupportedEffort(model);
1436
1447
  if (floor === undefined) return options;
1437
- return { ...options, reasoning: floor, disableReasoning: undefined };
1448
+ return { ...options, reasoning: floor, disableReasoning: undefined, forceReasoningOff: undefined };
1438
1449
  }
1439
1450
 
1440
1451
  function supportsExplicitOpenAIResponsesPromptCache(compat: unknown): boolean {
@@ -1515,12 +1526,12 @@ function mapOptionsForApi<TApi extends Api>(
1515
1526
  switch (model.api) {
1516
1527
  case "anthropic-messages": {
1517
1528
  // Explicitly disable thinking when reasoning is not specified, the caller
1518
- // disabled it, or the model doesn't support it. `disableReasoning` is a
1519
- // SimpleStreamOptions flag that never reaches AnthropicOptions on its own,
1520
- // so it must be folded into `thinkingEnabled` here (mandatory-reasoning
1521
- // models already clamp it away in normalizeMandatoryReasoningOptions).
1529
+ // disabled it, an external scratchpad replaces it, or the model doesn't
1530
+ // support it. These SimpleStreamOptions flags never reach AnthropicOptions
1531
+ // on their own, so fold them into thinkingEnabled here (mandatory-reasoning
1532
+ // models already clamp them away in normalizeMandatoryReasoningOptions).
1522
1533
  const reasoning = options?.reasoning;
1523
- if (!reasoning || !model.reasoning || options?.disableReasoning) {
1534
+ if (!reasoning || !model.reasoning || options?.disableReasoning || options?.forceReasoningOff) {
1524
1535
  return castApi<"anthropic-messages">({
1525
1536
  ...base,
1526
1537
  requestModelId: resolveWireModelId(model, undefined),
@@ -1692,6 +1703,7 @@ function mapOptionsForApi<TApi extends Api>(
1692
1703
  openrouterVariant: options?.openrouterVariant,
1693
1704
  maxTokensExplicit: rawOptions?.maxTokens !== undefined,
1694
1705
  disableReasoning: options?.disableReasoning,
1706
+ forceReasoningOff: options?.forceReasoningOff,
1695
1707
  textVerbosity: options?.textVerbosity,
1696
1708
  promptCache: options?.promptCache,
1697
1709
  statefulResponses: options?.statefulResponses,
@@ -1706,6 +1718,8 @@ function mapOptionsForApi<TApi extends Api>(
1706
1718
  reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1707
1719
  promptCache: options?.promptCache,
1708
1720
  statefulResponses: options?.statefulResponses,
1721
+ disableReasoning: options?.disableReasoning || options?.forceReasoningOff,
1722
+ forceReasoningOff: options?.forceReasoningOff,
1709
1723
  });
1710
1724
 
1711
1725
  case "openai-codex-responses":
@@ -1718,17 +1732,18 @@ function mapOptionsForApi<TApi extends Api>(
1718
1732
  codexCompaction: options?.codexCompaction,
1719
1733
  reasoningSummary: options?.hideThinkingSummary ? null : undefined,
1720
1734
  textVerbosity: options?.textVerbosity,
1735
+ forceReasoningOff: options?.forceReasoningOff,
1721
1736
  });
1722
1737
 
1723
1738
  case "google-generative-ai": {
1724
- // Explicitly disable thinking when reasoning is not specified or model doesn't support it
1725
- // This is needed because Gemini has "dynamic thinking" enabled by default
1739
+ // Explicitly disable thinking when reasoning is absent, unsupported, or
1740
+ // replaced by the caller's external scratchpad. Gemini defaults thinking on.
1726
1741
  const reasoning = options?.reasoning;
1727
- if (!reasoning || !model.reasoning) {
1742
+ if (!reasoning || !model.reasoning || options?.disableReasoning || options?.forceReasoningOff) {
1728
1743
  return castApi<"google-generative-ai">({
1729
1744
  ...base,
1730
1745
  serviceTier: options?.serviceTier,
1731
- thinking: { enabled: false },
1746
+ thinking: resolveGoogleThinkingOff(model),
1732
1747
  toolChoice: mapGoogleToolChoice(options?.toolChoice),
1733
1748
  cachedContent: options?.cachedContent,
1734
1749
  });
@@ -1768,7 +1783,7 @@ function mapOptionsForApi<TApi extends Api>(
1768
1783
  case "google-gemini-cli": {
1769
1784
  const reasoning = options?.reasoning;
1770
1785
  const toolChoice = mapGoogleToolChoice(options?.toolChoice);
1771
- if (reasoning && model.reasoning) {
1786
+ if (reasoning && model.reasoning && !options?.disableReasoning && !options?.forceReasoningOff) {
1772
1787
  const effort = requireSupportedEffort(model, reasoning);
1773
1788
 
1774
1789
  // Gemini 3+ models use thinkingLevel instead of thinkingBudget
@@ -1827,13 +1842,14 @@ function mapOptionsForApi<TApi extends Api>(
1827
1842
  }
1828
1843
 
1829
1844
  case "google-vertex": {
1830
- // Explicitly disable thinking when reasoning is not specified or model doesn't support it
1845
+ // Explicitly disable thinking when reasoning is absent, unsupported, or
1846
+ // replaced by the caller's external scratchpad.
1831
1847
  const reasoning = options?.reasoning;
1832
- if (!reasoning || !model.reasoning) {
1848
+ if (!reasoning || !model.reasoning || options?.disableReasoning || options?.forceReasoningOff) {
1833
1849
  return castApi<"google-vertex">({
1834
1850
  ...base,
1835
1851
  serviceTier: options?.serviceTier,
1836
- thinking: { enabled: false },
1852
+ thinking: resolveGoogleThinkingOff(model),
1837
1853
  toolChoice: mapGoogleToolChoice(options?.toolChoice),
1838
1854
  cachedContent: options?.cachedContent,
1839
1855
  });
package/src/types.ts CHANGED
@@ -478,6 +478,12 @@ export interface StreamOptions {
478
478
  * `false` so `previous_response_id` cannot explain a result.
479
479
  */
480
480
  statefulResponses?: boolean;
481
+ /**
482
+ * Disable native reasoning when the caller supplies an external scratchpad.
483
+ * OpenAI Responses emits `reasoning: { effort: "none" }`; Anthropic and
484
+ * Google transports use their native thinking-off controls.
485
+ */
486
+ forceReasoningOff?: boolean;
481
487
  /**
482
488
  * Provider-scoped mutable state store for this agent session.
483
489
  * Providers can use this to persist transport/session state between turns.